Stack manipulation is a necessary skill because everything starts there. However the stack has certain limitations that would restrict the flexibility of EVM if it was the only place to operate on values.
Each item in the stack is a fixed size of 32 bytes — we commonly show the values without zero-padding for clarity (a 32 byte value has 64 hex characters).
Each item in the stack is immutable — the stack can be reordered, items can be pushed and popped, and items can be copied. But once a value is on the stack, it cannot be modified.
Items on the stack are ephemeral — they are consumed and emitted by opcodes. It is not a place for durable storage of values.
The stack itself has a size limit (1024 items), and the SWAP & DUP opcodes reach to positions 1-16. An item at position 17 is effectively inaccessible until you pop an item.
To address many of these limitations, EVM provides access to memory. It is complementary but separate to the stack.
Part II of the series mentioned memory, but explicitly deferred discussion of it for later. Now is the time.
Memory is a dynamically allocated, volatile, but durable area to store and load values. Structurally, memory is represented as a bytes array, indexed in 32-byte chunks (words).
Values can be copied between the stack and memory using certain opcodes listed in The Yellow Paper Appendix H:
0x51 (MLOAD) takes a word from memory at some offset and places it onto the stack
0x52 (MSTORE) saves a word to memory at some offset
0x53 (MSTORE8) saves a single byte (8 bits) to memory at some offset
An interesting feature of the M-opcodes is that they can operate on offsets that do not have to be aligned with a word boundary. Where stack-only opcodes operate on implicitly ordered and word-aligned values, an M-opcode can read and write to a position that may cross two boundaries, or may operate on values that already exist.
This allows you to overwrite and extract arbitrary chunks of data from the memory array. It also allows you to manipulate data structures that exceed 32 bytes.
Importantly, calling other contracts and returning values can only be done using memory.
Memory Isn’t Fee
EVM assigns a gas fee whenever a new range of memory allocation is used by a call — “memory expansion”. A call begins with zero memory used, and a linear fee of 3 gas is charged on each new word up to a certain limit, then it becomes exponential. This value can only increase from zero, and does not decrease if a previously used memory region is cleared.
Indiscriminate memory expansion will lead to runaway gas costs, so some languages take care to reuse regions of memory if possible.
Memory in Solidity and Vyper
Memory is abstracted away by the Solidity and Vyper languages. The same limitations and behavior of the EVM apply, as described above, but nitty-gritty details about managing variables, moving items to and from the stack, and controlling the layout are left to the compiler.
Solidity
Solidity reserves four words for every contract per their documentation:
0x00 - 0x3f (64 bytes): scratch space for hashing methods
0x40 - 0x5f (32 bytes): a managed offset pointing to the next non-allocated memory location — start value is 0x80, the word boundary after the zero slot
0x60 - 0x7f (32 bytes): zero slot — the first variable is allocated here
A Solidity contract does not free memory or reuse regions after creation, so the free memory pointer should be treated as monotonically increasing.
Solidity offers access to raw memory via inline Yul, but you must be aware of the Solidity default behavior to avoid clobbering the expectations of the contract.
Vyper
Vyper exposes no control over memory, and allows no inline assembly, so you are both limited and protected by the decisions of the compiler.
Vyper takes a similar approach to Solidity with respect to the two word “scratch space”. Vyper differs because it dynamically reuses memory as it moves across different scopes, instead of continually adding and advancing like Solidity. Since Vyper offers no direct access to memory, it neither needs or provides a free memory pointer.
I recommend reading JTRiley’s deep dive on the Vyper compiler for more on this.
Playground Exploration
Open the evm.codes Playground and define a very simple contract:
[0x00] PUSH1 0x45 // 69
[0x02] PUSH0
[0x03] MSTORE
Run that and observe that 69 was pushed onto the stack (nice), then stored at offset 0 in memory. I will use [] to represent the stack, and {} to represent memory.
Final state:
[]
{
0x0000000000000000000000000000000000000000000000000000000000000045}
Note that the offset can be higher than zero if you want. Adjust the code and observe the behavior of the memory expansion described above:
[0x00] PUSH1 0x45 // 69
[0x02] PUSH1 0x01 // 1
[0x04] MSTORE
Final state:
[]
{
0000000000000000000000000000000000000000000000000000000000000000,
4500000000000000000000000000000000000000000000000000000000000000
}
We placed the 32-byte word at offset 1, which implies that the first offset (0) is empty (0x00). The least significant bits (0x45) fall at index 32, which is the beginning of the next word. Thus, EVM performs the memory expansion into the empty second word.
By using the lower boundary of the second word (32 bytes), we could place our value aligned within the second word of the memory array. The built-in memory expansion should engage, making the empty first word accessible:
[0x00] PUSH1 0x45 // 69
[0x02] PUSH1 0x20 // 32
[0x04] MSTORE
Final state:
[]
{
0000000000000000000000000000000000000000000000000000000000000000,
0000000000000000000000000000000000000000000000000000000000000045
}
While-Loop (Memory Remix)
I said that memory is durable above, but didn’t explain what that means. Unlike the stack, a value loaded from memory is not consumed by the associated opcode. Rather, the value is placed on the stack, but the associated memory is untouched. Therefore, the DUP tricks we played in Part II are optional, and the value can be retrieved anytime.
Let’s rewrite the stack-only while loop to access the loop counter using memory instead of juggling it on the stack:
Keep reading with a 7-day free trial
Subscribe to Degen Code to keep reading this post and get 7 days of free access to the full post archives.