Skip to content

feat: added rpc method to deal ERC20 tokens #10495

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
May 24, 2025
9 changes: 9 additions & 0 deletions crates/anvil/core/src/eth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,15 @@ pub enum EthRequest {
#[serde(rename = "anvil_setBalance", alias = "hardhat_setBalance")]
SetBalance(Address, #[serde(deserialize_with = "deserialize_number")] U256),

/// Modifies the ERC20 balance of an account.
#[serde(
rename = "anvil_dealERC20",
alias = "hardhat_dealERC20",
alias = "anvil_setERC20Balance",
alias = "tenderly_setErc20Balance"
)]
DealERC20(Address, Address, #[serde(deserialize_with = "deserialize_number")] U256),

/// Sets the code of a contract
#[serde(rename = "anvil_setCode", alias = "hardhat_setCode")]
SetCode(Address, Bytes),
Expand Down
76 changes: 75 additions & 1 deletion crates/anvil/src/eth/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ use alloy_rpc_types::{
},
request::TransactionRequest,
simulate::{SimulatePayload, SimulatedBlock},
state::EvmOverrides,
state::{AccountOverride, EvmOverrides, StateOverridesBuilder},
trace::{
filter::TraceFilter,
geth::{GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace},
Expand All @@ -67,6 +67,7 @@ use alloy_rpc_types::{
EIP1186AccountProofResponse, FeeHistory, Filter, FilteredParams, Index, Log, Work,
};
use alloy_serde::WithOtherFields;
use alloy_sol_types::{sol, SolCall, SolValue};
use alloy_transport::TransportErrorKind;
use anvil_core::{
eth::{
Expand Down Expand Up @@ -355,6 +356,9 @@ impl EthApi {
EthRequest::SetBalance(addr, val) => {
self.anvil_set_balance(addr, val).await.to_rpc_result()
}
EthRequest::DealERC20(addr, token_addr, val) => {
self.anvil_deal_erc20(addr, token_addr, val).await.to_rpc_result()
}
EthRequest::SetCode(addr, code) => {
self.anvil_set_code(addr, code).await.to_rpc_result()
}
Expand Down Expand Up @@ -1852,6 +1856,76 @@ impl EthApi {
Ok(())
}

/// Deals ERC20 tokens to a address
///
/// Handler for RPC call: `anvil_dealERC20`
pub async fn anvil_deal_erc20(
&self,
address: Address,
token_address: Address,
balance: U256,
) -> Result<()> {
node_info!("anvil_dealERC20");

sol! {
#[sol(rpc)]
contract IERC20 {
function balanceOf(address target) external view returns (uint256);
}
}

let calldata = IERC20::balanceOfCall { target: address }.abi_encode();
let tx = TransactionRequest::default().with_to(token_address).with_input(calldata.clone());

// first collect all the slots that are used by the balanceOf call
let access_list_result =
self.create_access_list(WithOtherFields::new(tx.clone()), None).await?;
let access_list = access_list_result.access_list;

// now we can iterate over all the accessed slots and try to find the one that contains the
// balance by overriding the slot and checking the `balanceOfCall` of
for item in access_list.0 {
if item.address != token_address {
continue;
};
for slot in &item.storage_keys {
let account_override = AccountOverride::default()
.with_state_diff(std::iter::once((*slot, B256::from(balance.to_be_bytes()))));

let state_override = StateOverridesBuilder::default()
.append(token_address, account_override)
.build();

let evm_override = EvmOverrides::state(Some(state_override));

let Ok(result) =
self.call(WithOtherFields::new(tx.clone()), None, evm_override).await
else {
// overriding this slot failed
continue;
};

let Ok(result_balance) = U256::abi_decode(&result) else {
// response returned something other than a U256
continue;
};

if result_balance == balance {
self.anvil_set_storage_at(
token_address,
U256::from_be_bytes(slot.0),
B256::from(balance.to_be_bytes()),
)
.await?;
return Ok(());
}
}
}

// unable to set the balance
Err(BlockchainError::Message("Unable to set ERC20 balance, no slot found".to_string()))
}

/// Sets the code of a contract.
///
/// Handler for RPC call: `anvil_setCode`
Expand Down
25 changes: 25 additions & 0 deletions crates/anvil/tests/it/fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1465,6 +1465,31 @@ async fn test_reset_dev_account_nonce() {
assert!(receipt.status());
}

#[tokio::test(flavor = "multi_thread")]
async fn test_set_erc20_balance() {
let config: NodeConfig = fork_config();
let address = config.genesis_accounts[0].address();
let (api, handle) = spawn(config).await;

let provider = handle.http_provider();

alloy_sol_types::sol! {
#[sol(rpc)]
contract ERC20 {
function balanceOf(address owner) public view returns (uint256);
}
}
let dai = address!("0x6B175474E89094C44Da98b954EedeAC495271d0F");
let erc20 = ERC20::new(dai, provider);
let value = U256::from(500);

api.anvil_deal_erc20(address, dai, value).await.unwrap();

let new_balance = erc20.balanceOf(address).call().await.unwrap();

assert_eq!(new_balance, value);
}

#[tokio::test(flavor = "multi_thread")]
async fn test_reset_updates_cache_path_when_rpc_url_not_provided() {
let config: NodeConfig = fork_config();
Expand Down