aere-research/formal-consensus/destinationsettler_smt.py
Aere Network 4a0b48588c Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:30 +03:00

216 lines
13 KiB
Python

#!/usr/bin/env python3
# -----------------------------------------------------------------------------
# destinationsettler_smt.py
#
# SMT proof (z3) of NO-ARBITRARY-RECIPIENT + AT-MOST-ONCE (no duplicate fill) +
# OUTPUT-MATCH for AereDestinationSettler, the ERC-7683 filling-side settler, with
# load-bearing NEGATIVE CONTROLS for each guard.
#
# Contract: contracts/contracts/intents/AereDestinationSettler.sol
#
# The settler's fill() delivers an order's promised output on AERE and records it
# so the origin chain can later repay the solver. The safety-critical guarantees
# (fail-closed) are: a fill can only deliver to the order's DECLARED recipient
# (never an attacker address), a duplicate fill is impossible (a solver is repaid
# at most once per intent), and an output that does not satisfy the declared leg
# (wrong token / wrong recipient / short amount) is rejected.
#
# We model fill()'s guards as first-order constraints over the decoded fields and
# prove the properties by asserting each property's NEGATION under the guards and
# showing z3 returns UNSAT (no counterexample). Each NEG-CTRL removes exactly one
# guard and shows the corresponding attack becomes satisfiable (SAT).
#
# Addresses / token ids are modelled as Ints (identity only; the proof needs
# equality/disequality, not byte layout). This checks the DESIGN-level guard
# logic, NOT the EVM bytecode.
#
# Decoded fields (from fill()):
# originData -> (declOrderId, declOutputToken, declOutputAmount, declRecipient)
# fillerData -> (deliveredToken, deliveredAmount, deliveredRecipient, repayAddr)
# orderId = the standard's canonical id argument
#
# Guards enforced by fill() (all must hold or it reverts, no state change):
# G1 declOrderId == orderId (OrderIdMismatch)
# G2 declRecipient != 0 (ZeroRecipient)
# G3 declOutputToken != 0 (ZeroAddress)
# G4 declOutputAmount != 0 (ZeroAmount)
# G5 fills[orderId].filler == 0 (unfilled) (AlreadyFilled)
# G6 deliveredToken == declOutputToken AND
# deliveredRecipient == declRecipient AND
# deliveredAmount >= declOutputAmount (OutputMismatch)
# On success: record fills[orderId], then safeTransferFrom(solver -> deliveredRecipient).
# -----------------------------------------------------------------------------
from z3 import Int, Bool, Solver, And, Or, Not, Implies, 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
def guards(s, orderId, declOrderId, declRecipient, declOutputToken, declOutputAmount,
deliveredToken, deliveredRecipient, deliveredAmount, alreadyFiller,
include_g1=True, include_g2=True, include_g6=True):
"""Assert fill()'s guards. Flags let a NEG-CTRL drop exactly one guard."""
if include_g1:
s.add(declOrderId == orderId) # G1
if include_g2:
s.add(declRecipient != 0) # G2 (ZeroRecipient)
s.add(declOutputToken != 0) # G3
s.add(declOutputAmount >= 1) # G4
s.add(alreadyFiller == 0) # G5 (unfilled)
if include_g6:
s.add(deliveredToken == declOutputToken) # G6a
s.add(deliveredRecipient == declRecipient) # G6b
s.add(deliveredAmount >= declOutputAmount) # G6c
print("### AereDestinationSettler -- NO-ARBITRARY-RECIPIENT + AT-MOST-ONCE + OUTPUT-MATCH\n")
# ---- P1 NO-ARBITRARY-RECIPIENT: a successful fill delivers ONLY to the order's
# declared recipient. Negation: fill succeeds yet the funds go to some other
# address `attacker` (attacker != declRecipient). Under the guards, UNSAT. --
s = Solver()
orderId, declOrderId = Int('orderId'), Int('declOrderId')
declRecipient, declOutputToken, declOutputAmount = Int('declRecipient'), Int('declOutputToken'), Int('declOutputAmount')
deliveredToken, deliveredRecipient, deliveredAmount = Int('deliveredToken'), Int('deliveredRecipient'), Int('deliveredAmount')
alreadyFiller, attacker = Int('alreadyFiller'), Int('attacker')
guards(s, orderId, declOrderId, declRecipient, declOutputToken, declOutputAmount,
deliveredToken, deliveredRecipient, deliveredAmount, alreadyFiller)
# The tokens are transferred to `deliveredRecipient` (the safeTransferFrom `to`).
# NEGATION: that recipient is an attacker distinct from the order's declared one.
s.add(attacker != declRecipient, deliveredRecipient == attacker)
check("P1 fill can only deliver to the DECLARED recipient (no arbitrary recipient)", s)
# ---- P1b the transfer destination is provably the declared recipient AND non-zero
s = Solver()
declRecipient, deliveredRecipient = Int('declRecipient'), Int('deliveredRecipient')
declOrderId, orderId = Int('declOrderId'), Int('orderId')
declOutputToken, declOutputAmount = Int('declOutputToken'), Int('declOutputAmount')
deliveredToken, deliveredAmount, alreadyFiller = Int('deliveredToken'), Int('deliveredAmount'), Int('alreadyFiller')
guards(s, orderId, declOrderId, declRecipient, declOutputToken, declOutputAmount,
deliveredToken, deliveredRecipient, deliveredAmount, alreadyFiller)
# NEGATION: the actual transfer target is zero OR differs from the declared recipient.
s.add(Or(deliveredRecipient == 0, deliveredRecipient != declRecipient))
check("P1b transfer target == declared recipient AND is non-zero", s)
# ---- P2 AT-MOST-ONCE: an order already filled cannot be filled again. The G5
# guard rejects fills[orderId].filler != 0. Model the double-fill directly:
# a second fill requires filler==0 but the slot is already set (filler!=0). -
s = Solver()
firstFiller = Int('firstFiller')
s.add(firstFiller != 0) # order already filled by someone (slot occupied)
s.add(firstFiller == 0) # ... yet the AlreadyFilled guard demands "unfilled"
check("P2 second fill of an already-filled order is guard-infeasible (AlreadyFilled)", s)
# ---- P2b the recorded slot is EXACTLY the arg orderId (G1 binds declOrderId to
# the id argument), so a fill can never be recorded under a different order's
# slot to dodge the duplicate check. Negation: recorded under a slot whose
# declared id differs from the arg. UNSAT under G1. -----------------------
s = Solver()
orderId, declOrderId = Int('orderId'), Int('declOrderId')
s.add(declOrderId == orderId) # G1
s.add(declOrderId != orderId) # NEGATION
check("P2b fill is bound to the arg orderId (no cross-slot record, G1)", s)
# ---- P3 OUTPUT-MATCH: a recorded fill satisfies the declared output exactly on
# token+recipient and is >= on amount. Negation: a fill succeeds yet delivers
# the wrong token OR short amount. Under G6, UNSAT. -----------------------
s = Solver()
orderId, declOrderId = Int('orderId'), Int('declOrderId')
declRecipient, declOutputToken, declOutputAmount = Int('declRecipient'), Int('declOutputToken'), Int('declOutputAmount')
deliveredToken, deliveredRecipient, deliveredAmount = Int('deliveredToken'), Int('deliveredRecipient'), Int('deliveredAmount')
alreadyFiller = Int('alreadyFiller')
guards(s, orderId, declOrderId, declRecipient, declOutputToken, declOutputAmount,
deliveredToken, deliveredRecipient, deliveredAmount, alreadyFiller)
# NEGATION: wrong token OR under-delivered amount slipped through.
s.add(Or(deliveredToken != declOutputToken, deliveredAmount < declOutputAmount))
check("P3 output-mismatch (wrong token OR short amount) is rejected", s)
# ============================ NEGATIVE CONTROLS ==============================
# ---- NEG-CTRL 1 (recipient guard G6b removed): without deliveredRecipient ==
# declRecipient, a solver can deliver the output to an arbitrary attacker.
s = Solver()
orderId, declOrderId = Int('orderId'), Int('declOrderId')
declRecipient, declOutputToken, declOutputAmount = Int('declRecipient'), Int('declOutputToken'), Int('declOutputAmount')
deliveredToken, deliveredRecipient, deliveredAmount = Int('deliveredToken'), Int('deliveredRecipient'), Int('deliveredAmount')
alreadyFiller, attacker = Int('alreadyFiller'), Int('attacker')
guards(s, orderId, declOrderId, declRecipient, declOutputToken, declOutputAmount,
deliveredToken, deliveredRecipient, deliveredAmount, alreadyFiller,
include_g6=False) # drop the whole output-match block...
s.add(deliveredToken == declOutputToken, deliveredAmount >= declOutputAmount) # ...keep token+amount
s.add(attacker != declRecipient, deliveredRecipient == attacker) # but NOT recipient
check("no-recipient-check settler CAN deliver to an arbitrary attacker", s,
expect_unsat=False, kind="NEG-CTRL")
# ---- NEG-CTRL 2 (duplicate guard G5 removed): without AlreadyFilled, a second
# fill of the same order succeeds -> the solver is repaid twice per intent. -
s = Solver()
firstFiller, secondFiller = Int('firstFiller'), Int('secondFiller')
s.add(firstFiller != 0, secondFiller != 0) # order already filled; a 2nd fill also runs
# No G5 guard tying the 2nd fill to "unfilled" -> both fills are recorded.
check("no-AlreadyFilled settler CAN double-fill one order (double repayment)", s,
expect_unsat=False, kind="NEG-CTRL")
# ---- NEG-CTRL 3 (zero-recipient guard G2 removed): without ZeroRecipient, an
# order can be "delivered" to address(0) (funds burned / undeliverable). ----
s = Solver()
orderId, declOrderId = Int('orderId'), Int('declOrderId')
declRecipient, declOutputToken, declOutputAmount = Int('declRecipient'), Int('declOutputToken'), Int('declOutputAmount')
deliveredToken, deliveredRecipient, deliveredAmount = Int('deliveredToken'), Int('deliveredRecipient'), Int('deliveredAmount')
alreadyFiller = Int('alreadyFiller')
guards(s, orderId, declOrderId, declRecipient, declOutputToken, declOutputAmount,
deliveredToken, deliveredRecipient, deliveredAmount, alreadyFiller,
include_g2=False) # drop ZeroRecipient
s.add(declRecipient == 0, deliveredRecipient == 0)
check("no-ZeroRecipient settler CAN record a delivery to address(0)", s,
expect_unsat=False, kind="NEG-CTRL")
# ---- NEG-CTRL 4 (output-match guard G6 removed): without OutputMismatch, a solver
# delivers a WORTHLESS/wrong token yet records a valid fill (still repaid). --
s = Solver()
orderId, declOrderId = Int('orderId'), Int('declOrderId')
declRecipient, declOutputToken, declOutputAmount = Int('declRecipient'), Int('declOutputToken'), Int('declOutputAmount')
deliveredToken, deliveredRecipient, deliveredAmount = Int('deliveredToken'), Int('deliveredRecipient'), Int('deliveredAmount')
alreadyFiller = Int('alreadyFiller')
guards(s, orderId, declOrderId, declRecipient, declOutputToken, declOutputAmount,
deliveredToken, deliveredRecipient, deliveredAmount, alreadyFiller,
include_g6=False)
s.add(deliveredToken != declOutputToken) # wrong token delivered, recorded anyway
check("no-OutputMismatch settler CAN record a wrong-token delivery", 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("AereDestinationSettler fill() safety:")
print(" PROVED -- under the four fail-closed guards, a fill can deliver ONLY to the order's")
print(" declared (non-zero) recipient (P1/P1b), is bound to the arg orderId and can be")
print(" recorded at most once (P2/P2b), and an output that mismatches the declared token or")
print(" is short is rejected (P3). Four NEG-CTRLs fire: dropping the recipient-match, the")
print(" AlreadyFilled, the ZeroRecipient, or the output-match guard each opens a real attack")
print(" (arbitrary recipient / double repayment / burn-to-zero / wrong-token repayment).")
print(" [VERIFY] ATOMICITY of the delivery (effects-before-interaction record, then a single")
print(" SafeERC20 pull solver->recipient that reverts the WHOLE tx on failure, under")
print(" nonReentrant) is an EVM-revert/whole-tx property, asserted structurally not by this")
print(" arithmetic model. Repayment on the ORIGIN chain is gated by the validator-set-anchored")
print(" finality+inclusion proof and is out of scope here [MEASURE].")
print(" BOUNDARY: this checks the DESIGN-level guard 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)