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.
228 lines
11 KiB
Solidity
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);
|
|
}
|
|
}
|