A hands-on Solidity training ground covering every section of solidity-by-example.org.
For each topic: a contract, a test, and where relevant a fuzz test and an invariant test. Everything runs inside Docker — no local Foundry install needed. NatSpec is mandatory on every contract. Inline comments explain the WHY, not the what.
This is not a reference. It is a dojo. You read, you implement, you break it, you fix it.
All tooling runs inside a container. You need Docker Desktop (Windows/macOS) or Docker Engine (Linux). Nothing else.
# First run — builds the image (~5-10 min)
docker compose up -d
# Every session
docker compose exec dojo bash
# Inside the container
forge --version # should print forge 0.2.xDockerfile — place at repo root:
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
curl git build-essential pkg-config libssl-dev \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
RUN curl -L https://foundry.paradigm.xyz | bash
ENV PATH="/root/.foundry/bin:$PATH"
RUN foundryup
WORKDIR /workspacedocker-compose.yml:
services:
dojo:
build: .
volumes:
- .:/workspace
- dojo-foundry:/root/.foundry
stdin_open: true
tty: true
volumes:
dojo-foundry:Project init (run once inside the container):
forge init solidity-by-example
cd solidity-by-example
forge install OpenZeppelin/openzeppelin-contracts --no-commit
forge install transmissions11/solmate --no-commitfoundry.toml:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc = "0.8.26"
[fuzz]
runs = 256
[invariant]
runs = 64
depth = 32src/
basic/
applications/
hacks/
evm/
defi/
test/
basic/
applications/
hacks/
evm/
defi/
Each topic gets one contract file and one test file. Test files contain all three test types when applicable: unit, fuzz, invariant.
Unit test — specific scenario, deterministic input. Every topic gets at least one.
Fuzz test — Foundry generates random inputs. Use when a function's behavior should hold across a range of values (math, state transitions, access control).
function testFuzz_deposit(uint256 amount) public {
amount = bound(amount, 1, 1e27); // clamp to valid range
// ...
}Invariant test — a property that must hold after ANY sequence of calls. Use for stateful contracts (vaults, AMMs, auctions, payment channels).
function invariant_totalSupplyMatchesBalances() public view {
assertEq(token.totalSupply(), handler.ghost_totalMinted() - handler.ghost_totalBurned());
}Use the handler pattern for invariant tests — never target the contract directly. The handler bounds inputs, tracks ghost variables, and filters out invalid calls.
Every contract you write must follow this format:
/// @title ContractName
/// @notice Plain English description for users.
/// @dev Technical notes: upgrade status, audit status, key invariants, known limitations.
contract ContractName {
/// @notice Description of what this state variable represents.
/// @dev Note any unit (wei, basis points, seconds) and constraints.
uint256 public someValue;
/// @notice Emitted when something important happens.
/// @param user The address that triggered the event.
/// @param value The value associated with the event.
event SomethingHappened(address indexed user, uint256 value);
/// @notice Thrown when the caller provides an invalid input.
/// @param provided What the caller sent.
/// @param expected What was expected.
error InvalidInput(uint256 provided, uint256 expected);
/// @notice Does X for the caller.
/// @dev Explain CEI pattern, reentrancy risk, or any non-obvious implementation detail.
/// @param input Description including units and valid range.
/// @return result Description of what is returned.
function doSomething(uint256 input) external returns (uint256 result) {Use // SECURITY: for security-critical lines, // CEI: to mark Check-Effects-Interactions steps, // INVARIANT: to mark enforced invariants.
Concepts: SPDX license, pragma, contract declaration, state variable, public getter.
Contract: src/basic/HelloWorld.sol
Declares a string public greet initialized to "Hello World".
Test: test/basic/HelloWorld.t.sol
- Unit: assert
greet()returns"Hello World"
Fuzz: no — no inputs. Invariant: no — no state transitions.
Key comment to include:
Why public auto-generates a getter. Why SPDX is required. What pragma solidity does and why you pin a version range.
Concepts: reading and writing state, increment/decrement patterns.
Contract: src/basic/Counter.sol
A uint256 public count with inc(), dec(), and get().
Test: test/basic/Counter.t.sol
- Unit: inc increments by 1, dec decrements by 1, dec on 0 reverts (underflow)
Fuzz: testFuzz_inc(uint8 times) — call inc N times, assert count == N.
Invariant: no — too simple, covered by fuzz.
Key comment: why uint256 underflows in Solidity 0.8+ and reverts automatically.
Concepts: bool, uint, int, address, bytes32, default values.
Contract: src/basic/Primitives.sol
Declares one public state variable of each primitive type with their default values.
Test: test/basic/Primitives.t.sol
- Unit: assert each variable equals its type's default value
Fuzz: testFuzz_uint256_never_negative(uint256 x) — assert x >= 0 (always true, shows type safety).
Invariant: no.
Key comment: int vs uint range, why address is 20 bytes, bytes32 vs string.
Concepts: local variables (stack/memory), state variables (storage), global variables (msg.sender, block.timestamp, block.number).
Contract: src/basic/Variables.sol
Exposes all three categories. A function that returns msg.sender, block.timestamp, block.number.
Test: test/basic/Variables.t.sol
- Unit:
vm.prank(alice)→ assertmsg.senderreturns alice - Unit:
vm.warp(1000)→ assertblock.timestampreturns 1000 - Unit:
vm.roll(42)→ assertblock.numberreturns 42
Fuzz: testFuzz_timestamp(uint48 ts) — warp to ts, assert returned value matches.
Invariant: no.
Concepts: constant keyword, compile-time values, gas savings vs storage reads.
Contract: src/basic/Constants.sol
Declares address public constant MY_ADDRESS and uint256 public constant MY_UINT.
Test: test/basic/Constants.t.sol
- Unit: assert values match expected literals
Fuzz: no.
Invariant: invariant_constants_never_change — trivially true, good intro to invariant syntax.
Key comment: constants cost ~200 gas to read vs ~2100 for a storage variable (SLOAD). Why this matters in hot paths.
Concepts: immutable keyword, set in constructor only, cheaper than storage.
Contract: src/basic/Immutable.sol
uint256 public immutable MY_UINT set in constructor.
Test: test/basic/Immutable.t.sol
- Unit: assert value matches constructor arg
- Unit: deploy with different values, assert each instance is independent
Fuzz: testFuzz_constructor(uint256 val) — deploy with fuzzed val, assert stored value matches.
Invariant: no.
Key comment: immutable is stored in bytecode, not storage. Reading it costs ~3 gas (like a constant) but the value is set at deployment time, not compile time.
Concepts: SSTORE vs SLOAD, getter functions, setter functions.
Contract: src/basic/SimpleStorage.sol
uint256 public num with set(uint256) and get().
Test: test/basic/SimpleStorage.t.sol
- Unit: set then get returns same value
- Unit: multiple sets, final get returns last value
Fuzz: testFuzz_set_get(uint256 x) — set x, assert get() == x.
Invariant: invariant_get_reflects_last_set — ghost variable tracks last set value, assert get() matches.
Concepts: ether, wei, gwei literals, 1 ether == 1e18 wei, address.balance.
Contract: src/basic/EtherUnits.sol
Exposes conversions as public pure functions. A payable receive function that tracks received ether.
Test: test/basic/EtherUnits.t.sol
- Unit: assert
1 ether == 1e18 - Unit: assert
1 gwei == 1e9 - Unit:
vm.deal(addr, 1 ether)→ assertaddr.balance == 1e18
Fuzz: testFuzz_wei_to_ether(uint64 gwei_amount) — convert, assert no overflow.
Invariant: no.
Key comment: why ETH is always denominated in wei internally. Why literals like 1 ether are just syntactic sugar for 1e18.
Concepts: gasleft(), tx.gasprice, block.basefee, EIP-1559 basics.
Contract: src/basic/Gas.sol
A function that returns gasleft() before and after an operation, to show gas consumption.
Test: test/basic/Gas.t.sol
- Unit: assert
gasleft()decreases after a storage write - Unit: assert a view function costs less gas than a state-modifying one
Fuzz: no — gas costs are deterministic per opcode. Invariant: no.
Key comment: difference between gas limit, gas used, and gas price. Why out-of-gas reverts the entire transaction. EIP-1559: base fee is burned, priority fee goes to the validator.
Concepts: conditional branching, ternary operator.
Contract: src/basic/IfElse.sol
Pure functions demonstrating if/else if/else and ternary.
Test: test/basic/IfElse.t.sol
- Unit: test each branch
Fuzz: testFuzz_ternary(uint256 x) — assert ternary and if/else produce identical results.
Invariant: no.
Concepts: for, while, break, continue, gas cost of loops.
Contract: src/basic/Loop.sol
Pure functions: sum from 1 to N using for loop, while loop. A function that breaks early.
Test: test/basic/Loop.t.sol
- Unit: sum(10) == 55
- Unit: break exits early
Fuzz: testFuzz_for_while_equivalent(uint8 n) — assert for-loop sum == while-loop sum.
Invariant: no.
Key comment: why unbounded loops are dangerous in production — every iteration costs gas, and if the loop exceeds the block gas limit the transaction reverts. Always bound loops by a max iteration count.
Concepts: mapping(K => V), nested mappings, default values, no iteration.
Contract: src/basic/Mapping.sol
mapping(address => uint256) public balances. Set, get, delete functions. A nested mapping(address => mapping(address => bool)) public nested.
Test: test/basic/Mapping.t.sol
- Unit: unset key returns 0
- Unit: set then get returns value
- Unit: delete resets to 0
Fuzz: testFuzz_set_get(address key, uint256 val) — set val at key, assert retrieval matches.
Invariant: no.
Key comment: mappings have no length, no iteration, no list of keys. You cannot enumerate them. If you need iteration, use a separate array to track keys (see Iterable Mapping in Applications).
Concepts: fixed-size vs dynamic arrays, push, pop, length, delete, storage vs memory arrays.
Contract: src/basic/Array.sol
Dynamic uint256[] public arr. Functions: push, pop, get by index, get length, remove by index (shift and swap-delete patterns).
Test: test/basic/Array.t.sol
- Unit: push increases length
- Unit: pop decreases length, removes last element
- Unit: delete leaves a zero, does not change length
- Unit: remove-by-swap changes length, does not preserve order
- Unit: out-of-bounds access reverts
Fuzz: testFuzz_push_pop(uint8 n) — push n elements, pop n elements, assert length == 0.
Invariant: invariant_length_matches_ghost — handler tracks expected length, assert matches.
Key comment: swap-delete is O(1) but breaks ordering. Shift-delete preserves order but is O(n) and expensive on large arrays. Choose based on whether order matters.
Concepts: enums, default value (first member), explicit casting to/from uint8.
Contract: src/basic/Enum.sol
enum Status { Pending, Active, Inactive }. State variable, getter, setter, reset.
Test: test/basic/Enum.t.sol
- Unit: default value is
Pending(0) - Unit: set to each member, assert value
- Unit: casting to uint8
Fuzz: testFuzz_set(uint8 raw) — only valid enum values (0-2) should succeed, others revert.
Invariant: no.
Concepts: type X is Y, zero-cost abstraction over primitives, .wrap() and .unwrap().
Contract: src/basic/UserDefinedValueTypes.sol
type Price is uint256. Math functions that operate on Price without unwrapping unnecessarily.
Test: test/basic/UserDefinedValueTypes.t.sol
- Unit: wrap then unwrap returns original value
- Unit: type safety — Price cannot be used where uint256 is expected without explicit cast
Fuzz: testFuzz_wrap_unwrap(uint256 x) — assert roundtrip is lossless.
Invariant: no.
Concepts: struct declaration, storage vs memory initialization, nested structs, struct arrays.
Contract: src/basic/Structs.sol
struct Todo { string text; bool completed; }. Array of Todos with create, update text, toggle completed functions.
Test: test/basic/Structs.t.sol
- Unit: create todo, assert fields
- Unit: update text, assert updated
- Unit: toggle completed twice returns to original state
Fuzz: no — string fuzzing is complex; cover with unit tests.
Invariant: invariant_todos_length_only_grows — no delete function, so length must be monotonically increasing.
Concepts: storage (persistent), memory (temporary), calldata (read-only input), gas cost differences.
Contract: src/basic/DataLocations.sol
Functions demonstrating each location. A function that shows the difference between modifying a storage reference vs a memory copy.
Test: test/basic/DataLocations.t.sol
- Unit: modifying a storage reference updates state
- Unit: modifying a memory copy does NOT update state
- Unit: calldata cannot be modified (compile-time check — document this)
Fuzz: no. Invariant: no.
Key comment: using calldata instead of memory for read-only function parameters saves gas — no copy is made. Always use calldata for external function array/string parameters you don't modify.
Concepts: EIP-1153, tstore / tload opcodes, slot cleared after transaction ends, use case: reentrancy locks.
Contract: src/basic/TransientStorage.sol
A reentrancy lock using transient storage instead of a boolean storage variable. Show cost difference.
Test: test/basic/TransientStorage.t.sol
- Unit: lock is set during call, cleared after
- Unit: reentrant call while locked reverts
- Unit: new transaction sees cleared lock
Fuzz: no.
Invariant: invariant_lock_cleared_after_call — after any call sequence, transient lock must be 0.
Key comment: transient storage is ~100x cheaper than regular storage for temporary values. Cleared at the end of every transaction (not just the call). Perfect for reentrancy guards and flash loan callbacks.
Concepts: function visibility (external, public, internal, private), mutability (pure, view, payable), modifiers.
Contract: src/basic/FunctionTypes.sol
A contract demonstrating all visibility and mutability combinations with NatSpec explaining when to use each.
Test: test/basic/FunctionTypes.t.sol
- Unit: test each function type works as expected
- Unit: verify pure functions cannot modify state (compile time)
- Unit: verify view functions cannot modify state (compile time)
Fuzz: no. Invariant: no.
Key comment: Visibility: external is cheaper than public for external calls (args read from calldata). Mutability: pure/view allow static calls, save gas by preventing SSTORE.
Concepts: view reads state but doesn't modify, pure doesn't read or modify state.
Contract: src/basic/ViewAndPure.sol
Demonstrates both types, including why a pure function can call other pure functions but not view functions that read state.
Test: test/basic/ViewAndPure.t.sol
- Unit: view function reads state correctly
- Unit: pure function computes correctly without reading state
- Unit: pure function calling pure works
Fuzz: testFuzz_pure_add(uint256 a, uint256 b) — assert add(a,b) == a + b.
Invariant: no.
Concepts: custom errors with error keyword, cheaper than revert strings, used with revert ErrorName().
Contract: src/basic/CustomError.sol
Demonstrates custom errors vs require strings with gas comparison.
Test: test/basic/CustomError.t.sol
- Unit: custom error reverts with correct selector
- Unit: compare gas costs (custom error vs require string)
Fuzz: no. Invariant: no.
Key comment: custom errors use 4-byte selector + abi-encoded params vs storing a full string in bytecode. Much cheaper for common revert paths.
Concepts: modifiers for reusable validation logic, _ placeholder, multiple modifiers, modifier arguments.
Contract: src/basic/FunctionModifier.sol
Modifiers: onlyOwner, noReentrancy, validAddress, minAmount(uint256). Show order of execution.
Test: test/basic/FunctionModifier.t.sol
- Unit: onlyOwner rejects non-owner
- Unit: noReentrancy blocks reentrant calls
- Unit: validAddress rejects zero address
- Unit: modifiers execute in declared order
Fuzz: no.
Invariant: invariant_owner_cannot_be_zero — owner is never zero address.
Concepts: event declaration, emit keyword, indexed topics (max 3), gas cost of logs.
Contract: src/basic/Events.sol
Multiple events: simple, with indexed params, all indexed. Function that emits for testing.
Test: test/basic/Events.t.sol
- Unit: event emitted with correct params
- Unit: indexed params are topics
- Unit: expectEmit works for topic and data verification
Fuzz: no. Invariant: no.
Key comment: Events are the cheapest form of onchain storage (~375 gas per topic + 8 gas per byte). Use indexed params for efficient filtering. Unindexed params go in data (cheaper, not searchable).
Concepts: constructor keyword, runs once at deployment, can take arguments, sets immutable state.
Contract: src/basic/Constructor.sol
Constructor takes owner address and initial value. Sets immutables and state.
Test: test/basic/Constructor.t.sol
- Unit: constructor sets owner correctly
- Unit: constructor sets value correctly
Fuzz: testFuzz_constructor_values(address owner, uint256 value) — deploy with fuzzed args, assert stored correctly.
Invariant: no.
Concepts: is keyword, virtual and override, super, constructor chaining, multiple inheritance with C3 linearization.
Contract: src/basic/Inheritance.sol
Base contract with virtual function. Derived contract overrides. Multiple inheritance example with C3 ordering.
Test: test/basic/Inheritance.t.sol
- Unit: override works correctly
- Unit: super calls parent implementation
- Unit: constructor chaining sets all values
Fuzz: no. Invariant: no.
Concepts: variable shadowing when child declares same name, how to access parent's variable.
Contract: src/basic/Shadowing.sol
Parent has uint256 public value. Child shadows it. Show how to disambiguate.
Test: test/basic/Shadowing.t.sol
- Unit: child's value is separate from parent's
- Unit: can access both via explicit casting or getter
Fuzz: no. Invariant: no.
Key comment: Shadowing is confusing. Avoid it. If you need to override behavior, use functions not variables.
Concepts: direct parent call vs super, multiple inheritance with super.
Contract: src/basic/CallingParent.sol
Multiple parents with same function name. Shows direct call vs super behavior (C3 linearization).
Test: test/basic/CallingParent.t.sol
- Unit: direct parent call executes only that parent
- Unit: super executes parents in C3 linearization order
Fuzz: no. Invariant: no.
Concepts: private, internal, external, public. Who can call what.
Contract: src/basic/Visibility.sol
Same function name with all 4 visibilities. External functions to test each.
Test: test/basic/Visibility.t.sol
- Unit: external callable from outside only
- Unit: public callable from inside and outside
- Unit: internal callable from derived contracts
- Unit: private only callable from this contract
Fuzz: no. Invariant: no.
Key comment: Default to external when possible (cheaper). Use public only if called internally. Internal for shared logic. Private for truly internal helpers.
Concepts: interface keyword, all functions external, no implementation, used for contract interaction.
Contract: src/basic/Interface.sol
ERC20-like interface. Contract that uses the interface to call external tokens.
Test: test/basic/Interface.t.sol
- Unit: can call external contract through interface
- Unit: interface enforces function signature
Fuzz: no. Invariant: no.
Concepts: payable keyword, msg.value, address(this).balance, transferring ether.
Contract: src/basic/Payable.sol
Payable functions, receive(), fallback(). Functions to deposit and withdraw.
Test: test/basic/Payable.t.sol
- Unit: payable function receives ether
- Unit: receive() called on plain ether transfer
- Unit: withdraw transfers ether correctly
Fuzz: testFuzz_deposit_withdraw(uint96 amount) — deposit amount, withdraw, assert balance changes correctly.
Invariant: invariant_balance_matches_deposits_minus_withdrawals — ghost accounting tracks net deposits, matches balance.
Concepts: transfer (2300 gas, reverts), send (2300 gas, returns bool), call (forwards all gas, returns bool+data).
Contract: src/basic/SendingEther.sol
Functions demonstrating all three methods with security considerations.
Test: test/basic/SendingEther.t.sol
- Unit: transfer reverts on failure
- Unit: send returns false on failure
- Unit: call returns success boolean
- Unit: call to contract with expensive fallback works (transfer would fail)
Fuzz: no. Invariant: no.
Key comment: call is preferred post-2020. Use with reentrancy guards. Always check return value. transfer/send have 2300 gas stipend that may break with smart contract wallets.
Concepts: fallback() external payable, called when no function matches, used for proxy patterns.
Contract: src/basic/Fallback.sol
Contract with fallback that emits an event with the calldata. Shows proxy pattern use case.
Test: test/basic/Fallback.t.sol
- Unit: fallback called on unknown function selector
- Unit: fallback receives ether when no receive() defined
- Unit: fallback gets correct calldata
Fuzz: no. Invariant: no.
Concepts: low-level call, delegatecall, staticcall, abi.encodeWithSelector, return data handling.
Contract: src/basic/Call.sol
Low-level call examples: calling specific functions, handling return data, checking success.
Test: test/basic/Call.t.sol
- Unit: call with encoded selector succeeds
- Unit: can decode return data
- Unit: failed call returns false (doesn't revert)
Fuzz: no. Invariant: no.
Key comment: low-level call bypasses type safety. Use only when necessary (proxies, generic call routers). Always check success boolean.
Concepts: delegatecall runs code in caller's context, preserves msg.sender and msg.value, storage layout must match.
Contract: src/basic/Delegatecall.sol
Implementation contract with state. Proxy contract using delegatecall. Show storage collision risks.
Test: test/basic/Delegatecall.t.sol
- Unit: delegatecall runs in caller's context
- Unit: msg.sender preserved through delegatecall
- Unit: state changes in caller's storage, not implementation
Fuzz: no. Invariant: no.
Key comment: delegatecall is powerful and dangerous. Storage layout MUST match exactly between proxy and implementation. Use standard proxy patterns (EIP-1967, EIP-1822).
Concepts: first 4 bytes of keccak256(function signature), used for function dispatch.
Contract: src/basic/FunctionSelector.sol
Functions to compute selectors, show how Solidity uses them internally.
Test: test/basic/FunctionSelector.t.sol
- Unit: selector computed correctly
- Unit: different params produce different selectors
Fuzz: no. Invariant: no.
Concepts: using contract type vs address + interface, type safety benefits.
Contract: src/basic/CallingContract.sol
Contract that calls another contract using both typed and low-level approaches.
Test: test/basic/CallingContract.t.sol
- Unit: typed call is safer
- Unit: low-level call with correct selector works
- Unit: low-level call with wrong selector fails gracefully
Fuzz: no. Invariant: no.
Concepts: new keyword, passing ether on creation, getting the created address.
Contract: src/basic/ContractFactory.sol
Factory that creates instances of another contract, tracks them in an array.
Test: test/basic/ContractFactory.t.sol
- Unit: factory creates contract
- Unit: created contract has correct constructor args
- Unit: can send ether during creation
Fuzz: testFuzz_create_multiple(uint8 n) — create n contracts, assert count matches.
Invariant: invariant_factory_tracks_all_children — sum of tracked children equals actual created count.
Concepts: try/catch for external calls, different catch blocks for different error types.
Contract: src/basic/TryCatch.sol
Contract that makes external calls with try/catch, handles success, revert, and panic.
Test: test/basic/TryCatch.t.sol
- Unit: try succeeds, returns value
- Unit: catch on revert with error string
- Unit: catch on custom error
- Unit: catch on panic (division by zero)
Fuzz: no. Invariant: no.
Concepts: import statement, local vs npm vs GitHub imports, named imports.
Contract: src/basic/Import.sol
Uses OpenZeppelin and Solmate imports to demonstrate different import styles.
Test: test/basic/Import.t.sol
- Unit: imported contracts work correctly
- Unit: named imports work
Fuzz: no. Invariant: no.
Concepts: library keyword, internal functions embedded in calling contract, external functions deployed separately and called via DELEGATECALL.
Contract: src/basic/Library.sol
Math library with internal functions, sorting library with external functions.
Test: test/basic/Library.t.sol
- Unit: internal library function works
- Unit: external library function works (deployed separately)
- Unit: using-for syntax works
Fuzz: testFuzz_math(uint256 a, uint256 b) — library math operations are correct.
Invariant: no.
Key comment: Internal library functions are copied into the calling contract at compile time (no external call). External library functions are deployed once and DELEGATECALL'd (saves bytecode, costs extra gas).
Concepts: abi.encode, abi.encodePacked, abi.decode, keccak256 for hashing.
Contract: src/basic/AbiEncode.sol
Functions demonstrating encoding for different use cases (calls, hashing, tight packing).
Test: test/basic/AbiEncode.t.sol
- Unit: abi.encode produces correct format
- Unit: abi.encodePacked produces tight packing
- Unit: abi.decode correctly decodes
- Unit: keccak256 of encoded matches expected
Fuzz: no. Invariant: no.
Key comment: encode for standard ABI encoding (function calls). encodePacked for tight packing (signatures, merkle proofs). decode for unpacking returned bytes.
Concepts: keccak256 hash function, used for unique IDs, commitment schemes, signatures.
Contract: src/basic/Keccak256.sol
Hashing examples: unique IDs, simple commitment scheme, merkle leaf hashing.
Test: test/basic/Keccak256.t.sol
- Unit: same input produces same hash
- Unit: different inputs produce different hashes
- Unit: commitment scheme works (hide then reveal)
Fuzz: no. Invariant: no.
Concepts: ecrecover, ECDSA signature verification, message prefix, signature malleability.
Contract: src/basic/VerifySignature.sol
Functions to verify ECDSA signatures, handle Ethereum signed message prefix.
Test: test/basic/VerifySignature.t.sol
- Unit: valid signature verifies correctly
- Unit: wrong signer fails verification
- Unit: tampered message fails verification
Fuzz: no. Invariant: no.
Key comment: Always hash with Ethereum message prefix (\x19Ethereum Signed Message:\n32). Check for signature malleability (s in lower half of secp256k1 curve). Consider using OpenZeppelin's ECDSA library.
Concepts: all data is public on blockchain, private only hides from other contracts, storage layout lets you read anything.
Contract: src/basic/PrivateData.sol
Contract with private variables. Show how to read them via storage slots.
Test: test/basic/PrivateData.t.sol
- Unit: can read "private" data via vm.load
- Unit: demonstrates storage layout
Fuzz: no. Invariant: no.
Key comment: Private means private-to-contracts, not private-to-the-world. Anyone can read storage. Never store secrets or passwords on chain.
Concepts: Uniswap V2 architecture, flash swaps, arbitrage, DEX integration.
Contract: src/defi/UniswapV2Swap.sol
Contract that performs flash swaps on Uniswap V2, repaying with a different token.
Test: test/defi/UniswapV2Swap.t.sol
- Unit: flash swap executes correctly
- Unit: profit calculation is correct
- Unit: revert if can't repay
Fuzz: testFuzz_swap_amounts(uint256 amount) — fuzz input amounts, verify outputs.
Invariant: invariant_k_constant — x * y = k holds after swaps (accounting for fees).
Concepts: concentrated liquidity, tick-based pricing, single-hop and multi-hop swaps, exact input vs exact output.
Contract: src/defi/UniswapV3Swap.sol
Contract integrating with Uniswap V3 for single and multi-hop swaps.
Test: test/defi/UniswapV3Swap.t.sol
- Unit: exact input single swap works
- Unit: exact output single swap works
- Unit: multi-hop swap works
- Unit: slippage protection reverts on excessive slippage
Fuzz: testFuzz_swap_parameters(uint24 feeTier, uint256 amount) — fuzz fee tier and amount.
Invariant: no — Uniswap V3 invariants are complex.
Concepts: V4 pools, hooks, flash accounting, Permit2, universal router.
Contract: src/defi/UniswapV4Swap.sol
Contract performing single and multi-hop swaps using Uniswap V4 PoolManager and Universal Router.
Test: test/defi/UniswapV4Swap.t.sol
- Unit: exact input single swap works
- Unit: exact output single swap works
- Unit: multi-hop swap works
- Unit: slippage protection reverts on excessive slippage
Fuzz: testFuzz_swap_amounts(uint256 amountIn, uint256 amountOutMinimum) — fuzz swap amounts.
Invariant: no — Uniswap V4 invariants are complex.
Concepts: V4 flash loans, callback architecture, flash accounting, hook callbacks.
Contract: src/defi/UniswapV4FlashLoan.sol
Contract demonstrating flash loan functionality in Uniswap V4 using the pool callback.
Test: test/defi/UniswapV4FlashLoan.t.sol
- Unit: flash loan executes successfully
- Unit: flash loan repayment is correct
- Unit: reverts if callback fails
Fuzz: testFuzz_flash_loan_amounts(uint256 amount) — fuzz loan amounts within bounds.
Invariant: no.
Concepts: V4 hooks, limit orders, tick manipulation, hook-driven liquidity.
Contract: src/defi/UniswapV4LimitOrder.sol
Contract implementing a limit order using Uniswap V4 hook architecture.
Test: test/defi/UniswapV4LimitOrder.t.sol
- Unit: limit order fills at correct price
- Unit: order does not fill above limit price
- Unit: partial fills work correctly
Fuzz: testFuzz_limit_order_price(uint256 amountIn, uint256 priceLimit) — fuzz order parameters.
Invariant: no.
Concepts: Chainlink oracle integration, price staleness checks, decimal handling.
Contract: src/defi/ChainlinkPriceFeed.sol
Contract that fetches ETH/USD price from Chainlink, includes staleness check.
Test: test/defi/ChainlinkPriceFeed.t.sol
- Unit: can fetch latest price
- Unit: reverts on stale price
- Unit: handles decimal conversion correctly
Fuzz: no. Invariant: no.
Concepts: reward accrual, staking/unstaking, reward rate math, stake-weighted distribution.
Contract: src/defi/StakingRewards.sol
Synthetix-style staking rewards contract with reward-per-token accumulation.
Test: test/defi/StakingRewards.t.sol
- Unit: staking updates reward balance
- Unit: rewards accrue over time
- Unit: withdrawing claims rewards
- Unit: reward rate is distributed correctly
Fuzz: testFuzz_stake_unstake(uint256 amount, uint256 duration) — fuzz stake amount and time.
Invariant: invariant_reward_per_token_increases — reward per token never decreases.
Concepts: price decay over time, commit-reveal pattern, fair price discovery.
Contract: src/defi/DutchAuction.sol
Dutch auction for NFTs or tokens, price starts high and decreases linearly.
Test: test/defi/DutchAuction.t.sol
- Unit: price decreases over time
- Unit: purchase at current price succeeds
- Unit: purchase after auction ends reverts
- Unit: refund if overpaid
Fuzz: testFuzz_purchase_timing(uint256 warpTime) — fuzz block.timestamp, verify price calculation.
Invariant: invariant_price_never_increases — current price is always <= start price.
Concepts: bidding increments, withdrawal pattern, highest bidder wins, time extensions.
Contract: src/defi/EnglishAuction.sol
Standard English auction with minimum bid increments and withdrawal.
Test: test/defi/EnglishAuction.t.sol
- Unit: bid must exceed current highest
- Unit: previous highest can withdraw
- Unit: auction ends after deadline
- Unit: winner can claim NFT
Fuzz: testFuzz_bidding_war(uint256[] bids) — fuzz sequence of bids.
Invariant: invariant_highest_bidder_has_highest_bid — highestBidder always has highestBid.
Concepts: goal-based funding, deadline, refund if goal not met, withdrawal if goal met.
Contract: src/defi/CrowdFund.sol
Simple crowdfunding campaign with goal and deadline.
Test: test/defi/CrowdFund.t.sol
- Unit: can pledge to campaign
- Unit: can refund if goal not met
- Unit: creator can claim if goal met
- Unit: cannot pledge after deadline
Fuzz: testFuzz_pledge_refund(uint256[] pledges) — fuzz pledge amounts.
Invariant: invariant_pledged_never_exceeds_goal_if_claimed — if claimed, totalPledged >= goal.
Concepts: n-of-m signatures, transaction nonce, execution threshold, replay protection.
Contract: src/applications/MultiSigWallet.sol
Gnosis-style multi-sig with configurable threshold.
Test: test/applications/MultiSigWallet.t.sol
- Unit: submit transaction
- Unit: approve by multiple owners
- Unit: execute when threshold reached
- Unit: revoke approval
Fuzz: testFuzz_approve_execute(uint256 signers) — fuzz which owners sign.
Invariant: invariant_executed_only_after_threshold — executed tx always has >= threshold approvals.
Concepts: Merkle proofs, leaf verification, gas-efficient airdrops.
Contract: src/applications/MerkleTree.sol
Merkle distributor for token airdrops with proof verification.
Test: test/applications/MerkleTree.t.sol
- Unit: valid proof claims successfully
- Unit: invalid proof reverts
- Unit: double claim prevented
Fuzz: testFuzz_proof_verification(bytes32[] proof) — fuzz proof elements.
Invariant: invariant_claimed_amount_matches_proof — total claimed equals sum of valid proofs.
Concepts: combining mapping with array for iteration, gas tradeoffs.
Contract: src/applications/IterableMapping.sol
Mapping that tracks keys in an array for iteration.
Test: test/applications/IterableMapping.t.sol
- Unit: set adds key to array
- Unit: remove deletes key from array
- Unit: can iterate all keys
Fuzz: testFuzz_set_remove(uint8 operations) — fuzz set/remove sequence.
Invariant: invariant_keys_array_has_no_duplicates — no address appears twice in keys array.
Concepts: deterministic contract addresses, salt parameter, counterfactual deployment.
Contract: src/applications/Create2.sol
Factory using CREATE2 for deterministic addresses, useful for layer 2 addresses.
Test: test/applications/Create2.t.sol
- Unit: same salt produces same address
- Unit: different salt produces different address
- Unit: can compute address before deployment
Fuzz: no. Invariant: no.
Concepts: clone pattern, minimal bytecode, cheap deployment of identical contracts.
Contract: src/applications/MinimalProxy.sol
Factory using OpenZeppelin's Clones library for minimal proxy deployment.
Test: test/applications/MinimalProxy.t.sol
- Unit: clone creates minimal proxy
- Unit: proxy delegates to implementation
- Unit: many clones are cheap to deploy
Fuzz: testFuzz_multiple_clones(uint8 n) — deploy n clones.
Invariant: invariant_all_clones_use_same_implementation — all clones delegate to same impl.
Concepts: generic factory, bytecode deployment, initcode handling.
Contract: src/applications/Deployer.sol
Factory that can deploy any contract given its creation bytecode.
Test: test/applications/Deployer.t.sol
- Unit: can deploy simple contract
- Unit: can deploy contract with constructor args
- Unit: returns correct deployed address
Fuzz: no. Invariant: no.
Concepts: simple vault, access control, withdrawal patterns.
Contract: src/applications/EtherWallet.sol
Simple contract that holds ether, only owner can withdraw.
Test: test/applications/EtherWallet.t.sol
- Unit: can receive ether
- Unit: only owner can withdraw
- Unit: withdrawal transfers correct amount
Fuzz: testFuzz_deposit_withdraw(uint96 amount) — fuzz amounts.
Invariant: invariant_balance_only_changes_via_withdraw — balance changes only on withdrawals.
Concepts: checks-effects-interactions pattern, reentrancy guard, push vs pull patterns.
Contract: src/applications/SendEtherSecure.sol
Secure ether sending with CEI pattern and reentrancy guard.
Test: test/applications/SendEtherSecure.t.sol
- Unit: sends ether correctly
- Unit: reentrant call is blocked
- Unit: CEI pattern prevents reentrancy
Fuzz: no.
Invariant: invariant_no_reentrant_calls — reentrancy flag is never set during external calls.
Concepts: Yul/inline assembly, unchecked math, bit manipulation, gas optimization.
Contract: src/evm/AssemblyMath.sol
Math operations implemented in Yul for gas efficiency.
Test: test/evm/AssemblyMath.t.sol
- Unit: assembly add matches Solidity
- Unit: assembly mul matches Solidity
- Unit: unchecked math handles overflow correctly
Fuzz: testFuzz_assembly_operations(uint256 a, uint256 b) — fuzz operations.
Invariant: no.
Concepts: Yul variables, memory layout, storage layout, let vs :=.
Contract: src/evm/AssemblyVariable.sol
Demonstrates Yul variable declaration and assignment patterns.
Test: test/evm/AssemblyVariable.t.sol
- Unit: let declares and initializes
- Unit: := assigns to existing variable
Fuzz: no. Invariant: no.
Concepts: Yul if/switch statements, comparison operators, jump logic.
Contract: src/evm/AssemblyConditionals.sol
Conditional logic implemented in Yul.
Test: test/evm/AssemblyConditionals.t.sol
- Unit: if statement works
- Unit: switch statement works
Fuzz: testFuzz_assembly_if(uint256 x) — fuzz condition.
Invariant: no.
Concepts: Yul for loops, continue/break equivalent, gas efficiency.
Contract: src/evm/AssemblyLoop.sol
Loops implemented in Yul.
Test: test/evm/AssemblyLoop.t.sol
- Unit: for loop sums correctly
- Unit: early exit works
Fuzz: testFuzz_assembly_sum(uint8 n) — fuzz loop iterations.
Invariant: no.
Concepts: practice writing Yul, implementing math in assembly.
Contract: src/evm/AssemblyMathExercise.sol
Exercises for implementing math operations in Yul.
Test: test/evm/AssemblyMathExercise.t.sol
- Unit: implement and test subtraction
- Unit: implement and test division
Fuzz: no. Invariant: no.
Concepts: reentrancy vulnerability, external call before state update, attacker contract.
Contract: src/hacks/ReentrancyVulnerable.sol, src/hacks/ReentrancyAttacker.sol
Vulnerable vault and attacker demonstrating the classic reentrancy exploit.
Test: test/hacks/ReentrancyAttack.t.sol
- Unit: attacker drains vault via reentrancy
Fuzz: no. Invariant: no.
Concepts: checks-effects-interactions, reentrancy guard, pull over push pattern.
Contract: src/hacks/ReentrancySecure.sol
Same vault functionality, secured against reentrancy.
Test: test/hacks/ReentrancySolution.t.sol
- Unit: reentrant attack fails
- Unit: normal withdrawal still works
Fuzz: no.
Invariant: invariant_balance_equals_sum_of_balances — vault balance equals sum of user balances.
Concepts: spot price manipulation, TWAP importance, manipulation cost.
Contract: src/hacks/OracleManipulation.sol
Lending protocol using manipulable spot price vs secure TWAP.
Test: test/hacks/OracleManipulation.t.sol
- Unit: spot price can be manipulated
- Unit: TWAP resists manipulation
- Unit: lending with spot price is vulnerable
Fuzz: no. Invariant: no.
Concepts: selfdestruct force-sends ether, can bypass payable checks, contract balance manipulation.
Contract: src/hacks/SelfDestructAttack.sol
Contract vulnerable to forced ether via selfdestruct, and the attacker.
Test: test/hacks/SelfDestructAttack.t.sol
- Unit: selfdestruct forces ether into contract
- Unit: balance can exceed recorded deposits
Fuzz: no. Invariant: no.
Concepts: tx.origin vs msg.sender, phishing attacks via tx.origin usage.
Contract: src/hacks/TxOriginVulnerable.sol, src/hacks/TxOriginAttacker.sol
Wallet using tx.origin for authentication and the phishing attacker.
Test: test/hacks/TxOriginAttack.t.sol
- Unit: attacker can phish via tx.origin check
Fuzz: no. Invariant: no.
Concepts: always use msg.sender, explicit authorization checks.
Contract: src/hacks/TxOriginSecure.sol
Secure version using msg.sender with proper access control.
Test: test/hacks/TxOriginSolution.t.sol
- Unit: phishing attack fails with msg.sender
Fuzz: no. Invariant: no.
Concepts: delegatecall vulnerability, storage collision, malicious implementation.
Contract: src/hacks/DelegatecallVulnerable.sol, src/hacks/DelegatecallAttacker.sol
Vulnerable proxy and attacker showing storage collision exploit.
Test: test/hacks/DelegatecallAttack.t.sol
- Unit: attacker can overwrite owner via delegatecall
Fuzz: no. Invariant: no.
Concepts: proper proxy patterns, implementation slot, non-upgradeable storage.
Contract: src/hacks/DelegatecallSecure.sol
EIP-1967 compliant proxy with proper storage slots.
Test: test/hacks/DelegatecallSolution.t.sol
- Unit: storage collision not possible with EIP-1967
Fuzz: no. Invariant: no.
Concepts: contracts can receive ether without payable functions via selfdestruct.
Contract: src/hacks/ForceEther.sol
Contract that refuses ether normally but can receive via selfdestruct.
Test: test/hacks/ForceEther.t.sol
- Unit: normal transfer fails
- Unit: selfdestruct forces ether in
Fuzz: no.
Invariant: invariant_balance_gte_recorded — balance always >= recorded deposits.
Concepts: first depositor attack, share dilution, decimal precision issues.
Contract: src/hacks/VaultInflation.sol
Vulnerable ERC4626 vault showing first depositor attack.
Test: test/hacks/VaultInflation.t.sol
- Unit: first depositor can inflate share price
- Unit: subsequent depositors get fewer shares
Fuzz: no. Invariant: no.
Concepts: virtual shares, offset, minimum deposit.
Contract: src/hacks/VaultInflationSecure.sol
ERC4626 vault with virtual shares protection.
Test: test/hacks/VaultInflationSolution.t.sol
- Unit: inflation attack fails
- Unit: shares minted proportionally
Fuzz: no.
Invariant: invariant_share_price_stable — share price doesn't change dramatically.
Concepts: missing nonce, signature replay across chains or contracts.
Contract: src/hacks/SignatureReplayVulnerable.sol
Vulnerable contract allowing signature replay.
Test: test/hacks/SignatureReplayAttack.t.sol
- Unit: same signature can be reused
Fuzz: no. Invariant: no.
Concepts: unique nonces, domain separation, EIP-712.
Contract: src/hacks/SignatureReplaySecure.sol
Secure contract using nonces and EIP-712.
Test: test/hacks/SignatureReplaySolution.t.sol
- Unit: signature cannot be replayed
- Unit: nonce tracking prevents reuse
Fuzz: no.
Invariant: invariant_nonce_increments — nonce always increases after use.
Concepts: miner manipulation of block.timestamp, don't use for entropy.
Contract: src/hacks/TimestampManipulation.sol
Game using block.timestamp for randomness (vulnerable).
Test: test/hacks/TimestampManipulation.t.sol
- Unit: miner can manipulate timestamp to win
Fuzz: no. Invariant: no.
Concepts: commit-reveal, VRF, external randomness.
Contract: src/hacks/TimestampSecure.sol
Game using commit-reveal for fairness.
Test: test/hacks/TimestampSolution.t.sol
- Unit: commit-reveal prevents manipulation
Fuzz: no. Invariant: no.
Concepts: blockhash, timestamp, difficulty are all manipulable/predictable.
Contract: src/hacks/PredictableRandomness.sol
Lottery using blockhash for randomness (vulnerable).
Test: test/hacks/PredictableRandomness.t.sol
- Unit: randomness can be predicted
Fuzz: no. Invariant: no.
Concepts: Chainlink VRF, commitment schemes, external randomness.
Contract: src/hacks/SecureRandomness.sol
Lottery using commit-reveal pattern.
Test: test/hacks/SecureRandomness.t.sol
- Unit: commit-reveal provides fairness
Fuzz: no. Invariant: no.
Concepts: unbounded operations, gas limit attacks, push vs pull.
Contract: src/hacks/DoSVulnerable.sol
Auction with push payments (vulnerable to gas limit DoS).
Test: test/hacks/DoSAttack.t.sol
- Unit: bidder array can grow unbounded
- Unit: last bidder can't withdraw if array too large
Fuzz: no. Invariant: no.
Concepts: pull over push pattern, withdrawal pattern.
Contract: src/hacks/DoSSecure.sol
Auction using pull pattern for withdrawals.
Test: test/hacks/DoSSolution.t.sol
- Unit: unlimited bidders work fine
- Unit: each bidder withdraws independently
Fuzz: no. Invariant: no.
Concepts: tx.origin phishing, social engineering.
Contract: src/hacks/PhishingVulnerable.sol
Wallet vulnerable to tx.origin phishing.
Test: test/hacks/PhishingAttack.t.sol
- Unit: attacker can steal via phishing
Fuzz: no. Invariant: no.
Concepts: msg.sender authentication, explicit approvals.
Contract: src/hacks/PhishingSecure.sol
Secure wallet using msg.sender.
Test: test/hacks/PhishingSolution.t.sol
- Unit: phishing attack fails
Fuzz: no. Invariant: no.
Concepts: external call to malicious contract, unexpected behavior.
Contract: src/hacks/HiddenMaliceVulnerable.sol, src/hacks/Malicious.sol
Contract with external call to manipulable address.
Test: test/hacks/HiddenMaliceAttack.t.sol
- Unit: external contract can behave maliciously
Fuzz: no. Invariant: no.
Concepts: whitelist, fixed addresses, careful external calls.
Contract: src/hacks/HiddenMaliceSecure.sol
Contract with whitelist for external calls.
Test: test/hacks/HiddenMaliceSolution.t.sol
- Unit: only whitelisted addresses callable
Fuzz: no. Invariant: no.
Concepts: efficient power calculation, Yul implementation.
Contract: src/evm/AssemblyBinaryExponentiation.sol
Binary exponentiation in Yul for gas-efficient power.
Test: test/evm/AssemblyBinaryExponentiation.t.sol
- Unit: calculates power correctly
- Unit: more gas efficient than naive multiplication
Fuzz: testFuzz_exponentiation(uint256 base, uint8 exp) — fuzz inputs.
Invariant: no.
Concepts: dynamic arrays in memory, length storage, pointer arithmetic.
Contract: src/evm/AssemblyArray.sol
Dynamic array operations implemented in Yul.
Test: test/evm/AssemblyArray.t.sol
- Unit: push adds element
- Unit: get retrieves element
- Unit: length tracks correctly
Fuzz: testFuzz_array_operations(uint8 n) — fuzz operations.
Invariant: no.
Concepts: and, or, xor, not, shifts, masks.
Contract: src/evm/BitwiseOperators.sol
Bitwise operations for packing data, permissions, flags.
Test: test/evm/BitwiseOperators.t.sol
- Unit: each operator works correctly
- Unit: can pack and unpack data
Fuzz: testFuzz_bitwise(uint256 a, uint256 b) — fuzz operations.
Invariant: no.
External resources and documentation used in this training:
Track your progress through the topics:
- Basic / Hello World
- Basic / First App
- Basic / Primitive Data Types
- Basic / Variables
- Basic / Constants
- Basic / Immutable
- Basic / Reading and Writing to a State Variable
- Basic / Ether and Wei
- Basic / Gas and Gas Price
- Basic / If / Else
- Basic / For and While Loop
- Basic / Mapping
- Basic / Array
- Basic / Enum
- Basic / User Defined Value Types
- Basic / Structs
- Basic / Data Locations
- Basic / Transient Storage
- Basic / Function
- Basic / View and Pure Functions
- Basic / Error
- Basic / Function Modifier
- Basic / Events
- Basic / Constructor
- Basic / Inheritance
- Basic / Shadowing
- Basic / Calling Parent Contracts
- Basic / Visibility
- Basic / Interface
- Basic / Payable
- Basic / Sending Ether
- Basic / Fallback
- Basic / Call
- Basic / Delegatecall
- Basic / Function Selector
- Basic / Calling Contract with ABI
- Basic / Contract that Creates Other Contracts
- Basic / Try / Catch
- Basic / Import
- Basic / Library
- Basic / ABI Encode and Decode
- Basic / Keccak256
- Basic / Verify Signature
- Basic / Accessing Private Data
- DeFi / Uniswap V2 Swap
- DeFi / Uniswap V3 Swap
- DeFi / Uniswap V4 Swap
- DeFi / Uniswap V4 Flash Loan
- DeFi / Uniswap V4 Limit Order
- DeFi / Chainlink Price Feed
- DeFi / Staking Rewards
- DeFi / Dutch Auction
- DeFi / English Auction
- DeFi / Crowd Fund
- Applications / Multi-Sig Wallet
- Applications / Merkle Tree
- Applications / Iterable Mapping
- Applications / Create2
- Applications / Minimal Proxy
- Applications / Deploy Any Contract
- Applications / Ether Wallet
- Applications / Send Ether Secure
- EVM / Assembly Math
- EVM / Assembly Variable
- EVM / Assembly Conditionals
- EVM / Assembly Loop
- EVM / Assembly Math Exercise
- EVM / Assembly Binary Exponentiation
- EVM / Assembly Array
- EVM / Bitwise Operators
- Hacks / Re-Entrancy Attack
- Hacks / Re-Entrancy Solution
- Hacks / Oracle Manipulation
- Hacks / Self Destruct Attack
- Hacks / Access Control Attack
- Hacks / Access Control Solution
- Hacks / Delegatecall Attack
- Hacks / Delegatecall Solution
- Hacks / Force Ether
- Hacks / Vault Inflation
- Hacks / Vault Inflation Solution
- Hacks / Signature Replay
- Hacks / Signature Replay Solution
- Hacks / Timestamp Manipulation
- Hacks / Timestamp Solution
- Hacks / Predictable Randomness
- Hacks / Secure Randomness
- Hacks / DoS Attack
- Hacks / DoS Solution
- Hacks / Phishing
- Hacks / Phishing Solution
- Hacks / Hidden Malice
- Hacks / Hidden Malice Solution