alloy_contract/storage_slot.rs
1use 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/// A utility for finding storage slots in smart contracts, particularly useful for ERC20 tokens.
9///
10/// This struct helps identify which storage slot contains a specific value by:
11/// 1. Creating an access list to find all storage slots accessed by a function call
12/// 2. Systematically overriding each slot with an expected value
13/// 3. Checking if the function returns the expected value to identify the correct slot
14///
15/// # Example
16///
17/// ```no_run
18/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
19/// use alloy_contract::StorageSlotFinder;
20/// use alloy_primitives::{address, U256};
21/// use alloy_provider::ProviderBuilder;
22///
23/// let provider = ProviderBuilder::new().connect_anvil();
24/// let token = address!("0x6B175474E89094C44Da98b954EedeAC495271d0F");
25/// let user = address!("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
26///
27/// // Find the storage slot for a user's balance
28/// let finder =
29/// StorageSlotFinder::balance_of(provider, token, user).with_expected_value(U256::from(1000));
30///
31/// if let Some(slot) = finder.find_slot().await? {
32/// println!("Balance stored at slot: {:?}", slot);
33/// }
34/// # Ok(())
35/// # }
36/// ```
37#[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}
48
49impl<P, N> StorageSlotFinder<P, N>
50where
51 P: Provider<N>,
52 N: Network,
53{
54 /// Creates a new storage slot finder for a generic function call.
55 ///
56 /// # Arguments
57 ///
58 /// * `provider` - The provider to use for making calls
59 /// * `contract` - The address of the contract to analyze
60 /// * `calldata` - The encoded function call to execute
61 /// * `expected_value` - The value we expect the function to return
62 ///
63 /// For common ERC20 use cases, consider using [`Self::balance_of`] instead.
64 pub fn new(provider: P, contract: Address, calldata: Bytes, expected_value: U256) -> Self {
65 Self {
66 provider,
67 contract,
68 calldata,
69 expected_value,
70 base_request: N::TransactionRequest::default(),
71 }
72 }
73
74 /// Convenience constructor for finding the storage slot of an ERC20 `balanceOf(address)`
75 /// mapping.
76 ///
77 /// Uses a default expected value of 1337. Call [`Self::with_expected_value`] to set a different
78 /// value.
79 ///
80 /// # Arguments
81 ///
82 /// * `provider` - The provider to use for making calls
83 /// * `token_address` - The address of the ERC20 token contract
84 /// * `user` - The address of the user whose balance slot we're finding
85 pub fn balance_of(provider: P, token_address: Address, user: Address) -> Self {
86 sol! {
87 contract IERC20 {
88 function balanceOf(address target) external view returns (uint256);
89 }
90 }
91 let calldata = IERC20::balanceOfCall { target: user }.abi_encode().into();
92 Self::new(provider, token_address, calldata, U256::from(1337))
93 }
94
95 /// Configures a specific value that should be used in the state override to identify the slot.
96 pub const fn with_expected_value(mut self, value: U256) -> Self {
97 self.expected_value = value;
98 self
99 }
100
101 /// Overrides the base request object that will be used for slot detection.
102 ///
103 /// For slot detection the target address of that request is set to the configured contract and
104 /// the input to the configured input.
105 pub fn with_request(mut self, base_request: N::TransactionRequest) -> Self {
106 self.base_request = base_request;
107 self
108 }
109
110 /// Finds the storage slot containing the expected value.
111 ///
112 /// This method:
113 /// 1. Creates an access list for the function call to identify all storage slots accessed
114 /// 2. Iterates through each accessed slot on the target contract
115 /// 3. Overrides each slot with the expected value using state overrides
116 /// 4. Checks if the function returns the expected value when that slot is overridden
117 /// 5. Returns the first slot that causes the function to return the expected value
118 ///
119 /// # Returns
120 ///
121 /// * `Ok(Some(slot))` - The storage slot that contains the value
122 /// * `Ok(None)` - No storage slot was found containing the value
123 /// * `Err(TransportError)` - An error occurred during RPC calls
124 ///
125 /// # Note
126 ///
127 /// This method assumes that the value is stored directly in a storage slot without
128 /// any encoding or hashing. For mappings, the actual storage location might be
129 /// computed using keccak256 hashing.
130 pub async fn find_slot(self) -> Result<Option<B256>, TransportError> {
131 let Self { provider, contract, calldata, expected_value, base_request } = self;
132
133 let tx = base_request.with_to(contract).with_input(calldata);
134
135 // first collect all the slots that are used by the function call
136 let access_list_result = provider.create_access_list(&tx).await?;
137 let access_list = access_list_result.access_list;
138
139 // Track whether any call succeeded and capture the first error for diagnostics.
140 // If all overridden calls fail, we propagate the first error instead of returning Ok(None).
141 let mut any_call_succeeded = false;
142 let mut first_call_err: Option<TransportError> = None;
143
144 // iterate over all the accessed slots and try to find the one that contains the
145 // target value by overriding the slot and checking the function call result
146 for item in access_list.0 {
147 if item.address != contract {
148 continue;
149 };
150 for slot in &item.storage_keys {
151 let account_override = AccountOverride::default().with_state_diff(std::iter::once(
152 (*slot, B256::from(expected_value.to_be_bytes())),
153 ));
154
155 let state_override =
156 StateOverridesBuilder::default().append(contract, account_override).build();
157
158 let result = match provider.call(tx.clone()).overrides(state_override).await {
159 Ok(res) => {
160 any_call_succeeded = true;
161 res
162 }
163 Err(err) => {
164 first_call_err.get_or_insert(err);
165 continue;
166 }
167 };
168
169 let Ok(result_value) = U256::abi_decode(&result) else {
170 // response returned something other than a U256
171 continue;
172 };
173
174 if result_value == expected_value {
175 return Ok(Some(*slot));
176 }
177 }
178 }
179
180 // If no call succeeded and we have an error, propagate it rather than silently returning
181 // None
182 if !any_call_succeeded {
183 if let Some(err) = first_call_err {
184 return Err(err);
185 }
186 }
187
188 Ok(None)
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use crate::StorageSlotFinder;
195 use alloy_network::TransactionBuilder;
196 use alloy_primitives::{address, Address, B256, U256};
197 use alloy_provider::{ext::AnvilApi, Provider, ProviderBuilder};
198 use alloy_rpc_types_eth::TransactionRequest;
199 use alloy_sol_types::sol;
200 const FORK_URL: &str = "https://ethereum.reth.rs/rpc";
201 use alloy_sol_types::SolCall;
202
203 async fn test_erc20_token_set_balance(token: Address) {
204 let provider = ProviderBuilder::new().connect_anvil_with_config(|a| a.fork(FORK_URL));
205 let user = address!("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
206 let amount = U256::from(500u64);
207 let finder = StorageSlotFinder::balance_of(provider.clone(), token, user);
208 let storage_slot = U256::from_be_bytes(finder.find_slot().await.unwrap().unwrap().0);
209
210 provider
211 .anvil_set_storage_at(token, storage_slot, B256::from(amount.to_be_bytes()))
212 .await
213 .unwrap();
214
215 sol! {
216 function balanceOf(address owner) view returns (uint256);
217 }
218
219 let balance_of_call = balanceOfCall::new((user,));
220 let input = balanceOfCall::abi_encode(&balance_of_call);
221
222 let result = provider
223 .call(TransactionRequest::default().with_to(token).with_input(input))
224 .await
225 .unwrap();
226 let balance = balanceOfCall::abi_decode_returns(&result).unwrap();
227
228 assert_eq!(balance, amount);
229 }
230
231 #[tokio::test]
232 async fn test_erc20_dai_set_balance() {
233 let dai = address!("0x6B175474E89094C44Da98b954EedeAC495271d0F");
234 test_erc20_token_set_balance(dai).await
235 }
236
237 #[tokio::test]
238 async fn test_erc20_usdc_set_balance() {
239 let usdc = address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
240 test_erc20_token_set_balance(usdc).await
241 }
242
243 #[tokio::test]
244 async fn test_erc20_tether_set_balance() {
245 let tether = address!("0xdAC17F958D2ee523a2206206994597C13D831ec7");
246 test_erc20_token_set_balance(tether).await
247 }
248}