Introduction
Smart contract audits are the last โ and often only โ line of defense between a protocol and catastrophic fund loss. Yet not all audits are created equal. Whether you are an auditor sharpening your methodology or a protocol team evaluating an audit report, understanding what to look for at a granular level is essential. This guide provides an advanced, opinionated checklist that goes beyond the OWASP-style top-10 lists and into the edge cases that routinely slip through.
1. Reentrancy โ Beyond the Classic Pattern
The DAO-era fallback() reentrancy is well-known, but modern variants are subtler:
- Cross-function reentrancy: State is consistent within a single function but inconsistent across two functions sharing the same state variable. A reentrant call into a different function can exploit the intermediate state.
- Cross-contract reentrancy: Protocol A calls Protocol B which calls back into Protocol A. The
ReentrancyGuardon a single contract is useless here because the mutex lives in a different storage slot. - Read-only reentrancy: Popularized by Curve/Vyper exploits in 2023. A view function returns stale state during a reentrant callback, misleading an external price oracle or share-price calculation. Check that
nonReentrantguards extend to view functions that downstream integrators may rely on.
What to audit: Verify checks-effects-interactions ordering, cross-contract call graphs, and whether any view function is consumed externally during a callback window.
2. Access Control & Privilege Escalation
- Verify every
external/publicfunction has explicit access control or is intentionally permissionless. - Check for unprotected initializers in proxy patterns (missing
initializermodifier, orinitialize()callable more than once). - Review
onlyOwnervs. role-based access (OpenZeppelinAccessControl). Look for roles that can grant themselves additional roles. - In Diamond/ERC-2535 proxies, audit
diamondCutpermissions โ a compromised facet manager owns the entire protocol. - Timelocks: confirm that admin functions with large blast radius (e.g., changing oracle addresses, pausing) are behind a
TimelockControlleror multisig.
3. Oracle & Price Manipulation
- Spot-price reliance: Any use of
getReserves()from a Uniswap V2-style pool is manipulable within a single transaction via flash loans. - TWAP window: For Uniswap V3 TWAP, confirm the observation window (commonly 30 min) is long enough relative to the chain's block time and liquidity depth.
- Chainlink staleness: Check
updatedAtfromlatestRoundData(). Protocols that skip the staleness check inherit a stale or zero price during L2 sequencer downtime. - L2 sequencer uptime feed: On Arbitrum/Optimism, integrate the Chainlink L2 sequencer uptime oracle to pause operations after sequencer recovery grace periods.
`solidity
(, int256 answer, , uint256 updatedAt, ) = priceFeed.latestRoundData();
require(answer > 0, "Negative price");
require(block.timestamp - updatedAt < STALENESS_THRESHOLD, "Stale price");
`
4. Arithmetic, Rounding & Precision
- Solidity 0.8+ has built-in overflow checks, but
uncheckedblocks reintroduce risk โ audit everyuncheckedblock independently. - Rounding direction: In vault share calculations (ERC-4626), always round against the user: round down on deposit (fewer shares minted), round up on withdrawal (more assets required). The first-depositor inflation attack exploits incorrect rounding.
- Precision loss cascading: Intermediate divisions before multiplications truncate silently. Reorder operations to multiply first.
- Token decimals: Never assume 18 decimals. USDC (6), WBTC (8), and some tokens use non-standard decimals. Normalize early.
5. ERC-20 Edge Cases
- Fee-on-transfer tokens: The received amount is less than the sent amount. Always measure
balanceAfter - balanceBefore. - Rebasing tokens (stETH, AMPL): Balances change between transactions. Wrap before internal accounting.
- Return-value quirks: USDT does not return a
bool. Use OpenZeppelinSafeERC20unconditionally. - Approval race condition:
approve(X)afterapprove(Y)can be front-run. PreferincreaseAllowance. - Pausable / blocklist tokens (USDC, USDT): Functions may revert unexpectedly โ ensure the protocol handles this gracefully rather than bricking funds.
6. MEV, Front-Running & Transaction Ordering
- Sandwich attacks: Any AMM swap without slippage protection or a deadline parameter is vulnerable.
- Commit-reveal gaps: If a protocol uses a commit-reveal scheme, ensure the reveal window is tight and the commitment is binding (hash includes
msg.sender). - Liquidation incentive alignment: If liquidation rewards are too high, searchers will grief healthy positions via oracle latency. If too low, bad debt accrues.
7. Proxy & Upgradeability Pitfalls
- Storage collisions: In UUPS/Transparent proxy patterns, verify storage layouts match between implementations using
@openzeppelin/upgrades-corestorage layout checks. selfdestructin implementation: Post-Dencun,selfdestructonly sends ETH and no longer destroys code except in the same transaction as creation โ but legacy behavior on some chains still applies. Audit target chain behavior.- Uninitialized implementation contract: Call
_disableInitializers()in the implementation constructor to prevent direct initialization of the logic contract. - Function selector clashing: In Transparent Proxies, admin vs. user function selectors must never collide. In Diamond proxies, verify
facetFunctionSelectorsmapping integrity.
8. Gas Griefing & DoS Vectors
- Unbounded loops: Iterating over a dynamic array that grows with user count is a time bomb. Prefer pull-over-push patterns.
- Return-data bombs: An external call that returns megabytes of data can OOG the caller. Use low-level calls with return-data size caps (
assembly { returndatacopy(...) }). - Block gas limit DoS: If a critical function (e.g.,
distribute()) can be pushed past the block gas limit by adversarial array inflation, the protocol is bricked.
9. Cross-Chain & Bridge-Specific Concerns
- Verify source-chain authentication: on the destination chain, ensure messages are accepted only from the canonical bridge endpoint and the expected source sender.
- Replay protection: messages should include
chainIdand nonce. - Finality assumptions: optimistic bridges have a challenge window; instant relaying without fraud-proof verification introduces trust.
10. Tooling & Methodology
No single tool is sufficient. Layer them:
| Layer | Tools (2025) |
|---|---|
| Static analysis | Slither, Aderyn, Wake |
| Fuzzing | Foundry fuzz, Echidna, Medusa |
| Formal verification | Certora Prover, Halmos, HEVM |
| Manual review | Line-by-line with threat model |
| Invariant testing | Foundry invariant campaigns (stateful fuzzing) |
Best practice: Write protocol-specific invariants (e.g., "total shares ร price โฅ total assets") and let stateful fuzzers hunt for violations over millions of call sequences.
Final Thoughts
An audit is not a binary pass/fail โ it is a risk-reduction exercise. The best audits combine automated tooling with adversarial manual review, maintain a living threat model, and produce findings ranked by exploitability and impact, not just severity labels. As protocols grow more composable and cross-chain, the attack surface expands accordingly. Continuous auditing, bug bounties, and monitoring (e.g., Forta, OpenZeppelin Defender) are no longer optional โ they are table stakes for any protocol managing meaningful TVL.