🌳 Advanced ⏱️ 40 min

Threat Modeling for Smart Contracts

Threat modeling is structured thinking about how a system can be attacked. Instead of randomly hunting for bugs, you systematically ask: "What does this system protect? Who would want to attack it? How could they do it?" For smart contracts, threat modeling is especially powerful because the attack surface is clearly defined by the code, and the assets at risk are usually denominated in exact dollar amounts. A two-hour threat model session often yields more findings than two days of unfocused code reading.

📖 Why Threat Modeling Works

Most auditors find vulnerabilities by pattern recognition — they recognize a reentrancy pattern, a flash loan opportunity, a missing access check. Threat modeling forces you to think about vulnerabilities you have not seen before, by reasoning from attacker goals rather than defender code. The two approaches are complementary: pattern recognition finds known issues fast, threat modeling finds novel issues that checklists miss.

Step 1: Define the Assets

What is this protocol protecting? Be specific. "User funds" is too vague. List every category of asset with an approximate dollar value at risk.

🔍 Asset Inventory Template
// Asset Inventory: DeFi Lending Protocol // Financial Assets // 1. Deposited collateral (ETH, WBTC, USDC) — $50M TVL // 2. Protocol treasury (fee income) — $2M // 3. Liquidation incentive reserves — $500K // Governance Assets // 4. Admin role — controls oracle, can upgrade contracts // 5. Governance token voting power — controls protocol direction // Data Assets // 6. Price oracle data — determines solvency of all positions // 7. User position data — health factors, borrowing limits // 8. Interest rate parameters — determines yield for depositors // Priority (by attack value): // $50M collateral >> $2M treasury >> governance >> data

Step 2: Map Trust Boundaries

🔍 Trust Boundary Analysis
// Trust levels in a typical DeFi protocol // FULLY TRUSTED (runs with protocol authority) // - Protocol's own contracts // - OpenZeppelin library code (audited, widely used) // CONDITIONALLY TRUSTED (trusted for specific functions only) // - Admin/Owner: trusted to not rug, but should be timelock-protected // - Chainlink: trusted for honest reporting, but can go stale or hit circuit breaker // - Uniswap pool: trusted for k invariant, NOT trusted for price // UNTRUSTED (must be treated as adversarial) // - Any msg.sender that is not a known address // - Token contracts (can have callbacks, fees, blacklists) // - Liquidators (rational profit-seekers, may be MEV bots) // - Governance proposals (any token holder can submit) // - Flash loan providers (give capital to unknown actors) // Critical question: What happens when a TRUSTED party becomes adversarial? // If admin is compromised: what can they do? What can they not do? // If Chainlink reports wrong price: what fails first? // If a governance vote passes a malicious proposal: what is the blast radius?

The STRIDE Framework for Smart Contracts

STRIDE is a threat categorization framework originally from Microsoft's software security team. Each letter represents a class of threat. Applying it systematically to a DeFi protocol surfaces issues that free-form analysis misses.

STRIDE CategorySmart Contract EquivalentExample
SpoofingImpersonating a trusted address (msg.sender manipulation, signature forgery)ecrecover returns address(0) accepted as valid signer
TamperingModifying contract state without authorization (storage slot collision, unauthorized writes)Upgradeable proxy storage collision overwrites admin slot
RepudiationActions that leave no trace (missing events, silent failures)Admin changes oracle with no event emitted
Information DisclosureMEV, front-running, sandwich attacks extracting value from pending transactionsPending liquidation front-run for profit
Denial of ServiceBlocking protocol functions (gas DoS, infinite loops, blacklisted tokens, paused markets)Attacker deposits dust to make a loop run out of gas
Elevation of PrivilegeGaining admin rights or bypassing access control (role confusion, initialize() not called)Uninitialized implementation contract taken over via initialize()

Attack Surface Mapping

🔍 Attack Surface Questionnaire (for every function)
// Apply these questions to EVERY external/public function: // 1. WHO can call this? // - anyone? specific role? specific address? // - is the access control check correct? can it be bypassed? // 2. WHEN can this be called (at what times)? // - can it be called before initialization? // - can it be called when the protocol is paused? // - can it be called multiple times in the same block? // 3. WHAT state does it change? // - what mappings/variables are modified? // - is the order of state changes correct (CEI pattern)? // - can state be left in an inconsistent intermediate state? // 4. What EXTERNAL calls does it make? // - are return values checked? // - can external calls revert unexpectedly? // - do external calls happen before or after state updates? // 5. What INPUT validation exists? // - what happens with amount = 0? // - what happens with amount = type(uint256).max? // - what happens with address(0)? // - what happens with a malicious contract address? // Example application to a withdraw() function: function withdraw(address token, uint256 amount) external { // WHO: anyone — correct? can they withdraw others' funds? // WHEN: while paused? after position is liquidated? // STATE: does balances[msg.sender] update before the transfer? // EXTERNAL: safeTransfer or raw transfer? return value checked? // INPUT: amount=0 (waste gas?), amount>balance (underflow?) }

Threat Actors in DeFi

⚠️ Know Your Adversary

Not all attackers have the same capabilities. A retail user might accidentally trigger a bug. A sophisticated MEV bot will exploit a two-block front-run opportunity automatically. A nation-state attacker (like North Korea's Lazarus Group) will spend months on social engineering and infrastructure infiltration. When threat modeling, consider each actor type and what they can realistically do.

Threat ActorCapabilitiesPrimary TargetsExamples
External AttackerFlash loans, MEV, public blockchain access, programming skillFinancial exploits via contract bugsBeanstalk hacker, Euler hacker
Rogue AdminAll admin key powersDirect fund theft, parameter manipulationMultichain admin key theft
MEV BotTransaction ordering, sandwich attacks, liquidation racingSlippage extraction, liquidation profitEvery mainnet block
Malicious GovernanceVoting power (bought or flash loaned)Treasury, protocol upgradesBeanstalk, Build Finance DAO
Compromised DependencyWhatever the dependency is trusted to doSupply chain — library injectionLedger Connect Kit hack
Nation-StateSocial engineering, zero-days, physical access to key holdersValidator keys, bridge operatorsRonin (Lazarus Group)

Writing Protocol Invariants

An invariant is a property that must ALWAYS be true regardless of the sequence of operations. Writing invariants before auditing gives you a concrete target: find any code path that violates them.

🔍 Protocol Invariant Examples
// Invariants for a lending protocol: // INVARIANT 1: Protocol is always solvent // totalCollateralValue >= totalDebtValue (at any price within liquidation bounds) // Violation: any mechanism that allows debt to exceed collateral value // INVARIANT 2: Share accounting is correct // sum(userDeposits[user]) == totalDeposited // Violation: double-counting on deposit, lost accounting on fee-on-transfer // INVARIANT 3: Undercollateralized positions can always be liquidated // if healthFactor(user) < 1.0 then liquidate(user) MUST succeed // Violation: reentrancy lock on collateral token blocks liquidation // INVARIANT 4: Interest always increases, never decreases // borrowIndex[t+1] >= borrowIndex[t] for all t // Violation: admin can reset borrowIndex to a lower value // INVARIANT 5: Only the depositor (or liquidator for undercollateralized) can withdraw // withdrawal requires: caller == depositor OR healthFactor(depositor) < 1.0 // Violation: missing access check on withdraw, reentrancy bypasses check // Invariants for an AMM: // reserve0 * reserve1 >= k_at_last_mint (k can only grow) // token0.balanceOf(pool) >= reserve0 (actual balance >= recorded reserve) // totalSupply(lpToken) > 0 iff reserve0 > 0 AND reserve1 > 0

DeFi-Specific Threat Categories

🔍 DeFi Threat Checklist by Category
// ORACLE THREATS // - Can price be manipulated in one block? (spot price oracle) // - Can price go stale? (missing heartbeat check) // - Can price report 0 or negative? (missing sanity check) // - Can oracle hit circuit breaker? (Chainlink min/max answer) // FLASH LOAN THREATS // - Does this function behave differently if caller has $1B in the same tx? // - Can flash-borrowed tokens manipulate governance? // - Can flash-borrowed collateral manipulate oracle? // REENTRANCY THREATS // - Does any external call happen before state updates? // - Does the protocol send ETH? (ETH is the original reentrancy vector) // - Are ERC777 or ERC721 callbacks possible? // - Is there cross-function reentrancy (reenters a different function)? // - Is there read-only reentrancy (reading stale state during callback)? // ADMIN/GOVERNANCE THREATS // - What can the owner do without timelock? // - Can a governance proposal drain the treasury? // - Can voting power be flash-borrowed? // - Is there a veto mechanism? // MEV THREATS // - Is there a pending transaction that can be sandwiched? // - Can liquidations be front-run? // - Are there any profitable arbitrage opportunities that damage users?

The Threat Model Template

✏️ Try It: Fill Out This Template for Any Protocol
// THREAT MODEL: [Protocol Name] // Date: [date] // Auditor: [handle] // === ASSETS === // 1. [Asset]: $[value] at risk via [mechanism] // 2. [Governance power]: controls [what] // === TRUST MODEL === // Fully trusted: [list] // Conditionally trusted: [list + what they're trusted for] // Untrusted: [list] // === INVARIANTS (must always be true) === // 1. [invariant description] — violation leads to: [consequence] // 2. ... // === TOP THREATS === // 1. [threat]: [actor] could [action] via [mechanism] → [impact] // Likelihood: [high/med/low], Impact: [high/med/low] // Mitigations present: [list] // Gaps to investigate: [list] // === HIGHEST PRIORITY FUNCTIONS TO AUDIT === // 1. [function] — because it [reason] // 2. ...
💡 Invariant-Driven Bug Hunting

Once you have written your invariants, the audit process becomes: "For each invariant, find a sequence of valid function calls that violates it." This is the foundation of property-based fuzzing (Echidna, Foundry invariant tests). Write your invariants in plain English first, then translate them into code for automated testing. Any path your fuzzer finds that violates an invariant is a bug.

Quantifying Threats: Likelihood × Impact Scoring

🔍 Threat Prioritization Matrix
// Score each threat: Likelihood (1-3) × Impact (1-3) = Priority (1-9) // Scoring guide: // Likelihood: // 3 = Any attacker can execute with public knowledge and no capital // 2 = Requires some capital ($10K-$1M) or timing // 1 = Requires key compromise, insider access, or nation-state resources // Impact: // 3 = Loss of >$1M or complete protocol failure // 2 = Loss of $10K-$1M or significant protocol impairment // 1 = Loss of <$10K or minor functionality degradation // Example threat scoring for a lending protocol: // Threat: Flash loan + oracle spot price manipulation // Likelihood: 3 (no capital needed — flash loans are free) // Impact: 3 (full protocol drain) // Priority: 9/9 — CRITICAL, investigate immediately // Threat: Admin key phishing (social engineering) // Likelihood: 1 (requires targeting specific person) // Impact: 3 (admin can drain protocol) // Priority: 3/9 — High but operationally controlled // Threat: Missing event on parameter change // Likelihood: 3 (always missing — it is a code bug) // Impact: 1 (monitoring impaired, no direct loss) // Priority: 3/9 — Low finding (Informational) // Use priority scores to allocate your audit time: // Priority 7-9: Drop everything and investigate now // Priority 4-6: Schedule for deep manual review // Priority 1-3: Checklist pass, may be Low/Info finding

MEV as a Threat Category

📖 Understanding MEV Threats in Threat Modeling

Maximal Extractable Value (MEV) is the profit that can be extracted by controlling transaction ordering. MEV exists on a spectrum: some MEV (arbitrage) is benign and makes markets more efficient. Other MEV (sandwich attacks, liquidation racing, front-running) directly harms users. When threat modeling, explicitly ask: "For each function that changes prices, balances, or states — can a miner or MEV bot profit by ordering transactions around this one?" Any "yes" is a MEV threat that deserves at least a Low finding in your report.

🔍 MEV Threat Identification Patterns
// Ask for each external function: "Can a MEV bot profit by sandwiching this?" // 1. TOKEN SWAPS — highest MEV surface function swap(uint256 amountIn) external { // MEV threat: sandwich attack // Defense: amountOutMin parameter (slippage protection) // Threat level: High if no amountOutMin, Low if protected } // 2. LIQUIDATIONS — competitive MEV function liquidate(address borrower) external { // MEV threat: front-running by bots scanning for unhealthy positions // This is "benign" MEV — liquidations should happen // But: if liquidation does not revert atomically, bot might succeed // even if position was already liquidated → wasted gas, minor DoS } // 3. ORACLE UPDATES — front-running function updatePrice(uint256 newPrice) external onlyOracle { // MEV threat: trades placed between old and new price // If new price is higher: buy before update, sell after // Synthetix-style oracle front-running // Defense: commit-reveal, TWAP, minimum price impact } // 4. REWARD HARVESTING — JIT sandwich function claimRewards() external { // MEV threat: flash stake before harvest, unstake after // Dilutes legitimate stakers' rewards // Defense: minimum staking time, snapshot-based rewards }

Threat Model for Common Protocol Types

Protocol TypePrimary AssetTop 3 ThreatsCommon Vulnerability
LendingCollateral ($M)Oracle manipulation, flash loan drain, bad debt cascadeMissing accrueInterest() before health check
AMM / DEXLP fundsSandwich attacks, read-only reentrancy, flash swap exploitMissing amountOutMin on internal swaps
BridgeLocked TVL ($M)Validator key compromise, message forgery, replay attackMissing chainId in signed message
GovernanceTreasury + adminFlash loan vote, whale accumulation, parameter griefingLive balance read instead of snapshot
ERC4626 VaultUser depositsShare inflation, fee token accounting, strategy exploitNo virtual offset for first depositor
NFT MarketplaceSale proceedsReentrancy via safeTransferFrom, royalty bypass, signature replayMissing nonReentrant on fulfillListing

Threat Model Documentation Template

A threat model is only useful if it is documented and can be referenced throughout the audit. The following template can be filled out in the first hour of any engagement and serves as the foundation for your time allocation decisions.

🔍 Threat Model Document Template
// ============================================================ // THREAT MODEL: [Protocol Name] // Auditor: [Your name] Date: [Date] Commit: [Hash] // ============================================================ // == 1. PROTOCOL SUMMARY == // What does this protocol do? (1-3 sentences) // _____________________________________________ // == 2. ASSET INVENTORY == // Asset | Location | Value | Custodians // User ETH | Vault.sol | $Xm | Vault contract // Governance tokens | GovernorToken.sol | $Ym | Token holders // Protocol fees | Treasury.sol | $Zm | DAO multisig // == 3. TRUST BOUNDARIES == // What addresses / roles does the protocol trust? // - Owner/Admin: what can they do? Is there a timelock? // - Oracle: Chainlink? TWAP? Who updates it? // - External protocol: Aave, Uniswap — upgrade risk? // - Users: what can any EOA do without permission? // == 4. TOP 5 THREATS (ranked Likelihood × Impact) == // 1. [Score: 9] Flash loan oracle manipulation → drain collateral pool // 2. [Score: 6] Admin rug via oracle update without timelock // 3. [Score: 6] Reentrancy in deposit via ERC777 callback // 4. [Score: 4] Fee accumulation rounds down to 0 at small amounts // 5. [Score: 3] TWAP manipulation on thin liquidity pool // == 5. KEY INVARIANTS TO TEST == // totalShares * exchangeRate == totalAssets (within rounding) // No user can withdraw more than they deposited (net) // All loans must be collateralized above 150% at all times // == 6. HIGH-PRIORITY FUNCTIONS (based on asset flow) == // 1. deposit() — asset intake // 2. withdraw() — asset release // 3. borrow() — debt creation // 4. liquidate() — forced position close // 5. setOracle() — price update // == 7. AUTOMATED TOOL RESULTS SUMMARY == // Slither: X findings (Y high, Z medium) — [investigate list] // 4naly3er: X findings — [investigate list] // Coverage: X% — [uncovered functions]
💡 Threat Model Anchors Time Allocation

Once your threat model document identifies the top 5 threats by Likelihood × Impact score, your time allocation becomes objective. Spend roughly 1 hour of investigation per point of score. A score-9 threat gets 9 hours; a score-3 threat gets 3 hours. In a 7-day audit with approximately 40 hours of available time, this ensures your highest-value threats are fully investigated before lower-value ones. If you find a threat is not exploitable after investigation, score it down and reallocate time accordingly.

Key Takeaways

  • Threat modeling before reading code ensures you focus on the highest-value attack surfaces rather than spending time on low-risk areas.
  • Defining assets with dollar values makes prioritization objective — a $50M collateral pool deserves more review time than a $10K fee collection mechanism.
  • The STRIDE framework provides a systematic way to think about threat categories, including ones that are easy to overlook like Repudiation (missing events) and Information Disclosure (MEV).
  • Trust boundary analysis identifies the most dangerous assumptions in a protocol — "we trust the oracle" becomes a concrete threat when the oracle can be manipulated or go stale.
  • Likelihood × Impact scoring gives every threat a numerical priority, which guides your time allocation during the audit.
  • MEV threats deserve explicit identification — sandwich attacks, front-running oracle updates, and JIT liquidity attacks are protocol design issues, not just external market forces.
  • Writing invariants before auditing gives you a testable specification — any code path that violates an invariant is a finding, and those invariants can be encoded directly into Foundry invariant tests.
  • DeFi-specific threats (oracle manipulation, flash loans, governance attacks, MEV) deserve dedicated threat modeling passes because they do not map cleanly to traditional STRIDE categories.