Gas and transaction fees

This section describes fees that are paid on L2 starting in Starknet 0.13.0. For information about messaging fees that are paid on L1, see L1 → L2 message fees.

Overall transaction fee

Starting with Starknet v0.13.1, Starknet distinguishes between blocks whose state diffs are sent to L1 as calldata and blocks whose state diffs are sent to L1 as blobs. The l1_da_mode property in the Starknet block header contains this information. The cost of computation remains the same on both options, but the cost related to data availability differs.

Overall transaction fee with blobs

This section shows the formula for determining a transaction’s fee. The following sections describe how this formula was derived.

The following formula describes the overall fee, \(F\), for a transaction:

\[\begin{align} F = \; & gas\_price \cdot \Bigg(\max_k v_k w_k + \\ & \quad + \; \text{message_calldata_cost}\cdot 3t + (\text{message_calldata_cost} + \text{l1_log_data_cost})\cdot \sum\limits_{i=1}^t q_i \; + \\ & \quad + \; \left(\text{l1_storage_write_cost}+\text{log_message_to_l1_cost}\right)\cdot t + \; \\ & \quad + \; \text{l2_payload_costs}\Bigg) + \\ & data\_gas\_price\cdot\text{felt_size_in_bytes}\cdot\bigg(2(n-1)+2(m-1) + \ell +2D \bigg) \end{align}\]

where:

  • \(v\) is a vector that represents resource usage, where each of its entries, \(v_k\), corresponds to different resource types: Cairo steps and number of applications of each builtin.

    For more information see Calculation of computation costs.

  • \(w\) is the CairoResourceFeeWeights vector.

  • \(n\) is the number of unique contracts updated, which also includes changes to classes of existing contracts and contract deployments, even if the storage of the newly deployed contract is untouched. In other words, \(n\ge\ell\). Notice that \(n\ge 1\) always holds, because the fee token contract is always updated, which does not incur any fee.

  • \(m\) is the number of values updated, not counting multiple updates for the same key. Notice that \(m\ge 1\) always holds, because the sequencer’s balance is always updated, which does not incur any fee.

  • \(t\) is the number of L2→L1 messages sent, where the corresponding payload sizes are denoted by \(q_1,...,q_t\).

  • \(\ell\) is the number of contracts whose class was changed, which happens on contract deployment and when applying the replace_class syscall.

  • \(D\) is 1 if the transaction is of type DECLARE and 0 otherwise. Declare transactions need to post on L1 the new class hash and compiled class hash which are added to the state.

  • L2→L1 messaging constants:

    • \(\text{message_calldata_cost}\) is 1124 gas per 32-byte word.

    • \(\text{l1_log_data_cost}\) is 256 gas.

    • \(\text{l1_storage_write_cost}\) is the cost of writing to a new storage slot on Ethereum, which is 20,000 gas.

    • \(\text{log_message_to_l1_cost}\) is 1637 gas.

    For more information, see Onchain data: L2→L1 messages.

  • \(\text{l2_payload_costs}\) is the gas cost of data sent over L2. This includes calldata, code, and event emission. For more details see L2 payloads: calldata, events, and code.

  • \(\text{felt_size_in_bytes}\) is 32, which is the number of bytes required to encode a single STARK field element.

Overall transaction fee with calldata

This section shows the formula for determining a transaction’s fee. The following sections describe how this formula was derived.

The following formula describes the overall fee, \(F\), for a transaction:

\[\begin{align} F = gas\_price\cdot&\Bigg(\max_k v_k w_k + \\ & + \; \text{da_calldata_cost}\left(2(n-1)+2(m-1) + \ell + 2D + 3t + \sum\limits_{i=1}^t q_i\right)\\ & - \; \text{contract_update_discount}\cdot (n-1) - 240 \\ & + \; \text{message_calldata_cost}\cdot 3t + (\text{message_calldata_cost} + \text{l1_log_data_cost})\cdot\sum\limits_{i=1}^t q_i \\ & + \; \left(\text{l1_storage_write_cost}+\text{log_message_to_l1_cost}\right)\cdot t \\ & + \; \text{l2_payload_costs}\Bigg) \end{align}\]

where:

  • The following constants are defined in the same manner as in the blob-based formula:

    • \(v, w, n, m, t, \ell, D\)

    • \(\text{message_calldata_cost}, \; \text{l1_log_data_cost}, \; \text{log_message_to_l1_cost}, \; \text{l1_storage_write_cost}\)

    • \(\text{l2_payload_costs}\)

  • \(\text{da_calldata_cost}\) is 551 gas per 32-byte word. This cost is derived as follows:

    • 512 gas per 32-byte word for calldata.

    • ~100 gas for onchain hashing that happens for every sent word.

    • a 10% discount, because the sequencer does not incur additional costs for repeated updates to the same storage slot within a single block.

  • \(240\) is the gas discount for updating the sender’s balance, for the derivation of this number see Onchain data: Storage updates.

  • \(\text{contract_update_discount}\) is 312 gas, for the derivation of this discount see Onchain data: Storage updates.

When is the fee charged?

The fee is charged atomically with the transaction execution on L2. The Starknet OS injects a transfer of the fee-related ERC-20, with an amount equal to the fee paid, the sender equal to the transaction submitter, and the sequencer as a receiver.

Transaction Fee limits

v3 transactions

With v3 transactions, users specify the max amount and max price for each resource. At the time of writing, the only available resource is L1 gas. In the future, we will introduce L2 gas which will be used to price L2 work (as opposed to only charging for the proof verification in L1 gas, which is what happens today).

Deprecated transactions (version < 3)

With older transaction versions, users specify the maximum fee that they are willing to pay for a transaction.

The only limitation on the sequencer, which is enforced by the Starknet OS, is that the actual fee charged is bounded by max_fee. While not enforced in the proof, the Starknet sequencer usually charges less than max_fee, as it charges in accordance with the above fee formula.

What do we price

At the time of writing, the following components are contributing to the transaction fee:

  • Computational complexity: The marginal cost of verifying the transaction on L1, measured in L1 gas.

  • Onchain data: The cost of posting the state diffs induced by the transaction to L1 (for more details, see data availability). This is measured in L1 gas or L1 data gas, depending on whether or not the L2 block in which the transaction was included uses calldata or blobs.

  • L2→L1 messages: Messages sent to L1 are eventually sent to the Starknet core contract as L1 calldata by the sequencer; therefore L2 transaction that send L2→L1 messages incur an additional L1 gas cost.

  • L2 calldata, events and code: From Starknet 0.13.1 onwards, there is a per-byte (or per felt) price for L2 payloads. For more details, see L2 payloads: calldata, events, and code.

Fee units

Each transaction is associated with an estimate of the amount of gas used. Combining this estimate with the price of gas yields the estimated fee.

For transactions prior to v3, the fee is denominated in WEI. For transactions v3 and later, the fee is denominated in STRK.

Fee calculation

Computation without builtins

Let’s analyze the correct metric for measuring transaction complexity. For simplicity, we will ignore Cairo’s builtins, and address them later.

A Cairo program execution yields an execution trace. When proving a Starknet block, we aggregate all the transactions appearing in that block to the execution trace.

Starknet’s prover generates proofs for execution traces, up to some maximal length \(L\), derived from the specs of the proving machine and the desired proof latency.

Tracking the execution trace length associated with each transaction is simple. Each assertion over field elements, such as verifying addition/multiplication over the field, requires the same, constant number of trace cells, which is where our "no-builtins" assumption kicks in: Pedersen occupies more trace cells than addition. Therefore, in a world without builtins, the fee of the transaction \(tx\) is correlated with \(\text{TraceCells}[tx]/L\).

Computation with builtins

In the Cairo execution trace each builtin has its own slot, which is important to consider when determining the fee.

For example, consider that the prover can process a trace with the following limits:

up to 500,000,000 Cairo Steps up to 20,000,000 Pedersen hashes up to 4,000,000 signature verifications up to 10,000,000 range checks

The proof is closed and sent to L1 when any of these slots is filled.

Suppose that a transaction uses 10,000 Cairo steps and 500 Pedersen hashes. At most 40,000 such transactions can fit into the hypothetical trace (20,000,000/500). Therefore, its gas price correlates with 1/40,000 of the cost of submitting proof.

Notice that this estimate ignores the number of Cairo steps, as it is not the limiting factor, since 500,000,000/10,000 > 20,000,000/500.

With this example in mind, it is possible to formulate the exact fee associated with L2 computation.

The allocation of resources among builtin operations must be predetermined; it is not possible to decide, post-execution, to include only 20,000,001 Pedersen hashes without additional components.

This safeguards fairness and prevents manipulation, ensuring integrity in proof generation and fee determination.

Calculation of computation costs

For each transaction, the sequencer calculates a vector, CairoResourceUsage, that contains the following:

  • The number of Cairo steps.

  • The number of applications of each Cairo builtin. For example, five range checks and two Pedersen hashes.

The sequencer crosses this information with the CairoResourceFeeWeights vector. For each resource type, either a Cairo step or a specific builtin application, CairoResourceFeeWeights has an entry that specifies the relative gas cost of that component in the proof.

Going back to the above example, if the cost of submitting a proof with 20,000,000 Pedersen hashes is roughly 5m gas, then the weight of the Pedersen builtin is 0.25 gas per application (5,000,000/20,000,000). The sequencer has a predefined weights vector, in accordance with the proof parameters.

The sequencer charges only according to the limiting factor. Therefore the fee is correlated with:

\[\max_k[\text{CairoResourceUsage}_k \cdot \text{CairoResourceFeeWeights}_k]\]

where \(k\) enumerates the Cairo resource components, that is the number of Cairo steps and builtins used.

Table 1. Amount of gas used per Cairo step or per each time a Cairo builtin is applied
Step or builtin Gas cost

Cairo step

0.0025 gas/step

Pedersen

0.08 gas/application

Poseidon

0.08 gas/application

Range check

0.04 gas/application

ECDSA

5.12 gas/application

Keccak

5.12 gas/application

Bitwise

0.16 gas/application

EC_OP

2.56 gas/application

Onchain data components

The onchain data associated with a transaction is composed of three parts

  • Storage updates

  • L2→L1 messages

  • Deployed contracts

  • Declared classes (only relevant for DECLARE transactions, and adds two additional words)

Onchain data: Storage updates

Whenever a transaction updates some value in the storage of some contract, the following data is sent to L1:

  • two 32-byte words per contract

  • two 32-byte words for every updated storage value

For information on the exact data and its construction, see Data availability.

Only the most recent value reaches L1. So the transaction’s fee only depends on the number of unique storage updates. If the same storage cell is updated multiple times within the transaction, the fee remains that of a single update.

The following formula describes the storage update fee for a transaction:

\[\underbrace{\textit{gas_price}\left(\text{da_calldata_cost} \cdot 2(n-1) - \text{contract_update_discount}\cdot (n-1)\right)}_{\text{contract addresses + new nonce and number of storage updates }} \\ + \\ \underbrace{\textit{gas_price} \cdot \left(\text{da_calldata_cost}(2(m-1))-240\right)}_{\text{storage updates}}\]

where:

  • \(n\) is the number of unique contracts updated, which also includes changes to classes of existing contracts and contract deployments, even if the storage of the newly deployed contract is untouched. In other words, \(n\ge\ell\). Notice that \(n\ge 1\) always holds, because the fee token contract is always updated at the end of each transaction, in order to update the sequencer’s and the sender’s balances. The fee token contract update is not taken into account when computing the fee.

  • \(m\) is the number of values updated, not counting multiple updates for the same key. Notice that \(m\ge 1\) always holds, because the sequencer’s balance is updated at the end of each transaction. The sequencer’s balance update is not taken into account when computing the fee.

  • \(\text{contract_update_discount}\) is 312 gas, which is discounted for every updated contract. This discount is a result of the fact that out of the \(2n\) words caused by updating contracts, \(n\) words are short, including at most 6 non-zero bytes:

    • three bytes for the nonce

    • two bytes for the number of storage updates

    • one byte for the class information flag

    Taking into account that zero bytes only cost 4 gas, the cost difference between a full 32-byte word, which does not contain zeros, and a word with only six non-zero bytes is \(32\cdot16-(6\cdot16+26\cdot4)=312\).

  • \(240\) is the gas discount for updating the sender’s balance, and is derived by assuming the balance requires at most 12 non-zero bytes, which is enough for 1.2B ETH or STRK, resulting in the following discount: \(512-(20\cdot4+12\cdot16)=240\).

Improvements to the above pessimistic estimation might be gradually implemented in future versions of Starknet.

For example, if different transactions within the same block update the same storage cell, there is no need to charge for both transactions, because only the last value reaches L1. In the future, Starknet might include a refund mechanism for such cases.

Onchain data: L2→L1 messages

When a transaction that raises the send_message_to_l1 syscall is included in a state update, the following data reaches L1:

  • L2 sender address

  • L1 destination address

  • Payload size

  • Payload (list of field elements)

Consequently, the gas cost associated with a single L2→L1 message is:

\[\begin{align} \text{MESSAGE_COST} = & \; \text{message_calldata_cost}\cdot\left(3+\text{payload_size}\right) \; + \\ & + \text{l1_log_data_cost}\cdot\text{payload_size} \; + \\ & + \text{log_message_to_l1_cost} \; + \\ & + \text{l1_storage_write_cost} \end{align}\]

Where:

  • \(\text{message_calldata_cost}\) is 1124 gas. This is the sum of the 512 gas paid to the core contract on submitting the state update, and 612 gas paid for the submitting of the same word to the verifier contract (which incurs ~100 additional gas for hashing). That is, messages are sent to Ethereum twice.

  • \(\text{log_message_to_l1_cost}\) is 1637 gas. This is the fixed cost involved in emitting a LogMessageToL1 event. This event has two topics and a data array, which adds two data words to the event, resulting in a total of \(375+2\cdot 375+2\cdot 256\) gas (log opcode cost, topic cost, and two data words cost).

  • \(\text{l1_log_data_cost}\) is 256 gas, which is paid for every payload element during the emission of the LogMessageToL1 event.

  • \(\text{l1_storage_write_cost}\) is 20,000 gas per message which is paid in order to store the message hash on the Starknet core contract. This recording of the message is what later enables the intended L1 contract to consume the message.

Onchain data: Deployed contracts

When a transaction that raises the deploy syscall is included in a state update, the following data reaches L1:

  • contract address

  • number of storage updates and the new nonce

  • class hash

The first two elements are counted in the number of unique modified contracts, denoted by \(n\) throughout this page. So the only additional word comes from publishing the class hash, which adds 551 gas. For more information, see \(\text{da_calldata_cost}\) in the final formula.

L2 payloads: calldata, events, and code

As of Starknet v0.13.1 onwards, L2 data is taken into account during pricing. This includes:

  • calldata: this includes transaction calldata (in the case of INVOKE transactions or L1_HANDLER), constructor calldata (in the case of DEPLOY_ACCOUNT transactions), and signatures

  • events: data and keys of emitted events

  • ABI: classes abi in DECLARE transactions (relevant only for DECLARE transactions of version ≥ 2)

  • CASM bytecode (for all available DECLARE transactions, where in version ≥ 2 this refers to the compiled class)

  • Sierra bytecode (relevant only for DECLARE transactions of version ≥ 2)

The pricing of the above components in terms of L1 gas is given by the following table:

Resource Gas cost

Event key

0.256 gas/felt

Event data

0.128 gas/felt

Calldata

0.128 gas/felt

CASM bytecode

1 gas/felt

Sierra bytecode

1 gas/felt

ABI

0.032 gas/character