◇ Theory
Checks, effects, interactions
When a contract calls another address, control leaves the function. If the contract has not finished updating its own state first, the callee can call back in and see the stale state — the root cause of reentrancy.
The defence is an ordering rule: check the conditions, apply the effects to your own storage, and only then interact with external addresses. Reentrancy guards and pull-payment patterns add a second layer.
Who is allowed to call this?
Every state-changing function needs an explicit answer to that question. Privileged functions — withdrawals, upgrades, parameter changes, initialisers — must be restricted to the right role, and initialisers must only ever run once.
In an audit, list each external function and write down who should be able to call it. Any function where the code does not enforce your answer is a finding.
Arithmetic and return values
Before Solidity 0.8, integers silently wrapped around on overflow; since 0.8 arithmetic reverts unless explicitly marked unchecked. Precision and rounding direction still matter, especially in share and fee calculations.
Low-level calls report failure through a return value rather than reverting. Ignoring that value means a failed transfer can look like a successful one.
Code that runs in someone else’s storage
delegatecall executes another contract’s code against the caller’s storage. It is how upgradeable proxies work — and why a proxy and its logic contract must agree exactly on storage layout.
Never let untrusted input choose a delegatecall target, and keep proxy admin data in dedicated slots (EIP-1967) so it cannot collide with application state.
Every lab pairs the vulnerable contract with its patched twin in the Post-Mortem stage — the exact lines that fail, and the exact lines that fix them.