// SPDX-License-Identifier: MIT pragma solidity 0.8.23; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; /** * @title AereAccountMigrator, atomic asset migration from a classical EOA to a * post-quantum account (roadmap #14, quantum layer 11) * * @notice The last quantum-resistance gap at the ACCOUNT layer. Aere Network * already ships live post-quantum smart accounts (AerePQCAccount, * Falcon-512 owned), a live CREATE2 factory (AerePQCAccountFactory), and a * hybrid ECDSA + Falcon primitive (AereHybridAuth). What was missing was a * clean, no-custody path for a holder who is still sitting on a classical * secp256k1 ECDSA externally-owned account (EOA) to MOVE their assets onto * one of those post-quantum accounts in a single, self-authorized * transaction. This contract is that path. * * The motivation is harvest-now-decrypt-later. An ECDSA key is * Shor-breakable by a future quantum computer, and an EOA's public key is * exposed the moment it sends its first transaction. Long-term holdings * therefore should not sit under a classical key indefinitely; they should * move to a Falcon-512-owned (AerePQCAccount) or hybrid (AereHybridAuth) * account whose spending authority is a lattice key. * * @dev NO CUSTODY, NO ADMIN. This contract is a pure conduit. In `migrate` the * caller (the old EOA) is the `from` of every SafeERC20.safeTransferFrom, * and the caller-specified `destination` is the `to`. Tokens move * caller -> destination in one hop; the migrator never holds an ERC-20 * balance and never becomes the recipient. Native AERE (msg.value) is * forwarded, in full, to that same `destination` inside the same call. * There is: * - no owner, no admin, no upgrade, no initializer; * - no withdraw / rescue / sweep function, so nothing accidentally sent * to this address can ever be pulled out by anyone; * - no code path that sends value to any address other than the * caller-specified `destination`. * * @dev FAIL-CLOSED / ATOMIC. `destination` must be non-zero. Token and amount * arrays must be equal length. Every entry must be a non-zero token and a * non-zero amount. Native handling is strict: `moveNative` true requires a * non-zero msg.value, and any msg.value sent while `moveNative` is false * reverts (StrayNative) so value can never be stranded in the migrator. A * single failed transfer reverts the WHOLE transaction (SafeERC20 reverts * on a false/failing ERC-20), so a migration is all-or-nothing: it can * never leave a holder with a partially drained old account. * * @dev AUTHORIZATION. The move is authorized by the caller's classical ECDSA * key. Two supply patterns are supported and documented: * * 1. PRE-APPROVAL (works today, no protocol feature). The holder sends one * `approve(migrator, amount)` per ERC-20 to this contract, then calls * `migrate`. Each `safeTransferFrom` consumes the holder's allowance and * moves the tokens straight to the destination. This is the default, * portable path. * * 2. EIP-7702 DELEGATION (single-transaction UX). Aere supports EIP-7702 * (see docs/AERE-EIP-COMPATIBILITY-MATRIX.md, 7702 Supported), so the * EOA can, in one transaction, temporarily set its account code to a * batching delegate that both approves and calls `migrate`, removing the * separate approve transactions. The migrator itself is agnostic to * which pattern is used; under EIP-7702 the caller is still the EOA and * the no-custody invariants above are unchanged. * * The destination is chosen by the caller. `migrateToPqcAccount` binds the * destination to a real Falcon-512 key by deriving it from the LIVE * AerePQCAccountFactory (predictAddress is pure CREATE2 math, so it works * even before the account is deployed), giving the holder cryptographic * assurance that they are sweeping into the post-quantum account that their * Falcon key controls, and nowhere else. * * @dev SCOPE BOUNDARY (unchanged, non-negotiable). Migrating an account's * authorization to a post-quantum scheme 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. This * contract changes who can spend an account's assets; it does not touch the * consensus layer. Network-key rotation (Foundation operator key, validator * signing keys) is a separate, founder-and-audit-gated operation, coupled * to the consensus-PQC activation, and is documented as design-only in * docs/AERE-QUANTUM-MIGRATION.md. */ interface IAerePQCAccountFactory { /// @notice The counterfactual AerePQCAccount address for (falconPubKey, salt). /// Pure CREATE2 math; returns an address whether or not it is deployed. function predictAddress(bytes calldata falconPubKey, uint256 salt) external view returns (address); } contract AereAccountMigrator is ReentrancyGuard { using SafeERC20 for IERC20; /// @notice Emitted once per successful migration, binding the old (classical) /// account, the new (post-quantum) destination account, and the exact /// asset set that moved. event Migrated( address indexed oldAccount, address indexed newAccount, address[] tokens, uint256[] amounts, uint256 nativeAmount ); error ZeroDestination(); error LengthMismatch(); error NothingToMigrate(); error ZeroToken(); error ZeroAmount(); error ZeroNative(); error StrayNative(); error NativeForwardFailed(); error AddressMismatch(); /** * @notice Atomically move a set of ERC-20 balances (and optionally native * AERE) from the caller (the old classical EOA) to `destination` (a * post-quantum account address). Authorized by the caller's ECDSA key; * requires a prior `approve(this, amount)` per ERC-20, or an EIP-7702 * delegated batch that performs the approvals in the same transaction. * * @param destination the post-quantum account to receive everything. Must be * non-zero. This is the ONLY recipient of any asset moved. * @param tokens the ERC-20 tokens to move (each non-zero). * @param amounts the amount of each token to move (each non-zero, same * length as `tokens`). * @param moveNative if true, forward the full msg.value to `destination`; if * false, msg.value must be zero. */ function migrate( address destination, address[] calldata tokens, uint256[] calldata amounts, bool moveNative ) external payable nonReentrant { _migrate(destination, tokens, amounts, moveNative); } /** * @notice Same as `migrate`, but the destination is DERIVED from a Falcon-512 * public key via the live AerePQCAccountFactory, so the holder cannot * accidentally (or be tricked into) sweeping into the wrong address: * the destination is exactly the post-quantum account controlled by * their Falcon key. Works even before that account is deployed, because * `predictAddress` is pure CREATE2 math (the account can hold assets at * its counterfactual address and be deployed later via the factory). * * @param factory the AerePQCAccountFactory (the live one on 2800). * @param falconPubKey the 897-byte NIST Falcon-512 public key that will * own the destination account. * @param salt the CREATE2 salt used with the factory. * @param expectedDestination optional guard: if non-zero, the derived address * must equal it, else revert AddressMismatch. Lets a * caller assert the address they reviewed off-chain. * @return destination the derived post-quantum account address. */ function migrateToPqcAccount( address factory, bytes calldata falconPubKey, uint256 salt, address expectedDestination, address[] calldata tokens, uint256[] calldata amounts, bool moveNative ) external payable nonReentrant returns (address destination) { destination = IAerePQCAccountFactory(factory).predictAddress(falconPubKey, salt); if (expectedDestination != address(0) && expectedDestination != destination) { revert AddressMismatch(); } _migrate(destination, tokens, amounts, moveNative); } /** * @notice Read-only helper: the counterfactual AerePQCAccount address for a * Falcon key, delegating to the factory. Lets a frontend confirm the * destination before signing, without trusting an off-chain derivation. */ function predictPqcAccount(address factory, bytes calldata falconPubKey, uint256 salt) external view returns (address) { return IAerePQCAccountFactory(factory).predictAddress(falconPubKey, salt); } /* -------------------------------- internal ------------------------------- */ function _migrate( address destination, address[] calldata tokens, uint256[] calldata amounts, bool moveNative ) internal { if (destination == address(0)) revert ZeroDestination(); if (tokens.length != amounts.length) revert LengthMismatch(); uint256 nativeAmount = msg.value; if (moveNative) { if (nativeAmount == 0) revert ZeroNative(); } else { // Fail-closed: never accept native the caller did not ask to move; it // would otherwise be stranded (there is no withdraw function). if (nativeAmount != 0) revert StrayNative(); } if (tokens.length == 0 && !moveNative) revert NothingToMigrate(); // Move every ERC-20 straight from the caller to the destination. The // migrator is never the `to`, so it never takes custody. Any failing // transfer reverts the whole call (SafeERC20), so the migration is atomic. for (uint256 i = 0; i < tokens.length; i++) { address token = tokens[i]; uint256 amount = amounts[i]; if (token == address(0)) revert ZeroToken(); if (amount == 0) revert ZeroAmount(); IERC20(token).safeTransferFrom(msg.sender, destination, amount); } // Forward native AERE, in full, to the SAME destination. Last, after the // token moves, and guarded by nonReentrant so a hostile destination cannot // reenter mid-migration. if (moveNative) { (bool ok, ) = payable(destination).call{value: nativeAmount}(""); if (!ok) revert NativeForwardFailed(); } emit Migrated(msg.sender, destination, tokens, amounts, moveNative ? nativeAmount : 0); } }