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 Idan handoff (
~/bounty/argus/idan_handoff.py LENS_MAP) routes a fresh target's
fit-classes to the matching lesson files. This INDEX is the human-readable mirror of that map plus
the lenses the LENS_MAP does not yet wire (V4-hook, oracle-provider, keystore/AA, methodology).
When you add a lesson, add a row HERE and a LENS_MAP tuple THERE - they should not drift.
- Each row: target-class -> lesson file -> the ONE-LINE key check ("when auditing X, do Y").
- For the deeper cross-cycle defenses, read
~/bounty/cycles/cycle-232-session-reflector/00-session-meta-lessons.md
(the 5 meta-patterns that span these lenses).
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
- Kernel-copy fuzz (C219/C220/C221/C231): when the value-moving core is a small pure-math lib,
copy it VERBATIM into a fresh forge-std-only project and fuzz the conservation invariant. Avoids
wiring a 30-file type graph or a mainnet fork on a load-constrained box; the copied bytes ARE the
production logic. State the limit (proves local arithmetic, not upstream delta returns) in the verdict.
- Role-fence FIRST (C214/C219/C221): read the fenced trust model, filter to the untrusted-reachable
entrypoints, then spend ALL empirical effort there.
- Rule-38 into submodules (C223/C224): audit fixes often land in a pinned library base (solady
ERC-7739) or a divergent branch - classify each "changed" file {logic|pragma|fmt} and confirm branch
identity before believing an alarming delta.
- Saturation-likely classes (C222/C230/C231): $1M+ multiply-audited targets (V4 forks,
Schnorr oracles) - frame as hunt-with-LEARN-fallback; WALK CLEAN with the banked lens IS the win.