GULLY CHAINDocs

Bridging

On this page

Gully Chain uses the canonical Arbitrum bridge: the rollup's own Inbox and Outbox for ETH, and the standard Arbitrum token bridge (gateway router plus gateways) for ERC-20 tokens. No third-party bridge or extra trust is involved.

The easiest way to bridge is the Gully Bridge UI. This page explains what happens underneath and how to do it from code. All addresses are on Contract addresses.

#Deposits: Arbitrum Sepolia to Gully Chain

#ETH

An ETH deposit is a call to depositEth() on the rollup's Inbox on Arbitrum Sepolia, sending the ETH as value. The same address receives the ETH on Gully Chain once the chain reads the message from the parent chain, which takes about a minute.

TypeScript
import { createWalletClient, custom, parseEther } from "viem";
import { arbitrumSepolia } from "viem/chains";

const INBOX = "0x7B8499f1C058c9bdE2Bc9BdFEBE40364232C52B5"; // on Arbitrum Sepolia

const wallet = createWalletClient({ chain: arbitrumSepolia, transport: custom(window.ethereum) });
const [account] = await wallet.requestAddresses();

const hash = await wallet.writeContract({
  account,
  address: INBOX,
  abi: [{ type: "function", name: "depositEth", stateMutability: "payable", inputs: [], outputs: [{ type: "uint256" }] }],
  functionName: "depositEth",
  value: parseEther("0.01"),
});

The deposit goes to the address that sent it. If the sender is a contract, its address is aliased on Gully Chain (the standard Arbitrum address alias), so deposit from an EOA unless you know you need otherwise.

#ERC-20 tokens

ERC-20 deposits go through the gateway router on Arbitrum Sepolia. The router picks the gateway for the token (the standard gateway for ordinary tokens), the gateway locks your tokens, and a matching token is minted to you on Gully Chain. The first deposit of a token also deploys its Gully Chain version. Deposits arrive in about a minute.

The Arbitrum SDK handles the approval, gas estimation and retryable ticket for you. Register Gully Chain once, then use Erc20Bridger:

TypeScript
// @arbitrum/sdk v4 (uses ethers v5 signers and providers)
import { registerCustomArbitrumNetwork, Erc20Bridger, EthBridger } from "@arbitrum/sdk";

const gully = registerCustomArbitrumNetwork({
  name: "Gully Chain Testnet",
  chainId: 485590,
  parentChainId: 421614,
  confirmPeriodBlocks: 150,
  isCustom: true,
  isTestnet: true,
  ethBridge: {
    bridge: "0x2073106fd3fc844Bbde31f5Bf0BB91cf1e2d6C8A",
    inbox: "0x7B8499f1C058c9bdE2Bc9BdFEBE40364232C52B5",
    sequencerInbox: "0x483f77f692635d4D8a8147D0f732A016614Ce685",
    outbox: "0x976C651fe5E63E45C8C28449507F51548922f68c",
    rollup: "0x85D80C06Fa5334E8Cd1C51F3F39D477E5dc35E0D",
  },
  tokenBridge: {
    parentGatewayRouter: "0xfbd7503009AB4ee5A1f980a2fCDEE4C7BEB6b2B9",
    parentErc20Gateway: "0x8E35E523bFd709d577A77F749c831cB045bCabd0",
    parentCustomGateway: "0xA90752f7057dc1BFe2677f1791c5084535A4E42a",
    parentWethGateway: "0xEB602d8Fc5F5d87DA629201905bE51885165EC4f",
    parentWeth: "0x980B62Da83eFf3D4576C647993b0c1D7faf17c73",
    parentMultiCall: "0xce1CAd780c529e66e3aa6D952a1ED9A6447791c1",
    childGatewayRouter: "0x32C680cA4c27A3566fE5Dfd4643fE420eE1039B7",
    childErc20Gateway: "0x5CbA5BFD52beA26Fb65c26Fa452f290cB8610fE2",
    childCustomGateway: "0x27530E706c67F210753081508302d69ba1693A41",
    childWethGateway: "0xB8BAAd59F553707534F546983912486562FA0Eb3",
    childWeth: "0x3292dE74305DF64dbe082c6834fa841353eB2415",
    childMultiCall: "0xc0Dee8709648264495af14c65fEA2Ae7a728E01a",
  },
});

const erc20Bridger = new Erc20Bridger(gully);

// 1. approve the gateway once per token
await (await erc20Bridger.approveToken({ erc20ParentAddress: TOKEN, parentSigner })).wait();

// 2. deposit, then wait for the tokens on Gully Chain
const tx = await erc20Bridger.deposit({
  amount: 1_000_000n,
  erc20ParentAddress: TOKEN,
  parentSigner,                       // signer on Arbitrum Sepolia
  childProvider,                      // provider for https://testnet-rpc.gullychain.xyz
});
const receipt = await tx.wait();
await receipt.waitForChildTransactionReceipt(childProvider);

EthBridger from the same SDK does ETH deposits and withdrawals with the same network object.

#Withdrawals: Gully Chain to Arbitrum Sepolia

Heads up

Withdrawals are rolling out. You can start a withdrawal today, but it can only be finished on Arbitrum Sepolia after the chain's validator has confirmed it, and the validator is still being staked. Until it is, treat bridged ETH and tokens as one-way. The Status page will say when withdrawals are live.

A withdrawal has three stages:

  1. Start it on Gully Chain. For ETH, call withdrawEth(destination) on the ArbSys precompile at 0x…64 with the ETH as value. For tokens, use the gateway router on Gully Chain (or Erc20Bridger.withdraw). This burns the funds on Gully Chain and creates an outgoing message.
  2. Wait for confirmation. The sequencer posts the batch to Arbitrum Sepolia, the validator posts an assertion that includes it, and the assertion must sit unchallenged for the rollup's confirmation period of 150 blocks. The rollup counts blocks with block.number on Arbitrum Sepolia, which follows Ethereum Sepolia's block number, so 150 blocks is roughly 30 minutes once the assertion is posted.
  3. Claim it on Arbitrum Sepolia. Execute the message on the Outbox. The SDK does this with getChildToParentMessages, waitUntilReadyToExecute and execute.
TypeScript
// start an ETH withdrawal on Gully Chain with viem
await wallet.writeContract({
  address: "0x0000000000000000000000000000000000000064", // ArbSys
  abi: [{ type: "function", name: "withdrawEth", stateMutability: "payable",
          inputs: [{ name: "destination", type: "address" }], outputs: [{ type: "uint256" }] }],
  functionName: "withdrawEth",
  args: [account],
  value: parseEther("0.001"),
});

The 150-block period is the testnet's setting. Mainnet's period will be published before launch.

#Retryable tickets

Anything sent from Arbitrum Sepolia to a contract on Gully Chain, including every ERC-20 deposit, travels as a retryable ticket:

  • You pay on Arbitrum Sepolia for a submission fee plus the L3 gas the call will need. Unused value is refunded to a refund address on Gully Chain.
  • Gully Chain tries to run the call automatically as soon as the message arrives (the auto-redeem).
  • If the auto-redeem fails, for example because the gas price rose or too little gas was given, the ticket is not lost. Anyone can retry it on Gully Chain through the ArbRetryableTx precompile (redeem(ticketId) at 0x…6E) for up to seven days, and the lifetime can be extended.

The Arbitrum SDK estimates the fees and waits for the redeem for you. Plain ETH deposits through depositEth() are not retryables; they credit the ETH directly.