Shedding smart contract storage with Slither

By Troy Sargent, Blockchain Security Engineer

You think you’ve found a critical bug in a Solidity smart contract that, if exploited, could drain a widely used cryptocurrency exchange’s funds. To confirm that it’s really a bug, you need to figure out the value at an obscure storage slot that has no getter method. Adrenaline courses through your body, followed by the pang of devastation when you lay your eyes on the Solidity storage documentation:

Your momentum crashes as you try to interpret these hieroglyphics, knowing that every wasted second could be catastrophic. Fortunately, slither-read-storage, a tool that retrieves storage slots from source code, can save you from this nightmare.

What is slither-read-storage?

slither-read-storage retrieves storage slots from source code by using information from Slither’s underlying type analysis (e.g., whether an array is fixed or dynamic) to inform slot calculations.

The tool can retrieve the storage slot(s) of a single variable or of entire contracts, and storage values can be retrieved by providing an Ethereum RPC.

slither-read storage is included with the latest release of Slither, version 0.8.3, and can be installed with Python’s package manager, pip:

pip install slither-analyzer==0.8.3

Use cases for slither-read-storage

Let’s explore some use cases for this tool.

Moonlight auditing

To determine all the addresses that can mint FRAX, we would have to manually input indices of the frax_pools_array one-by-one on Etherscan. Since frax_pools_array is dynamic, it would be helpful to know the current length of the array, which is not readily available. Finding these addresses in this way would be very time consuming, and we might waste time inputting indices that are out-of-bounds.

Querying frax_pools_array on Etherscan

Instead, we can run slither-read-storage on the FRAXStablecoin contract’s address to find the length of frax_pools_array:

slither-read-storage 0x853d955aCEf822Db058eb8505911ED77F175b99e --variable-name frax_pools_array --rpc-url $RPC_URL --value

Great! The length of a dynamic array is stored in one of the contract’s slots—in this case, slot 18. Examining the value at FRAXStablecoin’s slot 18, we can see that the length of the array is 25.

INFO:Slither-read-storage:
Contract 'FRAXStablecoin'
FRAXStablecoin.frax_pools_array with type address[] is located at slot: 18
INFO:Slither-read-storage:
Name: frax_pools_array
Type: address[]
Slot: 18
INFO:Slither-read-storage:
Value: 25

Now, we can retrieve the entirety of the FRAXStablecoin storage and specify the --max-depth 25 flag to pass the maximum depth of the data structures that we want slither-read-storage to return:

slither-read-storage 0x853d955aCEf822Db058eb8505911ED77F175b99e --layout  --rpc-url $RPC_URL --value --max-depth 25

The tool will then produce a JSON file with the storage layout and values, but we’re only interested in frax_pools_array. As of this writing, the tool retrieves 25 elements indicating the addresses that can mint FRAX.

   "frax_pools_array": {
"type_string": "address[]",
"slot": 18,
"size": 256,
"offset": 0,
“value”: 25,
"elems": {
// snip
"23": {
"type_string": "address",
"slot": 84827061063453624289975705683721713058963870421084015214609271099009937454171,
"size": 160,
"offset": 0,
"value": "0x36a0B6a5F7b318A2B4Af75FFFb1b51a5C78dEB8C"
},
"24": {
"type_string": "address",
"slot": 84827061063453624289975705683721713058963870421084015214609271099009937454172,
"size": 160,
"offset": 0,
"value": "0xcf37B62109b537fa0Cb9A90Af4CA72f6fb85E241"
}
}

Arbitrage bots

Aside from moonlight auditing on Etherscan, slither-read-storage can also be used to improve a program’s speed. For example, we can use slither-read-storage to have the program directly access an Ethereum node’s database rather than processing RPC calls. This is especially useful for contracts that do not provide a view function to retrieve the desired variable.

For instance, let’s say an arbitrage bot frequently reads the member sqrtPriceX96 of the variable slot0 on the WETH/USDC Uniswap V3 pool (see the data structure below).

struct Slot0 {
// the current price
uint160 sqrtPriceX96;
// the current tick
int24 tick;
// the most-recently updated index of the observations array
uint16 observationIndex;
// the current maximum number of observations that are being stored
uint16 observationCardinality;
// the next maximum number of observations to store, triggered in observations.write
uint16 observationCardinalityNext;
// the current protocol fee as a percentage of the swap fee taken on withdrawal
// represented as an integer denominator (1/x)%
uint8 feeProtocol;
// whether the pool is locked
bool unlocked;
}

Instead of calling the provided view function, we can use slither-read-storage to compute the slot as follows:

slither-read-storage 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 --layout

The file produced by the tool contains the slot, size, and offset of sqrtPriceX96, which can be easily retrieved from an Ethereum node’s key-value store and sliced based on the size and offset. It turns out that the Uniswap developers aptly named this variable slot0, but this is rarely available in practice.

{
"slot0": {
"type_string": "UniswapV3Pool.Slot0",
"slot": 0,
"size": 256,
"offset": 0,
"elems": {
"sqrtPriceX96": {
"type_string": "uint160",
"slot": 0,
"size": 160,
"offset": 0
},
/// snip

Additionally, one can modify storage values using the Ethereum node RPC, eth_call, by passing in the storage slot and desired value and simulating how transactions applied to the modified state are affected. Greater detail on how to accomplish this can be found in this tutorial.

Portfolio tracking

The balance slot of this account can be found using the following slither-read-storage command with the token address as the target and the account address as the key:

slither-read-storage 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984 --variable-name balances --key 0xab5801a7d398351b8be11c439e05c5b3259aec9b --rpc-url $RPC_URL --value

In this specific instance, the slot is the address of the account we intend to retrieve, 0xab5801a7d398351b8be11c439e05c5b3259aec9b, along with slot 9, padded to 32 bytes:

000000000000000000000000ab5801a7d398351b8be11c439e05c5b3259aec9b0000000000000000000000000000000000000000000000000000000000000009

This value is hashed with keccak256, and the balance is written at the resulting slot using SSTORE(slot, value).

Upgradeable ERC20 token

A normal smart contract has contract logic and storage at the same address. However, when the delegatecall proxy pattern is used (allowing the contract to be upgradeable), the proxy calls the implementation, and the storage values are written to the proxy contract. That is, the DELEGATECALL opcode writes to the storage at the caller’s address using the storage information of the logic contract. To accommodate this pattern, the --storage-address flag is required to retrieve the balance slot for the same address:

slither-read-storage 0xa2327a938Febf5FEC13baCFb16Ae10EcBc4cbDCF --variable-name balances --key 0xab5801a7d398351b8be11c439e05c5b3259aec9b --storage-address 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 --rpc-url $RPC_URL --value

Closing thoughts

I learned a lot about Solidity’s handling of storage and Slither’s API by building this tool, and its release completes work that I started before joining Trail of Bits as an apprentice. In fact, I can attribute my invaluable experience as an apprentice here to discussing Slither on Twitter and finding issues in Slither while working on the first iteration of this tool.

If you’re keen on doing the same, check out our GitHub and grab an open issue. We’d love to help new contributors.

Leave a Reply