Skip to main content

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    _phantom: std::marker::PhantomData<N>,
48}
49
50impl<P, N> StorageSlotFinder<P, N>
51where
52    P: Provider<N>,
53    N: Network,
54{
55    /// Creates a new storage slot finder for a generic function call.
56    ///
57    /// # Arguments
58    ///
59    /// * `provider` - The provider to use for making calls
60    /// * `contract` - The address of the contract to analyze
61    /// * `calldata` - The encoded function call to execute
62    /// * `expected_value` - The value we expect the function to return
63    ///
64    /// For common ERC20 use cases, consider using [`Self::balance_of`] instead.
65    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    /// Convenience constructor for finding the storage slot of an ERC20 `balanceOf(address)`
77    /// mapping.
78    ///
79    /// Uses a default expected value of 1337. Call [`Self::with_expected_value`] to set a different
80    /// value.
81    ///
82    /// # Arguments
83    ///
84    /// * `provider` - The provider to use for making calls
85    /// * `token_address` - The address of the ERC20 token contract
86    /// * `user` - The address of the user whose balance slot we're finding
87    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    /// Configures a specific value that should be used in the state override to identify the slot.
98    pub const fn with_expected_value(mut self, value: U256) -> Self {
99        self.expected_value = value;
100        self
101    }
102
103    /// Overrides the base request object that will be used for slot detection.
104    ///
105    /// For slot detection the target address of that request is set to the configured contract and
106    /// the input to the configured input.
107    pub fn with_request(mut self, base_request: N::TransactionRequest) -> Self {
108        self.base_request = base_request;
109        self
110    }
111
112    /// Finds the storage slot containing the expected value.
113    ///
114    /// This method:
115    /// 1. Creates an access list for the function call to identify all storage slots accessed
116    /// 2. Iterates through each accessed slot on the target contract
117    /// 3. Overrides each slot with the expected value using state overrides
118    /// 4. Checks if the function returns the expected value when that slot is overridden
119    /// 5. Returns the first slot that causes the function to return the expected value
120    ///
121    /// # Returns
122    ///
123    /// * `Ok(Some(slot))` - The storage slot that contains the value
124    /// * `Ok(None)` - No storage slot was found containing the value
125    /// * `Err(TransportError)` - An error occurred during RPC calls
126    ///
127    /// # Note
128    ///
129    /// This method assumes that the value is stored directly in a storage slot without
130    /// any encoding or hashing. For mappings, the actual storage location might be
131    /// computed using keccak256 hashing.
132    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        // first collect all the slots that are used by the function call
136        let access_list_result = self.provider.create_access_list(&tx.clone()).await?;
137        let access_list = access_list_result.access_list;
138        // iterate over all the accessed slots and try to find the one that contains the
139        // target value by overriding the slot and checking the function call result
140        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                    // overriding this slot failed
156                    continue;
157                };
158
159                let Ok(result_value) = U256::abi_decode(&result) else {
160                    // response returned something other than a U256
161                    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"); // https://polygonscan.com/address/0xc2132D05D31c914a87C6611C10748AEb04B58e8F
234        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}