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.
172 lines
8.4 KiB
Python
172 lines
8.4 KiB
Python
#!/usr/bin/env python3
|
|
# -----------------------------------------------------------------------------
|
|
# settlementhub_smt.py
|
|
#
|
|
# SMT proof (z3) of the SOLVENCY / NO-THEFT invariant for AereSettlementHubV2,
|
|
# the corrected settlement venue that fixes the V1 F-SWEEP griefing bug by
|
|
# tracking a per-asset `committedLiabilities` accumulator.
|
|
#
|
|
# Contract: contracts/contracts/settlement/AereSettlementHubV2.sol
|
|
#
|
|
# We model the per-asset accounting as an inductive transition system and prove
|
|
# the safety invariant is INDUCTIVE (base case + every op preserves it). Each
|
|
# Solidity `require`/revert is a guard; Solidity 0.8 checked arithmetic is
|
|
# modelled as "underflow reverts (no state change)".
|
|
#
|
|
# Per asset T:
|
|
# bal = IERC20(T).balanceOf(address(this)) -- real token balance
|
|
# committed = committedLiabilities[T] -- sum of every OPEN intent
|
|
# remainder + every posted
|
|
# solver bond (what the hub OWES)
|
|
#
|
|
# SOLVENCY INVARIANT (task target -- residual sweep can never touch owed funds,
|
|
# and settle()/cancelIntent() can always pay out):
|
|
# INV := bal >= committed AND committed >= 0
|
|
#
|
|
# INV => residualOf(T) = bal - committed is exactly the un-owed surplus, so a
|
|
# permissionless sweepResidual can only ever move genuine surplus (fees / un-
|
|
# flushable slashed bonds), NEVER a live intent remainder or a posted bond.
|
|
#
|
|
# Method: for each op OP, check INV(pre) AND guards AND post=OP(pre) AND
|
|
# NOT INV(post) is UNSAT. UNSAT => OP cannot break the invariant.
|
|
# Unbounded Ints = the accounting/design abstraction. Checks the DESIGN math,
|
|
# NOT the EVM bytecode.
|
|
#
|
|
# ASSUMPTIONS (bound every PROVED below):
|
|
# A1. Standard ERC20: transferFrom credits exactly `amount`, transfer debits
|
|
# exactly `amount`. Fee-on-transfer / rebasing tokens are OUT OF SCOPE
|
|
# (they would break bal>=committed for ANY escrow and are excluded by the
|
|
# asset-listing policy).
|
|
# A2. SINK is the trusted immutable AereSink; it can pull at most the approved
|
|
# amount. Its `flush` either succeeds (pulls `amount`) or reverts (pulls 0).
|
|
# A3. committed == (sum of open-intent remainders) + (sum of posted bonds) is
|
|
# the intended reading; the per-op guards below (amt<=committed portions)
|
|
# are exactly the Solidity checked-sub guards, which hold BECAUSE the item
|
|
# being removed is itself a summand of committed.
|
|
# -----------------------------------------------------------------------------
|
|
from z3 import Int, Solver, And, Or, Not, If, sat, unsat
|
|
|
|
FEE_BPS = 50 # PROTOCOL_FEE_BPS (immutable)
|
|
BPS_DEN = 10_000
|
|
|
|
def INV(bal, committed):
|
|
return And(bal >= committed, committed >= 0)
|
|
|
|
results = []
|
|
def check(name, s, expect_unsat=True, kind="PROOF"):
|
|
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:
|
|
m = s.model()
|
|
print(" witness:", {str(d): m[d].as_long() for d in m.decls()})
|
|
return ok
|
|
|
|
print("### AereSettlementHubV2 -- SOLVENCY INV: balanceOf(hub) >= committedLiabilities\n")
|
|
|
|
# ---- BASE CASE: fresh hub -----------------------------------------------------
|
|
s = Solver(); s.add(Not(INV(0, 0)))
|
|
check("base case (empty hub) satisfies INV", s)
|
|
|
|
# ---- deposit(amount): pull full amount, route fee (may stay if sink rejects),
|
|
# commit amountAfterFee. --------------------------------------------------
|
|
s = Solver()
|
|
bal, com, amount, feeStays = Int('bal'), Int('com'), Int('amount'), Int('feeStays')
|
|
fee = (amount * FEE_BPS) / BPS_DEN
|
|
aaf = amount - fee # amountAfterFee
|
|
# bal2 = bal + amount - (fee actually flushed). feeStays in {0, fee}: if the
|
|
# fee left to sink feeStays==0 (bal drops by fee); if sink rejected feeStays==fee.
|
|
s.add(INV(bal, com), amount >= 1, Or(feeStays == 0, feeStays == fee))
|
|
bal2 = bal + amount - fee + feeStays
|
|
com2 = com + aaf
|
|
s.add(Not(INV(bal2, com2)))
|
|
check("deposit() preserves INV (commit amountAfterFee)", s)
|
|
|
|
# ---- depositSolverBond(amount): pull amount, commit amount --------------------
|
|
s = Solver()
|
|
bal, com, amount = Int('bal'), Int('com'), Int('amount')
|
|
s.add(INV(bal, com), amount >= 1)
|
|
s.add(Not(INV(bal + amount, com + amount)))
|
|
check("depositSolverBond() preserves INV", s)
|
|
|
|
# ---- slashSolverBond(amount): committed-=amount; flush amount (may stay) ------
|
|
# Guard: bond>=amount and bond is a summand of committed => committed>=amount.
|
|
# sinkOk in {0(reject),1(accept)}: accept => bal-=amount; reject => bal unchanged.
|
|
s = Solver()
|
|
bal, com, bond, amount, sinkOk = Int('bal'), Int('com'), Int('bond'), Int('amount'), Int('sinkOk')
|
|
s.add(INV(bal, com), amount >= 1, bond >= amount, bond <= com, # bond is part of committed
|
|
Or(sinkOk == 0, sinkOk == 1))
|
|
bal2 = If(sinkOk == 1, bal - amount, bal)
|
|
com2 = com - amount
|
|
s.add(Not(INV(bal2, com2)))
|
|
check("slashSolverBond() preserves INV (sink accept OR reject)", s)
|
|
|
|
# ---- settle(intent): pay parked remainder amt to solver; uncommit it ---------
|
|
# Guard: amt is an open intent's remainder, a summand of committed => amt<=committed.
|
|
s = Solver()
|
|
bal, com, amt = Int('bal'), Int('com'), Int('amt')
|
|
s.add(INV(bal, com), amt >= 1, amt <= com)
|
|
s.add(Not(INV(bal - amt, com - amt)))
|
|
check("settle() preserves INV (pay solver, uncommit)", s)
|
|
|
|
# ---- cancelIntent(intent): refund parked remainder to depositor; uncommit ----
|
|
s = Solver()
|
|
bal, com, amt = Int('bal'), Int('com'), Int('amt')
|
|
s.add(INV(bal, com), amt >= 1, amt <= com)
|
|
s.add(Not(INV(bal - amt, com - amt)))
|
|
check("cancelIntent() preserves INV (refund depositor, uncommit)", s)
|
|
|
|
# ---- sweepResidual(amount): amount <= residualOf = max(bal-committed,0) -------
|
|
s = Solver()
|
|
bal, com, amount = Int('bal'), Int('com'), Int('amount')
|
|
residual = If(bal > com, bal - com, 0)
|
|
s.add(INV(bal, com), amount >= 1, amount <= residual) # else ExceedsResidual revert
|
|
s.add(Not(INV(bal - amount, com))) # committed unchanged
|
|
check("sweepResidual() V2 (bounded by residual) preserves INV", s)
|
|
|
|
# ---- NEG-CTRL 1: V1 permissionless sweep took an arbitrary amount<=bal, ignoring
|
|
# committed. This is the F-SWEEP griefing bug: an EOA sweeps a live intent's
|
|
# parked funds + posted bonds into the sink, bricking settle(). z3 finds it.
|
|
s = Solver()
|
|
bal, com, amount = Int('bal'), Int('com'), Int('amount')
|
|
s.add(INV(bal, com), com >= 1, amount >= 1, amount <= bal) # buggy bound: only bal
|
|
s.add(Not(INV(bal - amount, com))) # bal2 can drop below committed
|
|
check("PRE-V2 sweep (bound=balance, ignores committed) CAN steal owed funds", s,
|
|
expect_unsat=False, kind="NEG-CTRL")
|
|
|
|
# ---- NEG-CTRL 2: a BUGGY deposit that commits the FULL amount (not amountAfterFee)
|
|
# while the fee is flushed to the sink. Then committed grows by `amount` but
|
|
# balance only by amountAfterFee -> hub is over-committed, cannot pay. Shows
|
|
# the accounting MUST use amountAfterFee. z3 finds the under-backing.
|
|
s = Solver()
|
|
bal, com, amount = Int('bal'), Int('com'), Int('amount')
|
|
fee = (amount * FEE_BPS) / BPS_DEN
|
|
s.add(INV(bal, com), amount >= 1, fee >= 1) # fee>0 needs amount>=200
|
|
bal2 = bal + amount - fee # fee flushed
|
|
com2 = com + amount # BUG: full amount committed
|
|
s.add(Not(INV(bal2, com2)))
|
|
check("BUGGY deposit (commit full amount, flush fee) CAN over-commit (under-back)", s,
|
|
expect_unsat=False, kind="NEG-CTRL")
|
|
|
|
# ------------------------------------------------------------------------------
|
|
print("\n=== SUMMARY ===")
|
|
allok = True
|
|
for name, tag, ok, kind in results:
|
|
print(f" {tag:9} [{kind}] {name}")
|
|
allok = allok and ok
|
|
print()
|
|
print("SOLVENCY (INV: balanceOf(hub) >= committedLiabilities) for AereSettlementHubV2:")
|
|
if allok:
|
|
print(" PROVED inductive -- base case + deposit / depositSolverBond / slashSolverBond /")
|
|
print(" settle / cancelIntent / sweepResidual all preserve it. Two NEG-CTRLs confirm the")
|
|
print(" checks are load-bearing: the V1 unbounded sweep AND a full-amount-commit deposit")
|
|
print(" each reproduce a real under-backing counterexample.")
|
|
else:
|
|
print(" NOT fully established (see FAILED / unexpected CEX above).")
|
|
import sys
|
|
sys.exit(0 if allok else 1)
|