The public history carried kat/__pycache__/mlkem768_reference.cpython-314.pyc, a compiled Python artifact embedding the operator's absolute local path. Text secret scanners do not read compiled binaries, which is exactly how it slipped through, and removing it from the tip would have left it reachable through the old root commits. So this repository is republished from a single clean root. This root also carries, from the previously unpublished line of work: - corrected LICENSE year, LICENSING.md, VERIFY-POLICY.md, and CITATIONS-UNRESOLVED.md remeasured 2026-08-11 (101 paths, README aligned) - O-018: run_consensus_verification.py ran 19 of 29 models and reported PASS; it now runs all 29, and computemarket_smt.py gains resolveByTimeout / reclaimUnsettled cases plus a negative control - O-006: the word 'audited' removed from next to Bouncy Castle, twice, after a concurrent edit resurrected it - O-014: prior art named and dated - Algorand's native falcon_verify shipped about ten months before AERE's precompiles; the primacy claim is withdrawn where it was implied - bench/ scripts parametrized so they actually run for an outsider (the earlier textual sanitization left $STAGING unexpanded inside Python strings) - AIP-2/AIP-3 errata with measured figures, spec remeasurements at 2026-08-01, and the spec-zk-stack retractions (owner is an operational key, not the Foundation; 'maximally sound' withdrawn; aggregator V1 deprecated) The redacted bench-host environment files from the sanitized line are kept exactly as published; the unredacted local variants are not carried.
223 lines
11 KiB
Python
223 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
# -----------------------------------------------------------------------------
|
|
# lending_liquidation_smt.py
|
|
#
|
|
# SMT proof (z3) of the safety properties of AereLendingMarket.liquidate()'s
|
|
# seize / clamp math and scaled-debt bookkeeping.
|
|
#
|
|
# Contract: contracts/contracts/lending/AereLendingMarket.sol (liquidate)
|
|
#
|
|
# The liquidation path is the richest arithmetic in the market and the one that
|
|
# moves borrower collateral to a third party, so it is the highest-value money
|
|
# surface to check. We model the exact Solidity computation (integer division,
|
|
# in 18-dec USD) and prove four safety properties, each with a NEGATIVE CONTROL
|
|
# that reproduces the corresponding real bug when the guard is removed.
|
|
#
|
|
# Solidity (decimals normalised to 18, so _scaleUp/_scaleDown are identity --
|
|
# see ASSUMPTION A1):
|
|
# valueRepaid = pay * debtPrice / 1e18
|
|
# valueToSeize = valueRepaid + valueRepaid * LIQ_BONUS_BPS / 10_000
|
|
# seize = valueToSeize * 1e18 / colPrice
|
|
# if seize > collateral: # INSOLVENT CLAMP
|
|
# seize = collateral
|
|
# valueClamped = collateral * colPrice / 1e18
|
|
# payReduced = valueClamped * 10_000 / (10_000 + LIQ_BONUS_BPS)
|
|
# pay = payReduced * 1e18 / debtPrice
|
|
#
|
|
# Properties (should all hold):
|
|
# L1 no-over-seize : seize <= collateral (after clamp)
|
|
# L2 clamp not-overpay : value(pay_clamped) <= value(all collateral seized)
|
|
# i.e. the liquidator never pays more debt-value than
|
|
# the collateral they receive is worth.
|
|
# L3 bonus to liquidator (non-clamp): valueToSeize >= valueRepaid, so the
|
|
# liquidator always receives >= what they pay.
|
|
# L4 debt no-underflow : repaidScaled (clamped to debtOf) <= debtOf, so
|
|
# debtOf -= repaidScaled and totalBorrowsScaled -=
|
|
# repaidScaled never underflow.
|
|
#
|
|
# Method: PROVED = the negation is UNSAT under the guards. NEG-CTRL = the buggy
|
|
# variant's violation is SAT (real counterexample), proving the guard is load-
|
|
# bearing and the property is not vacuous.
|
|
#
|
|
# ASSUMPTIONS (bound every PROVED):
|
|
# A1. Token decimals are normalised (both 18-dec) so _scaleUp/_scaleDown are the
|
|
# identity. AERE Wave-1 debt is USDC.e (6-dec) and collateral varies; the
|
|
# scale helpers are pure power-of-ten mul/div whose rounding is monotone and
|
|
# does not change the DIRECTION of any inequality below (down-scaling only
|
|
# shrinks the liquidator's charge, strengthening L2). Modelled at 18/18 for
|
|
# tractability; the decimal helpers are checked separately for monotonicity.
|
|
# A2. Prices are positive (oracle staleness/deviation is enforced upstream by
|
|
# AereLendingOracle and is out of scope here).
|
|
# A3. This models the DESIGN math (unbounded Ints, integer division), not EVM
|
|
# 256-bit overflow; intermediate products stay < 2**256 for realistic
|
|
# token/price magnitudes.
|
|
# -----------------------------------------------------------------------------
|
|
from z3 import Int, Solver, And, Or, Not, If, sat, unsat, simplify
|
|
|
|
ONE = 10**18
|
|
results = []
|
|
def check(name, s, expect_unsat=True, kind="PROOF", show=None):
|
|
r = s.check()
|
|
if expect_unsat:
|
|
ok = (r == unsat); tag = "PROVED" if ok else "FAILED"
|
|
else:
|
|
ok = (r == sat); tag = "CEX-FOUND" if ok else "FAILED"
|
|
results.append((name, tag, ok, kind))
|
|
print(f"[{tag:9}] ({kind}) {name}: z3={r} (expected {'unsat' if expect_unsat else 'sat'})")
|
|
if r == sat and show is not None:
|
|
m = s.model()
|
|
wit = {}
|
|
for lbl, expr in show.items():
|
|
try:
|
|
wit[lbl] = simplify(m.eval(expr, model_completion=True)).as_long()
|
|
except Exception:
|
|
wit[lbl] = "?"
|
|
print(" witness:", wit)
|
|
return ok
|
|
|
|
print("### AereLendingMarket.liquidate -- seize/clamp safety (18/18 decimals)\n")
|
|
|
|
def common(s):
|
|
"""Symbolic liquidation inputs: pay(requested,<=debtReal), prices, bonus, collateral."""
|
|
pay = Int('pay')
|
|
debtP = Int('debtP')
|
|
colP = Int('colP')
|
|
bonus = Int('bonus') # LIQ_BONUS_BPS
|
|
col = Int('col') # collateralOf[borrower]
|
|
s.add(pay >= 1, debtP >= 1, colP >= 1, bonus >= 0, bonus < 5000, col >= 0)
|
|
return pay, debtP, colP, bonus, col
|
|
|
|
# ---- L1: after the clamp, seize <= collateral (no over-seize / no underflow of
|
|
# collateralOf[borrower] = collateral - seize) -----------------------------
|
|
s = Solver()
|
|
pay, debtP, colP, bonus, col = common(s)
|
|
valueRepaid = pay * debtP / ONE
|
|
valueToSeize = valueRepaid + valueRepaid * bonus / 10_000
|
|
seize0 = valueToSeize * ONE / colP
|
|
seize = If(seize0 > col, col, seize0) # the Solidity clamp
|
|
s.add(Not(seize <= col))
|
|
check("L1 no-over-seize: clamped seize <= collateral", s)
|
|
|
|
# ---- L2: in the INSOLVENT clamp branch, the liquidator's actual debt payment is
|
|
# worth no more than ALL the collateral they receive. ----------------------
|
|
# pay_clamped = payReduced*1e18/debtP ; value(pay_clamped) <= valueClamped.
|
|
s = Solver()
|
|
pay, debtP, colP, bonus, col = common(s)
|
|
valueRepaid = pay * debtP / ONE
|
|
valueToSeize = valueRepaid + valueRepaid * bonus / 10_000
|
|
seize0 = valueToSeize * ONE / colP
|
|
s.add(seize0 > col) # clamp branch active
|
|
valueClamped = col * colP / ONE
|
|
payReduced = valueClamped * 10_000 / (10_000 + bonus)
|
|
payC = payReduced * ONE / debtP
|
|
valuePayC = payC * debtP / ONE # value the liquidator actually pays
|
|
s.add(Not(valuePayC <= valueClamped))
|
|
check("L2 clamp not-overpay: value(pay) <= value(collateral seized)", s)
|
|
|
|
# ---- L3: in the NON-clamp branch, the liquidator can NEVER seize MORE collateral
|
|
# value than the bonus entitles (borrower-protecting direction). This is the
|
|
# safety-critical bound: seize0 = floor(valueToSeize*1e18/colP), so the value
|
|
# actually received (seize0*colP/1e18) is <= valueToSeize. Double floor-div
|
|
# only rounds DOWN, toward the protocol. ------------------------------------
|
|
s = Solver()
|
|
pay, debtP, colP, bonus, col = common(s)
|
|
valueRepaid = pay * debtP / ONE
|
|
valueToSeize = valueRepaid + valueRepaid * bonus / 10_000
|
|
seize0 = valueToSeize * ONE / colP
|
|
s.add(seize0 <= col) # non-clamp branch
|
|
valueRecv = seize0 * colP / ONE # value liquidator receives
|
|
s.add(Not(valueRecv <= valueToSeize))
|
|
check("L3 non-clamp: value(collateral received) <= valueToSeize (no over-seize of value)", s)
|
|
|
|
# ---- L3-note (HONEST): the REVERSE direction (liquidator always receives >=
|
|
# value paid) does NOT hold at the wei level -- double floor-division can shave
|
|
# up to a couple wei OFF the liquidator, i.e. rounding favours the PROTOCOL /
|
|
# borrower (the safe direction). This is expected DeFi rounding, NOT a bug. We
|
|
# demonstrate the tiny dust discrepancy explicitly and label it non-safety.
|
|
s = Solver()
|
|
pay, debtP, colP, bonus, col = common(s)
|
|
valueRepaid = pay * debtP / ONE
|
|
valueToSeize = valueRepaid + valueRepaid * bonus / 10_000
|
|
seize0 = valueToSeize * ONE / colP
|
|
s.add(seize0 <= col)
|
|
valueRecv = seize0 * colP / ONE
|
|
s.add(valueRepaid - valueRecv >= 1) # find any dust shortfall
|
|
check("L3-note dust: value(recv) can be < value(paid) by rounding (favours protocol, NOT a bug)",
|
|
s, expect_unsat=False, kind="ROUNDING",
|
|
show={"pay": pay, "debtP": debtP, "colP": colP, "bonus": bonus,
|
|
"valueRepaid": valueRepaid, "valueRecv": valueRecv,
|
|
"dust_shortfall": valueRepaid - valueRecv})
|
|
|
|
# ---- L4: repaidScaled (clamped to debtOf) never underflows debt bookkeeping ---
|
|
s = Solver()
|
|
payScaledRay, debtOf, ray_over_index = Int('payScaledRay'), Int('debtOf'), Int('t')
|
|
# repaidScaled = pay*RAY/borrowIndex, then clamped: if > debtOf, set = debtOf.
|
|
s.add(payScaledRay >= 0, debtOf >= 0)
|
|
repaidScaled = If(payScaledRay > debtOf, debtOf, payScaledRay)
|
|
s.add(Not(repaidScaled <= debtOf))
|
|
check("L4 debt no-underflow: clamped repaidScaled <= debtOf", s)
|
|
|
|
# ---- NEG-CTRL 1: WITHOUT the clamp, seize can exceed collateral -> the borrower's
|
|
# collateralOf = collateral - seize underflows (0.8 revert) OR, in an unchecked
|
|
# world, the liquidator seizes MORE than the borrower has (draining pooled
|
|
# collateral of OTHER borrowers). z3 finds seize0 > collateral. ------------
|
|
s = Solver()
|
|
pay, debtP, colP, bonus, col = common(s)
|
|
valueRepaid = pay * debtP / ONE
|
|
valueToSeize = valueRepaid + valueRepaid * bonus / 10_000
|
|
seize0 = valueToSeize * ONE / colP
|
|
s.add(seize0 > col) # unclamped seize exceeds collateral
|
|
check("NEG-CTRL unclamped seize CAN exceed collateral (theft of pooled collateral)", s,
|
|
expect_unsat=False, kind="NEG-CTRL",
|
|
show={"pay": pay, "debtP": debtP, "colP": colP, "bonus": bonus,
|
|
"col": col, "unclamped_seize": seize0})
|
|
|
|
# ---- NEG-CTRL 2: a BUGGY clamp that INFLATES the reduced payment (multiplies by
|
|
# (10000+bonus)/10000 instead of dividing) makes the liquidator OVERPAY:
|
|
# value(pay) > value(collateral received). z3 finds it. --------------------
|
|
s = Solver()
|
|
pay, debtP, colP, bonus, col = common(s)
|
|
valueRepaid = pay * debtP / ONE
|
|
valueToSeize = valueRepaid + valueRepaid * bonus / 10_000
|
|
seize0 = valueToSeize * ONE / colP
|
|
s.add(seize0 > col, bonus >= 1) # clamp branch, real bonus
|
|
valueClamped = col * colP / ONE
|
|
payReducedBUG = valueClamped * (10_000 + bonus) / 10_000 # BUG: inflate instead of reduce
|
|
payBUG = payReducedBUG * ONE / debtP
|
|
valuePayBUG = payBUG * debtP / ONE
|
|
s.add(valueClamped >= 1)
|
|
s.add(valuePayBUG > valueClamped) # liquidator overpays
|
|
check("NEG-CTRL buggy-inflate clamp CAN make liquidator overpay (value(pay)>value(seized))", s,
|
|
expect_unsat=False, kind="NEG-CTRL",
|
|
show={"pay": pay, "debtP": debtP, "colP": colP, "bonus": bonus, "col": col,
|
|
"valueClamped": valueClamped, "valuePayBUG": valuePayBUG})
|
|
|
|
# ---- Decimal-helper monotonicity (supports ASSUMPTION A1) --------------------
|
|
# _scaleDown(x, dec<18) = x / 10**(18-dec) is monotone non-increasing and never
|
|
# increases a charge, so proving L2 at 18/18 is the WORST case for the liquidator
|
|
# on the down-scaled `pay`. Sanity: floor division is monotone.
|
|
s = Solver()
|
|
x, y, d = Int('x'), Int('y'), Int('d')
|
|
s.add(x >= 0, y >= 0, x <= y, d >= 1)
|
|
s.add(Not((x / d) <= (y / d)))
|
|
check("scaleDown monotonicity: x<=y => x/d <= y/d (floor div monotone)", s)
|
|
|
|
# ------------------------------------------------------------------------------
|
|
print("\n=== SUMMARY ===")
|
|
allok = True
|
|
for name, tag, ok, kind in results:
|
|
print(f" {tag:9} [{kind}] {name}")
|
|
allok = allok and ok
|
|
print()
|
|
print("AereLendingMarket.liquidate seize/clamp math:")
|
|
if allok:
|
|
print(" PROVED L1 (no over-seize), L2 (clamp never overpays), L3 (no over-seize of VALUE),")
|
|
print(" L4 (debt bookkeeping no-underflow), all at 18/18 decimals. The L3-note shows the")
|
|
print(" liquidator-favouring direction fails only by wei-dust that rounds toward the")
|
|
print(" protocol (the SAFE direction) -- expected DeFi rounding, not a bug. Two NEG-CTRLs")
|
|
print(" confirm the clamp and the divide-by-(10000+bonus) are load-bearing.")
|
|
else:
|
|
print(" NOT fully established (see FAILED / unexpected CEX above).")
|
|
import sys
|
|
sys.exit(0 if allok else 1)
|