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}