Verification
bivium-protocol-lean4 holds Lean 4 models of the series ledger, machine-checked proofs about them, and a harness that replays the same operation streams through the model and through the real Solidity and compares every field.
Those are three different artifacts making three different claims, and the value is in how they chain — and in the gap they leave, which the repository states rather than papers over.
Three artifacts, three claims
| Establishes | Cannot establish | |
|---|---|---|
| Theorems over the models | the model has the property | anything about the Solidity |
The refinement layer (CoreAdmission → Global) |
every successful step and trace of the admission-guarded model projects onto the ledger model and inherits its accounting and A/B solvency results | still model-to-model — in the repo's own words, "mathematical refinement proofs between two Lean models, not proofs that Solidity implements them" |
| The conformance harness | the pinned Solidity and the model produce identical observable accounting for the same bounded operation streams | anything outside the generated domain, which is finite |
Chained, they give: the model has property P (proved) plus the Solidity agrees with the model wherever we looked (tested) ⟹ evidence that the Solidity has P on that domain. Not proof.
The gap is exactly the domain nobody generated, and it is the harness's own Domain and limitations section — not an unstated assumption. That is the honest shape of this kind of work:
the proofs make the model trustworthy, the harness makes the model relevant, and neither on its
own says the contract is correct.
The models
Standalone Lean 4 (pinned to 4.14.0 by lean-toolchain), namespace BiviumProtocol, standard
library only — no Mathlib, and no dependency on the older bivium-lean4 or any legacy credit/debt
model.
Organised by subject rather than by Solidity file: Series (dynamic shares, holder checkpoints,
pre-expiry A traces, fixed-epoch B bounds), Settlement (frozen-A redemption and its rounding),
Session and SessionLifecycle (the flash-accounting session, receipt and count invariants,
committed physical solvency, rollback), FeeLedger, GlobalLedger (multi-holder B potential across
all lifecycle transitions) and CoreAdmission, each with a companion module of executable
regressions.
CoreAdmission is the one to understand structurally: it checks exactly two admission conditions —
the mint cutoff (the MIN_A_PER_SHARE price floor, computed at Core's WAD scale, not the
accumulator's) and lazy creation — and then delegates all accounting arithmetic to Global.step.
The broader ledger model is left unchanged, and the layer's theorems are about the projection.
What the proofs claim, and what they do not
These are proofs about explicit mathematical models, not Solidity/EVM equivalence and not a security certificate.
That sentence is the repository's own, repeated here rather than softened. Applying a theorem to the
Solidity requires the call to succeed, divisors to be positive, every stored value and result to be
in uint256 range, and no underflow — the models use unbounded Nat, and Nat subtraction does not
model Solidity's reverting subtraction, so transition guards and theorem hypotheses have to supply
those bounds.
Explicitly outside the arithmetic proofs: fees in the aggregate trace, token balance mismatches, reentrancy, controllers, external callbacks, approvals, transaction rollback, gas, and timestamp validation. Exact-transfer ERC-20 behaviour is an environmental assumption. The nested model is one-level parent-long custody accounting, not recursive look-through solvency.
The README carries a theorem-to-source map: every theorem, the Solidity operation it corresponds to, and the scope and assumptions attached to it. Read a theorem's row before citing it.
The negative results are load-bearing
Two proofs establish that something is not true, and they are more useful than most of the positive ones because they pre-empt a reasonable but wrong assumption:
individual_order_counterexample— pool = 1, shares = 2: the first one-share redemption gets 0 and the second gets 1. Aggregate order-independence does not imply individual fairness or MEV resistance.positive_B_dust— at the realACC_SCALEof 10^36, three one-share holders and one unit of B input: all three receive zero and one B remains in the series. There is no zero-dust claim.
Neither is a bug. Each is a property of integer division that a reader would otherwise discover the hard way.
The conformance harness
conformance/ replays generated operation streams through both sides and compares them field by
field. Three suites, each with its own model, corpus, runner and results file:
| Suite | Covers |
|---|---|
ledger (run.py) |
mint, exercise, burnPair, transferFrom, collect, redeem on one plain ERC-20 series, zero fees |
session (session_run.py) |
four (caller, token) debt slots, exact and short settlement, a token that reverts, a genuinely insufficient balance, callbacks that continue or abort, duplicate and partial settlement, nested unlock |
fees (fee_run.py) |
prepaid fees, early refunds, claims before and after expiry, captured rates, underfunded unlock rollback |
Why the comparison is worth anything
A differential test is only as good as its inability to agree with itself, and the design is built around that:
- The corpus generates inputs only. It calculates no expected accounting. Neither side is handed an answer, so the suite cannot be satisfied by two implementations agreeing with a hand-written expectation.
- Python compares and never reimplements ledger arithmetic. It enforces the output schema and checks every field, so there is no third implementation to accidentally agree with.
- The Solidity side is the real Core — actual
unlock, actual debt, actual ERC-20 settlement. The observer adds only read-only access to a few internals and overrides no state-changing method; counters come from actual token balance movements, not from model formulas. - Mutation controls corrupt the accumulator, checkpoint, status, creation and cashflow fields to confirm each is rejected, and unit tests catch both traces being truncated or both omitting required fields. The comparison is itself tested.
- Guard witnesses assert exact Core custom-error bytes without providing expected state to either oracle, and every rejected attempt additionally requires unchanged state and zero flows.
- No expected-divergence exemption. Earlier divergence probes were converted into ordinary all-fields-equal cases rather than kept as known exceptions.
- Python is rejected under
-O, because optimized execution could strip the validation gates. The report is written only after every check passes, and a stale report is removed before a run.
What is compared is not a summary: status and action identity, locked A, nominal longs, frozen and
live supply, the B accumulator, redeemed shares, the lazy created flag, every holder's short
balance, checkpoint, stored claimable B and previewCollect, the cumulative counters, and the
Core's actual ERC-20 balances with per-attempt inflows and outflows.
Its domain, stated up front
One plain ERC-20-backed series at a time, standard exact-transfer tokens, a permissionless
controller, known holders, centralised long custody, successful session settlement, exercise from
zero, mint and exercise ending together. Values deliberately include fractional strike ratios,
rounding dust, and mulDiv intermediate products exceeding 256 bits while results stay in range.
Not validated: nested collateral, multiple series sharing assets, permission policies, arbitrary
individual long ownership, recipients' individual balances, callback and reentrancy attacks,
incomplete settlement, all window combinations, uint256 overflow bounds, gas, or the option and
lending integration. Passing finite traces establishes correspondence for the tested domain.
The axiom gate
python3 scripts/check_axioms.py inspects every named theorem against AxiomAudit.lean and rejects
incomplete output, sorryAx, or any axiom beyond Lean's standard propext, Classical.choice and
Quot.sound. A proof cannot quietly become an assumption.
Adding a model file means registering it in check_axioms.py and extending the root imports and
audit entries, so a new file cannot skip the gate. CI runs lake build, the Python unit tests and
the gate on every push and pull request.
The source pins, and their drift
The repository pins the exact revisions it modelled, and for the Core that pin is mechanically
enforced: conformance/solidity/vendor/protocol/ holds byte-identical Core.sol and ICore.sol
from the pinned commit, with a source.json recording SHA-256 and Git blob hashes that the runner
checks before replaying anything. Vendoring those two files is also what lets CI run without a
cross-private-repository token. As the harness puts it, a matching hash is a version check, not
proof that future Core changes correspond.
As of 2026-10-03:
| Pin | Revision | Relation to that repo's main |
|---|---|---|
bivium-protocol Core.sol |
23022ad1 |
An ancestor of main; main is 23 commits ahead |
bivium-option HedgeValidation.sol, HedgeSettler.sol |
025adfe7 |
Not an ancestor of main, and neither file exists on any main in the stack |
The contrast is instructive. The Core pin has a harness that will refuse to run if the vendored
source stops matching, so it can only fall behind — visibly. The hedge pin has no such harness, and
it is the one that drifted: the hedge responsibility now sits in bivium-lending's HedgeDesk, so
those proofs describe a design that was reorganised after they were written. Whether the pinned
branch is still in flight or the model should be re-pointed is a question for whoever owns that
work, not something to infer from the pin.
The general rule holds both ways: a pin does not imply coverage of later revisions, and a theorem about a function that has since moved is evidence about the old location until it is re-extracted. See Integration for where this sits among the stack's other drift surfaces.
Vendored from bivium-protocol-doc 57ed1bdc6cfe, synced 2026-10-27. Where this and a repository disagree about that repository, the repository wins.