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.
20 KiB
Formally analysed properties — AERE PQC contracts
Symbolic execution with halmos 0.3.3 (Z3 4.12.6) over Solidity 0.8.23 builds of the LIVE contracts:
contracts/pqc/AerePQCAttestation.sol(deployed mainnet 2800 at0x465d9E3b476BF98Aa1393079e240Db5D2a9bEA6A)contracts/pqc/AereCryptoRegistry.sol
This is an additive Foundry + halmos layer beside the existing Hardhat project.
It compiles only the two contracts under analysis (+ OpenZeppelin Ownable and a
tiny cheatcode shim) into its own out-forge/ / cache-forge/; the Hardhat build
(artifacts/, cache/) and the Hardhat test suite are untouched.
Scope disclaimer. "Proven" below means: halmos found no counterexample to the stated assertion, within the stated bounds and assumptions. It is not a claim that the contracts are "formally verified" in general. Only the specific properties and bounds listed here were checked. The cryptographic soundness of the PQC precompiles is explicitly out of scope (see the oracle assumption).
How to reproduce
# from contracts/ (foundry.toml is here)
forge build # sanity compile (skips legacy stack-too-deep verifiers)
# Suite 1 — AerePQCAttestation
halmos --forge-build-out out-forge --contract AerePQCAttestationSymbolic \
--default-bytes-lengths "0,32,65,897,1312,1793" --loop 4
# Suite 2 — AereCryptoRegistry
halmos --forge-build-out out-forge --contract AereCryptoRegistrySymbolic \
--loop 12 --default-bytes-lengths "0,65"
Note: if you run a plain forge build immediately before halmos it can strip the
AST from out-forge/; delete out-forge/ cache-forge/ and let halmos rebuild
(it invokes forge build --ast itself).
Toolchain provenance (honest note)
halmos pulls eth-hash[pysha3] → safe-pysha3, a C extension with no wheel for the
host Python and no MSVC available. It was satisfied with a pure-Python keccak shim
backed by pycryptodome, verified against the known-answer empty-input digest
(c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470). This only
affects keccak inside the Python tooling; the contracts' own keccak is modelled by
halmos itself (see keccak assumption). Foundry forge 1.7.1; solc 0.8.23 via svm.
Latest run (both suites PASS)
Running 3 tests for test/formal/AerePQCAttestation.symbolic.t.sol:AerePQCAttestationSymbolic
[PASS] check_noOverwriteAcrossTwoAttests(bytes32,bytes32,bytes,bytes) (paths: 1)
[PASS] check_nonceAndStateMoveOnlyOnSuccess(bytes32,bytes,bytes32) (paths: 10)
[PASS] check_registerKeyRejectsMalformedPubKey(uint8,bytes) (paths: 33)
Symbolic test result: 3 passed; 0 failed
Running 4 tests for test/formal/AereCryptoRegistry.symbolic.t.sol:AereCryptoRegistrySymbolic
[PASS] check_resolveActiveTerminatesAndReturnsActive(uint256) (paths: 50)
[PASS] check_revokedOrUnknownNeverVerifies(uint256,bytes,bytes32,bytes) (paths: 686)
[PASS] check_setStatusIsOwnerOnly(address,uint256,uint8) (paths: 6)
[PASS] check_setSuccessorIsOwnerOnly(address,uint256,uint256) (paths: 5)
Symbolic test result: 4 passed; 0 failed
A deliberately-false negative control (nonce increments by 2) was run separately and
halmos did return a counterexample, confirming the assertions above are not
vacuously passing.
AerePQCAttestation
| # | Property | Test | Status |
|---|---|---|---|
| P1 | A key's nonce increases by exactly 1 on a successful attest, and is unchanged on any non-successful attest. attestationCount moves in lock-step. |
check_nonceAndStateMoveOnlyOnSuccess |
PASS |
| P2 | An attestation slot (keyId, nonce) is written at most once (no overwrite): two consecutive successful attests land in distinct slots and the first slot's record is not mutated by the second. |
check_noOverwriteAcrossTwoAttests |
PASS |
| P3 | A reverting / failed attest leaves all observable state unchanged (nonce, attestationCount, and the target attestation slot). |
check_nonceAndStateMoveOnlyOnSuccess (the !ok branch) |
PASS |
| P4 | registerKey rejects malformed pubKeys: registration succeeds only for a (scheme, length, header) triple matching the scheme's spec; an unknown scheme can never register. |
check_registerKeyRejectsMalformedPubKey |
PASS |
AereCryptoRegistry
| # | Property | Test | Status |
|---|---|---|---|
| R1 | Fail-closed: a REVOKED or UNKNOWN algorithm id can never pass verify(), for any pubkey/message/signature and regardless of what the routed verifier would return. |
check_revokedOrUnknownNeverVerifies |
PASS |
| R2 | Status transitions are owner-only: a non-owner setStatus (and setSuccessor) call cannot change registry state. |
check_setStatusIsOwnerOnly, check_setSuccessorIsOwnerOnly |
PASS |
| R3 | resolveActive terminates on any symbolic start id (returns an ACTIVE row or reverts), never diverging — including on a deliberately-seeded successor cycle. |
check_resolveActiveTerminatesAndReturnsActive |
PASS |
Assumptions (these bound the strength of every "PASS" above)
-
Precompile as an uninterpreted oracle (central). halmos cannot execute AERE's native PQC precompiles (
0x0AE1..0x0AE4) — they are Bouncy-Castle native code on the Besu fork, not EVM bytecode. The tests etch a 1-slot EVM stub at the scheme's precompile address and drive its 32-byte return word with a symbolic value, so halmos explores both the "precompile says valid" and "precompile says invalid" branches on every call. This is strictly more adversarial than a faithful precompile: the safety properties are shown to hold no matter what the precompile returns. It proves nothing about Falcon/ML-DSA/SLH-DSA cryptographic soundness — that is covered elsewhere (NIST KAT vectors + liveeth_callchecks), not here. -
keccak injectivity. halmos models
keccak256as an injective, collision-free function (its default). P2's "distinct(keyId, nonce)⇒ distinct storage slot" rests on this. This matches standard EVM verification practice. -
Bounded symbolic
byteslengths. Dynamicbytesparameters are explored only at the lengths passed via--default-bytes-lengths:- Suite 1 used
0,32,65,897,1312,1793— this covers the exact valid pubkey lengths of all four schemes (897 Falcon-512, 1793 Falcon-1024, 1312 ML-DSA-44, 32 SLH-DSA-128s) plus invalid lengths, so P4's accept/reject boundary is genuinely exercised, not vacuous. Falcon signatures reach the verifier at length 65. - Suite 2 used
0,65(signature content is irrelevant to the fail-closed R1 gate). Lengths outside these sets are not explored.
- Suite 1 used
-
Bounded loop unrolling.
resolveActive's loop is structurally bounded by_count. R3 was run with--loop 12against a registry of_count = 7rows (5 seeded live schemes + 2 extra rows forming a successor cycle), i.e. the loop is unrolled well past its_count + 1maximum, so every path terminates within the bound. Termination is demonstrated only for_count ≤ 7; the argument that it holds for all_countis the structural loop bound in the source, not an unbounded proof. -
Representative key/scheme instantiation. P1–P3 fix a single registered Falcon-512 key (scheme 1). The nonce/slot/record logic in
attestis scheme-independent, so this is a representative instantiation, not a proof quantified over all four schemes simultaneously. -
Ownership model. In R2 the test contract is the deployer and hence the
Ownableowner; the symbolic non-owner caller is constrained to differ from it. -
EVM revert semantics. P3 and R2 rely on the EVM rolling back all state on a reverted call frame. The proofs additionally confirm the contracts contain no assembly path that writes state outside that revert scope.
What is NOT claimed
- Not "formally verified" as a blanket statement — only P1–P4, R1–R3 within the bounds above.
- No claim about the PQC precompiles' cryptographic correctness (oracle assumption).
- No claim about consensus: AERE consensus remains classical ECDSA QBFT; these are application-layer contract properties only.
- No proof for byte lengths /
_countvalues outside the enumerated bounds.
z3 SMT DESIGN MODELS (formal-consensus/*.py) — inductive accounting proofs
A second, complementary formal layer sits in formal-consensus/. These are z3
SMT design models in the style of formal-consensus/spokepool_smt.py (the model
that found the real SpokePool bond-accounting bug). Each models a contract's
money/authorization logic as an inductive transition system over unbounded
integers (the same abstraction solc's SMTChecker uses by default), proving the key
safety invariant is inductive — base case holds, and every operation preserves
it — and includes negative controls that reproduce the corresponding real bug
when a guard is removed (so the proofs are demonstrably non-vacuous).
Scope: these check the DESIGN math (the accounting/authorization arithmetic and its guards), NOT the compiled EVM bytecode. They complement the halmos bytecode-level suites above; they do not replace them.
Toolchain: z3 4.16.0 (pip), CPython 3.14. No forge/halmos needed.
Reproduce (from repo root):
python formal-consensus/settlementhub_smt.py # exit 0 on all-proved
python formal-consensus/lending_liquidation_smt.py
python formal-consensus/agentdid_session_smt.py
python formal-consensus/spokepool_smt.py # the original reference model
# --- the four LIVE (deployed 2026-07-12) PQC contracts ---
python formal-consensus/pqckeyregistry_smt.py # AerePQCKeyRegistry 0x1eCa..3691
python formal-consensus/hybridauthorizer_smt.py # AereHybridAuthorizer 0x168F..82E0
python formal-consensus/agentdid_lifecycle_smt.py # AereAgentDID 0xce64..22C5 (root lifecycle)
python formal-consensus/cryptoregistry_smt.py # AereCryptoRegistry 0xaE6f..baa5 (re-confirm)
LIVE PQC contracts, formally analysed 2026-07-12 (z3 4.16.0, CPython 3.14)
The four PQC contracts deployed to mainnet 2800 on 2026-07-12, each modelled as an
inductive/logical transition system with FIRING negative controls (buggy variant's
violation is sat), so every PROVED is demonstrably non-vacuous. forge/halmos
are not installed in this environment; these z3 DESIGN models are the runnable proof
here and complement (do not replace) the halmos BYTECODE suites above.
AerePQCKeyRegistry (0x1eCa3c5ADcBD0b22636D8672b00faC6D89363691) — pqckeyregistry_smt.py
| # | Property | Result |
|---|---|---|
| K1 | Cross-identity PoP replay impossible: the challenge binds owner, so a PoP for O cannot be one for O'≠O |
PROVED |
| K2 | Same-identity replay impossible: nonce is in the challenge and advances n→n+1 before store |
PROVED |
| K3 | A REVOKED (or NONE) key never verifies via verifyWithKey, for any precompile output |
PROVED |
| K4 | Rotation preserves the owner binding (successor.owner == predecessor.owner); old leaves ACTIVE |
PROVED |
| NC1–4 | owner-less challenge (cross-identity replay), nonce-less challenge (same-identity replay), missing-REVOKED gate, arbitrary-owner rotate | CEX (all fire) |
Assumptions: A1 signature-soundness (precompile accepts only a genuine signature over the exact message — out of scope, NIST-KAT/eth_call elsewhere); A2 keccak injectivity; design model, not bytecode.
AereHybridAuthorizer (0x168F2A6a3071e7654CF1784a6f5d7BC8e1a582E0) — hybridauthorizer_smt.py
| # | Property | Result |
|---|---|---|
| H1/H1b | Only an ACTIVE scheme authorizes: resolveActive returns ONLY an ACTIVE id; a success is verified under it (never REVOKED/DEPRECATED/UNKNOWN) |
PROVED |
| H2a | A REVOKED id with no ACTIVE successor → resolveActive reverts → fail-closed false (authorizes nothing) |
PROVED |
| H2b | A REVOKED id with an ACTIVE successor routes to the successor; the revoked id itself never verifies | PROVED |
| H3/H3b | resolveActive terminates within the _count-bounded walk (pigeonhole: no acyclic walk of length _count+1) |
PROVED |
| H3c/H3d | Fail-closed on every revert; algorithmFor (preference-else-default) resolution is total |
PROVED |
| NC1–4 | skip-resolveActive, status-blind resolver, fail-OPEN consumer | CEX (all fire) |
Assumptions: A1 registry verify is itself fail-closed (halmos R1 / cryptoregistry_smt.py); A2 loop bound is the source's structural bound; design model.
AereAgentDID (0xce641d7d7C10553D82b06B7C21d423550e7522C5) — agentdid_lifecycle_smt.py
Complements agentdid_session_smt.py (spend cap C1, gating C2, anti-replay C3, expiry C4).
| # | Property | Result |
|---|---|---|
| D1a/b/c | A session dies the instant its Falcon root is ROTATED/REVOKED or ceases to be Falcon (liveness re-reads the registry every call; authorize records nothing thereafter) |
PROVED |
| D2a | revokeSession is controller-only (current registry owner of the root) |
PROVED |
| D2b | createAgent is root-owner-only |
PROVED |
| D2c | Spend is recorded only with a valid session-key ECDSA signature (signer==sessionAddr) |
PROVED |
| D3 | Containment envelope: a successful action stays within cap AND within expiry | PROVED |
| NC1–3 | cached root-active flag, missing controller gate, missing signature gate | CEX (all fire) |
Assumptions: A1 ecrecover-soundness; A2 the liveness check reads the LIVE registry status (no stale cache — this is the fact D1 exploits); design model.
AereCryptoRegistry (0xaE6fC596bb3eCcbf5c5D02D67B0Ef065b3Afbaa5) — cryptoregistry_smt.py
Design-level re-confirmation of the halmos R1/R3 suite (the bytecode proof in
AereCryptoRegistry.symbolic.t.sol remains authoritative).
| # | Property | Result |
|---|---|---|
| R1a/b/c | verify() is fail-closed for REVOKED / UNKNOWN / hash-only rows before any routing, any precompile output |
PROVED |
| R3a/b/c | resolveActive returns only ACTIVE and terminates (pigeonhole over _count rows) |
PROVED |
| NC1–2 | status-gate-skipping verify, unconditional start-row resolver | CEX (both fire) |
Result (LIVE PQC set)
All stated safety invariants across the four contracts are inductive / valid under
the modelled guards; every negative control fires (guards are load-bearing). No
real counterexample to a should-hold invariant was found. One benign, non-exploitable
observation: AereAgentDID.authorize's overflow guard s.spent + amount < s.spent is
dead code under Solidity-0.8 checked arithmetic (the addition reverts before the
comparison can be true) — it fails safe (revert = no state change), so it is a
redundancy, not a defect. Consensus is unaffected: AERE remains classical ECDSA
QBFT; these are application-layer contract properties only. "Proven" means only the
specific properties + bounds + assumptions listed — not "formally verified" as a
blanket claim.
AereSettlementHubV2 — settlementhub_smt.py
Contract: contracts/settlement/AereSettlementHubV2.sol. Invariant
INV := balanceOf(hub) ≥ committedLiabilities[asset] (solvency / no-theft: a
permissionless sweepResidual can only ever move un-owed surplus, and
settle/cancelIntent can always pay out).
| # | Property | Result |
|---|---|---|
| S-base | fresh hub (0,0) satisfies INV | PROVED (unsat) |
| S1 | deposit() (pull amount, route fee, commit amountAfterFee) preserves INV |
PROVED |
| S2 | depositSolverBond() preserves INV |
PROVED |
| S3 | slashSolverBond() preserves INV (sink accepts OR rejects) |
PROVED |
| S4 | settle() preserves INV |
PROVED |
| S5 | cancelIntent() preserves INV |
PROVED |
| S6 | sweepResidual() (bounded by residualOf = bal − committed) preserves INV |
PROVED |
| NC1 | V1 unbounded sweep (bound = balance, ignores committed) steals owed funds | CEX (sat) |
| NC2 | buggy deposit committing the full amount (not amountAfterFee) over-commits | CEX (sat) |
AereLendingMarket.liquidate — lending_liquidation_smt.py
Contract: contracts/lending/AereLendingMarket.sol (the liquidate seize/clamp
math, modelled at 18/18 decimals — see assumption A1 in the file).
| # | Property | Result |
|---|---|---|
| L1 | clamped seize ≤ collateral (no over-seize / no collateralOf underflow) |
PROVED |
| L2 | insolvent-clamp branch: value(pay) ≤ value(collateral seized) (liquidator never overpays) |
PROVED |
| L3 | non-clamp branch: value(received) ≤ valueToSeize (no over-seize of VALUE) |
PROVED |
| L4 | repaidScaled (clamped to debtOf) ≤ debtOf (debt bookkeeping no-underflow) |
PROVED |
| — | scaleDown floor-division monotonicity (supports A1) |
PROVED |
| L3-note | reverse direction value(received) ≥ value(paid) fails by ≤ wei-dust that rounds toward the protocol (the SAFE direction) — expected DeFi rounding, NOT a bug |
demonstrated (sat, dust = 1 wei) |
| NC1 | without the clamp, seize can exceed collateral → theft of pooled collateral |
CEX (sat) |
| NC2 | buggy clamp that inflates the reduced pay (×(10000+bonus)/10000) makes the liquidator overpay |
CEX (sat) |
AereAgentDID — agentdid_session_smt.py
Contract: contracts/pqc/AereAgentDID.sol (session authorize / gating /
anti-replay). Invariant spent ≤ spendCap plus the containment gates.
| # | Property | Result |
|---|---|---|
| C-base | freshly issued session (spent = 0 ≤ cap) |
PROVED |
| C1 | authorize() preserves the cumulative spend cap spent ≤ cap (inductive) |
PROVED |
| C2 | a non-live session (revoked / expired / root-inactive) can never record spend | PROVED |
| C3 | actionNonce strictly increases each success → no signature replay |
PROVED |
| C4 | expiry is terminal (now > expiry stays true as time advances) |
PROVED |
| NC1 | a per-action cap check (instead of cumulative) lets total spend exceed the cap | CEX (sat) |
| NC2 | omitting the liveness gate lets a revoked session record spend | CEX (sat) |
Assumptions bounding these z3 models (honest)
- Design math, not bytecode. Unbounded
Int; Solidity-0.8 checked arithmetic is modelled as "underflow/overflow reverts (no state change)". No claim about 256-bit overflow beyond the explicit source guards, and no claim about the compiled bytecode (that is the halmos layer's job). - Standard ERC20 for the settlement hub (
transferFrom/transfermove exactlyamount). Fee-on-transfer / rebasing tokens are OUT OF SCOPE and would break the solvency invariant of any escrow — excluded by the asset-listing policy. - Trusted
AereSink(pulls at most the approved amount;flusheither succeeds or reverts). - Lending decimals normalised to 18/18 for tractability; the
_scaleUp/_scaleDownhelpers are pure power-of-ten mul/div whose floor rounding is proven monotone and only shrinks the liquidator's charge (strengthening L2). Oracle price staleness/deviation is enforced upstream byAereLendingOracleand is out of scope here. ecrecoveris a sound signature oracle for AereAgentDID (its cryptographic soundness is out of scope, same class as the halmos precompile-oracle assumption); the cap/nonce/gating proofs hold regardless of what it returns.
Result
No real counterexample was found in any of the three contracts: every stated
safety invariant is inductive under the modelled guards, and all negative
controls fire as expected (confirming the guards are load-bearing). The only sat
result on a "should-hold" phrasing was the AereLendingMarket L3-note, which is
a 1-wei rounding-dust discrepancy in the protocol-favouring (safe) direction —
documented above as expected DeFi rounding, not a defect. Consensus is
unaffected: AERE remains classical ECDSA QBFT; these are application-layer
contract properties only.