verified

moon add moonbit-community/verified@0.0.2
Download zip
Version
0.0.2
License
Apache-2.0
Last updated
4 months ago
Downloads
850
README

#MoonBit Formal Verification with moon prove

This project demonstrates MoonBit's experimental moon prove command, which enables formal verification of MoonBit programs by lowering specifications to Why3 and discharging proof obligations with the Z3 SMT solver.

#How it works

The pipeline has three stages:

.mbt + .mbtp moonc prove Why3 + Z3 ┌────────────┐ ┌──────────────┐ ┌──────────────┐ │ Source code │ ───► │ Generate │ ───► │ Prove goals │ │ + predicates│ │ WhyML (.mlw) │ │ via SMT │ └────────────┘ └──────────────┘ └──────────────┘

#1. User writes specifications

Each package contains two source files:

  • .mbt — MoonBit code with contracts and loop invariants
  • .mbtp — Predicate definitions used in contracts

Predicates (.mbtp) define logical properties using first-order logic:

predicate in_bounds(xs : FixedArray[Int], i : Int) {
(0 <= i) && (i < xs.length())
}

predicate sorted(xs : FixedArray[Int]) {
i : Int, j : Int,
((in_bounds(xs, i)) && (in_bounds(xs, j)) && (i <= j))
xs[i] <= xs[j]
}

Contracts (.mbt) annotate functions with preconditions, postconditions, and loop invariants:

pub fn lower_bound(xs : FixedArray[Int], key : Int) -> Int {
proof_require: sorted(xs),
proof_ensure: result => lower_bound_ok(xs, key, result),
} {
for lo = 0, hi = xs.length(); lo < hi; {
let mid = lo + (hi - lo) / 2
if xs[mid] < key {
continue mid + 1, hi
} else {
continue lo, mid
}
} {
lo
} {
proof_invariant: 0 <= lo,
proof_invariant: lo <= hi,
proof_invariant: hi <= xs.length(),
proof_invariant: all_less_before(xs, key, lo),
proof_invariant: all_geq_from(xs, key, hi),
}
}

  • proof_require: pred(...) — precondition (assumed true on entry)
  • proof_ensure: result => pred(..., result) — postcondition (must hold on exit; result refers to the return value)
  • where { proof_invariant: expr, ... } — loop invariants (must hold at every iteration boundary)

#2. Lowering to WhyML

moonc prove compiles each package's .mbt + .mbtp into a WhyML module (.mlw) under _build/verif/<pkg>/. The key transformations are:

MoonBitWhyML
FixedArray[Int]array int
xs.length()xs.length
xs[i]xs[i]
&& / \|\|/\ / \/
∀ x : Int,forall x : int.
->
for loopwhile loop with ref variables
loop invariantsinvariant { ... } clauses
proof_require / proof_ensurerequires { ... } / ensures { ... }

The compiler also auto-generates a termination variant for each loop (e.g. variant { hi - lo } for a loop with condition lo < hi), and links all predicates from the .mbtp file with Why3's with keyword for mutual recursion.

Integers are mathematical (unbounded) in Why3, so there are no overflow concerns. The WhyML modules import int.Int, int.ComputerDivision, ref.Ref, and array.Array from the Why3 standard library.

#3. Proving with Why3 + Z3

Why3 breaks the WhyML specification into individual proof obligations (goals). Each goal is a logical formula that Z3 must discharge. Typical goals:

  • Loop invariant initialization — invariant holds before the first iteration
  • Loop invariant preservation — if invariant holds before an iteration and the loop condition is true, it holds after the iteration body
  • Postcondition — the ensures clause holds when the function returns
  • Termination — the variant decreases and stays non-negative
  • Array bounds — array accesses are within bounds

The proving strategy (MoonBit_Auto in _build/verif/why3.conf) runs Z3 in multiple passes with increasing timeouts:

Z3 @ 0.2s, 1000 MB → quick wins Z3 @ 1s, 1000 MB → moderate goals compute_specified → unfold definitions split_vc → break conjunction goals apart Z3 @ 2s, 4000 MB → harder goals

Results are written to _build/verif/<pkg>/<pkg>.proof.json with per-goal status (valid, timeout, unknown, etc.).

#Running

moon prove

This verifies all packages in the workspace. Output shows per-package results and a summary of total goals proved.

#Examples

The project contains a compact set of example packages at increasing difficulty:

#No loops (branch-only proofs)

PackageWhat it proves
abs\|x\| >= 0 and equals x or -x
maxfnmax(a,b) >= a, >= b, and equals one of them
clamplo <= clamp(x, lo, hi) <= hi given lo <= hi

#Simple loops

PackageWhat it proves
findLinear search: result is -1 or a valid index with matching key
countCount non-negatives: 0 <= result <= length
gaussSum 1..n: result * 2 == n * (n + 1) (Gauss formula)

#Binary search variants

PackageWhat it proves
binary_searchOption-returning binary search with strong window invariants
invpredBinary search using a named invariant predicate
isqrtInteger square root via binary search: r*r <= n < (r+1)*(r+1)
divInteger division via binary search: q*b <= a < (q+1)*b

#Quantified invariants

PackageWhat it proves
maxarrIndex of max element: ∀ k, xs[k] <= xs[result]
lowerboundLower bound binary search: ∀ i < result, xs[i] < key and ∀ i >= result, xs[i] >= key — combines quantified invariants with sorted precondition
sumboundsArray sum bounded by element range: lo*n <= sum <= hi*n — nonlinear arithmetic with quantified precondition
checksortedIf result == 1, all adjacent pairs in order — connects a boolean flag to a quantified property
arreqTwo-array equality: if result == 1, ∀ k, xs[k] == ys[k]

#Investor-Grade Crypto / Finance Showcase

For more market-facing examples, see INVESTOR_SHOWCASE.md. The new packages are:

  • stablecoin_engine — verified mint/repay/withdraw/liquidate flows with exact bad-debt resolution
  • cpmm_swap — fee-adjusted constant-product AMM swap math with reserve-safety and invariant proofs
  • ltv_lending — solvency-preserving lending operations with exact risk-buffer accounting
  • batch_auction — verified uniform-price batch-auction frontier search
  • risk_limits — earliest-breach risk monitor for bounded exposure envelopes
  • vesting_stream — bounded token vesting and release logic
  • threshold_multisig — exact threshold execution via counted approvals

#Known limitations

See TODO.md for detailed writeups. Summary:

  1. Sequential continue assignmentcontinue i + 1, i sets the second variable to the updated i. Use a let binding to capture the old value.

  2. Contracts work best with named predicates — complex inline boolean expressions are brittle. Define a named predicate in .mbtp and reference it from proof_require / proof_ensure.

  3. must be at the top level of a predicate body — cannot be nested inside &&. Move conditions into the implication consequent instead.

  4. Loop variant requires loopvar < expr condition — conditions like r >= b or lo + 1 < hi produce no termination variant. Use lo < hi - 1 (recognized as loopvar < expr) or restructure the algorithm.