// SPDX-License-Identifier: MIT pragma solidity 0.8.23; import "@openzeppelin/contracts/access/Ownable.sol"; import {PQCGaslessOrder, IAerePQCOrderAuthorizer} from "../intents/IAerePQCOrder.sol"; /** * @title AerePQCOrderAuthorizer — a crypto-agility CONSUMER that authorizes ERC-7683 * cross-chain intents with post-quantum signatures (chain 2800) * * @notice This is the "PQC-order authorizer" for the AERE intent path. It turns a * declarative ERC-7683 order plus a NIST post-quantum signature (Falcon-512/ * 1024, ML-DSA-44, SLH-DSA-128s) into an on-chain accept/reject, verified by * AERE's LIVE native PQC precompiles behind the governed AereCryptoRegistry * (0xaE6fC596bb3eCcbf5c5D02D67B0Ef065b3Afbaa5). * * Like {AereHybridAuthorizer}, it never pins a scheme. Every check first calls * {AereCryptoRegistry.resolveActive} to follow the successor chain to the live * ACTIVE row, then verifies through the registry's routed verifier. So when the * Foundation retires a scheme on the REGISTRY with the standard swap * * 1. addAlgorithm(newScheme) // register the successor, ACTIVE * 2. setSuccessor(oldId, newId) // point old -> new * 3. setStatus(oldId, DEPRECATED|REVOKED) * * every settler that routes orders through this authorizer starts accepting the * NEW scheme and rejecting the OLD one on the very next block, with NO redeploy * of this authorizer or of any consuming SpokePool. * * @dev ORDER DIGEST. The 32-byte message the PQC key signs is a standard EIP-712 * digest, domain-separated to the CONSUMING SETTLER (its address is the * verifyingContract), so one authorizer serves many pools and a signature for * pool A never authorizes an order at pool B. The struct hash commits to the * funder, the replay nonce, the chain, both deadlines, the requested algorithm * id, the order-data type and payload hash, and the authorized public key. A * solver's fill therefore settles only against an order a lattice/hash key * actually authorized. * * @dev HONEST SCOPE. Application-layer authorization only; it holds no funds (no * payable function, no custody, no selfdestruct). It changes NOTHING about AERE * consensus: blocks are still produced and signed by Besu QBFT validators with * classical ECDSA / secp256k1. All post-quantum verification is delegated to the * SAME live native precompiles the registry routes to. This is NOT post-quantum * consensus. The token custody and ERC-7683 order lifecycle live in the settler * (AereSpokePool); this contract only answers "is this order PQC-authorized?". * * @dev POLICY. Only an ACTIVE signature scheme ever authorizes. {resolveActive} * returns the first ACTIVE row on the successor chain, so a REVOKED id is never * used (it is skipped for its ACTIVE successor, or, with no successor, the * fail-closed check returns false); a DEPRECATED id with an ACTIVE successor * routes to the successor; and a hash-only entry (e.g. SHAKE256) authorizes * nothing because {AereCryptoRegistry.verify} refuses to route it. */ interface IAereCryptoRegistry { /// @notice Follow the successor chain to the first ACTIVE id. Reverts on unknown id, /// a dead-end with no ACTIVE row, or a cycle. function resolveActive(uint256 algorithmId) external view returns (uint256); /// @notice Fail-closed routed signature verification (returns false, never reverts, /// for unknown/hash-only/UNKNOWN/REVOKED/malformed inputs). function verify(uint256 algorithmId, bytes calldata pubKey, bytes32 messageHash, bytes calldata signature) external view returns (bool); /// @notice True iff `id` is a signature scheme safe to adopt for NEW work (ACTIVE). function isUsable(uint256 id) external view returns (bool); } contract AerePQCOrderAuthorizer is Ownable, IAerePQCOrderAuthorizer { // ========================================================================== // Immutable wiring // ========================================================================== /// @notice The governed crypto-agility registry this authorizer consumes. IAereCryptoRegistry public immutable registry; // ========================================================================== // EIP-712 digest constants // ========================================================================== bytes32 private constant EIP712_DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); bytes32 private constant DOMAIN_NAME = keccak256("AerePQCOrder"); bytes32 private constant DOMAIN_VERSION = keccak256("1"); /// @notice The order struct typehash. `orderDataHash` = keccak256(orderData) and /// `pubKeyHash` = keccak256(pubKey) keep the digest fixed-size while binding /// the full payload and the authorized key. bytes32 public constant PQC_ORDER_TYPEHASH = keccak256( "AerePQCOrder(address user,uint256 nonce,uint256 originChainId,uint32 openDeadline,uint32 fillDeadline,uint256 algorithmId,bytes32 orderDataType,bytes32 orderDataHash,bytes32 pubKeyHash)" ); // ========================================================================== // Storage // ========================================================================== /** * @notice The governed DEFAULT algorithm id used when an order sets `algorithmId == 0`. * Owner-governed. It is re-resolved through {resolveActive} on every use, so * it auto-follows a registry swap even before {migrate} crystallizes it. */ uint256 public defaultAlgorithmId; // Authorization ledger (state-changing provenance surface; holds no value). /// @notice Number of successful {authorizeOrder} calls recorded. uint256 public authorizationCount; /// @notice The ACTIVE algorithm id used by the most recent successful authorization. uint256 public lastResolvedId; /// @notice The order digest of the most recent successful authorization. bytes32 public lastOrderDigest; // ========================================================================== // Events // ========================================================================== event DefaultAlgorithmSet(uint256 indexed id); event DefaultMigrated(uint256 indexed oldId, uint256 indexed newId); event OrderAuthorized( address indexed settler, address indexed user, uint256 indexed resolvedId, bytes32 orderDigest, uint256 nonce ); // ========================================================================== // Errors // ========================================================================== error ZeroRegistry(); error UnusableAlgorithm(uint256 id); // default id does not resolve to a usable ACTIVE signature scheme error NotCurrentDefault(uint256 got, uint256 current); // migrate(oldId) with oldId != defaultAlgorithmId error OrderAuthorizationFailed(uint256 requestedId, uint256 resolvedId); // ========================================================================== // Constructor // ========================================================================== /** * @param registry_ the AereCryptoRegistry to route through (non-zero). * @param defaultAlgorithmId_ the initial governed default for algorithmId == 0 orders. * It MUST resolve, via the registry, to a usable ACTIVE * signature scheme now (fail-fast "safe default"), so the * contract can never be born pointing at an unusable or * hash-only default. */ constructor(address registry_, uint256 defaultAlgorithmId_) { if (registry_ == address(0)) revert ZeroRegistry(); registry = IAereCryptoRegistry(registry_); if (!_resolvesToUsable(defaultAlgorithmId_)) revert UnusableAlgorithm(defaultAlgorithmId_); defaultAlgorithmId = defaultAlgorithmId_; emit DefaultAlgorithmSet(defaultAlgorithmId_); } // ========================================================================== // Owner: default governance // ========================================================================== /** * @notice Set the governed default algorithm id (used for algorithmId == 0 orders). * Owner only. The id must resolve to a usable ACTIVE signature scheme. */ function setDefaultAlgorithm(uint256 id) external onlyOwner { if (!_resolvesToUsable(id)) revert UnusableAlgorithm(id); defaultAlgorithmId = id; emit DefaultAlgorithmSet(id); } /** * @notice Owner helper: crystallize the default onto the ACTIVE successor of `oldId` * after a registry swap. `oldId` must equal the current {defaultAlgorithmId}. * Because every check already re-resolves, this is an optimization/clarity * step, not a correctness requirement. Reverts (from {resolveActive}) if the * chain dead-ends or cycles. * @return newId the ACTIVE id the default now points at (== oldId if already ACTIVE). */ function migrate(uint256 oldId) external onlyOwner returns (uint256 newId) { if (defaultAlgorithmId != oldId) revert NotCurrentDefault(oldId, defaultAlgorithmId); newId = registry.resolveActive(oldId); if (newId != oldId) { defaultAlgorithmId = newId; emit DefaultMigrated(oldId, newId); } } // ========================================================================== // Order digest // ========================================================================== /// @inheritdoc IAerePQCOrderAuthorizer function orderDigest(address settler, PQCGaslessOrder calldata order, bytes calldata pubKey) public view override returns (bytes32) { bytes32 structHash = keccak256( abi.encode( PQC_ORDER_TYPEHASH, order.user, order.nonce, order.originChainId, order.openDeadline, order.fillDeadline, order.algorithmId, order.orderDataType, keccak256(order.orderData), keccak256(pubKey) ) ); return keccak256(abi.encodePacked("\x19\x01", _domainSeparator(settler), structHash)); } // ========================================================================== // Authorization (fail-closed view) // ========================================================================== /// @inheritdoc IAerePQCOrderAuthorizer function checkOrder(address settler, PQCGaslessOrder calldata order, bytes calldata pubKey, bytes calldata signature) public view override returns (bool) { uint256 requestedId = order.algorithmId == 0 ? defaultAlgorithmId : order.algorithmId; uint256 active; try registry.resolveActive(requestedId) returns (uint256 a) { active = a; } catch { return false; // unknown id, dead-end, or cycle -> not authorized } bytes32 digest = orderDigest(settler, order, pubKey); try registry.verify(active, pubKey, digest, signature) returns (bool ok) { return ok; } catch { return false; } } // ========================================================================== // Authorization (state-changing, recorded) // ========================================================================== /** * @notice State-changing authorization for a PQC order under the ACTIVE scheme * reachable from `order.algorithmId` (0 => governed default). Reverts if the * successor chain has no ACTIVE row (surfacing the registry reason) or * {OrderAuthorizationFailed} if the routed verifier returns false. On success * it bumps {authorizationCount} and emits {OrderAuthorized}. This is a * provenance surface for standalone use; the consuming settler verifies with * the gas-light fail-closed {checkOrder} view during open(). * @return resolvedId the ACTIVE id the signature was verified under. */ function authorizeOrder( address settler, PQCGaslessOrder calldata order, bytes calldata pubKey, bytes calldata signature ) external returns (uint256 resolvedId) { uint256 requestedId = order.algorithmId == 0 ? defaultAlgorithmId : order.algorithmId; resolvedId = registry.resolveActive(requestedId); // reverts on unknown/dead-end/cycle bytes32 digest = orderDigest(settler, order, pubKey); bool ok = registry.verify(resolvedId, pubKey, digest, signature); if (!ok) revert OrderAuthorizationFailed(requestedId, resolvedId); authorizationCount += 1; lastResolvedId = resolvedId; lastOrderDigest = digest; emit OrderAuthorized(settler, order.user, resolvedId, digest, order.nonce); } // ========================================================================== // Internal // ========================================================================== /// @dev The EIP-712 domain separator bound to `settler` as the verifyingContract, so /// a PQC order authorized for one pool cannot authorize an order at another. function _domainSeparator(address settler) internal view returns (bytes32) { return keccak256(abi.encode(EIP712_DOMAIN_TYPEHASH, DOMAIN_NAME, DOMAIN_VERSION, block.chainid, settler)); } /// @dev True iff `id` resolves (via the successor chain) to a usable ACTIVE signature /// scheme. Fail-closed: a revert in resolveActive is treated as "not usable". function _resolvesToUsable(uint256 id) internal view returns (bool) { try registry.resolveActive(id) returns (uint256 active) { return registry.isUsable(active); } catch { return false; } } }