Claude-starter aptos-move-prover
Expert on Move Prover formal verification - specification language (MSL), preconditions, postconditions, invariants, aborts_if, ensures, requires, modifies, emits, global invariants, schema patterns, quantifiers, helper functions, pragma directives, verification strategies, and debugging proofs. Triggers on keywords move prover, formal verification, spec, invariant, ensures, requires, aborts_if, precondition, postcondition, quantifier, schema, pragma
git clone https://github.com/raintree-technology/claude-starter
T=$(mktemp -d) && git clone --depth=1 https://github.com/raintree-technology/claude-starter "$T" && mkdir -p ~/.claude/skills && cp -r "$T/templates/.claude/skills/aptos/move-prover" ~/.claude/skills/raintree-technology-claude-starter-aptos-move-prover && rm -rf "$T"
templates/.claude/skills/aptos/move-prover/skill.mdMove Prover Expert
Purpose
Provide deep expertise on the Move Prover - a formal verification tool that mathematically proves correctness properties of Move smart contracts. The Prover uses the Move Specification Language (MSL) to express properties and verifies them using SMT solvers (Z3, CVC5).
When to Use
Auto-invoke when users mention:
- Move Prover - formal verification, prove, verification
- Specifications - spec blocks, spec module, spec fun
- Properties - invariants, preconditions, postconditions
- MSL Keywords - ensures, requires, aborts_if, modifies, emits
- Advanced - quantifiers, schema, global invariants, pragma
- Verification - soundness, completeness, verification errors
- Debugging - proof failures, counterexamples, timeout
Move Prover Overview
What is the Move Prover?
The Move Prover is a formal verification tool that:
- Mathematically proves contract properties hold for ALL possible inputs
- Uses SMT (Satisfiability Modulo Theories) solvers
- Finds bugs that testing might miss
- Provides mathematical guarantees of correctness
- Generates counterexamples for failed proofs
Why Use the Move Prover?
Testing vs Verification:
// Testing: Checks specific inputs #[test] fun test_transfer() { transfer(alice, bob, 100); // Only tests this one case } // Verification: Proves for ALL inputs spec transfer { ensures sender_balance == old(sender_balance) - amount; ensures recipient_balance == old(recipient_balance) + amount; // Proven for ALL possible values of amount, sender, recipient! }
Benefits:
- ✅ Catches edge cases testing misses
- ✅ Proves absence of integer overflow/underflow
- ✅ Guarantees invariants always hold
- ✅ Verifies access control is sound
- ✅ Documents intended behavior formally
Specification Language (MSL) Basics
Spec Blocks
module 0x1::counter { struct Counter has key { value: u64 } public fun increment(addr: address) acquires Counter { let counter = borrow_global_mut<Counter>(addr); counter.value = counter.value + 1; } spec increment { // Specification for increment function requires exists<Counter>(addr); ensures global<Counter>(addr).value == old(global<Counter>(addr).value) + 1; } }
Spec Module
spec module { // Module-level specifications pragma verify = true; // Enable verification for this module pragma aborts_if_is_strict; // Require complete abort specifications }
Core Specification Constructs
1. Preconditions - requires
requiresConditions that must be true when function is called:
public fun withdraw(account: &signer, amount: u64) acquires Balance { let addr = signer::address_of(account); let balance = borrow_global_mut<Balance>(addr); balance.coins = balance.coins - amount; } spec withdraw { requires exists<Balance>(signer::address_of(account)); requires global<Balance>(signer::address_of(account)).coins >= amount; // Function only called when these conditions are true }
Multiple requires:
spec withdraw { requires exists<Balance>(addr); requires global<Balance>(addr).coins >= amount; requires amount > 0; requires amount <= MAX_WITHDRAW; }
2. Postconditions - ensures
ensuresConditions that must be true after function executes:
public fun transfer(from: address, to: address, amount: u64) acquires Balance { let from_balance = borrow_global_mut<Balance>(from); from_balance.coins = from_balance.coins - amount; let to_balance = borrow_global_mut<Balance>(to); to_balance.coins = to_balance.coins + amount; } spec transfer { // Postconditions ensures global<Balance>(from).coins == old(global<Balance>(from).coins) - amount; ensures global<Balance>(to).coins == old(global<Balance>(to).coins) + amount; // Conservation of funds ensures global<Balance>(from).coins + global<Balance>(to).coins == old(global<Balance>(from).coins + global<Balance>(to).coins); }
3. Abort Conditions - aborts_if
aborts_ifSpecify when function should abort:
const ERROR_INSUFFICIENT_BALANCE: u64 = 1; const ERROR_NOT_INITIALIZED: u64 = 2; public fun withdraw(addr: address, amount: u64) acquires Balance { assert!(exists<Balance>(addr), ERROR_NOT_INITIALIZED); let balance = borrow_global_mut<Balance>(addr); assert!(balance.coins >= amount, ERROR_INSUFFICIENT_BALANCE); balance.coins = balance.coins - amount; } spec withdraw { aborts_if !exists<Balance>(addr) with ERROR_NOT_INITIALIZED; aborts_if global<Balance>(addr).coins < amount with ERROR_INSUFFICIENT_BALANCE; // These are the ONLY ways this function can abort }
Conditional aborts:
spec withdraw { // Only aborts if these conditions are met aborts_if !exists<Balance>(addr); aborts_if exists<Balance>(addr) && global<Balance>(addr).coins < amount; }
4. Abort Specification Completeness
spec module { pragma aborts_if_is_strict; // Now MUST specify all abort conditions } public fun transfer(from: address, to: address, amount: u64) acquires Balance { assert!(exists<Balance>(from), 1); assert!(exists<Balance>(to), 2); assert!(global<Balance>(from).coins >= amount, 3); // ... transfer logic } spec transfer { // Must list ALL abort conditions aborts_if !exists<Balance>(from); aborts_if !exists<Balance>(to); aborts_if global<Balance>(from).coins < amount; // Missing any condition = verification error }
5. Modified Resources - modifies
modifiesSpecify which global resources are modified:
spec withdraw { modifies global<Balance>(addr); // Only Balance at addr is modified, nothing else } spec transfer { modifies global<Balance>(from); modifies global<Balance>(to); // Only these two resources are modified }
6. Event Emission - emits
emitsSpecify events that should be emitted:
#[event] struct TransferEvent has drop, store { from: address, to: address, amount: u64, } public fun transfer(from: address, to: address, amount: u64) { // ... transfer logic event::emit(TransferEvent { from, to, amount }); } spec transfer { emits TransferEvent { from: from, to: to, amount: amount } to @my_module; }
Advanced Features
The old()
Operator
old()Access values from before function execution:
spec increment { ensures global<Counter>(addr).value == old(global<Counter>(addr).value) + 1; // ^^^ value before function ran } spec swap { ensures a == old(b); ensures b == old(a); }
Global State Access
spec withdraw { // Access global state let balance = global<Balance>(addr); let initial_coins = old(global<Balance>(addr).coins); ensures balance.coins == initial_coins - amount; }
Let Bindings in Specs
spec transfer { let from_balance = global<Balance>(from); let to_balance = global<Balance>(to); let from_initial = old(from_balance.coins); let to_initial = old(to_balance.coins); ensures from_balance.coins == from_initial - amount; ensures to_balance.coins == to_initial + amount; }
Conditional Ensures
spec withdraw { ensures result == true ==> global<Balance>(addr).coins == old(global<Balance>(addr).coins) - amount; ensures result == false ==> global<Balance>(addr).coins == old(global<Balance>(addr).coins); }
Quantifiers
Universal Quantification - forall
forallProve property holds for ALL values:
spec module { // For all addresses, if Balance exists, coins >= 0 invariant forall addr: address: exists<Balance>(addr) ==> global<Balance>(addr).coins >= 0; } spec transfer { // Conservation: total supply unchanged ensures forall addr: address where addr != from && addr != to: global<Balance>(addr).coins == old(global<Balance>(addr).coins); }
Existential Quantification - exists
existsProve property holds for AT LEAST ONE value:
spec module { // There exists at least one admin invariant exists addr: address: exists<AdminCap>(addr); }
Range Quantifiers
spec module { // For all indices in vector, value is positive invariant forall i in 0..len(balances): balances[i] > 0; } spec sum_vector { ensures result == sum(0..len(v), |i| v[i]); }
Global Invariants
Module Invariants
Properties that hold for entire module:
spec module { // Global supply never exceeds max invariant [global] forall addr: address: exists<Supply>(addr) ==> global<Supply>(addr).total <= MAX_SUPPLY; // Conservation of total supply invariant [global] sum_of_balances() == global<TotalSupply>(@admin).value; }
Struct Invariants
Properties that always hold for a struct:
struct Balance has key { coins: u64, locked: u64, } spec Balance { // Available coins >= locked coins invariant coins >= locked; // Locked amount is multiple of lock unit invariant locked % LOCK_UNIT == 0; }
Update Invariants
Properties about state changes:
spec module { // Balance can only increase or decrease, never become negative invariant update [global] forall addr: address: old(exists<Balance>(addr)) ==> exists<Balance>(addr) && global<Balance>(addr).coins <= old(global<Balance>(addr).coins) + MAX_DEPOSIT; }
Schema - Reusable Specifications
Defining Schema
spec schema BalanceExists { addr: address; requires exists<Balance>(addr); } spec schema SufficientBalance { addr: address; amount: u64; requires global<Balance>(addr).coins >= amount; } spec schema BalanceNotChanged { addr: address; ensures global<Balance>(addr).coins == old(global<Balance>(addr).coins); }
Using Schema
spec withdraw { include BalanceExists; include SufficientBalance; } spec deposit { include BalanceExists { addr: recipient }; } // Apply to multiple functions spec withdraw, transfer, burn { include BalanceExists; }
Schema with Variables
spec schema TransferEnsures { from: address; to: address; amount: u64; ensures global<Balance>(from).coins == old(global<Balance>(from).coins) - amount; ensures global<Balance>(to).coins == old(global<Balance>(to).coins) + amount; } spec transfer { include TransferEnsures; }
Schema Composition
spec schema ValidTransfer { from: address; to: address; amount: u64; include BalanceExists { addr: from }; include BalanceExists { addr: to }; include SufficientBalance { addr: from }; } spec transfer { include ValidTransfer; include TransferEnsures; }
Helper Functions
Spec Functions
Define helper functions for specifications:
spec module { // Sum of all balances fun sum_of_balances(): u64 { sum(all_addresses(), |addr| { if (exists<Balance>(addr)) { global<Balance>(addr).coins } else { 0 } }) } // Check if address is admin fun is_admin(addr: address): bool { exists<AdminCap>(addr) } // Get total supply fun total_supply(): u64 { global<Supply>(@admin).total } } spec transfer { // Conservation using helper function ensures sum_of_balances() == old(sum_of_balances()); }
Spec-Only Functions
Functions only used in specifications:
spec module { fun balance_of(addr: address): u64 { if (exists<Balance>(addr)) { global<Balance>(addr).coins } else { 0 } } } spec transfer { ensures balance_of(from) == old(balance_of(from)) - amount; ensures balance_of(to) == old(balance_of(to)) + amount; }
Pragma Directives
Verification Control
spec module { // Enable/disable verification for module pragma verify = true; // Timeout for each function (seconds) pragma timeout = 60; // Random seed for solver pragma random_seed = 123; // Require strict abort specs pragma aborts_if_is_strict; // Require partial abort specs (default) pragma aborts_if_is_partial; }
Function-Specific Pragmas
spec complex_function { pragma verify = false; // Skip verification of this function } spec timeout_function { pragma timeout = 120; // Increase timeout to 2 minutes } spec opaque_function { pragma opaque; // Don't inline this function in proofs }
Solver Configuration
spec module { // Use specific SMT solver pragma solver = "z3"; // or "cvc5" // Increase memory limit pragma memory_limit = 16384; // MB // Verification condition generation pragma verify_duration_estimate = 300; }
Advanced Patterns
Pattern 1: Access Control Verification
struct AdminCap has key {} public fun admin_only_function(admin: &signer) acquires AdminCap { let admin_addr = signer::address_of(admin); assert!(exists<AdminCap>(admin_addr), ERROR_NOT_ADMIN); // ... admin operations } spec admin_only_function { // Precondition: caller must have AdminCap requires exists<AdminCap>(signer::address_of(admin)); // Abort condition aborts_if !exists<AdminCap>(signer::address_of(admin)); } spec module { // Global invariant: only one admin exists invariant [global] forall addr1: address, addr2: address: exists<AdminCap>(addr1) && exists<AdminCap>(addr2) ==> addr1 == addr2; }
Pattern 2: Supply Conservation
spec module { // Helper: sum of all balances fun total_balance(): u64 { sum(all_addresses(), |addr| { if (exists<Balance>(addr)) { global<Balance>(addr).coins } else { 0 } }) } // Invariant: total balance equals total supply invariant [global] total_balance() == global<TotalSupply>(@deployer).value; } spec mint { // Minting increases total supply ensures global<TotalSupply>(@deployer).value == old(global<TotalSupply>(@deployer).value) + amount; } spec burn { // Burning decreases total supply ensures global<TotalSupply>(@deployer).value == old(global<TotalSupply>(@deployer).value) - amount; } spec transfer { // Transfer doesn't change total supply ensures global<TotalSupply>(@deployer).value == old(global<TotalSupply>(@deployer).value); }
Pattern 3: Integer Overflow Prevention
spec add_balance { // Precondition: addition won't overflow requires global<Balance>(addr).coins + amount <= MAX_U64; // Postcondition: new balance is sum ensures global<Balance>(addr).coins == old(global<Balance>(addr).coins) + amount; // Won't abort due to overflow (arithmetic is checked in Move) aborts_if false; // No custom aborts }
Pattern 4: Reentrancy Safety (N/A in Move)
// Move has no reentrancy issues, but can verify atomicity spec withdraw_and_call { // State changes are atomic ensures global<Balance>(addr).coins == old(global<Balance>(addr).coins) - amount; // No intermediate states observable modifies global<Balance>(addr); }
Pattern 5: Temporal Properties
spec module { // Once frozen, always frozen invariant update [global] forall addr: address: old(exists<Frozen>(addr)) ==> exists<Frozen>(addr); // Can only freeze, never unfreeze invariant update [global] forall addr: address: !old(exists<Frozen>(addr)) || exists<Frozen>(addr); }
Complex Specification Examples
Example 1: Escrow Contract
struct Escrow has key { amount: u64, sender: address, recipient: address, deadline: u64, released: bool, } spec module { // Escrow invariants invariant [global] forall addr: address: exists<Escrow>(addr) ==> (global<Escrow>(addr).amount > 0 && global<Escrow>(addr).deadline > 0); } public fun create_escrow( sender: &signer, recipient: address, amount: u64, deadline: u64 ) { // ... } spec create_escrow { requires amount > 0; requires deadline > timestamp::now_seconds(); requires !exists<Escrow>(signer::address_of(sender)); ensures exists<Escrow>(signer::address_of(sender)); ensures global<Escrow>(signer::address_of(sender)).amount == amount; ensures global<Escrow>(signer::address_of(sender)).recipient == recipient; ensures global<Escrow>(signer::address_of(sender)).deadline == deadline; ensures global<Escrow>(signer::address_of(sender)).released == false; } public fun release(escrow_addr: address) acquires Escrow { let escrow = borrow_global_mut<Escrow>(escrow_addr); assert!(timestamp::now_seconds() >= escrow.deadline, ERROR_NOT_EXPIRED); assert!(!escrow.released, ERROR_ALREADY_RELEASED); escrow.released = true; // Transfer amount to recipient } spec release { let escrow = global<Escrow>(escrow_addr); requires exists<Escrow>(escrow_addr); requires timestamp::now_seconds() >= escrow.deadline; requires !escrow.released; ensures global<Escrow>(escrow_addr).released == true; // All other fields unchanged ensures global<Escrow>(escrow_addr).amount == old(escrow.amount); ensures global<Escrow>(escrow_addr).sender == old(escrow.sender); ensures global<Escrow>(escrow_addr).recipient == old(escrow.recipient); ensures global<Escrow>(escrow_addr).deadline == old(escrow.deadline); }
Example 2: Multi-Sig Wallet
struct MultiSig has key { owners: vector<address>, threshold: u64, proposal_count: u64, } struct Proposal has key { approvals: u64, executed: bool, } spec module { // Multi-sig invariants invariant [global] forall addr: address: exists<MultiSig>(addr) ==> (global<MultiSig>(addr).threshold > 0 && global<MultiSig>(addr).threshold <= vector::length(&global<MultiSig>(addr).owners)); } public fun approve_proposal( owner: &signer, wallet_addr: address, proposal_id: u64 ) acquires MultiSig, Proposal { // ... } spec approve_proposal { let multisig = global<MultiSig>(wallet_addr); let owner_addr = signer::address_of(owner); // Preconditions requires exists<MultiSig>(wallet_addr); requires exists<Proposal>(proposal_id); requires vector::contains(&multisig.owners, &owner_addr); requires !global<Proposal>(proposal_id).executed; // Postcondition: approvals increased ensures global<Proposal>(proposal_id).approvals == old(global<Proposal>(proposal_id).approvals) + 1; // Proposal executed when threshold reached ensures global<Proposal>(proposal_id).approvals >= multisig.threshold ==> global<Proposal>(proposal_id).executed; }
Example 3: Staking with Rewards
struct Stake has key { amount: u64, start_time: u64, reward_rate: u64, } spec module { // Helper: calculate expected rewards fun calculate_rewards(stake: Stake, current_time: u64): u64 { let duration = current_time - stake.start_time; (stake.amount * stake.reward_rate * duration) / PRECISION } } public fun claim_rewards(staker: &signer) acquires Stake { let addr = signer::address_of(staker); let stake = borrow_global<Stake>(addr); let current_time = timestamp::now_seconds(); let rewards = calculate_rewards(stake, current_time); // Transfer rewards } spec claim_rewards { let addr = signer::address_of(staker); let stake = global<Stake>(addr); let current_time = timestamp::now_seconds(); requires exists<Stake>(addr); requires current_time >= stake.start_time; // Rewards calculated correctly ensures result == calculate_rewards(stake, current_time); // Stake amount unchanged ensures global<Stake>(addr).amount == old(stake.amount); }
Debugging Verification Failures
Reading Error Messages
error: post-condition does not hold ┌── example.move:15:9 ─── │ 15 │ ensures global<Balance>(addr).coins == old(global<Balance>(addr).coins) + amount; │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ │ = Model: addr = 0x1 amount = MAX_U64 old(global<Balance>(addr).coins) = 1 Result: overflow in addition
Solution: Add precondition to prevent overflow
spec deposit { requires global<Balance>(addr).coins + amount <= MAX_U64; ensures global<Balance>(addr).coins == old(global<Balance>(addr).coins) + amount; }
Counterexamples
spec transfer { ensures global<Balance>(from).coins >= 0; } // Prover output: // Counterexample: // from = 0x1 // to = 0x2 // amount = 100 // old(global<Balance>(from).coins) = 50 // Result: assertion failure (underflow)
Solution: Add precondition
spec transfer { requires global<Balance>(from).coins >= amount; ensures global<Balance>(from).coins >= 0; }
Common Issues
1. Missing preconditions
// ❌ Fails: no precondition about existence spec withdraw { ensures global<Balance>(addr).coins == old(global<Balance>(addr).coins) - amount; } // ✅ Fixed spec withdraw { requires exists<Balance>(addr); ensures global<Balance>(addr).coins == old(global<Balance>(addr).coins) - amount; }
2. Incomplete abort specifications
spec module { pragma aborts_if_is_strict; } // ❌ Fails: missing abort condition spec transfer { aborts_if !exists<Balance>(from); // Missing: aborts_if !exists<Balance>(to); // Missing: aborts_if global<Balance>(from).coins < amount; } // ✅ Fixed spec transfer { aborts_if !exists<Balance>(from); aborts_if !exists<Balance>(to); aborts_if global<Balance>(from).coins < amount; }
3. Timeouts
// Complex verification may timeout spec complex_function { pragma timeout = 300; // Increase to 5 minutes } // Or simplify specification spec complex_function { pragma verify = false; // Skip for now }
Running the Move Prover
Basic Commands
# Verify all modules in package aptos move prove # Verify specific module aptos move prove --filter MyModule # Verify with verbose output aptos move prove --verbose # Skip timeout warnings aptos move prove --skip-timeout # Generate coverage report aptos move prove --coverage
Configuration File (Move.toml)
[prover] # Enable/disable prover enabled = true # Timeout per function (seconds) timeout = 60 # SMT solver solver = "z3" # Memory limit (MB) memory_limit = 8192 # Random seed random_seed = 42 # Verbosity level (0-4) verbosity_level = 1
CI/CD Integration
# .github/workflows/verify.yml name: Move Prover on: [push, pull_request] jobs: verify: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Install Aptos CLI run: | curl -fsSL "https://aptos.dev/scripts/install_cli.py" | python3 - name: Install Move Prover run: | aptos move setup-prover - name: Run Move Prover run: | aptos move prove --verbose
Best Practices
✅ Do
- Start simple - Add basic specs first, then refine
- Use schema - Reuse common specifications
- Write helper functions - Keep specs readable
- Specify aborts completely - Use
pragma aborts_if_is_strict - Verify invariants - Especially for critical properties
- Document with specs - Specs are executable documentation
- Run in CI - Catch regressions early
- Profile verification - Identify slow functions
❌ Avoid
- Over-specifying - Don't prove trivial properties
- Ignoring timeouts - Investigate and fix, don't skip
- Skipping aborts - Incomplete specs miss bugs
- Complex quantifiers - May cause timeouts
- Verifying everything - Focus on critical functions
- Assuming soundness - Prover can have false positives
Specification Checklist
For each critical function:
- Preconditions (
) - What must be true before?requires - Postconditions (
) - What must be true after?ensures - Abort conditions (
) - When should it fail?aborts_if - Modified resources (
) - What state changes?modifies - Events (
) - What events are emitted?emits - Invariants - What always holds?
- Conservation laws - Are resources conserved?
- Access control - Is authorization correct?
- Integer overflow - Are bounds checked?
- Temporal properties - Do state transitions make sense?
Response Style
- Formal but practical - Explain verification benefits clearly
- Example-driven - Show specs with corresponding code
- Error-aware - Help debug verification failures
- Pattern-focused - Teach reusable specification patterns
- Tool-savvy - Reference prover commands and pragmas
Follow-up Suggestions
After helping with Move Prover, suggest:
- Start with simple function specs
- Add module-level invariants
- Create schema for common patterns
- Set up CI/CD verification
- Profile and optimize slow verifications
- Document critical properties formally
- Review Aptos framework specs as examples