Formal Verification with Z3
Ora integrates with the Z3 SMT solver to provide formal verification capabilities, allowing you to mathematically prove properties about your smart contracts.
Overview
Formal verification in Ora helps you:
- Prove correctness: Mathematically verify that your contract behaves as intended
- Catch bugs early: Find edge cases that testing might miss
- Increase confidence: Deploy with mathematical guarantees about your code
- Document intent: Specifications serve as precise documentation
Verification Annotations
Preconditions (requires)
Specify what must be true before a function executes:
pub fn transfer(recipient: address, amount: u256) -> bool
requires amount > 0
requires recipient != address(0)
{
// Implementation
}
Postconditions (ensures)
Specify what must be true after a function executes:
pub fn transfer(sender: address, recipient: address, amount: u256) -> bool
requires amount > 0
ensures balances[sender] == old(balances[sender]) - amount
ensures balances[recipient] == old(balances[recipient]) + amount
{
// Implementation
}
Contract Invariants
Specify properties that must always hold true for the contract:
contract Token {
storage totalSupply: u256;
storage balances: map[address, u256];
// Contract-level invariant
invariant totalSupplyPreserved(
sumOfBalances() == totalSupply
);
// Functions...
}
Loop Invariants
Specify properties that remain true throughout loop execution:
pub fn sumBalances(accounts: slice[address]) -> u256 {
let total: u256 = 0;
for (accounts) |account|
invariant(total >= 0)
{
total = total + balances[account];
}
return total;
}
Quantifiers
Express properties over collections:
// Universal quantification
requires forall addr: address where addr != address(0) => balances[addr] >= 0
// Existential quantification
ensures exists addr: address => balances[addr] > 1000
Assertions
Runtime checks during verification:
pub fn complexOperation() {
let x = calculate();
assert(x > 0, "x must be positive");
// Continue with confidence that x > 0
}
Ghost Code
Specification-only code that doesn't generate bytecode:
contract Token {
// Ghost storage - for verification only
ghost storage sumOfAllBalances: u256;
// Ghost function - for specification only
ghost fn computeSum() -> u256 {
// Complex calculation used in proofs
return sumOfAllBalances;
}
pub fn transfer(recipient: address, amount: u256)
ensures computeSum() == old(computeSum()) // Uses ghost function
{
// Implementation
}
}
Complete Example: Verified ERC20
contract VerifiedToken {
storage totalSupply: u256;
storage balances: map[address, u256];
// Contract invariant: total supply is preserved
invariant supplyPreserved(
sumOfBalances() == totalSupply
);
ghost fn sumOfBalances() -> u256 {
// Specification-only function for verification
return totalSupply;
}
pub fn init(initialSupply: u256)
requires initialSupply > 0
ensures totalSupply == initialSupply
{
totalSupply = initialSupply;
balances[std.msg.sender()] = initialSupply;
}
pub fn transfer(recipient: address, amount: u256) -> bool
requires amount > 0
requires recipient != address(0)
requires balances[std.msg.sender()] >= amount
ensures balances[std.msg.sender()] == old(balances[std.msg.sender()]) - amount
ensures balances[recipient] == old(balances[recipient]) + amount
ensures totalSupply == old(totalSupply)
{
let sender = std.msg.sender();
let senderBalance = balances[sender];
if (senderBalance < amount) {
return false;
}
balances[sender] = senderBalance - amount;
let recipientBalance = balances[recipient];
balances[recipient] = recipientBalance + amount;
return true;
}
}
How It Works
Compilation Pipeline
- Parse: Ora parses verification annotations alongside regular code
- Generate VCs: Compiler generates verification conditions (VCs) from specifications
- Z3 Solving: VCs are sent to Z3 SMT solver for verification
- Report: Results show which properties are proven or violated
- Code Generation: Ghost code is filtered out during bytecode generation
What Gets Verified
- ✅ Preconditions are satisfied at all call sites
- ✅ Postconditions hold after function execution
- ✅ Invariants are maintained across all operations
- ✅ No integer overflows/underflows
- ✅ No null pointer dereferences
- ✅ Array bounds are respected
What Doesn't Affect Bytecode
Ghost code is specification-only and never appears in deployed bytecode:
- Ghost storage variables
- Ghost functions
requires/ensuresclausesinvariantdeclarationsassertstatements withghostmodifier
Current Status
🚧 Z3 integration is under active development
What's working:
- ✅ Full grammar support for verification annotations
- ✅ AST representation for all FV features
- ✅ Parser implementation complete
- ✅ Ghost code filtering (no verification code in bytecode)
- ✅ Z3 build system integration
In progress:
- 🔄 Verification condition generation
- 🔄 Z3 constraint encoding
- 🔄 Counterexample reporting
Coming soon:
- 📋 Full verification pipeline
- 📋 Interactive verification feedback
- 📋 Incremental verification
Installation
Z3 is automatically detected if installed on your system:
# macOS (Homebrew)
brew install z3
# Ubuntu/Debian
sudo apt-get install z3
# Arch Linux
sudo pacman -S z3
If Z3 is not found, Ora falls back to compilation without verification.
Usage
Verification runs automatically when you compile contracts with formal verification annotations:
# Compile with verification
ora contract.ora
# The compiler will:
# 1. Parse verification annotations
# 2. Generate and solve verification conditions
# 3. Report any property violations
# 4. Generate bytecode (without ghost code)
Learn More
Best Practices
- Start simple: Add
requiresandensuresto key functions first - Be specific: Precise specifications catch more bugs
- Use ghost code: For complex mathematical properties
- Verify incrementally: Don't try to verify everything at once
- Test + Verify: Formal verification complements testing, doesn't replace it
Why Formal Verification?
Traditional smart contract development relies on testing, which can only verify specific cases. Formal verification proves properties for all possible inputs, providing mathematical certainty that your contract behaves correctly.
This is especially critical for:
- 💰 DeFi protocols: Financial logic must be correct
- 🔐 Access control: Security properties must hold
- 🎯 Governance: Voting mechanisms must be fair
- 🏦 Custody: Asset transfers must preserve invariants