아비트럼 체인과 디카르고 체인 간의 토큰 유동에는 표준 게이트웨이 방식만으로도 충분합니다.
그러나 표준 게이트웨이 방식에서는 입금 시 디카르고 체인에 대응되는 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로 지불됩니다.
[L2 ERC20 Example]을 보면 registerTokenOnL2 메서드를 통해 customGateway.registerTokenToL2 메서드와 customGateway.setGateway 메서드가 모두 L2 ERC-20 컨트랙트에서 호출되는 것을 확인할 수 있습니다.
customGateway.registerTokenToL2 메서드는 L2 범용적 커스텀 게이트웨이에서 두 토큰을 페어링하는 작업을 수행하며, 디카르고 체인으로 메시지를 전달하여 L3 범용적 커스텀 게이트웨이에서도 동일한 작업을 수행하도록 합니다.
customGateway.setGateway 메서드는 L2 게이트웨이 라우터에 해당 토큰의 게이트웨이가 범용적 커스텀 게이트웨이임을 등록하는 작업을 수행합니다. 이 과정 역시 디카르고 체인으로 메시지를 전달하여 L3 게이트웨이 라우터에서 동일하게 적용됩니다.