Table of contents
- Overview
- Starknet vs. Ethereum
- Account structure
- Account nonces
- Deploying a new account
- Recommended standards
- Legacy and deprecated
- Version history
Overview
An account is your identity on a blockchain. It holds your funds, tracks your activity, and authorizes every action you take — such as transferring tokens or interacting with applications. Without an account, you cannot interact with the network. On most blockchains (including Ethereum), accounts are simple: they are controlled by a private key, and transactions are valid if they are correctly signed by that key. While this model is straightforward, it is also rigid. The rules for authorization, fee payment, and replay protection are fixed at the protocol level and cannot be customized. Starknet takes a fundamentally different approach: account abstraction is built into the protocol. Every account on Starknet is a smart contract. Instead of the protocol enforcing a single definition of a “valid transaction,” each account defines its own validation logic. This allows accounts to implement custom behaviors such as:- Multi-signature authorization
- Social recovery mechanisms
- Alternative signature schemes (e.g. hardware-based signing)
- Flexible fee payment via third parties (paymasters)
Starknet vs. Ethereum
Ethereum is evolving toward account abstraction via ERC-4337, where accounts are managed through a dedicated smart contract. On Starknet, this model is native and mandatory — there are no key-pair EOAs.
Account structure
Starknet’s account structure is inspired by Ethereum’s EIP-4337, where smart contract accounts with arbitrary verification logic are used instead of EOAs. A valid account contract in Starknet is a contract that includes the following functions:In practice, use an audited account implementation such as OpenZeppelin, Argent, or Braavos. Writing a custom account contract is advanced work with significant security implications.
Two critical validations must happen in
__execute__, and their absence can lead to draining of the account’s funds:(1) assert!(get_caller_address().is_zero())This asserts that the account’s __execute__ is not called from another contract, thus skipping validations. As of Starknet v0.14.0, transactions with internal calls to an entry point named __execute__ are reverted at the protocol level. However, this check remains important for defense-in-depth during validation.(2) assert!(get_tx_info().unbox().version.into() >= 1_u32)This asserts that the transaction’s version is at least 1, blocking deprecated INVOKE v0 transactions. v0 transactions assume signature verification happens inside __execute__, skipping __validate__ entirely. This check also blocks invocations via the meta_tx_v0 syscall (introduced in v0.14.0 to preserve access to legacy accounts without __validate__), which presents the transaction version as 0 internally.Although Starknet v0.14.0 removed support for transaction versions 0, 1, and 2 at the protocol level (making v3 the only valid version), the canonical minimum version check in the OpenZeppelin reference implementation remains
>= 1. This is because simulation and query transactions use a large version offset (0x100000000000000000000000000000000 + version), so the check must accommodate both real and simulation transactions with a single lower bound rather than hardcoding >= 3.You can only use the
__validate_deploy__ function in an account contract to validate the DEPLOY_ACCOUNT transaction for that same contract. That is, this function runs at most once throughout the lifecycle of the account.Transaction flow
When the sequencer receives a transaction, it calls the corresponding function with the appropriate input from the transaction’s data, as follows:-
For a
DECLAREtransaction, the sequencer validates the transaction by calling the__validate_declare__function. -
For an
INVOKEtransaction, the sequencer calls the__validate__function with the transaction’s calldata as input, after being deserialized to the arguments in the__validate__function’s signature. After successfully completing validation, the sequencer calls the__execute__function with the same arguments. -
For a
DEPLOY_ACCOUNTtransaction, the sequencer calls theconstructorfunction with the transaction’sconstructor_calldataas input, after being deserialized to the arguments in the constructor signature. After the successful execution of the constructor, the sequencer validates the transaction by calling the__validate_deploy__function.
Separating the validation and execution stages guarantees payment to sequencers for work completed and protects them from DoS attacks.
Validation limitations
The logic of the__validate__, __validate_deploy__, __validate_declare__, and constructor functions can be mostly arbitrary, except for the following limitations:
For the
constructor function, limitations apply only when run in a DEPLOY_ACCOUNT transaction (in particular, not when an account is deployed from an existing class via the deploy syscall).- You cannot use more than 1,000,000 Cairo steps
- You cannot use more than 100,000,000 gas
-
You cannot call the following syscalls:
-
get_class_hash_at -
get_sequencer_address(this syscall is only supported for Cairo 0 contracts)
-
-
You cannot call functions in external contracts
This restriction enforces a single storage update being able to invalidate only transactions from a single account. However, be aware that an account can always invalidate its own past transactions by, for example, changing its public key. This limitation implies that the fees you need to pay to invalidate transactions in the mempool are directly proportional to the number of unique accounts whose transactions you want to invalidate.
-
When calling the
get_execution_infosyscall:-
sequencer_addressis set to zero -
block_timestampreturns the time (in UTC), rounded to the most recent hour -
block_numberreturns the block number, rounded down to the nearest multiple of 100
-
-
An attacker could cause the sequencer to perform a large amount of work before a transaction fails validation, for example by spamming
INVOKEtransactions whose__validate__requires many steps but eventually fails, or by spammingDEPLOY_ACCOUNTtransactions whose constructor or__validate_deploy__fails. Capping the number of steps keeps this work bounded. -
Even with a simple validation step, a “mempool pollution” attack is possible: an attacker fills the mempool with valid-looking transactions, then sends a single transaction that invalidates all of them (e.g. flipping a storage slot that
__validate__depends on). Restricting__validate__from calling external contracts prevents this, because it ensures that a single storage update can only invalidate transactions from the account that owns that storage.
Validation failures and statuses
When the__validate__, __validate_deploy__, or __validate_declare__ functions fail, the account does not pay any fee and the transaction is dropped from the mempool without being included in a block. When __execute__ fails, the transaction is included in a block as REVERTED and the sequencer charges a fee for work done up to the point of failure.
Changed in Starknet v0.14.0 (RPC v0.9): The
REJECTED transaction status has been removed. Transactions that fail validation are no longer surfaced as REJECTED; they are simply dropped.Account nonces
Similar to Ethereum, every contract in Starknet, including account contracts, has a nonce. The nonce of a transaction sent from an account must match the nonce of that account, which changes after the transaction is executed — even if it was reverted. Nonces serve two important roles:- They guarantee transaction hash uniqueness, which is important for a good user experience
- They provide replay protection to the account, by binding the signature to a particular nonce and preventing a malicious party from replaying the transaction
CREATE and CREATE2 opcodes).
Deploying a new account
New accounts can be deployed in the following ways:-
Sending a
DEPLOY_ACCOUNTtransaction, which does not require a preexisting account. -
Using the Universal Deployer Contract (UDC), which requires an existing account to send the
INVOKEtransaction.
-
Runs the respective validation function in the contract, as follows:
-
When deploying with the
DEPLOY_ACCOUNTtransaction type, the sequencer executes the__validate_deploy__function in the deployed contract. -
When deploying using the UDC, the sequencer executes the
__validate__function in the contract of the sender’s address.
-
When deploying with the
- Executes the constructor with the given arguments.
- Charges fees from the new account address.
If you use a
DEPLOY_ACCOUNT transaction, the fees are paid from the address of the deployed account. If you use the UDC, which requires an INVOKE transaction, the fees are paid from the sender’s account.-
Sets the account’s nonce as follows:
-
1, when deployed with aDEPLOY_ACCOUNTtransaction -
0, when deployed with the UDC
-
Recommended standards
SNIP-6
While not mandatory at the protocol level, SNIP-6 defines a richer standard interface for accounts. It was developed by community members at OpenZeppelin, in close collaboration with wallet teams and other core Starknet developers. SNIP-6 adds anis_valid_signature function on top of the protocol-required functions, enabling DApps and Sign-In-with-Starknet flows to verify off-chain signatures without submitting a transaction.
Recommended for compatibility with wallets, tools, and standards across the ecosystem.
SNIP-9: Outside Execution
SNIP-9, also known as outside execution or meta-transactions, allows a third party (such as a protocol or relayer) to submit a transaction on behalf of a user account. The user signs an “outside execution” object off-chain; the relayer then submits it on-chain, paying the gas fees on the user’s behalf. This enables several use cases:- Delayed or conditional orders: A user pre-signs a trade to execute at a future time or price.
- Fee sponsorship: A dApp or protocol pays gas fees on behalf of a user (the signing account doesn’t need to hold STRK at execution time).
- Gasless onboarding: New users can interact with Starknet before funding their account.
execute_from_outside function (as defined in the SNIP). Wallet support as of early 2025:
SNIP-29: Paymaster
SNIP-29 introduces a standard interface for paymaster services. A paymaster is a third-party service that pays gas fees on behalf of a user, allowing the user to pay in alternative tokens (such as ETH or USDC) or to have fees sponsored entirely by a dApp.Using a paymaster requires the account to be SNIP-9 compatible.
Reference implementation
To review a concrete implementation of all the above standards, see OpenZeppelin’s account component, which adheres to SNIP-6 and SNIP-9.Legacy and deprecated
Old transaction versions
Transaction versions 0, 1, and 2 were removed in Starknet v0.14.0. Only version 3 is valid. Sending a v0, v1, or v2 transaction will be rejected by the network.Legacy accounts
Older accounts do not implement__validate__ and relied on version 0 transactions for authorization. These accounts can still be accessed using the meta_tx_v0 syscall, introduced in v0.14.0. This syscall wraps the call in a v3 transaction but presents it to the called account as a v0 transaction internally — replacing the transaction hash, version, and caller address — so that the legacy account can verify its original v0-style signature.
Do not use meta_tx_v0 for new account implementations. It exists solely to preserve access to already-deployed legacy accounts.
Internal calls to __execute__
Disallowed at the protocol level as of Starknet v0.14.0.
Transactions that contain an internal call_contract syscall targeting an entry point named __execute__ are reverted. Despite this protocol-level enforcement, it remains best practice to keep the following check in your __execute__ implementation for defense-in-depth: