Cover photo

EVM 101: Memory Layout

Smart contracts on Ethereum are written in a programming language called Solidity, which is a contract-oriented language with similarities to JavaScript and C++. Solidity code is compiled into bytecode that can be executed on the Ethereum Virtual Machine (EVM). Understanding how memory works in Ethereum smart contracts is essential for writing efficient and secure contracts. By carefully managing memory usage, developers can reduce gas costs and improve the performance of their contracts. In this gist, I will only discuss about memory layout.

Level: Intermediate

Memory is a temporary data storage area that is cleared after the execution of a contract. Memory is mainly used to store intermediate values during the execution of a contract and is bytes addressable, and each byte can store one value between 0 and 255.

When a contract is executed, memory is allocated dynamically as per smartcontracts. The contract can then read from and write to the memory as required. I have made a memory layout below indicating the empty contents of the memory, which gets filled as smartcontracts are executed. For e.g, a memory layout would look like below indicating empty memory data. This changes when we call a function in a smartcontract.

{
"0x0": "000...0000000",
"0x20": "000...000000",
"0x40": "000...0000000",
"0x60": "000...0000000",
"0x80": "000...0000000",
"0xa0": "000...0000000",
...
}

The below contract has a world() function, which uses assembly{} to load a free memory pointer (fmp) at location 0x40 which is 0x80. We are visualizing memory layout by storing a hex value of string "heythere" in memory and returning the integer type of stored hex using MLOAD.

// SPDX-License-Identifier: MIT
pragma solidity  >=0.8.2 <0.9.0;

contract hello {
 
  function world() external pure returns(uint256 data) {
        assembly {
            mstore(0x40, 0x80)
            let fmp := mload(0x40)
            // store a string - "hey there"
            mstore(add(fmp, 0x00), 0x6865797468657265)
            data := mload(add(fmp, 0x00))
        }
    }
}

The hex representation for "heythere" is 0x6865797468657265. Here, we are using MSTORE opcode to store 256-bit hex value at a free memory pointer location (fmp) with "0x00" offset. Once it gets stored, we read the hex using MLOAD at the same "fmp" location with 0x00 offset.

Then we return "data := mload(add(fmp, 0x00))" as `uint256` due to our function return type. This gives us integer version of stored hex "6865797468657265" from memory which in this case is 7522552293466927717.

If you don't want to change the return type and want the hex value only, you can use "bytes32 data" in place of "uint256 data", that way the output retains the hex value.

{
    "0": "uint256: data 7522552293466927717"
}

Here is an image showing the memory layout after the world() function executes, for you to understand:

{
    "0x0": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
    "0x20": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
    "0x40": "0000000000000000000000000000000000000000000000000000000000000080\t????????????????????????????????",
    "0x60": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
    "0x80": "0000000000000000000000000000000000000000000000006865797468657265\t????????????????????????heythere"
}

Now, according to solidity docs, 0x00 - 0x3f (64 bytes) is reserved for scratch space for hashing methods. This covers 0x0 <= 0x20 < 0x40 space.

0x40 - 0x5f (32 bytes) is reserved for free memory pointer location (fmp), In this case it's 0x80 and 0x60 - 0x7f (32 bytes) is used as a zero slot.

Assuming a starting position of 0x80, the data represented by the hexadecimal string "6865797468657265" is stored in the first 256-bit address space (imagine it like a slot 1 of memory). As additional data is added, it will be stored in the next available 256-bit address space, starting at the position 0xa0. This storage behavior is due to byte addressable memory organization, which allows data to be accessed and manipulated at the byte level.

I hope this article gave you a small gist about memory layout in Ethereum smart contracts. If you're interested in learning more about Solidity, EVM, Ethereum, and Assembly, I recommend checking out the official Solidity documentation and the Ethereum Yellow Paper.

Solidity Documentation: https://docs.soliditylang.org/ Ethereum Yellow Paper: https://ethereum.github.io/yellowpaper/paper.pdf