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 _phantom: std::marker::PhantomData<N>,
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 const fn new(
65 provider: P,
66 contract: Address,
67 calldata: Bytes,
68 expected_value: U256,
69 ) -> Self {
70 Self { provider, contract, calldata, expected_value, _phantom: std::marker::PhantomData }
71 }
72
73 /// Convenience constructor for finding the storage slot of an ERC20 `balanceOf(address)`
74 /// mapping.
75 ///
76 /// Uses a default expected value of 1337. Call [`Self::with_expected_value`] to set a different
77 /// value.
78 ///
79 /// # Arguments
80 ///
81 /// * `provider` - The provider to use for making calls
82 /// * `token_address` - The address of the ERC20 token contract
83 /// * `user` - The address of the user whose balance slot we're finding
84 pub fn balance_of(provider: P, token_address: Address, user: Address) -> Self {
85 sol! {
86 contract IERC20 {
87 function balanceOf(address target) external view returns (uint256);
88 }
89 }
90 let calldata = IERC20::balanceOfCall { target: user }.abi_encode().into();
91 Self::new(provider, token_address, calldata, U256::from(1337))
92 }
93
94 /// Configures a specific value that should be used in the state override to identify the slot.
95 pub const fn with_expected_value(mut self, value: U256) -> Self {
96 self.expected_value = value;
97 self
98 }
99
100 /// Finds the storage slot containing the expected value.
101 ///
102 /// This method:
103 /// 1. Creates an access list for the function call to identify all storage slots accessed
104 /// 2. Iterates through each accessed slot on the target contract
105 /// 3. Overrides each slot with the expected value using state overrides
106 /// 4. Checks if the function returns the expected value when that slot is overridden
107 /// 5. Returns the first slot that causes the function to return the expected value
108 ///
109 /// # Returns
110 ///
111 /// * `Ok(Some(slot))` - The storage slot that contains the value
112 /// * `Ok(None)` - No storage slot was found containing the value
113 /// * `Err(TransportError)` - An error occurred during RPC calls
114 ///
115 /// # Note
116 ///
117 /// This method assumes that the value is stored directly in a storage slot without
118 /// any encoding or hashing. For mappings, the actual storage location might be
119 /// computed using keccak256 hashing.
120 pub async fn find_slot(self) -> Result<Option<B256>, TransportError> {
121 let tx = N::TransactionRequest::default()
122 .with_to(self.contract)
123 .with_input(self.calldata.clone());
124
125 // first collect all the slots that are used by the function call
126 let access_list_result = self.provider.create_access_list(&tx.clone()).await?;
127 let access_list = access_list_result.access_list;
128 // iterate over all the accessed slots and try to find the one that contains the
129 // target value by overriding the slot and checking the function call result
130 for item in access_list.0 {
131 if item.address != self.contract {
132 continue;
133 };
134 for slot in &item.storage_keys {
135 let account_override = AccountOverride::default().with_state_diff(std::iter::once(
136 (*slot, B256::from(self.expected_value.to_be_bytes())),
137 ));
138
139 let state_override = StateOverridesBuilder::default()
140 .append(self.contract, account_override)
141 .build();
142
143 let Ok(result) = self.provider.call(tx.clone()).overrides(state_override).await
144 else {
145 // overriding this slot failed
146 continue;
147 };
148
149 let Ok(result_value) = U256::abi_decode(&result) else {
150 // response returned something other than a U256
151 continue;
152 };
153
154 if result_value == self.expected_value {
155 return Ok(Some(*slot));
156 }
157 }
158 }
159 Ok(None)
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use crate::StorageSlotFinder;
166 use alloy_network::TransactionBuilder;
167 use alloy_primitives::{address, Address, B256, U256};
168 use alloy_provider::{ext::AnvilApi, Provider, ProviderBuilder};
169 use alloy_rpc_types_eth::TransactionRequest;
170 use alloy_sol_types::sol;
171 const FORK_URL: &str = "https://reth-ethereum.ithaca.xyz/rpc";
172 use alloy_sol_types::SolCall;
173
174 async fn test_erc20_token_set_balance(token: Address) {
175 let provider = ProviderBuilder::new().connect_anvil_with_config(|a| a.fork(FORK_URL));
176 let user = address!("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
177 let amount = U256::from(500u64);
178 let finder = StorageSlotFinder::balance_of(provider.clone(), token, user);
179 let storage_slot = U256::from_be_bytes(finder.find_slot().await.unwrap().unwrap().0);
180
181 provider
182 .anvil_set_storage_at(token, storage_slot, B256::from(amount.to_be_bytes()))
183 .await
184 .unwrap();
185
186 sol! {
187 function balanceOf(address owner) view returns (uint256);
188 }
189
190 let balance_of_call = balanceOfCall::new((user,));
191 let input = balanceOfCall::abi_encode(&balance_of_call);
192
193 let result = provider
194 .call(TransactionRequest::default().with_to(token).with_input(input))
195 .await
196 .unwrap();
197 let balance = balanceOfCall::abi_decode_returns(&result).unwrap();
198
199 assert_eq!(balance, amount);
200 }
201
202 #[tokio::test]
203 async fn test_erc20_dai_set_balance() {
204 let dai = address!("0x6B175474E89094C44Da98b954EedeAC495271d0F");
205 test_erc20_token_set_balance(dai).await
206 }
207
208 #[tokio::test]
209 async fn test_erc20_usdc_set_balance() {
210 let usdc = address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
211 test_erc20_token_set_balance(usdc).await
212 }
213
214 #[tokio::test]
215 async fn test_erc20_tether_set_balance() {
216 let tether = address!("0xdAC17F958D2ee523a2206206994597C13D831ec7");
217 test_erc20_token_set_balance(tether).await
218 }
219}