From bef90b2f1a76b41ca5f9e73734ece370ce517866 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Sat, 12 Apr 2025 08:32:11 -0400 Subject: [PATCH] Fix gas estimation discrepancy when gas isn't monotonic --- processor/ethereum/router/src/gas.rs | 62 +++++++++----- processor/ethereum/router/src/tests/mod.rs | 99 +++++++++++++++++----- 2 files changed, 120 insertions(+), 41 deletions(-) diff --git a/processor/ethereum/router/src/gas.rs b/processor/ethereum/router/src/gas.rs index 786e2bce..df5190d9 100644 --- a/processor/ethereum/router/src/gas.rs +++ b/processor/ethereum/router/src/gas.rs @@ -241,9 +241,12 @@ impl Router { } /// The worst-case gas cost for a legacy transaction which executes this batch. - /// - /// This assumes the fee will be non-zero. - pub fn execute_gas(&self, coin: Coin, fee_per_gas: U256, outs: &OutInstructions) -> u64 { + pub fn execute_gas_and_fee( + &self, + coin: Coin, + fee_per_gas: U256, + outs: &OutInstructions, + ) -> (u64, U256) { // Unfortunately, we can't cache this in self, despite the following code being written such // that a common EVM instance could be used, as revm's types aren't Send/Sync and we expect the // Router to be send/sync @@ -252,10 +255,11 @@ impl Router { Coin::Erc20(erc20) => Some(erc20), }); - let fee = match coin { + let shimmed_fee = match coin { Coin::Ether => { - // Use a fee of 1 so the fee payment is recognized as positive-value - let fee = U256::from(1); + // Use a fee of 1 so the fee payment is recognized as positive-value, if the fee is + // non-zero + let fee = if fee_per_gas == U256::ZERO { U256::ZERO } else { U256::ONE }; // Set a balance of the amount sent out to ensure we don't error on that premise gas_estimator.data.ctx.modify_db(|db| { @@ -274,7 +278,7 @@ impl Router { // Use a nonce of 1 ProjectivePoint::GENERATOR, &public_key, - &Self::execute_message(CHAIN_ID, 1, coin, fee, outs.clone()), + &Self::execute_message(CHAIN_ID, 1, coin, shimmed_fee, outs.clone()), ); let s = Scalar::ONE + (c * private_key); let sig = Signature::new(c, s).unwrap(); @@ -305,7 +309,7 @@ impl Router { tx.data = abi::executeCall::new(( abi::Signature::from(&sig), Address::from(coin), - fee, + shimmed_fee, outs.0.clone(), )) .abi_encode() @@ -330,8 +334,14 @@ impl Router { }; gas += gas_estimator.into_inspector().unused_gas; - // The transaction uses gas based on the amount of non-zero bytes in the calldata, which is - // variable to the fee, which is variable to the gas used. This iterates until parity + /* + The transaction pays an initial gas fee which is dependent on the length of the calldata and + the amount of non-zero bytes in the calldata. This is variable to the fee, which was prior + shimmed to be `1`. + + Here, we calculate the actual fee, and update the initial gas fee accordingly. We then update + the fee again, until the initial gas fee stops increasing. + */ let initial_gas = |fee, sig| { let gas = calculate_initial_tx_gas( SPEC_ID, @@ -344,26 +354,37 @@ impl Router { assert_eq!(gas.floor_gas, 0); gas.initial_gas }; - let mut current_initial_gas = initial_gas(fee, abi::Signature::from(&sig)); + let mut current_initial_gas = initial_gas(shimmed_fee, abi::Signature::from(&sig)); + // Remove the current initial gas from the transaction's gas + gas -= current_initial_gas; loop { - let fee = fee_per_gas * U256::from(gas); + // Calculate the would-be fee + let fee = fee_per_gas * U256::from(gas + current_initial_gas); + // Calculate the would-be gas for this fee let new_initial_gas = initial_gas(fee, abi::Signature { c: [0xff; 32].into(), s: [0xff; 32].into() }); + // If the values are equal, or if it went down, return + /* + The gas will decrease if the new fee has more zero bytes in its encoding. Further + iterations are unhelpful as they'll simply loop infinitely for some inputs. Accordingly, we + return the current fee (which is for a very slightly higher gas rate) with the decreased + gas to ensure this algorithm terminates. + */ if current_initial_gas >= new_initial_gas { - return gas; + return (gas + new_initial_gas, fee); } - - gas += new_initial_gas - current_initial_gas; + // Update what the current initial gas is current_initial_gas = new_initial_gas; } } - /// The estimated fee for this `OutInstruction`. + /// The estimated gas for this `OutInstruction`. /// /// This does not model the quadratic costs incurred when in a batch, nor other misc costs such /// as the potential to cause one less zero byte in the fee's encoding. This is intended to - /// produce a per-`OutInstruction` fee to deduct from each `OutInstruction`, before all - /// `OutInstruction`s incur an amortized fee of what remains for the batch itself. + /// produce a per-`OutInstruction` value which can be ratioed against others to decide the fee to + /// deduct from each `OutInstruction`, before all `OutInstruction`s incur an amortized fee of + /// what remains for the batch itself. pub fn execute_out_instruction_gas_estimate( &mut self, coin: Coin, @@ -372,11 +393,12 @@ impl Router { #[allow(clippy::map_entry)] // clippy doesn't realize the multiple mutable borrows if !self.empty_execute_gas.contains_key(&coin) { // This can't be de-duplicated across ERC20s due to the zero bytes in the address - let gas = self.execute_gas(coin, U256::from(0), &OutInstructions(vec![])); + let (gas, _fee) = self.execute_gas_and_fee(coin, U256::from(0), &OutInstructions(vec![])); self.empty_execute_gas.insert(coin, gas); } - let gas = self.execute_gas(coin, U256::from(0), &OutInstructions(vec![instruction])); + let (gas, _fee) = + self.execute_gas_and_fee(coin, U256::from(0), &OutInstructions(vec![instruction])); gas - self.empty_execute_gas[&coin] } } diff --git a/processor/ethereum/router/src/tests/mod.rs b/processor/ethereum/router/src/tests/mod.rs index 433f988f..191b4afd 100644 --- a/processor/ethereum/router/src/tests/mod.rs +++ b/processor/ethereum/router/src/tests/mod.rs @@ -540,8 +540,8 @@ async fn test_empty_execute() { test.confirm_next_serai_key().await; { - let gas = test.router.execute_gas(Coin::Ether, U256::from(1), &[].as_slice().into()); - let fee = U256::from(gas); + let (gas, fee) = + test.router.execute_gas_and_fee(Coin::Ether, U256::from(1), &[].as_slice().into()); let () = test .provider @@ -581,8 +581,8 @@ async fn test_empty_execute() { assert_eq!(test.provider.estimate_gas(call).await.unwrap(), 21_000 + 16); } - let gas = test.router.execute_gas(Coin::Erc20(token), U256::from(0), &[].as_slice().into()); - let fee = U256::from(0); + let (gas, fee) = + test.router.execute_gas_and_fee(Coin::Erc20(token), U256::from(0), &[].as_slice().into()); let (_tx, gas_used) = test.execute(Coin::Erc20(token), fee, [].as_slice().into(), vec![]).await; const UNUSED_GAS: u64 = Router::GAS_FOR_ERC20_CALL - 16; assert_eq!(gas_used + UNUSED_GAS, gas); @@ -600,8 +600,7 @@ async fn test_eth_address_out_instruction() { let out_instructions = OutInstructions::from([(SeraiEthereumAddress::Address(rand_address), amount_out)].as_slice()); - let gas = test.router.execute_gas(Coin::Ether, U256::from(1), &out_instructions); - let fee = U256::from(gas); + let (gas, fee) = test.router.execute_gas_and_fee(Coin::Ether, U256::from(1), &out_instructions); let () = test .provider @@ -638,8 +637,7 @@ async fn test_erc20_address_out_instruction() { let out_instructions = OutInstructions::from([(SeraiEthereumAddress::Address(rand_address), amount_out)].as_slice()); - let gas = test.router.execute_gas(coin, U256::from(1), &out_instructions); - let fee = U256::from(gas); + let (gas, fee) = test.router.execute_gas_and_fee(coin, U256::from(1), &out_instructions); // Mint to the Router the necessary amount of the ERC20 erc20.mint(&test, test.router.address(), amount_out + fee).await; @@ -674,8 +672,7 @@ async fn test_eth_code_out_instruction() { .as_slice(), ); - let gas = test.router.execute_gas(Coin::Ether, U256::from(1), &out_instructions); - let fee = U256::from(gas); + let (gas, fee) = test.router.execute_gas_and_fee(Coin::Ether, U256::from(1), &out_instructions); let (tx, gas_used) = test.execute(Coin::Ether, fee, out_instructions, vec![true]).await; // We use call-traces here to determine how much gas was allowed but unused due to the complexity @@ -721,8 +718,7 @@ async fn test_eth_code_out_instruction_reverts() { .as_slice(), ); - let gas = test.router.execute_gas(Coin::Ether, U256::from(1), &out_instructions); - let fee = U256::from(gas); + let (gas, fee) = test.router.execute_gas_and_fee(Coin::Ether, U256::from(1), &out_instructions); let (tx, gas_used) = test.execute(Coin::Ether, fee, out_instructions, vec![true]).await; let unused_gas = test.gas_unused_by_calls(&tx).await; @@ -744,8 +740,7 @@ async fn test_erc20_code_out_instruction() { .as_slice(), ); - let gas = test.router.execute_gas(coin, U256::from(1), &out_instructions); - let fee = U256::from(gas); + let (gas, fee) = test.router.execute_gas_and_fee(coin, U256::from(1), &out_instructions); // Mint to the Router the necessary amount of the ERC20 erc20.mint(&test, test.router.address(), amount_out + fee).await; @@ -777,11 +772,11 @@ async fn test_result_decoding() { .as_slice(), ); - let gas = test.router.execute_gas(Coin::Ether, U256::from(0), &out_instructions); + let (gas, fee) = test.router.execute_gas_and_fee(Coin::Ether, U256::from(0), &out_instructions); // We should decode these in the correct order (not `false, true, true`) let (_tx, gas_used) = - test.execute(Coin::Ether, U256::from(0), out_instructions, vec![true, true, false]).await; + test.execute(Coin::Ether, fee, out_instructions, vec![true, true, false]).await; // We don't check strict equality as we don't know how much gas was used by the reverted call // (even with the trace), solely that it used less than or equal to the limit assert!(gas_used <= gas); @@ -817,9 +812,8 @@ async fn test_reentrancy() { .as_slice(), ); - let gas = test.router.execute_gas(Coin::Ether, U256::from(0), &out_instructions); - let (_tx, gas_used) = - test.execute(Coin::Ether, U256::from(0), out_instructions, vec![true]).await; + let (gas, fee) = test.router.execute_gas_and_fee(Coin::Ether, U256::from(0), &out_instructions); + let (_tx, gas_used) = test.execute(Coin::Ether, fee, out_instructions, vec![true]).await; // Even though this doesn't have failed `OutInstruction`s, our logic is incomplete upon any // failed internal calls for some reason. That's fine, as the gas yielded is still the worst-case // (which this isn't a counter-example to) and is validated to be the worst-case, but is peculiar @@ -883,8 +877,7 @@ async fn fuzz_test_out_instructions_gas() { }; let fee_per_gas = U256::from(1) + U256::from(OsRng.next_u64() % 10); - let gas = test.router.execute_gas(coin, fee_per_gas, &out_instructions); - let fee = U256::from(gas) * fee_per_gas; + let (gas, fee) = test.router.execute_gas_and_fee(coin, fee_per_gas, &out_instructions); // All of these should have succeeded let (tx, gas_used) = test.execute(coin, fee, out_instructions.clone(), vec![true; out_instructions.0.len()]).await; @@ -896,3 +889,67 @@ async fn fuzz_test_out_instructions_gas() { ); } } + +#[tokio::test] +async fn test_gas_increases_then_decreases() { + /* + This specific batch of `OutInstruction`s causes the gas to be initially calculated, and then + increase as the proper fee is written in (due to the increased amount of non-zero bytes). But + then, as the fee is updated until the final fee no longer increases the gas used, the gas + actually goes *back down*. To then derive the fee from this reduced gas causes the gas to go + back up. + + A prior version of this library would return the reduced amount of gas fee in this edge case, + which only rarely appeared via the fuzz test (yet did once, yielding this). Then, it'd derive + the fee from it, and expect the realized transaction to have parity (causing a test failure as + it didn't). Now, `execute_gas` is `execute_gas_and_fee`, yielding both the gas which is + expected *and the fee for it*. This fee is guaranteed to cost the reported amount of gas, + resolving this issue. + */ + #[rustfmt::skip] + let out_instructions = vec![ + (SeraiEthereumAddress::Address(**Address::from([73, 151, 53, 42, 64, 102, 196, 80, 244, 167, 149, 81, 236, 231, 65, 18, 68, 196, 173, 20])), U256::from(1u8)), + (SeraiEthereumAddress::Address(**Address::from([7, 232, 97, 33, 54, 141, 246, 45, 29, 138, 221, 30, 2, 179, 142, 165, 169, 45, 143, 126])), U256::from(1u8)), + (SeraiEthereumAddress::Contract(ContractDeployment::new(100000, vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).unwrap()), U256::from(1u8)), + (SeraiEthereumAddress::Contract(ContractDeployment::new(100000, vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).unwrap()), U256::from(1u8)), + (SeraiEthereumAddress::Address(**Address::from([73, 151, 53, 42, 64, 102, 196, 80, 244, 167, 149, 81, 236, 231, 65, 18, 68, 196, 173, 20])), U256::from(0u8)), + (SeraiEthereumAddress::Contract(ContractDeployment::new(100000, vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).unwrap()), U256::from(1u8)), + (SeraiEthereumAddress::Address(**Address::from([34, 132, 167, 44, 12, 171, 57, 177, 197, 88, 60, 255, 68, 75, 2, 139, 76, 138, 78, 222])), U256::from(1u8)), + (SeraiEthereumAddress::Address(**Address::from([221, 218, 210, 119, 213, 189, 65, 118, 205, 113, 19, 11, 83, 58, 129, 203, 123, 76, 202, 99])), U256::from(0u8)), + (SeraiEthereumAddress::Contract(ContractDeployment::new(100000, vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).unwrap()), U256::from(0u8)), + (SeraiEthereumAddress::Address(**Address::from([34, 132, 167, 44, 12, 171, 57, 177, 197, 88, 60, 255, 68, 75, 2, 139, 76, 138, 78, 222])), U256::from(0u8)), + (SeraiEthereumAddress::Address(**Address::from([82, 31, 116, 111, 9, 110, 56, 51, 122, 38, 10, 227, 36, 134, 181, 185, 255, 149, 195, 254])), U256::from(1u8)), + (SeraiEthereumAddress::Address(**Address::from([36, 68, 76, 140, 254, 107, 233, 107, 186, 85, 5, 37, 65, 201, 63, 17, 135, 244, 148, 1])), U256::from(1u8)), + (SeraiEthereumAddress::Contract(ContractDeployment::new(100000, vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).unwrap()), U256::from(1u8)), + (SeraiEthereumAddress::Contract(ContractDeployment::new(100000, vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).unwrap()), U256::from(0u8)), + (SeraiEthereumAddress::Address(**Address::from([34, 132, 167, 44, 12, 171, 57, 177, 197, 88, 60, 255, 68, 75, 2, 139, 76, 138, 78, 222])), U256::from(1u8)), + (SeraiEthereumAddress::Address(**Address::from([26, 54, 156, 184, 48, 50, 23, 219, 43, 54, 56, 131, 245, 126, 70, 17, 235, 56, 130, 124])), U256::from(1u8)), + (SeraiEthereumAddress::Address(**Address::from([152, 27, 239, 11, 196, 99, 61, 136, 23, 8, 58, 242, 166, 235, 106, 167, 45, 175, 69, 247])), U256::from(0u8)), + (SeraiEthereumAddress::Address(**Address::from([152, 27, 239, 11, 196, 99, 61, 136, 23, 8, 58, 242, 166, 235, 106, 167, 45, 175, 69, 247])), U256::from(0u8)), + (SeraiEthereumAddress::Contract(ContractDeployment::new(100000, vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).unwrap()), U256::from(0u8)), + (SeraiEthereumAddress::Contract(ContractDeployment::new(100000, vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).unwrap()), U256::from(0u8)), + (SeraiEthereumAddress::Address(**Address::from([7, 232, 97, 33, 54, 141, 246, 45, 29, 138, 221, 30, 2, 179, 142, 165, 169, 45, 143, 126])), U256::from(0u8)), + (SeraiEthereumAddress::Contract(ContractDeployment::new(100000, vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).unwrap()), U256::from(0u8)), + (SeraiEthereumAddress::Contract(ContractDeployment::new(100000, vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).unwrap()), U256::from(0u8)), + (SeraiEthereumAddress::Contract(ContractDeployment::new(100000, vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).unwrap()), U256::from(1u8)) + ]; + + let mut test = Test::new().await; + test.confirm_next_serai_key().await; + + let out_instructions = OutInstructions::from(out_instructions.as_slice()); + + let coin = { + let erc20 = Erc20::deploy(&test).await; + erc20.mint(&test, test.router.address(), U256::from(1_000_000_000)).await; + Coin::Erc20(erc20.address()) + }; + + let fee_per_gas = U256::from(4); + let (gas, fee) = test.router.execute_gas_and_fee(coin, fee_per_gas, &out_instructions); + assert!((U256::from(gas) * fee_per_gas) != fee); + let (tx, gas_used) = + test.execute(coin, fee, out_instructions.clone(), vec![true; out_instructions.0.len()]).await; + let unused_gas = test.gas_unused_by_calls(&tx).await; + assert_eq!(gas_used + unused_gas, gas); +}