Secure Smart Contract Design
Auditors review code that was designed by someone else. The principles below are not a design guide for engineers — they are the rubric an auditor applies when judging the security posture of an existing contract. Code that violates one of these principles is not necessarily wrong; code that violates them without justification is a finding waiting to be written.
Minimize the Attack Surface
Every externally callable function, every storage variable an attacker can influence, and every external contract the system interacts with is an attack surface. The questions to ask:
- Is each
public/externalfunction actually called from outside the contract? Functions that are only used internally should beinternalorprivate. Misclassified visibility is a recurring source of severe findings. - Does each function do only one thing? Multi-purpose functions ("if this flag is set, do X, otherwise do Y") concentrate complexity in ways that make security review harder.
- Are dead code paths and unused storage variables removed? They cost gas, confuse readers, and occasionally turn into footguns after an upgrade.
- Is the inheritance hierarchy as shallow as it can reasonably be? Diamond inheritance and deep chains hide the function actually being called.
Use Tested, Trusted Libraries
The contracts that have been attacked the most are also the ones that have been hardened the most. Auditors should look favorably on:
- OpenZeppelin Contracts for ERC-20 / ERC-721 / ERC-1155,
AccessControl,Ownable,ReentrancyGuard,SafeERC20,Pausable,Initializable, and the upgradeable variants. Pin to a specific audited release; do not depend onmain. - Solady (Vectorized) for gas-optimized primitives where every wei counts; widely audited but more terse than OZ — review carefully.
- Solmate (Transmissions11) for minimalist building blocks; widely forked but no longer the primary maintenance focus of its author.
- PRBMath, FixedPointMathLib for fixed-point arithmetic — never roll your own without a very good reason.
Conversely, treat the following as red flags:
- Hand-rolled re-implementations of ERC-20,
transferFrom, signature verification, or fixed-point math. - Forked libraries with local modifications and no diff against the upstream version.
- Unpinned or
^-pinned dependencies inpackage.json/foundry.toml.
Layer Access Control
Every privileged operation should be guarded by an access control mechanism whose granularity matches the action's blast radius.
- Two-tier minimum: owner/admin (rare, slow, governance-gated) and operator (frequent, fast, tightly scoped). A single
onlyOwnermodifier guarding both "set fee" and "withdraw all funds" is a design smell. - Prefer
AccessControloverOwnablefor systems with more than one privileged role; roles document themselves. - Time-lock anything that can rug. Parameter changes that affect user funds — fees, oracle sources, asset whitelists, upgrade pointers — should pass through a delay long enough for users to exit.
- Multisig privileged keys. Single-EOA ownership of a live protocol is a finding.
- Two-step ownership transfers (
Ownable2Stepor equivalent). One-step transfers to a typo'd address are unrecoverable. - Renouncing must be intentional. Accidental
renounceOwnership()calls have permanently broken contracts.
Follow Established Security Patterns
The patterns below have all failed in production at least once when not followed; the audit lens is "did the team know about this pattern and choose not to use it, or did they not know?"
- Checks-Effects-Interactions — validate inputs, update state, then make external calls. The single most violated pattern in reentrancy findings.
- Pull over push — let users withdraw funds from a contract balance, rather than the contract pushing funds to users. Push patterns are vulnerable to DoS from a single reverting recipient.
- Reentrancy guards — apply
nonReentrant(or equivalent) to any function that makes external calls and modifies state. Belt and suspenders: combine with CEI. - Fail loudly —
revertwith a clear, custom error rather than returningfalse, swallowing exceptions, or relying on the caller to check. - Explicit return values — never use low-level
callwithout checking the success boolean and the returned data length. - Safe ERC-20 — use
SafeERC20'ssafeTransfer/safeTransferFrom/forceApprovefor any third-party token; many tokens do not return a bool, and several (USDT being the canonical example) revert on non-zero-to-non-zero approvals. - Bounded loops — never iterate over an unbounded user-controlled array; an attacker can grow it until your function exceeds the block gas limit.
- Pull-payment escrows for batch payouts — see OZ
PullPayment.
Make Failure Modes Explicit
Contracts that have an answer to "what happens when this goes wrong?" are dramatically safer than ones that do not.
- Pause switches for the operations that can lose user funds, with the smallest possible authority needed to flip them.
- Circuit breakers on parameters that, when crossed (e.g. a 10% oracle deviation in a block), suspend the affected pathway.
- Bounded blast radius — segment the system so a bug in one module cannot drain the entire treasury.
- Recovery procedures for stuck or accidentally-sent tokens, with appropriate access control.
- Documented incident response — an on-chain pause is much more useful when the team has rehearsed using it.
Design for Upgradeability — Carefully
If the system is upgradeable, the audit must cover the upgrade path itself, not just the current implementation.
- Storage layout is part of the public ABI. Adding, removing, or reordering state variables across upgrades is a catastrophe waiting to happen — use storage gaps or namespaced storage (ERC-7201).
- Initializers must be guarded against reinitialization and front-running.
- Implementation contracts must be deployed, initialized at the proxy level only, and ideally have their own
_disableInitializers()in the constructor. - Function clashes between proxy and implementation can hide methods or, worse, redirect them; auditors should run
slither-check-upgradeabilityor equivalent. - Authorized upgraders should be timelocked multisigs, never single EOAs.
These topics are covered in depth in §4.12 (Upgradeability Patterns and Vulnerabilities).
Design for Composability — Defensively
Other contracts will call yours, in ways the original team did not anticipate. Audit-worthy questions:
- Is each external function safe to call from a contract (not just an EOA)? Will it behave correctly if the caller is reentrant?
- Are read functions cheap enough that integrators will actually use them, rather than reconstructing state from events?
- Are events emitted for every state change that an off-chain indexer or another contract might care about?
- Are price-reading functions resistant to flash-loan manipulation (TWAPs, chainlinked oracles with deviation checks), not spot-price reads from an AMM?
- Are deadline / nonce / chainId fields used everywhere a signature is verified?
A Design-Review Checklist
When working through a fresh contract, an auditor can use the following as a quick gut-check:
| Question | Look for |
|---|---|
| What is the most expensive thing this contract can lose? | Total value at risk; sets the bar for the rest of the review |
| Who can call the most expensive function? | Access control matrix |
| What happens if that caller is compromised? | Timelocks, multisig, circuit breakers |
| Where are the external calls? | CEI ordering, reentrancy guards |
| Where are the math operations? | Overflow/underflow, precision loss, rounding direction |
| What does this contract assume about callers, callees, and the outside world? | Oracle freshness, token quirks, block timing, gas limits |
| What is the recovery path when an assumption breaks? | Pause, upgrade, migration, social fallback |
If any of those questions does not have a clean answer, the audit has its first findings.