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 ReentrancyGuard on 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 nonReentrant guards 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/public function has explicit access control or is intentionally permissionless.
  • Check for unprotected initializers in proxy patterns (missing initializer modifier, or initialize() callable more than once).
  • Review onlyOwner vs. role-based access (OpenZeppelin AccessControl). Look for roles that can grant themselves additional roles.
  • In Diamond/ERC-2535 proxies, audit diamondCut permissions โ€” 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 TimelockController or 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 updatedAt from latestRoundData(). 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 unchecked blocks reintroduce risk โ€” audit every unchecked block 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 OpenZeppelin SafeERC20 unconditionally.
  • Approval race condition: approve(X) after approve(Y) can be front-run. Prefer increaseAllowance.
  • 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-core storage layout checks.
  • selfdestruct in implementation: Post-Dencun, selfdestruct only 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 facetFunctionSelectors mapping 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 chainId and 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.