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.
🎯 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`
📚 Prerequisites
Before this lesson, you should understand:
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
transfer(address,uint256)->0xa9059cbbapprove(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
- What is a Function Selector?
- Why is
privatevisibility a lie? - Why should you update
balancesbefore sending ETH? - How many bytes is a storage slot?
- What happens if you deploy code with a bug?
Answers
- The first 4 bytes of the Keccak hash of the function signature. It tells the EVM which code to run.
- State is public.
privateonly restricts internal solidity calls. Usecast storageto read anything. - To prevent Reentrancy. If you update after, an attacker can re-call the function before the balance is zeroed.
- 32 Bytes (256 bits).
- 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