From d51afbb48abe42056f8ee6096cde15a6175fe6fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Mon, 4 May 2026 18:47:49 -0700 Subject: [PATCH 01/18] docs: add 'Register via DAO governance' article for generic-custom gateway Documents the standardized token registration template for cases where the parent chain token can't self-register (non-upgradeable, missing ICustomToken). Covers the RegisterAndSetArbCustomGatewayAction contract, the calldata generator script, Tally proposal submission, and the post-execution sequence. Cross-links from setup-generic-custom-gateway.mdx (Step 1 prerequisite mention and FAQ entry). New sidebar entry under Configure token bridging. Sources: - https://github.com/OffchainLabs/governance/blob/main/src/gov-action-contracts/token-bridge/RegisterAndSetArbCustomGatewayAction.sol - https://forum.arbitrum.foundation/t/announcement-of-standardized-token-registrations-template/29764 - https://gist.github.com/gzeoneth/439dedc7bdd971345657a04c266daf00 --- .../register-via-dao-governance.mdx | 177 ++++++++++++++++++ .../setup-generic-custom-gateway.mdx | 6 +- sidebars.js | 5 + 3 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 docs/build-decentralized-apps/token-bridging/configure-token-bridging/register-via-dao-governance.mdx diff --git a/docs/build-decentralized-apps/token-bridging/configure-token-bridging/register-via-dao-governance.mdx b/docs/build-decentralized-apps/token-bridging/configure-token-bridging/register-via-dao-governance.mdx new file mode 100644 index 0000000000..6340cf74b9 --- /dev/null +++ b/docs/build-decentralized-apps/token-bridging/configure-token-bridging/register-via-dao-governance.mdx @@ -0,0 +1,177 @@ +--- +title: 'Register a custom gateway token via Arbitrum DAO governance' +sidebar_label: 'Register via DAO governance' +description: "Guide to registering a token in Arbitrum's generic-custom gateway when self-service registration isn't possible. Uses the standardized RegisterAndSetArbCustomGatewayAction template and a DAO proposal." +user_story: 'As a token issuer whose parent chain token cannot self-register with the generic-custom gateway, I want to register the token through an Arbitrum DAO governance proposal.' +content_type: how-to +displayed_sidebar: buildAppsSidebar +--- + +[Self-service registration](/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx) requires the parent chain token to implement [`ICustomToken`](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/ICustomToken.sol) and call `registerCustomL2Token` and `setGateway` itself. When the parent chain token is non-upgradeable, immutable, or otherwise can't make those calls, registration must go through Arbitrum DAO governance using the privileged `forceRegisterTokenToL2` and `setGateways` paths. + +Offchain Labs publishes a standardized calldata template — `RegisterAndSetArbCustomGatewayAction` — together with a payload generator script. Using the template is a [helpful utility, not a requirement](https://forum.arbitrum.foundation/t/announcement-of-standardized-token-registrations-template/29764), but it reduces manual debugging and gives reviewers a known-safe shape to verify against. + +## When to use this path + +Use this article if: + +- Your parent chain token is already deployed and **can't be upgraded** to add the `ICustomToken` methods. +- You'd rather not [wrap the token](/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx#step-1-review-the-prerequisites) and register the wrapper instead. +- A child chain counterpart implementing [`IArbToken`](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/arbitrum/IArbToken.sol) is already deployed. + +If your parent chain token is upgradeable or you're building it from scratch, follow the [self-service generic-custom gateway setup](/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx) instead — it's faster and doesn't require a governance vote. + +## How the action contract works + +`RegisterAndSetArbCustomGatewayAction` is a one-shot action contract executed by the DAO's `UpgradeExecutor` via the L1 timelock. It performs the two registration calls in a single privileged transaction: + +```solidity +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +contract RegisterAndSetArbCustomGatewayAction { + IL1AddressRegistry public immutable addressRegistry; + + function perform( + address[] memory _l1Tokens, + address[] memory _l2Tokens, + uint256 _maxGasForRegister, + uint256 _gasPriceBidForRegister, + uint256 _maxSubmissionCostForRegister, + uint256 _maxGasForSetGateway, + uint256 _gasPriceBidForSetGateway, + uint256 _maxSubmissionCostForSetGateway + ) external payable { + // 1. forceRegisterTokenToL2 on the L1 generic-custom gateway + // 2. setGateways on the L1 gateway router (pointing _l1Tokens at the generic-custom gateway) + } +} +``` + +Source: [`RegisterAndSetArbCustomGatewayAction.sol`](https://github.com/OffchainLabs/governance/blob/main/src/gov-action-contracts/token-bridge/RegisterAndSetArbCustomGatewayAction.sol). + +The two calls each emit a retryable ticket from the parent chain to the child chain. Both retryables are auto-redeemed when the action contract supplies enough submission cost, after which the token is fully registered on both chains. + +## Prerequisites + +Before generating a proposal, make sure: + +- The parent chain ERC-20 token is **already deployed**. +- The child chain token contract implementing [`IArbToken`](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/arbitrum/IArbToken.sol) is **already deployed**. +- Both addresses are final and immutable — once registered, the mapping cannot be changed. +- You have [Foundry](https://book.getfoundry.sh/getting-started/installation) installed locally (the payload generator uses `cast`). + +## Step 1: Generate the proposal calldata + +Save the following script as `reg-arb-custom.sh`. Set `L1_TOKEN_ADDRESS` and `L2_TOKEN_ADDRESS` to your token's parent and child chain addresses. Leave the other constants alone — they reference live governance contracts on Ethereum and the canonical action address. + +```shell +#!/usr/bin/env bash +set -euo pipefail + +# Token addresses (modify these) +L1_TOKEN_ADDRESS="0x000000000000000000000000000000000000dead" +L2_TOKEN_ADDRESS="0x000000000000000000000000000000000000dead" + +# Governance constants (do not modify) +readonly L1_ACTION_ADDRESS="0x997668Ee3C575dC060F80B06db0a8B04C9558969" +readonly L1_UPGRADE_EXECUTOR="0x3ffFbAdAF827559da092217e474760E2b2c3CeDd" +readonly L1_TIMELOCK="0xE6841D92B0C345144506576eC13ECf5103aC7f49" +readonly MAX_SUBMISSION_FEE="0.0005" +readonly TOTAL_VALUE="0.001" +readonly DELAY_SECONDS=259200 + +L1CALL=$(cast calldata \ + "perform(address[],address[],uint256,uint256,uint256,uint256,uint256,uint256)" \ + "[$L1_TOKEN_ADDRESS]" \ + "[$L2_TOKEN_ADDRESS]" \ + 0 \ + 0 \ + "$(cast to-wei "$MAX_SUBMISSION_FEE")" \ + 0 \ + 0 \ + "$(cast to-wei "$MAX_SUBMISSION_FEE")") +L1CALLVALUE=$(cast to-wei "$TOTAL_VALUE") +L2CALL=$(cast calldata \ + "execute(address,bytes)" \ + "$L1_ACTION_ADDRESS" \ + "$L1CALL") +PREDECESSOR=$(cast to-bytes32 0x00) +SALT=$(cast keccak \ + "$(cast abi-encode \ + "a(uint256[],address[])" \ + "[1]" \ + "[$L1_ACTION_ADDRESS]")") +FINAL_CALLDATA=$(cast calldata \ + "scheduleBatch(address[],uint256[],bytes[],bytes32,bytes32,uint256)" \ + "[$L1_UPGRADE_EXECUTOR]" \ + "[$L1CALLVALUE]" \ + "[$L2CALL]" \ + "$PREDECESSOR" \ + "$SALT" \ + "$DELAY_SECONDS") + +echo "===== Proposal =====" +echo "Target Contract: 0x0000000000000000000000000000000000000064" +echo "Value: 0" +echo "arbSysSendTxToL1Args.l1Timelock: " $L1_TIMELOCK +echo "arbSysSendTxToL1Args.calldata:" +echo "$FINAL_CALLDATA" +``` + +Run it: + +```shell +chmod +x reg-arb-custom.sh +./reg-arb-custom.sh +``` + +The script prints the four values you'll paste into the Tally proposal: the target contract (`ArbSys` precompile at `0x...0064`), the value (always `0`), the L1 Timelock address, and the encoded calldata that schedules the batched call on the L1 timelock. + +For a fully worked example, see the [BORING token registration payload gist](https://gist.github.com/hajnalben/b12cfae78ec88259be8c396c25bab1c2). + +## Step 2: Submit the Tally proposal + +Open a new proposal in [Tally](https://www.tally.xyz/gov/arbitrum) targeting the Arbitrum Core governance contract: + +- **Target contract:** `0x0000000000000000000000000000000000000064` (the `ArbSys` precompile) +- **Value:** `0` +- **Function:** `sendTxToL1(address destination, bytes data)` + - `destination`: the L1 Timelock address printed by the script + - `data`: the calldata printed by the script + +Before publishing, post a draft on the [Arbitrum Foundation forum](https://forum.arbitrum.foundation/) for community review. Token registrations have historically passed without controversy, but the off-chain feedback step catches encoding mistakes. + +## Step 3: After the proposal passes + +Once the proposal succeeds, the execution sequence is: + +1. The Arbitrum Core governance contract calls `ArbSys.sendTxToL1`, queuing a message from the child chain to the parent chain. +2. After the standard withdrawal delay, the L1 outbox executes the message, calling `scheduleBatch` on the L1 Timelock. +3. After the timelock's `DELAY_SECONDS` (3 days) elapses, anyone can call `executeBatch`, which has the `UpgradeExecutor` invoke `RegisterAndSetArbCustomGatewayAction.perform`. +4. The action contract calls `forceRegisterTokenToL2` on the parent chain generic-custom gateway and `setGateways` on the parent chain gateway router. Each call sends a retryable ticket to the child chain. +5. Both retryables auto-redeem (they're funded by the `MAX_SUBMISSION_FEE` constants), updating the L2 mappings. + +When all retryables are redeemed, the token is registered. Deposits and withdrawals through the generic-custom gateway will work normally from that point on. + +## Frequently asked questions + +### What if I get the L2 address wrong in the proposal? + +Registration is one-time and irreversible per (parent token) address — `forceRegisterTokenToL2` reverts on a second attempt. If a bad mapping is registered, recovery requires a second governance proposal with a new action contract that reregisters via different methods. Validate addresses carefully before submitting. + +### Why does the proposal target the `ArbSys` precompile (`0x...0064`)? + +Arbitrum DAO proposals execute on the child chain, but the registration calls happen on the parent chain. `ArbSys.sendTxToL1` is the precompile that creates outbound messages from the child chain to the parent chain. The proposal's child chain transaction queues the parent chain call; the L1 Timelock + UpgradeExecutor then dispatch it. + +### Is the standardized template mandatory? + +No. Per the [Foundation announcement](https://forum.arbitrum.foundation/t/announcement-of-standardized-token-registrations-template/29764), the template is a helpful utility, not a requirement. Custom proposals that achieve the same registration result are valid, but reviewers and the Foundation will scrutinize them more closely. + +## Resources + +- [Forum announcement: Standardized Token Registrations Template](https://forum.arbitrum.foundation/t/announcement-of-standardized-token-registrations-template/29764) +- [`RegisterAndSetArbCustomGatewayAction.sol`](https://github.com/OffchainLabs/governance/blob/main/src/gov-action-contracts/token-bridge/RegisterAndSetArbCustomGatewayAction.sol) (action contract source) +- [Payload generator gist](https://gist.github.com/gzeoneth/439dedc7bdd971345657a04c266daf00) (script used in Step 1) +- [Self-service generic-custom gateway setup](/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx) +- [Token bridging concept](/how-arbitrum-works/deep-dives/token-bridging.mdx) diff --git a/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx b/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx index b64e01eb81..57a23d0b2e 100644 --- a/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx +++ b/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx @@ -23,8 +23,8 @@ First of all, the **parent chain counterpart of the token** must conform to the These methods are needed to register the token via the gateway contract. If your parent chain contract does not include these methods and it is not upgradeable, you could register in one of these ways: -- As a chain owner, register via an [Arbitrum DAO](https://forum.arbitrum.foundation/) proposal. -- By wrapping your parent chain token and registering the wrapped version of your token. +- Submit an [Arbitrum DAO proposal using the standardized token registrations template](/build-decentralized-apps/token-bridging/configure-token-bridging/register-via-dao-governance.mdx). +- Wrap your parent chain token and register the wrapped version of your token. Note that registration is a one-time event. @@ -368,7 +368,7 @@ No, you can only register once a child chain token for the same parent chain tok ### What can I do if my parent chain token is not upgradable? -As mentioned on the concept page, token registration can also be completed as a chain owner registration via an **[Arbitrum DAO](https://forum.arbitrum.foundation/)** proposal. +Token registration can be completed through an Arbitrum DAO proposal using the standardized template. See [Register a custom gateway token via Arbitrum DAO governance](/build-decentralized-apps/token-bridging/configure-token-bridging/register-via-dao-governance.mdx) for the full process. ### Can I set up the generic-custom gateway after a standard **ERC-20** token has been deployed on the child chain? diff --git a/sidebars.js b/sidebars.js index e9a2e3c460..f4124807fc 100644 --- a/sidebars.js +++ b/sidebars.js @@ -1337,6 +1337,11 @@ const sidebars = { label: 'Set up standard gateway', id: 'build-decentralized-apps/token-bridging/configure-token-bridging/setup-standard-gateway', }, + { + type: 'doc', + label: 'Register via DAO governance', + id: 'build-decentralized-apps/token-bridging/configure-token-bridging/register-via-dao-governance', + }, ], }, ], From 89136b428d8bfdb39f415e579ed3307ce4495755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Tue, 12 May 2026 20:43:14 -0700 Subject: [PATCH 02/18] remove duplicate content, this belongs in the AF repository --- .../register-via-dao-governance.mdx | 177 ------------------ 1 file changed, 177 deletions(-) delete mode 100644 docs/build-decentralized-apps/token-bridging/configure-token-bridging/register-via-dao-governance.mdx diff --git a/docs/build-decentralized-apps/token-bridging/configure-token-bridging/register-via-dao-governance.mdx b/docs/build-decentralized-apps/token-bridging/configure-token-bridging/register-via-dao-governance.mdx deleted file mode 100644 index 6340cf74b9..0000000000 --- a/docs/build-decentralized-apps/token-bridging/configure-token-bridging/register-via-dao-governance.mdx +++ /dev/null @@ -1,177 +0,0 @@ ---- -title: 'Register a custom gateway token via Arbitrum DAO governance' -sidebar_label: 'Register via DAO governance' -description: "Guide to registering a token in Arbitrum's generic-custom gateway when self-service registration isn't possible. Uses the standardized RegisterAndSetArbCustomGatewayAction template and a DAO proposal." -user_story: 'As a token issuer whose parent chain token cannot self-register with the generic-custom gateway, I want to register the token through an Arbitrum DAO governance proposal.' -content_type: how-to -displayed_sidebar: buildAppsSidebar ---- - -[Self-service registration](/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx) requires the parent chain token to implement [`ICustomToken`](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/ICustomToken.sol) and call `registerCustomL2Token` and `setGateway` itself. When the parent chain token is non-upgradeable, immutable, or otherwise can't make those calls, registration must go through Arbitrum DAO governance using the privileged `forceRegisterTokenToL2` and `setGateways` paths. - -Offchain Labs publishes a standardized calldata template — `RegisterAndSetArbCustomGatewayAction` — together with a payload generator script. Using the template is a [helpful utility, not a requirement](https://forum.arbitrum.foundation/t/announcement-of-standardized-token-registrations-template/29764), but it reduces manual debugging and gives reviewers a known-safe shape to verify against. - -## When to use this path - -Use this article if: - -- Your parent chain token is already deployed and **can't be upgraded** to add the `ICustomToken` methods. -- You'd rather not [wrap the token](/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx#step-1-review-the-prerequisites) and register the wrapper instead. -- A child chain counterpart implementing [`IArbToken`](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/arbitrum/IArbToken.sol) is already deployed. - -If your parent chain token is upgradeable or you're building it from scratch, follow the [self-service generic-custom gateway setup](/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx) instead — it's faster and doesn't require a governance vote. - -## How the action contract works - -`RegisterAndSetArbCustomGatewayAction` is a one-shot action contract executed by the DAO's `UpgradeExecutor` via the L1 timelock. It performs the two registration calls in a single privileged transaction: - -```solidity -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.16; - -contract RegisterAndSetArbCustomGatewayAction { - IL1AddressRegistry public immutable addressRegistry; - - function perform( - address[] memory _l1Tokens, - address[] memory _l2Tokens, - uint256 _maxGasForRegister, - uint256 _gasPriceBidForRegister, - uint256 _maxSubmissionCostForRegister, - uint256 _maxGasForSetGateway, - uint256 _gasPriceBidForSetGateway, - uint256 _maxSubmissionCostForSetGateway - ) external payable { - // 1. forceRegisterTokenToL2 on the L1 generic-custom gateway - // 2. setGateways on the L1 gateway router (pointing _l1Tokens at the generic-custom gateway) - } -} -``` - -Source: [`RegisterAndSetArbCustomGatewayAction.sol`](https://github.com/OffchainLabs/governance/blob/main/src/gov-action-contracts/token-bridge/RegisterAndSetArbCustomGatewayAction.sol). - -The two calls each emit a retryable ticket from the parent chain to the child chain. Both retryables are auto-redeemed when the action contract supplies enough submission cost, after which the token is fully registered on both chains. - -## Prerequisites - -Before generating a proposal, make sure: - -- The parent chain ERC-20 token is **already deployed**. -- The child chain token contract implementing [`IArbToken`](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/arbitrum/IArbToken.sol) is **already deployed**. -- Both addresses are final and immutable — once registered, the mapping cannot be changed. -- You have [Foundry](https://book.getfoundry.sh/getting-started/installation) installed locally (the payload generator uses `cast`). - -## Step 1: Generate the proposal calldata - -Save the following script as `reg-arb-custom.sh`. Set `L1_TOKEN_ADDRESS` and `L2_TOKEN_ADDRESS` to your token's parent and child chain addresses. Leave the other constants alone — they reference live governance contracts on Ethereum and the canonical action address. - -```shell -#!/usr/bin/env bash -set -euo pipefail - -# Token addresses (modify these) -L1_TOKEN_ADDRESS="0x000000000000000000000000000000000000dead" -L2_TOKEN_ADDRESS="0x000000000000000000000000000000000000dead" - -# Governance constants (do not modify) -readonly L1_ACTION_ADDRESS="0x997668Ee3C575dC060F80B06db0a8B04C9558969" -readonly L1_UPGRADE_EXECUTOR="0x3ffFbAdAF827559da092217e474760E2b2c3CeDd" -readonly L1_TIMELOCK="0xE6841D92B0C345144506576eC13ECf5103aC7f49" -readonly MAX_SUBMISSION_FEE="0.0005" -readonly TOTAL_VALUE="0.001" -readonly DELAY_SECONDS=259200 - -L1CALL=$(cast calldata \ - "perform(address[],address[],uint256,uint256,uint256,uint256,uint256,uint256)" \ - "[$L1_TOKEN_ADDRESS]" \ - "[$L2_TOKEN_ADDRESS]" \ - 0 \ - 0 \ - "$(cast to-wei "$MAX_SUBMISSION_FEE")" \ - 0 \ - 0 \ - "$(cast to-wei "$MAX_SUBMISSION_FEE")") -L1CALLVALUE=$(cast to-wei "$TOTAL_VALUE") -L2CALL=$(cast calldata \ - "execute(address,bytes)" \ - "$L1_ACTION_ADDRESS" \ - "$L1CALL") -PREDECESSOR=$(cast to-bytes32 0x00) -SALT=$(cast keccak \ - "$(cast abi-encode \ - "a(uint256[],address[])" \ - "[1]" \ - "[$L1_ACTION_ADDRESS]")") -FINAL_CALLDATA=$(cast calldata \ - "scheduleBatch(address[],uint256[],bytes[],bytes32,bytes32,uint256)" \ - "[$L1_UPGRADE_EXECUTOR]" \ - "[$L1CALLVALUE]" \ - "[$L2CALL]" \ - "$PREDECESSOR" \ - "$SALT" \ - "$DELAY_SECONDS") - -echo "===== Proposal =====" -echo "Target Contract: 0x0000000000000000000000000000000000000064" -echo "Value: 0" -echo "arbSysSendTxToL1Args.l1Timelock: " $L1_TIMELOCK -echo "arbSysSendTxToL1Args.calldata:" -echo "$FINAL_CALLDATA" -``` - -Run it: - -```shell -chmod +x reg-arb-custom.sh -./reg-arb-custom.sh -``` - -The script prints the four values you'll paste into the Tally proposal: the target contract (`ArbSys` precompile at `0x...0064`), the value (always `0`), the L1 Timelock address, and the encoded calldata that schedules the batched call on the L1 timelock. - -For a fully worked example, see the [BORING token registration payload gist](https://gist.github.com/hajnalben/b12cfae78ec88259be8c396c25bab1c2). - -## Step 2: Submit the Tally proposal - -Open a new proposal in [Tally](https://www.tally.xyz/gov/arbitrum) targeting the Arbitrum Core governance contract: - -- **Target contract:** `0x0000000000000000000000000000000000000064` (the `ArbSys` precompile) -- **Value:** `0` -- **Function:** `sendTxToL1(address destination, bytes data)` - - `destination`: the L1 Timelock address printed by the script - - `data`: the calldata printed by the script - -Before publishing, post a draft on the [Arbitrum Foundation forum](https://forum.arbitrum.foundation/) for community review. Token registrations have historically passed without controversy, but the off-chain feedback step catches encoding mistakes. - -## Step 3: After the proposal passes - -Once the proposal succeeds, the execution sequence is: - -1. The Arbitrum Core governance contract calls `ArbSys.sendTxToL1`, queuing a message from the child chain to the parent chain. -2. After the standard withdrawal delay, the L1 outbox executes the message, calling `scheduleBatch` on the L1 Timelock. -3. After the timelock's `DELAY_SECONDS` (3 days) elapses, anyone can call `executeBatch`, which has the `UpgradeExecutor` invoke `RegisterAndSetArbCustomGatewayAction.perform`. -4. The action contract calls `forceRegisterTokenToL2` on the parent chain generic-custom gateway and `setGateways` on the parent chain gateway router. Each call sends a retryable ticket to the child chain. -5. Both retryables auto-redeem (they're funded by the `MAX_SUBMISSION_FEE` constants), updating the L2 mappings. - -When all retryables are redeemed, the token is registered. Deposits and withdrawals through the generic-custom gateway will work normally from that point on. - -## Frequently asked questions - -### What if I get the L2 address wrong in the proposal? - -Registration is one-time and irreversible per (parent token) address — `forceRegisterTokenToL2` reverts on a second attempt. If a bad mapping is registered, recovery requires a second governance proposal with a new action contract that reregisters via different methods. Validate addresses carefully before submitting. - -### Why does the proposal target the `ArbSys` precompile (`0x...0064`)? - -Arbitrum DAO proposals execute on the child chain, but the registration calls happen on the parent chain. `ArbSys.sendTxToL1` is the precompile that creates outbound messages from the child chain to the parent chain. The proposal's child chain transaction queues the parent chain call; the L1 Timelock + UpgradeExecutor then dispatch it. - -### Is the standardized template mandatory? - -No. Per the [Foundation announcement](https://forum.arbitrum.foundation/t/announcement-of-standardized-token-registrations-template/29764), the template is a helpful utility, not a requirement. Custom proposals that achieve the same registration result are valid, but reviewers and the Foundation will scrutinize them more closely. - -## Resources - -- [Forum announcement: Standardized Token Registrations Template](https://forum.arbitrum.foundation/t/announcement-of-standardized-token-registrations-template/29764) -- [`RegisterAndSetArbCustomGatewayAction.sol`](https://github.com/OffchainLabs/governance/blob/main/src/gov-action-contracts/token-bridge/RegisterAndSetArbCustomGatewayAction.sol) (action contract source) -- [Payload generator gist](https://gist.github.com/gzeoneth/439dedc7bdd971345657a04c266daf00) (script used in Step 1) -- [Self-service generic-custom gateway setup](/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx) -- [Token bridging concept](/how-arbitrum-works/deep-dives/token-bridging.mdx) From 593c84cb7b961d883043269a0f62a8299d5ad5d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Tue, 12 May 2026 20:44:14 -0700 Subject: [PATCH 03/18] add external link to AF docs --- sidebars.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sidebars.js b/sidebars.js index 9dc465d1af..f46a397b27 100644 --- a/sidebars.js +++ b/sidebars.js @@ -1343,9 +1343,9 @@ const sidebars = { id: 'build-decentralized-apps/token-bridging/configure-token-bridging/setup-standard-gateway', }, { - type: 'doc', + type: 'link', label: 'Register via DAO governance', - id: 'build-decentralized-apps/token-bridging/configure-token-bridging/register-via-dao-governance', + id: 'https://docs.arbitrum.foundation/how-tos/register-token-via-dao-governance', }, ], }, From 4e139ebfe8eded2b1d46decb173744604e853f8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Wed, 13 May 2026 10:59:53 -0700 Subject: [PATCH 04/18] point DAO token registration to AF docs --- .../setup-generic-custom-gateway.mdx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx b/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx index 57a23d0b2e..9d3fb9a7da 100644 --- a/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx +++ b/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx @@ -23,10 +23,9 @@ First of all, the **parent chain counterpart of the token** must conform to the These methods are needed to register the token via the gateway contract. If your parent chain contract does not include these methods and it is not upgradeable, you could register in one of these ways: -- Submit an [Arbitrum DAO proposal using the standardized token registrations template](/build-decentralized-apps/token-bridging/configure-token-bridging/register-via-dao-governance.mdx). +- Submit an [Arbitrum DAO proposal using the standardized token registrations template](https://docs.arbitrum.foundation/how-tos/register-token-via-dao-governance). - Wrap your parent chain token and register the wrapped version of your token. - -Note that registration is a one-time event. + Note that registration is a one-time event. Also, the **child chain counterpart of the token** must conform to the [`IArbToken`](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/arbitrum/IArbToken.sol) interface, meaning that: From 0af4c21126ad51b768c82d2f1c543c89c1feeb11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Wed, 13 May 2026 11:03:15 -0700 Subject: [PATCH 05/18] point second DAO token registration link to the AF doc --- .../configure-token-bridging/setup-generic-custom-gateway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx b/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx index 9d3fb9a7da..040c54c39d 100644 --- a/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx +++ b/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx @@ -367,7 +367,7 @@ No, you can only register once a child chain token for the same parent chain tok ### What can I do if my parent chain token is not upgradable? -Token registration can be completed through an Arbitrum DAO proposal using the standardized template. See [Register a custom gateway token via Arbitrum DAO governance](/build-decentralized-apps/token-bridging/configure-token-bridging/register-via-dao-governance.mdx) for the full process. +Token registration can be completed through an Arbitrum DAO proposal using the standardized template. See [Register a custom gateway token via Arbitrum DAO governance](https://docs.arbitrum.foundation/how-tos/register-token-via-dao-governance) for the full process. ### Can I set up the generic-custom gateway after a standard **ERC-20** token has been deployed on the child chain? From 552f072b12b022043c41c842917af934fb05786b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Wed, 13 May 2026 15:16:07 -0700 Subject: [PATCH 06/18] add missing docs/how-arbitrum-works/deep-dives/token-bridging.mdx to sidebars.js --- sidebars.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/sidebars.js b/sidebars.js index f46a397b27..24552b5598 100644 --- a/sidebars.js +++ b/sidebars.js @@ -1031,6 +1031,11 @@ const sidebars = { label: 'AnyTrust', id: 'how-arbitrum-works/deep-dives/anytrust-protocol', }, + { + type: 'doc', + label: 'Token bridging', + id: 'how-arbitrum-works/deep-dives/token-bridging', + }, { type: 'doc', label: 'ArbOS', @@ -1342,11 +1347,11 @@ const sidebars = { label: 'Set up standard gateway', id: 'build-decentralized-apps/token-bridging/configure-token-bridging/setup-standard-gateway', }, - { - type: 'link', - label: 'Register via DAO governance', - id: 'https://docs.arbitrum.foundation/how-tos/register-token-via-dao-governance', - }, + // { + // type: 'link', + // label: 'Register via DAO governance', + // id: 'https://docs.arbitrum.foundation/how-tos/register-token-via-dao-governance', + // }, ], }, ], From 3723c2906c93836fd6612e16884d5471c4eb6252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Thu, 14 May 2026 14:11:45 -0700 Subject: [PATCH 07/18] attempt: making setup gateway section clearer and more cohesive --- .../setup-custom-gateway.mdx | 259 ++++++++++-------- .../setup-generic-custom-gateway.mdx | 227 ++++++++++----- .../setup-standard-gateway.mdx | 40 +-- .../token-bridging/get-started.mdx | 24 +- 4 files changed, 351 insertions(+), 199 deletions(-) diff --git a/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-custom-gateway.mdx b/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-custom-gateway.mdx index 603435851b..031f229820 100644 --- a/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-custom-gateway.mdx +++ b/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-custom-gateway.mdx @@ -2,26 +2,43 @@ title: 'Configure custom gateway bridging' description: 'Guide to creating and deploying a custom gateway for specialized token bridging between Ethereum and Arbitrum. For advanced use cases not covered by the standard or generic-custom gateways.' user_story: 'As a developer, I want to deploy a custom token bridge gateway so I can implement specialized bridging logic for my token between Ethereum and Arbitrum.' -reader_audience: developers who want to build on Ethereum/Arbitrum and bridge tokens between parent and child chains content_type: how-to +author: anegg0 +sme: TBD displayed_sidebar: buildAppsSidebar --- -:::caution Do you really need a custom gateway? +import TokenBridgingPrereqs from '../partials/_token-bridging-prereqs.mdx'; +import TokenBridgingNextSteps from '../partials/_token-bridging-next-steps.mdx'; +import TokenBridgingResources from '../partials/_token-bridging-resources.mdx'; -Before implementing and deploying a custom gateway, it is strongly encouraged to analyze the current solutions that Arbitrum's token bridge provides: the [standard gateway](/build-decentralized-apps/token-bridging/configure-token-bridging/setup-standard-gateway.mdx) and the [generic-custom gateway](/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx). These solutions provide enough functionality to solve the majority of bridging needs from projects. And if you are in doubt about your current approach, you can always ask for assistance on our [Discord server](https://discord.gg/arbitrum). + -::: +**Do you really need a custom gateway?** Before deploying one, evaluate Arbitrum's built-in options: the [standard gateway](/build-decentralized-apps/token-bridging/configure-token-bridging/setup-standard-gateway.mdx) and the [generic-custom gateway](/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx) cover the majority of bridging needs. If in doubt, ask on the [Arbitrum Discord](https://discord.gg/arbitrum). -In this how-to, you'll learn how to bridge your own token between Ethereum (the parent chain) and Arbitrum (the child chain), using a custom gateway. For alternative ways of bridging tokens, check out the [token bridging overview](/build-decentralized-apps/token-bridging/get-started.mdx). + -Familiarity with [Arbitrum's token bridge system](/how-arbitrum-works/deep-dives/token-bridging.mdx), smart contracts, and decentralized application development is expected. If you're new to developing on Arbitrum, consider reviewing our [Quickstart: Build a dApp with Arbitrum (Solidity, Remix)](/build-decentralized-apps/01-quickstart-solidity-remix.mdx) before proceeding. We'll use [Arbitrum's SDK](https://github.com/OffchainLabs/arbitrum-sdk) throughout this how-to, although no prior knowledge is required. +In this how-to, you'll learn how to bridge your own token between Ethereum (parent chain) and Arbitrum (child chain) using a custom gateway. For alternative ways of bridging tokens, see the [token bridging overview](/build-decentralized-apps/token-bridging/get-started.mdx). -We will go through all the steps involved in the process. However, if you want to jump straight to the code, we have created a [custom gateway bridging tutorial script](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/custom-gateway-bridging) that encapsulates the entire process. +For other gateway types, see: + +- [Standard gateway](/build-decentralized-apps/token-bridging/configure-token-bridging/setup-standard-gateway.mdx) — for standard **ERC-20** tokens with no custom logic. +- [Generic-custom gateway](/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx) — when you only need a custom child chain token, not custom bridge mechanics. + + + +**Audience and scope.** This guide covers deploying a custom gateway on Arbitrum One. The contracts and SDK calls generalize to any Arbitrum chain, but the registration step differs: + +- **Arbitrum One or Nova:** custom gateway registration must go through Arbitrum DAO governance. +- **An Arbitrum chain you operate:** as the chain owner, register your custom gateway directly through your chain's `UpgradeExecutor`. No DAO vote is required. + + + + -## Step 0: Review the prerequisites (a.k.a. do I really need a custom gateway?) +We will go through all the steps involved in the process. However, if you want to jump straight to the code, we have created a [custom gateway bridging tutorial script](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/custom-gateway-bridging) that encapsulates the entire process. -Before implementing and deploying a custom gateway, it is strongly encouraged to analyze the current solutions that Arbitrum’s token bridge provides: the [standard gateway](/build-decentralized-apps/token-bridging/deposit-tokens.mdx) and the [generic-custom gateway](/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx). These solutions provide enough functionality to solve the majority of bridging needs from projects. And if you are in doubt about your current approach, you can always ask for assistance on our [Discord server](https://discord.gg/arbitrum). +## Prerequisites There are several prerequisites to consider when deploying your own custom gateway. @@ -48,11 +65,11 @@ If you are deploying custom gateways, you will likely want to support your custo ## Step 1: Create a gateway and deploy it on the parent chain -:::caution This code is for testing purposes + The code in the following sections is intended for testing purposes only and doesn't guarantee any level of security. It hasn't undergone any formal audit or security analysis, so it isn't ready for production use. Exercise caution and due diligence while using this code in any environment. -::: + We'll begin by creating our custom gateway and deploying it to the parent chain. A good example of a custom gateway is [Arbitrum’s generic-custom gateway](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol). It implements all required methods and adds additional methods, enabling the gateway to support a wide variety of tokens for bridging. @@ -363,46 +380,42 @@ abstract contract L1CrosschainMessenger { We now deploy that gateway to the parent chain. -```tsx -const { ethers } = require('hardhat'); -const { providers, Wallet, BigNumber } = require('ethers'); -const { getArbitrumNetwork, ParentToChildMessageStatus } = require('@arbitrum/sdk'); -const { - AdminErc20Bridger, - Erc20Bridger, -} = require('@arbitrum/sdk/dist/lib/assetBridger/erc20Bridger'); -require('dotenv').config(); +```ts +import { ethers } from 'hardhat'; +import { providers, Wallet } from 'ethers'; +import { getArbitrumNetwork, AdminErc20Bridger, Erc20Bridger } from '@arbitrum/sdk'; +import 'dotenv/config'; /** - * Set up: instantiate L1 / L2 wallets connected to providers + * Set up: instantiate parent and child chain wallets connected to providers. */ const walletPrivateKey = process.env.DEVNET_PRIVKEY; -const l1Provider = new providers.JsonRpcProvider(process.env.L1RPC); -const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC); -const l1Wallet = new Wallet(walletPrivateKey, l1Provider); -const l2Wallet = new Wallet(walletPrivateKey, l2Provider); +const parentProvider = new providers.JsonRpcProvider(process.env.PARENT_RPC); +const childProvider = new providers.JsonRpcProvider(process.env.CHILD_RPC); +const parentWallet = new Wallet(walletPrivateKey, parentProvider); +const childWallet = new Wallet(walletPrivateKey, childProvider); const main = async () => { /** - * Use l2Network to create an Arbitrum SDK AdminErc20Bridger instance - * We'll use AdminErc20Bridger for its convenience methods around registering tokens to a custom gateway + * Use the Arbitrum network info to create an AdminErc20Bridger instance. + * We'll use AdminErc20Bridger for its convenience methods around registering tokens to a custom gateway. */ - const l2Network = await getArbitrumNetwork(l2Provider); - const erc20Bridger = new Erc20Bridger(l2Network); - const adminTokenBridger = new AdminErc20Bridger(l2Network); - const l1Router = l2Network.tokenBridge.parentGatewayRouter; - const l2Router = l2Network.tokenBridge.childGatewayRouter; - const inbox = l2Network.ethBridge.inbox; + const arbitrumNetwork = await getArbitrumNetwork(childProvider); + const erc20Bridger = new Erc20Bridger(arbitrumNetwork); + const adminTokenBridger = new AdminErc20Bridger(arbitrumNetwork); + const parentGatewayRouter = arbitrumNetwork.tokenBridge.l1GatewayRouter; + const childGatewayRouter = arbitrumNetwork.tokenBridge.l2GatewayRouter; + const inbox = arbitrumNetwork.ethBridge.inbox; /** - * Deploy our custom gateway to L1 + * Deploy our custom gateway to the parent chain. */ - const L1CustomGateway = await await ethers.getContractFactory('L1CustomGateway', l1Wallet); - console.log('Deploying custom gateway to L1'); - const l1CustomGateway = await L1CustomGateway.deploy(l1Router, inbox); - await l1CustomGateway.deployed(); - console.log(`Custom gateway is deployed to L1 at ${l1CustomGateway.address}`); - const l1CustomGatewayAddress = l1CustomGateway.address; + const L1CustomGateway = await ethers.getContractFactory('L1CustomGateway', parentWallet); + console.log('Deploying custom gateway to the parent chain'); + const parentCustomGateway = await L1CustomGateway.deploy(parentGatewayRouter, inbox); + await parentCustomGateway.deployed(); + console.log(`Custom gateway is deployed to the parent chain at ${parentCustomGateway.address}`); + const parentCustomGatewayAddress = parentCustomGateway.address; }; main() @@ -685,47 +698,41 @@ abstract contract L2CrosschainMessenger { We now deploy that gateway to the child chain. -```tsx -const { ethers } = require('hardhat'); -const { providers, Wallet, BigNumber } = require('ethers'); -const { - getArbitrumNetwork, - ParentToChildMessageStatus, - AdminErc20Bridger, - Erc20Bridger, -} = require('@arbitrum/sdk'); -require('dotenv').config(); +```ts +import { ethers } from 'hardhat'; +import { providers, Wallet } from 'ethers'; +import { getArbitrumNetwork, AdminErc20Bridger, Erc20Bridger } from '@arbitrum/sdk'; +import 'dotenv/config'; /** - * Set up: instantiate L1 / L2 wallets connected to providers + * Set up: instantiate parent and child chain wallets connected to providers. */ const walletPrivateKey = process.env.DEVNET_PRIVKEY; -const l1Provider = new providers.JsonRpcProvider(process.env.L1RPC); -const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC); -const l1Wallet = new Wallet(walletPrivateKey, l1Provider); -const l2Wallet = new Wallet(walletPrivateKey, l2Provider); +const parentProvider = new providers.JsonRpcProvider(process.env.PARENT_RPC); +const childProvider = new providers.JsonRpcProvider(process.env.CHILD_RPC); +const parentWallet = new Wallet(walletPrivateKey, parentProvider); +const childWallet = new Wallet(walletPrivateKey, childProvider); const main = async () => { /** - * Use l2Network to create an Arbitrum SDK AdminErc20Bridger instance - * We'll use AdminErc20Bridger for its convenience methods around registering tokens to a custom gateway + * Use the Arbitrum network info to create an AdminErc20Bridger instance. */ - const l2Network = await getArbitrumNetwork(l2Provider); - const erc20Bridger = new Erc20Bridger(l2Network); - const adminTokenBridger = new AdminErc20Bridger(l2Network); - const l1Router = l2Network.tokenBridge.l1GatewayRouter; - const l2Router = l2Network.tokenBridge.l2GatewayRouter; - const inbox = l2Network.ethBridge.inbox; + const arbitrumNetwork = await getArbitrumNetwork(childProvider); + const erc20Bridger = new Erc20Bridger(arbitrumNetwork); + const adminTokenBridger = new AdminErc20Bridger(arbitrumNetwork); + const parentGatewayRouter = arbitrumNetwork.tokenBridge.l1GatewayRouter; + const childGatewayRouter = arbitrumNetwork.tokenBridge.l2GatewayRouter; + const inbox = arbitrumNetwork.ethBridge.inbox; /** - * Deploy our custom gateway to L2 + * Deploy our custom gateway to the child chain. */ - const L2CustomGateway = await await ethers.getContractFactory('L2CustomGateway', l2Wallet); - console.log('Deploying custom gateway to L2'); - const l2CustomGateway = await L2CustomGateway.deploy(l2Router); - await l2CustomGateway.deployed(); - console.log(`Custom gateway is deployed to L2 at ${l2CustomGateway.address}`); - const l2CustomGatewayAddress = l2CustomGateway.address; + const L2CustomGateway = await ethers.getContractFactory('L2CustomGateway', childWallet); + console.log('Deploying custom gateway to the child chain'); + const childCustomGateway = await L2CustomGateway.deploy(childGatewayRouter); + await childCustomGateway.deployed(); + console.log(`Custom gateway is deployed to the child chain at ${childCustomGateway.address}`); + const childCustomGatewayAddress = childCustomGateway.address; }; main() @@ -746,35 +753,35 @@ We won't go through the process of deploying custom tokens in this how-to, but y This step will also depend on your setup. In this case, our simplified gateway requires the method `setTokenBridgeInformation` to be called on both gateways to set the addresses of the counterpart gateway and both custom tokens. -```tsx +```ts /** - * Set the token bridge information on the custom gateways + * Set the token bridge information on the custom gateways. * (This is an optional step that depends on your configuration. In this example, we've added one-shot * functions on the custom gateways to set the token bridge addresses in a second step. This could be - * avoided if you are using proxies or the opcode CREATE2 for example) + * avoided if you are using proxies or the opcode CREATE2, for example.) */ console.log('Setting token bridge information on L1CustomGateway:'); -const setTokenBridgeInfoOnL1 = await l1CustomGateway.setTokenBridgeInformation( - l1CustomToken.address, - l2CustomToken.address, - l2CustomGatewayAddress, +const setTokenBridgeInfoOnParent = await parentCustomGateway.setTokenBridgeInformation( + parentCustomToken.address, + childCustomToken.address, + childCustomGatewayAddress, ); -const setTokenBridgeInfoOnL1Rec = await setTokenBridgeInfoOnL1.wait(); +const setTokenBridgeInfoOnParentRec = await setTokenBridgeInfoOnParent.wait(); console.log( - `Token bridge information set on L1CustomGateway! L1 receipt is: ${setTokenBridgeInfoOnL1Rec.transactionHash}`, + `Token bridge information set on L1CustomGateway! Receipt: ${setTokenBridgeInfoOnParentRec.transactionHash}`, ); console.log('Setting token bridge information on L2CustomGateway:'); -const setTokenBridgeInfoOnL2 = await l2CustomGateway.setTokenBridgeInformation( - l1CustomToken.address, - l2CustomToken.address, - l1CustomGatewayAddress, +const setTokenBridgeInfoOnChild = await childCustomGateway.setTokenBridgeInformation( + parentCustomToken.address, + childCustomToken.address, + parentCustomGatewayAddress, ); -const setTokenBridgeInfoOnL2Rec = await setTokenBridgeInfoOnL2.wait(); +const setTokenBridgeInfoOnChildRec = await setTokenBridgeInfoOnChild.wait(); console.log( - `Token bridge information set on L2CustomGateway! L2 receipt is: ${setTokenBridgeInfoOnL2Rec.transactionHash}`, + `Token bridge information set on L2CustomGateway! Receipt: ${setTokenBridgeInfoOnChildRec.transactionHash}`, ); ``` @@ -790,60 +797,98 @@ In this case, when using this function, a single action occurs: To simplify the process, we’ll use Arbitrum’s SDK and call the method [`registerCustomToken`](https://github.com/OffchainLabs/arbitrum-sdk/blob/main/packages/sdk/src/lib/assetBridger/erc20Bridger.ts) of the [`AdminErc20Bridger`](https://github.com/OffchainLabs/arbitrum-sdk/blob/main/packages/sdk/src/lib/assetBridger/erc20Bridger.ts) class, which will call the `registerTokenOnL2` method of the token passed by parameter. -```tsx +```ts /** - * Register the custom gateway as the gateway of our custom token + * Register the custom gateway as the gateway of our custom token. */ -console.log('Registering custom token on L2:'); +console.log('Registering custom token on the child chain:'); const registerTokenTx = await adminTokenBridger.registerCustomToken( - l1CustomToken.address, - l2CustomToken.address, - l1Wallet, - l2Provider, + parentCustomToken.address, + childCustomToken.address, + parentWallet, + childProvider, ); const registerTokenRec = await registerTokenTx.wait(); console.log( - `Registering token txn confirmed on L1! 🙌 L1 receipt is: ${registerTokenRec.transactionHash}.`, + `Registering token txn confirmed on the parent chain! 🙌 Receipt: ${registerTokenRec.transactionHash}.`, ); console.log( - `Waiting for L2 retryable (takes 10-15 minutes); current time: ${new Date().toTimeString()})`, + `Waiting for child chain retryable (takes 10-15 minutes); current time: ${new Date().toTimeString()})`, ); /** - * The L1 side is confirmed; now we listen and wait for the L2 side to be executed; we can do this by computing the expected txn hash of the L2 transaction. - * To compute this txn hash, we need our message's "sequence numbers", unique identifiers of each L1 to L2 message. - * We'll fetch them from the event logs with a helper method. + * The parent chain side is confirmed; now we wait for the child chain side to execute. + * Each parent-to-child message has a unique sequence number; we fetch them from the event logs via a helper. */ -const l1ToL2Msgs = await registerTokenRec.getParentToChildMessages(l2Provider); +const parentToChildMsgs = await registerTokenRec.getParentToChildMessages(childProvider); /** - * In this case, the registerTokenOnL2 method creates 1 L1-to-L2 messages to set the L1 token to the Custom Gateway via the Router - * Here, We check if that message is redeemed on L2 + * `registerTokenOnL2` produces a single parent-to-child message that sets the parent chain token to the custom gateway via the router. + * We confirm it's redeemed on the child chain. */ -expect(l1ToL2Msgs.length, 'Should be 1 message.').to.eq(1); +expect(parentToChildMsgs.length, 'Should be 1 message.').to.eq(1); -const setGateways = await l1ToL2Msgs[0].waitForStatus(); +const setGateways = await parentToChildMsgs[0].waitForStatus(); expect(setGateways.status, 'Set gateways not redeemed.').to.eq(ParentToChildMessageStatus.REDEEMED); console.log('Your custom token and gateways are now registered on the token bridge 🥳!'); ``` +### Verify the registration + +Once the retryable has redeemed, confirm the new mapping on both chains. If the router returns the zero address for your token, registration didn't land: + +```ts +import { Contract } from 'ethers'; + +const routerAbi = ['function l1TokenToGateway(address) view returns (address)']; + +const parentRouter = new Contract( + arbitrumNetwork.tokenBridge.l1GatewayRouter, + routerAbi, + parentProvider, +); +const childRouter = new Contract( + arbitrumNetwork.tokenBridge.l2GatewayRouter, + routerAbi, + childProvider, +); + +console.log( + 'Parent router → gateway:', + await parentRouter.l1TokenToGateway(parentCustomToken.address), +); +console.log( + 'Child router → gateway: ', + await childRouter.l1TokenToGateway(parentCustomToken.address), +); +``` + +If either call returns `0x0000…0000`, the retryable likely failed. Inspect its status with `parentToChildMsgs[0].status()` and manually redeem if needed; see [Parent-to-child messaging](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.mdx) for the recovery procedure. + ## Conclusion Upon completion of all the steps, registration of your parent chain and child chain gateways in the token bridge will be complete, and both tokens will have connections through your custom gateway. The full code for this how-to and other more extensive deployment and testing scripts is available in the [custom gateway bridging tutorial package](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/custom-gateway-bridging) of our tutorials repository. +## Frequently asked questions + +### The retryable in Step 5 didn't redeem. How do I recover? + +`registerCustomToken` produces a single parent-to-child retryable that updates the child chain router mapping. If it fails to auto-redeem — for example, because the supplied gas wasn't enough — the verification call in [Step 5](#verify-the-registration) will return `0x0000…0000`. Manually redeem the failed retryable from the child chain by calling `redeem()` on the message; see [Parent-to-child messaging](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.mdx) for the procedure. Registration succeeds as soon as the retryable is redeemed. + +### Do I have to register through Arbitrum DAO governance? + +Only on Arbitrum One and Nova. If you operate your own Arbitrum chain, your chain owner can register the custom gateway directly through the chain's `UpgradeExecutor` without going through any DAO. + ## Next steps Your custom gateway is now configured! Users can: -- [Deposit tokens to the child chain](/build-decentralized-apps/token-bridging/deposit-tokens.mdx) -- [Withdraw tokens back to the parent chain](/build-decentralized-apps/token-bridging/withdraw-tokens.mdx) + ## Resources -1. [Concept page: Token Bridge](/how-arbitrum-works/deep-dives/token-bridging.mdx) -2. [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) -3. [Token bridge contract addresses](/build-decentralized-apps/reference/02-contract-addresses.mdx) + diff --git a/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx b/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx index 040c54c39d..2cabb158dc 100644 --- a/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx +++ b/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx @@ -3,12 +3,32 @@ title: 'Configure generic-custom gateway bridging' description: "Guide to configuring Arbitrum's generic-custom gateway for token bridging with custom child chain token functionality. Enables custom ERC-20 behavior while using Arbitrum's built-in bridge infrastructure." user_story: 'As a developer, I want to configure the generic-custom gateway so I can bridge my token with custom functionality on the Arbitrum child chain.' content_type: how-to +author: anegg0 +sme: TBD displayed_sidebar: buildAppsSidebar --- -In this how-to, you'll learn how to bridge your own token between Ethereum (parent chain) and Arbitrum (child chain), using [Arbitrum's generic-custom gateway](/how-arbitrum-works/deep-dives/token-bridging.mdx#the-arbitrum-generic-custom-gateway). For alternative ways of bridging tokens, check out the [token bridging overview](/how-arbitrum-works/deep-dives/token-bridging.mdx). +import TokenBridgingPrereqs from '../partials/_token-bridging-prereqs.mdx'; +import TokenBridgingNextSteps from '../partials/_token-bridging-next-steps.mdx'; +import TokenBridgingResources from '../partials/_token-bridging-resources.mdx'; -Familiarity with [Arbitrum's token bridge system](/how-arbitrum-works/deep-dives/token-bridging.mdx), smart contracts, and blockchain development is expected. If you're new to blockchain development, consider reviewing our [Quickstart: Build a dApp with Arbitrum (Solidity, Hardhat)](/build-decentralized-apps/01-quickstart-solidity-remix.mdx) before proceeding. We'll use [Arbitrum's SDK](https://github.com/OffchainLabs/arbitrum-sdk) throughout this how-to, although no prior knowledge is required. +In this how-to, you'll learn how to bridge your own token between Ethereum (parent chain) and Arbitrum (child chain), using [Arbitrum's generic-custom gateway](/how-arbitrum-works/deep-dives/token-bridging.mdx#the-arbitrum-generic-custom-gateway). For alternative ways of bridging tokens, check out the [token bridging overview](/build-decentralized-apps/token-bridging/get-started.mdx). + +For other gateway types, see: + +- [Standard gateway](/build-decentralized-apps/token-bridging/configure-token-bridging/setup-standard-gateway.mdx) — for standard **ERC-20** tokens with no custom logic. +- [Custom gateway](/build-decentralized-apps/token-bridging/configure-token-bridging/setup-custom-gateway.mdx) — for advanced bridge-level customization. + + + + + +**Audience and scope.** This guide covers registration on Arbitrum One. The Solidity interfaces, SDK calls, and on-chain mechanics generalize to any Arbitrum chain — including Arbitrum Nova and chains you operate yourself — but the registration fallback for non-upgradeable parent chain tokens differs: + +- **Arbitrum One or Nova:** register through Arbitrum DAO governance, or wrap your token (see [Step 1](#step-1-review-the-prerequisites)). +- **An Arbitrum chain you operate:** as the chain owner, call `forceRegisterTokenToL2` and `setGateways` directly through your chain's `UpgradeExecutor`. No DAO vote is involved. You will also need to register your chain with the SDK via [`registerCustomArbitrumNetwork`](/sdk/dataEntities/networks.md) before calling `getArbitrumNetwork`. + + We'll go through all the steps involved in the process. However, if you want to jump straight to the code, we've created a [custom token bridging tutorial script](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/custom-token-bridging) that encapsulates the entire process. @@ -21,11 +41,13 @@ First of all, the **parent chain counterpart of the token** must conform to the - It must have an `isArbitrumEnabled` method that returns `0xb1` - It must have a method that makes an external call to `L1CustomGateway.registerCustomL2Token` specifying the address of the child chain contract, and to `L1GatewayRouter.setGateway` specifying the address of the custom gateway. Make these calls only once to configure the gateway. -These methods are needed to register the token via the gateway contract. If your parent chain contract does not include these methods and it is not upgradeable, you could register in one of these ways: +These methods are needed to register the token via the gateway contract. If your parent chain contract does not include these methods and is not upgradeable, you can register through one of these alternative paths: -- Submit an [Arbitrum DAO proposal using the standardized token registrations template](https://docs.arbitrum.foundation/how-tos/register-token-via-dao-governance). -- Wrap your parent chain token and register the wrapped version of your token. - Note that registration is a one-time event. +- **On Arbitrum One or Nova:** submit an [Arbitrum DAO proposal using the standardized token registrations template](https://docs.arbitrum.foundation/how-tos/register-token-via-dao-governance). The DAO's `UpgradeExecutor` routes the privileged `forceRegisterTokenToL2` and `setGateways` calls on your behalf. +- **On an Arbitrum chain you operate:** as the chain owner, call `forceRegisterTokenToL2` on the parent chain custom gateway and `setGateways` on the parent chain gateway router directly through your chain's `UpgradeExecutor`. No DAO vote is required. +- **Any Arbitrum chain:** deploy a new upgradeable wrapper contract that holds your existing token and itself implements `ICustomToken`. Register the wrapper instead of the original. + +Registration is a one-time event for any given parent chain token address. Also, the **child chain counterpart of the token** must conform to the [`IArbToken`](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/arbitrum/IArbToken.sol) interface, meaning that: @@ -34,6 +56,8 @@ Also, the **child chain counterpart of the token** must conform to the [`IArbTok import TokenCompatibilityPartial from '../partials/_token-compatibility.mdx'; +The interfaces above also place a few constraints on how your token implements standard ERC-20 behavior. Review these before continuing: + ## Step 2: Create a token and deploy it on the parent chain @@ -156,44 +180,44 @@ contract L1Token is Ownable, ICustomToken, ERC20 { We now deploy that token to the parent chain. ```typescript -const { ethers } = require('hardhat'); -const { providers, Wallet } = require('ethers'); -const { getArbitrumNetwork } = require('@arbitrum/sdk'); -require('dotenv').config(); +import { ethers } from 'hardhat'; +import { providers, Wallet } from 'ethers'; +import { getArbitrumNetwork } from '@arbitrum/sdk'; +import 'dotenv/config'; const walletPrivateKey = process.env.DEVNET_PRIVKEY; -const l1Provider = new providers.JsonRpcProvider(process.env.L1RPC); -const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC); -const l1Wallet = new Wallet(walletPrivateKey, l1Provider); +const parentProvider = new providers.JsonRpcProvider(process.env.PARENT_RPC); +const childProvider = new providers.JsonRpcProvider(process.env.CHILD_RPC); +const parentWallet = new Wallet(walletPrivateKey, parentProvider); /** - * For the purpose of our tests, here we deploy an standard ERC20 token (L1Token) to L1 - * It sends its deployer (us) the initial supply of 1000 + * For the purpose of our tests, here we deploy a standard ERC-20 token (L1Token) to the parent chain. + * It sends its deployer (us) the initial supply of 1000. */ const main = async () => { /** - * Use l2Network to get the token bridge addresses needed to deploy the token + * Use the Arbitrum network info to get the token bridge addresses needed to deploy the token. */ - const l2Network = await getArbitrumNetwork(l2Provider); + const arbitrumNetwork = await getArbitrumNetwork(childProvider); - const l1Gateway = l2Network.tokenBridge.l1CustomGateway; - const l1Router = l2Network.tokenBridge.l1GatewayRouter; + const parentCustomGateway = arbitrumNetwork.tokenBridge.l1CustomGateway; + const parentGatewayRouter = arbitrumNetwork.tokenBridge.l1GatewayRouter; /** - * Deploy our custom token smart contract to L1 - * We give the custom token contract the address of l1CustomGateway and l1GatewayRouter as well as the initial supply (premine) + * Deploy our custom token smart contract to the parent chain. + * We give the custom token contract the address of the parent custom gateway and gateway router, plus the initial supply (premine). */ - console.log('Deploying the test L1Token to L1:'); - const L1Token = await (await ethers.getContractFactory('L1Token')).connect(l1Wallet); - const l1Token = await L1Token.deploy(l1Gateway, l1Router, 1000); + console.log('Deploying the test L1Token to the parent chain:'); + const L1Token = await (await ethers.getContractFactory('L1Token')).connect(parentWallet); + const parentToken = await L1Token.deploy(parentCustomGateway, parentGatewayRouter, 1000); - await l1Token.deployed(); - console.log(`L1Token is deployed to L1 at ${l1Token.address}`); + await parentToken.deployed(); + console.log(`L1Token is deployed to the parent chain at ${parentToken.address}`); /** - * Get the deployer token balance + * Get the deployer token balance. */ - const tokenBalance = await l1Token.balanceOf(l1Wallet.address); + const tokenBalance = await parentToken.balanceOf(parentWallet.address); console.log(`Initial token balance of deployer: ${tokenBalance}`); }; @@ -253,37 +277,37 @@ contract L2Token is ERC20, IArbToken { We now deploy that token to the child chain. ```typescript -const { ethers } = require('hardhat'); -const { providers, Wallet } = require('ethers'); -const { getArbitrumNetwork } = require('@arbitrum/sdk'); -require('dotenv').config(); +import { ethers } from 'hardhat'; +import { providers, Wallet } from 'ethers'; +import { getArbitrumNetwork } from '@arbitrum/sdk'; +import 'dotenv/config'; const walletPrivateKey = process.env.DEVNET_PRIVKEY; -const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC); -const l2Wallet = new Wallet(walletPrivateKey, l2Provider); +const childProvider = new providers.JsonRpcProvider(process.env.CHILD_RPC); +const childWallet = new Wallet(walletPrivateKey, childProvider); -const l1TokenAddress = '
'; +const parentTokenAddress = '
'; /** - * For the purpose of our tests, here we deploy an standard ERC20 token (L2Token) to L2 + * For the purpose of our tests, here we deploy a standard ERC-20 token (L2Token) to the child chain. */ const main = async () => { /** - * Use l2Network to get the token bridge addresses needed to deploy the token + * Use the Arbitrum network info to get the token bridge addresses needed to deploy the token. */ - const l2Network = await getArbitrumNetwork(l2Provider); - const l2Gateway = l2Network.tokenBridge.childCustomGateway; + const arbitrumNetwork = await getArbitrumNetwork(childProvider); + const childCustomGateway = arbitrumNetwork.tokenBridge.childCustomGateway; /** - * Deploy our custom token smart contract to L2 - * We give the custom token contract the address of childCustomGateway as well as the address of the counterpart L1 token + * Deploy our custom token smart contract to the child chain. + * We give the custom token contract the address of the child custom gateway, plus the address of its counterpart parent chain token. */ - console.log('Deploying the test L2Token to L2:'); - const L2Token = await (await ethers.getContractFactory('L2Token')).connect(l2Wallet); - const l2Token = await L2Token.deploy(l2Gateway, l1TokenAddress); + console.log('Deploying the test L2Token to the child chain:'); + const L2Token = await (await ethers.getContractFactory('L2Token')).connect(childWallet); + const childToken = await L2Token.deploy(childCustomGateway, parentTokenAddress); - await l2Token.deployed(); - console.log(`L2Token is deployed to L2 at ${l2Token.address}`); + await childToken.deployed(); + console.log(`L2Token is deployed to the child chain at ${childToken.address}`); }; main() @@ -309,55 +333,107 @@ To simplify the process, we’ll use Arbitrum’s SDK. We’ll call the [`regist ```typescript /** - * Register custom token on our custom gateway + * Register the custom token on the generic-custom gateway. */ -const adminTokenBridger = new AdminErc20Bridger(l2Network); +const adminTokenBridger = new AdminErc20Bridger(arbitrumNetwork); const registerTokenTx = await adminTokenBridger.registerCustomToken( - l1CustomToken.address, - l2CustomToken.address, - l1Wallet, - l2Provider, + parentToken.address, + childToken.address, + parentWallet, + childProvider, ); const registerTokenRec = await registerTokenTx.wait(); console.log( - `Registering token txn confirmed on L1! 🙌 L1 receipt is: ${registerTokenRec.transactionHash}`, + `Registering token txn confirmed on the parent chain! 🙌 Receipt: ${registerTokenRec.transactionHash}`, ); /** - * The L1 side is confirmed; now we listen and wait for the L2 side to be executed; we can do this by computing the expected txn hash of the L2 transaction. - * To compute this txn hash, we need our message's "sequence numbers", unique identifiers of each L1 to L2 message. - * We'll fetch them from the event logs with a helper method. + * The parent chain side is confirmed; now we wait for the child chain side to execute. + * Each parent-to-child message has a unique sequence number; we fetch them from the event logs via a helper. */ -const l1ToL2Msgs = await registerTokenRec.getParentToChildMessages(l2Provider); +const parentToChildMsgs = await registerTokenRec.getParentToChildMessages(childProvider); /** - * In principle, a single L1 txn can trigger any number of L1-to-L2 messages (each with its own sequencer number). - * In this case, the registerTokenOnL2 method created 2 L1-to-L2 messages; - * - (1) one to set the L1 token to the Custom Gateway via the Router, and - * - (2) another to set the L1 token to its L2 token address via the Generic-Custom Gateway - * Here, We check if both messages are redeemed on L2 + * A single parent chain transaction can trigger any number of parent-to-child messages. + * `registerTokenOnL2` produces exactly two: + * (1) set the parent chain token to the custom gateway via the router + * (2) set the parent chain token to its child chain counterpart via the generic-custom gateway + * We confirm both are redeemed on the child chain. */ -expect(l1ToL2Msgs.length, 'Should be 2 messages.').to.eq(2); +expect(parentToChildMsgs.length, 'Should be 2 messages.').to.eq(2); -const setTokenTx = await l1ToL2Msgs[0].waitForStatus(); +const setTokenTx = await parentToChildMsgs[0].waitForStatus(); expect(setTokenTx.status, 'Set token not redeemed.').to.eq(ParentToChildMessageStatus.REDEEMED); -const setGateways = await l1ToL2Msgs[1].waitForStatus(); +const setGateways = await parentToChildMsgs[1].waitForStatus(); expect(setGateways.status, 'Set gateways not redeemed.').to.eq(ParentToChildMessageStatus.REDEEMED); console.log( - 'Your custom token is now registered on our custom gateway 🥳 Go ahead and make the deposit!', + 'Your custom token is now registered on the generic-custom gateway 🥳 Go ahead and make the deposit!', +); +``` + +### Verify the registration + +Once both retryables have redeemed, confirm the new mappings on both chains. The router and gateway expose view methods that return the registered counterpart for any given token; if they return the zero address, registration didn't land: + +```typescript +import { Contract } from 'ethers'; + +const routerAbi = ['function l1TokenToGateway(address) view returns (address)']; +const gatewayAbi = ['function l1ToL2Token(address) view returns (address)']; + +const parentRouter = new Contract( + arbitrumNetwork.tokenBridge.l1GatewayRouter, + routerAbi, + parentProvider, +); +const parentGateway = new Contract( + arbitrumNetwork.tokenBridge.l1CustomGateway, + gatewayAbi, + parentProvider, +); +const childRouter = new Contract( + arbitrumNetwork.tokenBridge.l2GatewayRouter, + routerAbi, + childProvider, +); +const childGateway = new Contract( + arbitrumNetwork.tokenBridge.childCustomGateway, + gatewayAbi, + childProvider, +); + +console.log( + 'Parent router → gateway: ', + await parentRouter.l1TokenToGateway(parentToken.address), +); +console.log( + 'Parent gateway → child token: ', + await parentGateway.l1ToL2Token(parentToken.address), +); +console.log( + 'Child router → gateway: ', + await childRouter.l1TokenToGateway(parentToken.address), +); +console.log( + 'Child gateway → child token: ', + await childGateway.l1ToL2Token(parentToken.address), ); ``` +If any of these calls returns `0x0000…0000`, the corresponding retryable likely failed. Inspect the message status with `parentToChildMsgs[i].status()` and manually redeem it if needed; see [Parent-to-child messaging](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.mdx) for the recovery procedure. + ## Conclusion Upon completion, the parent and child chain tokens are connected via the generic-custom gateway. You can bridge tokens between the parent and child chain using the origin parent chain token and the custom token deployed on the child chain, along with the router and gateway contracts from each layer. -Suppose you want to see an example of bridging a token from the parent to the child chain using Arbitrum’s SDK. In that case, you can check out [How to bridge tokens via Arbitrum’s standard **ERC-20** gateway](/build-decentralized-apps/token-bridging/deposit-tokens.mdx), specifically in Steps 2-5. +For a fully working end-to-end implementation of every step on this page — contract sources, deployment scripts, registration call, and post-registration checks — see the [`custom-token-bridging` tutorial package](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/custom-token-bridging). + +If you want to see an example of bridging a token from the parent to the child chain using Arbitrum's SDK, check out [How to bridge tokens via Arbitrum's standard **ERC-20** gateway](/build-decentralized-apps/token-bridging/deposit-tokens.mdx), specifically Steps 2-5. ## Frequently asked questions @@ -367,7 +443,15 @@ No, you can only register once a child chain token for the same parent chain tok ### What can I do if my parent chain token is not upgradable? -Token registration can be completed through an Arbitrum DAO proposal using the standardized template. See [Register a custom gateway token via Arbitrum DAO governance](https://docs.arbitrum.foundation/how-tos/register-token-via-dao-governance) for the full process. +It depends on which Arbitrum chain you're targeting: + +- **Arbitrum One or Nova:** registration can be completed through an Arbitrum DAO proposal using the standardized template. See [Register a custom gateway token via Arbitrum DAO governance](https://docs.arbitrum.foundation/how-tos/register-token-via-dao-governance) for the full process. +- **An Arbitrum chain you operate:** as the chain owner, call `forceRegisterTokenToL2` on the parent chain custom gateway and `setGateways` on the parent chain gateway router directly through your chain's `UpgradeExecutor`. +- **Any Arbitrum chain:** deploy a new upgradeable wrapper contract that holds your existing token and itself implements `ICustomToken`, then register the wrapper instead. + +### One of the retryables didn't redeem. How do I recover? + +`registerCustomToken` produces two parent-to-child retryables (one for the router, one for the gateway). If either fails to auto-redeem — for example, because the supplied gas wasn't enough — the corresponding child chain mapping won't update and the verification calls in [Step 4](#verify-the-registration) will return `0x0000…0000`. Manually redeem the failed retryable from the child chain by calling `redeem()` on the message; see [Parent-to-child messaging](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.mdx) for the procedure. Registration succeeds as soon as both retryables are redeemed. ### Can I set up the generic-custom gateway after a standard **ERC-20** token has been deployed on the child chain? @@ -377,11 +461,8 @@ Yes, if your token has a standard **ERC-20** counterpart on the child chain, you Your token is now configured for bridging! Users can: -- [Deposit tokens to the child chain](/build-decentralized-apps/token-bridging/deposit-tokens.mdx) -- [Withdraw tokens back to the parent chain](/build-decentralized-apps/token-bridging/withdraw-tokens.mdx) + ## Resources -1. [Concept page: Token Bridge](/how-arbitrum-works/deep-dives/token-bridging.mdx) -2. [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) -3. [Token bridge contract addresses](/build-decentralized-apps/reference/02-contract-addresses.mdx) + diff --git a/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-standard-gateway.mdx b/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-standard-gateway.mdx index 8af48e8fcf..faddd854a0 100644 --- a/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-standard-gateway.mdx +++ b/docs/build-decentralized-apps/token-bridging/configure-token-bridging/setup-standard-gateway.mdx @@ -3,9 +3,18 @@ title: Configure standard gateway bridging description: "Guide to configuring ERC-20 token bridging using Arbitrum's standard gateway, which automatically creates a standard token representation on the child chain with no custom configuration required." user_story: 'As a developer, I want to set up standard gateway bridging for my ERC-20 token so it can be transferred between Ethereum and Arbitrum.' content_type: how-to +author: anegg0 +sme: TBD +displayed_sidebar: buildAppsSidebar --- -This guide explains how to configure your **ERC-20** token to work with Arbitrum's standard gateway. The standard gateway is the simplest option—it automatically creates a standard **ERC-20** token on the child chain with no configuration required. +This guide explains how to configure your **ERC-20** token to work with Arbitrum's standard gateway. The standard gateway is the simplest option — it automatically creates a standard **ERC-20** token on the child chain with no configuration required. + + + +**Audience and scope.** This guide covers registration on Arbitrum One. The same mechanics apply on Arbitrum Nova and on Arbitrum chains you operate yourself — the standard gateway is permissionless on every Arbitrum chain, so no extra steps are needed if you operate the chain. The first deposit triggers automatic child chain token deployment in all cases. + + ## When to use the standard gateway @@ -57,9 +66,9 @@ contract DappToken is ERC20 { Deploy it to the parent chain: -```javascript -const { ethers } = require('hardhat'); -const { providers, Wallet } = require('ethers'); +```ts +import { ethers } from 'hardhat'; +import { providers, Wallet } from 'ethers'; const parentProvider = new providers.JsonRpcProvider(process.env.PARENT_RPC); const wallet = new Wallet(process.env.PRIVATE_KEY, parentProvider); @@ -98,7 +107,7 @@ The child chain token is created automatically on the first deposit. You can tri ### Using the Arbitrum SDK -```javascript +```ts import { getArbitrumNetwork, Erc20Bridger } from '@arbitrum/sdk'; import { providers, Wallet } from 'ethers'; @@ -135,7 +144,7 @@ After the first deposit, find your token's child chain address: ### Using the SDK -```javascript +```ts const childTokenAddress = await erc20Bridge.getChildErc20Address( parentTokenAddress, parentProvider, @@ -165,12 +174,14 @@ The automatically deployed token is an instance of [`StandardArbERC20`](https:// You can verify this on [Arbiscan](https://arbiscan.io/) by viewing the token contract. +import TokenBridgingNextSteps from '../partials/_token-bridging-next-steps.mdx'; +import TokenBridgingResources from '../partials/_token-bridging-resources.mdx'; + ## Configuration complete Your token is now bridgeable! Users can: -- [Deposit tokens to the child chain](/build-decentralized-apps/token-bridging/deposit-tokens.mdx) -- [Withdraw tokens back to parent chain](/build-decentralized-apps/token-bridging/withdraw-tokens.mdx) + ## Important considerations @@ -193,16 +204,9 @@ Once the first deposit occurs, the token is permanently assigned to the standard The gateway controls the automatically deployed child chain token—you can't modify or upgrade it. For control over the child chain token, use the generic-custom gateway. -## Next steps - -- [Deposit tokens](/build-decentralized-apps/token-bridging/deposit-tokens.mdx) -- [Withdraw tokens](/build-decentralized-apps/token-bridging/withdraw-tokens.mdx) -- [Understand token bridge architecture](/how-arbitrum-works/deep-dives/token-bridging.mdx) -- [View example code](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/token-deposit) - ## Resources -- [Token bridge conceptual overview](/how-arbitrum-works/deep-dives/token-bridging.mdx) + + - [Standard **ERC-20** bridging details](/how-arbitrum-works/deep-dives/token-bridging.mdx#default-standard-bridging) -- [Arbitrum SDK documentation](/sdk) -- [Contract addresses](/build-decentralized-apps/reference/02-contract-addresses.mdx) +- [Example code: token deposit](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/token-deposit) diff --git a/docs/build-decentralized-apps/token-bridging/get-started.mdx b/docs/build-decentralized-apps/token-bridging/get-started.mdx index 6f233eae0a..543fe2cbe8 100644 --- a/docs/build-decentralized-apps/token-bridging/get-started.mdx +++ b/docs/build-decentralized-apps/token-bridging/get-started.mdx @@ -3,11 +3,24 @@ title: 'Get started with token bridging' description: 'Get started with Arbitrum token bridging — learn the available options for bridging ERC-20 tokens between Ethereum and Arbitrum, including standard, generic-custom, and custom gateways.' user_story: 'As a developer, I want to understand my options for bridging tokens between Ethereum and Arbitrum so I can choose the right approach for my project.' content_type: quickstart +author: anegg0 +sme: TBD displayed_sidebar: buildAppsSidebar --- Token bridging is a fundamental aspect of any child chain protocol. It allows projects to quickly integrate with the Arbitrum ecosystem by leveraging their existing parent chain tokens. + + +**Audience and scope.** These guides describe how to make a token bridgeable on Arbitrum One. The Solidity interfaces, SDK calls, and on-chain mechanics generalize to any Arbitrum chain — including Arbitrum Nova and chains you operate yourself — but the privileged registration path for non-upgradeable parent chain tokens differs: + +- **Arbitrum One or Nova:** registration goes through Arbitrum DAO governance. +- **An Arbitrum chain you operate:** as the chain owner, you can register tokens directly through your chain's `UpgradeExecutor`. No DAO vote required. + +The gateway you choose below is the same in either case. + + + ## I want to deposit or withdraw tokens If your token is already configured for bridging, use these guides: @@ -19,7 +32,16 @@ These guides work for any token that's already bridgeable, regardless of which g ## I want to make my token bridgeable -If you're setting up bridging for a new token, you have three gateway options to choose from: +If you're setting up bridging for a new token, choose a gateway based on what your token needs: + +| Your token | Gateway | Why | +| --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | +| Standard **ERC-20**, no special transfer behavior | [Standard](/build-decentralized-apps/token-bridging/configure-token-bridging/setup-standard-gateway.mdx) | Automatic, no pre-configuration | +| Rebasing, fee-on-transfer, transfer hooks, or non-standard mint/burn | [Generic-custom](/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx) | Standard gateway can't represent these correctly | +| Standard transfer behavior but you need a custom child chain token (extra methods, role-gated mint, etc.) | [Generic-custom](/build-decentralized-apps/token-bridging/configure-token-bridging/setup-generic-custom-gateway.mdx) | You control the child chain token contract | +| Custom deposit/withdraw mechanics, child-chain-native tokens, or other bridge-level behavior | [Custom](/build-decentralized-apps/token-bridging/configure-token-bridging/setup-custom-gateway.mdx) | You implement the gateway itself | + +Each option is described in more detail below. ### 1. Standard gateway (recommended for most tokens) From a1bc339dcf1b1d8d4ebe6a04d279a7b11b0ecb3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Fri, 15 May 2026 10:10:50 -0700 Subject: [PATCH 08/18] add new cross-article partials for token bridging next steps, prereqs, and resources --- .../token-bridging/partials/_token-bridging-next-steps.mdx | 2 ++ .../token-bridging/partials/_token-bridging-prereqs.mdx | 1 + .../token-bridging/partials/_token-bridging-resources.mdx | 3 +++ 3 files changed, 6 insertions(+) create mode 100644 docs/build-decentralized-apps/token-bridging/partials/_token-bridging-next-steps.mdx create mode 100644 docs/build-decentralized-apps/token-bridging/partials/_token-bridging-prereqs.mdx create mode 100644 docs/build-decentralized-apps/token-bridging/partials/_token-bridging-resources.mdx diff --git a/docs/build-decentralized-apps/token-bridging/partials/_token-bridging-next-steps.mdx b/docs/build-decentralized-apps/token-bridging/partials/_token-bridging-next-steps.mdx new file mode 100644 index 0000000000..6ed8eeccad --- /dev/null +++ b/docs/build-decentralized-apps/token-bridging/partials/_token-bridging-next-steps.mdx @@ -0,0 +1,2 @@ +- [Deposit tokens to the child chain](/build-decentralized-apps/token-bridging/deposit-tokens.mdx) +- [Withdraw tokens back to the parent chain](/build-decentralized-apps/token-bridging/withdraw-tokens.mdx) diff --git a/docs/build-decentralized-apps/token-bridging/partials/_token-bridging-prereqs.mdx b/docs/build-decentralized-apps/token-bridging/partials/_token-bridging-prereqs.mdx new file mode 100644 index 0000000000..88c6b72ebd --- /dev/null +++ b/docs/build-decentralized-apps/token-bridging/partials/_token-bridging-prereqs.mdx @@ -0,0 +1 @@ +Familiarity with [Arbitrum's token bridge system](/how-arbitrum-works/deep-dives/token-bridging.mdx), smart contracts, and decentralized application development is required. If you're new to developing on Arbitrum, consider reviewing the [Quickstart: Build an app with Arbitrum](/build-decentralized-apps/01-quickstart-solidity-remix.mdx) before proceeding. We'll use [Arbitrum's SDK](https://github.com/OffchainLabs/arbitrum-sdk) throughout this how-to, although no prior knowledge is required. diff --git a/docs/build-decentralized-apps/token-bridging/partials/_token-bridging-resources.mdx b/docs/build-decentralized-apps/token-bridging/partials/_token-bridging-resources.mdx new file mode 100644 index 0000000000..9122218e95 --- /dev/null +++ b/docs/build-decentralized-apps/token-bridging/partials/_token-bridging-resources.mdx @@ -0,0 +1,3 @@ +- [Token bridge conceptual overview](/how-arbitrum-works/deep-dives/token-bridging.mdx) +- [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) +- [Token bridge contract addresses](/build-decentralized-apps/reference/02-contract-addresses.mdx) From 4628b9cc3a52ce6c0024292b246d72b46b2b52c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Tue, 26 May 2026 12:19:37 -0700 Subject: [PATCH 09/18] Update docs/how-arbitrum-works/timeboost/timeboost-reserve-originator.mdx Co-authored-by: Pete --- .../timeboost/timeboost-reserve-originator.mdx | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/how-arbitrum-works/timeboost/timeboost-reserve-originator.mdx b/docs/how-arbitrum-works/timeboost/timeboost-reserve-originator.mdx index 2a01b2793c..b7f2512b74 100644 --- a/docs/how-arbitrum-works/timeboost/timeboost-reserve-originator.mdx +++ b/docs/how-arbitrum-works/timeboost/timeboost-reserve-originator.mdx @@ -26,7 +26,6 @@ Reserve pricer: https://github.com/OffchainLabs/timeboost-reserve-pricer Auctioneer code: https://github.com/OffchainLabs/nitro/tree/master/cmd/autonomous-auctioneer -## Architecture ## How to use it? From ea856f403192237f3435ba6bf78f623c9489abe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Tue, 26 May 2026 12:19:47 -0700 Subject: [PATCH 10/18] Update docs/how-arbitrum-works/timeboost/timeboost-reserve-originator.mdx Co-authored-by: Pete --- .../timeboost/timeboost-reserve-originator.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-arbitrum-works/timeboost/timeboost-reserve-originator.mdx b/docs/how-arbitrum-works/timeboost/timeboost-reserve-originator.mdx index b7f2512b74..75511bc280 100644 --- a/docs/how-arbitrum-works/timeboost/timeboost-reserve-originator.mdx +++ b/docs/how-arbitrum-works/timeboost/timeboost-reserve-originator.mdx @@ -29,7 +29,7 @@ Auctioneer code: https://github.com/OffchainLabs/nitro/tree/master/cmd/autonomou ## How to use it? -Any auction participant looking to submit a valid bid will need to access the Timeboost Reserve Pricer API to get the round and minimum bid amount. There are 2 API endpoints that can be access using the following command +Any auction participant looking to submit a valid bid will need to access the Timeboost Reserve Pricer API to get the round and minimum bid amount. There are two API endpoints that can be access using the following command `curl -H "Authorization: Bearer replace_with_api_token" ip_addr:port/api/latest` From 7581779a9ef3f829c5c446934477d23b6c09c57b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Tue, 26 May 2026 12:19:55 -0700 Subject: [PATCH 11/18] Update docs/how-arbitrum-works/timeboost/timeboost-reserve-originator.mdx Co-authored-by: Pete --- .../timeboost/timeboost-reserve-originator.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-arbitrum-works/timeboost/timeboost-reserve-originator.mdx b/docs/how-arbitrum-works/timeboost/timeboost-reserve-originator.mdx index 75511bc280..1c10c073a0 100644 --- a/docs/how-arbitrum-works/timeboost/timeboost-reserve-originator.mdx +++ b/docs/how-arbitrum-works/timeboost/timeboost-reserve-originator.mdx @@ -34,6 +34,6 @@ Any auction participant looking to submit a valid bid will need to access the Ti `curl -H "Authorization: Bearer replace_with_api_token" ip_addr:port/api/latest` - `/api/latest` which returns the newest reserve price and round -- `/api/recent` which returns the reserve price and rounds of the last 2 hours +- `/api/recent` which returns the reserve price and rounds of the last two hours The price gets updated at the 31st second of every minute. From f8fbd36797a358dacbbf6c77a1bdfd703bfbcbdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Tue, 26 May 2026 12:20:05 -0700 Subject: [PATCH 12/18] Update docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx Co-authored-by: Pete --- .../bridging/configure-token-gateway/generic-custom.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx b/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx index 3cb9dbb8da..35ed70ff7b 100644 --- a/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx +++ b/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx @@ -23,7 +23,7 @@ For other gateway types, see: -**Audience and scope.** This guide covers registration on Arbitrum One. The Solidity interfaces, SDK calls, and on-chain mechanics generalize to any Arbitrum chain — including Arbitrum Nova and chains you operate yourself — but the registration fallback for non-upgradeable parent chain tokens differs: +**Audience and scope.** This guide covers registration on Arbitrum One. The Solidity interfaces, SDK calls, and on-chain mechanics generalize to any Arbitrum chain—including Arbitrum Nova and chains you operate yourself—but the registration fallback for non-upgradeable parent chain tokens differs: - **Arbitrum One or Nova:** register through Arbitrum DAO governance, or wrap your token (see [Step 1](#step-1-review-the-prerequisites)). - **An Arbitrum chain you operate:** as the chain owner, call `forceRegisterTokenToL2` and `setGateways` directly through your chain's `UpgradeExecutor`. No DAO vote is involved. You will also need to register your chain with the SDK via [`registerCustomArbitrumNetwork`](/sdk/dataEntities/networks.md) before calling `getArbitrumNetwork`. From 9d7574c75082573faf112caf2deeaf7b5ffe7465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Tue, 26 May 2026 12:20:14 -0700 Subject: [PATCH 13/18] Update docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx Co-authored-by: Pete --- .../bridging/configure-token-gateway/generic-custom.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx b/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx index 35ed70ff7b..ab48e5973e 100644 --- a/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx +++ b/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx @@ -56,7 +56,7 @@ Also, the **child chain counterpart of the token** must conform to the [`IArbTok import TokenCompatibilityPartial from '../partials/_token-compatibility.mdx'; -The interfaces above also place a few constraints on how your token implements standard ERC-20 behavior. Review these before continuing: +The interfaces above also place a few constraints on how your token implements standard **ERC-20** behavior. Review these before continuing: From dbe5de6664415cf8503facd08f17df2920bd18e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Tue, 26 May 2026 12:20:24 -0700 Subject: [PATCH 14/18] Update docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx Co-authored-by: Pete --- .../bridging/configure-token-gateway/generic-custom.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx b/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx index ab48e5973e..0d6ddd629e 100644 --- a/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx +++ b/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx @@ -191,7 +191,7 @@ const childProvider = new providers.JsonRpcProvider(process.env.CHILD_RPC); const parentWallet = new Wallet(walletPrivateKey, parentProvider); /** - * For the purpose of our tests, here we deploy a standard ERC-20 token (L1Token) to the parent chain. + * For the purpose of our tests, here we deploy a standard **ERC-20** token (`L1Token`) to the parent chain. * It sends its deployer (us) the initial supply of 1000. */ const main = async () => { From 44ca0573bea5ff5733c43bb3c6870cb7bed366c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Tue, 26 May 2026 12:20:51 -0700 Subject: [PATCH 15/18] Update docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx Co-authored-by: Pete --- .../bridging/configure-token-gateway/generic-custom.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx b/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx index 0d6ddd629e..162c919350 100644 --- a/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx +++ b/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx @@ -394,7 +394,7 @@ Upon completion, the parent and child chain tokens are connected via the generic You can bridge tokens between the parent and child chain using the origin parent chain token and the custom token deployed on the child chain, along with the router and gateway contracts from each layer. -For a fully working end-to-end implementation of every step on this page — contract sources, deployment scripts, registration call, and post-registration checks — see the [`custom-token-bridging` tutorial package](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/custom-token-bridging). +For a fully working end-to-end implementation of every step on this page—contract sources, deployment scripts, registration call, and post-registration checks—see the [`custom-token-bridging` tutorial package](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/custom-token-bridging). If you want to see an example of bridging a token from the parent to the child chain using Arbitrum's SDK, check out [How to bridge tokens via Arbitrum's standard **ERC-20** gateway](/build-decentralized-apps/bridging/deposit/tokens.mdx), specifically Steps 2-5. From d899d325598a0a9f3b389fee125d92e9559f491b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Tue, 26 May 2026 12:20:59 -0700 Subject: [PATCH 16/18] Update docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx Co-authored-by: Pete --- .../bridging/configure-token-gateway/generic-custom.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx b/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx index 162c919350..6ba2b73ef6 100644 --- a/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx +++ b/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx @@ -289,7 +289,7 @@ const childWallet = new Wallet(walletPrivateKey, childProvider); const parentTokenAddress = '
'; /** - * For the purpose of our tests, here we deploy a standard ERC-20 token (L2Token) to the child chain. + * For the purpose of our tests, here we deploy a standard **ERC-20** token (`L2Token`) to the child chain. */ const main = async () => { /** From aec01eb9d1acda6259adf8e979c7fa2e8c0298bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Tue, 26 May 2026 12:21:08 -0700 Subject: [PATCH 17/18] Update docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx Co-authored-by: Pete --- .../bridging/configure-token-gateway/generic-custom.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx b/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx index 6ba2b73ef6..f9631a5437 100644 --- a/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx +++ b/docs/build-decentralized-apps/bridging/configure-token-gateway/generic-custom.mdx @@ -414,7 +414,7 @@ It depends on which Arbitrum chain you're targeting: ### One of the retryables didn't redeem. How do I recover? -`registerCustomToken` produces two parent-to-child retryables (one for the router, one for the gateway). If either fails to auto-redeem — for example, because the supplied gas wasn't enough — the corresponding child chain mapping won't update and the verification calls in [Step 4](#verify-the-registration) will return `0x0000…0000`. Manually redeem the failed retryable from the child chain by calling `redeem()` on the message; see [Parent-to-child messaging](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.mdx) for the procedure. Registration succeeds as soon as both retryables are redeemed. +`registerCustomToken` produces two parent-to-child retryables (one for the router, one for the gateway). If either fails to auto-redeem—for example, because the supplied gas wasn't enough — the corresponding child chain mapping won't update and the verification calls in [Step 4](#verify-the-registration) will return `0x0000…0000`. Manually redeem the failed retryable from the child chain by calling `redeem()` on the message; see [Parent-to-child messaging](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.mdx) for the procedure. Registration succeeds as soon as both retryables are redeemed. ### Can I set up the generic-custom gateway after a standard **ERC-20** token has been deployed on the child chain? From 10fbc72478ae45e172845404aa16c5034445edce Mon Sep 17 00:00:00 2001 From: Pete Date: Fri, 29 May 2026 06:34:08 -0500 Subject: [PATCH 18/18] yarn format --- .../timeboost/timeboost-reserve-originator.mdx | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/how-arbitrum-works/timeboost/timeboost-reserve-originator.mdx b/docs/how-arbitrum-works/timeboost/timeboost-reserve-originator.mdx index 1c10c073a0..7d5b737e0b 100644 --- a/docs/how-arbitrum-works/timeboost/timeboost-reserve-originator.mdx +++ b/docs/how-arbitrum-works/timeboost/timeboost-reserve-originator.mdx @@ -26,7 +26,6 @@ Reserve pricer: https://github.com/OffchainLabs/timeboost-reserve-pricer Auctioneer code: https://github.com/OffchainLabs/nitro/tree/master/cmd/autonomous-auctioneer - ## How to use it? Any auction participant looking to submit a valid bid will need to access the Timeboost Reserve Pricer API to get the round and minimum bid amount. There are two API endpoints that can be access using the following command