We reject abstraction
We believe in bytecode
Bytecode is only true form
Let’s code with bytes now
Welcome back to Bytecode Tuesday. Last week, we saw how your contract gets compiled, deployed, and executed on Ethereum. This week, we cover something even more fundamental: the three data regions of the EVM.
Let’s recall them from the last post:
Stack for last-in, first-out temporary values,
Memory for intra-transaction data (a byte array that resets every call), and
Storage slots for persistent state (a mapping of 32-byte keys to 32-byte values).
Now let’s break each one down
Think of the stack like a pile of plates. You can only work with the top one. It’s:
Last-In, First-Out (LIFO)
Can hold up to 1024 items (each 32 bytes)
Super fast and cheap to use
PUSH1 VALUE → put VALUE on the stack
Bytecode: 60VALUE // Replace VALUE with your desire value in hexadecimal (1 byte)
PUSH32 VALUE → put VALUE on the stack
Bytecode: 7FVALUE // Replace VALUE with your desired value in hexadecimal (32 bytes)
ADD → pop the top two values, add them, and push the result
Bytecode: 01
Example:
Which translates as
PUSH1 0x02
PUSH1 0x03
ADD
Let’s now see how the stack is being manipulated in the following step by step animation.
Memory is like RAM: it’s cleared after each transaction or call. You can use it to:
Store temporary values during execution
Return data to the caller (via RETURN)
Store arrays, strings, etc.
Some Relevant Instructions
MSTORE → store 32 bytes at a memory offset
Bytecode: 52
MLOAD → read 32 bytes from memory
Bytecode: 51
Example:
Which translates as:
PUSH1 0x2A // Value to store
PUSH1 0x00 // Memory offset
MSTORE // Save 0x2A at offset 0x00, since MSTORE stores 32 bytes and we just push 1 byte (0x2A), the EVM left-pads the 1-byte value with 31 zero bytes to make it 32 bytes. This padded value is then stored at the specified memory offset (in this case 0x00).
PUSH1 0x20 // Length to return
PUSH1 0x00 // Start offset
RETURN // Return memory [0x000….2A]
In the following animation notice how the memory operates based on stack inputs.
Each smart contract has its own storage, which is:
Persistent: Survives across transactions
Expensive: Writing to storage costs significant gas.
Private to the contract: One contract cannot directly access another’s storage.
The EVM storage maps data using 32-byte (256 bits) keys and values.
So, the storage can be visualized as a map like:
0x0000000000000000000000000000000000000000000000000000000000000000 → 0x... (32 bytes)
0x0000000000000000000000000000000000000000000000000000000000000001 → 0x... (32 bytes)
Ethereum uses 32 bytes words as the native unit for storage and computation. This design aligns naturally with the Keccak-256 hashing, which produces 32 bytes outputs. Since Ethereum relies heavily on this algorithm, for addresses, storage keys, and more, structuring everything around 32-byte chunks makes contract development efficient.
However, this design isn’t without tradeoffs. Storing small values, like a boolean or a single digit, still consumes the full 32 bytes, which means wasted space and higher gas costs. Developers often have to pack and unpack multiple values manually to use storage efficiently, adding complexity on both smart contracts and dApps frontends.
The EVM's 32-byte word size made sense for early Ethereum, especially given its focus on DeFi, tokens, and ownership primitives. But as the platform evolves into a general purpose execution layer, the limitations of this native word size become more apparent. There’s growing interest, including from Vitalik himself, in exploring a more flexible, byte oriented virtual machine that better supports new use cases and broader types of applications.
Some Relevant Instructions:
SSTORE → write value to a storage slot
Bytecode: 55
SLOAD → read from a storage slot
Bytecode: 54
Example:
Which translates as:
PUSH1 0x2A // value
PUSH1 0x00 // storage key
SSTORE // storage[0x000…00] = 0x2A, since SSTORE uses a 32-byte key and value, the EVM left-pads the 1-byte value with 31 zero bytes to make it 32 bytes in the same way as
PUSH1 0x00 // key
SLOAD // load from storage[0x000...00], as SSTORE, it left-pads the 1-byte key with 31 zero bytes
As you can see, the memory and the state operate very similarly, the main difference is the memory is addressed by byte pointers while the state by 32 byte slots.
Feature | Stack | Memory | Storage |
Lifetime | One instruction | One transaction | Forever |
Cost | Very cheap | Medium | Expensive |
Access | Only top values | Offset-based | Key-value |
Use cases | Math & logic | Return values | Contract state |
Write and check using this EVM Disassembler bytecode that:
Stores the number 42 in memory
Loads it back from memory
Returns it
Bonus: Can you also store it in storage, then load and return it?
Bytecode isn’t magic. It’s just a sequence of stack gymnastics mixed with clever use of memory and storage.
Next week on Bytecode Tuesday, we’ll explore control flow, how to change the flow of execution inside a smart contract. Subscribe now for more weekly bytes and opcodes.