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.
206 lines
11 KiB
Python
206 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
# -----------------------------------------------------------------------------
|
|
# migrator_smt.py
|
|
#
|
|
# SMT proof (z3) of ONLY-DESTINATION (no third-party leakage, no custody) +
|
|
# ATOMICITY (all-or-nothing, no partial move, no stranded native) for
|
|
# AereAccountMigrator, the no-custody classical-EOA -> post-quantum-account sweep.
|
|
#
|
|
# Contract: contracts/contracts/pqc/AereAccountMigrator.sol
|
|
#
|
|
# migrate() moves a set of ERC-20 balances (and optionally native AERE) from the
|
|
# caller straight to a caller-specified `destination`, in one hop, holding no
|
|
# custody. The safety guarantees are:
|
|
# ONLY-DESTINATION : every asset moved lands at `destination` and nowhere else;
|
|
# the migrator is never the `to`, so it never skims or holds.
|
|
# ATOMICITY : a single failing transfer reverts the WHOLE call (SafeERC20
|
|
# reverts), so a migration is all-or-nothing and can never
|
|
# leave a holder with a partially drained old account.
|
|
# NO STRANDED VALUE: moveNative=false with msg.value!=0 reverts (StrayNative), so
|
|
# native can never be stuck in the migrator (there is no
|
|
# withdraw/sweep function).
|
|
#
|
|
# We model the conservation / partial-move / native-strand cores in decidable
|
|
# integer + boolean arithmetic and prove each property by asserting its NEGATION
|
|
# and showing z3 returns UNSAT. Each NEG-CTRL removes exactly one guard and shows
|
|
# the corresponding leak/partial/strand becomes satisfiable (SAT). Amounts are
|
|
# Ints; addresses are Int identities. This checks the DESIGN-level logic, NOT the
|
|
# EVM bytecode.
|
|
#
|
|
# Guards enforced by _migrate() (all hold or it reverts, no state change):
|
|
# H1 destination != 0 (ZeroDestination)
|
|
# H2 tokens.length == amounts.length (LengthMismatch)
|
|
# H3 each token != 0, each amount != 0 (ZeroToken / ZeroAmount)
|
|
# H4 moveNative => msg.value != 0 (ZeroNative)
|
|
# H5 !moveNative => msg.value == 0 (StrayNative) <-- no-strand
|
|
# H6 every ERC-20 leg: safeTransferFrom(caller, destination, amount) (to==destination)
|
|
# H7 native leg: call{value: msg.value}(destination) (to==destination), reverts on !ok
|
|
# -----------------------------------------------------------------------------
|
|
from z3 import Int, Bool, Solver, And, Or, Not, If, Sum, sat, unsat
|
|
|
|
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] for d in m.decls()})
|
|
return ok
|
|
|
|
print("### AereAccountMigrator -- ONLY-DESTINATION (no leak / no custody) + ATOMICITY\n")
|
|
|
|
N = 3 # model a migration of N ERC-20 legs (the argument is quantified structurally)
|
|
|
|
# ---- P1 ONLY-DESTINATION: every leg's transfer target is `destination`, which is
|
|
# non-zero (H1). Negation: some leg lands at an address != destination (a third
|
|
# party). In the contract the `to` is literally `destination` for every leg, so
|
|
# model to_i as a free var and let H6 bind it; UNSAT under the binding. -------
|
|
s = Solver()
|
|
destination = Int('destination')
|
|
to = [Int(f'to_{i}') for i in range(N)]
|
|
s.add(destination != 0) # H1
|
|
for i in range(N):
|
|
s.add(to[i] == destination) # H6: safeTransferFrom(..., destination, ...)
|
|
thirdParty = Int('thirdParty')
|
|
s.add(thirdParty != destination)
|
|
# NEGATION: some leg lands at the third party.
|
|
s.add(Or([to[i] == thirdParty for i in range(N)]))
|
|
check("P1 every ERC-20 leg delivers ONLY to `destination` (no third-party target)", s)
|
|
|
|
# ---- P2 NO CUSTODY / CONSERVATION: for each token the migrator is never `to`, so
|
|
# amount pulled from caller == amount delivered to destination, and the
|
|
# migrator's balance delta is exactly 0. Negation: migrator retains > 0. -----
|
|
s = Solver()
|
|
amount = [Int(f'amount_{i}') for i in range(N)]
|
|
pulledFromCaller = [Int(f'pull_{i}') for i in range(N)]
|
|
deliveredToDest = [Int(f'deliv_{i}') for i in range(N)]
|
|
for i in range(N):
|
|
s.add(amount[i] >= 1) # H3 (each amount != 0)
|
|
s.add(pulledFromCaller[i] == amount[i]) # transferFrom moves `amount`
|
|
s.add(deliveredToDest[i] == amount[i]) # ...straight to destination
|
|
migratorDelta = Sum([pulledFromCaller[i] - deliveredToDest[i] for i in range(N)])
|
|
s.add(migratorDelta != 0) # NEGATION: migrator holds custody
|
|
check("P2 no custody: sum(pulled) == sum(delivered), migrator ERC-20 delta == 0", s)
|
|
|
|
# ---- P3 ATOMICITY (all-or-nothing): a single failing leg reverts the WHOLE call,
|
|
# so either every leg's move is committed or none is. Model whole-tx revert:
|
|
# overallOk = AND(leg_ok_i); committed_i = amount_i iff overallOk else 0.
|
|
# Negation: a PARTIAL move -- some token committed while another (with a real
|
|
# amount) is not. Under whole-tx semantics, UNSAT. ---------------------------
|
|
s = Solver()
|
|
leg_ok = [Bool(f'leg_ok_{i}') for i in range(N)]
|
|
amount = [Int(f'amount_{i}') for i in range(N)]
|
|
overallOk = And([leg_ok[i] for i in range(N)]) # SafeERC20 reverts whole tx on any fail
|
|
committed = [If(And(overallOk, amount[i] >= 1), amount[i], 0) for i in range(N)]
|
|
for i in range(N):
|
|
s.add(amount[i] >= 1)
|
|
# NEGATION: partial move -- some leg moved (>0) while some other leg did NOT (==0).
|
|
s.add(Or([committed[i] > 0 for i in range(N)]))
|
|
s.add(Or([committed[j] == 0 for j in range(N)]))
|
|
check("P3 atomicity: no partial move -- all legs commit or none (whole-tx revert)", s)
|
|
|
|
# ---- P4 NO STRANDED NATIVE: with moveNative=false, H5 forces msg.value==0, so the
|
|
# migrator's native balance delta is 0; with moveNative=true the full msg.value
|
|
# is forwarded to destination (delta 0). Either way migrator native delta == 0.
|
|
# Negation: the migrator retains native. UNSAT under H4/H5/H7. --------------
|
|
s = Solver()
|
|
moveNative = Bool('moveNative')
|
|
msgValue = Int('msgValue')
|
|
s.add(msgValue >= 0)
|
|
# H4/H5: moveNative => value!=0 ; !moveNative => value==0.
|
|
s.add(If(moveNative, msgValue >= 1, msgValue == 0))
|
|
# H7: when moveNative, forward FULL msgValue to destination (retained = 0);
|
|
# when !moveNative, msgValue is 0 so retained = 0 as well.
|
|
migratorNativeRetained = If(moveNative, msgValue - msgValue, msgValue)
|
|
s.add(migratorNativeRetained != 0) # NEGATION
|
|
check("P4 no stranded native: migrator native delta == 0 (StrayNative + full forward)", s)
|
|
|
|
# ---- P5 DERIVED-DESTINATION binding (migrateToPqcAccount): destination is the
|
|
# factory's predictAddress(falconPubKey, salt); if expectedDestination != 0 the
|
|
# derived address MUST equal it. Negation: a caller who pinned an expected
|
|
# address gets a DIFFERENT destination. UNSAT under the AddressMismatch guard.
|
|
s = Solver()
|
|
derived, expected = Int('derived'), Int('expected')
|
|
s.add(expected != 0) # caller pinned an address
|
|
s.add(Or(expected == 0, expected == derived)) # AddressMismatch guard
|
|
s.add(expected != derived) # NEGATION
|
|
check("P5 migrateToPqcAccount honors a pinned expectedDestination (AddressMismatch)", s)
|
|
|
|
# ============================ NEGATIVE CONTROLS ==============================
|
|
|
|
# ---- NEG-CTRL 1 (skim to a third party): a BUGGY conduit that diverts a fee f>0 of
|
|
# one leg to a feeCollector (destination gets amount-f). Then delivered != pulled
|
|
# and the third party receives funds. z3 finds the leak. -------------------
|
|
s = Solver()
|
|
amount0, fee = Int('amount0'), Int('fee')
|
|
destGets, feeCollectorGets = Int('destGets'), Int('feeCollectorGets')
|
|
s.add(amount0 >= 2, fee >= 1, fee < amount0)
|
|
s.add(destGets == amount0 - fee) # BUG: skim a fee...
|
|
s.add(feeCollectorGets == fee) # ...to a third party
|
|
s.add(destGets != amount0) # destination is short-changed
|
|
check("BUGGY skim (fee to a third party) CAN leak funds off `destination`", s,
|
|
expect_unsat=False, kind="NEG-CTRL")
|
|
|
|
# ---- NEG-CTRL 2 (per-leg try/catch instead of whole-tx revert): a BUGGY migrator
|
|
# that CONTINUES past a failed leg commits a PARTIAL move (leg 0 moves, leg 1
|
|
# fails and is skipped). z3 finds the partial state. -----------------------
|
|
s = Solver()
|
|
leg_ok = [Bool(f'leg_ok_{i}') for i in range(N)]
|
|
amount = [Int(f'amount_{i}') for i in range(N)]
|
|
committed = [If(And(leg_ok[i], amount[i] >= 1), amount[i], 0) for i in range(N)] # BUG: per-leg commit
|
|
for i in range(N):
|
|
s.add(amount[i] >= 1)
|
|
s.add(leg_ok[0] == True, leg_ok[1] == False) # leg 1 fails but tx does NOT revert
|
|
s.add(committed[0] > 0, committed[1] == 0) # partial move realized
|
|
check("BUGGY per-leg-catch migrator CAN leave a PARTIAL move (old account half-drained)", s,
|
|
expect_unsat=False, kind="NEG-CTRL")
|
|
|
|
# ---- NEG-CTRL 3 (no StrayNative guard): a BUGGY migrator that accepts msg.value with
|
|
# moveNative=false leaves that value stuck in the contract forever (no sweep). --
|
|
s = Solver()
|
|
msgValue = Int('msgValue')
|
|
s.add(msgValue >= 1) # value sent...
|
|
# BUG: no H5 guard, moveNative is false, native is NOT forwarded -> retained in migrator.
|
|
migratorNativeRetained = msgValue
|
|
s.add(migratorNativeRetained > 0) # stranded, unrecoverable
|
|
check("BUGGY no-StrayNative migrator CAN strand native value (unrecoverable, no sweep)", s,
|
|
expect_unsat=False, kind="NEG-CTRL")
|
|
|
|
# ---- NEG-CTRL 4 (zero destination allowed): without H1 a migration could sweep to
|
|
# address(0), burning the holder's assets. z3 finds it. -------------------
|
|
s = Solver()
|
|
destination = Int('destination')
|
|
# BUG: no ZeroDestination guard.
|
|
s.add(destination == 0)
|
|
check("BUGGY no-ZeroDestination migrator CAN sweep to address(0) (burn)", 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()
|
|
if allok:
|
|
print("AereAccountMigrator safety:")
|
|
print(" PROVED -- every ERC-20 leg delivers ONLY to the caller's non-zero `destination`")
|
|
print(" (P1), the migrator takes no custody (pulled == delivered, delta 0, P2), a migration")
|
|
print(" is all-or-nothing with no partial move under whole-tx revert (P3), no native can be")
|
|
print(" stranded (P4), and a pinned expectedDestination is honored (P5). Four NEG-CTRLs fire:")
|
|
print(" a fee-skim, a per-leg try/catch, a missing StrayNative guard, and a missing")
|
|
print(" ZeroDestination guard each reproduce a real leak / partial-move / strand / burn.")
|
|
print(" [VERIFY] Whole-tx atomicity rests on SafeERC20 reverting on a false/failing ERC-20")
|
|
print(" and on nonReentrant; that is an EVM-revert property asserted structurally, and P3")
|
|
print(" models its consequence (all-or-nothing) rather than the opcode semantics.")
|
|
print(" BOUNDARY: this checks the DESIGN-level logic, not the compiled EVM bytecode.")
|
|
else:
|
|
print(" NOT fully established (see FAILED / unexpected CEX above).")
|
|
import sys
|
|
sys.exit(0 if allok else 1)
|