use alloy_network::{Network, TransactionBuilder};
use alloy_primitives::{Address, Bytes, B256, U256};
use alloy_provider::Provider;
use alloy_rpc_types_eth::state::{AccountOverride, StateOverridesBuilder};
use alloy_sol_types::{sol, SolCall, SolValue};
use alloy_transport::TransportError;
#[derive(Debug, Clone)]
pub struct StorageSlotFinder<P, N>
where
N: Network,
{
provider: P,
contract: Address,
calldata: Bytes,
expected_value: U256,
base_request: N::TransactionRequest,
}
impl<P, N> StorageSlotFinder<P, N>
where
P: Provider<N>,
N: Network,
{
pub fn new(provider: P, contract: Address, calldata: Bytes, expected_value: U256) -> Self {
Self {
provider,
contract,
calldata,
expected_value,
base_request: N::TransactionRequest::default(),
}
}
pub fn balance_of(provider: P, token_address: Address, user: Address) -> Self {
sol! {
contract IERC20 {
function balanceOf(address target) external view returns (uint256);
}
}
let calldata = IERC20::balanceOfCall { target: user }.abi_encode().into();
Self::new(provider, token_address, calldata, U256::from(1337))
}
pub const fn with_expected_value(mut self, value: U256) -> Self {
self.expected_value = value;
self
}
pub fn with_request(mut self, base_request: N::TransactionRequest) -> Self {
self.base_request = base_request;
self
}
pub async fn find_slot(self) -> Result<Option<B256>, TransportError> {
let Self { provider, contract, calldata, expected_value, base_request } = self;
let tx = base_request.with_to(contract).with_input(calldata);
let access_list_result = provider.create_access_list(&tx).await?;
let access_list = access_list_result.access_list;
let mut any_call_succeeded = false;
let mut first_call_err: Option<TransportError> = None;
for item in access_list.0 {
if item.address != contract {
continue;
};
for slot in &item.storage_keys {
let account_override = AccountOverride::default().with_state_diff(std::iter::once(
(*slot, B256::from(expected_value.to_be_bytes())),
));
let state_override =
StateOverridesBuilder::default().append(contract, account_override).build();
let result = match provider.call(tx.clone()).overrides(state_override).await {
Ok(res) => {
any_call_succeeded = true;
res
}
Err(err) => {
first_call_err.get_or_insert(err);
continue;
}
};
let Ok(result_value) = U256::abi_decode(&result) else {
continue;
};
if result_value == expected_value {
return Ok(Some(*slot));
}
}
}
if !any_call_succeeded {
if let Some(err) = first_call_err {
return Err(err);
}
}
Ok(None)
}
}
#[cfg(test)]
mod tests {
use crate::StorageSlotFinder;
use alloy_network::TransactionBuilder;
use alloy_primitives::{address, Address, B256, U256};
use alloy_provider::{ext::AnvilApi, Provider, ProviderBuilder};
use alloy_rpc_types_eth::TransactionRequest;
use alloy_sol_types::sol;
const FORK_URL: &str = "https://ethereum.reth.rs/rpc";
use alloy_sol_types::SolCall;
async fn test_erc20_token_set_balance(token: Address) {
let provider = ProviderBuilder::new().connect_anvil_with_config(|a| a.fork(FORK_URL));
let user = address!("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
let amount = U256::from(500u64);
let finder = StorageSlotFinder::balance_of(provider.clone(), token, user);
let storage_slot = U256::from_be_bytes(finder.find_slot().await.unwrap().unwrap().0);
provider
.anvil_set_storage_at(token, storage_slot, B256::from(amount.to_be_bytes()))
.await
.unwrap();
sol! {
function balanceOf(address owner) view returns (uint256);
}
let balance_of_call = balanceOfCall::new((user,));
let input = balanceOfCall::abi_encode(&balance_of_call);
let result = provider
.call(TransactionRequest::default().with_to(token).with_input(input))
.await
.unwrap();
let balance = balanceOfCall::abi_decode_returns(&result).unwrap();
assert_eq!(balance, amount);
}
#[tokio::test]
async fn test_erc20_dai_set_balance() {
let dai = address!("0x6B175474E89094C44Da98b954EedeAC495271d0F");
test_erc20_token_set_balance(dai).await
}
#[tokio::test]
async fn test_erc20_usdc_set_balance() {
let usdc = address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
test_erc20_token_set_balance(usdc).await
}
#[tokio::test]
async fn test_erc20_tether_set_balance() {
let tether = address!("0xdAC17F958D2ee523a2206206994597C13D831ec7");
test_erc20_token_set_balance(tether).await
}
}