Skip to main content

Troubleshooting: Building Arbitrum dApps

How does gas work on Arbitrum?

Fees on Arbitrum chains are collected on L2 in the chains' native currency (ETH on both Arbitrum One and Nova).

A transaction fee is comprised of both an L1 and an L2 component:

The L1 component is meant to compensate the Sequencer for the cost of posting transactions on L1 (but no more). (See L1 Pricing.)

The L2 component covers the cost of operating the L2 chain; it uses Geth for gas calculation and thus behaves nearly identically to L1 Ethereum. One difference is that unlike on Ethereum, Arbitrum chains enforce a gas price floor; currently 0.1 gwei on Arbitrum One and 0.01 gwei on Nova (See Gas).

L2 Gas price adjusts responsively to chain congestion, ala EIP 1559.

I tried to create a retryable ticket but the transaction reverted on L1. How can I debug the issue?

Creation of retryable tickets can revert with one of these custom errors:

  1. InsufficientValue: not enough gas included in your L1 transaction's callvalue to cover the total cost of your retryable ticket; i.e., msg.value < (maxSubmissionCost + l2CallValue + gasLimit * maxFeePerGas). Note that as of the Nitro upgrade, your L1 transaction's callvalue must cover this full cost (previously an L2 message's execution could be paid for with funds on L2: see dapp migration and "retryable ticket creation".
  2. InsufficientSubmissionCost: provided submission cost isn't high enough to create your retryable ticket.
  3. GasLimitTooLarge: provided gas limit is greater than 2^64
  4. DataTooLarge: provided data is greater than 117.964 KB (90% of Geth's 128 KB transaction size limit).

To figure out which error caused your transaction to revert, we recommend using etherscan's Parity VM trace support (Tenderly is generally a very useful debugging tool; however, it can be buggy when it comes to custom Geth errors).

Use the following link to view the Parity VM trace of your failed transaction (replacing the tx-hash with your own, and using the appropriate etherscan root url):

To find out the reversion error signature, go to the "Raw Traces" tab, and scroll down to find the last "subtrace" in which your transaction is reverted. Then find "output" field of that subtrace.

(In the above example the desirable "output" is:
0x7040b58c0000000000000000000000000000000000000000000000000001476b081e80000000000000000000000000000000000000000000000000000000000000000000 )

The first four bytes of the output is the custom error signature; in our example it's 0x7040b58c .

To let's find out which is custom error this signature represents, we can use this handy tool by Samzcsun:

Checking 0x7040b58c gives us InsufficientValue(uint256,uint256).

How is the L1 portion of an Arbitrum transaction's gas fee computed?

The L1 fee that a transaction is required to pay is determined by compressing its data with brotli and multiplying the size of the result (in bytes) by ArbOS's current calldata price; the latter value can be queried via the getPricesInWeimethod of the ArbGasInfoprecompile. You can find more information about gas calculations in Understanding Arbitrum: 2-Dimensional Fees and How to estimate gas in Arbitrum.

What is a retryable ticket's "submission fee"? How can I calculate it? What happens if I the fee I provide is insufficient?

retryable's submission fee is a special fee a user must pay to create a retryable ticket. The fee is directly proportional to the size of the L1 calldata the retryable ticket uses. The fee can be queried using the Inbox.calculateRetryableSubmissionFeemethod. If insufficient fee is provided, the transaction will revert on L1, and the ticket won't get created.

Which method in the Inbox contract should I use to submit a retryable ticket (aka L1 to L2 message)?

The method you should (almost certainly) use is Inbox.createRetryableTicket. There is an alternative method, Inbox.unsafeCreateRetryableTicket, which, as the name suggests, should only be used by those who fully understand its implications.

There are two differences between createRetryableTicket and unsafeCreateRetryableTicket:

  1. createRetryableTicket will check that provided L1 callvalue is sufficient to cover the costs of creating and executing the retryable ticket (at the specified parameters) and otherwise revert directly at L1 [TODO: link to "retryable reverts at L1" faq]. unsafeCreateRetryableTicket, in contrast, will allow a retryable ticket to be created that is guaranteed to revert on L2.
  2. createRetryableTicket will check if either the provided excessFeeRefundAddress or the callValueRefundAddress are contracts on L1; if they are, to prevent the situation where refunds are guaranteed to be irrecoverable on L2, it will convert them to their address alias, providing a potential path for fund recovery. unsafeCreateRetryableTicket will allow the creation of a retryable ticket with refund addresses that are L1 contracts; since no L1 contract can alias to an address that is also itself an L1 contract, refunds to these addresses on L2 will be irrecoverable.

(Astute observers may note a third ticket creation method, createRetryableTicketNoRefundAliasRewrite; this is included only for backwards compatibility, but should be considered deprecated in favor of unsafeCreateRetryableTicket)

Why do I get "custom tx type" errors when I use hardhat?

In Arbitrum, we use a number of non-standard EIP-2718 typed transactions. See here for the full list and the rationale.

Hardhat v2.12.2 added supports for forking networks like Arbitrum with custom transaction types, so if you're using hardhat, upgrade to 2.12.2!


Why does it look like two identical transactions consume a different amount of gas?

Calling an Arbitrum node's eth_estimateGas RPC returns a value sufficient to cover both the L1 and L2 components of the fee for the current gas price; this is the value that, e.g., will appear in users' wallets in the "gas limit" field.

Thus, if the L1 calldata price changes over time, it will appear (in e.g., a wallet) that a transaction's gas limit is changing. In fact, the L2 gas limit isn't changing, merely the total gas required to cover the transaction's L1 + L2 fees.

See 2-D fees and How to estimate gas in Arbitrum for more.

Why am I getting error "429 Too Many Requests" when using one of Offchain Labs' Public RPCs?

Offchain Labs offers public RPCs for free, but limits requests to prevent DOSing. Hitting the rate limit could come from your request frequency and/or the resources required to process the requests. If you are hitting our rate limit, we recommend running your own node or using a third party node provider.

How do block.number and block.timestamp work on Arbitrum?

Solidity calls to block.number on Arbitrum will return the block number/ timestamp of the underlying L1 on a slight delay; i.e., updated every few minutes. Note that L2 block numbers (i.e., as seen in block explorers / returned by RPCs) are different, and are typically updated roughly every second.

Solidity calls to block.timestamp on Arbitrum are not linked to the timestamp of the L1 block, it is updated every L2 block based on the sequencer's clock.

For more info, see block numbers and time.

Do I need to download any special npm libraries in order to use web3.js or ethers-js on Arbitrum?

Nope; web3.js and ethers.js will work out of the box just like they do on L1 Ethereum.

Once upon a time, Arbitrum developers were required to download supplemental packages with names like "arb-provider-ethers" and "arb-ethers-web3-bridge", but these packages are deprecated and no longer required! Any guide that directs devs to use them should be considered outdated.

How many block numbers must we wait for in Arbitrum before we can confidently state that the transaction has reached finality?

Arbitrum's block intervals fluctuate with throughput, so relying on block numbers for finality isn't recommended. However, Arbitrum nodes support Ethereum's JSON RPC, enabling the use of eth_getBlockByNumber() to determine block finality. Here, we provide additional details on how to achieve this.

You can use eth_getBlockByNumber() with the string "latest", "safe", or "finalized", each offering varying degrees of finality:

  • latest: Provides you with the most recent Arbitrum block number that the node has observed on the L1 and indicates that the Sequencer's batch has been just published as an L1 block on the Ethereum network. It's important to note that this block has the potential to be re-orged but you can consider and trust this block as final, if you trust the sequencer.
  • safe: Provides you with the most recent Arbitrum block number that has achieved attestations from a two-thirds majority of Ethereum's validator set. This occurs when the Sequencer's batch is posted as an L1 block on Ethereum and then the batch transactions achieve safe finality there. While safe blocks are typically resistant to re-orgs, they can still be re-orged in the event of a significant L1 re-org.
  • finalized: Provides you with the most recent Arbitrum block number that is finalized on Ethereum. This means that the Sequencer's batch has been published as an L1 block on the Ethereum network and has reached a substantial depth, making it eligible for hard finality. Unlike safe blocks, finalized blocks are highly improbable to undergo re-orgs.

To learn more about the different phases of an Arbitrum transaction, from client initiation to Layer 1 confirmation, check out The Lifecycle of an Arbitrum Transaction.

How can I list my token on the Arbitrum Bridge?

The L2 token list used in the Arbitrum bridge is generated from the L1 tokens that are part of the token list of Uniswap, Gemini or Coinmarketcap. This is valid for L1-native tokens that have been bridged over to L2, and for L2-native tokens that have been bridged over to L1 as long as they are part of any of those lists.

Currently, there isn't any L2-only token list.

What is a testnet or a devnet?

Testnets (or devnets) primarily serve developers who want to test out the applications they're building without having to use any real mainnet funds.

Arbitrum Goerli testnet has the same full feature-set as the mainnet networks. It is also a "true" L2 that runs on top of the Goerli testnet (L1), using it for security and settlement.

Users can bridge any asset from the Goerli testnet (L1) into the Arbitrum Goerli testnet (and back!), using the official bridge.

Is there any testnet available on Arbitrum?

Yes, there's an Arbitrum Goerli testnet (421613) that uses the Nitro tech stack, running on top of Ethereum Goerli. You can find more information here.

When was Arbitrum One upgraded from Classic to Nitro?

Arbitrum One was upgraded on August 31st, 2022, from the Classic stack to the improved Nitro tech stack, maintaining the same state.