Alchemix V3 Earmark Conservation

The sum of every account's earmarked debt always equals the global earmarked counter.

Alchemix is a self-repaying lending protocol. A user deposits a yield-bearing token (MYT) as collateral and mints a synthetic debt token (alAsset). The yield from that collateral flows to the Transmuter, which uses it to pay down the user's debt automatically.

Each position keeps two numbers:

  • debt: how much alAsset the user owes the protocol.
  • earmarked: how much of that debt has already been booked for repayment by incoming yield. The user's outstanding balance is debt - earmarked; as earmarked rises, the user owes less.

cumulativeEarmarked is the global sum the Transmuter reads when it pulls collateral. The conservation invariant says it always equals the per-account picture, even though the protocol never iterates accounts.

Why this matters

If the per-account and the global picture diverged, redemption would either over-debit user collateral (the protocol thinks more debt is earmarked than actually is) or short-pay the Transmuter (the global lags behind real per-account claims). Either failure breaks the synthetic peg.

The earmarking system updates the global immediately and the per-account state lazily. The conservation invariant is what makes the lazy update safe: every consumer that reads an account's current earmarked debt sees a value that, summed across all accounts, matches the global. Alchemix's lead developer flagged the lazy-lag pattern in the source at line 1014 (“Global can lag local by rounding”); the proof shows the lag is harmless under the invariant.

What this covers

This case targets the lazy-projected earmark conservation invariant for Alchemix V3 master at commit 117c95b6ee11a75221d6fbdc79f16ac6acdb96f5. The invariant is proven preserved under each of the five operations that touch the relevant state:

  • _earmark (lines 1582 to 1641, the global earmark step)
  • _sync (lines 1442 to 1472, the per-account refresh)
  • redeem (lines 655 to 731, the Transmuter-only redemption path)
  • _subEarmarkedDebt (lines 1002 to 1019, repay-driven local decrement)
  • _subDebt (lines 1300 to 1309, debt subtraction with the line-1306 cumulativeEarmarked clamp)

The projection function _computeUnrealizedAccount is also modeled (lines 1478 to 1579), in its within-epoch / within-survival-window form.

Out of scope: external calls to the Transmuter, ERC-20 token transfers, the cover-shares pre-amble of _earmark, access control, NFT ownership checks, Initializable, pause flags, and the cross-epoch path of _computeUnrealizedAccount (the snapshot-mapping reconciliation when the redemption epoch advances mid-projection).

How this was modeled / proven

Function names, storage slot names, and field names match the Solidity source verbatim, including leading underscores on private members. Every block in every modeled function carries an inline source-line annotation (-- src: AlchemistV3.sol:Lxxxx-Lyyyy) so a reviewer can read the Lean and the Solidity side by side.

For each of the five operations, the proof reads the post-state storage slots off the contract body via a dedicated slot-write lemma (the same pattern Lido VaulthubLocked uses), then chains through atomic Q128 algebra and the projection formula to land the conservation equality. The hardest steps, _earmark and redeem, both rely on a per-account survival-ratio scaling lemma derived from the slot-write rule for _redemptionWeight and _earmarkWeight.

The model deviates from the deployed contract in six places. Each is a deliberate scope choice; each weakens the proven statement compared to a guarantee about deployed bytecode. They are spelled out below rather than tucked into a footnote.

Simplifications
  • Q128 treated as exact rationals. The deployed contract uses floor-rounding mulQ128 / divQ128; the proof carries their algebraic identities (linearity, associativity, divide-and-scale commutation) as theorem hypotheses. The conservation theorem is about the design-level identity, not the per-step rounding drift it ignores. A follow-up case can sharpen this into a bounded drift theorem.
  • Cross-epoch path of _computeUnrealizedAccount not modeled. The function has a separate branch (lines 1539 to 1564) that splits the projection at a redemption-epoch boundary, reading from two snapshot mappings (_earmarkWeightAtEpoch, _redemptionWeightAtEpoch). This case study covers only the same-epoch branch. The proven conservation invariant therefore applies under the precondition that no redemption epoch transition has occurred since each touched account was last synced; it does not establish anything about the cross-epoch branch itself.
  • Cover-shares pre-amble abstracted. The first 27 lines of _earmark (transmuter balance read, pending-cover update, queryGraph call, cover application) all feed a single number: how much to earmark this window. The model reads that number from a ghost storage slot _transmuterEarmarkAmount. Conservation holds for any value.
  • External calls and access control elided. Transmuter queries, ERC-20 transfers, onlyTransmuter and friends, NFT ownership, pause flags, and Initializable all sit outside the accounting math.
  • Account fields pruned. The Solidity Account struct has 13 fields. Only 5 are read or written along paths that affect the invariant; the rest (collateralBalance, freeCollateral, mint allowances, etc.) are dropped from the model.
  • Active-id set is an explicit parameter. Verity's ContractState exposes a knownAddresses ghost set for mapping(address => T) cases but no parallel for mapping(uint256 => T). The set of active token IDs is therefore passed explicitly as a FiniteSet Uint256 parameter. We have proposed adding the parallel ghost-set to Verity to remove this workaround.
Verify it yourself
git clone https://github.com/lfglabs-dev/verity-benchmark
cd verity-benchmark
lake build Benchmark.Cases.Alchemix.EarmarkConservation.Compile

If the build succeeds, Lean has checked the stated theorems under the assumptions listed here. Source repository

Produced as part of the Verity benchmark, an open database of formally specified contract properties.

Proof status

The five core preservation theorems are fully proven from operational semantics. After a review pass, three of the hypotheses originally surfaced on those theorems were discharged: two via composition (call-site theorems) and one via a strengthened invariant (the line-1306 clamp). All in all, the file proves the user-facing theorems listed below plus their supporting lemmas, with no axioms, no sorry, no admit.

TheoremStatus
Core preservation
_sync_preserves_invariantproven
_subDebt_preserves_invariantproven
_subEarmarkedDebt_preserves_invariantproven
redeem_preserves_invariantproven
_earmark_preserves_invariantproven
Call-site composites (drop synced-at-touched-id)
_sync_then_subDebt_preserves_invariantproven
_sync_then_subDebt_preserves_invariant_v2proven
_sync_then_subEarmarkedDebt_preserves_invariantproven
Sister invariant: cumulativeEarmarked ≤ totalDebt
_sync_preserves_cumLeTotalDebtproven
_subDebt_preserves_cumLeTotalDebtproven
_subEarmarkedDebt_preserves_cumLeTotalDebtproven
redeem_preserves_cumLeTotalDebtproven
_earmark_preserves_cumLeTotalDebtproven
H6 bridge: projected debt conservation → original H6
H6_from_projectedDebt_conservationproven

View contract modelView specs in LeanView proofs in Lean

Assumptions / hypotheses

Three of the original six hypotheses were proved and removed. These are the three that remain.

  • Q128 idealizationfloor-rounded Q128 ops behave like exact rationals

    Modeling assumption (irreducible at this level)

    The contract uses floor-rounded Q128.128 math. The proof treats it like exact arithmetic, so it ignores tiny per-step rounding drift and proves the design-level conservation rule instead.
    Removing this assumption would require a different theorem: a bounded-rounding-error result for ONE_Q128.

  • Weight snapshots non-zero (model artifact)lastAccruedEarmarkWeight and lastAccruedRedemptionWeight are non-zero where required

    Model artifact, not present in deployed Solidity

    This one is a model artifact. We found concrete counterexamples showing that the relevant preservation statements are false in the current model if the weight snapshots needed by that operation are allowed to be zero.
    These are counterexamples to dropping the hypothesis, not counterexamples to the theorem as stated. The real contract advances the epoch and resets the index after a full wipe. Our model flattens that epoch-and-index structure into one weight, so zero snapshots must stay excluded until epochs are modeled directly.

  • Projected debt conservation (sister invariant)sumProjectedDebt s ids = totalDebt s

    Sister of the main invariant; preservation is a follow-up case

    The natural sister of the earmark-conservation invariant: where sumProjectedEarmarked = cumulativeEarmarked aggregates per-id projected earmarked debt, this aggregates per-id projected total debt and equates it to the global totalDebt. The bridge H6_from_projectedDebt_conservation proves the original Q128-projected bridging identity (that Σ unearmarkedRemaining × redemptionSurvivalRatio equals totalDebt − cumulativeEarmarked) follows from this sister + the main invariant + the line-1306 sister.
    It should be provable with the same proof pattern we used here, but only after modeling the debt-changing operations this case left out (_addDebt, _resetDebt, constructor setup). That is a follow-up case, so the sister itself remains a scope cut for now.

View specs in LeanView proofs in Lean

Learn more

Upstream Solidity contract: AlchemistV3.sol.

Full case directory in the Verity benchmark repository.

What is a formal proof? A short explanation for non-specialists.

More research