aere-contracts/contracts/passkey/AerePasskeyAccount.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

173 lines
6.3 KiB
Solidity

// 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 {}
}