Skip to main content

Table of contents

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)
This design shifts control from the protocol to the account itself, enabling significantly more flexibility without changing core protocol rules.

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 DECLARE transaction, the sequencer validates the transaction by calling the __validate_declare__ function.
  • For an INVOKE transaction, 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_ACCOUNT transaction, the sequencer calls the constructor function with the transaction’s constructor_calldata as 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.
For more information on transaction types and lifecycle, see Transactions.

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)
    As of Starknet version 0.14.0, calling the deploy syscall from the __validate__, __validate_deploy__, __validate_declare__, and constructor functions is also not possible.
  • 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_info syscall:
    • sequencer_address is set to zero
    • block_timestamp returns the time (in UTC), rounded to the most recent hour
    • block_number returns the block number, rounded down to the nearest multiple of 100
These limitations are designed to prevent DoS attacks on the sequencer:
  • An attacker could cause the sequencer to perform a large amount of work before a transaction fails validation, for example by spamming INVOKE transactions whose __validate__ requires many steps but eventually fails, or by spamming DEPLOY_ACCOUNT transactions 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.
For the full list of transaction statuses and their meanings, see Transaction statuses.

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
Starknet currently determines the nonce structure at the protocol level to be sequential (i.e., the nonce of a transaction sent from an account is incremented by one after the transaction is executed). In the future, Starknet will consider a more flexible design, extending account abstraction to nonce abstraction. However, unlike Ethereum, only the nonce of account contracts — that is, those adhering to Starknet’s account structure — can be non-zero in Starknet. In contrast, in Ethereum, regular smart contracts can also increment their nonce by deploying contracts (i.e., executing the CREATE and CREATE2 opcodes).

Deploying a new account

New accounts can be deployed in the following ways:
  • Sending a DEPLOY_ACCOUNT transaction, which does not require a preexisting account.
  • Using the Universal Deployer Contract (UDC), which requires an existing account to send the INVOKE transaction.
Upon receiving one of these transactions, the sequencer performs the following steps:
  1. Runs the respective validation function in the contract, as follows:
    • When deploying with the DEPLOY_ACCOUNT transaction 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.
  2. Executes the constructor with the given arguments.
  3. 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.
  1. Sets the account’s nonce as follows:
    • 1, when deployed with a DEPLOY_ACCOUNT transaction
    • 0, when deployed with the UDC

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 an is_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.
SNIP-9 requires the user’s account to implement the 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.
With the removal of ETH fee payment in v0.14.0 (which made v3 transactions mandatory, with STRK as the only native fee token), paymasters became the primary mechanism for paying fees in other tokens. Paymaster services such as AVNU are available on both Mainnet and Sepolia.

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.
For the full version history of each transaction type, see Transaction types.

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:

Version history

The following table summarizes key account-related changes across Starknet versions: