Fee mechanism

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.

Introduction

Every transaction on Starknet requires a small fee to process. The components contributing to this fee include L2 computation, L2 data, and L1 data, which are measured in L2 gas, L1 gas, and L1 data gas. A transaction’s fee is charged atomically when it is executed and can be estimated using the starknet_estimateFee call, based on the formula for determining a transaction’s fee.

Fee components

The following components contribute to a transaction’s fee:

  • L2 computation

  • L2 data (calldata, events and code)

  • L1 data, which includes:

    • The cost of posting the state diffs induced by the transaction to L1 (for more details, see Data availability)

    • L2→L1 messages (which are eventually sent to the Starknet core contract as L1 calldata by the Starknet sequencer)

Fee units and limits

The three components contributing to a transaction’s fee are measured by the following three resources types:

  • L2 gas, measuring L2 resources, including computation and data

  • L1 data gas, measuring the L1 data required to post the state diff induced by the transaction to L1

  • L1 gas fee, measuring the L1 gas required for sending L2→L1 messages, as well as replacing:

    • L1 data gas, in case the L2 block in which the transaction was included uses calldata instead of blobs for data availability (for more details, see Data availability)

    • L2 gas, in the case the transaction did not specify L2 gas bounds

There is a rather natural way to convert costs denominated in L1 gas to costs denominated in L2 gas and vice versa. One Cairo step costs 100 L2 gas, and we charge 0.0025 L1 gas per step when tracking VM resources (see L2 computation for more details), hence can decide that 1 L1 gas = 40,000 L2 gas.

This is only a temporary measure to use existing prices (denominated in L1 gas) when the transaction consumes L2 gas, as L1 gas and L2 gas are completely independent units. Moreover, the step cost of 0.0025 L1 gas is mostly arbitrary at this point due to our use of dynamic layouts (see VM resources for more information)

v3 transactions already provide max_amount and max_price_per_unit for each of these resources (as part of the resource_bounds dictionary), denominated in STRK, while previous versions only provide an overall max_fee, denominated in WEI. However, starting in Starknet v0.14.0, all transactions must specify bounds for all three resource types.

Starting in Starknet v0.14.0, all transactions must specify bounds for all three resource types.

The only limitation on the sequencer which is currently enforced by the Starknet OS (and as such, enforced by a proof), is that the actual fee charged is bounded by the max price specified in the transaction. However, the Starknet sequencer usually charges less, as charges in accordance with the formula for determining a transaction’s fee.

Charging fees

The fee for a transaction is charged atomically with the transaction execution on L2, by 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.

Estimating fees

The fee for a transaction can be estimated by using the starknet_estimateFee call, and interfaces for fee estimations are also exposed by the various Starknet SDKs.

When signing resource bounds for all three resource types — the only supported option when sending the transaction via Starknet’s JSON-RPC v0.8.0 onwards, and the only supported option in Starknet v0.14.0 — the fee estimation response is easily mapped to the transaction’s fields (i.e., the response contains price and amounts for all three resources, and the transaction is expected to specify bounds and max prices for each of them).

On the other hand, when submitting transactions via Starknet’s JSON-RPC v0.7.1 or below, the only bounded resource is L1 gas, while the fee estimation contains both L1 gas and L1 data gas. In that case, we can use the estimation’s overall_fee to decide on the L1 gas bounds.

You can find more details in the Starknet v0.13.1 prerelease notes.

Fee formula

The formula described in the following sections is highly non-trivial.

To mitigate this, the first section details the formula for determining a transaction’s overall fee, while subsequent ones dive into the different fee components and explain how this formula was derived.

Some parts may require reading more than once, but don’t hesitate to reach out if you feel further clarification is needed.

Overall 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.

The following formula describes the overall fee of a transaction:

\[\begin{align} \qquad & \text{l1_gas_price} \cdot\Bigg( \\ & \qquad \text{message_calldata_cost}\cdot 3t \; + \\ & \qquad \; (\text{message_calldata_cost} + \text{l1_log_data_cost})\cdot \sum\limits_{i=1}^t q_i \; + \\ & \qquad \; \left(\text{l1_storage_write_cost}+\text{log_message_to_l1_cost}\right)\cdot t \\ & \Bigg) + \; \text{l2_gas_price} \cdot (\text{sierra_gas_consumed} + \max_k v_k w_k + \text{l2_payload_costs}) \\ & \quad + \; \begin{cases}\text{l1_data_gas_price}\cdot\text{felt_size_in_bytes}\cdot\big(\ell + 2(n-1) + 2(m-1) + 2D \big) & \text{if l1_da_mode is BLOB} \\ \\ \text{l1_gas_price} \cdot \Big( \\ \qquad \text{da_calldata_cost} \cdot \left(\ell + 2(n-1)+2(m-1) + 2D\right) \; - \\ \qquad \text{contract_update_discount}\cdot (n-1) \; - & \text{if l1_da_mode is CALLDATA} \\ \qquad \text{sender_balance_update_discount} \\ \Big)\end{cases} \end{align}\]

where:

  • \(\text{l1_gas_price}\) is the averages of the last 60 L1 base gas prices sampled by the Starknet sequencer every 60 seconds, plus 1 Gwei

  • \(\text{message_calldata_cost}\) is 1,124 gas

  • \(t\) is the number of L2→L1 messages sent

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

  • \(t\) \(q_1,...,q_t\) are the payload sizes of the L2→L1 messages sent

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

  • \(\text{log_message_to_l1_cost}\) is 1,637 gas (see L2→L1 messages for more information)

  • \(\text{l2_gas_price}\) is a fixed amount denominated in WEI (the price in FRI is only dependent on the WEI to FRI ratio). With introduction of EIP1559, this price will be determined by Starknet’s fee market.

    If the transaction does not sign over L2 gas bounds (which will not be possible in Starknet v0.14.0), then \(\text{l2_gas_price}\) is replaced by \(\text{l1_gas_price}\)

  • \(\text{sierra_gas_consumed}\) is the amount of Sierra gas charged for computation

  • \(v\) is a vector that represents resource usage (Cairo steps or number of applications of each builtin), where each of its entries, \(v_k\), corresponds to the usage of a different resource type (see L2 computation for more information)

    The same transaction can track both raw VM resources (reflected by \(v_k\)) and Sierra gas, depending on what classes it goes through (see L2 computation for more details).

  • \(w\) is the CairoResourceFeeWeights vector (see L2 computation for more information)

    \(w_k\) is measured in L2 gas if the transaction signs over L2 gas bounds, and in L1 gas otherwise (which will not be possible in Starknet v0.14.0).

  • \(\text{l2_payload_costs}\) is the gas cost of data sent over L2, which includes calldata, code, and event emission (see L2 data for more details)

    \(\text{l2_payload_costs}\) is measured in L2 gas if the transaction signs over L2 gas bounds, and in L1 gas otherwise (which will not be possible in Starknet v0.14.0).

  • \(\text{l1_data_gas_price}\) is the averages of the last 60 L1 base data gas prices sampled by the Starknet sequencer every 60 seconds, plus 1 Gwei

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

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

  • \(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.

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

  • \(\text{da_calldata_cost}\) is 551 gas, derived as follows:

    • 512 gas per 32-byte word for calldata

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

    • a 10% discount for not incurring additional costs for repeated updates to the same storage slot within a single block

  • \(\text{contract_update_discount}\) is 312 gas (See Storage updates for more information)

  • \(\text{sender_balance_update_discount}\) is \(240\) gas (see Storage updates for more information)

L2 computation

Measuring the L2 computation component of a transaction differs depending on the contract class version of the caller:

  • For CairoZero classes or Sierra ≤ 1.6.0, computation is measured in VM resources (steps and builtins)

  • For Sierra ≥ 1.7.0, computation is measured in Sierra gas

    In Starknet v0.13.4, Sierra gas is only tracked if the parent call was also tracking Sierra gas, which means that if the account contract is Sierra 1.6.0 or older, VM resources will be tracked throughout the entire transaction. This condition may be relaxed in the future.

VM resources

A Cairo program execution yields an execution trace, and when proving a Starknet block, we aggregate all the transactions appearing in that block to the execution trace 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, as Cairo step requires the same constant number of trace cells. Therefore, in a world without builtins, the fee associated with the L2 computation component of a transaction \(tx\) should be correlated with \(\text{TraceCells}[tx]/L\).

The aforementioned observation is no longer true for Starknet’s next-gent prover Stwo, which handles some opcodes more efficiently than others. However, we neglect this intricacy for the purposes of this discussion.

When we introduce builtins into the equation, we need to consider an a priori limit for each builtin in the proof. This set of limits is known as the proof’s layout, which determines the ratio between steps and each builtin.

Today, Starknet’s prover is able to dynamically choose a layout based on a given block resource’s consumption, i.e. there is no longer an a priori fixed layout. However, pricing for old classes still behaves as if we are using a fixed layout.

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

Up to 500M Cairo steps Up to 20M Pedersen hashes Up to 4M signature verifications Up to 10M range checks

which means that a proof is closed and sent to L1 when any of these slots is filled. Now, suppose that a transaction uses 10K Cairo steps and 500 Pedersen hashes. At most 20M/500 = 40K such transactions can fit into the hypothetical trace, therefore its gas price should correlate with 1/40K of the cost of submitting proof (notice that this estimate ignores the number of Cairo steps as it is not the limiting factor, since 500M/10K > 20M/500).

With this example in mind, it is possible to formulate the exact fee associated with L2 computation. 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 (e.g., 5 range checks and 2 Pedersen hashes)

and crosses this information with a CairoResourceFeeWeights vector, a predefined weights vector in accordance with the proof parameters, in which each resource type has an entry that specifies the relative gas cost of that component in the proof. The sequencer then charges only according to the limiting factor, making the final fee defined by:

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

where \(k\) enumerates the Cairo resource components. Going back to the above example, if the cost of submitting a proof with 20M Pedersen hashes is roughly 5M gas, then the weight of the Pedersen builtin is 5,000,000/20,000,0000 = 25 gas per application.

The weights most commonly used in Starknet are the following table:

Resource L1 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

Sierra gas

The following is a very rough description of Sierra’s built-in gas accounting mechanism. For a comprehensive analysis, see Analysis of the gas accounting algorithm of Cairo 1.0 by CryptoExperts.

A Sierra program has a simple structure: types and function declaration, followed by a sequence of applications of libfuncs, Sierra’s basic logical units (similar to opcodes, e.g. u8_add is a libfunc).

The Cairo compiler defines a libfunc costs table, which is measured in “Sierra gas” and has a 1-1 ratio with L2 gas (i.e., a libfunc which costs 500 Sierra gas adds 500 to a transaction’s overall L2 gas)

Despite the 1-1 ratio between Sierra gas and L2 gas, L2 gas accounts for “everything L2”, while Sierra gas strictly deals with computation, hence the distinction in terminology.

The cost of each libfunc is determined by its expanded CASM generated via the Sierra→CASM compiler based on a 100-1 ratio with Cairo steps (i.e., if a libfunc’s assembly includes 10 Cairo steps, it will cost 1000 Sierra gas), while the costs of the various builtins are defined as follows:

Builtin Sierra gas cost

Range check

70

Pedersen

4050

Poseidon

491

Bitwise

583

ECDSA

-

EC_OP

4085

Keccak

-

ADD_MOD

230

MUL_MOD

604

Pricings for the EC_OP and Keccak builtins are missing since they cannot be accessed directly from Cairo contracts (as opposed to CairoZero contracts, which are no longer declarable). These operations can be used via syscalls, whose price is determined by the underlying trace cell consumption of the builtins involved. To review pricing for various syscalls, see the versioned constants.json files in the sequencer’s resources directory.

To handle gas usage, Sierra has special libfuncs for gas-handling, such as the withdraw_gas libfunc. For functions with neither branching nor recursion, the Cairo→Sierra compiler adds a single withdraw_gas(C) call in the beginning of the function, where C is the sum over the costs of the libfuncs included in the function. For functions with branching, the compiler adds a call to withdraw_gas(C) before the actual branching, where C is the maximal branch cost.

In its latest version, the compiler also adds a call to redeposit_gas(C) on the cheaper branches, where C is unused gas on that branch.

For functions with recursion (or other cases where costs can only be known in runtime), things get trickier. The naive way to handle such cases would be to add a withdraw_gas instruction after every libfunc, but since withdraw_gas itself has some cost (decreasing a counter and handling the insufficient gas case) this would incur a large burden on the program. Instead, the compiler constructs the call graph induced by the program, and asserts that every cycle includes a withdraw_gas(X) instruction, where X should cover the cost of a single run through the cycle, greatly reducing the overhead compared to the naive mechanism.

VM resources vs. Sierra gas

The difference in tracking Sierra gas vs. tracking VM resources can be summed up as follows:

  • For VM resources builtin weights reflect the proof layout, while for Sierra gas they reflect trace cell consumption

  • For VM resources only the maximal resource (e.g., most used builtin) is considered, while for Sierra gas the sum of all resources (i.e., all libfuncs) is considered

This means that when the tracking Sierra gas, step-heavy transactions will most likely be slightly more expensive, as builtins will be taken into account in addition to Cairo steps. On the other hand, builtin-heavy transactions will become much cheaper — depending on the builtin that maximized the old fee and with the exception of the Pedersen builtin.

L1 data

Storage updates

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

  • One 32-bye word if the transaction is a DEPLOY transaction (since we need to specify the deployed contract’s class hash)

  • Two 32-byte words per contract

  • Two 32-byte words for every updated storage value

Only the most recent value reaches L1, making the transaction’s fee depend 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.

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

Therefore, the storage update fee for a transaction is defined as follows:

\[\text{data_gas_price}\cdot\text{felt_size_in_bytes}\cdot\bigg(\ell + 2(n-1) + 2(m-1) + 2D \bigg)\]

This formula only refer to the case of submitting data to L1 via blobs, for the calldata case, see Overall fee).

where:

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

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

  • \(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.

  • \(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.

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.

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)

Therefore, the gas cost associated with a single L2→L1 message is defined as follows:

\[\qquad \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} \qquad\]

Where:

  • \(\text{message_calldata_cost}\) is 1,124 gas, which is the sum of the 512 gas for submitting the state update to the core contract and 612 gas for the submitting the state update the verifier contract (which incurs ~100 additional gas for hashing)

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

  • \(\text{log_message_to_l1_cost}\) is 1,637 gas, which is the fixed cost involved in emitting a LogMessageToL1 event with two topics and a two words data array, resulting in a total of \(375+2\cdot 375+2\cdot 256\) gas (log opcode cost, topics cost, and data array cost)

  • \(\text{l1_storage_write_cost}\) is 20K gas per message, paid in order to store the message hash on the Starknet core contract and enable the target L1 contract to consume the message

L2 data

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

  • Calldata, including transaction calldata (in the case of INVOKE transactions or L1_HANDLER), constructor calldata (in the case of DEPLOY_ACCOUNT transactions), and signatures

  • Events, including data and keys of emitted events

  • ABI, including classes ABI in DECLARE transactions (only relevant 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 L1 gas cost of each component in as follows:

When a transaction’s L2 cost is paid for by L2 gas, the following numbers are translated via the standard conversion rate of 1 L1 gas = 40K L2 gas.

Resource L2 Gas cost

Event key

10,240 gas/felt

Event data

5,120 gas/felt

Calldata

5,120 gas/felt

CASM bytecode

40,000 gas/felt

Sierra bytecode

40,000 gas/felt

ABI

1,280 gas/character