# 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 ```bash # 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.** P1–P3 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 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 / `_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):** ```bash 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`/`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.