Software Development, Technology

Smart contract security: 4 verification steps for developers

By James KillickSeptember 1, 2026

TL;DR: Smart contract security is a lifecycle, not a pre-launch audit. Four steps: threat model the economics and write invariants before you code, wire static analysis into CI with audited libraries, layer unit tests and fuzzing under symbolic execution and the Solidity SMTChecker, then audit a frozen commit and keep monitoring live state. The biggest losses came from economic design flaws and compromised governance keys, not from code an auditor missed.

Smart contract security is not an audit you book before launch. It is a lifecycle, and the audit is one step in it.

That distinction matters because the biggest losses in this space did not come from code an auditor missed. They came from economic design flaws nobody wrote down, and from governance keys compromised months after the report came back clean.

Here are the four verification steps that actually hold, and where the Solidity SMTChecker fits into them.

Why smart contracts get exploited

Most exploits are not clever. They hit the same handful of flaws over and over, which is why the OWASP Smart Contract Top 10 is worth learning properly rather than skimming once.

Reentrancy is the textbook one. A contract sends funds before it updates its own state. The receiving contract calls straight back in and drains against a stale balance. The 2016 DAO hack made this famous. It still shows up in new protocols that copy old code without understanding the checks-effects-interactions order.

Unchecked external calls are quieter. A `.call()` or `.send()` that ignores its return value can fail silently, so your contract thinks a transfer worked when it did not. Put that inside a loop over an untrusted array and one bad recipient can block payouts for everyone.

Arithmetic errors are mostly handled now. Solidity 0.8.x added overflow checks by default. But they come straight back in unchecked blocks, in inline assembly, and in old code on old compilers. Never assume a dependency was built on a safe one.

Oracle manipulation hits anything pricing assets from one source. Thin liquidity pools can be moved inside a single transaction, and flash loans make that cheap.

Front-running works because the transaction pool is public and ordered. Bots see your pending transaction, slip in ahead of it, and take the value.

Proxy and upgrade risk rounds it out. If a contract can be upgraded, that path can be hijacked. You get storage clashes between versions. You get contracts nobody set up properly, which anyone can then claim. And you get admin keys that become one weak point.

Build test cases against all of it:

  • Reentrancy through external calls before state updates
  • Ignored return values from low-level calls
  • Arithmetic errors in unchecked blocks or legacy compilers
  • Oracle price manipulation through thin or single-source feeds
  • Transaction ordering exploits like front-running and sandwich attacks
  • Denial of service through unbounded loops
  • Proxy initialisation and storage layout mismatches

Step one: design before you write Solidity

Decisions made on a whiteboard prevent more incidents than any tool you add later. Here is the important bit. Architecture is where you decide what can go wrong and how badly, and that is far cheaper to get right before deployment.

Threat model the economics, not just the code paths. A normal security review asks whether the wrong address can call a function. A contract threat model also has to ask what happens when someone takes a large flash loan, moves this pool's price, and calls the function in the same transaction.

Keep what you can off-chain. Not every calculation needs to live in a contract. Complex pricing, access decisions that do not need trustless enforcement, anything that tolerates a central step. Write only the final verifiable result on chain. The safest function is the one you never deploy.

Write your invariants down first. If a vault should never let withdrawals exceed deposits, write that sentence before you code the withdrawal. Plain English, so a non-technical stakeholder can sanity check it. That sentence becomes the spec your tests and SMTChecker check against.

Treat access control as a first-class problem. Role-based patterns beat a single owner key, because you can split admin from operations and revoke without redeploying. Put multi-signature on anything privileged: pausing, upgrades, treasury withdrawals. That limits the damage one compromised key can do. Same thinking as role-based access control in any other platform.

Decide upgradeable or immutable, on purpose. Immutable is safer in one way: there is no upgrade path for an attacker to hijack. But a bug is permanent unless you built a migration route. Proxies fix that and add their own risks. Ethereum's own guidance is blunt: plan the migration or governance model early, and document every recovery step before deployment, not during an incident.

Step two: catch the mechanical mistakes automatically

Most vulnerabilities are prevented, not discovered. That starts with what you reuse and what your pipeline catches before a human looks.

Use audited libraries. OpenZeppelin's contracts for token standards, access control and reentrancy guards have had years of public attack testing. Yours has not. And do not copy a contract off a block explorer and edit it. You inherit the bugs without the context that made the checks necessary.

Put static analysis in CI, not just on your laptop. Slither and Mythril catch a real share of the common classes on every pull request. The point is not catching everything. It is clearing the obvious stuff before a reviewer spends time on it.

Triage the output properly. These tools throw false alarms. So keep a file listing every warning you chose to ignore, with the reason written down. Have a second person check it. Never let someone quietly wave off a finding they did not understand.

A working setup looks like this:

  1. Pin the compiler version. No floating pragmas in production. An upgrade should not quietly change behaviour.
  2. Use `require` for validation and access checks, `assert` only for things that should be mathematically impossible, and `revert` with custom errors for cheap, clear failures.
  3. Run Slither or Mythril on every commit, failing the build on new high-severity findings.
  4. Write NatSpec on every public and external function. Parameters, returns, assumptions about who can call it. This becomes the document an auditor works from.
  5. Run the SMTChecker on functions with clear numeric or state invariants.
  6. Enforce a linter so review time goes to logic, not formatting.

None of this replaces judgement. Tools catch mechanical errors. A human finds the money bugs. The code reads fine. It just loses all your funds.

Step three: test, then fuzz, then verify

These sit on a continuum, not a ladder. Each one catches a different class of bug. The common mistake is assuming the expensive technique replaces the cheap one.

Unit tests and fuzzing are the floor. Unit tests confirm known scenarios. Fuzzers like Foundry and Echidna throw randomised inputs at your contract: edge-case integers, odd call orderings, boundary conditions. Most genuine bugs surface here, cheaply, before anyone reviews the code.

Symbolic execution explores paths, not values. Where fuzzing tries specific inputs, symbolic execution reasons about whole classes of input at once and traces every feasible path. Good for path-dependent exploits random fuzzing would miss by luck. It has real limits though: path explosion makes complex contracts intractable, and it only checks the properties you gave it.

Formal verification proves code against a spec. Solidity's built-in SMTChecker uses model checking to prove things like the absence of overflow, straight from your `require` and `assert` statements. It ships with the compiler, so there is no excuse not to run it.

But know the ceiling. It proves your code matches your spec. It does not prove your spec matches what you meant economically. A perfectly verified contract still gets drained if the spec missed an attack vector. That is why formal methods work best scoped tight.

Where each one earns its place:

  • Unit tests and fuzzing: every contract, every function, no exceptions
  • Symbolic execution: complex conditional logic and multi-step state changes
  • SMTChecker: numeric invariants, overflow properties, simple reentrancy checks
  • Full formal verification: small high-value modules like a token supply invariant, not whole protocols

Step four: audit, then keep watching

Treat the auditor as a specialist reviewer, not a safety net for bugs you could not be bothered finding.

Your prep decides the audit quality more than the auditor's name does. Freeze the commit. Hand over architecture, invariants and known limitations. Giving an auditor a moving codebase and no docs burns billable hours on questions your README should have answered.

The workflow:

  1. Code freeze and documentation handoff, with architecture, invariant list and prior test results.
  2. Automated analysis across the frozen commit for known patterns and gas waste.
  3. Manual line-by-line review, focused on economic logic and business rules the tools miss.
  4. Findings report with severity, each with a fix recommendation.
  5. Remediation and re-audit of the fixed commit.

Severity should drive your order of work. Critical and high block a mainnet deploy outright. Medium needs a documented decision if you ship anyway. Low and informational go to a backlog with an owner and a date.

Budget for a second short audit after the fixes land. A fix can add a new bug. An auditor spots that. Your own team, reading its own work, often will not.

Bug bounties extend the shelf life. Scope it precisely: which contracts, which vulnerability classes, whether testnet exploits count. Size the reward against the value at risk. A few thousand dollars for a bug that could drain a nine-figure treasury is an invitation to sell the exploit elsewhere. Publish a clear disclosure policy so a white-hat knows how to report without fearing legal trouble.

After launch, security changes shape

It moves from prevention to detection and response.

Key custody decides your worst case. One private key over upgrades, treasury and pausing is one point of catastrophic failure. Multi-signature plus a timelock forces a delay between proposing and executing, which buys your team a window to catch a compromised key.

Monitoring turns silent failures into alerts. Emit detailed events on every state-changing function. Run continuous invariant checks against live state, so a drain shows up while it is happening rather than afterwards.

Minimum coverage:

  • Real-time events on all privileged and state-changing functions
  • Automated checks comparing recorded and actual on-chain balances
  • Alerts on unusual transaction size, frequency or caller
  • A tested pause mechanism a multi-sig can fire in minutes

Write the incident plan before you need it. Who can trigger a pause. What users get told. Where funds migrate to. If an owner key looks compromised, revoking permissions and rotating to a new multi-sig has to be rehearsed, not improvised at 2am while funds leave.

The attacks that use no bug at all

Some of the worst exploits break nothing. They just use the rules as written.

Flash loan attacks borrow a huge uncollateralised sum for one transaction, move an oracle price, and repay in the same transaction. The attacker keeps the difference. Governance attacks do the same with voting power: borrow tokens, pass a malicious proposal, unwind before anyone reacts. MEV games the transaction ordering itself.

None of this shows up in a static analysis report, because none of it is a coding bug. It is a design flaw in the incentives.

To defend it you have to think about incentives, not code. Price feeds averaged over time are much harder to move in one transaction than a spot price. And if you snapshot voting power before a proposal goes public, the borrowed-vote trick stops working.

Bridges inherit trust you do not control

Cross-chain bridges have produced some of the largest losses in the industry, and the reason is structural. A bridge has to trust something outside the chain it protects: validators, a multi-sig, an oracle network.

Message verification is the core risk. If a bridge does not properly verify that a message really came from the source chain, an attacker forges a deposit and mints assets that were never locked. Replay is the sibling risk: one valid message processed twice.

If you compose with other protocols, you inherit their security whether you reviewed it or not. Put every external dependency in your own threat model. Write down what trust each one requires, and revisit that list every time a dependency upgrades.

Habits that keep causing bugs

A few show up again and again, often in code that otherwise looks fine.

  • Copy-pasting logic from a block explorer. You inherit assumptions about an ecosystem you are not in.
  • Using `tx.origin` for authentication. Use `msg.sender`. Otherwise a malicious contract can trick a user into authorising something they never meant to.
  • Relying on `block.timestamp` for anything security-critical. Validators have latitude over it.
  • Unbounded loops over arrays that can grow. Eventually the array exceeds the block gas limit and the function is dead.
  • Storing sensitive data on-chain. All contract storage is publicly readable. Solidity's `private` keyword does not hide anything.
  • Treating one audit as permanent clearance. Re-review after every meaningful change. This one is a process mistake and it is the most expensive on the list.

What we would tell a team starting now

The standard advice, get an audit before launch, is true but incomplete enough to mislead. An audit is a snapshot of a frozen commit. It cannot catch an economic flaw in a spec nobody wrote, and it cannot catch a governance key compromised six months later. Treating it as the finish line is the most consistent mistake behind the industry's biggest losses.

The underrated part is week one. Teams pour effort into scan dashboards and proof tools. Then they leave upgrades and access control as an afterthought. That afterthought is where the oracle and game-theory attacks live.

So: design and threat model before you code. Then treat audit and monitoring as ongoing operations, not a gate you pass once.

Where this connects

We build custom software and web products, and Web3 is part of that. Start with NFTs is an NFT education platform we built on Next.js and Sanity with live Web3 tools. Our blockchain development work covers the wider build side, and there is background in blockchain applications if you want the plain-English version first.

The process side carries over from other regulated builds. Software compliance explained covers evidence and how often you review. Role-based access control covers the permission model.

Across the group, AI agent security makes the same layered case for a different target. AI agent governance covers who owns it day to day. And securing against vendor dependency covers risk you inherit from things you did not build.

Three things worth bookmarking. The OWASP SCSVS gives you control groups to build a checklist from. The Smart Contract Top 10 maps your code against known attacks. The SMTChecker docs set out what the tool does and where it stops.

Building something that will hold real value?

If your contract will hold money, the security work starts at architecture, not after a draft exists.

We build custom software and platforms for founders who need this working in production. Have a look at blockchain development, AI app development, or turning a program into a platform.

Worth a chat?

Frequently asked questions

How do I check if a smart contract is legitimate?

Check the source code is published and matches the deployed bytecode, look for a public audit report, and check whether privileged functions sit behind a multi-signature or timelock rather than one owner key. The contract's age and transaction history are worth reading too.

What are the downsides of smart contracts?

Immutability cuts both ways. A bug is as permanent as a feature unless the team built a tested migration path first. Contracts also depend on oracles and other protocols, so their security is only as good as the weakest thing they compose with.

Are smart contracts legally enforceable?

That depends on the jurisdiction and the agreement. Code executing automatically does not make it a binding contract everywhere. If you are handling real value, treat this as a question for qualified legal counsel in your jurisdiction.

How much does a smart contract audit cost?

It scales with complexity, lines of code and review scope rather than a fixed rate. A simple contract costs meaningfully less than a DeFi protocol with several interacting modules. Budget for a shorter second audit after remediation as well.

What does the Solidity SMTChecker actually catch?

It proves properties like the absence of overflow and some reentrancy conditions directly from your require and assert statements. It ships with the compiler. Its guarantees are scoped to what you asked it to check, so it is not a blanket proof for multi-transaction invariants across several contracts.

About James Killick

10+ years building digital products · 200+ apps shipped since 2015

James is a co-founder of Devwiz and an AI product specialist. Since 2015 he has helped ship 200+ apps for founders, businesses and government, including work for NSW Government, Briometrix and Huskee. He builds AI-first platforms and writes about turning a proven program into software. He also hosts the Up in the AI podcast.

More articles by James · James's personal site · LinkedIn · AI Orchestrators

Tags: Security, Blockchain, Smart Contracts

Browse all Devwiz articles·See our case studies