Stage 3 of 6 · Estimated 6–8 weeks
This stage transforms you from a blockchain observer into a blockchain programmer. You will understand the Ethereum Virtual Machine at the instruction level, master Solidity as an engineering language (not a scripting tool), design and test production-grade smart contracts, and develop the security mindset essential for writing code that holds financial value. By the end, you will be capable of reading and auditing arbitrary Solidity code, reasoning about gas costs, and identifying vulnerabilities before they are exploited.
Smart contracts are the programmable layer of blockchain infrastructure. They govern trillions of dollars of assets across DeFi, NFTs, DAOs, and bridges. A bug in a smart contract is permanent and often exploitable — there are no patches once deployed to the canonical chain. This is the stage where engineering discipline becomes non-negotiable.
You can write, test, and deploy production-quality Solidity contracts. You understand what the EVM executes at the opcode level. You can estimate gas costs analytically. You can identify all major vulnerability classes in a given contract. You can write a full test suite with unit tests, fuzz tests, and invariant tests. You can reason about contract upgradeability and access control.
EVM Architecture
├── Stack, memory, storage, calldata
├── Opcodes and execution
├── Contract creation (initcode vs runtime code)
└── The call stack (CALL, DELEGATECALL, STATICCALL, CREATE2)
│
▼
Gas Model
├── Opcode costs
├── Storage costs (cold vs warm)
├── Refunds and EIP-3529
├── Gas optimization patterns
└── Gas griefing attacks
│
▼
Solidity Engineering
├── Types and encoding (ABI)
├── Control flow and functions
├── Contract structure and inheritance
├── Modifiers and events
├── Interfaces and abstract contracts
├── Libraries (internal vs external)
├── Error handling (require, revert, custom errors)
└── Assembly (Yul/inline assembly)
│
▼
Contract Architecture Patterns
├── Ownable, Access Control
├── Proxy patterns (Transparent, UUPS, Beacon)
├── Factory pattern
├── Pull-over-push payments
└── Checks-Effects-Interactions
│
▼
Testing and Debugging
├── Foundry (forge test, fuzz, invariant)
├── Hardhat
├── Unit testing patterns
├── Fuzz testing
├── Invariant (property-based) testing
└── On-chain debugging with traces
│
▼
Security Vulnerabilities
├── Reentrancy (classic, cross-function, cross-contract, read-only)
├── Integer overflow/underflow
├── Access control failures
├── Front-running and MEV
├── Oracle manipulation
├── Flash loan attacks
├── Delegatecall and storage collisions
├── Signature replay
├── Denial of service
└── Phishing (tx.origin)
The Ethereum Virtual Machine is a quasi-Turing-complete, stack-based virtual machine that executes bytecode. "Quasi" because execution is bounded by gas — preventing infinite loops.
Data Locations
The EVM has four distinct data locations, each with different cost and persistence characteristics:
| Location | Persistence | Access | Cost |
|---|---|---|---|
| Storage | Permanent (on-chain) | Keyed (32-byte slots) | Highest (SLOAD: 2100 gas cold, 100 gas warm; SSTORE: 20,000 for zero→nonzero) |
| Memory | Transaction-scoped | Linear (byte-addressed) | Quadratic above 724 bytes |
| Stack | Expression-scoped | LIFO, max 1024 elements | Cheapest |
| Calldata | Read-only, transaction-scoped | Byte-indexed | Cheaper than memory (used for function arguments) |
The Stack Machine
The EVM executes instructions that push and pop 32-byte words onto a stack. Most opcodes consume their operands from the stack and push results. Example sequence:
PUSH1 0x03 // Stack: [3]
PUSH1 0x05 // Stack: [5, 3]
ADD // Stack: [8]
Key Opcodes
| Category | Opcodes | Description |
|---|---|---|
| Arithmetic | ADD, SUB, MUL, DIV, MOD, EXP | 256-bit arithmetic. Overflow wraps silently (Solidity <0.8 had this risk). |
| Comparison | LT, GT, EQ, ISZERO | Push 1 (true) or 0 (false). |
| Bitwise | AND, OR, XOR, SHL, SHR | Bitwise ops. |
| Storage | SLOAD, SSTORE | Read/write persistent storage. |
| Memory | MLOAD, MSTORE, MSTORE8 | Read/write memory. |
| Control | JUMP, JUMPI, JUMPDEST | Unconditional/conditional jumps (must land on JUMPDEST). |
| Call | CALL, DELEGATECALL, STATICCALL, CALLCODE | Inter-contract calls. |
| Create | CREATE, CREATE2 | Deploy new contracts. |
| Context | CALLER, CALLVALUE, CALLDATALOAD, TIMESTAMP, BLOCKHASH | Environmental information. |
| Log | LOG0, LOG1, LOG2, LOG3, LOG4 | Emit events. |
| Termination | RETURN, REVERT, SELFDESTRUCT, STOP | End execution. |
Contract Lifecycle
to field and data containing
initcode is sent.
CALL vs DELEGATECALL
CALL(target, gas, value, args): Executes target's code in
target's context (target's storage,
address(this) is the target). Most common form of
inter-contract call.
DELEGATECALL(target, gas, args): Executes target's code
in the caller's context (caller's storage,
address(this) is the caller, msg.sender is
preserved). Used in proxy patterns. Dangerous if target address is not
trusted.
STATICCALL(target, gas, args): Like CALL but disallows
state modification (SSTORE, LOG, etc.). Used for read-only calls and
Solidity's view functions.
CREATE vs CREATE2
CREATE: Contract address =
keccak256(deployer_address, nonce).
CREATE2: Contract address =
keccak256(0xff, deployer_address, salt, keccak256(initcode)). Deterministic — address is known before
deployment. Enables counterfactual instantiation (open channels, safe
factory patterns).
Every Solidity construct compiles to EVM opcodes. Understanding the EVM means you can reason about gas costs precisely, understand why certain patterns are dangerous, and read decompiled bytecode. Proxy patterns (DELEGATECALL) power the upgradeable contract ecosystem — and power its most insidious bugs. CREATE2 enables counterfactual deployment patterns used by state channels and account abstraction.
initWallet function and become owner.
| Tool | Purpose |
|---|---|
evm.codes |
Interactive EVM opcode reference with gas costs and stack traces |
ethervm.io |
Online decompiler for EVM bytecode |
foundry cast disassemble |
Disassemble bytecode to opcodes |
hevm |
Haskell EVM implementation, used for symbolic execution |
pyrometer |
Rust-based Ethereum bytecode analysis |
uint256 x = 5 + 3; through the EVM
opcode level. What does the stack look like after each opcode?
DELEGATECALL to Contract B. Whose storage is modified?
Who is msg.sender inside B's code?
SELFDESTRUCT even
if the contract has no receive/fallback function?
initcode and the
runtime code? What happens if the constructor reverts?
TIMESTAMP and BLOCKHASH considered
unreliable sources of randomness?
return a + b). Compile it with
solc --opcodes. Read the opcode output and explain what
each instruction does.
cast disassemble. Identify: the function dispatcher, the
SLOAD/SSTORE calls.
Build an EVM Instruction Tracer. Using
ethereumjs-vm (JavaScript) or py-evm (Python),
execute simple bytecode manually. For each opcode executed, log: current
opcode, gas remaining, stack state, and memory state. This gives you a
window into the exact execution of any Ethereum computation.
Gas is the metering system that prevents the halting problem — infinite loops — from being exploitable. Every EVM opcode has a defined gas cost. Transaction execution consumes gas; when gas is exhausted, the transaction reverts (all state changes rolled back) but the gas fee is still paid to the block producer.
Gas Cost Structure
Gas Optimization Patterns
| Pattern | Savings |
|---|---|
Pack multiple uints into one storage slot |
Up to 15,000 gas per eliminated SSTORE |
Use unchecked arithmetic in Solidity 0.8+ when
overflow is provably impossible
|
~200 gas per arithmetic op |
Use calldata instead of memory for
read-only function parameters
|
3–5x cheaper |
| Cache storage reads in memory variables | 2100 gas saved per additional read beyond the first |
Use custom errors instead of require with strings
|
Variable savings (~50+ gas per revert) |
| Short-circuit conditionals (cheapest check first) | Variable |
Use immutable instead of constant for
non-compile-time values
|
~200 gas per read vs SLOAD |
Gas Limit and the 63/64 Rule
When Contract A calls Contract B, A can specify the gas passed. EIP-150 introduced the 63/64 rule: Ethereum withholds 1/64 of the remaining gas for the calling frame even if the caller specifies "pass all gas." This ensures the caller has enough gas to handle the return and any cleanup.
Gas Griefing
An attack where a malicious callee intentionally uses up all forwarded gas to prevent the caller from completing. Defense: use Pull-over-Push payment patterns and avoid arbitrary external calls in critical paths.
Gas is not just a fee mechanism — it is the entire security model preventing computational abuse. Gas optimization directly impacts user experience (lower fees) and protocol competitiveness. Gas griefing is a real attack vector in multi-contract systems. Understanding the 63/64 rule is essential for safe inter-contract call design.
| Tool | Purpose |
|---|---|
foundry gas reports |
Automatic gas benchmarking per function |
hardhat-gas-reporter |
Gas analysis plugin for Hardhat |
evmdiff.com |
Compare opcodes and gas costs across EVM versions |
evm.codes |
Per-opcode gas cost breakdown |
uint128 and a uint128 in
separate storage slots cost twice as much as packing them into one
uint256 slot? What exactly happens at the EVM level?
gasleft() check defend against it in a contract that
processes deposits?
uint64 variables. First, declare them individually (four
separate slots). Then, declare them in one
uint64[4] stored in a struct. Measure the gas cost
difference for reading and writing all four.
require(condition, "Long error string"). Convert to a
custom error. Measure the gas savings on successful calls and on
reverts.
memory and once as
calldata. Measure gas on the call.
Gas Benchmark Suite. Write 10 pairs of "naive" and "optimized" implementations of common operations (e.g., array traversal, accumulation, token transfer). For each pair, write a Foundry test that compares the gas cost. Produce a report showing the gas savings of each optimization and explain the EVM-level reason.
Solidity is a statically-typed, compiled language with Rust-like ownership semantics for data locations and EVM-specific behaviors that make it unlike any other language.
Type System
| Type | Notes |
|---|---|
uint256, int256 |
Default integer type. Arithmetic is checked (overflow reverts) in >=0.8. |
address, address payable |
20-byte Ethereum address. Only address payable can
receive ETH via transfer/send.
|
bytes32, bytes |
Fixed and dynamic byte arrays. bytes32 is value type
(stored in one slot).
|
string |
Dynamic, UTF-8 encoded. Expensive to manipulate on-chain. |
bool |
ABI-encoded as a 32-byte word (not 1 bit) — no packing. |
struct |
Composite value type. Packing applies within structs. |
mapping(K => V) |
Hash table. Keys are hashed with slot index to find storage
location. mapping values are NOT iterable natively.
|
enum |
Represented as uint8 internally. |
ABI Encoding
The Application Binary Interface (ABI) is the standard encoding for function calls:
keccak256("functionName(paramTypes)").
Understanding ABI encoding is required for: manual transaction construction, low-level call composition, and debugging call failures.
Function Visibility and State Mutability
| Modifier | Meaning |
|---|---|
public |
Callable externally and internally; generates getter for state variables |
external |
Only callable from outside the contract |
internal |
Only callable from this contract and inheriting contracts |
private |
Only callable from this exact contract |
view |
Reads state but does not modify it. Compiled to STATICCALL internally for external calls. |
pure |
Neither reads nor modifies state. |
payable |
Can receive ETH. Functions without this modifier revert on ETH receipt. |
Critical: private does not mean "secret."
All contract storage is publicly readable from the blockchain.
private only restricts which code can call the
function or read the variable.
Inheritance and Linearization
Solidity uses C3 linearization for multiple inheritance. The order of inheritance matters significantly — functions are resolved in reverse order of the inheritance list.
Libraries
Error Handling
require(condition, "error"): Reverts on false. Gas
refunded for remaining gas.
revert CustomError(): Reverts with a typed error
(Solidity 0.8+). More gas-efficient than string errors.
assert(condition): Reverts on false with all gas
consumed. Used for invariant violations.
Yul / Inline Assembly
Solidity allows inline assembly via assembly { ... } blocks
using Yul, EVM's intermediate language. Used for:
Solidity has subtle traps that are not obvious from its syntax: integer
division truncates, address type conversion requires explicit casting,
delete on a mapping doesn't clear it, and
msg.value persists through internal calls. Mastering
Solidity means internalizing these traps and writing defensively around
them.
Ownable,
AccessControl, ERC20, or
ReentrancyGuard.
override and virtual keywords
enforce explicit override acknowledgment — a lesson learned from
security bugs caused by accidental function overrides.
| Tool | Purpose |
|---|---|
solc |
Solidity compiler |
Foundry (forge) |
All-in-one testing and build tool |
| Hardhat | Node.js-based Ethereum development environment |
| OpenZeppelin Contracts | Audited contract library |
slither |
Static analysis for Solidity |
| Remix IDE | Browser-based IDE for quick iteration |
memory and
storage reference types? What happens to memory when a
function returns?
Contract C is A, B if
A and B both define the same function? How is this resolved?
delete myMapping[key] not clear the mapping's
content? What does it actually do?
callFoo(uint256, bool, bytes).
What does the calldata look like byte-by-byte?
&& and || in Solidity? How can you
exploit this for gas optimization?
msg.value the same across reentrant calls in a
single transaction? What attack vector does this create if not
handled?
receive() function vs the
fallback() function? Under what conditions is each
triggered?
transfer(address to, uint256 amount) with
hardcoded values. Verify by comparing to
abi.encodeWithSignature.
memcpy(dest, src, len) in Yul inline assembly. Compare
gas to the Solidity equivalent.
cast storage to read each slot directly. Verify the
packing of small types.
Build a feature-complete ERC-20 token from scratch.
Without inheriting OpenZeppelin, implement: transfer,
transferFrom, approve, mint,
burn, Ownable (only owner can mint/burn),
Pausable (owner can pause all transfers), and
permit (EIP-2612 signature-based approval). Write natspec
documentation for all functions. This exercise forces mastery of
Solidity fundamentals without shortcuts.
Checks-Effects-Interactions (CEI)
The most important pattern for reentrancy prevention. Structure every state-modifying function in three phases:
Violation: performing an external call before updating state allows the callee to reenter and observe stale state.
Pull-over-Push Payments
Never send ETH via push (calling the recipient from your contract).
Instead, track owed balances in a mapping and let recipients
withdraw() themselves. This isolates the risk of the
recipient's address being a malicious contract.
Proxy Patterns for Upgradeability
Upgradeability in an immutable system is an architectural challenge. Proxy patterns solve this via DELEGATECALL:
Storage Layout in Proxies
DELEGATECALL executes code in the caller's storage context. If the proxy and logic contract have different storage layouts, variables overwrite each other — a storage collision. EIP-1967 defines standard storage slots for proxy admin and implementation addresses using pseudo-random slots within the implementation's storage space.
Factory Pattern
A factory contract deploys instances of a child contract. Enables: batch deployment, tracking deployed instances, and centralized initialization. Often combined with CREATE2 for deterministic child addresses.
Access Control
Ownable: Single owner (usually deployer) controls
privileged functions.
AccessControl (OpenZeppelin): Role-based access with
granular permissions. Multiple roles (e.g., MINTER_ROLE, PAUSER_ROLE).
Roles can be granted/revoked per address.
Timelock: Privileged operations must be queued and
executed after a delay, giving users time to exit before potentially
harmful changes take effect.
Production smart contracts are not isolated functions — they are systems of interacting contracts that manage thousands of participants and billions of dollars. Architecture patterns provide the structure for multi-contract systems, upgrade paths, and privilege management. The wrong architecture creates systemic risk; the right architecture creates a system that evolves safely.
| Tool | Purpose |
|---|---|
| OpenZeppelin Upgrades Plugins | Safe proxy deployment and upgrade validation |
hardhat-deploy |
Deployment scripts with proxy management |
foundry upgradeability tests |
Test proxy pattern correctness |
upgradeTo from
within the logic contract's initialize function?
constructor and
initialize in upgradeable contracts, and why can't you
use a constructor with a proxy?
storageLayout plugin to validate that an upgrade doesn't
break the storage layout.
Build an Upgradeable Token Vault. A UUPS-upgradeable contract where users can deposit ERC-20 tokens. Version 1: basic deposit and withdraw with per-user balance tracking. Version 2: add a withdrawal fee (without breaking existing balances). Demonstrate: deploying V1, making deposits, upgrading to V2, verifying balances are preserved, verifying fee is now charged on withdrawal.
Testing Hierarchy
| Test Type | What It Checks | Tools |
|---|---|---|
| Unit tests | Individual function behavior | Foundry, Hardhat |
| Integration tests | Multi-contract interactions | Foundry with forks, Hardhat |
| Fuzz tests | Function behavior over random inputs | Foundry forge fuzz |
| Invariant tests | System-wide properties that must always hold | Foundry forge invariant |
| Formal verification | Mathematical proof of correctness | Certora, Halmos |
Foundry Testing
Foundry is the preferred tool for serious smart contract testing. Key features:
contract TokenTest is Test {
Token token;
function setUp() public {
token = new Token(1_000_000e18);
}
function test_transferReducesSenderBalance() public {
token.transfer(alice, 100e18);
assertEq(token.balanceOf(alice), 100e18);
}
// Fuzz test: runs 256 times with random inputs
function test_fuzz_transferNeverOverflows(uint256 amount) public {
amount = bound(amount, 0, token.balanceOf(address(this)));
token.transfer(alice, amount);
assertLe(token.balanceOf(alice), token.totalSupply());
}
// Invariant test: Foundry calls handler functions in random order,
// then checks invariants after each call sequence
function invariant_totalSupplyEqualsAllBalances() public {
assertEq(token.totalSupply(), sumOfAllBalances());
}
}
Mainnet Forking
Foundry and Hardhat support forking mainnet state at a specific block. This enables testing your contract against real deployed contracts (real USDC, real Uniswap pools) without touching mainnet.
forge test --fork-url $ETH_RPC_URL --fork-block-number 18000000
Trace Debugging
Foundry's forge test -vvvv outputs a full execution trace
with every call, every storage access, and revert reasons.
cast run <txhash> --rpc-url traces a mainnet
transaction.
Formal Verification with Halmos
Halmos performs symbolic execution — instead of running with concrete input values, it explores all possible inputs using constraint solving (SMT). It can prove that a property holds for all inputs or find a counterexample.
Testing in smart contract development is not optional hygiene — it is mandatory engineering. The cost of a bug in production is irreversible fund loss. The DeFi ecosystem has lost billions to exploits that would have been caught by invariant tests. Coverage metrics are insufficient; what matters is whether you have tested the right properties.
totalBorrow <= totalDeposits —
a property that was violated by the exploit.
| Tool | Purpose |
|---|---|
Foundry (forge) |
Testing, fuzzing, invariant testing, deployment |
| Hardhat | JS-based testing with Mocha/Chai |
forge coverage |
Measure code coverage |
| Halmos | Symbolic execution for Solidity |
| Certora Prover | Formal verification via specification language (CVL) |
slither |
Static analysis — detects 90+ vulnerability patterns |
mythril |
Symbolic execution-based security scanner |
slither on your
ERC-20 token. Analyze each finding — is it a true positive or false
positive? Fix the true positives.
Test Coverage Engineering. Take an existing open-source DeFi contract (e.g., a Compound fork or Uniswap V2 pair). Write a comprehensive Foundry test suite including: unit tests for every function, fuzz tests for all arithmetic operations, and 10+ invariant tests. Achieve 100% line coverage. Produce a security report identifying any gaps between coverage and actual security assurance.
Reentrancy
The most infamous smart contract vulnerability class. A malicious external call executes before the caller's state is updated.
// VULNERABLE: Effects after Interactions
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
(bool success, ) = msg.sender.call{value: amount}(""); // External call!
require(success);
balances[msg.sender] -= amount; // State updated AFTER call
}
Attack: the recipient's receive() function re-calls
withdraw() before balances is updated. Fix:
Update state BEFORE the external call (CEI pattern) or use a
ReentrancyGuard mutex.
Variants:
Integer Overflow/Underflow (Pre-0.8)
Before Solidity 0.8, arithmetic silently wrapped around.
uint8(255) + 1 == 0. The Proof-of-Concept exploit of the
BatchOverflow bug on BEC token (2018) minted $900M worth of tokens from
overflow.
Fix: Use Solidity 0.8+ (checked arithmetic by default) or OpenZeppelin's
SafeMath for 0.7 and earlier.
Access Control Failures
Missing or incorrect permission checks. Functions that should be restricted are public or reachable via an unexpected call path.
Example: The Parity multisig wallet bug left the
initWallet function public, allowing anyone to become the
owner.
Front-Running and Sandwich Attacks
MEV bots monitor the mempool and inject transactions before and after a victim's transaction:
Defenses: commit-reveal schemes, private mempools (Flashbots Protect), slippage limits.
Oracle Manipulation
On-chain oracles (like Uniswap V2 spot price) can be manipulated within a single transaction using flash loans. Protocol that reads a spot price (not a time-weighted average) is vulnerable.
Example: The Harvest Finance exploit (2020, $34M) manipulated USDC/USDT prices in Curve's pool via flash loans to drain the vault.
Fix: Use time-weighted average prices (TWAP), multiple oracle sources, and Chainlink decentralized oracles.
Flash Loan Attacks
Flash loans provide unsecured, immediate liquidity that must be repaid in the same transaction. They amplify the attacker's effective capital for oracle manipulation, governance attacks, and arbitrage exploits.
DELEGATECALL Storage Collision
When a proxy uses DELEGATECALL, the logic contract's storage layout must exactly match the proxy's. If the proxy has state variables in slots 0–2 and the logic contract defines different variables in those slots, writes to the logic variables corrupt the proxy's admin address.
Signature Replay
A valid signature for one transaction can be replayed in another context if the message doesn't include a unique identifier (nonce, chain ID, contract address).
Example: A signed "approve this withdrawal" message replayed on a fork chain or against a different contract instance.
Fix: EIP-712 typed structured data signing includes domain separator (chain ID + contract address + version).
tx.origin Phishing
tx.origin returns the original EOA that initiated the
transaction. A malicious contract tricking a user into calling it can
then call the victim contract and pass the
require(tx.origin == owner) check.
Fix: Never use tx.origin for authorization. Use
msg.sender.
Denial of Service via Gas Exhaustion
A contract that loops over an unbounded array can be griefed by an attacker who makes the array large enough that the loop runs out of gas. Example: a contract that distributes ETH to all holders in a loop — anyone can add many zero-value holder addresses.
Every vulnerability class here has caused real, significant financial losses. The combined DeFi hacks from 2020–2024 total over $10 billion. Cryptocurrency transactions are irreversible. Smart contract developers must internalize every vulnerability class and audit their code against each one before deployment.
| Exploit | Year | Loss | Vulnerability |
|---|---|---|---|
| The DAO | 2016 | $60M | Reentrancy |
| Parity Multisig | 2017 | $150M | Missing access control |
| bZx | 2020 | $8M | Oracle manipulation (flash loan) |
| Harvest Finance | 2020 | $34M | Oracle manipulation |
| Cream Finance | 2021 | $130M | Reentrancy (cross-function) |
| Nomad Bridge | 2022 | $190M | Access control / initialization |
| Euler Finance | 2023 | $197M | Missing health check (unexpected state path) |
| Tool | Purpose |
|---|---|
slither |
Static analysis for 90+ vulnerability patterns |
mythril |
Symbolic execution vulnerability scanner |
echidna |
Property-based fuzzer for security testing |
| Foundry | Exploit PoC scripting |
| Tenderly | Transaction simulation and trace analysis |
for loop. Identify the DoS vector and propose a fix.
address admin in slot 0 and the logic contract
stores address owner in slot 0. What happens when the
logic contract writes to owner?
tx.origin considered dangerous for authorization?
Construct an exact attack scenario.
withdraw function, implement an attacking contract, and
run the exploit in a Foundry test. Then fix the vulnerability with CEI
and ReentrancyGuard, and verify the attack no longer works.
Audit a Protocol. Take an open-source, publicly known vulnerable contract (e.g., from Damn Vulnerable DeFi or EtherNaut). For each of the 12+ vulnerability classes covered in this topic: (a) describe whether the contract is vulnerable, (b) if vulnerable, write a working exploit as a Foundry test, (c) propose a fix. Write a formal security report with: executive summary, findings ranked by severity, recommended remediations.
Build a basic lending protocol: deposit ERC-20 collateral, borrow against it, repay loans, and liquidate under-collateralized positions. Implement proper access control, reentrancy guards, and a simulated price oracle. Avoid all known vulnerability classes.
A UUPS-upgradeable contract where sellers can list ERC-721 NFTs at a price, and buyers can purchase them (ERC-20 payment). Version 1: fixed-price sales. Version 2: add auction functionality. Demonstrate upgrade without losing seller listings.
Implement on-chain governance: token holders vote on proposals, proposals have a time delay before execution, and approved proposals call arbitrary functions via a timelock. Model after Compound Governor Bravo.
Build a minimal over-collateralized stablecoin (inspired by MakerDAO):
| Mistake | Reality |
|---|---|
"private means secret" |
Private state variables are fully readable by anyone who reads the blockchain. Use encryption off-chain for actual secrets. |
| "Solidity 0.8 prevents all overflow" |
unchecked blocks disable overflow protection.
Libraries and assembly bypass it entirely.
|
| "ReentrancyGuard prevents all reentrancy" |
Read-only reentrancy bypasses the guard because
view functions don't trigger it.
|
| "Audit = complete security" | Audits find bugs based on auditor skill and scope. Formal verification provides stronger guarantees. Novel attack vectors are discovered post-audit regularly. |
| "Gas fees are the user's problem" | Contracts that make users pay excessive gas will not be used. Gas optimization is a UX requirement. |
| "Hard-coded addresses are fine" | Dependency on hard-coded deployed contract addresses creates fragility across networks and protocol upgrades. |
At this stage, internalize these disciplines:
Books
Papers
Documentation
Security Resources