Storage
Storage Basics
.slot
- returns the slot location (in uint256) that the variable is located in. Determined at compile-time and doesn’t changesstore
has no consideration for existing variables already declared or stored in the contract, so using it may override values that are already there
contract YulStorage {
uint x = 2; // slot 0
uint y = 5; // slot 1
uint z = 10; // slot 2
function getXYul() external view returns (uint256 ret) {
assembly {
ret := sload(x.slot)
}
}
function getVarYul(uint256 slot) external view returns (bytes32 ret) {
assembly {
ret := sload(slot)
}
}
// risky, shouldn't be used unless you know what you're doing
function setVarYul(uint256 slot, uint256 value) external {
assembly {
sstore(slot, value)
}
}
}
Storage Offsets & Bitshifting
Multiple variables can be stored within a slot
contract StorageBits {
uint128 public C = 4;
uint96 public D = 6;
uint16 public E = 8;
uint8 public F = 1;
function readBySlot(uint256 slot) external view returns (bytes32 value) {
assembly {
value := sload(slot)
}
}
}
Calling readBySlot returns the below bytes32 data, where all variables C to F are stored in slot 0:
0x00
01 // F
0008 // E
000000000000000000000006 // D
00000000000000000000000000000004 // C
To derive the specific values with Yul, we have to use the
.offset
helper.offset
is the number of bytes to the left in a bytes32 object that we have to look in order to find the location of the variable we want
function getOffsetE() external pure returns (uint256 slot, uint256 offset) {
assembly {
slot := E.slot // returns 0
offset := E.offset // returns 28 -> shift 28 bytes to the left
}
}
To obtain the value of E:
shr(x, y)
- shift value y right by x number of bits
function readE() external view returns (uint256 e) {
assembly {
let value := sload(E.slot) // must load in 32 byte increments
// E.offset = 28
let shifted := shr(mul(E.offset, 8), value)
// 0x0000000000000000000000000000000000000000000000000000000000010008
// equivalent to
// 0x000000000000000000000000000000000000000000000000000000000000ffff
e := and(0xffff, shifted)
}
}
and
is a bitwise operation that compares values bit by bit1 and 0 returns 0
0 and F returns 0
0 and 8 returns 8 → we get
E = 8
In order to write to specific values in a slot (while not overriding all the values in the slot), we need to use bit masking & bit shifting
// masks can be hardcoded because variable storage slot and offsets are fixed
// V and 00 = 00
// V and FF = V
// V or 00 = V
// function arguments are always 32 bytes long under the hood
function writeToE(uint16 newE) external {
assembly {
// newE = 0x000000000000000000000000000000000000000000000000000000000000000a - decimal 10
let c := sload(E.slot) // slot 0
// c = 0x0001000800000000000000000000000600000000000000000000000000000004
let clearedE := and(
c,
0xffff0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff
)
// mask = 0xffff0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff
// c = 0x0001000800000000000000000000000600000000000000000000000000000004
// clearedE = 0x0001000000000000000000000000000600000000000000000000000000000004
let shiftedNewE := shl(mul(E.offset, 8), newE)
// shiftedNewE = 0x0000000a00000000000000000000000000000000000000000000000000000000
let newVal := or(shiftedNewE, clearedE)
// shiftedNewE = 0x0000000a00000000000000000000000000000000000000000000000000000000
// clearedE = 0x0001000000000000000000000000000600000000000000000000000000000004
// newVal = 0x0001000a00000000000000000000000600000000000000000000000000000004
sstore(C.slot, newVal)
}
}
first load the slot that contains E
selectively delete the bytes that represent E by using bitwise
and
to conduct maskingshift the new E value by 28 bytes and conduct bitwise
or
to combine the E value and current slot values togethercall
sstore
to set the new value into the storage slot
Storage of Arrays & Mappings
For fixed arrays, essentially the same as declaring 3
uint256
variables that will take up slots 0, 1 and 2.
contract StorageComplex {
uint256[3] fixedArray;
constructor() {
fixedArray = [99, 999, 9999];
}
function fixedArrayView(uint256 index) external view returns (uint256 ret) {
assembly {
ret := sload(add(fixedArray.slot, index))
// 0 - 99
// 1 - 999
// 2 - 9999
}
}
}
For dynamic arrays, the data that’s being stored in the storage slot is the length of the array.
Items in a dynamic array will not be stored sequentially down the slots, like in a fixed array, because the array could overrun and clash with other variables in the following slots
contract StorageComplex {
uint256[] bigArray;
constructor() {
bigArray = [10, 20, 30, 40];
}
// returns 4
function bigArrayLength() external view returns (uint256 ret) {
assembly {
ret := sload(bigArray.slot)
}
}
}
Solidity stores values of dynamic arrays by taking the
keccak256
hash of the array’s length, then adding the index of the value to the hash
function readBigArrayLocation(uint256 index)
external
view
returns (uint256 ret)
{
uint256 slot;
assembly {
slot := bigArray.slot
}
bytes32 location = keccak256(abi.encode(slot));
assembly {
ret := sload(add(location, index))
}
}
For mappings, Solidity takes the hash of the mapping’s storage slot, and concatenates it with the mapping key, then stores the value in the corresponding slot location
function getMapping(uint256 key) external view returns (uint256 ret) {
uint256 slot;
assembly {
slot := myMapping.slot
}
bytes32 location = keccak256(abi.encode(key, uint256(slot)));
assembly {
ret := sload(location)
}
}
Nested mappings: hashes of hashes
Keys have to be hashed in order of operation
mapping(uint256 => mapping(uint256 => uint256)) public nestedMapping;
constructor {
nestedMapping[2][4] = 7;
******}******
function getNestedMapping(uint256 key1, uint256 key2) external view returns (uint256 ret) {
uint256 slot;
assembly {
slot := nestedMapping.slot
}
bytes32 location = keccak256(
abi.encode(
uint256(key2),
keccak256(abi.encode(uint256(key1), uint256(slot)))
)
);
assembly {
ret := sload(location)
}
}
Last updated