Manual-audit pre-grade CHECKLIST v1 (validated across Module 2 Escher + Module 3 RabbitHole)
Standing checklist to run on every codebase BEFORE concluding, targeting the logic layer scanners miss.
M2 derived items 1-3; M3 VALIDATED them (closed the M2 gaps: severity, adversarial-input, access-control) and
ADDED items 4-7 from the M3 misses. Apply ALL by default.
- Degenerate inputs: amount=0 / dust / type-max on each value-moving fn. ("Conserves on the honest path" is
necessary, not sufficient. M2 miss: refund theft via buy(0) after sale end.)
- Call AFTER terminal state (sale ended / finalized / quest ended / paused) and call REPEATEDLY (no
once-only flag?). [M3 WIN: caught H-02 repeatable withdrawFee this way.]
- Call AS the privileged party / receiver / counterparty, not just as an honest user.
- EVERY external call is an adversarial surface - incl. the REWARD/ASSET token's transfer hooks
(ERC777 tokensReceived, ERC1155 onERC1155Received, ERC721 onERC721Received) and any low-level call. Map
CEI for each. [M3 miss M-05: reward-token-callback reentrancy in claim(); I only checked NFT-mint callbacks.]
B. PRIVILEGED-RUG MATRIX - for each role/owner, "what can a LEGITIMATE holder DO to harm users?"
- Enumerate every role/owner and list the rug actions: revoke a role a live flow depends on, pause to block
claims, retarget an address (receipt/signer/oracle/fee contract), drain mid-flow, re-initialize, upgrade.
[M3 WIN: broken onlyMinter = no-op access control (H-01); F5 owner-pause griefs claims.]
- DUAL-AUTHORITY SEAMS: any contract that mixes Ownable + AccessControl (or two auth systems). transferOwnership
usually does NOT move DEFAULT_ADMIN_ROLE -> old owner keeps role power. Flag the desync. [M2 M-13, M3 F4.]
- BROKEN-MODIFIER scan: read every custom modifier - does it actually require/revert and run
_? A bare
comparison (msg.sender == x;) or a missing revert = no-op gate = full access-control bypass. [M3 H-01.]
C. SEVERITY DISCIPLINE - rate by permanence of impact, one-line justification per finding BEFORE the key
- Permanent fund theft/loss/lock or a bricked contract = HIGH, even when the trigger feels like an edge
(bad config / contract recipient / specific timing). Reachability lowers it only if genuinely unreachable.
[M2 gap: under-rated all Highs. M3 FIX: both Highs rated High via this rule.]
D. COMPLETENESS PASSES - what I still miss (M3 signal)
- SIBLING-DIFF: when one interface has multiple implementations (Erc20Quest vs Erc1155Quest, multi-token
adapters, per-chain variants), DIFF them line-by-line. The bug hides in the asymmetry / the sibling you
skipped. [M3 miss M-04: Erc1155Quest withdraw lacked the unclaimed-accounting Erc20Quest had. I have a banked
module-pair-asymmetry skill and STILL skipped it - apply by default.]
- "SAFE" must be re-checked from EACH party's POV (protocol vs buyer vs seller vs LP). "No protocol loss" !=
"no user loss." [M3 miss M-08: claimed receipt still sellable -> BUYER loses funds; I called it safe.]
- LIFECYCLE-BOUNDARY MAP: lay every state-changing fn against start/active/end/withdraw boundaries; look for a
fn missing a boundary check that another fn assumes finality of. [M3 miss M-02: mintReceipt has no endTime
check while withdrawRemainingTokens assumes mints are done.]
- DEPLOYMENT DETERMINISM: factory using create (nonce-based) vs create2(salt) -> reorg/address-prediction
risk on Polygon/Arbitrum/Optimism. [M3 miss M-01.]
- Read the SPEC/README and diff vs code: documented-but-absent functionality + intended-vs-actual behavior. [M2 M-01/M-05.]
reachability (e.g. _mint vs _safeMint callback). Pair every scanner hit with a manual reachability check,
and run A-D for the bugs scanners never raise at all.