// SPDX-License-Identifier: MIT pragma solidity 0.8.23; /** * @title AerePQCAccount, a post-quantum-secured ERC-4337 smart account (roadmap #270) * @notice An ERC-4337 v0.7 smart account whose sole owner is a NIST Falcon-512 * public key (897 bytes, the 0x09-headed NIST encoding). Every * authorization, whether a bundled UserOperation or an EIP-1271 * off-chain signature check, is decided by the LIVE, spec-complete * on-chain AereFalcon512Verifier (chain 2800: 0x4E8e9682...D8fFC): full * SHAKE256 HashToPoint, 14-bit public-key decode, compressed-signature * (comp_decode) decode, negacyclic NTT multiply mod q=12289 in * Z_q[x]/(x^512+1), centering, and l2-norm bound check. There is no * classical ECDSA fallback: the account is controlled by, and only by, a * quantum-resistant lattice key. * * This turns AERE's on-chain PQC verifier suite into a usable wallet * primitive: a user can hold an account whose spending authority is a * Falcon-512 key, verified by real lattice cryptography on chain. * * @dev SIGNATURE ENVELOPE. Wherever this account consumes a Falcon signature * (validateUserOp, isValidSignature, executeWithFalcon), the `signature` * bytes are `abi.encode(bytes nonce, bytes compSig)` where `nonce` is the * 40-byte Falcon salt and `compSig` is the compressed s2 encoding * (comp_encode output, the esig body WITHOUT the 0x29 header byte). The * signed message is DOMAIN-SEPARATED: a fund-moving authorization * (validateUserOp, executeWithFalcon) signs `abi.encodePacked(domain, hash)` * (64 bytes, domain = USEROP_DOMAIN / EXEC_DOMAIN), while the EIP-1271 * isValidSignature surface signs the raw 32 bytes `abi.encodePacked(hash)`. * The two message shapes can never collide, so a signature obtained through * a dapp "login" (isValidSignature over an attacker-chosen hash) can NOT be * replayed as a spend. For a UserOperation the hash is the ERC-4337 * userOpHash, which already binds sender + chainid. * * @dev GAS / EIP-7825. A Falcon-512 verify is roughly 10.5M gas. A full * handleOps → validateUserOp path therefore runs as a single ordinary * transaction that must stay under the Fusaka EIP-7825 per-transaction * cap of 2^24 = 16,777,216 gas; the deploy/proof script measures the * real number. `isValidSignature` is a `view`, so integrators can verify * a Falcon signature for free via eth_call regardless of the cap. A * native SHAKE/keccak precompile would cut the per-auth cost sharply * (tracked as a separate roadmap item); today PQC auth is heavy but real. * * No owner, no admin, no upgrade beyond the one-shot factory initializer. */ interface IFalcon512Verifier { /// @notice Full on-chain Falcon-512 verification. Returns true iff /// (pk, message, nonce, compSig) is a valid Falcon-512 signature. function verify( bytes memory pk, bytes memory message, bytes memory nonce, bytes memory compSig ) external view returns (bool); } contract AerePQCAccount { /// @notice The 897-byte NIST Falcon-512 public key that owns this account. bytes public falconPubKey; /// @notice The LIVE AereFalcon512Verifier this account delegates verification to. IFalcon512Verifier public falcon; /// @notice ERC-4337 EntryPoint allowed to call validateUserOp + executeFromEntryPoint. address public entryPoint; /// @notice Factory that deployed this account (gates the one-shot initializer). address public immutable factory; /// @notice Monotonic replay counter for the direct Falcon-authorized path. uint256 public execNonce; /// @notice EIP-1271 magic value returned by isValidSignature on success. bytes4 internal constant EIP1271_MAGIC = 0x1626ba7e; /// @notice ERC-4337 signature-validation-failed sentinel (validationData != 0). uint256 internal constant SIG_VALIDATION_FAILED = 1; /// @notice NIST Falcon-512 public key: 897 bytes, first byte 0x09 (0x00 | logn=9). uint256 internal constant FALCON512_PK_LEN = 897; uint8 internal constant FALCON512_PK_HEADER = 0x09; /// @notice Signing-domain separators (adversarial-review HIGH, 2026-07-12). /// The message handed to the Falcon verifier for a fund-moving /// authorization (validateUserOp / executeWithFalcon) is /// `abi.encodePacked(domain, hash)`, i.e. 64 bytes, which can NEVER /// collide with the raw 32-byte message that isValidSignature (EIP-1271) /// verifies. Without this separation, a Falcon signature phished through /// an EIP-1271 "login" over a crafted 32-byte hash (set equal to an /// execute-challenge or a userOpHash, both computable from public state /// since execNonce is public) could be replayed as a spend authorization /// and drain the account. bytes32 internal constant USEROP_DOMAIN = keccak256("AerePQCAccount.validateUserOp.v1"); bytes32 internal constant EXEC_DOMAIN = keccak256("AerePQCAccount.executeWithFalcon.v1"); /// @notice Generic call struct (used by executeBatch). struct Call { address target; uint256 value; bytes data; } /// @notice ERC-4337 v0.7 PackedUserOperation shape (matches AereEntryPointV2). struct PackedUserOperation { address sender; uint256 nonce; bytes initCode; bytes callData; bytes32 accountGasLimits; uint256 preVerificationGas; bytes32 gasFees; bytes paymasterAndData; bytes signature; } event Initialized(address indexed entryPoint, address indexed falcon, uint256 pubKeyLen); event Executed(uint256 indexed nonce, address indexed target, uint256 value, bytes data); event ExecutedBatch(uint256 indexed nonce, uint256 count); error Unauthorized(); error AlreadyInitialized(); error NotInitialized(); error InvalidEntryPoint(); error InvalidFalconVerifier(); error InvalidFalconPubKey(); error InvalidSignature(); error CallFailed(bytes returnData); constructor() { factory = msg.sender; } /// @notice One-shot initializer, callable only by the deploying factory. /// @param _entryPoint the ERC-4337 EntryPoint the account will trust. /// @param _falcon the live AereFalcon512Verifier. /// @param _falconPubKey the 897-byte NIST Falcon-512 public key that owns this account. function initialize(address _entryPoint, address _falcon, bytes calldata _falconPubKey) external { if (msg.sender != factory) revert Unauthorized(); if (entryPoint != address(0)) revert AlreadyInitialized(); if (_entryPoint == address(0)) revert InvalidEntryPoint(); if (_falcon == address(0)) revert InvalidFalconVerifier(); if (_falconPubKey.length != FALCON512_PK_LEN || uint8(_falconPubKey[0]) != FALCON512_PK_HEADER) { revert InvalidFalconPubKey(); } entryPoint = _entryPoint; falcon = IFalcon512Verifier(_falcon); falconPubKey = _falconPubKey; emit Initialized(_entryPoint, _falcon, _falconPubKey.length); } // ─── ERC-4337 path ──────────────────────────────────────────────────────── /// @notice Called by the EntryPoint to validate a user operation. /// @dev Returns 0 on success and SIG_VALIDATION_FAILED (1) on a bad Falcon /// signature, per ERC-4337 (does not revert on bad signature). function validateUserOp( PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds ) external returns (uint256 validationData) { if (msg.sender != entryPoint) revert Unauthorized(); // Post-quantum authorization: the userOpHash must carry a valid Falcon-512 // signature under this account's owner key, verified fully on chain. The // signed message is domain-separated (USEROP_DOMAIN) so an EIP-1271 login // signature over a raw hash can never be replayed here. if (!_verifyFalconDomained(USEROP_DOMAIN, userOpHash, userOp.signature)) { return SIG_VALIDATION_FAILED; } // Pay the EntryPoint any prefund it needs (EntryPoint enforces the gas budget). if (missingAccountFunds > 0) { (bool sent, ) = payable(entryPoint).call{value: missingAccountFunds}(""); (sent); // ignore — EntryPoint reverts if underfunded } return 0; } /// @notice Called by the EntryPoint after validation to dispatch the user's call. function executeFromEntryPoint(address target, uint256 value, bytes calldata data) external { if (msg.sender != entryPoint) revert Unauthorized(); (bool ok, bytes memory ret) = target.call{value: value}(data); if (!ok) revert CallFailed(ret); } function executeBatchFromEntryPoint(Call[] calldata calls) external { if (msg.sender != entryPoint) revert Unauthorized(); for (uint256 i = 0; i < calls.length; i++) { (bool ok, bytes memory ret) = calls[i].target.call{value: calls[i].value}(calls[i].data); if (!ok) revert CallFailed(ret); } } // ─── Direct Falcon-authorized path (self-relay, no bundler required) ─────── /// @notice Hash the account owner's Falcon key must sign for an executeWithFalcon() call. function getExecuteChallenge( address target, uint256 value, bytes calldata data, uint256 expectedNonce ) public view returns (bytes32) { return keccak256(abi.encode(address(this), block.chainid, expectedNonce, target, value, data)); } /// @notice Execute a single call, authorized by a Falcon-512 signature over the /// current execute-challenge. Lets the account be driven directly (e.g. /// by a Foundation relayer) without routing through a 4337 bundler. function executeWithFalcon( bytes calldata signature, address target, uint256 value, bytes calldata data ) external returns (bytes memory result) { if (entryPoint == address(0)) revert NotInitialized(); bytes32 challenge = getExecuteChallenge(target, value, data, execNonce); // Domain-separated (EXEC_DOMAIN): a signature phished via the raw-hash // isValidSignature surface cannot authorize a spend here. if (!_verifyFalconDomained(EXEC_DOMAIN, challenge, signature)) revert InvalidSignature(); uint256 n = execNonce; execNonce = n + 1; (bool ok, bytes memory ret) = target.call{value: value}(data); if (!ok) revert CallFailed(ret); emit Executed(n, target, value, data); return ret; } // ─── EIP-1271 ───────────────────────────────────────────────────────────── /// @notice EIP-1271: returns the magic value iff `signature` is a valid /// Falcon-512 signature over `hash` under this account's owner key. /// A `view`, so integrators (Permit2, OpenSea, Snapshot, dapp logins) /// can verify a post-quantum signature for free via eth_call. function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) { return _verifyFalcon(hash, signature) ? EIP1271_MAGIC : bytes4(0); } // ─── Falcon verification (internal) ─────────────────────────────────────── /// @dev Decode the (nonce, compSig) envelope and run the full on-chain /// Falcon-512 verification of `hash` (as raw 32 bytes) via the live verifier. function _verifyFalcon(bytes32 hash, bytes calldata signature) internal view returns (bool) { (bytes memory nonce, bytes memory compSig) = abi.decode(signature, (bytes, bytes)); return falcon.verify(falconPubKey, abi.encodePacked(hash), nonce, compSig); } /// @dev Domain-separated Falcon verify for the fund-moving paths. The signed /// message is `abi.encodePacked(domain, hash)` (64 bytes), which can never /// equal the raw 32-byte message that isValidSignature verifies, so a /// signature obtained through the EIP-1271 login surface cannot be replayed /// as a validateUserOp or executeWithFalcon spend authorization. function _verifyFalconDomained(bytes32 domain, bytes32 hash, bytes calldata signature) internal view returns (bool) { (bytes memory nonce, bytes memory compSig) = abi.decode(signature, (bytes, bytes)); return falcon.verify(falconPubKey, abi.encodePacked(domain, hash), nonce, compSig); } receive() external payable {} }