From 27c1dc46463e1a6d86ef28dcd679766c8c9fcb2e Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Fri, 24 Jan 2025 18:46:17 -0500 Subject: [PATCH] Test ETH address/code OutInstructions --- processor/ethereum/router/src/lib.rs | 104 ++++++++---------- processor/ethereum/router/src/tests/mod.rs | 116 ++++++++++++++++++--- 2 files changed, 148 insertions(+), 72 deletions(-) diff --git a/processor/ethereum/router/src/lib.rs b/processor/ethereum/router/src/lib.rs index c17526c3..053aebce 100644 --- a/processor/ethereum/router/src/lib.rs +++ b/processor/ethereum/router/src/lib.rs @@ -68,14 +68,6 @@ use abi::{ #[cfg(test)] 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 { fn from(signature: &Signature) -> Self { Self { @@ -247,6 +239,7 @@ pub struct Router { } impl Router { // Gas allocated for ERC20 calls + #[cfg(test)] const GAS_FOR_ERC20_CALL: u64 = 100_000; /* @@ -262,7 +255,12 @@ impl Router { */ const CONFIRM_NEXT_SERAI_KEY_GAS: u64 = 57_736; const UPDATE_SERAI_KEY_GAS: u64 = 60_045; - const EXECUTE_BASE_GAS: u64 = 51_131; + const EXECUTE_ETH_BASE_GAS: u64 = 51_131; + const EXECUTE_ERC20_BASE_GAS: u64 = 149_831; + const EXECUTE_ETH_ADDRESS_OUT_INSTRUCTION_GAS: u64 = 41_453; + const EXECUTE_ETH_CODE_OUT_INSTRUCTION_GAS: u64 = 51_723; + const EXECUTE_ERC20_ADDRESS_OUT_INSTRUCTION_GAS: u64 = 0; // TODO + const EXECUTE_ERC20_CODE_OUT_INSTRUCTION_GAS: u64 = 0; // TODO const ESCAPE_HATCH_GAS: u64 = 61_238; /* @@ -432,51 +430,39 @@ impl Router { 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; + // 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 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)); + (size * NON_ZERO_BYTE_GAS_COST) + (size.div_ceil(32) * MEMORY_EXPANSION_COST); - 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 + match coin { + Coin::Ether => match instruction.destinationType { + // The calldata and memory cost is already part of this + abi::DestinationType::Address => Self::EXECUTE_ETH_ADDRESS_OUT_INSTRUCTION_GAS, + 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(); + Self::EXECUTE_ETH_CODE_OUT_INSTRUCTION_GAS + 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!(), - } + u64::from(code.gasLimit) } - }) + abi::DestinationType::__Invalid => unreachable!(), + }, + Coin::Erc20(_) => match instruction.destinationType { + abi::DestinationType::Address => Self::EXECUTE_ERC20_ADDRESS_OUT_INSTRUCTION_GAS, + abi::DestinationType::Code => { + let code = abi::CodeDestination::abi_decode(&instruction.destination, true).unwrap(); + Self::EXECUTE_ERC20_CODE_OUT_INSTRUCTION_GAS + + calldata_memory_cost + + u64::from(code.gasLimit) + } + abi::DestinationType::__Invalid => unreachable!(), + }, + } } /// The estimated gas cost for this OutInstruction. @@ -495,18 +481,16 @@ impl Router { /// 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::() + (match coin { + // This is warm as it's the message sender who is called with the fee payment + Coin::Ether => Self::EXECUTE_ETH_BASE_GAS, + // This is cold as we say the fee payment is the one warming the ERC20 + Coin::Erc20(_) => Self::EXECUTE_ERC20_BASE_GAS, + }) + outs + .0 + .iter() + .map(|out| Self::execute_out_instruction_gas_estimate_internal(coin, out)) + .sum::() } /// Construct a transaction to execute a batch of `OutInstruction`s. diff --git a/processor/ethereum/router/src/tests/mod.rs b/processor/ethereum/router/src/tests/mod.rs index 8d8930ab..77e8b50d 100644 --- a/processor/ethereum/router/src/tests/mod.rs +++ b/processor/ethereum/router/src/tests/mod.rs @@ -19,7 +19,7 @@ use alloy_node_bindings::{Anvil, AnvilInstance}; use scale::Encode; use serai_client::{ - networks::ethereum::Address as SeraiEthereumAddress, + networks::ethereum::{ContractDeployment, Address as SeraiEthereumAddress}, primitives::SeraiAddress, in_instructions::primitives::{ InInstruction as SeraiInInstruction, RefundableInInstruction, Shorthand, @@ -41,6 +41,8 @@ mod constants; mod erc20; use erc20::Erc20; +const CALL_GAS_STIPEND: u64 = 2_300; + pub(crate) fn test_key() -> (Scalar, PublicKey) { loop { let key = Scalar::random(&mut OsRng); @@ -348,7 +350,7 @@ impl Test { fee: U256, out_instructions: &[(SeraiEthereumAddress, U256)], results: Vec, - ) -> u64 { + ) -> (Signed, u64, 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); @@ -371,7 +373,7 @@ impl Test { 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) + (tx.clone(), receipt.gas_used, CalldataAgnosticGas::calculate(tx.tx(), receipt.gas_used)) } fn escape_hatch_tx(&self, escape_to: Address) -> TxLegacy { @@ -645,28 +647,118 @@ async fn test_empty_execute() { 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); + { + let (tx, raw_gas_used, gas_used) = test.execute(Coin::Ether, U256::from(1), &[], vec![]).await; + // We don't use the call gas stipend here + const UNUSED_GAS: u64 = CALL_GAS_STIPEND; + assert_eq!(gas_used + UNUSED_GAS, Router::EXECUTE_ETH_BASE_GAS); + + assert_eq!(test.provider.get_balance(test.router.address()).await.unwrap(), U256::from(0)); + let minted_to_sender = u128::from(tx.tx().gas_limit) * tx.tx().gas_price; + let spent_by_sender = u128::from(raw_gas_used) * tx.tx().gas_price; + assert_eq!( + test.provider.get_balance(tx.recover_signer().unwrap()).await.unwrap() - + U256::from(minted_to_sender - spent_by_sender), + U256::from(1) + ); + } + + { + // This uses a token of Address(0) as it'll be interpreted as a non-standard ERC20 which uses 0 + // gas, letting us safely evaluate the EXECUTE_ERC20_BASE_GAS constant + let (_tx, _raw_gas_used, gas_used) = + test.execute(Coin::Erc20(Address::ZERO), U256::from(1), &[], vec![]).await; + // Add an extra 1000 gas for decoding the return value which would exist if a compliant ERC20 + const UNUSED_GAS: u64 = Router::GAS_FOR_ERC20_CALL + 1000; + assert_eq!(gas_used + UNUSED_GAS, Router::EXECUTE_ERC20_BASE_GAS); + } } #[tokio::test] async fn test_eth_address_out_instruction() { - todo!("TODO") + let mut test = Test::new().await; + test.confirm_next_serai_key().await; + let () = + test.provider.raw_request("anvil_setBalance".into(), (test.router.address(), 3)).await.unwrap(); + + let mut rand_address = [0xff; 20]; + OsRng.fill_bytes(&mut rand_address); + let (tx, raw_gas_used, gas_used) = test + .execute( + Coin::Ether, + U256::from(1), + &[(SeraiEthereumAddress::Address(rand_address), U256::from(2))], + vec![true], + ) + .await; + // We don't use the call gas stipend here + const UNUSED_GAS: u64 = CALL_GAS_STIPEND; + // This doesn't model the quadratic memory costs + let gas_for_eth_address_out_instruction = gas_used + UNUSED_GAS - Router::EXECUTE_ETH_BASE_GAS; + // 2000 gas as a surplus for the quadratic memory cost and any inaccuracies + assert_eq!( + gas_for_eth_address_out_instruction + 2000, + Router::EXECUTE_ETH_ADDRESS_OUT_INSTRUCTION_GAS + ); + + assert_eq!(test.provider.get_balance(test.router.address()).await.unwrap(), U256::from(0)); + let minted_to_sender = u128::from(tx.tx().gas_limit) * tx.tx().gas_price; + let spent_by_sender = u128::from(raw_gas_used) * tx.tx().gas_price; + assert_eq!( + test.provider.get_balance(tx.recover_signer().unwrap()).await.unwrap() - + U256::from(minted_to_sender - spent_by_sender), + U256::from(1) + ); + assert_eq!(test.provider.get_balance(rand_address.into()).await.unwrap(), U256::from(2)); } #[tokio::test] async fn test_erc20_address_out_instruction() { todo!("TODO") + /* + assert_eq!(erc20.balance_of(&test, test.router.address()).await, U256::from(0)); + assert_eq!(erc20.balance_of(&test, test.state.escaped_to.unwrap()).await, amount); + */ } #[tokio::test] async fn test_eth_code_out_instruction() { - todo!("TODO") + let mut test = Test::new().await; + test.confirm_next_serai_key().await; + let () = + test.provider.raw_request("anvil_setBalance".into(), (test.router.address(), 3)).await.unwrap(); + + let mut rand_address = [0xff; 20]; + OsRng.fill_bytes(&mut rand_address); + let (tx, raw_gas_used, gas_used) = test + .execute( + Coin::Ether, + U256::from(1), + &[( + SeraiEthereumAddress::Contract(ContractDeployment::new(100_000, vec![]).unwrap()), + U256::from(2), + )], + vec![true], + ) + .await; + // This doesn't model the quadratic memory costs + let gas_for_eth_code_out_instruction = gas_used - Router::EXECUTE_ETH_BASE_GAS; + // 2000 gas as a surplus for the quadratic memory cost and any inaccuracies + assert_eq!(gas_for_eth_code_out_instruction + 2000, Router::EXECUTE_ETH_CODE_OUT_INSTRUCTION_GAS); + + assert_eq!(test.provider.get_balance(test.router.address()).await.unwrap(), U256::from(0)); + let minted_to_sender = u128::from(tx.tx().gas_limit) * tx.tx().gas_price; + let spent_by_sender = u128::from(raw_gas_used) * tx.tx().gas_price; + assert_eq!( + test.provider.get_balance(tx.recover_signer().unwrap()).await.unwrap() - + U256::from(minted_to_sender - spent_by_sender), + U256::from(1) + ); + assert_eq!( + test.provider.get_balance(test.router.address().create(1)).await.unwrap(), + U256::from(2) + ); } #[tokio::test] @@ -854,7 +946,7 @@ async fn test_eth_address_out_instruction() { let instructions = OutInstructions::from([].as_slice()); let receipt = publish_outs(&provider, &router, key, 2, Coin::Ether, fee, instructions).await; assert!(receipt.status()); - assert_eq!(Router::EXECUTE_BASE_GAS, ((receipt.gas_used + 1000) / 1000) * 1000); + assert_eq!(Router::EXECUTE_ETH_BASE_GAS, ((receipt.gas_used + 1000) / 1000) * 1000); assert_eq!(router.next_nonce(receipt.block_hash.unwrap().into()).await.unwrap(), 3); }