HomeCurriculumBlockchain BasicsEthereum Virtual Machine (EVM) & Smart Contracts
Ethereum Virtual Machine (EVM) & Smart Contracts

Ethereum Virtual Machine (EVM) & Smart Contracts

Solidity smart contract syntax, EVM bytecode execution, reentrancy vulnerabilities, and Web3 integration.

DT
Authored by DevThesis Systems Editorial Team
Reviewed for Technical Accuracy & Code Determinism

The invention of the Ethereum Virtual Machine (EVM) by Vitalik Buterin and Gavin Wood (2014) transformed blockchain technology from a distributed cryptographic ledger of static balances into a globally accessible, decentralized, Turing-complete world computer. By executing untrusted, deterministic bytecode across thousands of consensus nodes, the EVM enables self-executing programmable agreements known as Smart Contracts.

Operating as a sandboxed, stack-based state virtual machine, the EVM governs state transitions across millions of smart contracts—powering decentralized finance (DeFi), non-fungible tokens (NFTs), algorithmic stablecoins, and decentralized autonomous organizations (DAOs). Engineering secure decentralized applications requires an exhaustive understanding of EVM low-level architecture, volatile and persistent memory hierarchies, gas economics, bytecode execution mechanics, and foundational security vulnerabilities.


1. EVM Architecture & Computational Runtime Environment#

The EVM is a quasi-Turing complete, registerless, stack-based execution runtime. It is "quasi"-Turing complete because while it supports arbitrary computation (including loops, conditional branching, and recursion), execution is strictly bounded by a finite, prepaid resource constraint: Gas.

text
EVM Execution Sandbox Topology:

Global World State (Account State & Storage Tries)
 ▲
 │ SLOAD / SSTORE
 ▼
┌───────────────────────────────────────────────────────────┐
│ EVM Sandboxed Execution Core │
│ │
│ [ Program Counter (PC) ] ──► [ Opcode Bytecode Stream ] │
│ │
│ [ Transient Memory Spaces ]: │
│ ├── Stack : 1024 depth x 256-bit words (LIFO) │
│ ├── Memory : Volatile byte-addressable linear array │
│ └── Calldata: Read-only transaction input byte array │
│ │
│ [ Gas Metering Engine ]: Gas Remaining -= OpcodeCost │
└───────────────────────────────────────────────────────────┘
 ▲
 │ Transaction Context
 ▼
Environment: msg.sender, msg.value, block.timestamp, gasprice

1. The 256-Bit Word Standard#

Unlike standard desktop CPU architectures that operate on 32-bit or 64-bit registers, the EVM natively computes on 256-bit (32-byte) words using Big-Endian byte ordering.

This design choice was deliberate:

  • Cryptographic hash functions (Keccak-256, SHA-256) and elliptic curve digital signature algorithms (ECDSA secp256k1) operate natively on 256-bit integer fields.
  • 256-bit integers facilitate direct modulo arithmetic over large prime fields without requiring multi-precision software arithmetic libraries.

2. Execution Environment & Global Context#

During smart contract execution, the EVM exposes global context variables:

  • Transaction Context:

- msg.sender (20 bytes): The direct caller address (can be an EOA or another contract).

- msg.value (256-bit uint): The amount of Ether (in Wei) transferred with the call.

- tx.origin (20 bytes): The original EOA that signed the transaction initiating the call chain.

- gasleft() (256-bit uint): The remaining computational gas units available in the current frame.

  • Block Context:

- block.number: Current block height.

- block.timestamp: Unix epoch timestamp of current block.

- block.prevrandao: Randomness beacon output provided by consensus layer.

- block.basefee: Current EIP-1559 base fee per gas unit.


2. EVM Memory Hierarchy: Four Distinct Storage Regimes#

The EVM maintains four distinct, segregated data regimes with vastly different persistence lifecycles, addressing mechanisms, and gas cost structures.

text
EVM Memory Hierarchy Compared:

Data Space Persistence Addressing Word Size Relative Gas Cost
───────────────────────────────────────────────────────────────────────────
Stack Transient LIFO Index 256 bits Minimal (3 gas)
Memory Transient Byte-addressed 8 bits Quadratic expansion
Calldata Transient Byte-addressed 8 bits Low (read-only)
Storage Permanent Slot-addressed 256 bits Exorbitant (up to 20k)

1. The Stack#

The EVM stack is a standard Last-In, First-Out (LIFO) data structure:

  • Capacity: Maximum depth of 1,024 words (1,024 × 32 bytes = 32 KB). Attempting to push past 1,024 items triggers a StackOverflow revert.
  • Accessibility: Most operations pop 1-2 words from the top and push a result.
  • Stack Reach Limit: The EVM provides opcodes to inspect or duplicate items up to a depth of 16 (DUP1 through DUP16, SWAP1 through SWAP16). Accessing variables deeper than 16 slots triggers the infamous Solidity compiler error: "Stack too deep".

2. Volatile Memory (RAM)#

Memory is a contiguous, byte-addressable linear array instantiated anew for each internal message call and zero-initialized. It is used to hold temporary data such as string concatenations, ABI encoding payloads, and local array structures.

Quadratic Memory Expansion Gas Cost:

While reading or writing within already allocated memory costs a flat 3 gas (MLOAD, MSTORE), expanding memory from size s to size s' incurs a quadratic gas penalty. This prevents denial-of-service memory exhaustion attacks.

For an allocated memory size of a words (a = ⌈ bytes / 32 ⌉):

C_mem(a) = 3 · a + ⌊ (a^2 / 512) ⌋

When memory expands from a_1 words to a_2 words, the incremental gas charged is:

Δ Gas = C_mem(a_2) - C_mem(a_1)

Because the cost scales with a^2 / 512, allocating large memory arrays (e.g., several megabytes) quickly exhausts the block gas limit, causing the transaction to revert with OutOfGas.

3. Persistent Storage#

Storage is the contract's permanent state database, stored globally across the distributed ledger inside a dedicated Merkle Patricia Trie for that contract.

  • Structure: A massive key-value mapping from 2^256 keys to 2^256 values:

Storage: \{0, 1\}^256 → \{0, 1\}^256

  • Persistence: Data survives across transactions and blocks indefinitely until explicitly overwritten or cleared.
  • Opcode Costs: Storage access is the most expensive operation in the EVM:

- SLOAD: Reads a 32-byte word from storage. Under EIP-2929, reading a "cold" (previously untouched in transaction) slot costs 2,100 gas, while reading a "warm" slot costs 100 gas.

- SSTORE: Writes a 32-byte word to storage. Modifying a storage slot from zero (0x00) to non-zero costs up to 20,000 gas.

4. Calldata#

Calldata is a read-only, byte-addressable memory buffer holding the exact serialized byte payload sent with the transaction (the 4-byte function selector followed by ABI-encoded arguments). Because calldata is immutable, loading from it (CALLDATALOAD, CALLDATACOPY) is significantly cheaper than loading from memory, making calldata parameter declarations the gold standard for high-performance Solidity functions.


3. Gas Economics & The EIP-1559 Dynamic Fee Market#

To solve the Halting Problem, the EVM meters execution through Gas. Every low-level opcode has a deterministic gas cost specified in the Ethereum Yellow Paper (e.g., ADD costs 3 gas, SHA3 costs 30 gas + 6 gas per word, SSTORE costs up to 20,000 gas).

If the gas counter reaches zero before execution completes, the EVM triggers an Out of Gas (OOG) error: all state mutations are reverted, yet the miner retains all consumed gas fees to compensate for burned CPU cycles.

text
EIP-1559 Transaction Fee Architecture:

Total Fee Paid by User (Per Gas Unit)
┌───────────────────────────────────────┬───────────────────────────────────────┐
│ Base Fee (r_base) │ Priority Tip (r_tip) │
│ Set algorithmically by protocol │ Set by user to incentivize validator │
│ BURNED (Permanently removed from ETH) │ PAID TO VALIDATOR │
└───────────────────────────────────────┴───────────────────────────────────────┘

The EIP-1559 Dynamic Fee Mechanism#

Prior to London Hard Fork (August 2021, EIP-1559), Ethereum utilized a first-price auction model, leading to severe fee volatility and gas price bidding wars. EIP-1559 replaced this with a predictable base fee mechanism.

1. Target Gas vs. Maximum Block Gas Limit

  • Block Target Gas: 15,000,000 gas (15 M, optimal long-term network capacity).
  • Block Maximum Gas Limit: 30,000,000 gas (30 M, absolute maximum burst ceiling).

2. Base Fee Adjustment Formula

The base fee per gas unit (r_base) adjusts dynamically every block as an exponential moving function of the preceding block's gas utilization:

r_base, t+1 = r_base, t · ( 1 + (1 / 8) · (GasUsed_t - GasTarget / GasTarget) )

  • If Block is Full (GasUsed = 30 M):

Adjustment = (1 / 8) · (30M - 15M / 15M) = +(1 / 8) = +12.5\%

The base fee increases by exactly 12.5\% for the next block.

  • If Block is Empty (GasUsed = 0):

Adjustment = (1 / 8) · (0 - 15M / 15M) = -(1 / 8) = -12.5\%

The base fee decreases by exactly 12.5\%.

  • If Block is Exactly at Target (GasUsed = 15 M):

The base fee remains completely unchanged (Δ r = 0\%).

3. Base Fee Burning & Deflation

Crucially, the entire base fee is burned (permanently destroyed and removed from the total Ether supply). The validator receives only the Priority Tip:

Total Fee Deducted = GasUsed × ( r_base + min(MaxPriorityFee, MaxFee - r_base) )

Burned Amount = GasUsed × r_base

Validator Revenue = GasUsed × min(MaxPriorityFee, MaxFee - r_base)

When network transaction activity is high, daily burned base fees frequently exceed daily PoS staking issuance, turning Ether into a mathematically deflationary asset ("Ultra Sound Money").


4. Smart Contract Security Vulnerabilities & Attack Vectors#

Because smart contract code is immutable once deployed and holds billions of dollars in liquid assets, software bugs result in irreversible financial theft. Security audits focus on defending against four classic attack vectors:

1. Reentrancy Vulnerability (The DAO Exploit)#

Reentrancy occurs when a contract transfers Ether or calls an external address before updating its internal accounting state. The recipient (which can be a malicious contract) intercepts execution via its receive() or fallback() function and calls the vulnerable withdrawal function recursively before the original state is mutated.

text
Reentrancy Attack Execution Sequence:

Vulnerable Bank Contract Attacker Contract
┌───────────────────────────────┐ ┌───────────────────────────────┐
│ Balance of Attacker: 5 ETH │ │ 1. Calls withdraw(5 ETH) │
│ │◄─────────────┤ │
│ Checks: balance >= 5 (OK) │ │ │
│ Transfers 5 ETH to Attacker ──┼─────────────►│ 2. receive() function executes!│
│ │ │ Calls withdraw(5 ETH) AGAIN│
│ │◄─────────────┤ (Re-enters before balance │
│ Checks: balance >= 5 (STILL 5)│ │ is set to 0!) │
│ Transfers 5 ETH to Attacker ──┼─────────────►│ │
│ │ │ 3. Loops until Bank drained! │
│ Finally sets: balance = 0 │ │ │
└───────────────────────────────┘ └───────────────────────────────┘

The Checks-Effects-Interactions (CEI) Pattern:

To eliminate reentrancy vulnerabilities, code must strictly adhere to CEI:

  1. Checks: Validate all require conditions and arguments first.
  2. Effects: Mutate all internal state variables and balances before external interaction.
  3. Interactions: Perform external calls or Ether transfers last.
solidity
// VULNERABLE TO REENTRANCY:
function withdraw() external {
 uint256 amount = balances[msg.sender];
 (bool ok, ) = msg.sender.call{value: amount}(""); // Interaction first!
 require(ok, "Transfer failed");
 balances[msg.sender] = 0; // Effect too late!
}

// SECURE VIA CHECKS-EFFECTS-INTERACTIONS:
function withdrawSecure() external {
 uint256 amount = balances[msg.sender]; // 1. Check
 require(amount > 0, "Zero balance");
 balances[msg.sender] = 0; // 2. Effect (State updated FIRST!)
 (bool ok, ) = msg.sender.call{value: amount}(""); // 3. Interaction
 require(ok, "Transfer failed");
}

2. Frontrunning & Maximal Extractable Value (MEV)#

In public mempools, all broadcast transactions are publicly visible prior to block inclusion. Specialized arbitrage searchers monitor mempools to extract profit by manipulating transaction order:

  • Sandwich Attacks: A searcher spots a large pending buy order on a decentralized exchange (Uniswap). The searcher broadcasts a frontrunning transaction with a higher priority tip to buy the asset immediately ahead of the user (driving the price up), lets the user's order execute at a worse price, and immediately broadcasts a backrunning transaction to sell the asset, pocketing a risk-free profit.
  • Mitigation: Private mempools (Flashbots Protect, MEV-Share), slippage tolerances, and commit-reveal schemes.

3. Delegatecall Storage Collisions#

The DELEGATECALL opcode allows a proxy contract to execute code from an implementation contract while preserving the caller's storage context, msg.sender, and msg.value.

  • The Vulnerability: If the proxy contract and implementation contract define state variables in different storage orders (e.g., Proxy defines address owner at Slot 0, while Implementation defines uint256 balance at Slot 0), executing implementation functions directly overwrites the proxy's owner variable with integer data, completely compromising contract ownership.
  • Mitigation: Unstructured storage patterns (EIP-1967), assigning implementation storage pointers to random pseudo-hashes:

StorageSlot = bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)


5. Industrial Production Implementation: Secure Vault & EVM Stack Simulator#

The following production-grade implementation consists of two parts:

  1. A Production Solidity Smart Contract implementing a secure deposit/withdrawal vault with custom errors, strict Checks-Effects-Interactions, and OpenZeppelin-style Reentrancy Guards.
  2. A Python EVM Stack and Memory Simulator that tracks low-level opcodes (PUSH1, MSTORE, MLOAD, ADD) and computes exact quadratic memory expansion costs.
python
# ==========================================================
# 1. PRODUCTION-GRADE SECURE SOLIDITY VAULT CONTRACT
# ==========================================================

SECURE_VAULT_SOLIDITY_SOURCE = """
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

/// @notice Custom gas-efficient errors (replaces expensive revert strings)
error InsufficientBalance(uint256 requested, uint256 available);
error TransferFailed();
error ReentrancyGuardLocked();

/**
 * @title SecureVault
 * @notice Demonstrates Checks-Effects-Interactions and Mutex Reentrancy Protection.
 */
contract SecureVault {
 mapping(address => uint256) private _balances;
 uint256 private _status; // Mutex state: 1 = Unlocked, 2 = Locked

 event Deposited(address indexed user, uint256 amount);
 event Withdrawn(address indexed user, uint256 amount);

 constructor() {
 _status = 1; // Unlocked
 }

 modifier nonReentrant() {
 if (_status == 2) revert ReentrancyGuardLocked();
 _status = 2; // Lock mutex
 _;
 _status = 1; // Unlock mutex
 }

 function deposit() external payable {
 _balances[msg.sender] += msg.value;
 emit Deposited(msg.sender, msg.value);
 }

 function withdraw(uint256 amount) external nonReentrant {
 uint256 currentBalance = _balances[msg.sender];
 if (amount > currentBalance) {
 revert InsufficientBalance(amount, currentBalance);
 }

 // 1. CHECKS & EFFECTS: Mutate internal state BEFORE external transfer
 _balances[msg.sender] = currentBalance - amount;

 // 2. INTERACTIONS: Low-level call performed last
 (bool success, ) = msg.sender.call{value: amount}("");
 if (!success) {
 revert TransferFailed();
 }

 emit Withdrawn(msg.sender, amount);
 }

 function getBalance(address account) external view returns (uint256) {
 return _balances[account];
 }
}
"""

# ==========================================================
# 2. PYTHON EVM STACK & QUADRATIC MEMORY ENGINE
# ==========================================================

class EVMSimulator:
 """Simulates EVM stack, linear memory, and quadratic expansion gas costs."""
 def __init__(self) -> None:
 self.stack: list[int] = []
 self.memory: bytearray = bytearray()
 self.gas_consumed: int = 0

 def memory_gas_cost(self, num_words: int) -> int:
 """Computes C_mem(a) = 3*a + floor(a^2 / 512)."""
 return 3 * num_words + (num_words ** 2) // 512

 def ensure_memory(self, offset: int, size: int) -> None:
 """Expands linear memory and bills quadratic gas penalty."""
 if size == 0:
 return
 end_byte = offset + size
 new_words = (end_byte + 31) // 32
 current_words = len(self.memory) // 32

 if new_words > current_words:
 incremental_cost = self.memory_gas_cost(new_words) - self.memory_gas_cost(current_words)
 self.gas_consumed += incremental_cost
 self.memory.extend(bytes((new_words - current_words) * 32))

 def push(self, val: int) -> None:
 if len(self.stack) >= 1024:
 raise RuntimeError("StackOverflow: Stack depth exceeded 1024 words.")
 self.stack.append(val & ((1 << 256) - 1))
 self.gas_consumed += 3 # PUSH costs 3 gas

 def pop(self) -> int:
 if not self.stack:
 raise RuntimeError("StackUnderflow: Attempted pop from empty stack.")
 self.gas_consumed += 2 # POP costs 2 gas
 return self.stack.pop()

 def mstore(self, offset: int, value: int) -> None:
 """Writes 32-byte word to memory offset."""
 self.ensure_memory(offset, 32)
 val_bytes = value.to_bytes(32, byteorder="big")
 self.memory[offset:offset + 32] = val_bytes
 self.gas_consumed += 3 # MSTORE base cost

 def mload(self, offset: int) -> int:
 """Reads 32-byte word from memory offset."""
 self.ensure_memory(offset, 32)
 val_bytes = self.memory[offset:offset + 32]
 self.gas_consumed += 3 # MLOAD base cost
 return int.from_bytes(val_bytes, byteorder="big")

 def add(self) -> None:
 a = self.pop()
 b = self.pop()
 res = (a + b) & ((1 << 256) - 1)
 self.push(res)
 self.gas_consumed += 3 # ADD base cost


# Execution & Verification
if __name__ == "__main__":
 evm = EVMSimulator()

 # Step 1: PUSH 42, PUSH 58, ADD -> Stack should have 100
 evm.push(42)
 evm.push(58)
 evm.add()
 result = evm.pop()
 print(f"Stack Addition Result: {result} (Expected: 100)")
 assert result == 100

 # Step 2: Test Memory Allocation and Expansion
 # Store value at memory offset 64 (requires expanding memory to 3 words = 96 bytes)
 evm.mstore(64, 0xCAFEBABE)
 retrieved = evm.mload(64)
 print(f"Retrieved from Memory (Offset 64): {hex(retrieved)}")
 print(f"Total Gas Consumed (Stack + Memory Expansion): {evm.gas_consumed}")
 assert retrieved == 0xCAFEBABE

6. Common Anti-Patterns & Engineering Pitfalls#

1. Using `tx.origin` for Access Control#

The Anti-Pattern: Validating administrative authority using require(tx.origin == owner).

Why it Breaks: If the owner is tricked into interacting with a malicious phishing contract (e.g., minting an innocent-looking free NFT), the malicious contract can call the protected vault contract. Because the original signer who initiated the call is the owner, tx.origin == owner evaluates to True, allowing the attacker to drain the vault.

Correct Practice: Strictly use msg.sender == owner for all access control.

2. Unbounded Dynamic Loops (Denial of Service)#

The Anti-Pattern: Iterating over an unbounded dynamic array in a public transaction (e.g., for (uint i=0; i < registeredUsers.length; i++)).

Why it Breaks: As the array grows to thousands of elements, the gas required to execute the loop exceeds the maximum block gas limit (30 M). Once this occurs, the function becomes permanently unexecutable, permanently locking any funds tied to that logic.

Correct Practice: Implement a pull-over-push withdrawal pattern or enforce strict pagination windows.

3. Floating Pragma & Unlocked Compiler Versions#

The Anti-Pattern: Deploying production contracts with floating compiler pragma pragma solidity ^0.8.0;.

Why it Breaks: Floating pragmas allow contracts to be compiled with arbitrary future compiler versions that may introduce breaking bytecode generation changes or unvetted optimizer bugs.

Correct Practice: Lock the compiler version to a specific, audited patch release (e.g., pragma solidity 0.8.24;).


7. Checkpoint Practice Problems#

Problem 1: EVM Quadratic Memory Expansion Gas Calculation#

A smart contract executes a complex cryptographic verification that writes a 32-byte word to memory starting at byte offset 992.

Prior to this operation, the transaction has already allocated 320 bytes of memory.

Required:

  1. Calculate the initial memory size in words (a_1) before the write.
  2. Determine the new total memory size in bytes and words (a_2) required to accommodate the write of 32 bytes ending at byte index 992 + 32 = 1,024.
  3. Using the standard EVM memory gas formula C_mem(a) = 3a + ⌊ a^2 / 512 ⌋, compute:

- The total cumulative memory cost for a_1 words.

- The total cumulative memory cost for a_2 words.

- The exact incremental expansion gas penalty (Δ Gas) charged to the user for this single memory write.

Step-by-Step Solution:

Step 1: Compute Initial Word Count (a_1)

Memory words are 32 bytes:

a_1 = (320 bytes / 32 bytes/word) = 10 words

Step 2: Compute Final Word Count (a_2)

The write spans byte offset 992 to byte offset 1,024:

Total Required Bytes = 1,024 bytes

a_2 = (1,024 bytes / 32 bytes/word) = 32 words

Step 3: Compute Cumulative Memory Costs

  • Initial Cost (a_1 = 10 words):

C_mem(10) = 3(10) + ⌊ (10^2 / 512) ⌋ = 30 + ⌊ (100 / 512) ⌋ = 30 + 0 = 30 gas

  • Final Cost (a_2 = 32 words):

C_mem(32) = 3(32) + ⌊ (32^2 / 512) ⌋ = 96 + ⌊ (1,024 / 512) ⌋ = 96 + 2 = 98 gas

  • Incremental Gas Expansion Fee:

Δ Gas = C_mem(32) - C_mem(10) = 98 - 30 = 68 gas

The user is billed 68 gas for the memory expansion (in addition to the flat 3 gas base cost of MSTORE).


Problem 2: EIP-1559 Dynamic Base Fee Retargeting & Burn Mechanics#

Ethereum block height 19,500,000 executes with the following parameters:

  • Base fee of block 19,500,000: r_base = 40.0 Gwei = 40.0 × 10^9 Wei
  • Target gas utilization: GasTarget = 15,000,000 gas
  • Actual gas consumed in block: GasUsed = 22,500,000 gas (75\% of the 30 M maximum limit)

Required:

  1. Compute the exact percentage change in base fee for the subsequent block 19,500,001.
  2. Calculate the exact numerical base fee r_base, 19500001 in Gwei.
  3. Compute the total amount of Ether burned permanently during block 19,500,000.
  4. A user broadcasts a transaction in block 19,500,000 consuming 120,000 gas with parameters:

- MaxFeePerGas = 50.0 Gwei

- MaxPriorityFeePerGas = 2.0 Gwei

Compute the total transaction fee paid by the user in ETH, the portion burned, and the portion paid to the validator.

Step-by-Step Solution:

Step 1: Compute Base Fee Percentage Change

Recall the EIP-1559 adjustment equation:

Δ \% = (1 / 8) · ( (GasUsed - GasTarget / GasTarget) )

Substitute the values:

Δ \% = (1 / 8) · ( (22,500,000 - 15,000,000 / 15,000,000) ) = (1 / 8) · ( (7,500,000 / 15,000,000) ) = (1 / 8) · 0.50 = 0.0625 (+6.25\%)

The base fee increases by exactly +6.25\%.

Step 2: Calculate New Base Fee for Block 19,500,001

r_base, 19500001 = r_base × (1 + 0.0625) = 40.0 × 1.0625 = 42.50 Gwei

Step 3: Calculate Total ETH Burned in Block 19,500,000

Burned Wei = GasUsed × r_base = 22,500,000 gas × (40.0 × 10^9 Wei/gas)

Burned Wei = 9.0 × 10^17 Wei

Convert to ETH (1 ETH = 10^18 Wei):

Total Burned ETH = (9.0 × 10^17 / 10^18) = 0.90 ETH

In this single 12-second block, 0.90 ETH was permanently removed from circulation.

Step 4: User Transaction Fee Breakdown

  • Base fee = 40.0 Gwei.
  • Priority tip = min(MaxPriorityFee, MaxFee - r_base) = min(2.0, 50.0 - 40.0) = min(2.0, 10.0) = 2.0 Gwei.
  • Effective Gas Price paid = r_base + tip = 40.0 + 2.0 = 42.0 Gwei.
  • Total user payment:

Total Fee = 120,000 gas × 42.0 Gwei = 5,040,000 Gwei = 0.00504 ETH

  • Portion burned:

Burned = 120,000 × 40.0 Gwei = 4,800,000 Gwei = 0.00480 ETH (95.24\%)

  • Portion paid to validator:

Tip = 120,000 × 2.0 Gwei = 240,000 Gwei = 0.00024 ETH (4.76\%)


Problem 3: Reentrancy Exploit Mathematical Tracing#

A decentralized crowdfunding contract holds a total balance of 12.0 ETH contributed by honest donors.

An attacker deposits 1.0 ETH into the contract, bringing total balance to 13.0 ETH.

The withdrawal function is vulnerable to classic reentrancy:

solidity
function withdraw() external {
 uint256 bal = balances[msg.sender];
 require(bal > 0);
 (bool success, ) = msg.sender.call{value: bal}("");
 require(success);
 balances[msg.sender] = 0;
}

The attacker's contract contains a malicious fallback function that re-invokes withdraw() as long as the victim contract holds ≥ 1.0 ETH.

Assume each call frame consumes a negligible gas overhead well within the 30M gas limit.

Required:

  1. Trace the recursive call stack depth and state balance of the victim contract at each recursion step.
  2. Calculate how many total recursive calls will execute before the loop terminates.
  3. Compute the final balance of the victim contract and the total ETH extracted by the attacker.
  4. Show how applying the Checks-Effects-Interactions (CEI) pattern completely neutralizes this attack on the very first recursive call.

Step-by-Step Solution:

Step 1 & 2: Recursive Execution Trace

Initial state: Victim contract holds 13.0 ETH. Attacker recorded balance = 1.0 ETH.

  • Call 1 (Attacker initiates `withdraw()`):

- bal = balances[attacker] = 1.0 ETH.

- Checks: 1.0 > 0 (Pass).

- Victim sends 1.0 ETH to Attacker contract. Victim balance drops to 12.0 ETH.

- Attacker's receive() intercepts execution! Attacker re-invokes withdraw().

  • Call 2 (Recursive):

- Inside new call frame: bal = balances[attacker].

- Because Call 1 has not yet executed balances[msg.sender] = 0, balances[attacker] is still 1.0 ETH!

- Checks: 1.0 > 0 (Pass).

- Victim sends 1.0 ETH to Attacker. Victim balance drops to 11.0 ETH.

- Attacker re-invokes withdraw().

  • Calls 3 through 13:

- The recursion continues identically for 13 full iterations.

- At Call 13, the victim contract transfers its final remaining 1.0 ETH. Victim balance reaches 0.0 ETH.

  • Call 14 (Termination):

- Attacker calls withdraw().

- Victim attempts to transfer 1.0 ETH. Because victim holds 0.0 ETH, the low-level call fails.

- Attacker catches the failure and stops recursion, allowing Call 13, Call 12, ..., Call 1 to unwind and complete.

Step 3: Final Balance and Stolen Capital

  • Total recursive withdrawal calls executed = 13.
  • Final victim balance = 0.0 ETH.
  • Total ETH extracted by attacker = 13.0 ETH.
  • Net profit = 13.0 extracted - 1.0 deposit = 12.0 ETH (The entire treasury is stolen).

Step 4: CEI Pattern Neutralization

If CEI is applied:

solidity
uint256 bal = balances[msg.sender];
require(bal > 0);
balances[msg.sender] = 0; // State updated BEFORE external transfer!
(bool success, ) = msg.sender.call{value: bal}("");
  • In Call 1: balances[attacker] is set to 0 immediately before the 1.0 ETH is sent.
  • When Attacker's receive() re-invokes withdraw() in Call 2:

bal = balances[attacker] = 0.

The check require(bal > 0) evaluates to False!

Call 2 reverts immediately, neutralizing the attack completely with zero loss of donor funds.


Problem 4: Storage Slot Packing & SSTORE Gas Optimization#

A Solidity developer declares four state variables in a smart contract:

solidity
// Design A (Unpacked):
uint256 public a; // 256 bits
uint128 public b; // 128 bits
uint256 public c; // 256 bits
uint128 public d; // 128 bits

// Design B (Optimized & Packed):
uint256 public a; // 256 bits (Slot 0)
uint256 public c; // 256 bits (Slot 1)
uint128 public b; // 128 bits (Slot 2, low 16 bytes)
uint128 public d; // 128 bits (Slot 2, high 16 bytes)

Required:

  1. Map out the exact 32-byte storage slot assignments (Slot 0, Slot 1, ...) for Design A and Design B according to Solidity's sequential storage packing rules.
  2. A constructor initializes all four variables from zero (0x00) to non-zero values.

Under standard EVM gas schedules, initializing a previously zero storage slot costs 20,000 gas per unique slot (SSTORE).

Compute the total initialization gas cost for Design A versus Design B.

  1. Calculate the absolute gas savings and the percentage reduction in storage cost achieved by packing variables into 32-byte words.

Step-by-Step Solution:

Step 1: Storage Slot Mapping

Solidity packs contiguous variables into the same 32-byte slot only if their combined bit-size is ≤ 256 bits. If a variable does not fit in the remaining space of the current slot, it is moved to the next slot.

  • Design A (Unpacked Layout):

- a (256 bits = 32 bytes): Takes entire Slot 0. Remaining: 0 bytes.

- b (128 bits = 16 bytes): Starts in Slot 1. Remaining: 16 bytes.

- c (256 bits = 32 bytes): Cannot fit into the 16 remaining bytes of Slot 1. Forced into Slot 2.

- d (128 bits = 16 bytes): Starts in Slot 3.

Total Slots Used in Design A = 4 slots (Slots 0, 1, 2, 3)

  • Design B (Packed Layout):

- a (256 bits): Takes entire Slot 0.

- c (256 bits): Takes entire Slot 1.

- b (128 bits): Takes lower 16 bytes of Slot 2. Remaining: 16 bytes.

- d (128 bits): Exactly fits into the remaining 16 upper bytes of Slot 2!

Total Slots Used in Design B = 3 slots (Slots 0, 1, 2)

Step 2: Initialization SSTORE Gas Costs

Initializing a slot from zero (0x00) to non-zero costs 20,000 gas:

  • Design A (4 Slots):

Gas_A = 4 × 20,000 = 80,000 gas

  • Design B (3 Slots):

- Slot 0 (a): 20,000 gas

- Slot 1 (c): 20,000 gas

- Slot 2 (b and d): The first write (b) initializes the slot from zero (20,000 gas). The second write (d) modifies an already-warm, non-zero slot (2,900 gas under EIP-2929).

Gas_B = 20,000 + 20,000 + 20,000 + 2,900 = 62,900 gas

Step 3: Gas Savings Analysis

Absolute Savings = 80,000 - 62,900 = 17,100 gas

Percentage Savings = (17,100 / 80,000) × 100\% ≈ 21.38\%

Furthermore, every subsequent transaction that reads both b and d saves an entire SLOAD operation (2,100 gas), compounding financial savings over the lifetime of the contract.

Key Architectural Takeaways

  • Mastering Ethereum Virtual Machine (EVM) & Smart Contracts is critical for Blockchain Basics production systems.
  • Strict invariant verification guarantees runtime stability and predictability.
  • Always follow industry-standard telemetry, logging, and security practices.

Hands-on Code Playground Available

Execute, edit, and experiment with code for this topic in the live sandbox.

Practice Check • Quick Quiz

Test Your Understanding

In Blockchain Basics, what is the main purpose of Ethereum Virtual Machine (EVM) & Smart Contracts?

A
To guarantee deterministic performance and correctness for ethereum virtual machine (evm) & smart contracts.
B
To bypass all system security checks during execution.
C
To increase memory allocation overhead unnecessarily.
D
To force non-blocking single-threaded execution globally.