We model the core logic of Morpho Market and prove that its safety-critical properties hold across every protocol action. Every theorem is machine-checked by Lean.
Our Lean model sits between paper proofs (exact arithmetic, no rounding) and the Solidity implementation (access control, oracle integration, interest rate model, callbacks, and so on). We capture the math with fixed-point arithmetic — accounting for every rounding error — and abstract price changes and interest accruals as environment actions, separated from user operations.
The market revolves around three pools — a supply
pool that funds borrows, a borrow pool that
tracks each borrower's debt, and a collateral
pool that backs each position. Seven user operations move assets
in and out of these pools — including liquidate,
which any external user (the liquidator) can call against an
unhealthy borrower. Interest accrual is the one environment
action: it inflates the borrow and supply pools by the same
amount, with no user trigger.
Note on modeling. In real lending implementations,
accrueInterest is not a
standalone call — it is folded into every user operation,
running at the start before the user's intent is processed. We
model it as a distinct environment action so the
proofs can talk about user effects and interest effects
independently — for example, "this property holds across every
user action when no interest accrues in between".
The two model views are equivalent: any concrete user op in
practice is just an envAccrueInt immediately
followed by the corresponding user… action in our
model.
Every borrower position lives in one of five regions, organized by
how a position can leave them. Regions are defined using
ltv := debt / (collateral · price) and the
two protocol constants lltv (the LLTV
threshold) and incentive (the liquidation
incentive factor). A Healable position can be
fully liquidated back to Healthy. Once a position
enters the Bad-debt path, a full liquidation is no
longer possible — the liquidation incentive cannot be covered by
the remaining collateral. Only repeated partial liquidations work,
and they only drain collateral until the position is
Insolvent. From there, only writeOff
can clear the debt — at which point the loss is realized and
socialized among lenders.
Not shown: repay reduces a borrower's debt
and supplyCollateral adds collateral, so either can
also move a position toward a lower-debt region — including
directly to Cleared if the entire debt is
repaid. Both are omitted above to keep the focus on the
liquidation story.
Each card states the property in plain English first; toggle to see the Lean statement, and click through to the full proof on GitHub.
After any user operation, every borrower who had the LLTV buffer before still has the LLTV buffer after. Borrows and collateral withdrawals are explicitly gated on the new state being healthy at the observed price.
theorem step_preserves_allHealthy {a : Action} {w w' : World} (h_pm : NoPriceMove a) (h_acc : NoAccrual a) (hbk : Bookkeep w) (hAH : AllHealthy w) (hstep : step a w = some w') : AllHealthy w'
Starting from an AllHealthy market, every user operation and every environment action leaves the protocol fully solvent — total debt covered by collateral at the observed price. Environment actions are gated by two closed-form budgets:
PriceMoveBudget caps how far the oracle price
can drop in a single tick. The new price
p' must satisfy
lltv · p ≤ p' — a tick can erase
at most a factor of 1 − lltv of
the price, exactly the room a marginal AllHealthy borrower
has before crossing into insolvency.
AccrualBudget caps how much interest can
accrue at once. The accrued amount
Δ must satisfy
Δ · lltv ≤ totalBorrowAssets · (1 − lltv)
— one accrual cannot consume more than the aggregate LTV
headroom across all borrowers.
theorem step_remains_assetCovered_under_allHealthy {a : Action} {w w' : World} (h_pmb : PriceMoveBudget a (w.oracle.read)) (hbk : Bookkeep w) (hAH : AllHealthy w) (h_acb : AccrualBudget a w) (hstep : step a w = some w') : AssetCovered w'
We partition unhealthy positions into a healable region (liquidation profitable enough to fully cover the debt) and a bad-debt region (debt-times-incentive already exceeds collateral value). The bad-debt region is absorbing: no partial liquidation moves a position out of it.
theorem liquidate_preserves_bad_debt_path {w w' : World} {lq b : Addr} {sh seized : ℕ} (hp : (0 : OraclePrice) < w.oracle.read) (hbk : Bookkeep w) (hBDP : BadDebtPath w b) (hl : liquidate w lq b sh = some (w', seized)) : BadDebtPath w' b
If a borrower is still in the Healable region — their bonus-inflated debt is still covered by their collateral at the observed price — then a single full-burn liquidation can return the position to Healthy. This is the constructive partner to the absorbing bad-debt theorem above, completing the dichotomy: every unhealthy position is either healable (one liquidation away from Healthy) or absorbing (no liquidation can rescue it). The existence holds under one closed-form budget:
HealableLiquidationBudget requires the
borrower's coverage gap — collateral value minus
bonus-inflated debt — to be wide enough to absorb two
rounding wei. Letting C be the
borrower's collateral, D their
debt, p the oracle price, and
α the liquidation incentive
factor, the budget reads
α + p ≤ C · p − α · D — the
right-hand side is the coverage gap, the left-hand side is
one repay-rounding unit plus one collateral-rounding unit
valued at the current price.
theorem exists_full_liquidation_to_healthy {w : World} {lq b : Addr} (hp : (0 : OraclePrice) < w.oracle.read) (hUnhealthy : ¬ Healthy w b) (hBudget : HealableLiquidationBudget w b) (hLiquidatorFunds : LiquidatorHasFullRepayFunds w lq b) (hAccounting : MarketAccounting w) : ∃ sh w' seized, liquidate w lq b sh = some (w', seized) ∧ Healthy w' b
Starting from an AssetCovered (solvent) market, every user
operation leaves the protocol solvent — total debt still
covered by collateral at the observed price. The two
share-burn actions — userRepay and
userLiquidate — are each gated by a closed-form
budget:
LiquidateStepBudget fires for
userLiquidate: the targeted borrower must
satisfy the HealableLiquidationBudget
explained in the previous headline (coverage gap wide
enough to absorb two rounding wei). Outside this region,
partial liquidation mechanically drains collateral instead
of restoring solvency.
BurnStepBudget fires for both
userRepay and userLiquidate:
the asset cost of the share burn must stay inside the real
borrowed assets, not dip into the virtual borrow-asset
reserve. Letting R denote
totalBorrowAssets and
sh the number of debt shares
being burned, the budget reads
repayCost(sh) ≤ R — the burn's
asset cost stays within the real pool. Crossing this
threshold lifts the share-rate for every other borrower,
which can knock them into insolvency.
theorem step_preserves_assetCovered {a : Action} {w w' : World} (hp : 0 < w.oracle.read_q) (h_pm : NoPriceMove a) (h_acc : NoAccrual a) (hbk : Bookkeep w) (hAC : AssetCovered w) (h_liq : LiquidateStepBudget a w) (h_burn : BurnStepBudget a w) (hstep : step a w = some w') : AssetCovered w'
No user operation and no environment action can reduce a
supplier's position value. Suppliers are guaranteed never to
lose from protocol behavior — the single exception is
envWriteOff, when bad debt is realized and the
loss is socialized across all suppliers.
theorem step_preserves_supplySharePriceLE {a : Action} {w w' : World} (h_nbd : NoBadDebt a) (hbk : Bookkeep w) (hstep : step a w = some w') : SupplySharePriceLE w w'