# Aere Quantum Migration Toolkit A starting toolkit to assess and migrate EVM accounts toward post-quantum readiness on Aere Network (chain 2800), or on any EVM chain reachable by RPC. Aere Network is the post-quantum EVM L1: it ships five live NIST post-quantum verification precompiles on mainnet (Falcon-512, Falcon-1024, ML-DSA-44, SLH-DSA-128s, SHAKE256, at `0x0AE1..0x0AE5`, activated at block 9,189,161), plus a live Falcon-512-owned smart account and factory. That makes Aere the natural home for tooling that helps others measure their quantum exposure and move to a post-quantum account. This toolkit extends Aere's internal account-migration work outward into something anyone can run. It is a real product with one unified CLI and zero runtime dependencies (pure Node built-ins, Node 18+): 1. `scan.js` / `aere-pqc scan`, a real PQC-readiness scanner (EOA vs contract, and which signature primitives a contract uses). 2. `lib/migrator.js` / `aere-pqc build-migration`, the transaction-CONSTRUCTION SDK: given a classical ECDSA EOA and a target Falcon-512 public key it derives the counterfactual post-quantum account, enumerates the assets to move (read-only), and emits the exact sequence of UNSIGNED transactions a holder would sign. It never signs, deploys, or moves funds. 3. `lib/readiness.js` / `aere-pqc readiness` + `simulate.js` / `aere-pqc simulate`, the quantum-readiness cost simulator: one-time migration gas plus ongoing per-authorization cost, from the measured PQC verify gas. `aere-pqc` is the single entry point that wraps `scan`, `simulate`, `build-migration`, and `readiness`. The lower-level `scan.js`, `plan.js`, `simulate.js` CLIs remain for direct use. ## The scope boundary (this never moves) Migrating an account's authentication to a post-quantum scheme changes how that account authorizes. It does NOT make Aere consensus post-quantum. Mainnet 2800 still seals blocks with classical secp256k1 QBFT, and the on-chain ZK verifiers (BN254 Groth16) are classical. Nothing in this toolkit claims otherwise, and the scanner and simulator both restate the boundary in their output. ## Honest status This is a starting toolkit, not a finished product. What is real and verified is listed under "What actually runs" below. What is heuristic, out of scope, or not yet deployed is flagged inline with `[VERIFY]` and `[MEASURE]`, in the code and in the reports the tools print. ## 1. The readiness scanner: `scan.js` Given an EVM address, the scanner reports quantum exposure as a colour: - **RED**, classical-only. An EOA (secp256k1 ECDSA authenticated, so Shor-breakable and unable to hold verification code), or a contract with no detectable post-quantum precompile usage. - **YELLOW**, hybrid. A contract that uses both a classical signature primitive (ecrecover at `0x01`, or P-256 at `0x100`) and Aere's live PQC precompile band. - **GREEN**, PQC-capable. A contract that uses Aere's live native PQC precompiles (`0x0AE1..0x0AE5`). ```bash # live scan over JSON-RPC (this is the only part that needs network access) node scan.js 0x465d9E3b476BF98Aa1393079e240Db5D2a9bEA6A --rpc https://rpc.aere.network # offline: scan a bundled real-bytecode fixture node scan.js --fixture AerePQCAttestation # offline: scan bytecode you already have node scan.js 0xYourContract --bytecode 0x60806040... # machine-readable node scan.js 0x465d... --rpc https://rpc.aere.network --json ``` ### How it detects, and why it is honest The scanner does not grep the hex. A naive search for `610ae1` (PUSH2 0x0AE1) false-positives, because those bytes can sit inside another instruction's push data. Two techniques make the detection real: 1. **Opcode-aware decoding.** The bytecode is walked as an instruction stream, skipping each PUSH's immediate bytes, so a matched constant is a genuine push. 2. **Local stack simulation to resolve CALL targets.** For every CALL/STATICCALL/DELEGATECALL/CALLCODE the scanner recovers the address argument (the second stack item) by simulating the stack per basic block. That distinguishes an actual call to `0x0AE1` from the bare number `0x0AE5` used as a memory length near an unrelated staticcall, which is precisely the false positive a proximity heuristic hits. (An earlier proximity-only version of this tool misread `AereHybridAuth` as hybrid for exactly that reason; the stack-simulation version reads it correctly.) The signature-verify addresses `0x0AE1..0x0AE4` (2785..2788) are not round numbers, so a genuine opcode-aligned push of one is a medium-confidence signal even when the call target is staged through memory and cannot be resolved locally. The common literals `0x01`, `0x100`, and `0x0AE5` are never used as a fallback signal; they count only when stack simulation resolves them as an actual call target. ### Scanner limitations (read these) - Bytecode scanning only sees NATIVE precompile calls. A contract that verifies Falcon through the SOLIDITY `AereFalcon512Verifier` (`0x4E8e...D8fFC`, the path the deployed `AerePQCAccount` and `AereHybridAuth` actually take) delegates to a normal contract address, so it reads as RED here even though it is PQC-capable. The bundled `AereHybridAuth` fixture demonstrates this on purpose. Confirm an auth path against source when the scanner says RED. - `ecrecover` (`0x01`) detection is high confidence only because stack simulation resolves the call target to the exact address `1`. A contract that never calls a signature precompile (authorization is by owner or role) also reads RED, which is correct: it has no quantum-vulnerable signature surface to migrate. - The scanner classifies signature-verification exposure. It does not audit key management, upgradeability, or business logic. ## 2. The migration SDK: `lib/migrator.js` (transaction construction) Given a classical ECDSA EOA and a target NIST Falcon-512 public key, this module builds the exact sequence of UNSIGNED transactions a holder would sign to move their assets onto a post-quantum `AerePQCAccount`. It is the concrete, signable-artifact layer on top of the older `lib/migrate.js` plan builder. ```bash # demo (uses the live sample account's real Falcon key; demo EOA 0x..dEaD) node aere-pqc.js build-migration # real build: derive the target, enumerate a real EOA's assets read-only, and # attach a live gas estimate to each unsigned tx node aere-pqc.js build-migration --eoa 0xYourEoa --falcon 0x09... \ --rpc https://rpc.aere.network --native 1000000000000000 --estimate # atomic sweep via the (audit-gated, not-yet-deployed) AereAccountMigrator node aere-pqc.js build-migration --eoa 0xYourEoa --path migrator \ --migrator 0xVerifiedMigrator --tokens 0xTokenA,0xTokenB --amounts 1000,2000 # machine-readable node aere-pqc.js build-migration --eoa 0xYourEoa --rpc https://rpc.aere.network --json ``` Key functions (`import ... from '@aere/pqc-migration-toolkit/migrator'`): - `predictTargetAddress({ falconPubKey, salt })`: the counterfactual `AerePQCAccount` address, pure CREATE2 math, no RPC. Mirrors `AerePQCAccountFactory.predictAddress` and is verified in `selftest.js` against the live on-chain sample account. - `enumerateAssets({ rpcUrl, owner, tokens })`: READ-ONLY. `eth_getBalance` for native AERE and `eth_call balanceOf(owner)` per ERC-20 candidate; returns which assets have a non-zero balance and therefore need moving. Signs nothing. - `buildMigrationTransactions({ path, eoa, falconPubKey, tokens, nativeWei, ... })`: the core. Returns an array of UNSIGNED tx objects, each stamped `signed:false`, shaped `{ to, from, value, data, chainId }` (nonce/gas left for the signer). Two paths: - `direct` (works today, no extra contract): per-asset `ERC-20 transfer` + native send straight to the target account. Not atomic. - `migrator` (atomic, all-or-nothing): `approve(migrator, amount)` per token then one `AereAccountMigrator.migrateToPqcAccount(...)` that binds the destination to the Falcon key on-chain. REQUIRES an explicit `--migrator` address, because `AereAccountMigrator` is repo source in the audit-gated `notDeployedHeld` set and has NO live mainnet address (see below). - `estimateMigrationGas(plan, rpcUrl)`: attaches a read-only `eth_estimateGas` result to each unsigned tx (or an error string when a step cannot be simulated pre-deploy). Broadcasts nothing. - `encodeMigrateCalldata`, `encodeMigrateToPqcAccountCalldata`, `encodeApproveCalldata`, `encodeTransferCalldata`, `encodeCreateAccountCalldata`: the individual calldata builders, each verified byte-for-byte against ethers v6 (see "What actually runs"). The calldata encoder (`lib/abi.js`) is a zero-dependency Solidity ABI encoder for exactly the types the migrator needs (`uint256, address, bool, bytes, address[], uint256[]`), validated against ethers 6.16.0. ### The migration paths **PQC account (Falcon-512 owned).** Derive the counterfactual account (step 1, off-chain), deploy it through the live factory with `createAccount(falconPubKey, salt)` (step 2, permissionless and idempotent), then move assets and roles off the old EOA into it (step 3). After the move, spending authority is the Falcon-512 key only. There is no ECDSA fallback, so register the key in `AerePQCKeyRegistry` (proof-of-possession) and set PQC social-recovery guardians before moving material value. **Hybrid account (ECDSA + Falcon-512).** Register a hybrid identity in `AereHybridAuth` binding your existing classical address to a Falcon-512 key, then authorize with both legs during the transition. An attacker must break both secp256k1 AND Falcon-512. **EIP-7702 delegation.** Aere supports EIP-7702, so an EOA can temporarily delegate execution to account code to batch a migration move in a single EOA transaction. ### Founder-gated caveat, and what works today The `AerePQCAccountFactory` (`0xd5315Ea7...CE58`) is live, and `createAccount` is permissionless, so deploying a PQC account and moving assets into it with the `direct` path is possible TODAY with no special authority: the whole `direct` sequence is plain factory + ERC-20 + native transactions the holder signs. What is NOT deployed: - `AereAccountMigrator` (the atomic `migrate` / `migrateToPqcAccount` conduit) is repo source (`contracts/pqc/AereAccountMigrator.sol`) in the audit-gated `notDeployedHeld.externalAuditGated_fundFlow` set. It has NO mainnet address. The `migrator` path therefore refuses to build unless you pass an explicit `--migrator` address, and it prints a `[VERIFY]` warning telling you to confirm that address holds the exact audited bytecode before granting it any approval. - `AereHybridAuth` is repo source with no mainnet address, so the hybrid path's deploy step is a founder/deployer step, flagged `[VERIFY]`. Deploying NEW contracts or a corrected V2 redeploy is signed by the Foundation/deployer key, never by this toolkit. ## 3. The cost simulator: `aere-pqc readiness` and `simulate.js` Two views on cost, both over the MEASURED PQC verify gas from `aerenew/docs/AERE-BENCHMARK-REPORT.md`: - `aere-pqc readiness
--rpc ` is the per-account report: it scans the account, enumerates its movable assets read-only, and prints the one-time migration gas (account deploy + asset moves) plus the ongoing per-authorization cost. Asset-move gas is a clearly-labeled `[VERIFY]` planning estimate anchored to the one asset-move figure measured live this session (a native AERE send at 21,246 gas via `eth_estimateGas`); PQC auth gas is `[CITED]`. - `simulate.js` / `aere-pqc simulate` is the fleet projection across many accounts. ```bash aere-pqc readiness 0xYourEoa --rpc https://rpc.aere.network --tokens 0xTokenA node simulate.js --count 100 --scheme hybrid --auths 5 node simulate.js --accounts accounts.json --gwei 1 node simulate.js --schemes # list modeled schemes ``` Modeled per-authorization gas (native precompile verify-and-record receipt, `[CITED: AERE-BENCHMARK-REPORT.md Part C.1]`): | Scheme | Marginal verify gas | Verify-and-record tx gas | |---|--:|--:| | Falcon-512 | 40,000 | 86,336 | | Hybrid (ECDSA + Falcon-512) | 43,000 | 89,336 | | Falcon-1024 | 75,000 | 145,496 | | ML-DSA-44 | 55,000 | 351,050 | | SLH-DSA-128s | 350,000 | 558,276 | The `falcon512_solidity` scheme instead models the CURRENTLY-DEPLOYED `AerePQCAccount`, which verifies Falcon in the EVM via the Solidity verifier: one authorization measured 10,278,313 gas on-chain `[CITED: pqc-account.json]`, under the EIP-7825 2^24 per-tx cap. These two Falcon paths (native precompile ~86k vs deployed-account Solidity ~10.5M) must not be conflated; the simulator keeps them distinct. Account CREATE2 deploy gas is 1,493,084 `[CITED: pqc-account.json]`. ## What actually runs (verified, not asserted) `node selftest.js` runs with no network and checks all of this (21 assertions, all PASS): - `keccak256("")` equals the known Ethereum/Keccak vector (the zero-dependency keccak is correct). - `predictPqcAccountAddress` re-derives the LIVE sample `AerePQCAccount` address `0xa42a5e7F72E46BadC11367650Ec34D676194326f` from its real 897-byte Falcon-512 key (proves the CREATE2 derivation is byte-correct against a real deployment). - The scanner classifies the four bundled REAL bytecode fixtures correctly: `AerePQCAttestation` GREEN, `AerePQCTxAccount` GREEN, `AereHybridAuth` RED (the Solidity-verifier scope limit), `AereFalcon512Verifier` RED (its `0x0100` constants are literals, not P-256). A synthetic hybrid reads YELLOW; empty code reads RED (EOA). - The migration-SDK calldata builders (`migrate`, `approve`, `transfer`, `balanceOf`) are byte-identical to values independently generated by ethers 6.16.0 (the encoder in `lib/abi.js` is a correct Solidity ABI encoder; the ground truth was generated with `aerenew/contracts/node_modules/ethers` and hardcoded into `selftest.js` so the check stays zero-dependency). - `buildMigrationTransactions` returns UNSIGNED objects only: every tx is stamped `signed:false`, none carries an `r`/`s`/`v`/`signature` field, and the target matches the CREATE2 derivation. The `migrator` path refuses to build without an explicit migrator address (because it is not deployed). - The readiness report composes the CITED deployed-account auth figure (10,278,313 gas) and counts movable assets correctly. Additionally verified live against `https://rpc.aere.network` (chain 2800, ~block 10,416,783, 2026-07-19): - `aere-pqc scan` fetched real bytecode and classified the live `AerePQCAttestation` (`0x465d...`) as GREEN (4402 bytes) and the live deployer EOA (`0xbeB3...`) as RED (0 bytes of code). - `aere-pqc build-migration --eoa 0xbeB3... --native 1000000000000000 --estimate` produced two unsigned txs and attached REAL `eth_estimateGas` results: `createAccount` 59,064 gas (low because the salt-0 sample account is ALREADY deployed, so the factory's idempotent `createAccount` short-circuits; a FIRST deploy is the cited 1,493,084), and the native send 21,246 gas. - Honest hazard surfaced by the live estimate: auto-draining the FULL native balance of an ACTIVE account fails, because the sender must reserve gas AND the balance can move between the read and the estimate. The tool now reserves a gas buffer by default and supports `--native ` for a fixed, race-free amount. - `aere-pqc readiness 0xbeB3... --rpc ...` reported a one-time migration cost of 1,514,330 gas (1,493,084 deploy + 21,246 native) = 0.001514330 AERE at 1 Gwei, and an ongoing 10,278,313 gas/auth (deployed reality) vs an 86,336 gas/auth native-precompile projection. Nothing above broadcasts a transaction. Every network call is a read (`eth_getCode`, `eth_getBalance`, `eth_call`, `eth_estimateGas`, `eth_gasPrice`). ## Addresses used (chain 2800) | Name | Address | Status | |---|---|---| | PQC precompile band | `0x0AE1..0x0AE5` | live (block 9,189,161) | | P-256 (RIP-7212/7951) | `0x100` | live (Osaka) | | ecrecover | `0x01` | protocol constant | | AerePQCAccountFactory | `0xd5315Ea7caa60d320c4f34b1bEd70dd9cc02CE58` | live | | AerePQCAccount (sample) | `0xa42a5e7F72E46BadC11367650Ec34D676194326f` | live | | AereFalcon512Verifier (Solidity) | `0x4E8e9682329e646784fB3bd01430aA4bA54D8fFC` | live | | AerePQCKeyRegistry | `0x1eCa3c5ADcBD0b22636D8672b00faC6D89363691` | live | | AereEntryPointV2 | `0x8D6f40598d552fF0Cb358b6012cF4227B86aF770` | live | | AereHybridAuth | (repo source) | not deployed `[VERIFY]` | | AereAccountMigrator | (repo source) | not deployed, audit-gated `[VERIFY]` | Testnet-only PQC precompiles `0x0AE6..0x0AE8` are NOT on mainnet 2800; a mainnet call to them returns empty. The scanner names them and flags `[VERIFY]` if seen. ## Files ``` aere-pqc.js unified CLI: scan | simulate | build-migration | readiness scan.js readiness scanner CLI (also aere-pqc scan) plan.js migration-plan CLI (older step-by-step plan builder) simulate.js migration cost simulator CLI (also aere-pqc simulate) selftest.js no-network proof: keccak + CREATE2 + fixtures + calldata + txs lib/keccak.js zero-dependency keccak256 + hex/checksum helpers lib/abi.js zero-dependency Solidity ABI encoder (validated vs ethers v6) lib/bytecode.js opcode-aware decoder + stack-simulation CALL-target resolver lib/scanner.js account classifier (EOA vs contract, RED/YELLOW/GREEN) lib/migrate.js plan builder + CREATE2 derivation (the original SDK) lib/migrator.js transaction-construction SDK (unsigned txs, asset enumeration) lib/readiness.js per-account quantum-readiness + migration-cost report lib/simulate.js fleet cost model over the measured PQC verify gas lib/precompiles.js address bands + measured gas, single source of truth lib/rpc.js minimal read-only JSON-RPC client (global fetch) fixtures/ real deployed bytecode extracted from this repo's artifacts ``` ## The one hard invariant This toolkit CONSTRUCTS and SIMULATES. It never signs, deploys, or moves funds. There is no private key anywhere in it. `build-migration` hands you inert, unsigned transaction objects; you review and sign them in your own wallet. Every network call is read-only. Executing a migration is user-gated (and, for the migrator/hybrid contracts, audit-and-founder-gated). ## License MIT.