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.
62 lines
3.0 KiB
Solidity
62 lines
3.0 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
import {AerePasskeyAccountV2} from "./AerePasskeyAccountV2.sol";
|
|
|
|
/// @title AerePasskeyAccountFactoryV2 — CREATE2 factory for V2 passkey accounts.
|
|
/// @notice Upgrade over V1:
|
|
/// - Deploys V2 accounts (MultiOwnable + ERC-4337 + EIP-1271).
|
|
/// - Accepts a list of initial owners (≥1 — at least one passkey to bootstrap).
|
|
/// - Address derivation includes the initial owner set + salt — same passkey
|
|
/// with different initial-owner-set produces a different counterfactual address,
|
|
/// preventing pre-deploy front-running of multi-owner accounts.
|
|
contract AerePasskeyAccountFactoryV2 {
|
|
bytes32 public constant ACCOUNT_INIT_CODE_HASH = keccak256(type(AerePasskeyAccountV2).creationCode);
|
|
|
|
/// @notice The default EntryPoint that newly created accounts trust.
|
|
/// Hardcoded at factory deployment time so all accounts share the same EntryPoint.
|
|
address public immutable defaultEntryPoint;
|
|
|
|
event AccountCreated(address indexed account, address indexed entryPoint, bytes[] owners, uint256 salt);
|
|
|
|
/// @notice Deploying an account with an empty owner set would brick it: no
|
|
/// key could ever authorize a call, so any funds at its
|
|
/// counterfactual address would be permanently locked.
|
|
error NoOwners();
|
|
|
|
constructor(address _defaultEntryPoint) {
|
|
require(_defaultEntryPoint != address(0), "factory: entryPoint=0");
|
|
defaultEntryPoint = _defaultEntryPoint;
|
|
}
|
|
|
|
/// @notice Deterministic address from (initialOwners, salt). Same inputs → same address.
|
|
function predictAddress(bytes[] calldata initialOwners, uint256 salt) public view returns (address) {
|
|
bytes32 saltMix = _saltMix(initialOwners, salt);
|
|
return address(uint160(uint256(keccak256(abi.encodePacked(
|
|
bytes1(0xff), address(this), saltMix, ACCOUNT_INIT_CODE_HASH
|
|
)))));
|
|
}
|
|
|
|
/// @notice Idempotent: if already deployed, returns existing; else deploys + initialises.
|
|
function createAccount(bytes[] calldata initialOwners, uint256 salt)
|
|
external returns (AerePasskeyAccountV2 account)
|
|
{
|
|
// An owner-less account is a permanent brick (see NoOwners). The factory
|
|
// NatSpec promises >= 1 initial owner; enforce it before doing anything.
|
|
if (initialOwners.length == 0) revert NoOwners();
|
|
address predicted = predictAddress(initialOwners, salt);
|
|
if (predicted.code.length > 0) {
|
|
return AerePasskeyAccountV2(payable(predicted));
|
|
}
|
|
bytes32 saltMix = _saltMix(initialOwners, salt);
|
|
account = new AerePasskeyAccountV2{salt: saltMix}();
|
|
account.initialize(defaultEntryPoint, initialOwners);
|
|
require(address(account) == predicted, "factory: address mismatch");
|
|
emit AccountCreated(address(account), defaultEntryPoint, initialOwners, salt);
|
|
}
|
|
|
|
function _saltMix(bytes[] calldata initialOwners, uint256 salt) internal pure returns (bytes32) {
|
|
return keccak256(abi.encode(initialOwners, salt));
|
|
}
|
|
}
|