Account contracts
Frost ships a small family of pre-built account contracts. There is no Solidity involved: each is hand-assembled EVM bytecode generated by the reference tooling, so the runtime is small, auditable, and byte-identical for every user of a given type.
Code IDs
Section titled “Code IDs”| Code ID | Type | Scheme | Precompile | Runtime size |
|---|---|---|---|---|
0x01 |
Fixed-key v1 | ML-DSA-44 | 0x15 |
~1,440 B |
0x02 |
Fixed-key v1 | ML-DSA-65 | 0x14 |
~2,080 B |
0x03 |
Rotatable v2 | ML-DSA-65 | 0x14 |
fixed, key in state |
0x04 |
Slim hash-commit v3 | ML-DSA-65 | 0x14 |
fixed, key committed by hash |
The slim hash-commit v3 account stores only keccak256 commitments to its
active and backup keys and carries the active public key as a witness with
every spend; an account that proves active can later promote itself in place to
a v2-style pointer contract. Its bytecode was frozen on 2026-08-11 and is
supported by the frametx harness and viem-8141. The rotatable v2 account
remains the default onboarding shape.
Address derivation
Section titled “Address derivation”initcode = [constructor | runtime | key material]salt = keccak256("frost/account/v1" ‖ ACCOUNT_CODE_ID ‖ uint64_le(index))address = keccak256(0xff ‖ 0x4e59b44847b379578588920cA78FbF26c0B4956C ‖ salt ‖ keccak256(initcode))[12:]The deployer is the EIP-7997 deterministic CREATE2 factory, a genesis predeploy
at 0x4e59b44847b379578588920cA78FbF26c0B4956C.
For the rotatable v2 account the key material is
activePk (1,952 B) ‖ keccak256(backupPk) (32 B).
For fixed-key accounts it is the public key alone.
index lets one keypair address multiple accounts. It defaults to 0.
The whole computation is offline. An account address is a mathematical fact about a public key, not a record of something that happened.
Fixed-key accounts (v1)
Section titled “Fixed-key accounts (v1)”The simplest working design. The runtime is
[verify stub | padding to offset 0x80 | public key], and the public key is
part of the code.
Verification is:
TXPARAM 0x08→ the sig-hash into memory.SIGPARAM 0x04→ copy the signature into memory.CODECOPYfrom offset0x80→ append the account’s own public key.STATICCALLthe verifier precompile.APPROVEif it returned 1, otherwise revert.
The key can never change. These accounts have no recovery path.
Rotatable account (v2)
Section titled “Rotatable account (v2)”The runtime is one fixed byte string for every account; per-account data lives in storage.
| Slot | Content |
|---|---|
| 0 | Address of a pointer contract whose code is 0x00 ‖ activePk (1,953 bytes; the leading 0x00 keeps the blob non-executable) |
| 1 | keccak256(backupPk) over the 1,952-byte encoding |
Storing the active key in a separate contract’s code rather than in storage
words makes reading it a single EXTCODECOPY instead of sixty-one SLOADs.
Dispatch
Section titled “Dispatch”The account routes on its caller:
| Condition | Behaviour |
|---|---|
CALLER == ADDRESS, non-empty calldata |
Rotate. Requires the exact 1,984-byte rotation shape, so a malformed rotation fails loudly. |
CALLER == ADDRESS, empty calldata |
STOP. A value transfer the account sends to itself is a plain transfer, not a rotation request. |
CALLER == 0x…aa (frame entry point) |
Verify. The mode is chosen by the VERIFY frame’s data, delivered as calldata and committed by the sig-hash. |
| anything else | STOP, so plain transfers fund the account. |
The VERIFY frame’s data selects the path: empty for a spend, 0x01 for a
rotation.
Rotation
Section titled “Rotation”frames: [0] VERIFY target: ∅ (self) flags: 3 gas: 30,000 data: 0x01 [1] SENDER target: ∅ (self) gas: 520,000 data = newActivePk ‖ keccak256(newBackupPk)
signatures: [0] {ARBITRARY, msg: ∅, sig = ML-DSA-65 by the BACKUP key} [1] {ARBITRARY, msg: ∅, sig = the backup public key blob}The public-key witness is elided from the sig-hash but authenticated against state: the account hashes it and compares against slot 1.
The rotate path re-verifies the backup authorisation itself, so a compromised active key can never rotate the account. Spending authority and recovery authority are genuinely separate.
Each rotation consumes the backup commitment, installs a new one, and deploys a fresh pointer contract. Slot 0 changes; the account address does not.
Nonce discipline
Section titled “Nonce discipline”An account’s nonce is its contract nonce, and it does not start where you would guess.
| Event | Nonce after |
|---|---|
| Counterfactual, not yet deployed | 0 |
| First send, rotatable v2 | 3 |
| First send, fixed-key v1 | 2 |
| Each subsequent spend | +1 |
| Each rotation | +2 |
The rotatable account reaches 3 because CREATE2 sets it to 1 per EIP-161, the constructor’s pointer deployment bumps it to 2, and payment approval bumps it to 3.
Verification cost
Section titled “Verification cost”The VERIFY frame runs comfortably inside a 30,000 gas frame limit and the 100,000 gas admission budget, for both the v2 spend and the rotation shape. That limit also covers the frame-entry access charge — 2,600 gas when the account is cold, 100 when warm — which is debited from the VERIFY frame’s gas before its code runs. The v3 slim spend, which hashes the witnessed key, needs a little more; the harness budgets 36,000. The dominant cost of a post-quantum transaction is calldata gas on the witness, not the verification.
Both VERIFY shapes are legal under the ERC-7562 validation rules enforced at the submission gate.
Paymaster
Section titled “Paymaster”A canonical paymaster bytecode exists for sponsored onboarding, but no instance is predeployed: each sponsor deploys its own copy with its own balance, owner, per-transaction limit and sender allowlist. It is deliberately narrow — it approves only nonce-0, factory-deployed, allowlisted first sends.
Both clients recognise instances by code hash in their mempool policy. A recognised paymaster may back up to a quarter of the frame pool’s slots (64 of 256) and up to 0.25 native tokens of committed in-flight cost; an unrecognised paymaster is limited to one pending transaction. These are admission limits, not consensus rules.
Most users never need it — a counterfactual account pays for its own deployment out of funds already sent to it. The paymaster exists for flows where a user should not have to acquire the native token first.
See also
Section titled “See also”- Accounts and keys — the concepts.
- Create a wallet — doing it.
- Frame opcodes — the instructions the verify stub uses.
- Counterfactual onboarding — why it works this way.