Skip to main content

Get Storage Layout

The Solidity documentation already provides an explanation of the Layout of State Variables in Storage. In order to further clarify this concept, I will provide a few examples to demonstrate how we can achieve the same result in Solidity. To do so, I will use the example contract provided in the documentation with a few modifications. I will add a dynamic array and a one-dimensional map to the existing contract.

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.16;


contract C {
mapping(uint256 => uint256) public test;
struct S { uint16 a; uint16 b; uint256 c; }
uint x;
mapping(uint => mapping(uint => S)) public data;
uint256[] public test2;

function settest(uint256 first, uint256 sec)public{
test[first] = sec;
}

function setdata(uint256 index1, uint256 index2, uint16 val1, uint16 val2, uint256 val3) external{
data[index1][index2] = S(val1, val2, val3);
}

function settest2(uint256 val) public{
test2.push(val);
}
}

Storage Layout Of Arrays

The Solidity documentation explains that array data is stored beginning at keccak256(p) and is laid out similarly to statically-sized array data. This means that array elements are stored consecutively, potentially sharing storage slots if their sizes are less than or equal to 16 bytes. By hashing the offset of the array with sha3, we can access the start index of the array. If integers are uint256, which is 32 bytes, then each integer will have its own storage slot and will not share any slot with other data. Types that are smaller than 32 bytes can share a single slot, such as two 16-byte integers sharing one slot.

Upon inspecting the contract, we can observe that the dynamic array variable test2 is located on slot 3 in the storage. This is determined by the fact that the variable test is located on slot 0, uint x is located on slot 1, and the variable data is located on slot 2. As per the Solidity documentation, dynamic arrays are located at keccak256(p) + x in the storage, where x represents the index of the array. Therefore, if we have a dynamic array with 4 elements and we want to access the third element, the value of x would be 2. It is important to note that if the elements of the array are less than 16 bytes, they may share storage slots.

>>> contr = C.deploy({'from': accounts[0]})
Transaction sent: 0x6f350c27da2954724af95d37b207d9973b8155d72fdbe346afba2ce3d4e13725
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 0
C.constructor confirmed Block: 1 Gas used: 210614 (1.76%)
C deployed at: 0x3194cBDC3dbcd3E11a07892e7bA5c3394048Cc87

>>> contr.settest2(15)
Transaction sent: 0x2be0712362c589b47c9b73ed89d62763932dd4ee77d1801bc2db0e8e2d0fb56b
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 1
C.settest2 confirmed Block: 2 Gas used: 62230 (0.52%)

<Transaction '0x2be0712362c589b47c9b73ed89d62763932dd4ee77d1801bc2db0e8e2d0fb56b'>
>>> contr.settest2(14)
Transaction sent: 0xdc3e2932196da5c171d1ed9e8f9f9a9b259fa04a7cf81853c202e6e95b8f23ab
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 2
C.settest2 confirmed Block: 3 Gas used: 47230 (0.39%)

<Transaction '0xdc3e2932196da5c171d1ed9e8f9f9a9b259fa04a7cf81853c202e6e95b8f23ab'>
>>> contr.settest2(13)
Transaction sent: 0x5ee1a4a6f90b3153a18427022db9eb87f2ffa3ba26756240db16068336129315
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 3
C.settest2 confirmed Block: 4 Gas used: 47230 (0.39%)

<Transaction '0x5ee1a4a6f90b3153a18427022db9eb87f2ffa3ba26756240db16068336129315'>
>>> from brownie import convert
>>> web3.eth.getStorageAt(contr.address, web3.toInt(web3.sha3(convert.to_bytes(3)))+0)
HexBytes('0x0f')
>>> web3.eth.getStorageAt(contr.address, web3.toInt(web3.sha3(convert.to_bytes(3)))+1)
HexBytes('0x0e')
>>> web3.eth.getStorageAt(contr.address, web3.toInt(web3.sha3(convert.to_bytes(3)))+2)
HexBytes('0x0d')
>>> 0x0f == 15
True
>>> contr.test2(0)
15
>>> contr.test2(1)
14
>>> contr.test2(2)
13

If the test2 variable was declared as private, we could still access it using the getStorageAt function. Since the dynamic array is located at slot 3, we can access all the variables using the algorithm web3.toInt(web3.sha3(convert.to_bytes(3))) + x. Here, x represents the index of the element we are looking for in the array. For example, if we are looking for the third element in an array of four elements, then x would be 2.

Storage Layout Of Maps

According to Solidity documentation, the layout for mapping values is determined by a hash function applied to the key of the mapping. Specifically, the value corresponding to a mapping key "k" is located at keccak256(h(k) . p), where "." denotes concatenation and "h" is a function applied to the key depending on its type. For value types, h pads the value to 32 bytes, while for strings and byte arrays, h(k) is simply the unpadded data. If the mapping value is a non-value type, the computed slot marks the start of the data.

For example, to access data[4][9].c in the provided contract, we need to first locate the position of data[4]. This is stored at keccak256(uint256(4) . uint256(1)), where the 1 represents the slot occupied by variable x. Since the type of data[4] is also a mapping, we can locate the position of data[4][9] at slot keccak256(uint256(9) . keccak256(uint256(4) . uint256(1))). Since the variables a and b are packed in a single slot, the slot offset of the member "c" inside the struct S is 1. Thus, the slot for data[4][9].c is located at keccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) + 1. Since the value is of type uint256, it uses a single slot. To access data[4], we need to getstorage at keccak256(uint256(4) . uint256(p)), where "p" represents the storage slot for the mapping itself.

>>> contr.settest(15, 123)
Transaction sent: 0x73bfe877954b9177238d9db42111f3eca5dd1f6a36bc03f68b0bc86d42eb2e0a
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 4
C.settest confirmed Block: 5 Gas used: 41695 (0.35%)

<Transaction '0x73bfe877954b9177238d9db42111f3eca5dd1f6a36bc03f68b0bc86d42eb2e0a'>
>>> contr.test(15)
123
>>> web3.eth.getStorageAt(contr.address, web3.sha3(convert.to_bytes(15)+ convert.to_bytes(0)))
HexBytes('0x7b')
>>> 0x7b
123

In the given example web3.sha3(convert.to_bytes(15)+ convert.to_bytes(0)), the value 15 represents the index of the desired element, while 0 represents the storage slot of the dynamic map variable test. By concatenating the index and the storage slot and hashing them with web3.sha3, we can obtain the storage location of the desired element.

>>> contr.setdata(15, 23, 1,2,3)
Transaction sent: 0x108420d43eb58c41d81acc17e08724456cbbf1369e0469faae77df2ee6e2cb05
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 5
C.setdata confirmed Block: 6 Gas used: 63397 (0.53%)

<Transaction '0x108420d43eb58c41d81acc17e08724456cbbf1369e0469faae77df2ee6e2cb05'>
>>> contr.data(15,23)
(1, 2, 3)
>>> web3.eth.getStorageAt(contr.address, web3.toInt(web3.sha3(convert.to_bytes(23)+web3.sha3(convert.to_bytes(15)+ convert.to_bytes(2)))))
HexBytes('0x020001')
>>> web3.eth.getStorageAt(contr.address, web3.toInt(web3.sha3(convert.to_bytes(23)+web3.sha3(convert.to_bytes(15)+ convert.to_bytes(2))))+1)
HexBytes('0x03')

In the given example, the value 23 represents the second index of the desired element, 15 represents the first index of the desired element, while 2 represents the storage slot of the dynamic map variable data. The result of web3.eth.getStorageAt(contr.address, web3.toInt(web3.sha3(convert.to_bytes(23)+web3.sha3(convert.to_bytes(15)+ convert.to_bytes(2))))) is 0x020001 because the first two elements of struct S (a and b) are uint16, so they are packed together in one storage slot. If we want to access data[15][23].c, then we need to add 1 to the result to obtain the second slot of that storage.