// 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)); } }