Skip to content

Defence stack for contracts

No single layer is enough, so contracts are defended in five: build and test, review, deploy, run, respond. The first two try to stop a bug from shipping. The last three limit what a shipped bug or a stolen key can do.

Ordinary software leans on patching and rollback. On-chain, deployed code is immutable, transactions are final, and every bug has a payout. See Basics. That shifts the weight in two directions.

  • Before deployment, because there is no quiet hotfix. Hence fuzzing, invariant tests, formal verification and several kinds of external review.
  • Around the contract at runtime, because prevention fails anyway. The largest recent losses were key and infrastructure failures, not code bugs (Key and signer attacks). Hence timelocks, pause switches, withdrawal caps and monitoring that pages in minutes. Ronin’s 2022 loss was noticed six days later.
  • Foundry for unit tests, fuzz tests, invariant tests, and fork tests against mainnet state.
  • Static analysis in CI: Slither, Aderyn, Semgrep Solidity rules.
  • Fuzzers for longer campaigns: Echidna, Medusa.
  • Formal verification for core maths: Certora, Halmos.

An invariant is a statement that must hold after any sequence of calls, for example “no account ends insolvent” or “total assets are at least total claims”. The fuzzer generates random call sequences and tries to break it. Euler’s 2023 loss came from one function that skipped a solvency check the others had, which is what an invariant test exists to find.

  • An audit per release, scoped to the diff plus the storage layout.
  • Competitive audits (Code4rena, Sherlock, Cantina), where many reviewers compete on findings.
  • A standing bug bounty (Immunefi) with a published scope.

Closed source is not much of a control. Bytecode, storage and every transaction are public, and decompilers are good. It slows a casual attacker by days.

  • Deploy from CI with provenance (SLSA, Sigstore), not from a laptop.
  • Deterministic addresses (CREATE2), verified source, and on-chain bytecode compared with the build artefact.
  • The deployer key has no lasting power. Ownership moves to a multisig plus timelock in the same script.
  • Deploy and initialise in one transaction. See Proxies and upgrades.
  • Monitoring: Forta, Hypernative, Hexagate, Tenderly alerts.
  • What to watch: Upgraded, DiamondCut, RoleGranted, OwnershipTransferred, oracle deviation, outflows against a baseline, unusual callers on sensitive functions.
  • Circuit breakers: a pause guardian, per-period withdrawal caps.
  • Timelock: a mandatory change window before an admin change applies. It gives watchers time to react, and it turns a bad signature into a visible queued action instead of an instant loss.
  • Role separation: a pauser that can only pause, an upgrader behind multisig and timelock. The pause guardian is a separate low-privilege role so pausing is fast.
  • A runbook: detect, pause, war room, communicate.
  • SEAL 911 for live help from trusted security responders. SEAL Safe Harbor is a framework that lets a protocol offer legal protection to white-hats who rescue funds during an active exploit.
  • A pre-signed pause is fine. Pre-signed anything else is how Drift lost about $285M in 2026.
Technique What it does Finds Misses Tools
Unit test Checks one function with chosen inputs Mistakes you thought of Everything you did not think of Foundry
Fuzz test Runs one function with many random inputs Edge values, rounding at tiny amounts Bugs that need a sequence of calls Foundry, Echidna, Medusa
Invariant test Runs random sequences of calls and checks a property after each Missing checks, broken accounting across functions Properties nobody wrote down Foundry, Echidna, Medusa
Fork test Runs against a copy of real mainnet state Integration surprises with real tokens and protocols Future state Foundry
Static analysis Scans source for known bad patterns without running it Reentrancy shapes, tx.origin, unchecked calls Logic errors; produces false positives Slither, Aderyn, Semgrep
Formal verification Proves a property for all inputs Maths errors no fuzzer reaches Anything outside the written specification; costly Certora, Halmos
Review Who looks When Strength Limit
Internal review The team Every change Knows the system Shares the authors’ blind spots
Audit A hired firm, a few people for a few weeks Per release Depth on the scoped diff Only what was in scope, only at that commit
Competitive audit Many independent researchers Per release Breadth; many eyes Uneven depth; needs triage
Bug bounty Anyone Continuously, on deployed code Covers the live system for its whole life Pays only after something is found; needs a clear scope
Control What it limits Cost Fails when
Timelock How fast an admin change can take effect Slower legitimate changes Nobody watches the queue; delay set to zero
Pause guardian How long an exploit can run A role that can halt the system The role is slow to reach, or can do more than pause
Withdrawal cap per period How much can leave in one window Friction for large legitimate withdrawals The cap is above what the protocol can afford to lose
Multisig on admin roles Who can change the system Slower operations Signers share one UI or one fleet of devices
Monitoring and alerts Time to notice On-call load, false positives Alerts go to a channel nobody reads at night
Bug bounty The incentive to exploit instead of report Payouts The bounty is far smaller than the money at risk
Symptom Cause Check
An exploit used a call sequence no test covered No invariant tests, or too few runs forge test --match-test invariant; raise the run count and depth in foundry.toml
A bug only appears with a real token or pool Tests use mocks forge test --fork-url $RPC
Rounding bug at tiny amounts Fuzz inputs never reached the edge forge test --fuzz-runs 100000; bound inputs to include 0, 1 and the maximum
A known pattern shipped anyway Static analysis not in CI, or findings ignored slither . as a required CI step
Deployed code differs from the audited commit Built or deployed by hand Compare cast code $ADDR with the build artefact; verify source with forge verify-contract
Upgrade corrupted state Storage layout changed forge inspect MyContract storageLayout, diffed against the previous release
An admin change went through unnoticed No alert on admin events Alert on Upgraded, DiamondCut, RoleGranted, OwnershipTransferred
The pause came too late Pause needs the same slow multisig as upgrades A separate pause role, with a pre-signed pause transaction ready
The deployer address still owns the contract Ownership transfer was a manual follow-up step Transfer ownership in the deploy script; assert the owner at the end

Incident runbooks worth having ahead of time, one per scenario: oracle failure, liquidation or keeper bot down, key suspected compromised, contract exploit. Each names who may pause, and who to call.

defence in depth, Foundry, forge test, unit test, fuzz test, invariant test, fork test, Echidna, Medusa, Slither, Aderyn, Semgrep, static analysis, formal verification, Certora, Halmos, audit, competitive audit, Code4rena, Sherlock, Cantina, bug bounty, Immunefi, provenance, SLSA, Sigstore, CREATE2, deterministic deployment, verified source, bytecode comparison, deployer key, multisig, timelock, TimelockController, pause, Pausable, pause guardian, circuit breaker, withdrawal cap, rate limit, role separation, AccessControl, monitoring, Forta, Hypernative, Hexagate, Tenderly, Upgraded, DiamondCut, RoleGranted, OwnershipTransferred, oracle deviation, outflow baseline, runbook, war room, SEAL 911, Safe Harbor, white-hat.