Progress · 0/5 phases
🔐 Phase 4 — Security & Auditing (Days 56–75)
12 min read · Days 56–75 · Notion
Core insight: In traditional software, a bug causes downtime or data corruption. In smart contracts, a bug causes irreversible loss of funds from users who trusted your code. There is no patch server, no customer support line, no rollback. Security is not a feature you add at the end — it is the entire design discipline.
📚 Topics in order
Day 56–57 — The attacker mindset
How to think like an attacker:
Every function in your contract is a potential attack surface. For every function ask:
- What is the worst thing someone can do if they call this?
- What if they call it with extreme values? (0, uint256.max, address(0))
- What if they call it multiple times in the same transaction?
- What if they are a smart contract with a malicious fallback function?
- What if they frontrun this transaction?
- What if they control the oracle price?
- What if they have unlimited capital (flash loans)?
The four categories of smart contract vulnerabilities:
- Logic errors: incorrect business logic. Hard to detect with automated tools.
- Improper access control: functions callable by anyone that should be restricted.
- Unsafe external calls: reentrancy, unchecked return values, arbitrary calls.
- Economic design flaws: flash loan attacks, oracle manipulation, incentive misalignment.
Day 58–60 — Top vulnerability classes
1. Reentrancy
// ATTACK VECTOR:
// Attacker contract has fallback() that calls back into victim.withdraw()
// State not updated before external call -> attacker drains repeatedly
contract Attack {
Victim victim;
function attack() external payable {
victim.deposit{value: 1 ether}();
victim.withdraw(1 ether);
}
fallback() external payable {
if (address(victim).balance >= 1 ether) {
victim.withdraw(1 ether); // re-enters before state update
}
}
}
// CROSS-FUNCTION REENTRANCY: re-enter a DIFFERENT function
// Read-only reentrancy: re-enter a view function while state is inconsistent
// ERC-777 reentrancy: transfer hooks trigger before state updates
// DEFENCES:
// 1. CEI pattern (primary)
// 2. nonReentrant modifier (secondary)
// 3. Never assume external code is safe2. Access Control
// Missing auth: function should be onlyOwner but isn't
// Wrong auth: checks msg.sender but should check tx.origin (or vice versa)
// Initialiser not protected: initialize() callable by anyone (proxy pattern bug)
// tx.origin attack:
// User calls MaliciousContract, which calls Victim using tx.origin check
// tx.origin = User (who is authorised). Victim thinks User called it directly.
// NEVER use tx.origin for authorisation. Always use msg.sender.
// Real exploit: Parity Multisig hack ($30M) - unprotected initWallet function
// Anyone could call initWallet and become the owner of any Parity wallet3. Integer arithmetic
// Solidity 0.8+: overflow/underflow reverts automatically
// But: unchecked blocks, type casting, and division can still cause issues
// Type casting precision loss:
uint256 a = 300;
uint8 b = uint8(a); // b = 44 (300 % 256). No revert. Silent truncation.
// Division rounding:
// 1 / 2 = 0 in Solidity (always rounds down)
// This can cause dust accumulation or protocol insolvency over many operations
// Multiplication overflow in unchecked:
unchecked {
uint256 x = type(uint256).max;
uint256 y = x * 2; // silently overflows to a smaller number
}4. Flash loan attacks
Attack pattern:
1. Flash loan $100M of token X
2. Deposit X into a protocol (moves price or passes governance threshold)
3. Exploit the protocol (liquidate positions at wrong price, pass malicious proposal)
4. Withdraw X
5. Repay flash loan + fee
6. Profit: everything extracted in step 3
Defences:
- Use time-weighted prices (TWAP) not spot prices for critical operations
- Use governance snapshots (token balance at proposal block, not current block)
- Require a delay between deposit and any privileged action5. Frontrunning and MEV
Scenario: you submit a profitable arbitrage transaction
MEV searcher sees it in mempool, copies it with higher gas
MEV transaction executes first, takes your profit
Sandwich attack:
1. Bot sees your large swap: 1000 ETH for USDC
2. Bot buys ETH (pushing price up) - frontrun
3. Your swap executes at worse price
4. Bot sells ETH - backrun. Profit from your slippage.
Defences:
- Set tight slippage tolerances (amountOutMinimum)
- Use Flashbots Protect or private RPC for sensitive transactions
- Commit-reveal for randomness and auctions6. Unchecked return values
// ERC-20 tokens: some return false instead of reverting on failure
// If you don't check the return value, the transfer silently fails
// WRONG:
token.transfer(recipient, amount); // if transfer returns false, we don't know
// CORRECT:
require(token.transfer(recipient, amount), "Transfer failed");
// OR:
token.safeTransfer(recipient, amount); // OpenZeppelin handles this
// Low-level calls always return (bool success, bytes memory data)
(bool success,) = recipient.call{value: amount}("");
require(success, "ETH transfer failed");7. Timestamp and block number manipulation
// block.timestamp: validators can manipulate by up to ~12 seconds in PoS
// Never use for precise timing of financial operations
// Use block numbers for rough timing. Use off-chain systems for precise timing.
// ACCEPTABLE: time-based vesting with multi-minute granularity
require(block.timestamp >= vestingStart + 365 days);
// DANGEROUS: using timestamp as randomness source
uint256 random = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender)));
// Validator can choose which timestamp to use within the valid range8. Signature replay attacks
// If you use ecrecover for authorisation, an attacker can replay the signature
// on a different chain, different contract, or for a different operation
// ALWAYS include in your signed hash:
// 1. chainId (prevents cross-chain replay)
// 2. contract address (prevents cross-contract replay)
// 3. nonce (prevents same-contract replay)
// 4. expiry (prevents stale signature use)
// EIP-712 structured data signing handles all of this:
bytes32 domainSeparator = keccak256(abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
keccak256(bytes("1")),
block.chainid,
address(this)
));Day 61–63 — Audit methodology
Step 1: Understand the protocol (2-3 hours)
- Read ALL documentation: whitepaper, README, natspec comments
- Draw an architecture diagram: all contracts, their relationships, external dependencies
- List all external calls (to tokens, oracles, other protocols)
- Map all trust assumptions: who can do what?
- Identify the critical invariants: what MUST always be true?
Step 2: Manual review (systematic)
- Read every state variable: what does it track? Who can modify it?
- Read every modifier: does it actually protect what it claims?
- Read every external/public function: what are the preconditions? Postconditions?
- Follow the money: trace ETH and token flows through every path
- Check CEI compliance: every state-modifying function
- Check every arithmetic operation: overflow? Precision loss? Division before multiply?
- Check every external call: return value checked? Reentrancy possible?
- Check every access control: is the correct modifier applied?
Step 3: Threat modelling
- Enumerate actors: normal user, malicious user, admin, MEV bot, flash loan attacker
- For each actor: what is their maximum rational harm?
- For each valuable state: how can it be manipulated?
Step 4: Automated tools
# Slither: static analysis
slither . --detect all
slither . --detect reentrancy-eth,reentrancy-no-eth
slither . --detect arbitrary-send-eth
# Mythril: symbolic execution
myth analyze src/Contract.sol --execution-timeout 300
# Echidna: fuzzing
# Write property functions (invariants) that should never be false
# Echidna tries to find inputs that break them
function echidna_balance_invariant() public returns (bool) {
return totalSupply == sumOfAllBalances();
}
# Foundry fuzzing:
function testFuzz_Invariant(uint256 amount) public {
amount = bound(amount, 1, type(uint128).max);
// property that should always hold
}Step 5: Write a report
- Severity: Critical (funds at risk immediately), High (funds at risk under conditions), Medium (protocol malfunction), Low (best practices), Informational
- For each finding: description, impact, proof of concept (code), recommendation
- Proof of concept: a working Foundry test that demonstrates the exploit
Day 64–66 — Advanced attack patterns
Price manipulation via donation:
// If share price = totalAssets() / totalShares
// and totalAssets() reads token.balanceOf(address(this))
// attacker can donate tokens directly to inflate share price
// New depositors get fewer shares than expected -> effectively robbed
// Fix: track totalAssets internally, don't rely on balanceOfInflation attack (first depositor):
1. Attacker is first LP: deposits 1 wei, receives 1 share
2. Attacker donates 1000e18 tokens directly (not through deposit)
3. Now: 1 share represents 1000e18 + 1 wei of assets
4. Victim deposits 1999e18 tokens: receives floor(1999e18 * 1 / (1000e18 + 1)) = 1 share
5. Attacker withdraws 1 share: gets half of all assets
6. Attacker stole ~500e18 from the victim
Fix: Virtual shares/assets (add 1000 to both numerator and denominator)Storage collision in proxies:
// Transparent proxy stores admin at slot:
// keccak256("eip1967.proxy.admin") - 1 = 0xb53127...
// If implementation has a mapping at slot 0 and a user's key hashes
// to the same slot as the admin storage -> they can overwrite the admin
// EIP-1967: standardised storage slots for proxy variables
// Always use EIP-1967 slots for proxy variables
bytes32 constant IMPLEMENTATION_SLOT =
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;Day 67–70 — CTF and real exploit analysis
Day 67-68: Ethernaut (OpenZeppelin CTF)
Complete levels 1-20. Each level is a vulnerable contract you must exploit. Key levels:
- Level 5: Token (integer underflow)
- Level 6: Delegation (delegatecall)
- Level 8: Vault (private storage is public)
- Level 10: Re-entrancy (classic reentrancy)
- Level 16: Preservation (delegatecall storage collision)
- Level 20: Denial (gas griefing)
Day 69: Damn Vulnerable DeFi (advanced levels)
Complete challenges 10-18:
- Backdoor (Gnosis Safe initialiser exploit)
- Climber (access control + timelock attack)
- Free Rider (flash loan + NFT marketplace)
- Wallet Mining (CREATE2 exploit)
Day 70: Real exploit recreation
Choose one of these and recreate the exploit in Foundry:
- Euler Finance hack ($197M, March 2023): donation attack + flash loan
- Nomad Bridge hack ($190M, August 2022): initialisation bug
- Poly Network hack ($611M, August 2021): access control on privileged function
Required reading for each exploit:
- Rekt.news writeup
- Original attacker transaction on Etherscan
- Post-mortem from the team
- Trace the transaction with
cast run <txhash> --rpc-url mainnet
Day 71–75 — Audit tools and formal verification
Slither static analysis:
# Run all detectors:
slither .
# Most important detectors:
# reentrancy-eth: reentrancy with ETH transfer
# arbitrary-send-eth: unprotected ETH send
# controlled-delegatecall: delegatecall with untrusted data
# msg-value-loop: msg.value in a loop (always a bug)
# tx-origin: tx.origin for authentication
# suicidal: anyone can call selfdestruct
# Print human-readable contract summary:
slither . --print human-summary
slither . --print call-graph
slither . --print data-dependencyEchidna fuzzing (property-based testing):
// echidna_* functions are properties that must always return true
// Echidna generates random sequences of function calls trying to break them
contract VaultTest is Vault {
address echidna_caller = address(0x1234);
function echidna_solvency() public view returns (bool) {
// Total assets must always equal sum of all deposits
return totalAssets() >= totalDeposited;
}
function echidna_no_free_shares() public view returns (bool) {
// You cannot get shares without depositing
return balanceOf[echidna_caller] <= deposited[echidna_caller];
}
}Formal verification basics:
- Certora Prover: specification language (CVL) that proves properties hold for ALL inputs
- Halmos: symbolic execution on Foundry tests. Runs tests symbolically to find counterexamples.
- K framework: formal semantics of EVM. Used to prove properties about the EVM itself.
🔨 Projects
Project 1 — Audit your Phase 3 lending protocol
Deliverable: Conduct a full audit of the lending protocol you built in Phase 3:
- Run Slither. Fix all high/medium findings.
- Write an Echidna fuzzing suite with 5 invariants (e.g., “total borrows can never exceed total deposits”)
- Write a formal audit report in markdown: executive summary, findings table, detailed findings with PoC, recommendations
- Attempt to exploit your own protocol with flash loans and oracle manipulation
Project 2 — CTF Portfolio
Deliverable: Complete and document solutions to: all 20 Ethernaut levels + Damn Vulnerable DeFi 1-12. For each: working exploit in Foundry test, explanation of the vulnerability, the fix. Host solutions on GitHub with clear READMEs.
Project 3 — Recreate a real exploit
Deliverable: Pick any historical DeFi hack from rekt.news. Write a complete Foundry test that:
- Forks mainnet at the exact block before the attack
- Replicates the attacker's transactions step by step
- Verifies the funds were drained
- Documents the fix (what one line of code would have prevented it?)
- Writes the actual fix and proves it blocks the attack
⚠️ Common mistakes
Mistake 1
❌ Relying only on automated tools for security.
Slither and Mythril find syntax-level issues well. They completely miss economic attack vectors, incorrect business logic, and novel attack patterns. The Euler Finance exploit ($197M) would not have been caught by any static analysis tool.
✅ Correct approach: Automated tools catch the obvious. Manual review by experienced humans catches the subtle. The correct process: automated tools first (to clear known issues) → manual review → CTF-style adversarial thinking → fuzzing with invariants → formal verification for critical math.
Mistake 2
❌ Writing exploits without understanding the root cause.
Some engineers copy CTF solutions without understanding why the exploit works. This means they can’t spot the same class of vulnerability in a different form.
✅ Correct approach: For every vulnerability: name the class, explain why it exists, explain why the fix works, find 3 other real-world examples of the same class. Depth over breadth.
Mistake 3
❌ Not testing failure paths.
Tests that only test the happy path miss every security vulnerability. The vulnerability is always in the edge case.
✅ Correct approach: For every function: test with zero inputs, maximum inputs, empty arrays, zero address, already-completed state, and the caller being a malicious smart contract. Use fuzz testing to discover edge cases you wouldn’t think of manually.
Mistake 4
❌ Trusting comments over code.
A natspec comment says “only callable by the admin” but the function has no modifier. Comments are for humans; the EVM runs the code.
✅ Correct approach: In a security review, the code is the truth. Comments are hints. Verify every security claim by reading the actual modifier implementation and tracing the call path.