State variables are stored in different "slots" in a contract, and each slot is 32 bytes (256 bits). However, multiple adjacent state variables declared in the contract may be packed into the same slot by the Solidity compiler if their combined size does not exceed 32 bytes.
When it comes to mappings and dynamic arrays, things get complicated. As an example, consider the following contract:
contract MyContract {
struct Market {
uint256 marketId; // slot: key's slot + 0
bytes32 priceFeedId; // slot: key's slot + 1
}
address public owner; // slot 0
uint256 public counter; // slot 1
mapping(uint256 => Market) public marketMap; // slot 2
constructor() {
owner = msg.sender;
}
function createMarket(uint256 marketId, bytes32 priceFeedId) external {
marketMap[marketId] = Market(marketId, priceFeedId);
}
function getMarket(uint256 marketId) external view returns (Market memory) {
return marketMap[marketId];
}
}
The elements (values) of a mapping are stored in different storage slots that are computed using keccak256()
with the key and the slot number of that mapping. Two arbitrary keys' slots are very unlikely to be adjacent since the slot number is derived from a hash function. Nevertheless, the slots of members within a struct are laid out sequentially, marketId
and priceFeedId
in the above case, so they will be adjacent.
You could use vm.load()
or stdstore.read()
to read values by slots directly in Foundry. Though the later method requires the state variable to be public.
import "forge-std/Test.sol";
contract MyTest is Test {
using stdStorage for StdStorage;
MyContract myContract;
uint256 ownerSlot = 0;
uint256 mappingSlot = 2;
MyContract.Market market;
uint256 key = 123;
function setUp() public {
myContract = new MyContract();
myContract.createMarket(123, 0x4567890000000000000000000000000000000000000000000000000000000000);
market = myContract.getMarket(key);
}
function test_VmLoad() public {
// slot 0
console.log(ownerSlot);
bytes32 ownerRaw = vm.load(address(myContract), bytes32(ownerSlot));
address owner = address(uint160(uint256(ownerRaw)));
console.logAddress(owner);
assertEq(myContract.owner(), owner);
uint256 keySlot = uint256(keccak256(abi.encode(key, mappingSlot)));
// slot 88533158270886526242754899650855253077067544535846224911147251622586185207028
uint256 marketIdSlotOffset = 0;
bytes32 marketIdSlot = bytes32(keySlot + marketIdSlotOffset);
console.log(uint256(marketIdSlot));
uint256 marketId = uint256(vm.load(address(myContract), marketIdSlot));
console.log(marketId);
assertEq(market.marketId, marketId);
// slot 88533158270886526242754899650855253077067544535846224911147251622586185207029
uint256 priceFeedIdSlotOffset = 1;
bytes32 priceFeedIdSlot = bytes32(keySlot + priceFeedIdSlotOffset);
console.log(uint256(priceFeedIdSlot));
bytes32 priceFeedId = vm.load(address(myContract), priceFeedIdSlot);
console.logBytes32(priceFeedId);
assertEq(market.priceFeedId, priceFeedId);
}
function test_StdStore() public {
// slot 0
console.log(ownerSlot);
address owner = stdstore.target(address(myContract)).sig("owner()").read_address();
console.logAddress(owner);
assertEq(myContract.owner(), owner);
// slot 88533158270886526242754899650855253077067544535846224911147251622586185207028
console.log(stdstore.target(address(myContract)).sig("marketMap(uint256)").with_key(key).depth(0).find());
uint256 marketId = stdstore
.target(address(myContract))
.sig("marketMap(uint256)")
.with_key(key)
.depth(0)
.read_uint();
console.log(marketId);
assertEq(market.marketId, marketId);
// slot 88533158270886526242754899650855253077067544535846224911147251622586185207029
console.log(stdstore.target(address(myContract)).sig("marketMap(uint256)").with_key(key).depth(1).find());
bytes32 priceFeedId = stdstore
.target(address(myContract))
.sig("marketMap(uint256)")
.with_key(key)
.depth(1)
.read_bytes32();
console.logBytes32(priceFeedId);
assertEq(market.priceFeedId, priceFeedId);
}
}
// run: forge test --mc MyTest -vv
ref:
https://docs.soliditylang.org/en/v0.8.19/internals/layout_in_storage.html
https://book.getfoundry.sh/cheatcodes/load
https://book.getfoundry.sh/reference/forge-std/std-storage
You could also find the slot numbers of each top-level state variables in OpenZepplin Upgrades Plugin's manifest file (the {network}.json
) if your contracts are upgradeable.
In addition to that, Foundry's cast
as well as provides a command to read storage at a certain slot:
// read the slot 2 (decimals) of WETH on Ethereum
// WETH slot 0: name
// WETH slot 1: symbol
// WETH slot 2: decimals
cast storage 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 2 --rpc-url https://rpc.flashbots.net/
ref:
https://book.getfoundry.sh/reference/cast/cast-storage
https://etherscan.io/address/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code