Memory

Solidity reserves four 32-byte slots, with specific byte ranges (inclusive of endpoints) being used as follows:

  • 0x00 - 0x3f (64 bytes): scratch space for hashing methods

  • 0x40 - 0x5f (32 bytes): currently allocated memory size (aka. free memory pointer)

  • 0x60 - 0x7f (32 bytes): zero slot

I made a diagram for easier memorization:

Scratch space can be used between statements (i.e. within inline assembly). The zero slot is used as initial value for dynamic memory arrays and should never be written to (the free memory pointer points to 0x80 initially).

Solidity always places new objects at the free memory pointer and memory is never freed (this might change in the future).

Memory is NOT bitpacked (in contrast with storage). For example, uint8[4] a occupies 4 * 32 = 128 bytes of space in memory but occupies 1 * 32 = 32 bytes in storage. The same is true for structs.

bytes/string is made of two parts:

  1. The first 32 bytes is the length of that bytes/string

  2. The actual value starts from the 33th byte

This fact was used in Ethernaut MagicNumber:

contract MagicNumberHack {
    constructor(MagicNum challenge) {
        // len(bytecode) = 19 = 0x13
        bytes memory bytecode = hex"69602a60005260206000f3600052600a6016f3";
        
        address addr;
        assembly {
            // create(value, offset, size)
            addr := create(0, add(bytecode, 0x20), 0x13)
        }
        
        // Verify if the contract was successfully deployed
        require(addr != address(0));

        // Interact with the challenge contract
        challenge.setSolver(addr);
    }
}

Here we used add(bytecode, 0x20) to get the actual value of bytecode since the first slot is its length and its actual value is in the second slot.

Last updated