SimpleAccountFactory
src/SimpleAccountFactory.sol deploys accounts as EIP-1167 minimal proxies pointing at a single shared implementation. It deploys that implementation once in its own constructor, so a factory deployment uniquely determines the account bytecode it produces.
contract SimpleAccountFactory {
IEntryPoint public immutable ENTRY_POINT;
ISignatureVerifier public immutable VERIFIER; // ForsVerifier
ISphincsVerifier public immutable SPHINCS_VERIFIER; // SphincsVerifier
address public immutable ACCOUNT_IMPL; // = new SimpleAccount(ENTRY_POINT, VERIFIER, SPHINCS_VERIFIER)
function createAccount(bytes32 initialSignerRoot, bytes32 backupPkSeed, bytes32 backupPkRoot, uint256 salt)
external returns (address);
function getAddress(bytes32 initialSignerRoot, bytes32 backupPkSeed, bytes32 backupPkRoot, uint256 salt)
external view returns (address);
}Creating an account
function createAccount(bytes32 initialSignerRoot, bytes32 backupPkSeed, bytes32 backupPkRoot, uint256 salt)
external returns (address accountAddr)
{
bytes32 fullSalt = _salt(initialSignerRoot, backupPkSeed, backupPkRoot, salt);
address predicted = LibClone.predictDeterministicAddress(ACCOUNT_IMPL, fullSalt, address(this));
if (predicted.code.length > 0) return predicted; // idempotent
accountAddr = LibClone.cloneDeterministic(ACCOUNT_IMPL, fullSalt);
SimpleAccount(payable(accountAddr)).initialize(initialSignerRoot, backupPkSeed, backupPkRoot);
emit AccountCreated(accountAddr, initialSignerRoot, salt);
}createAccount is idempotent: if the predicted address already has code it returns it instead of redeploying, which is what makes the standard ERC-4337 initCode flow safe. getAddress returns the same prediction without deploying, so a wallet can know the account address before it exists onchain.
The three inputs besides the user salt:
| Input | Meaning | Where it comes from |
|---|---|---|
initialSignerRoot | Merkle root over (chainId, firstSigner) leaves for every committed chain | Building the initial-signer tree |
backupPkSeed, backupPkRoot | the SPHINCS backup public key, each 16 bytes left-aligned in a bytes32 | SPHINCS backup |
salt | user-chosen uint256, lets one user own several accounts | wallet |
A zero root or a zero backup key is rejected in _salt, and a non-canonical backup key is rejected by initialize.
The salt
The CREATE2 salt is domain-separated via InitialSignerCommitment and commits to both the root and the backup key:
backupLeaf = keccak256(abi.encode(
keccak256("NiceTryBackupSignerLeaf:v1(bytes32 pkSeed,bytes32 pkRoot)"),
backupPkSeed, backupPkRoot
));
fullSalt = keccak256(abi.encode(
keccak256("NiceTryAccountSalt:v2(bytes32 initialSignerRoot,bytes32 backupSignerLeaf,uint256 salt)"),
initialSignerRoot, backupLeaf, salt
));Binding the backup key into the address is what makes the recovery key front-run-safe: a deploy-race attacker who calls createAccount with a different backup key lands at a different address, so the user's counterfactual address can only ever be initialised with the user's own backup key.
Address stability
Because the factory, the implementation, the verifiers, the EntryPoint and the compiler settings are all inputs to the address, they must be identical on every chain. The deploy script guarantees that through the standard CREATE2 deployer and fixed salts. The full invariant, the initial-signer tree, and the deployment-race handling are on the Multichain addresses page.
Events
| Event | Emitted when |
|---|---|
AccountCreated(account, initialSignerRoot, salt) | a new clone is deployed and initialised (not on the idempotent early return) |