범용적 커스텀 게이트웨이
아비트럼 체인과 디카르고 체인 간의 토큰 유동에는 표준 게이트웨이 방식만으로도 충분합니다.
그러나 표준 게이트웨이 방식에서는 입금 시 디카르고 체인에 대응되는 ERC-20 토큰 컨트랙트가 자동으로 배포되며, 이는 StandardArbERC20.sol로 구현된 컨트랙트를 강제합니다.
개발자 또는 프로젝트 빌더가 자신이 구현한 ERC-20 컨트랙트에 추가적인 기능을 포함하거나, 디카르고 체인에서 특정 ERC-20 컨트랙트와 페어링되도록 설정하기를 원한다면, 범용적 커스텀 게이트웨이 (Generic-Custom Gateway)를 선택하여 더 큰 자유도를 얻을 수 있습니다.
범용적 커스텀 게이트웨이를 통한 토큰 설정
범용적 커스텀 게이트웨이를 사용하기 위해서는 L2에 배포된 ERC-20 컨트랙트는 다음과 같은 규칙을 준수해야합니다.
1. 인터페이스
L2에 배포된 ERC20 토큰 컨트랙트는 ICustomToken 인터페이스를 준수해야 합니다.
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;
}
}
L3에 배포된 ERC20 토큰 컨트랙트는 IArbToken 인터페이스를 준수해야 합니다.
입출금 과정에서 범용적 커스텀 게이트웨이 컨트랙트에서만 호출 가능한(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 토큰을 등록하여 페어링을 설정할 예정입니다.
두 토큰은 독립적인 두 체인에 배포되어 있기 때문에, retryable ticket을 활용하여 페어링을 진행하며, 이 과정에서 L2 ERC20 DKA가 수수료로 지불됩니다.
페어링을 설정하기 위한 등록 과정은 L2 ERC20 토큰 컨트랙트의 메서드를 호출하면서 시작되며, 이를 위해 L2 ERC-20 토큰 컨트랙트가 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 컨트랙트에서 호출되는 것을 확인할 수 있습니다.
c
ustomGateway.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();
상태는 dScanner의 L2 ➔ L3 Transactions 페이지에서 확인할 수 있습니다.
토큰 입출금
이제 범용적 커스텀 게이트웨이를 통해 아비트럼 체인과 디카르고 체인 간의 입출금을 수행할 준비가 완료되었습니다. 이후 입출금 과정은 모두 동일한 방식으로 동작하며, 표준 게이트웨이 (Standard Gateway)의 입출금 방식을 참고하여 진행하면 됩니다.
여기서 주의해야 할 점이 있습니다. 표준 게이트웨이를 선택한 경우, 최초 입금 시 디카르고 체인에 대응되는 ERC-20 토큰 컨트랙트가 자동으로 배포되며, 이는 StandardArbERC20.sol로 구현된 컨트랙트를 강제한다고 설명드렸습니다.
반면, 범용적 커스텀 게이트웨이를 선택한 경우에는 반드시 위 과정을 먼저 완료한 후 첫 입금을 수행해야 한다는 점을 강조드립니다.
Last updated