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.
173 lines
6.3 KiB
Solidity
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 {}
|
|
}
|