Bridging L1 and L2 (Portal)

Thanos에서 Portal은 L1의 OptimismPortal 컨트랙트와 L2의 L2ToL1MessagePasser 컨트랙트를 사용하여 네트워크 간 자산의 입출금을 지원합니다. 단, Portal을 통해 전송할 수 있는 자산은 NativeToken으로 한정되며 CrossChainMessenger를 사용하는 것보다 더 낮은 가스비로 거래를 처리할 수 있습니다.

본 문서에서는 입금과 출금 시에 사용되는 Portal의 함수들을 살펴보고 Portal을 import하여 NativeToken을 L1과 L2 간에 입출금하는 방법을 예제를 통해 단계별로 설명합니다.

여기를 통해 Portal에 대한 자세한 코드를 확인할 수 있습니다. Thanos SDK를 활용하여 L1과 L2 간에 NativeToken을 입출금하는 예제 코드는 여기에서 확인할 수 있습니다.

생성자 (Constructor)

생성자는 CrossChainProvider 인스턴스를 초기화하는 데 필요한 정보를 설정합니다. 각 생성자는 L1과 L2 간의 상호작용을 원활하게 처리하기 위해 필수적인 요소들로 구성됩니다.

  • l1SignerOrProvider: L1과 상호작용하기 위한 서명자 또는 제공자

  • l2SignerOrProvider: L2와 상호작용하기 위한 서명자 또는 제공자

  • l1ChainId: L1 체인의 고유 식별자

  • l2ChainId: L2 체인의 고유 식별자

  • depositConfirmationBlocks: 입금이 확인되기까지 필요한 블록 수

  • l1BlockTimeSeconds: L1 체인의 블록 생성 시간 (초)

  • contracts: 오버라이드할 수 있는 특정 계약 주소

/**
   * Creates a new CrossChainProvider instance.
   *
   * @param opts Options for the provider.
   * @param opts.l1SignerOrProvider Signer or Provider for the L1 chain, or a JSON-RPC url.
   * @param opts.l2SignerOrProvider Signer or Provider for the L2 chain, or a JSON-RPC url.
   * @param opts.l1ChainId Chain ID for the L1 chain.
   * @param opts.l2ChainId Chain ID for the L2 chain.
   * @param opts.depositConfirmationBlocks Optional number of blocks before a deposit is confirmed.
   * @param opts.l1BlockTimeSeconds Optional estimated block time in seconds for the L1 chain.
   * @param opts.contracts Optional contract address overrides.
   */
  constructor(opts: {
    l1SignerOrProvider: SignerOrProviderLike
    l2SignerOrProvider: SignerOrProviderLike
    l1ChainId: NumberLike
    l2ChainId: NumberLike
    depositConfirmationBlocks?: NumberLike
    l1BlockTimeSeconds?: NumberLike
    contracts?: DeepPartial<OEContractsLike>
  })

Set Up

Ethereum 블록체인과 상호작용하는 ethers 라이브러리와 @tokamak-network/thanos-sdk 패키지를 import 합니다.

또한, Portal의 생성자를 통해 L1과 L2간의 상호작용을 위한 인스턴스를 생성하고 Portal에서 제공하는 다양한 함수를 사용할 준비를 합니다.

import { ethers } from 'ethers'
import { Portals } from '@tokamak-network/thanos-sdk'

const l1Provider = new ethers.providers.StaticJsonRpcProvider(
  process.env.L1_URL
)
const l2Provider = new ethers.providers.StaticJsonRpcProvider(
  process.env.L2_URL
)

const l1ChainId = (await l1Provider.getNetwork()).chainId
const l2ChainId = (await l2Provider.getNetwork()).chainId

const l1Wallet = new ethers.Wallet(privateKey, l1Provider)
const l2Wallet = new ethers.Wallet(privateKey, l2Provider)

const l1Contracts = {
  AddressManager: addressManager,
  OptimismPortal: optimismPortal,
  L2OutputOracle: l2OutputOracle,
}

const portals = new Portals({
  contracts: {
    l1: l1Contracts,
  },
  l1ChainId,
  l2ChainId,
  l1SignerOrProvider: l1Wallet,
  l2SignerOrProvider: l2Wallet,
})

입금 (Deposit)

  1. NativeToken 생성: L1에서 NativeToken 컨트랙트와 상호작용하기 위한 인스턴스를 생성합니다. 해당 인스턴스를 사용하여 이후 토큰의 승인 및 전송 작업을 수행할 수 있습니다. 컨트랙트의 주소와 ERC20 표준 ABI를 사용하여 NativeToken 컨트랙트 객체를 생성하며 이를 기반으로 OptimismPortal 컨트랙트와의 상호작용이 가능해집니다.

// NativeTokenContract in L1 (like TON)
const l2NativeTokenContract = new ethers.Contract(
  l2NativeToken,
  erc20ABI,
  l1Wallet
)
  1. 사전 승인: L1에서 NativeToken을 입금하기 전, OptimismPortal 컨트랙트가 해당 토큰을 사용할 수 있도록 사전에 승인을 받아야 합니다. 이를 위해 approve 함수를 호출하여 지정된 수량만큼의 토큰을 OptimismPortal에서 처리할 수 있도록 허용합니다. 이 절차는 입금 과정에서 필수이며 승인 이후에 입금 트랜잭션이 성공적으로 진행됩니다.

// approve
const approveTx = await l2NativeTokenContract.approve(optimismPortal, amount)
await approveTx.wait()
console.log('approveTx:', approveTx.hash)
  1. 입금 실행: portals.depositTransaction 함수를 사용하여 L2로 NativeToken을 입금합니다. 이 과정에서 to, value, gasLimit, data와 같은 매개변수를 설정하여 트랜잭션을 실행합니다. 트랜잭션이 블록에 포함되면 depositReceipt을 통해 트랜잭션 해시를 확인할 수 있습니다.

  • to: 입금을 진행할 L2 주소

  • value: 입금 금액

  • gasLimit: 트랜잭션 실행 시 필요한 가스 한도 설정, data.length * 16 + 21000 보다 커야합니다.

  • data: 추가로 전달한 데이터 (기본 값은 0x)

// deposit NativeToken in L1
const depositTx = await portals.depositTransaction({
  to: l2Wallet.address,
  value: BigNumber.from(amount),
  gasLimit: BigNumber.from('200000'), // It will be greater than data.length * 16 + 21000
  data: '0x',
})
const depositReceipt = await depositTx.wait()
console.log('depositTx:', depositReceipt.transactionHash)
  1. 입금 확인: 입금이 성공적으로 처리되었는지 확인하기 위해 portals.waitingDepositTransactionRelayed 함수를 사용합니다. 이 함수는 트랜잭션이 L2에 도달하고 최종적으로 처리되었는지 확인 후 관련된 트랜잭션 해시를 반환합니다. getTransactionReceipt를 통해 입금이 올바르게 처리되었는지 확인합니다.

// get relayedTxHash in L2
const relayedTxHash = await portals.waitingDepositTransactionRelayed(
  depositReceipt,
  {}
)
console.log('relayedTxHash:', relayedTxHash)
const depositedTxReceipt = await l2Provider.getTransactionReceipt(
  relayedTxHash
)

출금 (Withdrawal)

  1. 출금 시작: portals.initiateWithdrawal함수를 사용하여 L2에서 L1으로 출금을 시작합니다. 이 함수는 출금 트랜잭션을 생성하고 target, value, gasLimit, data와 같은 매개변수를 사용하여 출금을 요청합니다. withdrawalReceipt을 통해 트랜잭션이 완료될 때까지 대기합니다.

  • target: 출금을 진행할 L1 주소

  • value: 출금 금액

  • gasLimit: 가스 한도 설정

  • data: 추가로 전달한 데이터 (기본 값은 0x)

const withdrawalTx = await portals.initiateWithdrawal({
  target: l1Wallet.address,
  value: BigNumber.from(amount),
  gasLimit: BigNumber.from('200000'),
  data: '0x12345678',
})
const withdrawalReceipt = await withdrawalTx.wait()
  1. 릴레이 대기: portals.waitForWithdrawalTxReadyForRelayUsingL2Tx 함수를 통해 L2 트랜잭션 해시를 기반으로 출금 트랜잭션이 L1으로 릴레이가 될 때까지 대기합니다.

await portals.waitForWithdrawalTxReadyForRelayUsingL2Tx(
  withdrawalReceipt.transactionHash
)
  1. 출금 증명: 출금 트랜잭션이 L2에서 L1으로 릴레이될 준비가 되었음을 확인한 후 portals.proveWithdrawalTransactionUsingL2Tx 함수를 통해 L2 트랜잭션 해시를 기반으로 출금 트랜잭션을 증명합니다. 이 단계에서는 출금 트랜잭션의 유효성을 확인하기 위해 증명 트랜잭션을 제출합니다.

const proveTransaction = await portals.proveWithdrawalTransactionUsingL2Tx(
  withdrawalReceipt.transactionHash
)
await proveTransaction.wait()
  1. 확정 대기: portals.waitForFinalizationUsingL2Tx 함수를 사용하여 출금 트랜잭션이 확정될 때까지 대기합니다. 출금 트랜잭션의 확정이 완료되면 출금이 성공적으로 처리됩니다.

await portals.waitForFinalizationUsingL2Tx(withdrawalReceipt.transactionHash)
  1. 출금 확정: portals.finalizeWithdrawalTransactionUsingL2Tx함수를 사용하여 L2 트랜잭션 해시를 기반으로 출금 트랜잭션을 확정합니다. 이 단계에서 출금 트랜잭션의 최종 처리가 완료되며 finalizedTransactionReceipt을 확인합니다.

const finalizedTransaction =
  await portals.finalizeWithdrawalTransactionUsingL2Tx(
    withdrawalReceipt.transactionHash
  )
const finalizedTransactionReceipt = await finalizedTransaction.wait()
console.log('finalized transaction receipt:', finalizedTransactionReceipt)
  1. 토큰 전송: 출금이 확정된 후 OptimismPortal 컨트랙트를 이용하여 사용자의 지갑 주소로 토큰을 전송합니다. 이 과정은 확정된 출금 트랜잭션에 따라 L1으로 토큰을 전송하는 작업을 수행합니다.

// Transferfrom (after finalization, user have to transferFrom to get his token)
const transferTx = await l2NativeTokenContract.transferFrom(
  l1Contracts.OptimismPortal,
  l1Wallet.address,
  amount
)
await transferTx.wait()

트랜잭션 상태 확인 (getMessageStatus)

getMessageStatusOptimismPortalL2ToL1MessagePasser를 사용할 때 L1과 L2 간의 입출금 트랜잭션의 상태를 확인하는 데 사용됩니다.

OptimismPortal

  1. 트랜잭션 생성: portals.depositTransaction 함수를 사용하여 L1에서 L2로 NativeToken 입금 트랜잭션을 생성합니다.

const depositTx = await portals.depositTransaction({
    to: l2Wallet.address,
    value: BigNumber.from(amount),
    gasLimit: BigNumber.from('200000'),
    data: '0x',
  })
  1. 트랜잭션 대기: depositTx.wait()을 호출하여 트랜잭션이 완료되면 트랜잭션 영수증인 depositReceipt 를 받습니다. 이 영수증에는 transactionHash 등 중요한 정보가 포함됩니다.

const depositReceipt = await depositTx.wait()
console.log('depositTx:', depositReceipt.transactionHash)
  1. 릴레이 해시: 트랜잭션이 L1에서 성공적으로 처리한 후 해당 트랜잭션이 L2로 릴레이될 때까지 기다려야 합니다. portals.waitingDepositTransactionRelayed 함수를 사용하여 L1에서 제출된 트랜잭션이 L2에 성공적으로 릴레이되었는지 확인하고 relayedTxHash를 가져옵니다. 이 해시는 L2에서 발생한 트랜잭션을 추적하는 데 사용됩니다.

const relayedTxHash = await portals.waitingDepositTransactionRelayed(
  depositReceipt,
  {}
)
  1. 상태 확인: portals.getMessageStatus를 사용하여 입금 트랜잭션의 상태를 확인하고 L2에서 성공적으로 릴레이되었는지 확인합니다.

const status = await portals.getMessageStatus(depositReceipt)
console.log('deposit status relayed:', status === MessageStatus.RELAYED)

이 흐름을 통해 L1에서 L2로 NativeToken을 입금한 후 getMessageStatus를 사용하여 트랜잭션의 최종 상태를 모니터링하여 전체 입금 과정이 정상적으로 처리되었는지 확인할 수 있습니다.

L2ToL1MessagePasser

  1. 출금 트랜잭션 생성 및 제출: portals.initiateWithdrawal을 통해 L2에서 L1으로 출금 트랜잭션을 생성합니다. 생성된 트랜잭션의 결과는 withdrawalReceipt 으로 반환되며 이후 출금 트랜잭션의 상태를 추적하는 데 사용됩니다.

const withdrawalTx = await portals.initiateWithdrawal({
    target: l1Wallet.address,
    value: BigNumber.from(amount),
    gasLimit: BigNumber.from('200000'),
    data: '0x12345678',
  })
const withdrawalReceipt = await withdrawalTx.wait()
  1. 출금 메세지 정보 계산: portals.calculateWithdrawalMessage는 출금 메시지에 대한 정보를 계산합니다. 이 정보는 출금 트랜잭션의 다음 단계인 증명 및 확정 프로세스에서 활용됩니다.

const withdrawalMessageInfo = await portals.calculateWithdrawalMessage(
  withdrawalReceipt
)
console.log('withdrawalMessageInfo:', withdrawalMessageInfo)
  1. 출금 트랜잭션 상태 확인: portals.getMessageStatus를 사용하여 출금 트랜잭션의 상태를 확인합니다. 이 단계에서는 L2에서 root가 성공적으로 게시되었는지 확인합니다.

let status = await portals.getMessageStatus(withdrawalReceipt)
console.log('[Withdrawal Status] check publish L2 root:', status)
  1. 릴레이 준비 상태 대기: 출금 트랜잭션이 L1으로 릴레이될 준비가 될 때까지 portals.waitForWithdrawalTxReadyForRelay 함수를 사용하여 대기합니다. 이 과정은 L2에서 생성된 출금 메시지가 L1으로 전송될 준비가 되었음을 나타냅니다.

await portals.waitForWithdrawalTxReadyForRelay(withdrawalReceipt)
  1. 출금 트랜잭션 상태 확인 1: getMessageStatus를 다시 호출하여 출금 트랜잭션이 L1에서 증명될 준비가 되었는지 확인합니다.

status = await portals.getMessageStatus(withdrawalReceipt)
console.log('[Withdrawal Status] check ready for proving:', status)
  1. 출금 트랜잭션 증명: portals.proveWithdrawalTransaction을 사용하여 L1에서 출금 트랜잭션을 증명합니다. 이 단계는 출금 트랜잭션을 L1에서 확인하는 과정입니다.

const proveTransaction = await portals.proveWithdrawalTransaction(
  withdrawalMessageInfo
)
await proveTransaction.wait()
  1. 출금 트랜잭션 상태 확인 2: 트랜잭션이 L1에서 챌린징 상태에 있는지 getMessageStatus로 확인합니다. 이 상태는 트랜잭션이 아직 처리 중임을 나타냅니다.

status = await portals.getMessageStatus(withdrawalReceipt)
console.log('[Withdrawal Status] check in challenging:', status)
  1. 확정 대기: 트랜잭션이 최종적으로 완료될 때까지 portals.waitForFinalization을 호출해 대기합니다. 이후, 트랜잭션의 확정 프로세스를 수행합니다.

await portals.waitForFinalization(withdrawalMessageInfo)
const finalizedTransaction = await portals.finalizeWithdrawalTransaction(
  withdrawalMessageInfo
  1. 출금 트랜잭션 확정 및 상태 확인: portals.finalizeWithdrawalTransaction을 사용하여 출금 트랜잭션을 확정합니다. 이 과정은 트랜잭션이 성공적으로 완료되었음을 나타냅니다. 마지막으로 getMessageStatus를 호출하여 출금 트랜잭션이 L1에서 성공적으로 릴레이되었는지 확인합니다.

const finalizedTransactionReceipt = await finalizedTransaction.wait()
console.log('finalized transaction receipt:', finalizedTransactionReceipt)

status = await portals.getMessageStatus(withdrawalReceipt)
console.log('[Withdrawal Status] check relayed:', status)

Last updated