Generic-custom Gateway

For token movement between Arbitrumn and dKargo, the Standard Gateway method is generally sufficient.

However, in the Standard Gateway model:

  • When depositing tokens, an ERC-20 token contract on dKargo Chain is automatically deployed.

  • This contract is enforced to use StandardArbERC20.sol, limiting customization.

For developers or project builders who want:

  • To add custom functionalities to their ERC-20 contract

  • To pair their own ERC-20 contract on dKargo Chain with a specific contract

Using the Generic-Custom Gateway provides greater flexibility in these cases.

Smart contracts implemented in Solidity follow a naming convention where "L1" and "L2" prefixes are specified.

  • This is because dKargo utilizes Arbitrum’s token bridge contracts.

  • In the dKargo Token Bridge context:

    • L1 = Arbitrum (L2)

    • L2 = dKargo (L3)

This distinction clarifies the corresponding layers when interacting with Arbitrum’s bridge infrastructure within dKargo’s ecosystem.

Setting Up a Token with the Generic-Custom Gateway

To use the generic-custom gateway, the ERC-20 contract deployed on L2 must comply with the following rules:

1. Interface

The ERC-20 token contract on L2 must implement the ICustomToken interface.

The isArbitrumEnabled() method is called during the token registration process. To utilize the Generic-Custom Gateway, this method must return uint8(0xb1).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {ICustomToken} from "@arbitrum/token-bridge-contracts/contracts/tokenbridge/ethereum/ICustomToken.sol";
import "@arbitrum/token-bridge-contracts/contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol";

import {L1OrbitGatewayRouter} from "@arbitrum/token-bridge-contracts/contracts/tokenbridge/ethereum/gateway/L1OrbitGatewayRouter.sol";
import {L1OrbitCustomGateway} from "@arbitrum/token-bridge-contracts/contracts/tokenbridge/ethereum/gateway/L1OrbitCustomGateway.sol";
import {IL1GatewayRouter} from "@arbitrum/token-bridge-contracts/contracts/tokenbridge/ethereum/gateway/IL1GatewayRouter.sol";
import { IERC20Bridge } from "@arbitrum/token-bridge-contracts/contracts/tokenbridge/libraries/IERC20Bridge.sol";

import "@openzeppelin/contracts/utils/Context.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract L2TokenCustomGas is Ownable, ERC20, ICustomToken {
    using SafeERC20 for IERC20;

    address public gateway;
    address public router;
    bool internal shouldRegisterGateway;

    constructor(string memory name_, string memory symbol_,uint256 _initialSupply,address _gateway, address _router) ERC20(name_, symbol_) {
        gateway = _gateway;
        router = _router;
        _mint(msg.sender, _initialSupply * 10 ** decimals());
    }

    /// @dev See {ERC20-transferFrom}
    function transferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) public override(ICustomToken, ERC20) returns (bool) {
        return super.transferFrom(sender, recipient, amount);
    }

    /// @dev See {ERC20-balanceOf}
    function balanceOf(address account) public view override(ICustomToken, ERC20) returns (uint256) {
        return super.balanceOf(account);
    }

    /// @dev we only set shouldRegisterGateway to true when in `registerTokenOnL2`
    function isArbitrumEnabled() external view override returns (uint8) {
        require(shouldRegisterGateway, "NOT_EXPECTED_CALL");
        return uint8(0xb1);
    }

    function registerTokenOnL2(
        address l2CustomTokenAddress,
        uint256 maxSubmissionCostForCustomGateway,
        uint256 maxSubmissionCostForRouter,
        uint256 maxGasForCustomGateway,
        uint256 maxGasForRouter,
        uint256 gasPriceBid,
        uint256 valueForGateway,
        uint256 valueForRouter,
        address creditBackAddress
    ) public payable override onlyOwner {
        // we temporarily set `shouldRegisterGateway` to true for the callback in registerTokenToL2 to succeed
        bool prev = shouldRegisterGateway;
        shouldRegisterGateway = true;

        address inbox = IL1GatewayRouter(router).inbox();
        address bridge = address(IInbox(inbox).bridge());

        // transfer fees from user to here, and approve router to use it
        {
            address nativeToken = IERC20Bridge(bridge).nativeToken();

            IERC20(nativeToken).safeTransferFrom(
                msg.sender,
                address(this),
                valueForGateway + valueForRouter
            );
            IERC20(nativeToken).approve(router, valueForRouter);
            IERC20(nativeToken).approve(gateway, valueForGateway);
        }

        L1OrbitCustomGateway(gateway).registerTokenToL2(
            l2CustomTokenAddress,
            maxGasForCustomGateway,
            gasPriceBid,
            maxSubmissionCostForCustomGateway,
            creditBackAddress,
            valueForGateway
        );

        L1OrbitGatewayRouter(router).setGateway(
            gateway,
            maxGasForRouter,
            gasPriceBid,
            maxSubmissionCostForRouter,
            creditBackAddress,
            valueForRouter
        );

        // reset allowance back to 0 in case not all approved native tokens are spent
        {
            address nativeToken = IERC20Bridge(bridge).nativeToken();

            IERC20(nativeToken).approve(router, 0);
            IERC20(nativeToken).approve(gateway, 0);
        }

        shouldRegisterGateway = prev;
    }
}

The ERC-20 token contract on L3 must implement the IArbToken interface.

  • The contract must include:

    • bridgeMint and bridgeBurn functions, which can only be called by the Generic-Custom Gateway contract (onlyL2Gateway).

    • The l1Address variable to store the corresponding L2 ERC-20 token contract address.

// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

import "@arbitrum/token-bridge-contracts/contracts/tokenbridge/arbitrum/IArbToken.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";

contract L3Token is ERC20, ERC20Permit, IArbToken {
    address public l2Gateway;
    address public override l1Address;

    modifier onlyL2Gateway() {
        require(msg.sender == l2Gateway, "NOT_GATEWAY");
        _;
    }

    constructor(string memory name_, string memory symbol_,address _l2Gateway, address _l1TokenAddress) ERC20(name_, symbol_) ERC20Permit(name_) {
        l2Gateway = _l2Gateway;
        l1Address = _l1TokenAddress;
    }

    /**
     * @notice should increase token supply by amount, and should only be callable by the L2Gateway.
     */
    function bridgeMint(address account, uint256 amount) external override onlyL2Gateway {
        _mint(account, amount);
    }

    /**
     * @notice should decrease token supply by amount, and should only be callable by the L2Gateway.
     */
    function bridgeBurn(address account, uint256 amount) external override onlyL2Gateway {
        _burn(account, amount);
    }
}

2. Approve

The next step is registering and pairing the L2 and L3 ERC-20 tokens through the Generic-Custom Gateway.

Since the two tokens are deployed on independent chains, the pairing process is conducted using retryable tickets, and the associated fees are paid in L2 ERC-20 DKA.

The registration process begins by calling a method on the L2 ERC-20 token contract. Before initiating this step, the L2 ERC-20 token contract must be approved to use L2 ERC-20 DKA for transaction fees.

The dKargo Token Bridge is a dApp built using Arbitrum’s Retryable Ticket mechanism. A Retryable Ticket allows an L2 transaction to be generated and executed on L3.

This mechanism enables users to perform L3 operations directly from L2.The transaction fees for this process are paid in L2 ERC-20 DKA.

const res = await customTokenBridge.approveGasTokenForCustomTokenRegistration({
  erc20ParentAddress: parentERC20.address,
  parentSigner,
});

const receipt = await res.wait();
console.log(`approve gas token to L2 ERC20 Contract tx hash: ${receipt.transactionHash}`

const allowance = await customTokenBridge.allowanceGasTokenToParentERC20(
  parentERC20.address,
  parentSigner.address,
  parentProvider
);
console.log(`allowance amount: ${allowance}`

3. Register

The L2 and L3 ERC-20 tokens must be registered with the Generic-Custom Gateway to complete the pairing process.

By referring to [L2 ERC-20 Example], it can be observed that the registerTokenOnL2 method calls both:

  • customGateway.registerTokenToL2

  • customGateway.setGateway

These methods are executed on the L2 ERC-20 contract.

  • customGateway.registerTokenToL2

    • Executes the pairing process within the L2 Generic-Custom Gateway.

    • Sends a cross-chain message to dKargo Chain, ensuring that the same pairing operation is executed on L3 Generic-Custom Gateway.

  • customGateway.setGateway

    • Registers Generic-Custom Gateway as the designated gateway for the token in the L2 GatewayRouter.

    • Sends a cross-chain message to dKargo Chain, ensuring that the L3 GatewayRouter applies the same configuration.

const res = await customTokenBridge.registerCustomToken(
  parentERC20.address,
  childERC20.address,
  parentSigner,
  childProvider
);

const receipt = await res.wait();
console.log(`register tx hash: ${receipt.transactionHash}`

Since the two tokens are registered on separate chains, pairing them requires some processing time.

  • The registration request status will initially be marked as "Pending".

  • After approximately 10 minutes, the process is finalized, and the token pairing is completed on dKargo Chain (L3).

const l2ToL3Msgs = await receipt.getParentToChildMessages(childProvider);

// The process waits until the message is executed on the dKargo chain.
await l2ToL3Msgs[0].waitForStatus();
await l2ToL3Msgs[1].waitForStatus();

The status of the registration request can be checked on dScanner’s [L2 ➔ L3 Transactions] page.

Depositing & Withdrawing Tokens

Now that the Generic-Custom Gateway is set up, deposits and withdrawals between Arbitrum Chain and dKargo Chain can now be performed. The deposit and withdrawal process functions exactly the same as the Standard Gateway method, so refer to its process for execution.

It is important to note that when using the Standard Gateway, the first deposit automatically deploys an ERC-20 token contract on dKargo Chain, enforcing the use of StandardArbERC20.sol.

However, when using the Generic-Custom Gateway, it is mandatory to complete the registration process before making the first deposit.

Last updated