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 |
|---|---|---|---|---|
{ACCOUNTS.codeIds.fixedMldsa44} |
Fixed-key v1 | ML-DSA-44 | 0x15 |
~1,440 B |
{ACCOUNTS.codeIds.fixedMldsa65} |
Fixed-key v1 | ML-DSA-65 | 0x14 |
~2,080 B |
{ACCOUNTS.codeIds.rotatableV2} |
Rotatable v2 | ML-DSA-65 | 0x14 |
fixed, key in state |
Address derivation
Section titled “Address derivation”initcode = [constructor | runtime | key material]salt = keccak256("{ACCOUNTS.saltDomain}" ‖ ACCOUNT_CODE_ID ‖ uint64_le(index))address = keccak256(0xff ‖ {ACCOUNTS.factory} ‖ salt ‖ keccak256(initcode))[12:]The deployer is the EIP-7997 deterministic CREATE2 factory, a genesis predeploy
at {ACCOUNTS.factory}.
For the rotatable v2 account the key material is
activePk ({MLDSA_SIZES.mldsa65.publicKey.toLocaleString('en-US')} 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 spend and the rotation shape. 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 contract exists for sponsored onboarding. It is instance-per-sponsor, recognised by code hash, and deliberately narrow: it approves only nonce-0, factory-deployed, allowlisted first sends.
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.