Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions crates/anvil/core/src/eth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,11 +380,19 @@ pub enum EthRequest {
#[serde(
rename = "anvil_dealERC20",
alias = "hardhat_dealERC20",
alias = "anvil_setERC20Balance",
alias = "tenderly_setErc20Balance"
alias = "anvil_setERC20Balance"
)]
DealERC20(Address, Address, #[serde(deserialize_with = "deserialize_number")] U256),

/// Sets the ERC20 allowance for a spender
#[serde(rename = "anvil_setERC20Allowance")]
SetERC20Allowance(
Address,
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
75 changes: 75 additions & 0 deletions crates/anvil/src/eth/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,10 @@ impl EthApi {
EthRequest::DealERC20(addr, token_addr, val) => {
self.anvil_deal_erc20(addr, token_addr, val).await.to_rpc_result()
}
EthRequest::SetERC20Allowance(owner, spender, token_addr, val) => self
.anvil_set_erc20_allowance(owner, spender, 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 @@ -1939,6 +1943,77 @@ impl EthApi {
Err(BlockchainError::Message("Unable to set ERC20 balance, no slot found".to_string()))
}

/// Sets the ERC20 allowance for a spender
///
/// Handler for RPC call: `anvil_set_erc20_allowance`
pub async fn anvil_set_erc20_allowance(
&self,
owner: Address,
spender: Address,
token_address: Address,
amount: U256,
) -> Result<()> {
node_info!("anvil_setERC20Allowance");

sol! {
#[sol(rpc)]
contract IERC20 {
function allowance(address owner, address spender) external view returns (uint256);
}
}

let calldata = IERC20::allowanceCall { owner, spender }.abi_encode();
let tx = TransactionRequest::default().with_to(token_address).with_input(calldata.clone());

// first collect all the slots that are used by the allowance 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
// allowance by overriding the slot and checking the `allowanceCall` result
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(amount.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_allowance) = U256::abi_decode(&result) else {
// response returned something other than a U256
continue;
};

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

// unable to set the allowance
Err(BlockchainError::Message("Unable to set ERC20 allowance, 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 @@ -1490,6 +1490,31 @@ async fn test_set_erc20_balance() {
assert_eq!(new_balance, value);
}

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

let provider = handle.http_provider();

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

api.anvil_set_erc20_allowance(owner, spender, dai, value).await.unwrap();

let allowance = erc20.allowance(owner, spender).call().await.unwrap();
assert_eq!(allowance, value);
}

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