aere-contracts/test/formal/PROVEN-PROPERTIES.md
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
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:37 +03:00

20 KiB
Raw Blame History

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 at 0x465d9E3b476BF98Aa1393079e240Db5D2a9bEA6A)
  • 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)

  1. 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 + live eth_call checks), not here.

  2. keccak injectivity. halmos models keccak256 as an injective, collision-free function (its default). P2's "distinct (keyId, nonce) ⇒ distinct storage slot" rests on this. This matches standard EVM verification practice.

  3. Bounded symbolic bytes lengths. Dynamic bytes parameters 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.
  4. Bounded loop unrolling. resolveActive's loop is structurally bounded by _count. R3 was run with --loop 12 against a registry of _count = 7 rows (5 seeded live schemes + 2 extra rows forming a successor cycle), i.e. the loop is unrolled well past its _count + 1 maximum, so every path terminates within the bound. Termination is demonstrated only for _count ≤ 7; the argument that it holds for all _count is the structural loop bound in the source, not an unbounded proof.

  5. Representative key/scheme instantiation. P1P3 fix a single registered Falcon-512 key (scheme 1). The nonce/slot/record logic in attest is scheme-independent, so this is a representative instantiation, not a proof quantified over all four schemes simultaneously.

  6. Ownership model. In R2 the test contract is the deployer and hence the Ownable owner; the symbolic non-owner caller is constrained to differ from it.

  7. 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 P1P4, R1R3 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 / _count values 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
NC14 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
NC14 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
NC13 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
NC12 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/transfer move exactly amount). 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; flush either succeeds or reverts).
  • Lending decimals normalised to 18/18 for tractability; the _scaleUp/ _scaleDown helpers 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 by AereLendingOracle and is out of scope here.
  • ecrecover is 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.