Chapter 12 Exercises: The Ethereum Virtual Machine
Exercise Set A: Stack and Memory Operations
Exercise 12.1 — Manual Stack Tracing (Bloom Level: Apply)
Trace the following EVM opcode sequence by hand. After each instruction, write the complete stack contents (top of stack on the left).
PUSH1 0x05
PUSH1 0x03
ADD
PUSH1 0x02
MUL
PUSH1 0x0A
SWAP1
SUB
Tasks: 1. Write the stack state after each instruction executes. 2. What is the final value on the stack? Express it in both decimal and hexadecimal. 3. What mathematical expression does this opcode sequence compute? 4. How much gas does this sequence consume? (Use the gas costs from Section 12.4.)
Expected deliverable: A table with columns for instruction, gas cost, and stack state after execution.
Exercise 12.2 — Reverse Engineering (Bloom Level: Analyze)
The following stack trace shows the state before and after an unknown sequence of 5 opcodes:
Initial stack: [0x0A, 0x14, 0x03] (0x0A on top)
Final stack: [0x3C] (single value)
Note: 0x0A = 10, 0x14 = 20, 0x03 = 3, 0x3C = 60.
Tasks: 1. Determine one possible sequence of opcodes that transforms the initial stack into the final stack. 2. Is your solution unique? If not, provide an alternative sequence. 3. For each solution, compute the total gas cost. 4. Which solution is the most gas-efficient?
Exercise 12.3 — Memory Cost Calculation (Bloom Level: Apply)
A contract needs to store a 2,048-byte array in memory during execution.
Tasks:
1. Calculate the memory expansion cost for allocating 2,048 bytes (64 words of 32 bytes each), using the formula: cost = 3 * n + floor(n^2 / 512) where n is the number of 32-byte words.
2. If the contract instead stored this data in 64 separate storage slots (one per 32-byte word), what would be the gas cost? Assume all slots are cold and previously zero.
3. What is the ratio of storage cost to memory cost? What does this tell you about contract optimization?
4. At what memory size (in words) does the quadratic term exceed the linear term?
Exercise 12.4 — Stack Depth Limits (Bloom Level: Analyze)
A Solidity function has the following local variables:
function complexCalculation(
uint256 a, uint256 b, uint256 c, uint256 d,
uint256 e, uint256 f, uint256 g, uint256 h
) public pure returns (uint256) {
uint256 sum1 = a + b + c + d;
uint256 sum2 = e + f + g + h;
uint256 product = sum1 * sum2;
uint256 average = product / 16;
uint256 adjusted = average + a - h;
uint256 result = adjusted * b / (c + 1);
return result;
}
Tasks: 1. Count the total number of values that must coexist on the stack at the point of maximum pressure. Consider function parameters, intermediate values, and return values. 2. Would this function trigger a "Stack too deep" error? Why or why not? 3. Suggest two strategies for refactoring this function if it did hit the stack depth limit. 4. Why can the EVM only access the top 16 stack items directly? What would be different if it could access all 1,024?
Exercise Set B: Gas Economics
Exercise 12.5 — Gas Cost Analysis of Token Transfer (Bloom Level: Analyze)
An ERC-20 token transfer (transfer(address,uint256)) involves the following operations at the EVM level:
- Load the function selector and validate it (calldata operations)
- Load the sender's balance from storage (SLOAD, cold)
- Check that sender has sufficient balance (comparison opcodes)
- Subtract amount from sender's balance (arithmetic)
- Store sender's new balance (SSTORE, non-zero to non-zero)
- Load receiver's balance from storage (SLOAD, cold)
- Add amount to receiver's balance (arithmetic)
- Store receiver's new balance (SSTORE — consider two cases: zero to non-zero vs. non-zero to non-zero)
- Emit a Transfer event (LOG3)
Tasks: 1. Calculate the gas cost for each step. Use the gas costs from the chapter: SLOAD cold = 2,100; SSTORE (non-zero to non-zero) = 5,000; SSTORE (zero to non-zero) = 20,000; LOG3 base = 1,125 (375 base + 375 per topic x 2 additional topics). 2. What is the total gas cost for a transfer where the receiver already has a non-zero balance? 3. What is the total gas cost for a transfer where the receiver has a zero balance (first time receiving this token)? 4. Explain why the first transfer to a new address is more expensive than subsequent transfers. 5. A protocol design decision: Should a token contract initialize all user balances to 1 wei instead of 0 to avoid the 20,000 gas new-slot cost? What are the trade-offs?
Exercise 12.6 — Storage Optimization (Bloom Level: Create)
A contract stores the following data for each user:
struct UserProfile {
uint256 balance; // User's token balance
uint256 lastActivity; // Timestamp of last activity
uint8 level; // User level (0-255)
bool isActive; // Whether user is active
address referrer; // Who referred this user
}
Tasks: 1. How many storage slots does this struct occupy in its current layout? (Remember: Solidity packs variables into 32-byte slots.) 2. Rearrange the struct fields to minimize the number of storage slots used. 3. Calculate the gas difference between writing a full UserProfile with the original layout vs. your optimized layout, assuming all slots are new (zero to non-zero). 4. If the contract has 10,000 users, what is the total gas savings from your optimization? Convert this to ETH at a gas price of 30 gwei, and to USD at ETH = $3,000.
Exercise 12.7 — Cold vs. Warm Access (Bloom Level: Apply)
Consider this Solidity function:
function updateAndCheck(uint256 newValue) public {
uint256 old = storedValue; // First read
require(newValue > old, "Too low"); // Uses 'old' from stack
storedValue = newValue; // Write
emit Updated(old, newValue); // Uses 'old' from stack
uint256 verify = storedValue; // Second read
require(verify == newValue, "Bug"); // Uses 'verify' from stack
}
Tasks:
1. Identify each SLOAD and SSTORE operation and classify each as cold or warm.
2. Calculate the total gas cost for just the storage operations.
3. The second SLOAD (reading verify) is redundant — the compiler could optimize it away. With the optimizer enabled, would solc eliminate it? Why or why not?
4. Rewrite the function to minimize gas usage while preserving the same behavior.
Exercise Set C: Bytecode and ABI
Exercise 12.8 — Bytecode Decoding (Bloom Level: Apply)
Decode the following bytecode into human-readable opcodes:
6080604052600436106043575f3560e01c806360fe47b11460475780636d4ce63c14605b575b5f80fd
Tasks:
1. Split the bytecode into individual opcodes, noting where PUSH instructions include their data bytes.
2. Identify the free memory pointer initialization.
3. Find the function dispatcher and identify the two function selectors it checks.
4. Look up these selectors. What Solidity functions do they correspond to?
5. Verify your decoding by running the code/bytecode_decoder.py script against this bytecode.
Exercise 12.9 — ABI Encoding by Hand (Bloom Level: Apply)
Encode the following function call using ABI encoding rules:
function transferFrom(address sender, address recipient, uint256 amount)
Called with:
- sender = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
- recipient = 0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413
- amount = 1000000000000000000 (1 ETH in wei, or 10^18)
Tasks:
1. Compute the function selector from the canonical signature.
2. Encode each parameter as a 32-byte word.
3. Assemble the complete calldata (selector + encoded parameters).
4. What is the total size of the calldata in bytes?
5. Verify your encoding using the code/abi_encoder.py script.
Exercise 12.10 — Event Log Encoding (Bloom Level: Apply)
Given the event:
event Approval(address indexed owner, address indexed spender, uint256 value);
And the emission:
emit Approval(0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045, 0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413, 500);
Tasks: 1. What is topic[0]? (Compute the keccak256 hash of the event signature.) 2. What are topic[1] and topic[2]? 3. What data goes in the log's data field? 4. How many topics does this log have in total? What is the maximum number of topics allowed? 5. Why does the EVM limit events to 4 topics (LOG0 through LOG4)?
Exercise Set D: Security and Design
Exercise 12.11 — DELEGATECALL Vulnerability Analysis (Bloom Level: Analyze)
A proxy contract uses DELEGATECALL to forward all calls to an implementation contract:
contract Proxy {
address public implementation; // slot 0
address public admin; // slot 1
fallback() external payable {
(bool success, bytes memory data) = implementation.delegatecall(msg.data);
require(success);
assembly { return(add(data, 0x20), mload(data)) }
}
}
contract Implementation {
uint256 public value; // slot 0
address public owner; // slot 1
function setValue(uint256 _v) public {
value = _v;
}
function setOwner(address _o) public {
owner = _o;
}
}
Tasks:
1. When setValue(42) is called on the Proxy, which storage slot in which contract gets modified? What variable does that slot correspond to in the Proxy?
2. When setOwner(attacker_address) is called on the Proxy, what happens? Why is this a critical vulnerability?
3. Explain why storage layout compatibility between proxy and implementation is essential.
4. How do OpenZeppelin's proxy patterns (TransparentProxy, UUPS) prevent this class of bugs?
5. Design a corrected storage layout for both contracts that would work safely with DELEGATECALL.
Exercise 12.12 — Sandbox Limitations Design Exercise (Bloom Level: Create)
You are designing a decentralized insurance contract that pays out when a natural disaster occurs. The contract needs: - Current weather data - Geolocation data for affected areas - A random number for premium calculation - A precise timestamp for payout deadlines
Tasks:
1. For each data requirement, explain why the EVM cannot provide it directly.
2. For each requirement, design a solution that works within the EVM's sandbox constraints. Consider oracles, commit-reveal schemes, and beacon chain randomness.
3. For each solution, identify at least one attack vector or failure mode.
4. Write pseudocode for the contract's core claimPayout() function, showing how it would interact with your oracle solution.
5. Compare the trust assumptions of your design with a traditional insurance system. Which requires more trust, and in what?
Challenge Problems
Challenge 12.A — Build a Minimal EVM (Bloom Level: Create)
Extend the code/evm_simulator.py to support the following additional opcodes:
- ISZERO, LT, GT, EQ
- JUMPI, JUMPDEST (with program counter tracking)
- DUP1, SWAP1
- CALLDATALOAD (with a configurable calldata buffer)
Then write a bytecode program that implements a simple function dispatcher: given calldata beginning with a 4-byte selector, it should jump to one of two code blocks.
Deliverable: The extended Python script and a test bytecode program with at least 3 test cases.
Challenge 12.B — Gas Cost Estimator (Bloom Level: Create)
Write a Python program that: 1. Accepts a sequence of EVM opcodes as input. 2. Calculates the total gas cost, accounting for: - Base opcode costs - Memory expansion costs (tracking the high-water mark) - Cold vs. warm storage access (tracking accessed slots) 3. Produces a detailed gas report showing the cost contribution of each opcode.
Test it against the bytecode for a simple ERC-20 transfer function.
Deliverable: A Python script with at least 5 test cases covering different gas cost categories.