Skip to content
Quantix Security Labs
Technical analysis

Formal verification in DeFi: what a proof buys you

Provers can establish that a property holds in every reachable state. Whether that matters depends on which properties were written, what the model assumed, and what was left out. A practitioner’s map.

Written by
Quantix Research, Formal Methods
Published
Reading time
6 minutes
Category
Formal Verification

Technical analysis. Built from the public sources in the bibliography. Incident details are as reported there; no new discovery is claimed.

Illustration: a proof tree in which properties of the code support lemmas and a theorem, with one branch ending in a counterexample.
Contents
  1. What a proof promises
  2. Four techniques, four strengths
  3. The boundary of a proof
  4. Writing properties worth proving
  5. Assumptions that decide the result
  6. Where proofs pay off in DeFi
  7. Reading a verification report
  8. Conclusions
  9. Recommendations
  10. Bibliography

Testing shows that code behaves correctly on the inputs that were tried. Formal verification aims to show that it behaves correctly on all of them. For contracts that hold other people’s money and cannot easily be changed once deployed, that difference is attractive, and formal methods are now a routine part of reviewing the most critical DeFi code.

A proof is also easy to over-read. It establishes that a specific property holds for a specific model of the code under specific assumptions. Each of those qualifiers can hide the bug that matters. This analysis maps the main techniques, what each one establishes, and the questions a reader should ask of any verification result.

What a proof promises

Every verification result has three ingredients. A specification states what must be true, such as “total shares never exceed total assets” or “only the owner can pause.” A model describes what the code can do, derived from source or bytecode together with the semantics of the virtual machine. A tool searches for a way the model can violate the specification. If it finds none, within the limits it was given, the property is proved for that model.

The guarantee is therefore conditional. It says nothing about properties that were not written down, and it is only as faithful to the deployed contract as the model is.

Four techniques, four strengths

TechniqueEstablishesExamplesTypical limits
Built-in model checkingAssertions and arithmetic safety within a contractSolidity SMTChecker (BMC and CHC engines)Scales poorly across contracts; external calls abstracted
Symbolic testingA test passes for every input in a range, from a given starting statehalmos, hevmLoops and call depth bounded; starting state is concrete
Rule-based provingRules and invariants over all reachable statesCertora ProverNeeds summaries for external calls; specification effort
Equivalence checkingTwo versions of code behave identicallyhevm equivalence modeOnly as useful as the reference version
Formal techniques used on smart contracts and what each establishes. Tools named are examples documented in the bibliography.

The techniques are complementary. Symbolic tests are cheap to adopt because they look like unit tests. Rule-based provers can reason about every reachable state rather than one starting point, at the cost of more specification work. Equivalence checking is the right tool for a narrow but important question, such as whether a gas optimisation or a recompilation changed behaviour.

The boundary of a proof

What a formal proof covers and what it assumesA specification and the code are translated into a model, which includes EVM semantics, summaries of external calls and loop bounds. A prover or SMT solver checks the model and returns proved, a counterexample, or timeout. Outside the boundary of the proof: properties nobody wrote, oracle and governance behaviour, summarised external calls, and compiler and deployment configuration.Inside the proofSpecificationrules, invariantsCodesource or bytecodeModelEVM semanticscall summariesloop and size boundsProverSMT or symbolicexecutionProvedfor the model, as specifiedCounterexampleconcrete failing traceTimeout, unknownno conclusionOutside the proofProperties nobody wrote · oracle and governance behaviour · summarised external calls · compiler, deployment and upgrade configuration
A proof covers the model and the properties written for it. Everything in the lower band is outside it unless modelled explicitly.

Three outcomes are possible. A proof holds for the model as specified. A counterexample is a concrete trace that violates the property, and is often the most valuable output because it can be replayed as a test. A timeout means the tool gave up, and it is not evidence either way. Reports that list only proved properties without the timeouts are incomplete.

Writing properties worth proving

The hardest part of formal verification is deciding what to prove. Good properties come from the protocol’s promises to its users, stated as precisely as possible. The symbolic test below checks a simple but meaningful one: a deposit followed by a redemption cannot create value.

VaultRoundTrip.t.sol (illustrative, halmos)solidity
// Illustrative symbolic test for halmos: functions prefixed check_ are explored// with symbolic arguments, covering every value in the assumed range at once.contract VaultRoundTripTest is Test {    Vault vault;     function setUp() public {        vault = new Vault(new TestToken());   // the proof is about this starting state    }     /// Depositing and immediately redeeming never returns more than was deposited.    function check_noValueCreatedOnRoundTrip(uint256 assets) public {        vm.assume(assets > 0 && assets < 1e30);        uint256 shares = vault.deposit(assets, address(this));        uint256 back   = vault.redeem(shares, address(this), address(this));        assert(back <= assets);    }}

The test explores every value of assets in the assumed range at once. It does so from one starting state, the one setUp creates, so it proves nothing about a vault that already holds deposits from others. That is a typical limit of symbolic testing and the reason rule-based provers reason about invariants that must hold in every reachable state.

rules.spec (illustrative, Certora Verification Language)solidity
// Illustrative rule in the Certora Verification Language (CVL).methods {    function balanceOf(address) external returns (uint256) envfree;} /// A withdrawal by one account never changes another account's balance.rule withdrawIsolated(address other) {    env e;    uint256 amount;    require other != e.msg.sender;     mathint before = balanceOf(other);    withdraw(e, amount);    assert balanceOf(other) == before, "withdraw touched a third party";}

The rule states an isolation property: one account’s withdrawal never changes another account’s balance. Because it is checked from an arbitrary state, it covers vaults with any history. Properties of this kind, about who can affect whom, catch access-control and accounting bugs that are hard to anticipate as individual test cases. The primer on writing invariants covers how to derive such properties from requirements.

Assumptions that decide the result

Every model simplifies. The simplifications that most often matter in DeFi are these:

  • External calls. Calls to tokens, oracles and other protocols are usually replaced by summaries: a function that returns any value, or a fixed one. A summary that is more polite than the real contract, for instance one that never re-enters, hides the bugs the real contract would expose.
  • Bounds. Loops are unrolled a fixed number of times, arrays are limited in length, and call depth is capped. Bugs that appear only beyond those bounds are not found.
  • Environment. Assumptions about msg.sender, timestamps, token decimals and initial state narrow the space explored. Each one is a statement that the property is only claimed under that condition.
  • What is deployed. Compiler version and settings, constructor arguments, proxy configuration and later upgrades are outside the proof unless verified separately.

Where proofs pay off in DeFi

Formal methods earn their cost where a property is easy to state, hard to test exhaustively, and catastrophic to violate. In DeFi that typically means solvency and accounting invariants (assets cover liabilities, the sum of balances equals total supply), access control (which functions can change which state, and who can call them), monotonicity (an index or exchange rate that must never decrease), and upgrade safety (the new implementation behaves like the old one except where intended).

Proofs pay off less where the risk is economic rather than logical. A lending market can be proved to liquidate exactly as specified and still be drained by an oracle whose price was manipulated, because the price is an input the proof treats as given. That boundary is why formal verification complements, rather than replaces, economic review and adversarial testing.

Reading a verification report

When a protocol advertises formal verification, these questions determine what it actually means:

  1. Which properties were specified, and do they cover the protocol’s promises to users?
  2. Which contracts and functions were in scope, and which external calls were summarised, and how?
  3. What bounds were used for loops, arrays and call depth?
  4. Which properties timed out or were not proved, and what was done about them?
  5. Were vacuity or sanity checks run on every rule?
  6. Does the verified code match the deployed bytecode and configuration?

The Fenwick Lending research assessment shows how properties and their assumptions are recorded alongside manual findings, and the formal verification service describes the engagement structure.

Conclusions

A formal proof is the strongest evidence available that a property holds, and it is strictly limited to the property, the model and the assumptions. The most consequential choices in a verification effort are therefore not about tools but about which properties to write, how external behaviour is summarised, and what bounds are accepted.

Used with that discipline, formal methods find bugs that no test suite would. Used as a badge, they can produce confidence the evidence does not support.

Recommendations

Protocol teams

  1. Start from a written list of promises to users, and turn each into a property before choosing a tool.
  2. Begin with symbolic tests of the most critical functions, and move to invariants over all states for accounting.
  3. Publish properties, summaries, bounds and unproved items together with the result.

Reviewers

  1. Check every external-call summary against the real contract, especially for reentrancy and fee-on-transfer behaviour.
  2. Run vacuity checks and mutate the code to confirm that each property can fail.
  3. Replay every counterexample as a concrete regression test.

Readers of reports

  1. Read the list of assumptions before the list of proved properties.
  2. Treat an unexplained timeout as an unverified property.

Bibliography

Primary sources: standards, official documentation, incident reports and papers.

  1. 1SMTChecker and formal verification(opens in a new tab)Solidity documentation
  2. 2halmos: symbolic testing for EVM smart contracts(opens in a new tab)a16z, GitHub
  3. 3hevm: symbolic execution and equivalence checking for the EVM(opens in a new tab)GitHub
  4. 4Certora Prover documentation(opens in a new tab)Certora
  5. 5Invariant testing(opens in a new tab)Foundry documentation

Applied on this site

Assessments, case studies and tools that apply this publication.

Want this question asked of your code?

Quantix reviewers apply the same reasoning to production systems. Share what you are building and when it ships.