← back to lessons

DeFi / Smart-Contract Audit Lens INDEX

Master pre-read map for the DeFi/audit lessons in this library. Consolidated by the Cycle 232 Session Reflector pass (2026-06-10) over cycles 204-231. The AI-agent lenses have their OWN index at ai-agent-security/INDEX.md and are NOT duplicated here.

How to use this file

The single question that opens most DeFi audits

"Who controls the numerator / the clock, and can an UNTRUSTED caller move it?" If the value-moving input is a keeper/precompile/balanceOf read the user cannot move, half the rounding/manipulation classes are structurally closed - confirm in minutes and reallocate. See meta-pattern M1.


Vault / ERC4626 (deposit-side inflation + rounding sign)

Lesson file Key check
concrete-earn-v2-cycle214.md Is PPS EXOGENOUS (accounting var) or BALANCE-DERIVED (asset.balanceOf)? Exogenous + virtual-shares/decimals-offset on both mulDiv sides => donation/first-depositor inflation closed; one donation test + one 1-wei fresh-vault test confirms the whole family fast.
2026-06-10-rebasing-two-track-share-drift-measure-the-sign.md Enumerate every accounting track; for hot-path "net-zero" skip-optimizations measure the SIGN of the drift over SUSTAINED accumulation, not the single-step excursion - protocol-favorable sign = WALK.
2026-06-10-cycle204-async-filler-split-rounding-value-gate.md Two predicates with OPPOSING rounding (floor-on-accept, ceil-on-require) over the SAME realized quantity compose to value-conservation; same-direction rounding = the leak.

Async withdrawal queue (ERC7540 / epoch-batched)

Lesson file Key check
concrete-earn-v2-cycle214.md Single epoch-UNIFORM settlement price + cancel-only-while-epoch-OPEN (post-price cancel = free option) + obligated assets fenced from withdraw float and allocator. Breaking the open-only-cancel leg is the #1 thing to hunt.

Cross-chain messaging / OFT (omnichain fungible token - LayerZero V2)

Lesson file Key check
2026-06-10-cycle234-cross-chain-oft-audit-primitives.md Canonical OFT core is saturated - yield is in the integrator's _debit/_credit/lzReceive/lzCompose overrides. 6 primitives: (1) supply conservation is EMERGENT not on-chain-checked - one adapter per mesh, no 2nd mint authority; (2) dust removed on DEBIT side, watch fee-after-dust + uint64 SD-cast truncation; (3) lzReceive = OnlyEndpoint + OnlyPeer + per-path nonce consume-once (wrong peer = inflation door); (4) HIGHEST YIELD - lzCompose is PERMISSIONLESS, composer MUST assert msg.sender==endpoint + _from==trusted OFT + bind action to message-decoded composeFrom/amountLD; (5) rate-limit is optional, check debit-vs-credit enforcement side; (6) OFTAdapter assumes lossless - fee-on-transfer/rebasing inner token = mesh-wide over-unlock. Immediate consumer: dreUSD (Sherlock 1259) trio, pre-flight PF0-PF5 in the cycle verdict.

Cross-chain BRIDGE - intent / optimistic flavor (Across-class, distinct from OFT row above)

Lesson file Key check
2026-06-10-cycle235-cross-chain-bridge-audit-primitives.md Every bridge SHIFTS trust (validator-set vs optimistic proposer) - audit Q#1: "where is trust shifted, crypto-check or economic dispute?". Classify archetype A (attestation/verifier-set: OFT/Wormhole/Ronin -> C234 lens) vs B (optimistic/intent: Across/UMA -> this). 7 primitives: P1 deposit<->fill matching (compare the field-SET hashed into fill-id vs deposit-id; a MISSING field = forgery door; is matching on-chain or DEFERRED off-chain?); P2 replay (nullifier keyed by FULL src+dst chainId+nonce tuple; flag silent caller-suppliable nonces); P3 optimistic settlement = HIGHEST YIELD (merkle proves leaf-in-root NOT root-honest; root honesty is purely bond+dispute-window+>=1-honest-watcher; Nomad analog - chain-halt/censorship/oracle-capture during liveness = full drain; check uninitialized-root revert + admin escape hatches); P4 fallback exclusivity (fast+slow share ONE marker; subtle bug = double-FUND-from-pool not double-pay-recipient); P5 conservation invariant + signed-reserve over/underflow + spoofed canonical-return + reentrancy-before-reserve-update; P6 handler calldata-trust (LiFi/C210 analog) - hook caller arg must NOT be trusted (relayer-controlled), leftovers stealable, marker-set BEFORE external call; P7 amount-sent==recorded? FoT/rebasing/decimals + allowlist-reachability (permissionless-input vs curated-refund mismatch). P1+P3 joined = Ronin/Wormhole/Nomad loss class. Complements C234: shared P1/P2/P6/P7, distinct axis is verifier-set-acceptance (C234) vs dispute-window-integrity (C235).

Oracle - CONSUMER side (price caps / staleness)

Lesson file Key check
2026-06-10-cycle212-rwa-vault-decimals-equality-gate-and-validate-then-mutate-slippage.md Staleness/deviation/min-max guards on EVERY mint/redeem/liquidate path; a transfer-level base18<->token-decimals equality require (positive control) deprioritizes the whole rounding-drift class.
mezo-musd-cycle220.md A trusted oracle with only a staleness guard (missing price>0 / answeredInRound / deviation) is Low/Informational hardening when the feed is fenced OOS - do not inflate.

Oracle - PROVIDER side (Schnorr / aggregate-sig / optimistic)

Lesson file Key check
oracle-provider-schnorr-aggregate-cycle222.md 6-point checklist: what is BOUND into the signed message (replay), aggregate-key re-derivation (rogue-key), double-sign/threshold-fake guard, malleability bounds, optimistic-challenge SYMMETRY (valid-not-challengeable AND invalid-challengeable), finalization read-gate. On 3+ audits, crypto-primitive edge cases yield ~0 signal - pivot to calldata-encoding ambiguity in the challenge path.

Router / aggregator / intent-settlement

Lesson file Key check
2026-06-10-cycle209-dex-router-audit-primitives.md The one question: can the funds-pull from/payer ever be set to anyone but msg.sender/the signer? Trace every transferFrom/claimTokens sink. minReturn on MEASURED balance-delta of FINAL token at FINAL receiver. Callback auth (transient-pool or CREATE2 init-code-hash; multi-fork = the seam). The arbitrary adapter .call is INTENDED - defense is at the funds-pull, not the call.
2026-06-10-cycle210-lifi-allowlisted-aggregator-callback-auth.md Allowlisted/Diamond/separate-allowance-proxy refinement: transient stored-pool callback auth (reset on every exit incl revert + lock + router-set data), arbitrary-from proxy safe IFF caller-allowlisted + from=msg.sender + swap-loop blocks callTo==proxy, approveTo escape sentinel.
2026-06-10-cycle215-intent-settlement-witness-binding.md Intent/solver inversion: from is the SIGNER by design, so the canary becomes "is every value-affecting field (recipient/buyToken/minOut/actions/deadline) bound into the recomputed signed WITNESS?" Classify each entrypoint relayer-open (full action list must be in witness) vs solver-gated (output still witness-bound + audit the caller allowlist).
2026-06-10-cycle204-async-filler-split-rounding-value-gate.md Async/EIP-1271 filler: never verdict on the accept predicate alone; find the SETTLEMENT-COMPLETION predicate and verify opposing rounding over the same realized quantity.

Restaking / shared-security (epoch withdrawal accounting)

Lesson file Key check
2026-06-10-cycle213-restaking-slash-vs-withdrawal-epoch-union-bound.md slashableBalanceOf is a UNION upper-bound over valid capture epochs; each slash takes the capture-appropriate SUBSET. "reported > slashed-at-one-capture" is NOT a leak if every epoch's funds are reachable by SOME valid capture - test BOTH capture branches. Real bug lives in the epoch off-by-one gate or claim-before-slash race. Checkpoint-tracked activeStake = donation/inflation-proof (whole share-math class dead).

Funding / perp / margin venue

Lesson file Key check
2026-06-10-cycle219-funding-index-venue-keeper-clock-closes-both-rounding-and-maturity-seams.md Find the funding CLOCK. If it's a keeper-published shared index whose DELTA the user cannot choose, and accrual is signed-size x Δindex floored toward -inf with margin rounded up, the rounding-farm AND maturity-boundary seams are BOTH closed - confirm in minutes, reallocate to cross-margin netting + callback actor-binding.
2026-06-10-cycle221-LST-exchange-rate-funding-clock-generalizes-balance-read-vs-price-quote.md The "clock" generalizes to "what feeds the exchangeRate numerator?" BALANCE-READ backing (LST/precompile/balanceOf) = keeper-fenced, manipulation hunt is a dead end; PRICE-QUOTE backing (AMM/TWAP/illiquid synth) = the manipulable case. Same question, opposite answer.

CDP / stablecoin (Liquity-fork-with-interest)

Lesson file Key check
mezo-musd-cycle220.md Hunt the FORK-DELTA. When a Liquity fork ADDS interest, reconcile dual interest accounting (aggregate_minted == sum(per-trove), aggregate floors once >= per-trove floored N times). The bug lives in any addPrincipal/removePrincipal NOT preceded by an aggregate updateSystemInterest() sync. When a fork REMOVES canonical danger (decaying base-rate, recovery-mode mass-liq), stop grinding the removed surface - pivot to the added complexity.

V4-hook / singleton AMM / launchpad

Lesson file Key check
v4-singleton-hook-amm-cycle230.md Hooks are user-supplied + OOS for their own pool; the only valid core axis is "can a hook/locker break value conservation at the VAULT level or reach a DIFFERENT pool's reserves?" Test flash-accounting Layer A (unsettled-delta==0 at unlock) AND Layer B (per-app reserve ledger - forks that drop it lose app-isolation); hook-return-delta must be the hook's own settle-obligation and SafeCast-bounded against int128 wrap.
v4-launchpad-graduation-cycle231.md On launchpad-as-V4-hook, hunt the GRADUATION HANDOFF, not the curve math: trigger gate (early/late/never), recipient binding (hard-bound vs caller-supplied), replay/double-graduate. NEW class: migration-brick when fees > balance underflows balance - fees (permanent stuck liquidity) - verify fees is a proven subset of the migrated physical balance. Use WITH cycle230.

Concentrated-liquidity AMM / V3-fork (tick + fee-growth + rounding) - DISTINCT from V4-hook above

Lesson file Key check
2026-06-10-cycle237-concentrated-liquidity-amm-audit-primitives.md V3 tick/fee-growth/swap-math is the most-forked AMM surface (PancakeSwap V3, Algebra, Ramses + dozens). 8 primitives: (1) fee-growth = global-below-above with INTENTIONAL unchecked wraparound (the classic fork bug - a Solidity-0.8 port WITHOUT unchecked{} either DoS's or corrupts accounting; cross() must flip BOTH feeGrowthOutside0/1; >=/< boundary operators exact); (2) tick bounds + magic constants in getSqrtRatioAtTick UNCHANGED, MIN/MAX_SQRT_RATIO == ratio(MIN/MAX_TICK), swap-loop bounds-clamp present; (3) HIGHEST YIELD - swap-step rounding direction: tabulate every getAmount0/1Delta call, roundUp=true for amountIn, false for amountOut, fee mulDivRoundingUp - one flipped flag = pool loses dust PER swap step, amplified to a drain; (4) liquidityNet sign keyed to swap direction (negate on zeroForOne), upper subtracts/lower adds delta, gross cap + int128-min safety, clear only at gross==0; (5) tokensOwed = unchecked delta x liquidity (uint128 overflow intentional), collect MINs vs owed + decrements before transfer, no 0-liquidity poke; (6) next-sqrt-price rounding: Amount0 rounds UP, Amount1 rounds DOWN (swap = overshoot tick), overflow requires (product/amount==sqrtP, numerator1>product, sqrtP>quotient) intact; (7) every entry lock+noDelegateCall, post-callback balanceBefore+owed <= balanceAfter MEASURED on-chain not trusted (pool trusts balances not callback claims); (8) oracle is pure storage - manipulation lives in the CONSUMER's TWAP window length / spot-price reads, re-derive economic cost per chain. Rounding-direction-critical hotspots = #3 and #6; #1+#5 are checked-vs-unchecked-port hazards. Distinct from V4 (cycle230/231): V3 is per-pool contract + tick/fee-growth math; V4 is singleton + flash-accounting + hooks.

Rebasing / shares accounting

Lesson file Key check
2026-06-10-rebasing-two-track-share-drift-measure-the-sign.md SUM(per-wallet balances) == aggregate/totalSupply; find hot-path "net-zero" skips that update one track not the other; finding hinges on the SIGN of the drift (per-wallet > aggregate = drain direction) AND reachability of that sign by an untrusted caller funding an attacker-controllable withdrawal.

Cross-chain keystore / AA config / OFT

Lesson file Key check
stackup-keystore-cycle223.md Replay is a BINDING question not a presence question (cross-chain replay is often the FEATURE): enumerate the signed preimage - account (anti cross-account-hijack), nonce, current-config hash, NEXT-config hash (anti substitution), chainid (opt-in). Next-config install must require the NEW controller's approval. Rule-38 must follow inheritance into pinned library submodules (fixes can live in solady base, not the in-scope diff).

RWA redemption (tokenized treasury / tbill)

Lesson file Key check
2026-06-10-cycle212-rwa-vault-decimals-equality-gate-and-validate-then-mutate-slippage.md Decimals-equality gate (positive control), validate-then-mutate slippage seam (a require(x>=min) checked on a value later REASSIGNED in a branch), oracle staleness.
2026-06-10-cycle218-validate-then-mutate-escalation-decider.md The DECIDER for a validate-then-mutate seam: profitable only if the mutated value is (1) attacker mid-tx movable, (2) NOT trusted-deterministic, (3) NOT re-validated by a downstream guard. Atomic balance-delta + trusted oracle + downstream recheck => LOW ceiling. "User loses X" != "attacker gains X" - prove the capture.

Methodology deciders (cross-class, not target-specific)

Lesson file Key check
2026-06-10-cycle218-validate-then-mutate-escalation-decider.md The 3-question escalation decider (above) - turns a "looks like a bug" missing-check into its honest severity.
2026-06-10-rebasing-two-track-share-drift-measure-the-sign.md Measure-the-SIGN over sustained accumulation before filing any rounding-drift finding.
2026-06-10-cycle216-list-vs-detail-api-asymmetry-lead-supply.md Lead-supply: a classifying field present only in the DETAIL endpoint (not the list payload) silently drops the richest leads - fetch detail or scan free-text; denylist dependency-org links when resolving a scope repo.
2026-06-10-cycle227-per-source-metric-semantics-normalize-before-threshold.md Lead-supply: the SAME metric means different things per source (contest in-window findings vs bounty lifetime findings) - normalize before thresholding; when the board looks empty, suspect the supplier (Cycle-198 doctrine).
2026-06-10-cycle217-contest-scanner-substring-status-trap-and-codeaccess-gate.md Lead-supply tooling: bind status to the JSON KEY not a substring/badge; filter codeAccess==public before clone; platform-lifecycle (wind-down) is a lead-quality signal; pull RAW HTML/RSC and parse deterministically, never WebFetch-summarize structured status.
2026-06-10-cycle236-lending-liquidation-audit-primitives.md Lending/CDP/perp LIQUIDATION lens (8 primitives, mined from Morpho Blue): health-check rounding (debt UP / collateral DOWN, HF==1 boundary, oracle decimal-scale cancels), LIF bonus bound+limits (cap, LLTV->1 degenerate, no caller input), bad-debt socialize asset-side not shares + min-clamp gated on collateral==0, partial-liquidation griefs/self-liq net<=0, accrue-interest-FIRST in every solvency entrypoint, oracle is integrator-chosen trust boundary (audit shifts to the feed), liquidate callback CEI+actor-bound, SharesMathLib four-way rounding always favors pool + virtual-offset kills first-depositor. Highest-yield class: rounding-direction/oracle-scale mutations in Morpho/Aave/Compound FORKS.

Governance / Timelock / Access-control (privileged-operation surface - present in MOST targets)

Lesson file Key check
2026-06-10-cycle238-governance-timelock-access-audit-primitives.md Governance/timelock/access lens (7 primitives, mined from OZ Governor+TimelockController+AccessControl+AccessManager+UUPS+Ownable2Step): (1) proposalId binds full payload so voted==executed calldata + executed-flag-before-call blocks replay/reentrancy; (2) vote weight from PAST snapshot not current balance (flash-mint-vote test) + Governor.clock==token.clock; (3) quorum supply read at SAME past snapshot, numerator checkpointed, hasVoted guard; (4) timelock delay-setter reachable ONLY through the delay + bootstrap/deployer admin renounced (retained instant-admin = full bypass); (4b) enumerate ALL PROPOSER/EXECUTOR/CANCELLER holders besides the Governor (extra proposer = vote bypass, extra canceller = DoS); (5) DEFAULT_ADMIN_ROLE=0x00 self-admin single-point; (6) UUPS _authorizeUpgrade MUST be overridden with real auth (empty/missing = anyone upgrades) + no re-callable initializer; (7) gov is arbitrary-caller, trust IS the vote so every weakness 1-4 becomes arbitrary-call. HIGHEST-YIELD: privileged fn reachable WITHOUT vote/delay (missing modifier, wrong role, extra timelock proposer, empty _authorizeUpgrade, re-init) - lives at the INTEGRATION SEAM, not OZ canon. CHEAP FIRST PASS: 4-column table fn -> modifier -> resolved role/address -> role-admin; the outlier cell is the bug (covers primitives 4,4b,5,6,7).

Recurring methodology notes that travel across ALL the above

Generated 2026-07-02 13:15:04 UTC | auto-sync /15min