By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.
Smart Contract Auditing & Formal Verification – Study Guide (Designed for software engineers & finance professionals moving into Web3)
Smart contract auditing is the systematic review of Solidity (or other EVM‑compatible) code to find bugs, economic flaws, and security vulnerabilities before the contract is deployed. Formal verification goes a step further: it uses mathematical proofs and tools (e.g., SMT solvers, model checkers) to prove that the contract’s logic always satisfies a set of safety properties. Together they give users confidence that a DeFi swap, NFT minting flow, or DAO voting contract will behave exactly as intended—a prerequisite for handling real money on‑chain.
Real‑world example: The Uniswap V2 router contract was audited and formally verified for the “no‑loss” invariant reserve0 * reserve1 = k. The proof guarantees that a swap cannot magically create or destroy tokens, protecting liquidity providers.
reserve0 * reserve1 = k
Reentrancy Attack – A malicious contract re‑enters a vulnerable function before the first call finishes, draining funds. solidity // vulnerable function withdraw(uint256 amount) external { require(balances[msg.sender] >= amount); (bool ok,) = msg.sender.call{value: amount}(""); require(ok); balances[msg.sender] -= amount; // <-- wrong order }
solidity // vulnerable function withdraw(uint256 amount) external { require(balances[msg.sender] >= amount); (bool ok,) = msg.sender.call{value: amount}(""); require(ok); balances[msg.sender] -= amount; // <-- wrong order }
Check‑Effects‑Interactions (CEI) Pattern – The safe ordering: check conditions, effect state changes, then interact with external contracts. solidity balances[msg.sender] -= amount; (bool ok,) = msg.sender.call{value: amount}(""); require(ok);
solidity balances[msg.sender] -= amount; (bool ok,) = msg.sender.call{value: amount}(""); require(ok);
Static Analyzer – Automated tool (e.g., Slither, MythX) that scans Solidity source for known vulnerability patterns without executing the code.
Formal Specification – A precise, math‑based description of what a contract must do (e.g., “totalSupply never decreases”). Written in languages like Solidity’s assert, require, or external DSLs such as K‑framework.
assert
require
SMT Solver – Satisfiability Modulo Theories engine (e.g., Z3) that checks logical formulas generated from the contract; used by tools like Certora and VeriSol.
Model‑Checking – Exhaustively explores all possible states of a contract (within a bounded depth) to find violations. Example tool: Echidna (property‑based fuzzing).
Delegatecall Vulnerability – delegatecall runs code in the context of the calling contract, allowing storage‑slot clashes. Audits must verify that only trusted libraries are used.
delegatecall
Gas‑Profiler – A tool (Hardhat’s hardhat-gas-reporter or Tenderly) that measures gas consumption per function, helping auditors spot expensive patterns.
hardhat-gas-reporter
Economic Attack – Not a code bug but a design flaw (e.g., price‑oracle manipulation). Auditors model attacker incentives and simulate worst‑case scenarios.
ERC‑4626 (Vault Standard) – A recent tokenized vault interface; auditors check that deposit/withdraw maintain a 1:1 share‑to‑asset ratio under all conditions.
deposit
withdraw
Optimistic Rollup Fraud Proof – In an Optimistic rollup, auditors must ensure that any state transition can be challenged within the dispute window; formal verification often includes the fraud‑proof logic.
Zero‑Knowledge (ZK) Rollup Proof – ZK rollups provide on‑chain validity proofs; auditors verify that the SNARK verifier contract correctly validates the proof and that the off‑chain prover respects the same spec.
Write & Unit‑Test – Develop the contract in Remix or VS Code, add a comprehensive test suite with Hardhat/Foundry. bash npx hardhat test
bash npx hardhat test
Static Analysis – Run Slither and MythX on the source. Fix every “high‑severity” finding before moving on. bash slither contracts/MyToken.sol
bash slither contracts/MyToken.sol
Formal Specification – Encode critical invariants with require/assert and, if needed, write a Certora/VeriSol spec file (.spec).
require/assert
.spec
Model‑Check / Fuzz – Execute Echidna (property‑based fuzz) and/or Foundry’s forge fuzz to explore edge‑case inputs. bash echidna-test contracts/MyToken.sol --contract MyToken
forge fuzz
bash echidna-test contracts/MyToken.sol --contract MyToken
Formal Verification – Run the chosen verifier (e.g., Certora Prover) against the spec. Address any counter‑example it produces. bash certora run MyToken.spec --solc solc-0.8.20
bash certora run MyToken.spec --solc solc-0.8.20
Deploy & Monitor – Deploy to a testnet (Goerli) via Hardhat + Infura, then attach a monitoring bot (Tenderly) that watches for abnormal state changes. bash npx hardhat run scripts/deploy.js --network goerli
bash npx hardhat run scripts/deploy.js --network goerli
Mistake: Using tx.origin for access control. Correction: Use msg.sender and role‑based libraries (OpenZeppelin AccessControl). tx.origin can be spoofed through a phishing contract, allowing attackers to bypass restrictions.
tx.origin
msg.sender
Mistake: Forgetting to reset a reentrancy guard after a require fails. Correction: Implement the nonReentrant modifier from OpenZeppelin; it automatically restores the guard even on revert, preventing a lock‑out.
reset
nonReentrant
Mistake: Assuming delegatecall is safe because the library contract is “audited”. Correction: Verify storage layout compatibility and restrict the delegatecall address to an immutable constant; otherwise an attacker could upgrade the library to malicious code.
Mistake: Relying solely on unit tests for security. Correction: Complement tests with static analysis, fuzzing, and formal verification; tests cover expected paths, while the other tools explore the unexpected.
Mistake: Hard‑coding block timestamps (block.timestamp) for critical deadlines. Correction: Use block.number or a trusted oracle for time‑sensitive logic; miners can manipulate timestamps within a small window, potentially triggering early withdrawals.
block.timestamp
block.number
“Explain the difference between call and delegatecall and when each is appropriate.” Look for understanding of context (storage, msg.sender) and security implications.
call
“How would you prove that an ERC‑20 token’s totalSupply never exceeds a cap?” Expect a mention of require(totalSupply <= CAP) plus a formal invariant checked by a tool like Certora.
totalSupply
require(totalSupply <= CAP)
“What are the trade‑offs between Optimistic Rollups and ZK Rollups from an auditor’s perspective?” Key points: fraud‑proof window vs on‑chain proof verification, data availability, and the extra verification logic that must be audited.
“Describe a scenario where a price‑oracle manipulation could break a DeFi contract, and how you’d mitigate it.” Look for multi‑source aggregation, time‑weighted averages, and fallback mechanisms.
Scenario: A contract uses tx.origin == owner to restrict withdrawals. Answer: Dangerous – a malicious contract can trick the owner into calling it, making tx.origin the owner’s address and allowing the attacker to withdraw.
tx.origin == owner
Scenario: Your ERC‑4626 vault’s withdraw function updates totalShares after transferring assets. Answer: Wrong order – if the external token transfer re‑enters, the attacker can withdraw more than their share; fix by updating state before the external call (CEI).
totalShares
Scenario: You run Slither and see a “Unchecked low‑level call” warning on a function that sends ETH. Answer: The call’s return value must be checked (require(success)) to ensure the transfer succeeded; otherwise the contract may think it sent funds when it didn’t.
require(success)
SafeMath
SLOAD
tokensReceived
Happy auditing! ?
Join 4M+ learners. Unlock unlimited quizzes, wrong-answer tracking, flashcards + reminders, study guides, and 1-on-1 challenges.