alloy_contract/
storage_slot.rs1use alloy_network::{Network, TransactionBuilder};
2use alloy_primitives::{Address, Bytes, B256, U256};
3use alloy_provider::Provider;
4use alloy_rpc_types_eth::state::{AccountOverride, StateOverridesBuilder};
5use alloy_sol_types::{sol, SolCall, SolValue};
6use alloy_transport::TransportError;
7
8#[derive(Debug, Clone)]
38pub struct StorageSlotFinder<P, N>
39where
40 N: Network,
41{
42 provider: P,
43 contract: Address,
44 calldata: Bytes,
45 expected_value: U256,
46 base_request: N::TransactionRequest,
47 _phantom: std::marker::PhantomData<N>,
48}
49
50impl<P, N> StorageSlotFinder<P, N>
51where
52 P: Provider<N>,
53 N: Network,
54{
55 pub fn new(provider: P, contract: Address, calldata: Bytes, expected_value: U256) -> Self {
66 Self {
67 provider,
68 contract,
69 calldata,
70 expected_value,
71 base_request: N::TransactionRequest::default(),
72 _phantom: std::marker::PhantomData,
73 }
74 }
75
76 pub fn balance_of(provider: P, token_address: Address, user: Address) -> Self {
88 sol! {
89 contract IERC20 {
90 function balanceOf(address target) external view returns (uint256);
91 }
92 }
93 let calldata = IERC20::balanceOfCall { target: user }.abi_encode().into();
94 Self::new(provider, token_address, calldata, U256::from(1337))
95 }
96
97 pub const fn with_expected_value(mut self, value: U256) -> Self {
99 self.expected_value = value;
100 self
101 }
102
103 pub fn with_request(mut self, base_request: N::TransactionRequest) -> Self {
108 self.base_request = base_request;
109 self
110 }
111
112 pub async fn find_slot(self) -> Result<Option<B256>, TransportError> {
133 let tx = self.base_request.clone().with_to(self.contract).with_input(self.calldata.clone());
134
135 let access_list_result = self.provider.create_access_list(&tx.clone()).await?;
137 let access_list = access_list_result.access_list;
138 for item in access_list.0 {
141 if item.address != self.contract {
142 continue;
143 };
144 for slot in &item.storage_keys {
145 let account_override = AccountOverride::default().with_state_diff(std::iter::once(
146 (*slot, B256::from(self.expected_value.to_be_bytes())),
147 ));
148
149 let state_override = StateOverridesBuilder::default()
150 .append(self.contract, account_override)
151 .build();
152
153 let Ok(result) = self.provider.call(tx.clone()).overrides(state_override).await
154 else {
155 continue;
157 };
158
159 let Ok(result_value) = U256::abi_decode(&result) else {
160 continue;
162 };
163
164 if result_value == self.expected_value {
165 return Ok(Some(*slot));
166 }
167 }
168 }
169 Ok(None)
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use crate::StorageSlotFinder;
176 use alloy_network::TransactionBuilder;
177 use alloy_primitives::{address, ruint::uint, Address, B256, U256};
178 use alloy_provider::{ext::AnvilApi, Provider, ProviderBuilder};
179 use alloy_rpc_types_eth::TransactionRequest;
180 use alloy_sol_types::sol;
181 const FORK_URL: &str = "https://reth-ethereum.ithaca.xyz/rpc";
182 use alloy_sol_types::SolCall;
183
184 async fn test_erc20_token_set_balance(token: Address) {
185 let provider = ProviderBuilder::new().connect_anvil_with_config(|a| a.fork(FORK_URL));
186 let user = address!("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
187 let amount = U256::from(500u64);
188 let finder = StorageSlotFinder::balance_of(provider.clone(), token, user);
189 let storage_slot = U256::from_be_bytes(finder.find_slot().await.unwrap().unwrap().0);
190
191 provider
192 .anvil_set_storage_at(token, storage_slot, B256::from(amount.to_be_bytes()))
193 .await
194 .unwrap();
195
196 sol! {
197 function balanceOf(address owner) view returns (uint256);
198 }
199
200 let balance_of_call = balanceOfCall::new((user,));
201 let input = balanceOfCall::abi_encode(&balance_of_call);
202
203 let result = provider
204 .call(TransactionRequest::default().with_to(token).with_input(input))
205 .await
206 .unwrap();
207 let balance = balanceOfCall::abi_decode_returns(&result).unwrap();
208
209 assert_eq!(balance, amount);
210 }
211
212 #[tokio::test]
213 async fn test_erc20_dai_set_balance() {
214 let dai = address!("0x6B175474E89094C44Da98b954EedeAC495271d0F");
215 test_erc20_token_set_balance(dai).await
216 }
217
218 #[tokio::test]
219 async fn test_erc20_usdc_set_balance() {
220 let usdc = address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
221 test_erc20_token_set_balance(usdc).await
222 }
223
224 #[tokio::test]
225 async fn test_erc20_tether_set_balance() {
226 let tether = address!("0xdAC17F958D2ee523a2206206994597C13D831ec7");
227 test_erc20_token_set_balance(tether).await
228 }
229 #[tokio::test]
230 async fn test_erc20_token_polygon() {
231 let provider =
232 ProviderBuilder::new().connect_http("https://polygon-rpc.com".parse().unwrap());
233 let usdt = address!("0xc2132D05D31c914a87C6611C10748AEb04B58e8F"); let user = address!("0x0aD71c9106455801eAe0e11D5A1Dd5232537E662");
235 let finder = StorageSlotFinder::balance_of(provider.clone(), usdt, user)
236 .with_request(TransactionRequest::default().gas_limit(100000));
237 let storage_slot = U256::from_be_bytes(finder.find_slot().await.unwrap().unwrap().0);
238 assert_eq!(
239 storage_slot,
240 uint!(
241 38414845661641411266428303013962925072609060211040678298987263275302781786590_U256
242 )
243 );
244 }
245}