Skip to content

Commit bba521f

Browse files
mattsseclaude
andcommitted
refactor: unify ERC20 storage slot discovery logic
Extracts duplicated access list handling code from `anvil_deal_erc20` and `anvil_set_erc20_allowance` into a shared `find_erc20_storage_slot` helper function. Changes: - Add comprehensive documentation explaining the slot discovery process - Reduce code duplication by ~80 lines - Improve maintainability and consistency between functions - Fix unfulfilled clippy expectation in cast/tx.rs This is a follow-up cleanup to #10746 which introduced `anvil_set_erc20_allowance`. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 1836d5e commit bba521f

File tree

2 files changed

+81
-81
lines changed

2 files changed

+81
-81
lines changed

crates/anvil/src/eth/api.rs

Lines changed: 81 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1873,41 +1873,45 @@ impl EthApi {
18731873
Ok(())
18741874
}
18751875

1876-
/// Deals ERC20 tokens to a address
1877-
///
1878-
/// Handler for RPC call: `anvil_dealERC20`
1879-
pub async fn anvil_deal_erc20(
1876+
/// Helper function to find the storage slot for an ERC20 function call by testing slots
1877+
/// from an access list until one produces the expected result.
1878+
///
1879+
/// Rather than trying to reverse-engineer the storage layout, this function uses a
1880+
/// "trial and error" approach: try overriding each slot that the function accesses,
1881+
/// and see which one actually affects the function's return value.
1882+
///
1883+
/// ## Parameters
1884+
/// - `token_address`: The ERC20 token contract address
1885+
/// - `calldata`: The encoded function call (e.g., `balanceOf(user)` or `allowance(owner,
1886+
/// spender)`)
1887+
/// - `expected_value`: The value we want to set (balance or allowance amount)
1888+
///
1889+
/// ## Returns
1890+
/// The storage slot (B256) that contains the target ERC20 data, or an error if no slot is
1891+
/// found.
1892+
async fn find_erc20_storage_slot(
18801893
&self,
1881-
address: Address,
18821894
token_address: Address,
1883-
balance: U256,
1884-
) -> Result<()> {
1885-
node_info!("anvil_dealERC20");
1886-
1887-
sol! {
1888-
#[sol(rpc)]
1889-
contract IERC20 {
1890-
function balanceOf(address target) external view returns (uint256);
1891-
}
1892-
}
1893-
1894-
let calldata = IERC20::balanceOfCall { target: address }.abi_encode();
1895+
calldata: Bytes,
1896+
expected_value: U256,
1897+
) -> Result<B256> {
18951898
let tx = TransactionRequest::default().with_to(token_address).with_input(calldata.clone());
18961899

1897-
// first collect all the slots that are used by the balanceOf call
1900+
// first collect all the slots that are used by the function call
18981901
let access_list_result =
18991902
self.create_access_list(WithOtherFields::new(tx.clone()), None).await?;
19001903
let access_list = access_list_result.access_list;
19011904

1902-
// now we can iterate over all the accessed slots and try to find the one that contains the
1903-
// balance by overriding the slot and checking the `balanceOfCall` of
1905+
// iterate over all the accessed slots and try to find the one that contains the
1906+
// target value by overriding the slot and checking the function call result
19041907
for item in access_list.0 {
19051908
if item.address != token_address {
19061909
continue;
19071910
};
19081911
for slot in &item.storage_keys {
1909-
let account_override = AccountOverride::default()
1910-
.with_state_diff(std::iter::once((*slot, B256::from(balance.to_be_bytes()))));
1912+
let account_override = AccountOverride::default().with_state_diff(std::iter::once(
1913+
(*slot, B256::from(expected_value.to_be_bytes())),
1914+
));
19111915

19121916
let state_override = StateOverridesBuilder::default()
19131917
.append(token_address, account_override)
@@ -1922,25 +1926,55 @@ impl EthApi {
19221926
continue;
19231927
};
19241928

1925-
let Ok(result_balance) = U256::abi_decode(&result) else {
1929+
let Ok(result_value) = U256::abi_decode(&result) else {
19261930
// response returned something other than a U256
19271931
continue;
19281932
};
19291933

1930-
if result_balance == balance {
1931-
self.anvil_set_storage_at(
1932-
token_address,
1933-
U256::from_be_bytes(slot.0),
1934-
B256::from(balance.to_be_bytes()),
1935-
)
1936-
.await?;
1937-
return Ok(());
1934+
if result_value == expected_value {
1935+
return Ok(*slot);
19381936
}
19391937
}
19401938
}
19411939

1942-
// unable to set the balance
1943-
Err(BlockchainError::Message("Unable to set ERC20 balance, no slot found".to_string()))
1940+
Err(BlockchainError::Message("Unable to find storage slot".to_string()))
1941+
}
1942+
1943+
/// Deals ERC20 tokens to a address
1944+
///
1945+
/// Handler for RPC call: `anvil_dealERC20`
1946+
pub async fn anvil_deal_erc20(
1947+
&self,
1948+
address: Address,
1949+
token_address: Address,
1950+
balance: U256,
1951+
) -> Result<()> {
1952+
node_info!("anvil_dealERC20");
1953+
1954+
sol! {
1955+
#[sol(rpc)]
1956+
contract IERC20 {
1957+
function balanceOf(address target) external view returns (uint256);
1958+
}
1959+
}
1960+
1961+
let calldata = IERC20::balanceOfCall { target: address }.abi_encode().into();
1962+
1963+
// Find the storage slot that contains the balance
1964+
let slot =
1965+
self.find_erc20_storage_slot(token_address, calldata, balance).await.map_err(|_| {
1966+
BlockchainError::Message("Unable to set ERC20 balance, no slot found".to_string())
1967+
})?;
1968+
1969+
// Set the storage slot to the desired balance
1970+
self.anvil_set_storage_at(
1971+
token_address,
1972+
U256::from_be_bytes(slot.0),
1973+
B256::from(balance.to_be_bytes()),
1974+
)
1975+
.await?;
1976+
1977+
Ok(())
19441978
}
19451979

19461980
/// Sets the ERC20 allowance for a spender
@@ -1962,56 +1996,23 @@ impl EthApi {
19621996
}
19631997
}
19641998

1965-
let calldata = IERC20::allowanceCall { owner, spender }.abi_encode();
1966-
let tx = TransactionRequest::default().with_to(token_address).with_input(calldata.clone());
1967-
1968-
// first collect all the slots that are used by the allowance call
1969-
let access_list_result =
1970-
self.create_access_list(WithOtherFields::new(tx.clone()), None).await?;
1971-
let access_list = access_list_result.access_list;
1972-
1973-
// now we can iterate over all the accessed slots and try to find the one that contains the
1974-
// allowance by overriding the slot and checking the `allowanceCall` result
1975-
for item in access_list.0 {
1976-
if item.address != token_address {
1977-
continue;
1978-
};
1979-
for slot in &item.storage_keys {
1980-
let account_override = AccountOverride::default()
1981-
.with_state_diff(std::iter::once((*slot, B256::from(amount.to_be_bytes()))));
1982-
1983-
let state_override = StateOverridesBuilder::default()
1984-
.append(token_address, account_override)
1985-
.build();
1986-
1987-
let evm_override = EvmOverrides::state(Some(state_override));
1999+
let calldata = IERC20::allowanceCall { owner, spender }.abi_encode().into();
19882000

1989-
let Ok(result) =
1990-
self.call(WithOtherFields::new(tx.clone()), None, evm_override).await
1991-
else {
1992-
// overriding this slot failed
1993-
continue;
1994-
};
2001+
// Find the storage slot that contains the allowance
2002+
let slot =
2003+
self.find_erc20_storage_slot(token_address, calldata, amount).await.map_err(|_| {
2004+
BlockchainError::Message("Unable to set ERC20 allowance, no slot found".to_string())
2005+
})?;
19952006

1996-
let Ok(result_allowance) = U256::abi_decode(&result) else {
1997-
// response returned something other than a U256
1998-
continue;
1999-
};
2000-
2001-
if result_allowance == amount {
2002-
self.anvil_set_storage_at(
2003-
token_address,
2004-
U256::from_be_bytes(slot.0),
2005-
B256::from(amount.to_be_bytes()),
2006-
)
2007-
.await?;
2008-
return Ok(());
2009-
}
2010-
}
2011-
}
2007+
// Set the storage slot to the desired allowance
2008+
self.anvil_set_storage_at(
2009+
token_address,
2010+
U256::from_be_bytes(slot.0),
2011+
B256::from(amount.to_be_bytes()),
2012+
)
2013+
.await?;
20122014

2013-
// unable to set the allowance
2014-
Err(BlockchainError::Message("Unable to set ERC20 allowance, no slot found".to_string()))
2015+
Ok(())
20152016
}
20162017

20172018
/// Sets the code of a contract.

crates/cast/src/tx.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ use serde_json::value::RawValue;
2727
use std::fmt::Write;
2828

2929
/// Different sender kinds used by [`CastTxBuilder`].
30-
#[expect(clippy::large_enum_variant)]
3130
pub enum SenderKind<'a> {
3231
/// An address without signer. Used for read-only calls and transactions sent through unlocked
3332
/// accounts.

0 commit comments

Comments
 (0)