aere-contracts/contracts/pqc/AereAccountMigrator.sol
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

228 lines
11 KiB
Solidity

// 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);
}
}