Why audits disappoint
Teams commission a smart contract audit expecting it to certify that the code is safe. What they receive is a report on one commit, written by reviewers who had a few weeks to understand a system its authors spent months building. When the audit is booked for the week before launch, the findings arrive with no time to fix them properly, fixes go in without review, and the contract that reaches mainnet is not the one that was audited.
An audit works when it is the middle of a process rather than the end of one. The work before it determines what the auditors can find; the work after it determines whether their findings are fixed without introducing new faults. This checklist covers both, sequenced backwards from the mainnet date so that each step has the time it needs.
Work backwards from mainnet
Fix the launch date, then count back. A realistic external audit takes two to four weeks for a contract system of moderate size, and auditors are often booked well in advance. Remediation and re-review need at least another two weeks. The preparation that makes the audit worth its cost — a threat model, written invariants, a complete test suite and fuzzing — needs to be finished before the audit starts, which puts code freeze four to six weeks before launch and design decisions earlier still.
If the date cannot accommodate that sequence, move the date or reduce the scope. Compressing the sequence is how the unaudited fix reaches mainnet.
The checklist
Checklist
When | Check | What it catches |
|---|---|---|
Six to eight weeks out | Threat model: actors, assets, trust assumptions, privileged roles | Attacks nobody thought to test for |
Six to eight weeks out | Invariants written in plain language, such as "total supply equals the sum of balances" | Logic errors that pass every example-based test |
Six to eight weeks out | Access control and upgrade model decided and documented | Admin keys holding more power than anyone intended |
Six to eight weeks out | Oracle and price inputs reviewed for manipulation | Flash-loan and price-manipulation attacks |
Four to six weeks out | Unit and integration tests, including every revert path | Regressions and unhandled edge cases |
Four to six weeks out | Invariant and fuzz testing against the written invariants | State sequences no one wrote a test for |
Four to six weeks out | Static analysis run and every finding triaged | Known vulnerability patterns |
Four to six weeks out | Audit scope frozen at a commit hash, with documentation | Auditors reviewing a moving target |
Audit window | Auditors given the threat model, invariants and test suite | Audit time spent rediscovering what the team knew |
Remediation | One commit per finding, each with a regression test | Fixes that introduce new faults |
Remediation | Auditors re-review the fixes before deployment | Unreviewed code reaching mainnet |
Launch week | Scripted deployment rehearsed on a mainnet fork | Wrong constructor arguments, owners or initialisation |
Launch week | Source verified; privileged roles on a multisig behind a timelock | A single key able to move user funds |
After launch | Monitoring, a tested pause, an incident runbook and a bug bounty | Slow response when something goes wrong |
Before the audit
Write the threat model down
List who interacts with the system, what each of them can do, what is worth stealing or breaking, and which parties the design trusts. Most serious contract failures are failures of a trust assumption nobody stated — a price feed assumed to be honest, an admin assumed never to be compromised, an external contract assumed never to call back. Writing the assumptions down is what makes them testable.
Turn properties into tests
Example-based unit tests check the cases their authors imagined. Invariant testing and fuzzing check properties across thousands of generated sequences, which is where the surprising bugs live. Tools such as Foundry's invariant testing and Echidna make this routine; the hard part is writing invariants worth checking, which is why they belong in the design phase rather than the week before the audit.
Run static analysis and read the results
Static analysers such as Slither catch known patterns — reentrancy, unchecked return values, dangerous delegate calls — quickly and cheaply. Their output is noisy. Triage every finding and record why each one does or does not apply, so that the auditors start from a clean baseline instead of spending their time on issues a tool could have closed.
During and after the audit
Give the auditors everything: the threat model, the invariants, the test suite, and an honest account of the parts the team is least sure of. An audit is a limited amount of expert attention, and it should go where the risk is.
When findings arrive, fix each in its own commit with a regression test that fails before the fix and passes after it. Then have the auditors review the fixes. A fix made under time pressure is new code, and new code is exactly what the audit did not see.
Reading the report
An audit report lists findings by severity, usually from critical to informational, against the commit it reviewed. Read three things before the findings themselves. The commit hash, to confirm it matches what will be deployed. The scope, to see which contracts were reviewed and which were not. And the findings marked acknowledged rather than fixed, because those are the risks the team has decided to carry into production.
Publish the report alongside the deployed addresses so that users and integrators can check the same things. A report that covers a different commit from the one on mainnet, or leaves out a contract the system depends on, tells its readers less than it appears to — and they deserve to know that.
Launching and operating
Deployment is its own source of failure. Script it, rehearse it on a fork of mainnet state, and review every constructor argument and initialisation call — a contract deployed with the wrong owner is as exploitable as one with a bug. Verify the source on the block explorer so that users and monitors can read what is running.
After launch, the privileged roles should sit on a multisig behind a timelock, so no single key can act and every administrative change is visible before it takes effect. A pause mechanism should exist and have been tested. Monitoring should alert on unusual flows, and an incident runbook should name who decides and who acts. A bug bounty invites the scrutiny that continues after the auditors have gone.
Upgradeable contracts deserve particular care. An upgrade is a deployment of new code, and it should pass through the same sequence — threat model, tests, review, rehearsal — compressed if the change is small, but never skipped because the system is already live.
This sequence is how we run contract work in Security Engineering, alongside the protocol design in Blockchain Engineering. The order matters more than any single check: every step exists so that the next one has something reliable to stand on.