Storage

Storage Basics

  • .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.

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