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 / external function actually called from outside the contract? Functions that are only used internally should be internal or private. 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 on main.
  • 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 in package.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 onlyOwner modifier guarding both "set fee" and "withdraw all funds" is a design smell.
  • Prefer AccessControl over Ownable for 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 (Ownable2Step or 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 loudlyrevert with a clear, custom error rather than returning false, swallowing exceptions, or relying on the caller to check.
  • Explicit return values — never use low-level call without checking the success boolean and the returned data length.
  • Safe ERC-20 — use SafeERC20's safeTransfer / safeTransferFrom / forceApprove for 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-upgradeability or 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:

QuestionLook 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.