.slot - returns the slot location (in uint256) that the variable is located in. Determined at compile-time and doesn’t change
sstore 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 bit
1 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 masking
shift the new E value by 28 bytes and conduct bitwise or to combine the E value and current slot values together
call 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.
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
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