// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {WebAuthn} from "./WebAuthn.sol"; /// @title AerePasskeyAccount — passkey-controlled smart account /// @notice An on-chain account whose only signer is a WebAuthn passkey (Touch ID, Face ID, /// Windows Hello, YubiKey, EU Digital Identity Wallet, Android biometric). /// Verification runs through the Fusaka RIP-7951 secp256r1 precompile at address /// 0x100 for ~3,450 gas — vs ~250,000 gas for a pure-Solidity P-256 verifier. /// /// The account exposes a single public entry point: `execute()`. To call it, the /// caller must supply a WebAuthn assertion whose challenge is the hash of the /// call parameters (target, value, data, nonce, chainId). Anyone may be the /// msg.sender — typically a Foundation-funded relayer or any third party. The /// account itself enforces auth via the passkey signature. /// /// Counterfactual: the account address is fully deterministic from /// (factoryAddress, pubkey, salt) via CREATE2 — so dApps can compute and /// show the user's address BEFORE the account is deployed, then fund it / /// interact with it under the assumption it will exist. contract AerePasskeyAccount { /// @notice The secp256r1 (P-256) public key bound to this account. Immutable once set. uint256 public publicKeyX; uint256 public publicKeyY; /// @notice Replay-protection counter. Each successful execute() increments. uint256 public nonce; /// @notice The factory that deployed this account. Trusted only for one purpose: /// a constructor-time initialization can occur via the factory's clone proxy. address public immutable factory; event Executed(uint256 indexed nonce, address indexed target, uint256 value, bytes data); event ExecutedBatch(uint256 indexed nonce, uint256 count); error InvalidSignature(); error CallFailed(bytes returnData); error NotInitialized(); error AlreadyInitialized(); constructor() { factory = msg.sender; // pub key not set here — initialised via initialize() by the factory in the same tx. } /// @notice Initializes the account with its passkey owner. Callable only once, /// only by the factory, in the deploy transaction. function initialize(uint256 _x, uint256 _y) external { if (msg.sender != factory) revert AlreadyInitialized(); if (publicKeyX != 0 || publicKeyY != 0) revert AlreadyInitialized(); publicKeyX = _x; publicKeyY = _y; } /// @notice Hash that the user's passkey must sign to authorize `execute()`. /// @dev The challenge passed to navigator.credentials.get() must be this exact bytes32. 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 an arbitrary call from this account, authorized by a passkey signature. /// @param auth The WebAuthn assertion from the user's authenticator. /// @param target Destination contract / EOA. /// @param value AERE to send (wei). /// @param data Calldata for the target. function execute( WebAuthn.WebAuthnAuth calldata auth, address target, uint256 value, bytes calldata data ) external returns (bytes memory result) { if (publicKeyX == 0 && publicKeyY == 0) revert NotInitialized(); bytes32 challenge = getExecuteChallenge(target, value, data, nonce); // Verify passkey signature over the challenge. // Require User Verified (UV) — Touch ID / Face ID etc., not just touch. bool ok = WebAuthn.verify( abi.encodePacked(challenge), true, // requireUV _toMem(auth), publicKeyX, publicKeyY ); if (!ok) revert InvalidSignature(); uint256 n = nonce; nonce = n + 1; (bool callOk, bytes memory ret) = target.call{value: value}(data); if (!callOk) revert CallFailed(ret); emit Executed(n, target, value, data); return ret; } /// @notice Execute a batch of calls under a single passkey assertion. /// @dev Challenge for a batch = keccak256(this, chainid, nonce, abi.encode(Call[])). struct Call { address target; uint256 value; bytes data; } function getExecuteBatchChallenge( Call[] calldata calls, uint256 expectedNonce ) public view returns (bytes32) { return keccak256(abi.encode( address(this), block.chainid, expectedNonce, keccak256(abi.encode(calls)) )); } function executeBatch( WebAuthn.WebAuthnAuth calldata auth, Call[] calldata calls ) external returns (bytes[] memory results) { if (publicKeyX == 0 && publicKeyY == 0) revert NotInitialized(); bytes32 challenge = getExecuteBatchChallenge(calls, nonce); bool ok = WebAuthn.verify( abi.encodePacked(challenge), true, _toMem(auth), publicKeyX, publicKeyY ); if (!ok) revert InvalidSignature(); uint256 n = nonce; nonce = n + 1; results = new bytes[](calls.length); for (uint256 i = 0; i < calls.length; i++) { (bool callOk, bytes memory ret) = calls[i].target.call{value: calls[i].value}(calls[i].data); if (!callOk) revert CallFailed(ret); results[i] = ret; } emit ExecutedBatch(n, calls.length); } /// @dev Calldata → memory adapter for WebAuthn.verify which takes memory struct. function _toMem(WebAuthn.WebAuthnAuth calldata auth) private pure returns (WebAuthn.WebAuthnAuth memory m) { m.authenticatorData = auth.authenticatorData; m.clientDataJSON = auth.clientDataJSON; m.challengeIndex = auth.challengeIndex; m.typeIndex = auth.typeIndex; m.r = auth.r; m.s = auth.s; } /// @notice Receive AERE — accounts can hold native balance. receive() external payable {} }