#![cfg(feature = "reactive")]
mod common;
use alloy_primitives::{Address, Bytes, U256};
use alloy_sol_types::SolCall;
use anyhow::Result;
use revm::context::result::ExecutionResult;
use revm::state::AccountInfo;
use common::{
MOCK_ERC20_BALANCE_SLOT, MockERC20, install_default_account, install_mock_erc20, setup_cache,
};
use evm_fork_cache::{BundleOptions, BundleTx, EvmOverlay, RevertPolicy, TxConfig};
fn transfer_calldata(to: Address, amount: u64) -> Bytes {
Bytes::from(
MockERC20::transferCall {
to,
amount: U256::from(amount),
}
.abi_encode(),
)
}
fn overlay_balance(overlay: &mut EvmOverlay, token: Address, owner: Address) -> Result<U256> {
let calldata = Bytes::from(MockERC20::balanceOfCall { account: owner }.abi_encode());
match overlay.call_raw(owner, token, calldata)? {
ExecutionResult::Success { output, .. } => Ok(
MockERC20::balanceOfCall::abi_decode_returns(&output.into_data())?,
),
other => anyhow::bail!("balanceOf failed: {other:?}"),
}
}
async fn token_with_funded_alice()
-> Result<(evm_fork_cache::EvmCache, Address, Address, Address, Address)> {
let mut cache = setup_cache().await?;
let token = Address::repeat_byte(0x11);
let alice = Address::repeat_byte(0x22);
let bob = Address::repeat_byte(0x33);
let carol = Address::repeat_byte(0x44);
install_default_account(&mut cache, Address::ZERO);
install_default_account(&mut cache, alice);
install_default_account(&mut cache, bob);
install_default_account(&mut cache, carol);
install_mock_erc20(&mut cache, token);
cache.insert_mapping_storage_slot(
token,
U256::from(MOCK_ERC20_BALANCE_SLOT),
alice,
U256::from(1_000u64),
)?;
Ok((cache, token, alice, bob, carol))
}
#[tokio::test(flavor = "multi_thread")]
async fn bundle_applies_txs_over_cumulative_state() -> Result<()> {
let (mut cache, token, alice, bob, carol) = token_with_funded_alice().await?;
let mut overlay = EvmOverlay::new(cache.snapshot(), None);
let txs = vec![
BundleTx::new(alice, token, transfer_calldata(bob, 100)),
BundleTx::new(bob, token, transfer_calldata(carol, 30)),
];
let result = overlay.simulate_bundle(
&txs,
&BundleOptions {
commit: true,
..Default::default()
},
)?;
assert!(result.succeeded, "atomic bundle should succeed");
assert_eq!(result.per_tx.len(), 2);
assert!(result.per_tx.iter().all(|o| !o.reverted));
assert_eq!(
overlay_balance(&mut overlay, token, alice)?,
U256::from(900u64)
);
assert_eq!(
overlay_balance(&mut overlay, token, bob)?,
U256::from(70u64)
);
assert_eq!(
overlay_balance(&mut overlay, token, carol)?,
U256::from(30u64)
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn atomic_bundle_reverts_whole_on_failure() -> Result<()> {
let (mut cache, token, alice, bob, _carol) = token_with_funded_alice().await?;
let mut overlay = EvmOverlay::new(cache.snapshot(), None);
let txs = vec![
BundleTx::new(alice, token, transfer_calldata(bob, 100)),
BundleTx::new(alice, token, Bytes::from(vec![0xde, 0xad, 0xbe, 0xef])),
];
let result = overlay.simulate_bundle(
&txs,
&BundleOptions {
revert_policy: RevertPolicy::Atomic,
commit: true,
},
)?;
assert!(
!result.succeeded,
"atomic bundle must fail when a tx reverts"
);
assert!(result.per_tx.last().map(|o| o.reverted).unwrap_or(false));
assert_eq!(
overlay_balance(&mut overlay, token, alice)?,
U256::from(1_000u64)
);
assert_eq!(overlay_balance(&mut overlay, token, bob)?, U256::ZERO);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn allow_reverts_keeps_prior_effects() -> Result<()> {
let (mut cache, token, alice, bob, _carol) = token_with_funded_alice().await?;
let mut overlay = EvmOverlay::new(cache.snapshot(), None);
let txs = vec![
BundleTx::new(alice, token, transfer_calldata(bob, 100)),
BundleTx::new(alice, token, Bytes::from(vec![0xde, 0xad, 0xbe, 0xef])),
];
let result = overlay.simulate_bundle(
&txs,
&BundleOptions {
revert_policy: RevertPolicy::AllowReverts(vec![1]),
commit: true,
},
)?;
assert!(
result.succeeded,
"bundle should succeed when the revert is allowed"
);
assert!(!result.per_tx[0].reverted);
assert!(result.per_tx[1].reverted);
assert_eq!(
overlay_balance(&mut overlay, token, alice)?,
U256::from(900u64)
);
assert_eq!(
overlay_balance(&mut overlay, token, bob)?,
U256::from(100u64)
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn direct_coinbase_payment_is_captured() -> Result<()> {
let mut cache = setup_cache().await?;
let searcher = Address::repeat_byte(0x22);
install_default_account(&mut cache, Address::ZERO);
cache.db_mut().insert_account_info(
searcher,
AccountInfo {
balance: U256::from(10_000u64),
..Default::default()
},
);
let pay = U256::from(777u64);
let tx = BundleTx::with_config(
searcher,
Address::ZERO, Bytes::new(),
TxConfig {
value: pay,
gas_price: Some(0),
..Default::default()
},
);
let mut overlay = EvmOverlay::new(cache.snapshot(), None);
let result = overlay.simulate_bundle(
std::slice::from_ref(&tx),
&BundleOptions {
commit: true,
..Default::default()
},
)?;
assert!(result.succeeded);
assert_eq!(result.coinbase_payment, pay);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn coinbase_payment_is_priority_fee_only() -> Result<()> {
let mut cache = setup_cache().await?;
let token = Address::repeat_byte(0x11);
let caller = Address::repeat_byte(0x22);
install_default_account(&mut cache, Address::ZERO);
install_mock_erc20(&mut cache, token);
cache.db_mut().insert_account_info(
caller,
AccountInfo {
balance: U256::from(10u64).pow(U256::from(20u64)),
..Default::default()
},
);
let basefee: u128 = 1_000_000_000; let priority: u128 = 2_000_000_000; cache.set_basefee(U256::from(basefee));
let tx = BundleTx::with_config(
caller,
token,
Bytes::from(MockERC20::balanceOfCall { account: caller }.abi_encode()),
TxConfig {
gas_price: Some(basefee + priority),
..Default::default()
},
);
let mut overlay = EvmOverlay::new(cache.snapshot(), None);
let result = overlay.simulate_bundle(std::slice::from_ref(&tx), &BundleOptions::default())?;
assert!(result.gas_used > 0);
assert_eq!(
result.coinbase_payment,
U256::from(result.gas_used) * U256::from(priority),
"coinbase payment must be gas_used * priority_fee (base fee excluded)"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn commit_flag_controls_overlay_persistence() -> Result<()> {
let (mut cache, token, alice, bob, _carol) = token_with_funded_alice().await?;
let snapshot = cache.snapshot();
let mut overlay = EvmOverlay::new(snapshot.clone(), None);
overlay.simulate_bundle(
&[BundleTx::new(alice, token, transfer_calldata(bob, 100))],
&BundleOptions {
commit: false,
..Default::default()
},
)?;
assert_eq!(overlay_balance(&mut overlay, token, bob)?, U256::ZERO);
let mut overlay = EvmOverlay::new(snapshot, None);
overlay.simulate_bundle(
&[BundleTx::new(alice, token, transfer_calldata(bob, 100))],
&BundleOptions {
commit: true,
..Default::default()
},
)?;
assert_eq!(
overlay_balance(&mut overlay, token, bob)?,
U256::from(100u64)
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn allow_reverts_exposes_reverted_and_successful_gas() -> Result<()> {
let (mut cache, token, alice, bob, _carol) = token_with_funded_alice().await?;
let mut overlay = EvmOverlay::new(cache.snapshot(), None);
let txs = vec![
BundleTx::new(alice, token, transfer_calldata(bob, 100)),
BundleTx::new(alice, token, Bytes::from(vec![0xde, 0xad, 0xbe, 0xef])),
];
let result = overlay.simulate_bundle(
&txs,
&BundleOptions {
revert_policy: RevertPolicy::AllowReverts(vec![1]),
commit: true,
},
)?;
assert!(result.succeeded);
assert!(!result.per_tx[0].reverted);
assert!(result.per_tx[1].reverted);
assert_eq!(
result.successful_tx_gas, result.per_tx[0].gas_used,
"successful bucket = kept tx gas"
);
assert_eq!(
result.reverted_tx_gas, result.per_tx[1].gas_used,
"reverted bucket = reverted tx gas"
);
assert!(
result.reverted_tx_gas > 0,
"the reverted whitelisted tx still consumed gas"
);
assert_eq!(
result.successful_tx_gas + result.reverted_tx_gas,
result.gas_used,
"the two buckets must reconstruct total gas_used"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn successful_bundle_reports_zero_reverted_gas() -> Result<()> {
let (mut cache, token, alice, bob, carol) = token_with_funded_alice().await?;
let mut overlay = EvmOverlay::new(cache.snapshot(), None);
let txs = vec![
BundleTx::new(alice, token, transfer_calldata(bob, 100)),
BundleTx::new(bob, token, transfer_calldata(carol, 30)),
];
let result = overlay.simulate_bundle(
&txs,
&BundleOptions {
commit: true,
..Default::default()
},
)?;
assert!(result.succeeded);
assert_eq!(result.reverted_tx_gas, 0, "no tx reverted");
assert_eq!(
result.successful_tx_gas, result.gas_used,
"all gas is in the successful bucket"
);
Ok(())
}