Counterfactual onboarding
Smart-contract wallets have a chicken-and-egg problem. The wallet is a contract; deploying a contract costs gas; paying gas requires funds; and funds have to go somewhere. On most chains this is solved with a sponsor — a bundler, a relayer, or a paymaster — which means a new user’s first experience depends on someone else’s infrastructure and someone else’s key.
Frost removes the loop entirely. An account’s address is a pure function of its public keys, so it exists as a mathematical fact before it exists as state, and its first outgoing transaction deploys it using funds already sent to that address.
How the address exists first
Section titled “How the address exists first”initcode = [constructor | runtime | key material]salt = keccak256("frost/account/v1" ‖ ACCOUNT_CODE_ID ‖ uint64_le(index))address = keccak256(0xff ‖ FACTORY ‖ salt ‖ keccak256(initcode))[12:]Everything on the right-hand side is known the moment the keypair exists. No transaction, no network access, no gas. A wallet can display a receiving address in the same instant it generates a key.
Balances in the EVM live in the state trie keyed by address; they do not require
code. So funds sent to a counterfactual address simply sit there, exactly as
they would at any other address, and eth_getCode returns 0x until the
account materialises.
The self-paying first send
Section titled “The self-paying first send”The first transaction from the account carries a DEPLOY frame. In one transaction it:
- deploys the account through the CREATE2 factory,
- writes its key material into the account’s storage,
- performs whatever the user actually asked for,
and pays for all of it out of the balance already at the address.
There is no separate “activate your wallet” step, no sponsor, and no moment where the user must hold some other asset to get started. Receiving funds is sufficient to be able to spend them.
Why this is possible here
Section titled “Why this is possible here”Two features of the frame transaction format make it work, and neither exists in a standard Ethereum transaction.
The sender is explicit. A frame transaction names its sender as a field rather than recovering it from a signature. There is no requirement that the sender have code yet, so a transaction can name an account that is about to exist.
Payment and execution are separately authorised. A frame can approve payment without approving execution, and vice versa. That lets the deployment be paid for by the account being deployed, in the same transaction that deploys it.
The consequence that bites: nonces
Section titled “The consequence that bites: nonces”Materialising an account moves its nonce further than you would expect. For the rotatable v2 account:
| Step | Nonce |
|---|---|
| CREATE2 deployment (EIP-161) | 1 |
| Constructor deploys the key-pointer contract | 2 |
| Payment approval | 3 |
So a freshly materialised account is at 3, not 1. A fixed-key account lands at 2, and each later rotation adds 2.
Any wallet that pipelines transactions by counting locally will desynchronise
the first time it rotates. Read eth_getTransactionCount.
What has to be frozen
Section titled “What has to be frozen”Counterfactual addressing has a hard requirement: the bytecode behind a code ID can never change. If it did, every not-yet-deployed account of that type would re-derive to a different address, and funds already sent to the old address would be unreachable — no upgrade, no migration, no recovery.
So a bytecode change is definitionally a new code ID, never an edit to an existing one, and the code hashes are pinned by tests. This is a genuine constraint on the design’s evolution, accepted deliberately: an account type is a permanent commitment.
Ordering requirement
Section titled “Ordering requirement”Admission checks the payer’s balance against head state, so the funding transaction must be mined before the first send is submitted — not merely accepted into consensus. Submitting both in the same instant will fail the second one.
This is the one piece of sequencing a wallet has to get right, and it is why both SDKs’ lifecycle examples wait for the funding receipt before proceeding.
When you would still want a paymaster
Section titled “When you would still want a paymaster”Counterfactual onboarding handles “the user has funds at their address”. It does not handle “the user has no native token at all” — someone who has been sent only an ERC-20, for instance, still needs gas.
For that, a canonical paymaster exists. It is deliberately narrow: instance per sponsor, recognised by code hash, and it approves only nonce-0, factory-deployed first sends from an allowlist. It is a targeted onboarding tool rather than a general sponsorship system, because a general one is a much larger attack surface.
See also
Section titled “See also”- Create a wallet — the flow in practice.
- Account contracts — derivation and storage layout.
- Keys in accounts, not a registry — the choice that makes offline address derivation possible.