# 범용적 커스텀 게이트웨이

아비트럼 체인과 디카르고 체인 간의 토큰 유동에는 표준 게이트웨이 방식만으로도 충분합니다.&#x20;

그러나 표준 게이트웨이 방식에서는 입금 시 디카르고 체인에 대응되는 ERC-20 토큰 컨트랙트가 자동으로 배포되며, 이는 [**StandardArbERC20.sol**](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/arbitrum/StandardArbERC20.sol)로 구현된 컨트랙트를 강제합니다.

개발자 또는 프로젝트 빌더가 자신이 구현한 ERC-20 컨트랙트에 추가적인 기능을 포함하거나, 디카르고 체인에서 특정 ERC-20 컨트랙트와 페어링되도록 설정하기를 원한다면, 범용적 커스텀 게이트웨이 (Generic-Custom Gateway)를 선택하여 더 큰 자유도를 얻을 수 있습니다.

{% hint style="info" %}
솔리디티로 구현된 컨트랙트 이름에는 L1, L2가 prefix로 명시 되어 있습니다. 이는 아비트럼에서 운영되는 [토큰 브릿지 컨트랙트](https://github.com/OffchainLabs/token-bridge-contracts)를 디카르고에서 사용하기 때문입니다. 디카르고 토큰 브릿지 컨트랙트의 L1 = 아비트럼(L2), L2 = 디카르고(L3)로 해석됩니다.
{% endhint %}

## 범용적 커스텀 게이트웨이를 통한 토큰 설정

범용적 커스텀 게이트웨이를 사용하기 위해서는 L2에 배포된 ERC-20 컨트랙트는 다음과 같은 규칙을 준수해야합니다.

### 1. 인터페이스

L2에 배포된 ERC20 토큰 컨트랙트는 [ICustomToken](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/ICustomToken.sol) 인터페이스를 준수해야 합니다.

<mark style="color:blue;">`isArbitrumEnabled()`</mark> 메서드는 토큰을 등록하는 과정에서 호출되며, 범용적 커스텀 게이트웨이를 이용하기 위해서는 <mark style="color:blue;">`uint8(0xb1)`</mark> 값을 반환해야 합니다.

{% code overflow="wrap" %}

```solidity
// 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;
    }
}

```

{% endcode %}

L3에 배포된 ERC20 토큰 컨트랙트는 [IArbToken](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/arbitrum/IArbToken.sol) 인터페이스를 준수해야 합니다.

입출금 과정에서 범용적 커스텀 게이트웨이 컨트랙트에서만 호출 가능한(onlyL2Gateway) <mark style="color:blue;">`bridgeMint`</mark> 및 <mark style="color:blue;">`bridgeBurn`</mark> 메서드와 L2 ERC-20 토큰 컨트랙트 주소를 저장하는 <mark style="color:blue;">`l1Address`</mark> 변수가 구현되어 있어야 합니다.

{% code overflow="wrap" %}

```solidity
// 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);
    }
}

```

{% endcode %}

### 2. 승인

다음 단계에서는 범용적 커스텀 게이트웨이를 통해 L2 ERC-20 토큰과 L3 ERC-20 토큰을 등록하여 페어링을 설정할 예정입니다.&#x20;

두 토큰은 독립적인 두 체인에 배포되어 있기 때문에, [**retryable ticket**](https://docs.arbitrum.io/build-decentralized-apps/cross-chain-messaging)을 활용하여 페어링을 진행하며, 이 과정에서 L2 ERC20 DKA가 수수료로 지불됩니다.

페어링을 설정하기 위한 등록 과정은 L2 ERC20 토큰 컨트랙트의 메서드를 호출하면서 시작되며, \
이를 위해 L2 ERC-20 토큰 컨트랙트가 L2 ERC-20 DKA를 사용할 수 있도록 사전에 승인을 완료해야 합니다.

{% hint style="info" %}
디카르고가 제공하는 토큰 브릿지는 아비트럼의 **Retryable ticket**을 활용하여 구축된 디앱입니다. Retryable ticket은 L2에서 트랜잭션을 생성하고 이를 L3에서 실행할 수 있도록 전달하는 메커니즘입니다.

사용자는 이 과정을 통해 L3에서 원하는 작업을 L2에서 수행할 수 있으며, **트랜잭션 처리에 필요한 수수료는 L2의 ERC-20 DKA로 지불됩니다.**
{% endhint %}

{% code overflow="wrap" %}

```tsx
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}`
```

{% endcode %}

### 3. 등록

범용적 커스텀 게이트웨이에 L2 ERC-20 토큰과 L3 ERC-20 토큰을 등록합니다.

\[L2 ERC20 Example]을 보면 <mark style="color:blue;">`registerTokenOnL2`</mark> 메서드를 통해 <mark style="color:blue;">`customGateway.registerTokenToL2`</mark> 메서드와 <mark style="color:blue;">`customGateway.setGateway`</mark> 메서드가 모두 L2 ERC-20 컨트랙트에서 호출되는 것을 확인할 수 있습니다.

* c<mark style="color:blue;">`ustomGateway.registerTokenToL2`</mark> 메서드는 L2 범용적 커스텀 게이트웨이에서 두 토큰을 페어링하는 작업을 수행하며, 디카르고 체인으로 메시지를 전달하여 L3 범용적 커스텀 게이트웨이에서도 동일한 작업을 수행하도록 합니다.
* <mark style="color:blue;">`customGateway.setGateway`</mark> 메서드는 L2 게이트웨이 라우터에 해당 토큰의 게이트웨이가 범용적 커스텀 게이트웨이임을 등록하는 작업을 수행합니다. 이 과정 역시 디카르고 체인으로 메시지를 전달하여 L3 게이트웨이 라우터에서 동일하게 적용됩니다.

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

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

두 토큰이 각각 독립적인 두 체인에 등록되고 최종적으로 페어링되기까지는 일정 시간이 필요합니다. 등록 요청 상태는 "대기"로 표시되며, 약 10분 후 최종적으로 디카르고 체인에서 호출됩니다.

```tsx
const l2ToL3Msgs = await receipt.getParentToChildMessages(childProvider);

// 메시지가 디카르고 체인에서 호출될 때 까지 대기합니다.
await l2ToL3Msgs[0].waitForStatus();
await l2ToL3Msgs[1].waitForStatus();
```

상태는 [dScanner의 L2 ➔ L3 Transactions 페이지](https://warehouse.dscanner.io/txs-deposits)에서 확인할 수 있습니다.

## 토큰 입출금

이제 범용적 커스텀 게이트웨이를 통해 아비트럼 체인과 디카르고 체인 간의 입출금을 수행할 준비가 완료되었습니다. 이후 입출금 과정은 모두 동일한 방식으로 동작하며, [**표준 게이트웨이 (Standard Gateway)**](https://www.notion.so/160f208f1da980478de7d5372ebc5919?pvs=21)의 입출금 방식을 참고하여 진행하면 됩니다.

여기서 주의해야 할 점이 있습니다. 표준 게이트웨이를 선택한 경우, 최초 입금 시 디카르고 체인에 대응되는 ERC-20 토큰 컨트랙트가 자동으로 배포되며, 이는 [**StandardArbERC20.sol**](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/arbitrum/StandardArbERC20.sol)로 구현된 컨트랙트를 강제한다고 설명드렸습니다.

반면, 범용적 커스텀 게이트웨이를 선택한 경우에는 반드시 위 과정을 먼저 완료한 후 첫 입금을 수행해야 한다는 점을 강조드립니다.
