dKargo Docs
dKargo.ioWhitepaper
Docs KOR
Docs KOR
  • Documentation
  • 노드 구축
    • 체인 RPC
    • 공통 설치 가이드
    • 체인 노드 종류
    • 풀 노드
    • 아카이브 풀 노드
    • 밸리데이터 노드
  • 지갑 설정
    • MetaMask 연결
    • 지갑 생성
    • 지갑 가져오기
    • 네트워크 추가
  • 밸리데이터 운영
    • 밸리데이터 스테이킹
    • 스테이킹
    • 언스테이킹
    • 클레임
  • 컨트랙트 배포
    • 컨트랙트 배포 방식
    • Remix-IDE를 이용한 컨트랙트 배포
    • Hardhat을 이용한 컨트랙트 배포
    • Foundry를 이용한 컨트랙트 배포
  • ERC-20 브릿징
    • ERC-20 브릿징이란?
    • 표준 게이트웨이
    • 범용적 커스텀 게이트웨이
    • 커스텀 게이트웨이
  • DKA 브릿징
    • DKA 브릿징이란?
    • DKA 입금
    • DKA 출금
  • 포우셋
    • 포우셋이란?
    • 포우셋 사용하기
  • 체인 스냅샷
    • 체인 스냅샷 다운로드
  • 컨트랙트 주소
    • 디카르고 컨트랙트 주소
  • 버그 바운티
    • 버그 바운티 프로그램
Powered by GitBook
On this page
  • 범용적 커스텀 게이트웨이를 통한 토큰 설정
  • 1. 인터페이스
  • 2. 승인
  • 3. 등록
  • 토큰 입출금
  1. ERC-20 브릿징

범용적 커스텀 게이트웨이

Previous표준 게이트웨이Next커스텀 게이트웨이

Last updated 2 months ago

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

그러나 표준 게이트웨이 방식에서는 입금 시 디카르고 체인에 대응되는 ERC-20 토큰 컨트랙트가 자동으로 배포되며, 이는 로 구현된 컨트랙트를 강제합니다.

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

솔리디티로 구현된 컨트랙트 이름에는 L1, L2가 prefix로 명시 되어 있습니다. 이는 아비트럼에서 운영되는 를 디카르고에서 사용하기 때문입니다. 디카르고 토큰 브릿지 컨트랙트의 L1 = 아비트럼(L2), L2 = 디카르고(L3)로 해석됩니다.

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

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

1. 인터페이스

L2에 배포된 ERC20 토큰 컨트랙트는 인터페이스를 준수해야 합니다.

isArbitrumEnabled() 메서드는 토큰을 등록하는 과정에서 호출되며, 범용적 커스텀 게이트웨이를 이용하기 위해서는 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;
    }
}

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

// 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. 승인

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

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

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

사용자는 이 과정을 통해 L3에서 원하는 작업을 L2에서 수행할 수 있으며, 트랜잭션 처리에 필요한 수수료는 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. 등록

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

[L2 ERC20 Example]을 보면 registerTokenOnL2 메서드를 통해 customGateway.registerTokenToL2 메서드와 customGateway.setGateway 메서드가 모두 L2 ERC-20 컨트랙트에서 호출되는 것을 확인할 수 있습니다.

  • customGateway.registerTokenToL2 메서드는 L2 범용적 커스텀 게이트웨이에서 두 토큰을 페어링하는 작업을 수행하며, 디카르고 체인으로 메시지를 전달하여 L3 범용적 커스텀 게이트웨이에서도 동일한 작업을 수행하도록 합니다.

  • customGateway.setGateway 메서드는 L2 게이트웨이 라우터에 해당 토큰의 게이트웨이가 범용적 커스텀 게이트웨이임을 등록하는 작업을 수행합니다. 이 과정 역시 디카르고 체인으로 메시지를 전달하여 L3 게이트웨이 라우터에서 동일하게 적용됩니다.

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

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

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

const l2ToL3Msgs = await receipt.getParentToChildMessages(childProvider);

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

토큰 입출금

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

L3에 배포된 ERC20 토큰 컨트랙트는 인터페이스를 준수해야 합니다.

두 토큰은 독립적인 두 체인에 배포되어 있기 때문에, 을 활용하여 페어링을 진행하며, 이 과정에서 L2 ERC20 DKA가 수수료로 지불됩니다.

상태는 에서 확인할 수 있습니다.

이제 범용적 커스텀 게이트웨이를 통해 아비트럼 체인과 디카르고 체인 간의 입출금을 수행할 준비가 완료되었습니다. 이후 입출금 과정은 모두 동일한 방식으로 동작하며, 의 입출금 방식을 참고하여 진행하면 됩니다.

여기서 주의해야 할 점이 있습니다. 표준 게이트웨이를 선택한 경우, 최초 입금 시 디카르고 체인에 대응되는 ERC-20 토큰 컨트랙트가 자동으로 배포되며, 이는 로 구현된 컨트랙트를 강제한다고 설명드렸습니다.

StandardArbERC20.sol
토큰 브릿지 컨트랙트
ICustomToken
IArbToken
retryable ticket
dScanner의 L2 ➔ L3 Transactions 페이지
표준 게이트웨이 (Standard Gateway)
StandardArbERC20.sol