mod common;
use alloy_primitives::{Address, I256, U256};
use alloy_sol_types::SolCall;
use anyhow::Result;
use common::{
MOCK_ERC20_BALANCE_SLOT, MockERC20, install_default_account, install_mock_erc20, setup_cache,
};
use evm_fork_cache::errors::RevertReason;
fn transfer_calldata(to: Address, amount: U256) -> alloy_primitives::Bytes {
MockERC20::transferCall { to, amount }.abi_encode().into()
}
#[tokio::test(flavor = "multi_thread")]
async fn transfer_tracking_reports_sender_delta_and_logs() -> Result<()> {
let mut cache = setup_cache().await?;
let token = Address::repeat_byte(0x11);
let owner = Address::repeat_byte(0x22);
let recipient = Address::repeat_byte(0x33);
install_default_account(&mut cache, Address::ZERO);
install_default_account(&mut cache, owner);
install_default_account(&mut cache, recipient);
install_mock_erc20(&mut cache, token);
let balance_slot = U256::from(MOCK_ERC20_BALANCE_SLOT);
cache.insert_mapping_storage_slot(token, balance_slot, owner, U256::from(1_000u64))?;
cache.insert_mapping_storage_slot(token, balance_slot, recipient, U256::ZERO)?;
let result = cache.simulate_with_transfer_tracking(
owner,
token,
transfer_calldata(recipient, U256::from(250u64)),
owner,
Some([token]),
false, )?;
assert_eq!(
result.token_deltas.get(&token),
Some(&I256::try_from(-250i64).unwrap()),
"sender's delta is -amount"
);
assert_eq!(result.logs.len(), 1, "exactly one Transfer log emitted");
assert!(
result
.access_list
.0
.iter()
.any(|item| item.address == token),
"access list includes the token account"
);
assert_eq!(
common::balance_of(&mut cache, token, owner)?,
U256::from(1_000u64),
"a non-committing sim must not change cache state"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn transfer_tracking_reports_recipient_delta() -> Result<()> {
let mut cache = setup_cache().await?;
let token = Address::repeat_byte(0x44);
let owner = Address::repeat_byte(0x55);
let recipient = Address::repeat_byte(0x66);
install_default_account(&mut cache, Address::ZERO);
install_default_account(&mut cache, owner);
install_default_account(&mut cache, recipient);
install_mock_erc20(&mut cache, token);
cache.insert_mapping_storage_slot(
token,
U256::from(MOCK_ERC20_BALANCE_SLOT),
owner,
U256::from(500u64),
)?;
let result = cache.simulate_with_transfer_tracking(
owner,
token,
transfer_calldata(recipient, U256::from(120u64)),
recipient,
None::<Vec<Address>>,
false,
)?;
assert_eq!(
result.token_deltas.get(&token),
Some(&I256::try_from(120i64).unwrap()),
"recipient's delta is +amount"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn transfer_tracking_token_filter_excludes_other_tokens() -> Result<()> {
let mut cache = setup_cache().await?;
let token = Address::repeat_byte(0x77);
let other_token = Address::repeat_byte(0x78);
let owner = Address::repeat_byte(0x88);
let recipient = Address::repeat_byte(0x89);
install_default_account(&mut cache, Address::ZERO);
install_default_account(&mut cache, owner);
install_default_account(&mut cache, recipient);
install_mock_erc20(&mut cache, token);
cache.insert_mapping_storage_slot(
token,
U256::from(MOCK_ERC20_BALANCE_SLOT),
owner,
U256::from(1_000u64),
)?;
let result = cache.simulate_with_transfer_tracking(
owner,
token,
transfer_calldata(recipient, U256::from(250u64)),
owner,
Some([other_token]),
false,
)?;
assert!(
result.token_deltas.is_empty(),
"the transferred token is filtered out, leaving no deltas"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn transfer_tracking_surfaces_revert_reason() -> Result<()> {
let mut cache = setup_cache().await?;
let token = Address::repeat_byte(0xAA);
let owner = Address::repeat_byte(0xBB);
let recipient = Address::repeat_byte(0xCC);
install_default_account(&mut cache, Address::ZERO);
install_default_account(&mut cache, owner);
install_default_account(&mut cache, recipient);
install_mock_erc20(&mut cache, token);
let err = cache
.simulate_with_transfer_tracking(
owner,
token,
transfer_calldata(recipient, U256::from(100u64)),
owner,
None::<Vec<Address>>,
false,
)
.expect_err("transfer with no balance must revert");
assert!(err.is_revert(), "expected a revert, got {err:?}");
let revert = err.as_revert().expect("revert payload");
assert_eq!(
revert.reason(),
&RevertReason::Error("balance".to_string()),
"MockERC20._transfer reverts with require(.., \"balance\")"
);
Ok(())
}