Skip to content

Commit 3e1c72b

Browse files
Soubhik-10mattsse
andauthored
feat: added rpc method to deal ERC20 tokens (#10495)
* wip * wip * wip * wip * call + storage * type fix * wip * wip * smol fix * cleanup * cleanup * cleanup * cleanup * chore: pedantic touchups --------- Co-authored-by: Matthias Seitz <[email protected]>
1 parent 6adc3d6 commit 3e1c72b

File tree

3 files changed

+109
-1
lines changed

3 files changed

+109
-1
lines changed

crates/anvil/core/src/eth/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,15 @@ pub enum EthRequest {
363363
#[serde(rename = "anvil_setBalance", alias = "hardhat_setBalance")]
364364
SetBalance(Address, #[serde(deserialize_with = "deserialize_number")] U256),
365365

366+
/// Modifies the ERC20 balance of an account.
367+
#[serde(
368+
rename = "anvil_dealERC20",
369+
alias = "hardhat_dealERC20",
370+
alias = "anvil_setERC20Balance",
371+
alias = "tenderly_setErc20Balance"
372+
)]
373+
DealERC20(Address, Address, #[serde(deserialize_with = "deserialize_number")] U256),
374+
366375
/// Sets the code of a contract
367376
#[serde(rename = "anvil_setCode", alias = "hardhat_setCode")]
368377
SetCode(Address, Bytes),

crates/anvil/src/eth/api.rs

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ use alloy_rpc_types::{
5656
},
5757
request::TransactionRequest,
5858
simulate::{SimulatePayload, SimulatedBlock},
59-
state::EvmOverrides,
59+
state::{AccountOverride, EvmOverrides, StateOverridesBuilder},
6060
trace::{
6161
filter::TraceFilter,
6262
geth::{GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace},
@@ -67,6 +67,7 @@ use alloy_rpc_types::{
6767
EIP1186AccountProofResponse, FeeHistory, Filter, FilteredParams, Index, Log, Work,
6868
};
6969
use alloy_serde::WithOtherFields;
70+
use alloy_sol_types::{sol, SolCall, SolValue};
7071
use alloy_transport::TransportErrorKind;
7172
use anvil_core::{
7273
eth::{
@@ -355,6 +356,9 @@ impl EthApi {
355356
EthRequest::SetBalance(addr, val) => {
356357
self.anvil_set_balance(addr, val).await.to_rpc_result()
357358
}
359+
EthRequest::DealERC20(addr, token_addr, val) => {
360+
self.anvil_deal_erc20(addr, token_addr, val).await.to_rpc_result()
361+
}
358362
EthRequest::SetCode(addr, code) => {
359363
self.anvil_set_code(addr, code).await.to_rpc_result()
360364
}
@@ -1852,6 +1856,76 @@ impl EthApi {
18521856
Ok(())
18531857
}
18541858

1859+
/// Deals ERC20 tokens to a address
1860+
///
1861+
/// Handler for RPC call: `anvil_dealERC20`
1862+
pub async fn anvil_deal_erc20(
1863+
&self,
1864+
address: Address,
1865+
token_address: Address,
1866+
balance: U256,
1867+
) -> Result<()> {
1868+
node_info!("anvil_dealERC20");
1869+
1870+
sol! {
1871+
#[sol(rpc)]
1872+
contract IERC20 {
1873+
function balanceOf(address target) external view returns (uint256);
1874+
}
1875+
}
1876+
1877+
let calldata = IERC20::balanceOfCall { target: address }.abi_encode();
1878+
let tx = TransactionRequest::default().with_to(token_address).with_input(calldata.clone());
1879+
1880+
// first collect all the slots that are used by the balanceOf call
1881+
let access_list_result =
1882+
self.create_access_list(WithOtherFields::new(tx.clone()), None).await?;
1883+
let access_list = access_list_result.access_list;
1884+
1885+
// now we can iterate over all the accessed slots and try to find the one that contains the
1886+
// balance by overriding the slot and checking the `balanceOfCall` of
1887+
for item in access_list.0 {
1888+
if item.address != token_address {
1889+
continue;
1890+
};
1891+
for slot in &item.storage_keys {
1892+
let account_override = AccountOverride::default()
1893+
.with_state_diff(std::iter::once((*slot, B256::from(balance.to_be_bytes()))));
1894+
1895+
let state_override = StateOverridesBuilder::default()
1896+
.append(token_address, account_override)
1897+
.build();
1898+
1899+
let evm_override = EvmOverrides::state(Some(state_override));
1900+
1901+
let Ok(result) =
1902+
self.call(WithOtherFields::new(tx.clone()), None, evm_override).await
1903+
else {
1904+
// overriding this slot failed
1905+
continue;
1906+
};
1907+
1908+
let Ok(result_balance) = U256::abi_decode(&result) else {
1909+
// response returned something other than a U256
1910+
continue;
1911+
};
1912+
1913+
if result_balance == balance {
1914+
self.anvil_set_storage_at(
1915+
token_address,
1916+
U256::from_be_bytes(slot.0),
1917+
B256::from(balance.to_be_bytes()),
1918+
)
1919+
.await?;
1920+
return Ok(());
1921+
}
1922+
}
1923+
}
1924+
1925+
// unable to set the balance
1926+
Err(BlockchainError::Message("Unable to set ERC20 balance, no slot found".to_string()))
1927+
}
1928+
18551929
/// Sets the code of a contract.
18561930
///
18571931
/// Handler for RPC call: `anvil_setCode`

crates/anvil/tests/it/fork.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1465,6 +1465,31 @@ async fn test_reset_dev_account_nonce() {
14651465
assert!(receipt.status());
14661466
}
14671467

1468+
#[tokio::test(flavor = "multi_thread")]
1469+
async fn test_set_erc20_balance() {
1470+
let config: NodeConfig = fork_config();
1471+
let address = config.genesis_accounts[0].address();
1472+
let (api, handle) = spawn(config).await;
1473+
1474+
let provider = handle.http_provider();
1475+
1476+
alloy_sol_types::sol! {
1477+
#[sol(rpc)]
1478+
contract ERC20 {
1479+
function balanceOf(address owner) public view returns (uint256);
1480+
}
1481+
}
1482+
let dai = address!("0x6B175474E89094C44Da98b954EedeAC495271d0F");
1483+
let erc20 = ERC20::new(dai, provider);
1484+
let value = U256::from(500);
1485+
1486+
api.anvil_deal_erc20(address, dai, value).await.unwrap();
1487+
1488+
let new_balance = erc20.balanceOf(address).call().await.unwrap();
1489+
1490+
assert_eq!(new_balance, value);
1491+
}
1492+
14681493
#[tokio::test(flavor = "multi_thread")]
14691494
async fn test_reset_updates_cache_path_when_rpc_url_not_provided() {
14701495
let config: NodeConfig = fork_config();

0 commit comments

Comments
 (0)