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,
_phantom: std::marker::PhantomData<N>,
}
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(),
_phantom: std::marker::PhantomData,
}
}
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 tx = self.base_request.clone().with_to(self.contract).with_input(self.calldata.clone());
let access_list_result = self.provider.create_access_list(&tx.clone()).await?;
let access_list = access_list_result.access_list;
for item in access_list.0 {
if item.address != self.contract {
continue;
};
for slot in &item.storage_keys {
let account_override = AccountOverride::default().with_state_diff(std::iter::once(
(*slot, B256::from(self.expected_value.to_be_bytes())),
));
let state_override = StateOverridesBuilder::default()
.append(self.contract, account_override)
.build();
let Ok(result) = self.provider.call(tx.clone()).overrides(state_override).await
else {
continue;
};
let Ok(result_value) = U256::abi_decode(&result) else {
continue;
};
if result_value == self.expected_value {
return Ok(Some(*slot));
}
}
}
Ok(None)
}
}
#[cfg(test)]
mod tests {
use crate::StorageSlotFinder;
use alloy_network::TransactionBuilder;
use alloy_primitives::{address, ruint::uint, 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://reth-ethereum.ithaca.xyz/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
}
#[tokio::test]
async fn test_erc20_token_polygon() {
let provider =
ProviderBuilder::new().connect_http("https://polygon-rpc.com".parse().unwrap());
let usdt = address!("0xc2132D05D31c914a87C6611C10748AEb04B58e8F"); let user = address!("0x0aD71c9106455801eAe0e11D5A1Dd5232537E662");
let finder = StorageSlotFinder::balance_of(provider.clone(), usdt, user)
.with_request(TransactionRequest::default().gas_limit(100000));
let storage_slot = U256::from_be_bytes(finder.find_slot().await.unwrap().unwrap().0);
assert_eq!(
storage_slot,
uint!(
38414845661641411266428303013962925072609060211040678298987263275302781786590_U256
)
);
}
}