SimpleAccountFactory
src/SimpleAccountFactory.sol deploys FORS-backed 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;
address public immutable ACCOUNT_IMPL; // = new SimpleAccount(ENTRY_POINT, VERIFIER)
function createAccount(bytes32 initialSignerRoot, uint256 salt) external returns (address);
function getAddress(bytes32 initialSignerRoot, uint256 salt) external view returns (address);
}Creating an account
function createAccount(bytes32 initialSignerRoot, uint256 salt) external returns (address accountAddr) {
bytes32 fullSalt = _salt(initialSignerRoot, 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);
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 CREATE2 salt is domain-separated via InitialSignerCommitment:
accountSalt(root, salt) = keccak256(abi.encode(
keccak256("NiceTryAccountSalt:v1(bytes32 initialSignerRoot,uint256 salt)"),
root, salt
));A zero root is rejected.
Multi-chain consistent addresses
The goal is one address per user across every chain, while still using a different first signer on each chain (first-signer reuse across chains would weaken the few-time guarantee).
The tension: CREATE2 fixes the address before deployment, so the inputs must be identical on every chain, but the first signer is not. The solution is to commit to all first signers in the salt instead of putting any single one in it. That commitment is a Merkle root.
Each supported chain contributes one leaf:
initialSignerLeaf(chainId, signer) = keccak256(abi.encode(
keccak256("NiceTryInitialSignerLeaf:v1(uint256 chainId,address signer)"),
chainId, signer
));Ethereum Sepolia -> signer A ┐
Base Sepolia -> signer B ├─ Merkle root (same on every chain)
Arbitrum Sepolia -> signer C ┘The root is identical everywhere because it is built from the same full list. The account address therefore depends on the root, not on any one chain's signer:
account address = f( factory address, account implementation, initialSignerRoot, user salt )At activation, the account rebuilds the leaf with block.chainid and verifies a Merkle proof, so on each chain only that chain's committed signer can activate, and a proof for one chain can't be replayed on another.
Deployment uniformity
Because the factory and implementation addresses are part of the calculation, they must also match across chains. The deploy script deploys the verifier and factory through the standard CREATE2 deployer (0x4e59…4956C) so the same bytecode, salts, and constructor args yield the same addresses everywhere. The invariant a wallet must verify before trusting a predicted address:
same verifier address
same factory address
same account implementation address
same initialSignerRoot
same user saltIf any of these drift, the account address drifts.
Deployment race
The first UserOp may carry deployment initCode (standard ERC-4337). If someone else deploys the account first, that's harmless: the account is still inactive and still holds the expected root. The original UserOp may fail (EntryPoint rejects initCode for an existing account); the wallet recovers by resubmitting activation with empty initCode. Because the resubmitted UserOp has a different hash, it needs a fresh activation signature, accepted under the FORS+C bounded-reuse policy for this specific race.
Full rationale: docs/multichain-consistent-addresses.md.