Fix gas estimation discrepancy when gas isn't monotonic

This commit is contained in:
Luke Parker
2025-04-12 08:32:11 -04:00
parent 184c02714a
commit bef90b2f1a
2 changed files with 120 additions and 41 deletions

View File

@@ -241,9 +241,12 @@ impl Router {
} }
/// The worst-case gas cost for a legacy transaction which executes this batch. /// The worst-case gas cost for a legacy transaction which executes this batch.
/// pub fn execute_gas_and_fee(
/// This assumes the fee will be non-zero. &self,
pub fn execute_gas(&self, coin: Coin, fee_per_gas: U256, outs: &OutInstructions) -> u64 { 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 // 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 // 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 // Router to be send/sync
@@ -252,10 +255,11 @@ impl Router {
Coin::Erc20(erc20) => Some(erc20), Coin::Erc20(erc20) => Some(erc20),
}); });
let fee = match coin { let shimmed_fee = match coin {
Coin::Ether => { Coin::Ether => {
// Use a fee of 1 so the fee payment is recognized as positive-value // Use a fee of 1 so the fee payment is recognized as positive-value, if the fee is
let fee = U256::from(1); // 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 // Set a balance of the amount sent out to ensure we don't error on that premise
gas_estimator.data.ctx.modify_db(|db| { gas_estimator.data.ctx.modify_db(|db| {
@@ -274,7 +278,7 @@ impl Router {
// Use a nonce of 1 // Use a nonce of 1
ProjectivePoint::GENERATOR, ProjectivePoint::GENERATOR,
&public_key, &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 s = Scalar::ONE + (c * private_key);
let sig = Signature::new(c, s).unwrap(); let sig = Signature::new(c, s).unwrap();
@@ -305,7 +309,7 @@ impl Router {
tx.data = abi::executeCall::new(( tx.data = abi::executeCall::new((
abi::Signature::from(&sig), abi::Signature::from(&sig),
Address::from(coin), Address::from(coin),
fee, shimmed_fee,
outs.0.clone(), outs.0.clone(),
)) ))
.abi_encode() .abi_encode()
@@ -330,8 +334,14 @@ impl Router {
}; };
gas += gas_estimator.into_inspector().unused_gas; 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 initial_gas = |fee, sig| {
let gas = calculate_initial_tx_gas( let gas = calculate_initial_tx_gas(
SPEC_ID, SPEC_ID,
@@ -344,26 +354,37 @@ impl Router {
assert_eq!(gas.floor_gas, 0); assert_eq!(gas.floor_gas, 0);
gas.initial_gas 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 { 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 = let new_initial_gas =
initial_gas(fee, abi::Signature { c: [0xff; 32].into(), s: [0xff; 32].into() }); 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 { if current_initial_gas >= new_initial_gas {
return gas; return (gas + new_initial_gas, fee);
} }
// Update what the current initial gas is
gas += new_initial_gas - current_initial_gas;
current_initial_gas = new_initial_gas; 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 /// 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 /// 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 /// produce a per-`OutInstruction` value which can be ratioed against others to decide the fee to
/// `OutInstruction`s incur an amortized fee of what remains for the batch itself. /// 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( pub fn execute_out_instruction_gas_estimate(
&mut self, &mut self,
coin: Coin, coin: Coin,
@@ -372,11 +393,12 @@ impl Router {
#[allow(clippy::map_entry)] // clippy doesn't realize the multiple mutable borrows #[allow(clippy::map_entry)] // clippy doesn't realize the multiple mutable borrows
if !self.empty_execute_gas.contains_key(&coin) { if !self.empty_execute_gas.contains_key(&coin) {
// This can't be de-duplicated across ERC20s due to the zero bytes in the address // 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); 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] gas - self.empty_execute_gas[&coin]
} }
} }

View File

@@ -540,8 +540,8 @@ async fn test_empty_execute() {
test.confirm_next_serai_key().await; test.confirm_next_serai_key().await;
{ {
let gas = test.router.execute_gas(Coin::Ether, U256::from(1), &[].as_slice().into()); let (gas, fee) =
let fee = U256::from(gas); test.router.execute_gas_and_fee(Coin::Ether, U256::from(1), &[].as_slice().into());
let () = test let () = test
.provider .provider
@@ -581,8 +581,8 @@ async fn test_empty_execute() {
assert_eq!(test.provider.estimate_gas(call).await.unwrap(), 21_000 + 16); 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 (gas, fee) =
let fee = U256::from(0); 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; 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; const UNUSED_GAS: u64 = Router::GAS_FOR_ERC20_CALL - 16;
assert_eq!(gas_used + UNUSED_GAS, gas); assert_eq!(gas_used + UNUSED_GAS, gas);
@@ -600,8 +600,7 @@ async fn test_eth_address_out_instruction() {
let out_instructions = let out_instructions =
OutInstructions::from([(SeraiEthereumAddress::Address(rand_address), amount_out)].as_slice()); OutInstructions::from([(SeraiEthereumAddress::Address(rand_address), amount_out)].as_slice());
let gas = test.router.execute_gas(Coin::Ether, U256::from(1), &out_instructions); let (gas, fee) = test.router.execute_gas_and_fee(Coin::Ether, U256::from(1), &out_instructions);
let fee = U256::from(gas);
let () = test let () = test
.provider .provider
@@ -638,8 +637,7 @@ async fn test_erc20_address_out_instruction() {
let out_instructions = let out_instructions =
OutInstructions::from([(SeraiEthereumAddress::Address(rand_address), amount_out)].as_slice()); OutInstructions::from([(SeraiEthereumAddress::Address(rand_address), amount_out)].as_slice());
let gas = test.router.execute_gas(coin, U256::from(1), &out_instructions); let (gas, fee) = test.router.execute_gas_and_fee(coin, U256::from(1), &out_instructions);
let fee = U256::from(gas);
// Mint to the Router the necessary amount of the ERC20 // Mint to the Router the necessary amount of the ERC20
erc20.mint(&test, test.router.address(), amount_out + fee).await; erc20.mint(&test, test.router.address(), amount_out + fee).await;
@@ -674,8 +672,7 @@ async fn test_eth_code_out_instruction() {
.as_slice(), .as_slice(),
); );
let gas = test.router.execute_gas(Coin::Ether, U256::from(1), &out_instructions); let (gas, fee) = test.router.execute_gas_and_fee(Coin::Ether, U256::from(1), &out_instructions);
let fee = U256::from(gas);
let (tx, gas_used) = test.execute(Coin::Ether, fee, out_instructions, vec![true]).await; 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 // 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(), .as_slice(),
); );
let gas = test.router.execute_gas(Coin::Ether, U256::from(1), &out_instructions); let (gas, fee) = test.router.execute_gas_and_fee(Coin::Ether, U256::from(1), &out_instructions);
let fee = U256::from(gas);
let (tx, gas_used) = test.execute(Coin::Ether, fee, out_instructions, vec![true]).await; let (tx, gas_used) = test.execute(Coin::Ether, fee, out_instructions, vec![true]).await;
let unused_gas = test.gas_unused_by_calls(&tx).await; let unused_gas = test.gas_unused_by_calls(&tx).await;
@@ -744,8 +740,7 @@ async fn test_erc20_code_out_instruction() {
.as_slice(), .as_slice(),
); );
let gas = test.router.execute_gas(coin, U256::from(1), &out_instructions); let (gas, fee) = test.router.execute_gas_and_fee(coin, U256::from(1), &out_instructions);
let fee = U256::from(gas);
// Mint to the Router the necessary amount of the ERC20 // Mint to the Router the necessary amount of the ERC20
erc20.mint(&test, test.router.address(), amount_out + fee).await; erc20.mint(&test, test.router.address(), amount_out + fee).await;
@@ -777,11 +772,11 @@ async fn test_result_decoding() {
.as_slice(), .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`) // We should decode these in the correct order (not `false, true, true`)
let (_tx, gas_used) = 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 // 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 // (even with the trace), solely that it used less than or equal to the limit
assert!(gas_used <= gas); assert!(gas_used <= gas);
@@ -817,9 +812,8 @@ async fn test_reentrancy() {
.as_slice(), .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);
let (_tx, gas_used) = let (_tx, gas_used) = test.execute(Coin::Ether, fee, out_instructions, vec![true]).await;
test.execute(Coin::Ether, U256::from(0), out_instructions, vec![true]).await;
// Even though this doesn't have failed `OutInstruction`s, our logic is incomplete upon any // 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 // 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 // (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 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 (gas, fee) = test.router.execute_gas_and_fee(coin, fee_per_gas, &out_instructions);
let fee = U256::from(gas) * fee_per_gas;
// All of these should have succeeded // All of these should have succeeded
let (tx, gas_used) = let (tx, gas_used) =
test.execute(coin, fee, out_instructions.clone(), vec![true; out_instructions.0.len()]).await; 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);
}