Smart Contracts: Immutability, Storage Slots, and the ABI

It's not 'Code is Law.' It's 'Code is Compiled Bytecode.' Deep dive into EVM storage layout, Function Selectors, and ABI encoding.

Beginner 50 min read Expert Version →

🎯 What You'll Learn

  • Visualizing EVM Storage Layout (Slots)
  • Decoding Function Selectors (0xa9059cbb)
  • Analyzing Immutability Risks (Proxy patterns)
  • Tracing the Checks-Effects-Interactions pattern
  • Reading storage directly with `cast`

Introduction

A “Smart Contract” is a misleading name. It is neither smart nor a contract. It is Immutable Bytecode living at a specific address on the Global State Machine.

When you “deploy” a contract, you are burning code into the blockchain’s history forever. When you “interact” with it (call a function), you are sending a specific hexadecimal payload that triggers a jump in the program counter.

In this lesson, we will stop treating contracts like magic scripts and start treating them like EVM Bytecode.


The Physics: EVM Storage Slots

Variables in Solidity are not stored in “memory” like RAM. They are stored in 256-bit Storage Slots on the blockchain’s hard drive (State Trie).

This storage is expensive ($20 per write). Packed appropriately, it saves thousands.

Visualization: The Slot Map

contract StorageExample {
    uint256 public val1;      // Slot 0 (32 bytes)
    uint128 public val2;      // Slot 1 (16 bytes) \__ Packed into Slot 1
    uint128 public val3;      // Slot 1 (16 bytes) /
    address public owner;     // Slot 2 (20 bytes)
}

Reading Slots Directly

You don’t need a “getter function” to read private variables. You just need to query the slot.

# Using Foundry's cast tool (or web3.eth.getStorageAt)
cast storage 0xContractAddress 0
> 0x0000000000000000000000000000000000000000000000000000000000000abc (Value of val1)

Lesson: Nothing is private on the blockchain. private only means “other contracts can’t call it.” Humans can always read it.


The Interface: ABI & Function Selectors

How does the EVM know which function to run? It looks at the first 4 bytes of your transaction data.

The Function Selector

Selector=keccak256("transfer(address,uint256)")[0:4]Selector = \text{keccak256("transfer(address,uint256)")}[0:4]

  • transfer(address,uint256) -> 0xa9059cbb
  • approve(address,uint256) -> 0x095ea7b3

The Payload (ABI Encoding)

The rest of the transaction data is the arguments, padded to 32 bytes (256 bits).

Example Transaction Data: 0xa9059cbb (Function: transfer) 000000000000000000000000abc123... (Argument 1: Recipient Address) 000000000000000000000000000000...01 (Argument 2: Amount = 1 wei)


Security Pattern: Checks-Effects-Interactions

The DAO Hack happened because this pattern was violated. Reentrancy occurs when you surrender control flow to an external contract before updating your own internal state.

❌ The Vulnerable Pattern

function withdraw() public {
    uint amount = balances[msg.sender];
    
    // 1. Interaction (External Call)
    (bool success, ) = msg.sender.call{value: amount}(""); 
    
    // 2. Effect (State Update)
    balances[msg.sender] = 0; 
}

The Hack: The msg.sender receives the ETH, and their fallback() function instantly calls withdraw() again. Since balances hasn’t been set to 0 yet, they drain the vault.

✅ The Secure Pattern

function withdraw() public {
    // 1. Checks
    uint amount = balances[msg.sender];
    require(amount > 0);

    // 2. Effects (Optimistic Update)
    balances[msg.sender] = 0;

    // 3. Interactions
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);
}

Practice Exercises

Exercise 1: Slot Packing (Intermediate)

Scenario: You have 3 variables: uint64 a, uint256 b, uint64 c. Task: Order them to minimize storage usage (Gas Optimization). (Hint: Slots are 256 bits wide).

Exercise 2: Manual Decoding (Advanced)

Scenario: Tx Data: 0x70a08231000000000000000000000000... Task: Identify the function. (Hint: It’s a standard ERC-20 read function).

Exercise 3: Proxy Storage Collision (Expert)

Scenario: You are using an Upgradable Proxy. The Logic Contract creates a variable uint x at Slot 0. The Proxy Contract also has a variable address impl at Slot 0. Task: Explain what happens when you write to x. (This is a catastrophic bug).


Knowledge Check

  1. What is a Function Selector?
  2. Why is private visibility a lie?
  3. Why should you update balances before sending ETH?
  4. How many bytes is a storage slot?
  5. What happens if you deploy code with a bug?
Answers
  1. The first 4 bytes of the Keccak hash of the function signature. It tells the EVM which code to run.
  2. State is public. private only restricts internal solidity calls. Use cast storage to read anything.
  3. To prevent Reentrancy. If you update after, an attacker can re-call the function before the balance is zeroed.
  4. 32 Bytes (256 bits).
  5. It is there forever. You cannot patch it. You must deploy a new contract and migrate all state (impossible) or users (hard).

Summary

  • Storage: Is permanent and expensive. Pack variables.
  • ABI: Is the language of the EVM. It’s just bytes.
  • Security: Assume every external call is malicious.
  • Immutability: Measure twice, cut once. Deployment is final.

Questions about this lesson? Working on related infrastructure?

Let's discuss