Test ERC20 OutInstructions

This commit is contained in:
Luke Parker 2025-01-27 02:08:01 -05:00
parent 5164a710a2
commit e742a6b0ec
No known key found for this signature in database
4 changed files with 83 additions and 27 deletions
Cargo.lock
processor/ethereum/router

11
Cargo.lock generated
View file

@ -317,6 +317,7 @@ dependencies = [
"alloy-network-primitives",
"alloy-primitives",
"alloy-rpc-client",
"alloy-rpc-types-debug",
"alloy-rpc-types-eth",
"alloy-rpc-types-trace",
"alloy-transport",
@ -392,6 +393,16 @@ dependencies = [
"alloy-serde",
]
[[package]]
name = "alloy-rpc-types-debug"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "358d6a8d7340b9eb1a7589a6c1fb00df2c9b26e90737fa5ed0108724dd8dac2c"
dependencies = [
"alloy-primitives",
"serde",
]
[[package]]
name = "alloy-rpc-types-eth"
version = "0.9.2"

View file

@ -61,7 +61,7 @@ rand_core = { version = "0.6", default-features = false, features = ["std"] }
k256 = { version = "0.13", default-features = false, features = ["std"] }
alloy-provider = { version = "0.9", default-features = false, features = ["trace-api"] }
alloy-provider = { version = "0.9", default-features = false, features = ["debug-api", "trace-api"] }
alloy-rpc-client = { version = "0.9", default-features = false }
alloy-node-bindings = { version = "0.9", default-features = false }

View file

@ -169,8 +169,14 @@ impl Router {
// Clear the existing return data
interpreter.return_data_buffer.clear();
// If calling an ERC20, trigger the return data's worst-case by returning `true`
// (as expected by compliant ERC20s)
/*
If calling an ERC20, trigger the return data's worst-case by returning `true`
(as expected by compliant ERC20s). Else return none, as we expect none or won't bother
copying/decoding the return data.
This doesn't affect calls to ecrecover as those use STATICCALL and this overrides CALL
alone.
*/
if Some(address_called) == erc20 {
interpreter.return_data_buffer = true.abi_encode().into();
}

View file

@ -13,7 +13,10 @@ use alloy_consensus::{TxLegacy, Signed};
use alloy_rpc_types_eth::{BlockNumberOrTag, TransactionInput, TransactionRequest};
use alloy_simple_request_transport::SimpleRequest;
use alloy_rpc_client::ClientBuilder;
use alloy_provider::{Provider, RootProvider, ext::TraceApi};
use alloy_provider::{
Provider, RootProvider,
ext::{DebugApi, TraceApi},
};
use alloy_node_bindings::{Anvil, AnvilInstance};
@ -120,7 +123,7 @@ impl Test {
async fn new() -> Self {
// The following is explicitly only evaluated against the cancun network upgrade at this time
let anvil = Anvil::new().arg("--hardfork").arg("cancun").spawn();
let anvil = Anvil::new().arg("--hardfork").arg("cancun").arg("--tracing").spawn();
let provider = Arc::new(RootProvider::new(
ClientBuilder::default().transport(SimpleRequest::new(anvil.endpoint()), true),
@ -435,6 +438,38 @@ impl Test {
tx.gas_price = 100_000_000_000;
tx
}
async fn gas_unused_by_calls(&self, tx: &Signed<TxLegacy>) -> u64 {
let mut unused_gas = 0;
// Handle the difference between the gas limits and gas used values
let traces = self.provider.trace_transaction(*tx.hash()).await.unwrap();
// Skip the initial call to the Router and the call to ecrecover
let mut traces = traces.iter().skip(2);
while let Some(trace) = traces.next() {
let trace = &trace.trace;
// We're tracing the Router's immediate actions, and it doesn't immediately call CREATE
// It only makes a call to itself which calls CREATE
let gas_provided = trace.action.as_call().as_ref().unwrap().gas;
let gas_spent = trace.result.as_ref().unwrap().gas_used();
unused_gas += gas_provided - gas_spent;
for _ in 0 .. trace.subtraces {
// Skip the subtraces for this call (such as CREATE)
traces.next().unwrap();
}
}
// Also handle any refunds
{
let trace =
self.provider.debug_trace_transaction(*tx.hash(), Default::default()).await.unwrap();
let refund =
trace.try_into_default_frame().unwrap().struct_logs.last().unwrap().refund_counter;
unused_gas += refund.unwrap_or(0)
}
unused_gas
}
}
#[tokio::test]
@ -772,11 +807,32 @@ async fn test_eth_address_out_instruction() {
#[tokio::test]
async fn test_erc20_address_out_instruction() {
todo!("TODO")
/*
let mut test = Test::new().await;
test.confirm_next_serai_key().await;
let erc20 = Erc20::deploy(&test).await;
let coin = Coin::Erc20(erc20.address());
let mut rand_address = [0xff; 20];
OsRng.fill_bytes(&mut rand_address);
let amount_out = U256::from(2);
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);
// Mint to the Router the necessary amount of the ERC20
erc20.mint(&test, test.router.address(), amount_out + fee).await;
let (tx, gas_used) = test.execute(coin, fee, out_instructions, vec![true]).await;
// Uses traces due to the complexity of modeling Erc20::transfer
let unused_gas = test.gas_unused_by_calls(&tx).await;
assert_eq!(gas_used + unused_gas, gas);
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);
*/
assert_eq!(erc20.balance_of(&test, tx.recover_signer().unwrap()).await, U256::from(fee));
assert_eq!(erc20.balance_of(&test, rand_address.into()).await, amount_out);
}
#[tokio::test]
@ -806,24 +862,7 @@ async fn test_eth_code_out_instruction() {
// We use call-traces here to determine how much gas was allowed but unused due to the complexity
// of modeling the call to the Router itself and the following CREATE
let mut unused_gas = 0;
{
let traces = test.provider.trace_transaction(*tx.hash()).await.unwrap();
// Skip the call to the Router and the ecrecover
let mut traces = traces.iter().skip(2);
while let Some(trace) = traces.next() {
let trace = &trace.trace;
// We're tracing the Router's immediate actions, and it doesn't immediately call CREATE
// It only makes a call to itself which calls CREATE
let gas_provided = trace.action.as_call().as_ref().unwrap().gas;
let gas_spent = trace.result.as_ref().unwrap().gas_used();
unused_gas += gas_provided - gas_spent;
for _ in 0 .. trace.subtraces {
// Skip the subtraces for this call (such as CREATE)
traces.next().unwrap();
}
}
}
let unused_gas = test.gas_unused_by_calls(&tx).await;
assert_eq!(gas_used + unused_gas, gas);
assert_eq!(