mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Test an empty execute
This commit is contained in:
@@ -68,6 +68,14 @@ use abi::{
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
// As per Dencun, used for estimating gas for determining relayer fees
|
||||||
|
const NON_ZERO_BYTE_GAS_COST: u64 = 16;
|
||||||
|
const MEMORY_EXPANSION_COST: u64 = 3; // Does not model the quadratic cost
|
||||||
|
const COLD_COST: u64 = 2_600;
|
||||||
|
const WARM_COST: u64 = 100;
|
||||||
|
const POSITIVE_VALUE_COST: u64 = 9_000;
|
||||||
|
const EMPTY_ACCOUNT_COST: u64 = 25_000;
|
||||||
|
|
||||||
impl From<&Signature> for abi::Signature {
|
impl From<&Signature> for abi::Signature {
|
||||||
fn from(signature: &Signature) -> Self {
|
fn from(signature: &Signature) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -134,20 +142,12 @@ pub struct InInstruction {
|
|||||||
pub data: Vec<u8>,
|
pub data: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A list of `OutInstruction`s.
|
impl From<&(SeraiAddress, U256)> for abi::OutInstruction {
|
||||||
#[derive(Clone)]
|
fn from((address, amount): &(SeraiAddress, U256)) -> Self {
|
||||||
pub struct OutInstructions(Vec<abi::OutInstruction>);
|
|
||||||
impl From<&[(SeraiAddress, U256)]> for OutInstructions {
|
|
||||||
fn from(outs: &[(SeraiAddress, U256)]) -> Self {
|
|
||||||
Self(
|
|
||||||
outs
|
|
||||||
.iter()
|
|
||||||
.map(|(address, amount)| {
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
let (destinationType, destination) = match address {
|
let (destinationType, destination) = match address {
|
||||||
SeraiAddress::Address(address) => {
|
SeraiAddress::Address(address) => {
|
||||||
// Per the documentation, `DestinationType::Address`'s value is an ABI-encoded
|
// Per the documentation, `DestinationType::Address`'s value is an ABI-encoded address
|
||||||
// address
|
|
||||||
(abi::DestinationType::Address, (Address::from(address)).abi_encode())
|
(abi::DestinationType::Address, (Address::from(address)).abi_encode())
|
||||||
}
|
}
|
||||||
SeraiAddress::Contract(contract) => (
|
SeraiAddress::Contract(contract) => (
|
||||||
@@ -160,9 +160,15 @@ impl From<&[(SeraiAddress, U256)]> for OutInstructions {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
abi::OutInstruction { destinationType, destination: destination.into(), amount: *amount }
|
abi::OutInstruction { destinationType, destination: destination.into(), amount: *amount }
|
||||||
})
|
}
|
||||||
.collect(),
|
}
|
||||||
)
|
|
||||||
|
/// A list of `OutInstruction`s.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct OutInstructions(Vec<abi::OutInstruction>);
|
||||||
|
impl From<&[(SeraiAddress, U256)]> for OutInstructions {
|
||||||
|
fn from(outs: &[(SeraiAddress, U256)]) -> Self {
|
||||||
|
Self(outs.iter().map(Into::into).collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +195,8 @@ pub enum Executed {
|
|||||||
nonce: u64,
|
nonce: u64,
|
||||||
/// The hash of the signed message for the Batch executed.
|
/// The hash of the signed message for the Batch executed.
|
||||||
message_hash: [u8; 32],
|
message_hash: [u8; 32],
|
||||||
|
/// The results of the `OutInstruction`s executed.
|
||||||
|
results: Vec<bool>,
|
||||||
},
|
},
|
||||||
/// The escape hatch was set.
|
/// The escape hatch was set.
|
||||||
EscapeHatch {
|
EscapeHatch {
|
||||||
@@ -238,12 +246,15 @@ pub struct Router {
|
|||||||
address: Address,
|
address: Address,
|
||||||
}
|
}
|
||||||
impl Router {
|
impl Router {
|
||||||
|
// Gas allocated for ERC20 calls
|
||||||
|
const GAS_FOR_ERC20_CALL: u64 = 100_000;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The gas limits to use for transactions.
|
The gas limits to use for transactions.
|
||||||
|
|
||||||
These are expected to be constant as a distributed group signs the transactions invoking these
|
These are expected to be constant as a distributed group may sign the transactions invoking
|
||||||
calls. Having the gas be constant prevents needing to run a protocol to determine what gas to
|
these calls. Having the gas be constant prevents needing to run a protocol to determine what
|
||||||
use.
|
gas to use.
|
||||||
|
|
||||||
These gas limits may break if/when gas opcodes undergo repricing. In that case, this library is
|
These gas limits may break if/when gas opcodes undergo repricing. In that case, this library is
|
||||||
expected to be modified with these made parameters. The caller would then be expected to pass
|
expected to be modified with these made parameters. The caller would then be expected to pass
|
||||||
@@ -251,9 +262,18 @@ impl Router {
|
|||||||
*/
|
*/
|
||||||
const CONFIRM_NEXT_SERAI_KEY_GAS: u64 = 57_736;
|
const CONFIRM_NEXT_SERAI_KEY_GAS: u64 = 57_736;
|
||||||
const UPDATE_SERAI_KEY_GAS: u64 = 60_045;
|
const UPDATE_SERAI_KEY_GAS: u64 = 60_045;
|
||||||
const EXECUTE_BASE_GAS: u64 = 48_000;
|
const EXECUTE_BASE_GAS: u64 = 51_131;
|
||||||
const ESCAPE_HATCH_GAS: u64 = 61_238;
|
const ESCAPE_HATCH_GAS: u64 = 61_238;
|
||||||
|
|
||||||
|
/*
|
||||||
|
The percentage to actually use as the gas limit, in case any opcodes are repriced or errors
|
||||||
|
occurred.
|
||||||
|
|
||||||
|
Per prior commentary, this is just intended to be best-effort. If this is unnecessary, the gas
|
||||||
|
will be unspent. If this becomes necessary, it avoids needing an update.
|
||||||
|
*/
|
||||||
|
const GAS_REPRICING_BUFFER: u64 = 120;
|
||||||
|
|
||||||
fn code() -> Vec<u8> {
|
fn code() -> Vec<u8> {
|
||||||
const BYTECODE: &[u8] = {
|
const BYTECODE: &[u8] = {
|
||||||
const BYTECODE_HEX: &[u8] =
|
const BYTECODE_HEX: &[u8] =
|
||||||
@@ -325,7 +345,7 @@ impl Router {
|
|||||||
TxLegacy {
|
TxLegacy {
|
||||||
to: TxKind::Call(self.address),
|
to: TxKind::Call(self.address),
|
||||||
input: abi::confirmNextSeraiKeyCall::new((abi::Signature::from(sig),)).abi_encode().into(),
|
input: abi::confirmNextSeraiKeyCall::new((abi::Signature::from(sig),)).abi_encode().into(),
|
||||||
gas_limit: Self::CONFIRM_NEXT_SERAI_KEY_GAS * 120 / 100,
|
gas_limit: Self::CONFIRM_NEXT_SERAI_KEY_GAS * Self::GAS_REPRICING_BUFFER / 100,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -351,7 +371,7 @@ impl Router {
|
|||||||
))
|
))
|
||||||
.abi_encode()
|
.abi_encode()
|
||||||
.into(),
|
.into(),
|
||||||
gas_limit: Self::UPDATE_SERAI_KEY_GAS * 120 / 100,
|
gas_limit: Self::UPDATE_SERAI_KEY_GAS * Self::GAS_REPRICING_BUFFER / 100,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -404,18 +424,103 @@ impl Router {
|
|||||||
.abi_encode()
|
.abi_encode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The estimated gas cost for this OutInstruction.
|
||||||
|
///
|
||||||
|
/// This is not guaranteed to be correct or even sufficient. It is a hint and a hint alone used
|
||||||
|
/// for determining relayer fees.
|
||||||
|
fn execute_out_instruction_gas_estimate_internal(
|
||||||
|
coin: Coin,
|
||||||
|
instruction: &abi::OutInstruction,
|
||||||
|
) -> u64 {
|
||||||
|
// The assigned cost for performing an additional iteration of the loop
|
||||||
|
const ITERATION_COST: u64 = 5_000;
|
||||||
|
// The additional cost for a `DestinationType.Code`, as an additional buffer for its complexity
|
||||||
|
const CODE_COST: u64 = 10_000;
|
||||||
|
|
||||||
|
let size = u64::try_from(instruction.abi_encoded_size()).unwrap();
|
||||||
|
let calldata_memory_cost =
|
||||||
|
(NON_ZERO_BYTE_GAS_COST * size) + (MEMORY_EXPANSION_COST * size.div_ceil(32));
|
||||||
|
|
||||||
|
ITERATION_COST +
|
||||||
|
(match coin {
|
||||||
|
Coin::Ether => match instruction.destinationType {
|
||||||
|
// We assume we're tranferring a positive value to a cold, empty account
|
||||||
|
abi::DestinationType::Address => {
|
||||||
|
calldata_memory_cost + COLD_COST + POSITIVE_VALUE_COST + EMPTY_ACCOUNT_COST
|
||||||
|
}
|
||||||
|
abi::DestinationType::Code => {
|
||||||
|
// OutInstructions can't be encoded/decoded and doesn't have pub internals, enabling it
|
||||||
|
// to be correct by construction
|
||||||
|
let code = abi::CodeDestination::abi_decode(&instruction.destination, true).unwrap();
|
||||||
|
// This performs a call to self with the value, incurring the positive-value cost before
|
||||||
|
// CREATE's
|
||||||
|
calldata_memory_cost +
|
||||||
|
CODE_COST +
|
||||||
|
(WARM_COST + POSITIVE_VALUE_COST + u64::from(code.gasLimit))
|
||||||
|
}
|
||||||
|
abi::DestinationType::__Invalid => unreachable!(),
|
||||||
|
},
|
||||||
|
Coin::Erc20(_) => {
|
||||||
|
// The ERC20 is warmed by the fee payment to the relayer
|
||||||
|
let erc20_call_gas = WARM_COST + Self::GAS_FOR_ERC20_CALL;
|
||||||
|
match instruction.destinationType {
|
||||||
|
abi::DestinationType::Address => calldata_memory_cost + erc20_call_gas,
|
||||||
|
abi::DestinationType::Code => {
|
||||||
|
let code = abi::CodeDestination::abi_decode(&instruction.destination, true).unwrap();
|
||||||
|
calldata_memory_cost +
|
||||||
|
CODE_COST +
|
||||||
|
erc20_call_gas +
|
||||||
|
// Call to self to deploy the contract
|
||||||
|
(WARM_COST + u64::from(code.gasLimit))
|
||||||
|
}
|
||||||
|
abi::DestinationType::__Invalid => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The estimated gas cost for this OutInstruction.
|
||||||
|
///
|
||||||
|
/// This is not guaranteed to be correct or even sufficient. It is a hint and a hint alone used
|
||||||
|
/// for determining relayer fees.
|
||||||
|
pub fn execute_out_instruction_gas_estimate(coin: Coin, address: SeraiAddress) -> u64 {
|
||||||
|
Self::execute_out_instruction_gas_estimate_internal(
|
||||||
|
coin,
|
||||||
|
&abi::OutInstruction::from(&(address, U256::ZERO)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The estimated gas cost for this batch.
|
||||||
|
///
|
||||||
|
/// This is not guaranteed to be correct or even sufficient. It is a hint and a hint alone used
|
||||||
|
/// for determining relayer fees.
|
||||||
|
pub fn execute_gas_estimate(coin: Coin, outs: &OutInstructions) -> u64 {
|
||||||
|
Self::EXECUTE_BASE_GAS +
|
||||||
|
(match coin {
|
||||||
|
// This is warm as it's the message sender who is called with the fee payment
|
||||||
|
Coin::Ether => WARM_COST + POSITIVE_VALUE_COST,
|
||||||
|
// This is cold as we say the fee payment is the one warming the ERC20
|
||||||
|
Coin::Erc20(_) => COLD_COST + Self::GAS_FOR_ERC20_CALL,
|
||||||
|
}) +
|
||||||
|
outs
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|out| Self::execute_out_instruction_gas_estimate_internal(coin, out))
|
||||||
|
.sum::<u64>()
|
||||||
|
}
|
||||||
|
|
||||||
/// Construct a transaction to execute a batch of `OutInstruction`s.
|
/// Construct a transaction to execute a batch of `OutInstruction`s.
|
||||||
///
|
///
|
||||||
/// The gas limit and gas price are not set and are left to the caller.
|
/// The gas limit is set to an estimate which may or may not be sufficient. The caller is
|
||||||
|
/// expected to set a correct gas limit. The gas price is not set and is left to the caller.
|
||||||
pub fn execute(&self, coin: Coin, fee: U256, outs: OutInstructions, sig: &Signature) -> TxLegacy {
|
pub fn execute(&self, coin: Coin, fee: U256, outs: OutInstructions, sig: &Signature) -> TxLegacy {
|
||||||
// TODO
|
let gas = Self::execute_gas_estimate(coin, &outs);
|
||||||
let gas_limit = Self::EXECUTE_BASE_GAS + outs.0.iter().map(|_| 200_000 + 10_000).sum::<u64>();
|
|
||||||
TxLegacy {
|
TxLegacy {
|
||||||
to: TxKind::Call(self.address),
|
to: TxKind::Call(self.address),
|
||||||
input: abi::executeCall::new((abi::Signature::from(sig), Address::from(coin), fee, outs.0))
|
input: abi::executeCall::new((abi::Signature::from(sig), Address::from(coin), fee, outs.0))
|
||||||
.abi_encode()
|
.abi_encode()
|
||||||
.into(),
|
.into(),
|
||||||
gas_limit: gas_limit * 120 / 100,
|
gas_limit: gas * Self::GAS_REPRICING_BUFFER / 100,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -436,7 +541,7 @@ impl Router {
|
|||||||
TxLegacy {
|
TxLegacy {
|
||||||
to: TxKind::Call(self.address),
|
to: TxKind::Call(self.address),
|
||||||
input: abi::escapeHatchCall::new((abi::Signature::from(sig), escape_to)).abi_encode().into(),
|
input: abi::escapeHatchCall::new((abi::Signature::from(sig), escape_to)).abi_encode().into(),
|
||||||
gas_limit: Self::ESCAPE_HATCH_GAS * 120 / 100,
|
gas_limit: Self::ESCAPE_HATCH_GAS * Self::GAS_REPRICING_BUFFER / 100,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -679,6 +784,24 @@ impl Router {
|
|||||||
TransportErrorKind::Custom(format!("failed to convert nonce to u64: {e:?}").into())
|
TransportErrorKind::Custom(format!("failed to convert nonce to u64: {e:?}").into())
|
||||||
})?,
|
})?,
|
||||||
message_hash: event.messageHash.into(),
|
message_hash: event.messageHash.into(),
|
||||||
|
results: {
|
||||||
|
let results_len = usize::try_from(event.resultsLength).map_err(|e| {
|
||||||
|
TransportErrorKind::Custom(
|
||||||
|
format!("failed to convert resultsLength to usize: {e:?}").into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if results_len.div_ceil(8) != event.results.len() {
|
||||||
|
Err(TransportErrorKind::Custom(
|
||||||
|
"resultsLength didn't align with results length".to_string().into(),
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
let mut results = Vec::with_capacity(results_len);
|
||||||
|
for b in 0 .. results_len {
|
||||||
|
let byte = event.results[b / 8];
|
||||||
|
results.push(((byte >> (b % 8)) & 1) == 1);
|
||||||
|
}
|
||||||
|
results
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Some(&EscapeHatchEvent::SIGNATURE_HASH) => {
|
Some(&EscapeHatchEvent::SIGNATURE_HASH) => {
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ pub struct Erc20(Address);
|
|||||||
impl Erc20 {
|
impl Erc20 {
|
||||||
pub(crate) async fn deploy(test: &Test) -> Self {
|
pub(crate) async fn deploy(test: &Test) -> Self {
|
||||||
const BYTECODE: &[u8] = {
|
const BYTECODE: &[u8] = {
|
||||||
const BYTECODE_HEX: &[u8] =
|
const BYTECODE_HEX: &[u8] = include_bytes!(concat!(
|
||||||
include_bytes!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-router/TestERC20.bin"));
|
env!("OUT_DIR"),
|
||||||
|
"/serai-processor-ethereum-router/tests/TestERC20.bin"
|
||||||
|
));
|
||||||
const BYTECODE: [u8; BYTECODE_HEX.len() / 2] =
|
const BYTECODE: [u8; BYTECODE_HEX.len() / 2] =
|
||||||
match hex::const_decode_to_array::<{ BYTECODE_HEX.len() / 2 }>(BYTECODE_HEX) {
|
match hex::const_decode_to_array::<{ BYTECODE_HEX.len() / 2 }>(BYTECODE_HEX) {
|
||||||
Ok(bytecode) => bytecode,
|
Ok(bytecode) => bytecode,
|
||||||
|
|||||||
@@ -322,7 +322,7 @@ impl Test {
|
|||||||
coin: Coin,
|
coin: Coin,
|
||||||
fee: U256,
|
fee: U256,
|
||||||
out_instructions: &[(SeraiEthereumAddress, U256)],
|
out_instructions: &[(SeraiEthereumAddress, U256)],
|
||||||
) -> TxLegacy {
|
) -> ([u8; 32], TxLegacy) {
|
||||||
let out_instructions = OutInstructions::from(out_instructions);
|
let out_instructions = OutInstructions::from(out_instructions);
|
||||||
let msg = Router::execute_message(
|
let msg = Router::execute_message(
|
||||||
self.chain_id,
|
self.chain_id,
|
||||||
@@ -331,8 +331,47 @@ impl Test {
|
|||||||
fee,
|
fee,
|
||||||
out_instructions.clone(),
|
out_instructions.clone(),
|
||||||
);
|
);
|
||||||
|
let msg_hash = ethereum_primitives::keccak256(&msg);
|
||||||
let sig = sign(self.state.key.unwrap(), &msg);
|
let sig = sign(self.state.key.unwrap(), &msg);
|
||||||
self.router.execute(coin, fee, out_instructions, &sig)
|
|
||||||
|
let mut tx = self.router.execute(coin, fee, out_instructions, &sig);
|
||||||
|
// Restore the original estimate as the gas limit to ensure it's sufficient, at least in our
|
||||||
|
// test cases
|
||||||
|
tx.gas_limit = (tx.gas_limit * 100) / Router::GAS_REPRICING_BUFFER;
|
||||||
|
|
||||||
|
(msg_hash, tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&mut self,
|
||||||
|
coin: Coin,
|
||||||
|
fee: U256,
|
||||||
|
out_instructions: &[(SeraiEthereumAddress, U256)],
|
||||||
|
results: Vec<bool>,
|
||||||
|
) -> u64 {
|
||||||
|
let (message_hash, mut tx) = self.execute_tx(coin, fee, out_instructions);
|
||||||
|
tx.gas_price = 100_000_000_000;
|
||||||
|
let tx = ethereum_primitives::deterministically_sign(tx);
|
||||||
|
let receipt = ethereum_test_primitives::publish_tx(&self.provider, tx.clone()).await;
|
||||||
|
assert!(receipt.status());
|
||||||
|
// We don't check the gas for `execute` as it's infeasible. Due to our use of account
|
||||||
|
// abstraction, it isn't a critical if we do ever under-estimate, solely an unprofitable relay
|
||||||
|
|
||||||
|
{
|
||||||
|
let block = receipt.block_number.unwrap();
|
||||||
|
let executed = self.router.executed(block ..= block).await.unwrap();
|
||||||
|
assert_eq!(executed.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
executed[0],
|
||||||
|
Executed::Batch { nonce: self.state.next_nonce, message_hash, results }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state.next_nonce += 1;
|
||||||
|
self.verify_state().await;
|
||||||
|
|
||||||
|
// We do return the gas used in case a caller can benefit from it
|
||||||
|
CalldataAgnosticGas::calculate(tx.tx(), receipt.gas_used)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn escape_hatch_tx(&self, escape_to: Address) -> TxLegacy {
|
fn escape_hatch_tx(&self, escape_to: Address) -> TxLegacy {
|
||||||
@@ -402,7 +441,7 @@ async fn test_no_serai_key() {
|
|||||||
IRouterErrors::SeraiKeyWasNone(IRouter::SeraiKeyWasNone {})
|
IRouterErrors::SeraiKeyWasNone(IRouter::SeraiKeyWasNone {})
|
||||||
));
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
test.call_and_decode_err(test.execute_tx(Coin::Ether, U256::from(0), &[])).await,
|
test.call_and_decode_err(test.execute_tx(Coin::Ether, U256::from(0), &[]).1).await,
|
||||||
IRouterErrors::SeraiKeyWasNone(IRouter::SeraiKeyWasNone {})
|
IRouterErrors::SeraiKeyWasNone(IRouter::SeraiKeyWasNone {})
|
||||||
));
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
@@ -555,7 +594,7 @@ async fn test_erc20_router_in_instruction() {
|
|||||||
let tx = TxLegacy {
|
let tx = TxLegacy {
|
||||||
chain_id: None,
|
chain_id: None,
|
||||||
nonce: 0,
|
nonce: 0,
|
||||||
gas_price: 100_000_000_000u128,
|
gas_price: 100_000_000_000,
|
||||||
gas_limit: 1_000_000,
|
gas_limit: 1_000_000,
|
||||||
to: test.router.address().into(),
|
to: test.router.address().into(),
|
||||||
value: U256::ZERO,
|
value: U256::ZERO,
|
||||||
@@ -592,7 +631,7 @@ async fn test_erc20_top_level_transfer_in_instruction() {
|
|||||||
let shorthand = Test::in_instruction();
|
let shorthand = Test::in_instruction();
|
||||||
|
|
||||||
let mut tx = test.router.in_instruction(coin, amount, &shorthand);
|
let mut tx = test.router.in_instruction(coin, amount, &shorthand);
|
||||||
tx.gas_price = 100_000_000_000u128;
|
tx.gas_price = 100_000_000_000;
|
||||||
tx.gas_limit = 1_000_000;
|
tx.gas_limit = 1_000_000;
|
||||||
|
|
||||||
let tx = ethereum_primitives::deterministically_sign(tx);
|
let tx = ethereum_primitives::deterministically_sign(tx);
|
||||||
@@ -600,6 +639,21 @@ async fn test_erc20_top_level_transfer_in_instruction() {
|
|||||||
test.publish_in_instruction_tx(tx, coin, amount, &shorthand).await;
|
test.publish_in_instruction_tx(tx, coin, amount, &shorthand).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_empty_execute() {
|
||||||
|
let mut test = Test::new().await;
|
||||||
|
test.confirm_next_serai_key().await;
|
||||||
|
let () =
|
||||||
|
test.provider.raw_request("anvil_setBalance".into(), (test.router.address(), 1)).await.unwrap();
|
||||||
|
let gas_used = test.execute(Coin::Ether, U256::from(1), &[], vec![]).await;
|
||||||
|
|
||||||
|
// For the empty ETH case, we do compare this cost to the base cost
|
||||||
|
const CALL_GAS_STIPEND: u64 = 2_300;
|
||||||
|
// We don't use the call gas stipend here
|
||||||
|
const UNUSED_GAS: u64 = CALL_GAS_STIPEND;
|
||||||
|
assert_eq!(gas_used + UNUSED_GAS, Router::EXECUTE_BASE_GAS);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_eth_address_out_instruction() {
|
async fn test_eth_address_out_instruction() {
|
||||||
todo!("TODO")
|
todo!("TODO")
|
||||||
@@ -643,7 +697,7 @@ async fn test_escape_hatch() {
|
|||||||
let tx = ethereum_primitives::deterministically_sign(TxLegacy {
|
let tx = ethereum_primitives::deterministically_sign(TxLegacy {
|
||||||
to: Address([1; 20].into()).into(),
|
to: Address([1; 20].into()).into(),
|
||||||
gas_limit: 21_000,
|
gas_limit: 21_000,
|
||||||
gas_price: 100_000_000_000u128,
|
gas_price: 100_000_000_000,
|
||||||
value: U256::from(1),
|
value: U256::from(1),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
@@ -679,7 +733,7 @@ async fn test_escape_hatch() {
|
|||||||
IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
|
IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
|
||||||
));
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
test.call_and_decode_err(test.execute_tx(Coin::Ether, U256::from(0), &[])).await,
|
test.call_and_decode_err(test.execute_tx(Coin::Ether, U256::from(0), &[]).1).await,
|
||||||
IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
|
IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
|
||||||
));
|
));
|
||||||
// We reject further attempts to update the escape hatch to prevent the last key from being
|
// We reject further attempts to update the escape hatch to prevent the last key from being
|
||||||
|
|||||||
@@ -97,12 +97,19 @@ impl primitives::Block for FullEpoch {
|
|||||||
> {
|
> {
|
||||||
let mut res = HashMap::new();
|
let mut res = HashMap::new();
|
||||||
for executed in &self.executed {
|
for executed in &self.executed {
|
||||||
let Some(expected) =
|
let Some(mut expected) =
|
||||||
eventualities.active_eventualities.remove(executed.nonce().to_le_bytes().as_slice())
|
eventualities.active_eventualities.remove(executed.nonce().to_le_bytes().as_slice())
|
||||||
else {
|
else {
|
||||||
// TODO: Why is this a continue, not an assert?
|
// TODO: Why is this a continue, not an assert?
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
// If this is a Batch Eventuality, we didn't know how the OutInstructions would resolve at
|
||||||
|
// time of creation. Copy the results from the actual transaction into the expectation
|
||||||
|
if let (Executed::Batch { results, .. }, Executed::Batch { results: expected_results, .. }) =
|
||||||
|
(executed, &mut expected.0)
|
||||||
|
{
|
||||||
|
*expected_results = results.clone();
|
||||||
|
}
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
executed,
|
executed,
|
||||||
&expected.0,
|
&expected.0,
|
||||||
@@ -119,7 +126,7 @@ impl primitives::Block for FullEpoch {
|
|||||||
Accordingly, we have free reign as to what to set the transaction ID to.
|
Accordingly, we have free reign as to what to set the transaction ID to.
|
||||||
|
|
||||||
We set the ID to the nonce as it's the most helpful value and unique barring someone
|
We set the ID to the nonce as it's the most helpful value and unique barring someone
|
||||||
finding the premise for this as a hash.
|
finding the preimage for this as a hash.
|
||||||
*/
|
*/
|
||||||
let mut tx_id = [0; 32];
|
let mut tx_id = [0; 32];
|
||||||
tx_id[.. 8].copy_from_slice(executed.nonce().to_le_bytes().as_slice());
|
tx_id[.. 8].copy_from_slice(executed.nonce().to_le_bytes().as_slice());
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ impl Action {
|
|||||||
Executed::NextSeraiKeySet { nonce: *nonce, key: key.eth_repr() }
|
Executed::NextSeraiKeySet { nonce: *nonce, key: key.eth_repr() }
|
||||||
}
|
}
|
||||||
Self::Batch { chain_id: _, nonce, .. } => {
|
Self::Batch { chain_id: _, nonce, .. } => {
|
||||||
Executed::Batch { nonce: *nonce, message_hash: keccak256(self.message()) }
|
Executed::Batch { nonce: *nonce, message_hash: keccak256(self.message()), results: vec![] }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ pub const ADDRESS_GAS_LIMIT: u32 = 950_000;
|
|||||||
pub struct ContractDeployment {
|
pub struct ContractDeployment {
|
||||||
/// The gas limit to use for this contract's execution.
|
/// The gas limit to use for this contract's execution.
|
||||||
///
|
///
|
||||||
/// THis MUST be less than the Serai gas limit. The cost of it will be deducted from the amount
|
/// This MUST be less than the Serai gas limit. The cost of it, and the associated costs with
|
||||||
/// transferred.
|
/// making this transaction, will be deducted from the amount transferred.
|
||||||
gas_limit: u32,
|
gas_limit: u32,
|
||||||
/// The initialization code of the contract to deploy.
|
/// The initialization code of the contract to deploy.
|
||||||
///
|
///
|
||||||
|
|||||||
Reference in New Issue
Block a user