メインコンテンツへスキップ

Optimismの標準ブリッジコントラクトを紹介します

SolidityOptimismブリッジレイヤー2
中級
Ori Pomerantz
2022年3月30日
50 分の読書 minute read

Optimism(opens in a new tab)は、Optimisitc ロールアップを行うメカニズムのひとつです。 Optimistic ロールアップでは、ネットワークに含まれるすべてノードではなく一部のノードのみを対象としてトランザクションが処理されるため、イーサリアム・メインネット(「レイヤー 1」または「L1」とも呼ばれます)よりも手数料が低くなります。 一部のノードのみを対象として処理されるものの、すべてのデータは L1 に書き込まれるため、あらゆる事項につき、メインネットにおける完全性および可用性についての保証に基づいて証明、再構築することが可能です。

Optimism(またはその他の L2)上で L1 のアセットを使用するには、当該アセットをブリッジする必要があります。 アセットをブリッジする方法のひとつとして、アセット(最も一般的なのは、ETH やERC-20 トークンです)を L1 上でロックし、L2 上で同等のアセットを受け取る方法があります。 最終的に、これらのアセットを所持するユーザーは、再度 L1 にブリッジする必要があるでしょう。 L1 にアセットをブリッジすると、L2 上のアセットはバーンされ、L1 上のアセットがユーザーに戻されます。

以上が、Optimism における標準ブリッジ(opens in a new tab)の仕組みです。 この記事では、このブリッジ機能について Solidity 上で適切に作成したソースコードを確認しながら、その仕組みを学びます。

制御フロー

ブリッジは、2 つのメインフローで構成されます:

  • L1 から L2 への入金
  • L2 から L1 への出金

入金フロー

L1

  1. ERC-20 を入金する場合、入金者はブリッジに対し、入金額を使用するためのアローワンスを与えます。
  2. 入金者は、L1 ブリッジ(depositERC20depositERC20TodepositETHあるいは depositETHTo)を呼び出します。
  3. L1 ブリッジが、ブリッジされたアセットを保持します。
    • ETH の場合:アセットは、呼び出しを通じて入金者に送信されます。
    • ERC-20 トークンの場合:アセットは、入金者が提供するアローワンスを使用して、ブリッジ自体に送信されます。
  4. L1 のブリッジが、クロスドメインのメッセージメカニズムを通じて、L2 のブリッジ上でfinalizeDepositを呼び出します。

L2

  1. L2 のブリッジは、finalizeDepositの呼び出しにつき、以下が適切であることを確認します:
    • クロスドメインのメッセージ・コントラクトからの呼び出しであること。
    • ブリッジが L1 上で作成されたものであること。
  2. L2 のブリッジはさらに、L2 上の ERC-20 トークンコントラクトにつき、以下が適切であることを確認します:
    • L2 のコントラクトにおいて、L1 における対応するコントラクトが、L1 上で送信されたトークンと同一であると報告していること。
    • L2 のコントラクトが、(ERC-165 を使用した(opens in a new tab))適切なインターフェイスをサポートすると報告していること。
  3. L2 のコントラクトが適切であると確認できた場合は、適切なアドレスに対して希望する量のトークンをミントするために、そのコントラクトを呼び出してください。 そうでない場合は、出金プロセスを開始して、ユーザーが L1 上のトークンを請求できるようにします。

出金フロー

L2

  1. 出金者は、L2 のブリッジ(withdrawまたはwithdrawTo)を呼び出します 。
  2. L2 のブリッジは、msg.senderが所有する適切な数のトークンをバーンします。
  3. L2 のブリッジは、クロスドメインのメッセージ・メカニズムを利用して、L1 のブリッジ上で、 finalizeETHWithdrawalまたはfinalizeERC20Withdrawalを呼び出します。

L1

  1. L1 のブリッジは、finalizeETHWithdrawまたはfinalizeERC20Withdralの呼び出しにつき、以下が適切であるかを確認します:
    • クロスドメインのメッセージ・メカニズムを経由していること。
    • L2 のブリッジで作成されていること。
  2. L1 のブリッジは、適切な資産(ETH または ERC-20)を適切なアドレスに送信します。

レイヤー 1 のコード

以下は、イーサリアム・メインネット(L1)で実行されるコードです。

IL1ERC20Bridge

このインターフェイスは、こちら(opens in a new tab)で定義されています。 このインターフェイスには、ERC-20 トークンをブリッジするために必要な機能と定義が含まれます。

1// SPDX-License-Identifier: MIT
コピー

Optimism のコードの大部分は、MIT ライセンス(opens in a new tab)に基づいています。

1pragma solidity >0.5.0 <0.9.0;
コピー

本記事の執筆時点で、Solidity の最新バージョンは 0.8.12 です。 バージョン 0.9.0 においてこのコードが利用できるかは、同バージョンがリリースされるまで不明です。

1/**
2 * @title IL1ERC20Bridge
3 */
4interface IL1ERC20Bridge {
5 /**********
6 * Events *
7 **********/
8
9 event ERC20DepositInitiated(
すべて表示
コピー

Optimism のブリッジ関連用語において、入金とは、L1 から L2 に送金することを意味し、出金とは、L2 から L1 へ送金することを意味します。

1 address indexed _l1Token,
2 address indexed _l2Token,
コピー

ほとんどの場合、L1 上の ERC-20 のアドレスは、L2 上で対応する ERC-20 のアドレスとは異なります。 トークンアドレスのリストは、こちら(opens in a new tab)を参照してください。 chainIdが 1 のアドレスであれば、L1 (メインネット) 上のトークンであり、chainIdが 10 のアドレスでれば、L2(Optimism)上のトークンです。 残りの 2 つのchainIdの値は、Kovan テストネットワーク(42)と Optimistic Kovan テストネットワーク(69)のためのものです。

1 address indexed _from,
2 address _to,
3 uint256 _amount,
4 bytes _data
5 );
コピー

転送にはメモを追加することができ、メモは転送を報告するイベントに追加されます。

1 event ERC20WithdrawalFinalized(
2 address indexed _l1Token,
3 address indexed _l2Token,
4 address indexed _from,
5 address _to,
6 uint256 _amount,
7 bytes _data
8 );
コピー

入金と出金の両方向につき、同じブリッジのコントラクトが処理します。 つまり、L1 のブリッジでは入金を初期化し、出金を確定します。

1
2 /********************
3 * Public Functions *
4 ********************/
5
6 /**
7 * @dev get the address of the corresponding L2 bridge contract.
8 * @return Address of the corresponding L2 bridge contract.
9 */
10 function l2TokenBridge() external returns (address);
すべて表示
コピー

実際には、L2 のブリッジは事前にデプロイされており、常に「0x42000000000000000000000000000000000000000010」のアドレスとなるため、この機能は必要ではありません。 この機能は、L2 上のブリッジとの対称性を確保するためのものです。というのも、L1 ブリッジのアドレスを確認することは無意味とは言えないためです。

1 /**
2 * @dev deposit an amount of the ERC20 to the caller's balance on L2.
3 * @param _l1Token Address of the L1 ERC20 we are depositing
4 * @param _l2Token Address of the L1 respective L2 ERC20
5 * @param _amount Amount of the ERC20 to deposit
6 * @param _l2Gas Gas limit required to complete the deposit on L2.
7 * @param _data Optional data to forward to L2. This data is provided
8 * solely as a convenience for external contracts. Aside from enforcing a maximum
9 * length, these contracts provide no guarantees about its content.
10 */
11 function depositERC20(
12 address _l1Token,
13 address _l2Token,
14 uint256 _amount,
15 uint32 _l2Gas,
16 bytes calldata _data
17 ) external;
すべて表示
コピー

_l2Gasのパラメータは、このトランザクションが使用できる L2 上のガス量です。 この値は、一定の(高い)上限まで無料であるため(opens in a new tab)、ミント時に ERC-20 がコントラクトが特に異常な動作を行わない限り、問題は発生しません。 以下の関数は、異なるブロックチェーンにおける同一アドレスにアセットをブリッジしたいという一般的なシナリオで用いることができます。

1 /**
2 * @dev deposit an amount of ERC20 to a recipient's balance on L2.
3 * @param _l1Token Address of the L1 ERC20 we are depositing
4 * @param _l2Token Address of the L1 respective L2 ERC20
5 * @param _to L2 address to credit the withdrawal to.
6 * @param _amount Amount of the ERC20 to deposit.
7 * @param _l2Gas Gas limit required to complete the deposit on L2.
8 * @param _data Optional data to forward to L2. This data is provided
9 * solely as a convenience for external contracts. Aside from enforcing a maximum
10 * length, these contracts provide no guarantees about its content.
11 */
12 function depositERC20To(
13 address _l1Token,
14 address _l2Token,
15 address _to,
16 uint256 _amount,
17 uint32 _l2Gas,
18 bytes calldata _data
19 ) external;
すべて表示
コピー

次のコードはdepositERC20とほぼ同一の関数ですが、ERC-20 トークンを異なるアドレスに送信することができます。

1 /*************************
2 * Cross-chain Functions *
3 *************************/
4
5 /**
6 * @dev Complete a withdrawal from L2 to L1, and credit funds to the recipient's balance of the
7 * L1 ERC20 token.
8 * This call will fail if the initialized withdrawal from L2 has not been finalized.
9 *
10 * @param _l1Token Address of L1 token to finalizeWithdrawal for.
11 * @param _l2Token Address of L2 token where withdrawal was initiated.
12 * @param _from L2 address initiating the transfer.
13 * @param _to L1 address to credit the withdrawal to.
14 * @param _amount Amount of the ERC20 to deposit.
15 * @param _data Data provided by the sender on L2. This data is provided
16 * solely as a convenience for external contracts. Aside from enforcing a maximum
17 * length, these contracts provide no guarantees about its content.
18 */
19 function finalizeERC20Withdrawal(
20 address _l1Token,
21 address _l2Token,
22 address _from,
23 address _to,
24 uint256 _amount,
25 bytes calldata _data
26 ) external;
27}
すべて表示
コピー

Optimism における出金(および、他の L2 から L1 へのメッセージ送信)は、2 つのステップで実行されます:

  1. L2 でトランザクションを開始する。
  2. L1 で、トランザクションを確定/クレームする。 L1 でのトランザクションは、L2 でのトランザクションに対する異議申し立て期間(opens in a new tab) が終了した後で実行可能になります。

IL1StandardBridge

このインターフェイスは、こちら(opens in a new tab)で定義されています。 このブリッジには、ETH を対象とするイベントおよび関数の定義が含まれています。 これらの定義は、ERC-20 トークンを対象とするIL1ERC20Bridgeの定義とほぼ同一です。

一部の ERC-20 トークンは、標準ブリッジでは処理できず、カスタム処理が必要となるため、このブリッジのインターフェイスは 2 つのファイルで構成されています。 これにより、カスタム処理が必要なトークンを取り扱うブリッジについてはIL1ERC20Bridgeを実装すればよく、ETH のブリッジ機能を実装する必要はありません。

1// SPDX-License-Identifier: MIT
2pragma solidity >0.5.0 <0.9.0;
3
4import "./IL1ERC20Bridge.sol";
5
6/**
7 * @title IL1StandardBridge
8 */
9interface IL1StandardBridge is IL1ERC20Bridge {
10 /**********
11 * Events *
12 **********/
13 event ETHDepositInitiated(
14 address indexed _from,
15 address indexed _to,
16 uint256 _amount,
17 bytes _data
18 );
すべて表示
コピー

このイベントは、ERC-20 トークン用のイベント(ERC20DepositInitiated)とほぼ同一ですが、L1 および L2 上のトークンアドレスが含まれていない点が異なります。 他のイベントおよび関数についても、この点が異なります。

1 event ETHWithdrawalFinalized(
2 .
3 .
4 .
5 );
6
7 /********************
8 * Public Functions *
9 ********************/
10
11 /**
12 * @dev Deposit an amount of the ETH to the caller's balance on L2.
13 .
14 .
15 .
16 */
17 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable;
18
19 /**
20 * @dev Deposit an amount of ETH to a recipient's balance on L2.
21 .
22 .
23 .
24 */
25 function depositETHTo(
26 address _to,
27 uint32 _l2Gas,
28 bytes calldata _data
29 ) external payable;
30
31 /*************************
32 * Cross-chain Functions *
33 *************************/
34
35 /**
36 * @dev Complete a withdrawal from L2 to L1, and credit funds to the recipient's balance of the
37 * L1 ETH token. Since only the xDomainMessenger can call this function, it will never be called
38 * before the withdrawal is finalized.
39 .
40 .
41 .
42 */
43 function finalizeETHWithdrawal(
44 address _from,
45 address _to,
46 uint256 _amount,
47 bytes calldata _data
48 ) external;
49}
すべて表示
コピー

CrossDomainEnabled

このコントラクト(opens in a new tab)は、L1およびL2の両方のブリッジにおいて継承され、相手のレイヤーに対してメッセージを送信します。

1// SPDX-License-Identifier: MIT
2pragma solidity >0.5.0 <0.9.0;
3
4/* Interface Imports */
5import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";
コピー

このインターフェース(opens in a new tab)は、クロスドメインのメッセンジャーを用いて、相手のレイヤーに対するメッセージの送信方法をコントラクトに通知するものです。 このクロスドメインのメッセンジャーは完全に別個のシステムであるため、今後改めて記事を執筆したいと考えています。

1/**
2 * @title CrossDomainEnabled
3 * @dev Helper contract for contracts performing cross-domain communications
4 *
5 * Compiler used: defined by inheriting contract
6 */
7contract CrossDomainEnabled {
8 /*************
9 * Variables *
10 *************/
11
12 // Messenger contract used to send and receive messages from the other domain.
13 address public messenger;
14
15 /***************
16 * Constructor *
17 ***************/
18
19 /**
20 * @param _messenger Address of the CrossDomainMessenger on the current layer.
21 */
22 constructor(address _messenger) {
23 messenger = _messenger;
24 }
すべて表示
コピー

このコントラクトが知る必要がある唯一のパラメータは、当該レイヤーにおけるクロスドメイン・メッセンジャーのアドレスです。 このパラメータは、コンストラクタ上で設定された後は変更されません。

1
2 /**********************
3 * Function Modifiers *
4 **********************/
5
6 /**
7 * Enforces that the modified function is only callable by a specific cross-domain account.
8 * @param _sourceDomainAccount The only account on the originating domain which is
9 * authenticated to call this function.
10 */
11 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {
すべて表示
コピー

クロスドメインのメッセージング機能は、ブロックチェーン(イーサリアム・メインネットまたは Optimism)上で実行されているあらゆるコントラクトからアクセス可能です。 ただし、相手方のブリッジから送信された特定のメッセージのみを信頼するためには、双方のブリッジが必要になります。

1 require(
2 msg.sender == address(getCrossDomainMessenger()),
3 "OVM_XCHAIN: messenger contract unauthenticated"
4 );
コピー

適切なクロスドメイン・メッセンジャー (以下のmessengerを参照)から送信されたメッセージのみ、信頼できます。

1
2 require(
3 getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount,
4 "OVM_XCHAIN: wrong sender of cross-domain message"
5 );
コピー

クロスドメイン・メッセンジャーでは、the .xDomainMessageSender() 関数(opens in a new tab)を用いて、相手方のレイヤーにメッセージを送信するアドレスを提供します。 当該メッセージで開始されたトランザクションで呼び出す場合に限り、メッセージの送信元アドレスを表示することができます。

このメッセージにつき、相手方のブリッジから送信されたものであることを確認する必要があります。

1
2 _;
3 }
4
5 /**********************
6 * Internal Functions *
7 **********************/
8
9 /**
10 * Gets the messenger, usually from storage. This function is exposed in case a child contract
11 * needs to override.
12 * @return The address of the cross-domain messenger contract which should be used.
13 */
14 function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) {
15 return ICrossDomainMessenger(messenger);
16 }
すべて表示
コピー

この機能は、クロスドメイン・メッセンジャーを返します。 messengerの変数ではなく関数を使うのは、この値を継承するコントラクトに対し、どのクロスドメイン・メッセンジャーを使用するかを特定するアルゴリズムを使用できるようにするためです。

1
2 /**
3 * Sends a message to an account on another domain
4 * @param _crossDomainTarget The intended recipient on the destination domain
5 * @param _message The data to send to the target (usually calldata to a function with
6 * `onlyFromCrossDomainAccount()`)
7 * @param _gasLimit The gasLimit for the receipt of the message on the target domain.
8 */
9 function sendCrossDomainMessage(
10 address _crossDomainTarget,
11 uint32 _gasLimit,
12 bytes memory _message
すべて表示
コピー

最後に、次の関数は相手方のレイヤーにメッセージを送信するものです。

1 ) internal {
2 // slither-disable-next-line reentrancy-events, reentrancy-benign
コピー

Slither(opens in a new tab)は、Optimism において、すべてのコントラクトの脆弱性やその他の潜在的な問題箇所を特定するために実行される静的解析ツールです。 ここでは、以下の行により 2 つの脆弱性がトリガーされます。

  1. リエントランシーのイベント(opens in a new tab)
  2. 無害のリエントランシー(opens in a new tab)
1 getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
2 }
3}
コピー

このケースでは、Slither が当該情報を把握する方法を持たない場合でも、getCrossDomainMessenger()が信頼できるアドレスを返すと分かっているため、リエントランシーについて心配する必要はありません。

L1 上のブリッジコントラクト

このコントラクトのソースコードは、こちら(opens in a new tab)で入手してください。

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;
コピー

このインターフェイスは、他のコントラクトにも含まれる場合があるため、Solidity のさまざまなバージョンをサポートする必要があります。 しかしここでは、ブリッジ自体がコントラクトであるため、使用できる Solidity のバージョンを限定することが可能です。

1/* Interface Imports */
2import { IL1StandardBridge } from "./IL1StandardBridge.sol";
3import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol";
コピー

IL1ERC20BridgeIL1StandardBridgeについては、すでに説明しました。

1import { IL2ERC20Bridge } from "../../L2/messaging/IL2ERC20Bridge.sol";
コピー

このインターフェイス(opens in a new tab)では、L2 上で標準ブリッジを制御するためのメッセージを作成します。

1import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
コピー

このインターフェイス(opens in a new tab)は、ERC-20 のコントラクトを制御するために使用します。 詳細については、こちらをご覧ください。

1/* Library Imports */
2import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";
コピー

上記で説明したように、このコントラクトはレイヤー間のメッセージングに使用します。

1import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";
コピー

Lib_PredeployAddresses(opens in a new tab)には、常に同じアドレスを持つ L2 上のコントラクトのアドレスが含まれています。 これには、L2 上の標準ブリッジが含まれます。

1import { Address } from "@openzeppelin/contracts/utils/Address.sol";
コピー

OpenZeppelin のアドレス・ユーティリティ(opens in a new tab)です。 これは、コントラクト上のアドレスと外部所有アカウント(EOA)に含まれるアドレスを区別するために使われます。

これは、直接の呼び出しとコントラクトのコンストラクタで作成した呼び出しを区別できないため完全なソリューションとは言えませんが、少なくとも、よくあるユーザーエラーを特定、防止することは可能です。

1import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
コピー

ERC-20 標準(opens in a new tab)では、コントラクトが実行失敗を報告する手段として以下の 2 つがあります:

  1. 元に戻す
  2. falseを返す

両方のケースに対応するとコードがより複雑になるため、代わりにOpenZeppelin のSafeERC20(opens in a new tab)を使用します。これにより、失敗した場合は常に元に戻されます(opens in a new tab)

1/**
2 * @title L1StandardBridge
3 * @dev The L1 ETH and ERC20 Bridge is a contract which stores deposited L1 funds and standard
4 * tokens that are in use on L2. It synchronizes a corresponding L2 Bridge, informing it of deposits
5 * and listening to it for newly finalized withdrawals.
6 *
7 */
8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {
9 using SafeERC20 for IERC20;
すべて表示
コピー

この行は、IERC20インターフェイスを使用する際に、常にIERC20ラッパーを用いることを指定するものです。

1
2 /********************************
3 * External Contract References *
4 ********************************/
5
6 address public l2TokenBridge;
コピー

L2StandardBridgeのアドレスです。

1
2 // Maps L1 token to L2 token to balance of the L1 token deposited
3 mapping(address => mapping(address => uint256)) public deposits;
コピー

2 次元スパース配列(opens in a new tab)を定義するには、このようなダブルマッピング(opens in a new tab)を用います。 このデータ構造の値は、deposit[L1 token addr][L2 token addr]として識別されます。 初期値はゼロになります。 ゼロ以外の値が設定されたセルのみが、ストレージに書き込まれます。

1
2 /***************
3 * Constructor *
4 ***************/
5
6 // This contract lives behind a proxy, so the constructor parameters will go unused.
7 constructor() CrossDomainEnabled(address(0)) {}
コピー

ストレージ内のすべての変数をコピーせずに、このコントラクトを更新するには、 プロキシ(opens in a new tab)を使用します。プロキシは、delegatecall(opens in a new tab)を用いて、プロキシのコントラクトにおいてアドレスが保存された別個のコントラクトに対して呼び出しを転送するものです(コントラクトを更新する際に、プロキシに対してこのアドレスを変更するように指示することになります)。 「delegatecall」を使用すると、ストレージは、呼び出し元のコントラクトのストレージのままになるため、すべてのコントラクト状態変数の値は影響を受けません。

このパターンを用いる効果のひとつとして、delegatecall呼び出し先であるコントラクトのストレージが使用されないため、送信されたコンストラクタの値が意味を持たない点が挙げられます。 これこそ、CrossDomainEnabledのコンストラクタに対して無意味な値を入力できる理由です。 同時に、以下の初期化がコンストラクタとは別個に存在するである理由でもあります。

1 /******************
2 * Initialization *
3 ******************/
4
5 /**
6 * @param _l1messenger L1 Messenger address being used for cross-chain communications.
7 * @param _l2TokenBridge L2 standard bridge address.
8 */
9 // slither-disable-next-line external-function
すべて表示
コピー

このSlither のテスト(opens in a new tab)は、コントラクトのコードにより呼び出される関数以外の関数であり、publicではなく、externalと宣言される関数を特定するものです。 external関数のガス代は、コールデータに含まれるパラメータで指定できるため、安価に抑えることができます。 publicと宣言された関数は、コントラクト内でアクセス可能である必要があります。 コントラクトはそれ自体のコールデータを変更できないため、パラメータはメモリに保存する必要があります。 このような関数を外部から呼び出す場合、コールデータをメモリにコピーする必要があるため、ガス代が発生します。 今回は関数を 1 回のみ呼び出すため、コスト上の非効率性は度外視できます。

1 function initialize(address _l1messenger, address _l2TokenBridge) public {
2 require(messenger == address(0), "Contract has already been initialized.");
コピー

initialize関数は、1 回だけ呼び出す必要があります。 L1 のクロスドメイン・メッセンジャーまたは L2 のトークンブリッジのいずれかのアドレスが変更されると、新しいプロキシとそれを呼び出す新しいブリッジが作成されます。 これは、システム全体がアップグレードされた場合を除いて発生する可能性は低く、非常にまれです。

この関数には、呼び出し可能なアカウントを制限するメカニズムが含まれない点に注意してください。 つまり理論的には、ネットワークに対する攻撃者は、プロキシとブリッジの最初のバージョンがデプロイされるまで待機し、正当なユーザーがinitialize関数にアクセスできる前にフロントラン(opens in a new tab)を実行することが可能です。 これを防ぐには、以下の 2 つの方法があります:

  1. コントラクトが EOA により直接デプロイされるのではなく、別のコントラクトが作成したトランザクションにおいて(opens in a new tab)デプロイされる場合、全体のプロセスがアトミックになり、他のトランザクションが実行される前に終了する場合があります。
  2. 正当なinitialize呼び出しが失敗した場合、常に、新たに作成されたプロキシおよびブリッジを無視し、さらに新しいものを作成することが可能です。
1 messenger = _l1messenger;
2 l2TokenBridge = _l2TokenBridge;
3 }
コピー

これらは、ブリッジが知る必要がある 2 つのパラメータです。

1
2 /**************
3 * Depositing *
4 **************/
5
6 /** @dev Modifier requiring sender to be EOA. This check could be bypassed by a malicious
7 * contract via initcode, but it takes care of the user error we want to avoid.
8 */
9 modifier onlyEOA() {
10 // Used to stop deposits from contracts (avoid accidentally lost tokens)
11 require(!Address.isContract(msg.sender), "Account not EOA");
12 _;
13 }
すべて表示
コピー

これは、OpenZeppelin のAddressユーティリティが必要となる理由です。

1 /**
2 * @dev This function can be called with no data
3 * to deposit an amount of ETH to the caller's balance on L2.
4 * Since the receive function doesn't take data, a conservative
5 * default amount is forwarded to L2.
6 */
7 receive() external payable onlyEOA {
8 _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));
9 }
すべて表示
コピー

この関数は、テスト用のものです。 通常の使用を目的とするものではないため、インターフェイスの定義には含まれない点に注意してください。

1 /**
2 * @inheritdoc IL1StandardBridge
3 */
4 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {
5 _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);
6 }
7
8 /**
9 * @inheritdoc IL1StandardBridge
10 */
11 function depositETHTo(
12 address _to,
13 uint32 _l2Gas,
14 bytes calldata _data
15 ) external payable {
16 _initiateETHDeposit(msg.sender, _to, _l2Gas, _data);
17 }
すべて表示
コピー

これら 2 つの関数は、実際の ETH 入金を処理する関数である_initiateETHDepositに関連したラッパーです。

1 /**
2 * @dev Performs the logic for deposits by storing the ETH and informing the L2 ETH Gateway of
3 * the deposit.
4 * @param _from Account to pull the deposit from on L1.
5 * @param _to Account to give the deposit to on L2.
6 * @param _l2Gas Gas limit required to complete the deposit on L2.
7 * @param _data Optional data to forward to L2. This data is provided
8 * solely as a convenience for external contracts. Aside from enforcing a maximum
9 * length, these contracts provide no guarantees about its content.
10 */
11 function _initiateETHDeposit(
12 address _from,
13 address _to,
14 uint32 _l2Gas,
15 bytes memory _data
16 ) internal {
17 // Construct calldata for finalizeDeposit call
18 bytes memory message = abi.encodeWithSelector(
すべて表示
コピー

クロスドメインのメッセージは、メッセージをコールデータとして宛先のコントラクトを呼び出す仕組みとして機能します。 Solidity のコントラクトは常に、コールデータをABI 仕様(opens in a new tab)に従って解釈します。 Solidity のabi.encodeWithSelector(opens in a new tab)は、このコールデータを作成するものです。

1 IL2ERC20Bridge.finalizeDeposit.selector,
2 address(0),
3 Lib_PredeployAddresses.OVM_ETH,
4 _from,
5 _to,
6 msg.value,
7 _data
8 );
コピー

ここでのメッセージは、 finalizeDeposit関数(opens in a new tab)を以下のパラメータで呼び出すことです。

パラメータ説明
_l1Tokenaddress(0)L1 上の ETH (ERC-20 トークンではない) を表す特別な値
_l2TokenLib_PredeployAddresses.OVM_ETHOptimism 上で ETH を管理する L2 のコントラクト0xDeadDeAddeAddEAddeadDEaDDeaDDeAD0000 (このコントラクトは、Optimism 内部でのみ使用されます)
_from_fromL1 上の ETH 送信元アドレス
_to_toL2 上の ETH 受領用アドレス
amountmsg.value送信された wei 額(すでにブリッジに送信済みの額)
_data_data入金に添付する追加の日付
1 // Send calldata into L2
2 // slither-disable-next-line reentrancy-events
3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);
コピー

クロスドメイン・メッセンジャーを通じて、メッセージを送信します。

1 // slither-disable-next-line reentrancy-events
2 emit ETHDepositInitiated(_from, _to, msg.value, _data);
3 }
コピー

この送信をリッスンするすべての分散型アプリケーションに対し、通知イベントを発行します。

1 /**
2 * @inheritdoc IL1ERC20Bridge
3 */
4 function depositERC20(
5 .
6 .
7 .
8 ) external virtual onlyEOA {
9 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _l2Gas, _data);
10 }
11
12 /**
13 * @inheritdoc IL1ERC20Bridge
14 */
15 function depositERC20To(
16 .
17 .
18 .
19 ) external virtual {
20 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _l2Gas, _data);
21 }
すべて表示
コピー

これら 2 つの関数は、実際の ERC-20 トークンの入金を処理する関数である_initiateERC20Depositに関連したラッパーです。

1 /**
2 * @dev Performs the logic for deposits by informing the L2 Deposited Token
3 * contract of the deposit and calling a handler to lock the L1 funds. (e.g. transferFrom)
4 *
5 * @param _l1Token Address of the L1 ERC20 we are depositing
6 * @param _l2Token Address of the L1 respective L2 ERC20
7 * @param _from Account to pull the deposit from on L1
8 * @param _to Account to give the deposit to on L2
9 * @param _amount Amount of the ERC20 to deposit.
10 * @param _l2Gas Gas limit required to complete the deposit on L2.
11 * @param _data Optional data to forward to L2. This data is provided
12 * solely as a convenience for external contracts. Aside from enforcing a maximum
13 * length, these contracts provide no guarantees about its content.
14 */
15 function _initiateERC20Deposit(
16 address _l1Token,
17 address _l2Token,
18 address _from,
19 address _to,
20 uint256 _amount,
21 uint32 _l2Gas,
22 bytes calldata _data
23 ) internal {
すべて表示
コピー

この関数は、上記の_initiateETHDepositに似ていますが、いくつかの重要な点が異なります。 最大の違いは、この関数では、トークンのアドレスおよび送信額をパラメータとして受け取るという点です。 ETH の場合、ブリッジへの呼び出しにはすでに、ブリッジアカウントへの資産の移転(msg.value)が含まれています。

1 // When a deposit is initiated on L1, the L1 Bridge transfers the funds to itself for future
2 // withdrawals. safeTransferFrom also checks if the contract has code, so this will fail if
3 // _from is an EOA or address(0).
4 // slither-disable-next-line reentrancy-events, reentrancy-benign
5 IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);
コピー

ERC-20 トークンについては、ETH の場合とは異なる送信プロセスが実行されます:

  1. ユーザー(_from)は、ブリッジに対し、送信したいトークン量に見合ったアローワンスを与えます。
  2. 次に、トークンコントラクトのアドレスや金額等で、ブリッジを呼び出します。
  3. ブリッジは、入金プロセスの一環として、トークンをブリッジ自体に送信します。

最初のステップは、第 2、3 のステップとは別のトランザクションで実行しても構いません。 ただし、_initiateERC20Depositを呼び出す 2 つの関数 (depositERC20depositERC20To)は、_fromをパラメータとしてmsg.senderを含むこの関数を呼び出すだけなので、フロントランニングの問題は発生しません。

1 // Construct calldata for _l2Token.finalizeDeposit(_to, _amount)
2 bytes memory message = abi.encodeWithSelector(
3 IL2ERC20Bridge.finalizeDeposit.selector,
4 _l1Token,
5 _l2Token,
6 _from,
7 _to,
8 _amount,
9 _data
10 );
11
12 // Send calldata into L2
13 // slither-disable-next-line reentrancy-events, reentrancy-benign
14 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);
15
16 // slither-disable-next-line reentrancy-benign
17 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount;
すべて表示
コピー

depositsのデータ構造に、トークンの入金額を追加します。 L2 上には、L1 上にある同一の ERC-20 トークンに対応した複数のアドレスが存在する場合があるため、入金の推移を追跡するには、L1 上の ERC-20 トークンに関するブリッジ上の残高を参照するだけでは不十分です。

1
2 // slither-disable-next-line reentrancy-events
3 emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data);
4 }
5
6 /*************************
7 * Cross-chain Functions *
8 *************************/
9
10 /**
11 * @inheritdoc IL1StandardBridge
12 */
13 function finalizeETHWithdrawal(
14 address _from,
15 address _to,
16 uint256 _amount,
17 bytes calldata _data
すべて表示
コピー

L2 のブリッジは、L2 のクロスドメイン・メッセンジャーに対して、L1 のクロスドメイン・メッセンジャーがこの関数を呼び出すことを指示するメッセージを送信します(もちろん、L1 上で、このメッセージを確定するトランザクション(opens in a new tab)が送信された後においてです)。

1 ) external onlyFromCrossDomainAccount(l2TokenBridge) {
コピー

このメッセージにつき、L2 のトークン・ブリッジで作成され、クロスドメイン・メッセンジャーを経由して送信された正当なメッセージであることを確認してください。 この関数は、ブリッジから ETH を出金するために使用するため、承認された呼び出し元だけが呼び出したことを確認する必要があります。

1 // slither-disable-next-line reentrancy-events
2 (bool success, ) = _to.call{ value: _amount }(new bytes(0));
コピー

ETH を送信するには、msg.valueで wei 金額を指定して、受領者を呼び出します。

1 require(success, "TransferHelper::safeTransferETH: ETH transfer failed");
2
3 // slither-disable-next-line reentrancy-events
4 emit ETHWithdrawalFinalized(_from, _to, _amount, _data);
コピー

出金イベントを発行します。

1 }
2
3 /**
4 * @inheritdoc IL1ERC20Bridge
5 */
6 function finalizeERC20Withdrawal(
7 address _l1Token,
8 address _l2Token,
9 address _from,
10 address _to,
11 uint256 _amount,
12 bytes calldata _data
13 ) external onlyFromCrossDomainAccount(l2TokenBridge) {
すべて表示
コピー

この関数は、上記の finalizeETHWithdrawal関数に類似していますが、ERC-20 トークンに関する事項が異なります。

1 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] - _amount;
コピー

depositsのデータ構造を更新します。

1
2 // When a withdrawal is finalized on L1, the L1 Bridge transfers the funds to the withdrawer
3 // slither-disable-next-line reentrancy-events
4 IERC20(_l1Token).safeTransfer(_to, _amount);
5
6 // slither-disable-next-line reentrancy-events
7 emit ERC20WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);
8 }
9
10
11 /*****************************
12 * Temporary - Migrating ETH *
13 *****************************/
14
15 /**
16 * @dev Adds ETH balance to the account. This is meant to allow for ETH
17 * to be migrated from an old gateway to a new gateway.
18 * NOTE: This is left for one upgrade only so we are able to receive the migrated ETH from the
19 * old contract
20 */
21 function donateETH() external payable {}
22}
すべて表示
コピー

このブリッジは、従来の実装から変更されています。 以前の実装から現在の実装に移行した際に、すべての資産を移転させる必要がありました。 ERC-20 トークンについては、移転のみが可能でした。 一方、ETH をコントラクトに移転するには、そのコントラクトの承認が必要であり、これはdonateETHで実行できます。

L2 上の ERC-20 トークン

ERC-20 トークンを標準ブリッジに適合させるためには、標準ブリッジのみがトークンをミントできるようにする必要があります。 これが必要になるのは、各ブリッジにおいて、Optimism 上で流通するトークンの数が L1 のブリッジコントラクト内でロックされたトークンの数と一致することを保証する必要があるためです。 L2 上のトークンが多すぎる場合、L2 上のアセット L1 にブリッジして戻すことができないユーザーが発生します。 この場合、信頼できるブリッジが存在せず、事実上、部分準備銀行制度(opens in a new tab)を生み出すことになってしまいます。 一方、L1 上でのトークンが多くなりすぎると、L2 トークンをバーンしない限りトークンをリリースできなくなるため、ブリッジコントラクト内の一部のトークンは永遠にロックされた状態になってしまいます。

IL2StandardERC20

標準ブリッジを使用する L2 上のすべての ERC-20 トークンは、標準ブリッジが必要とする関数およびイベントが搭載されたこのインターフェイス(opens in a new tab)を提供する必要があります。

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;
3
4import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
コピー

ERC-20 に対する標準インターフェイス(opens in a new tab)には、mintおよびburn関数が含まれません。 これらのメソッドは、ERC-20 標準(opens in a new tab)において必須でないため、トークンを作成、破壊するメカニズムは指定されていないのです。

1import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
コピー

ERC-165 インターフェイス(opens in a new tab)は、コントラクトが提供する機能を特定するために用います。 この標準については、こちらで読むことができます(opens in a new tab)

1interface IL2StandardERC20 is IERC20, IERC165 {
2 function l1Token() external returns (address);
コピー

この関数は、このコントラクトに対してブリッジされた L1 上のトークンアドレスを提供するものです。 同じ機能の逆方向の関数が存在しない点に注意してください。 L1 上のトークンについては、L2 のサポートが計画されていたか、あるいはいつ実装されたかを問わず、常にブリッジ可能である必要があります。

1
2 function mint(address _to, uint256 _amount) external;
3
4 function burn(address _from, uint256 _amount) external;
5
6 event Mint(address indexed _account, uint256 _amount);
7 event Burn(address indexed _account, uint256 _amount);
8}
コピー

トークンをミント (作成) およびバーン (破棄) するための関数とイベントです。 トークン数が適切である(L1 上でロックされたトークン数と一致する)ことを保証するため、これらの関数を実行できるのはこのブリッジのみである必要があります。

L2StandardERC20

これは(opens in a new tab)IL2StandardERC20インターフェイスの実装です。 カスタムロジックが必要ない場合は、常にこの関数を用いてください。

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;
3
4import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
コピー

OpenZeppelin で作成した ERC-20 コントラクト(opens in a new tab)です。 Optimism では、既存の機能がよく監査され、資産を保持する上で十分信頼できる場合は、新たな機能を追加すべきではないと考えています。

1import "./IL2StandardERC20.sol";
2
3contract L2StandardERC20 is IL2StandardERC20, ERC20 {
4 address public l1Token;
5 address public l2Bridge;
コピー

これらは、通常 ERC-20 では必要になりませんが、ここでは追加で必要となる 2 つの設定パラメータです。

1
2 /**
3 * @param _l2Bridge Address of the L2 standard bridge.
4 * @param _l1Token Address of the corresponding L1 token.
5 * @param _name ERC20 name.
6 * @param _symbol ERC20 symbol.
7 */
8 constructor(
9 address _l2Bridge,
10 address _l1Token,
11 string memory _name,
12 string memory _symbol
13 ) ERC20(_name, _symbol) {
14 l1Token = _l1Token;
15 l2Bridge = _l2Bridge;
16 }
すべて表示
コピー

まず、ERC20(_name, _symbol)で継承したコントラクトのコンストラクタを呼び出し、新たに変数を設定します。

1
2 modifier onlyL2Bridge() {
3 require(msg.sender == l2Bridge, "Only L2 Bridge can mint and burn");
4 _;
5 }
6
7
8 // slither-disable-next-line external-function
9 function supportsInterface(bytes4 _interfaceId) public pure returns (bool) {
10 bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC165
11 bytes4 secondSupportedInterface = IL2StandardERC20.l1Token.selector ^
12 IL2StandardERC20.mint.selector ^
13 IL2StandardERC20.burn.selector;
14 return _interfaceId == firstSupportedInterface || _interfaceId == secondSupportedInterface;
15 }
すべて表示
コピー

ERC-165(opens in a new tab)は、このように動作します。 各インターフェイスは、サポートする一連の関数を含んでおり、これらの機能のexclusive(opens in a new tab)またはABI 関数セレクタ(opens in a new tab)として特定されます。

L2 のブリッジは、ERC-165 を健全性チェックとして用いて、資産を送信するのに用いる ERC-20 コントラクトが IL2StandardERC20であるか確認します。

**注意:不正なコントラクトがsupportsInterfaceに対して虚偽の値を返すことを防ぐメカニズムは含まれていないため、これはセキュリティ保護のメカニズムではなく、健全性チェックのメカニズムです。

1 // slither-disable-next-line external-function
2 function mint(address _to, uint256 _amount) public virtual onlyL2Bridge {
3 _mint(_to, _amount);
4
5 emit Mint(_to, _amount);
6 }
7
8 // slither-disable-next-line external-function
9 function burn(address _from, uint256 _amount) public virtual onlyL2Bridge {
10 _burn(_from, _amount);
11
12 emit Burn(_from, _amount);
13 }
14}
すべて表示
コピー

資産をミント/バーンできるのは、L2 のブリッジのみです。

_mintおよび_burnは、実際には、OpenZeppelin で作成した ERC-20 コントラクトで定義されています。 トークンをミント/バーンできる条件は、ERC-20 を使用する方法と同じように多種多様であるため、このコントラクトは単純に外部に露出していないのです。

L2 ブリッジのコード

これは、Optimism でブリッジを実行するコードです。 このコントラクトのソースコードは、こちら(opens in a new tab)から入手できます。

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;
3
4/* Interface Imports */
5import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";
6import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";
7import { IL2ERC20Bridge } from "./IL2ERC20Bridge.sol";
コピー

IL2ERC20Bridge(opens in a new tab)のインターフェースは、上述したL1 用のインターフェイスとほぼ同様ですが、 以下の 2 点が大きく異なります。

  1. L1 では、あなたが入金を開始し、出金を確定します。 L2 の場合、あなたは出金を開始し、入金を確定します。
  2. L1 では、ETH と ERC-20 トークンを区別する必要があります。 L2 では、Optimism 上の ETH 残高は内部で、アドレス「0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000(opens in a new tab)」の ERC-20 トークンとして処理されるため、両方で同じ関数を使います。
1/* Library Imports */
2import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
3import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";
4import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";
5
6/* Contract Imports */
7import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";
8
9/**
10 * @title L2StandardBridge
11 * @dev The L2 Standard bridge is a contract which works together with the L1 Standard bridge to
12 * enable ETH and ERC20 transitions between L1 and L2.
13 * This contract acts as a minter for new tokens when it hears about deposits into the L1 Standard
14 * bridge.
15 * This contract also acts as a burner of the tokens intended for withdrawal, informing the L1
16 * bridge to release L1 funds.
17 */
18contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {
19 /********************************
20 * External Contract References *
21 ********************************/
22
23 address public l1TokenBridge;
すべて表示
コピー

これは、L1 ブリッジのアドレスを追跡する関数です。 L1 用のブリッジの場合とは異なり、この変数は必須です。 L1 ブリッジのアドレスは、事前に知ることはできません。

1
2 /***************
3 * Constructor *
4 ***************/
5
6 /**
7 * @param _l2CrossDomainMessenger Cross-domain messenger used by this contract.
8 * @param _l1TokenBridge Address of the L1 bridge deployed to the main chain.
9 */
10 constructor(address _l2CrossDomainMessenger, address _l1TokenBridge)
11 CrossDomainEnabled(_l2CrossDomainMessenger)
12 {
13 l1TokenBridge = _l1TokenBridge;
14 }
15
16 /***************
17 * Withdrawing *
18 ***************/
19
20 /**
21 * @inheritdoc IL2ERC20Bridge
22 */
23 function withdraw(
24 address _l2Token,
25 uint256 _amount,
26 uint32 _l1Gas,
27 bytes calldata _data
28 ) external virtual {
29 _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);
30 }
31
32 /**
33 * @inheritdoc IL2ERC20Bridge
34 */
35 function withdrawTo(
36 address _l2Token,
37 address _to,
38 uint256 _amount,
39 uint32 _l1Gas,
40 bytes calldata _data
41 ) external virtual {
42 _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);
43 }
すべて表示
コピー

これら 2 つの関数は、出金を開始するものです。 L1 上のトークンアドレスを指定する必要がない点に注意してください。 L1 上で対応するトークンのアドレスは、L2 トークンが伝達するからです。

1
2 /**
3 * @dev Performs the logic for withdrawals by burning the token and informing
4 * the L1 token Gateway of the withdrawal.
5 * @param _l2Token Address of L2 token where withdrawal is initiated.
6 * @param _from Account to pull the withdrawal from on L2.
7 * @param _to Account to give the withdrawal to on L1.
8 * @param _amount Amount of the token to withdraw.
9 * @param _l1Gas Unused, but included for potential forward compatibility considerations.
10 * @param _data Optional data to forward to L1. This data is provided
11 * solely as a convenience for external contracts. Aside from enforcing a maximum
12 * length, these contracts provide no guarantees about its content.
13 */
14 function _initiateWithdrawal(
15 address _l2Token,
16 address _from,
17 address _to,
18 uint256 _amount,
19 uint32 _l1Gas,
20 bytes calldata _data
21 ) internal {
22 // When a withdrawal is initiated, we burn the withdrawer's funds to prevent subsequent L2
23 // usage
24 // slither-disable-next-line reentrancy-events
25 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);
すべて表示
コピー

_fromパラメータに依存するのではなくmsg.senderを使用する点に注意してください。これにより、偽造がより困難になります(私の知る限り、不可能です)。

1
2 // Construct calldata for l1TokenBridge.finalizeERC20Withdrawal(_to, _amount)
3 // slither-disable-next-line reentrancy-events
4 address l1Token = IL2StandardERC20(_l2Token).l1Token();
5 bytes memory message;
6
7 if (_l2Token == Lib_PredeployAddresses.OVM_ETH) {
コピー

L1 では、ETH と ERC-20 トークンを区別する必要があります。

1 message = abi.encodeWithSelector(
2 IL1StandardBridge.finalizeETHWithdrawal.selector,
3 _from,
4 _to,
5 _amount,
6 _data
7 );
8 } else {
9 message = abi.encodeWithSelector(
10 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,
11 l1Token,
12 _l2Token,
13 _from,
14 _to,
15 _amount,
16 _data
17 );
18 }
19
20 // Send message up to L1 bridge
21 // slither-disable-next-line reentrancy-events
22 sendCrossDomainMessage(l1TokenBridge, _l1Gas, message);
23
24 // slither-disable-next-line reentrancy-events
25 emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data);
26 }
27
28 /************************************
29 * Cross-chain Function: Depositing *
30 ************************************/
31
32 /**
33 * @inheritdoc IL2ERC20Bridge
34 */
35 function finalizeDeposit(
36 address _l1Token,
37 address _l2Token,
38 address _from,
39 address _to,
40 uint256 _amount,
41 bytes calldata _data
すべて表示
コピー

この関数は、L1StandardBridgeが呼び出します。

1 ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {
コピー

メッセージの発信元が適切であることを確認します。 この関数は、_mintを呼び出すものであり、L1 上でブリッジが所有するトークンに含まれないトークンを提供するために使用できるため、重要です。

1 // Check the target token is compliant and
2 // verify the deposited token on L1 matches the L2 deposited token representation here
3 if (
4 // slither-disable-next-line reentrancy-events
5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
6 _l1Token == IL2StandardERC20(_l2Token).l1Token()
コピー

以下の健全性チェックを実行してください:

  1. 正しいインターフェースがサポートされていること。
  2. L2 上の ERC-20 コントラクトにおける L1 アドレスが、L1 上のトークンソースと一致すること。
1 ) {
2 // When a deposit is finalized, we credit the account on L2 with the same amount of
3 // tokens.
4 // slither-disable-next-line reentrancy-events
5 IL2StandardERC20(_l2Token).mint(_to, _amount);
6 // slither-disable-next-line reentrancy-events
7 emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);
コピー

健全性チェックに合格したら、入金を確定します。

  1. トークンをミントします。
  2. 適切なイベントを発行します。
1 } else {
2 // Either the L2 token which is being deposited-into disagrees about the correct address
3 // of its L1 token, or does not support the correct interface.
4 // This should only happen if there is a malicious L2 token, or if a user somehow
5 // specified the wrong L2 token address to deposit into.
6 // In either case, we stop the process here and construct a withdrawal
7 // message so that users can get their funds out in some cases.
8 // There is no way to prevent malicious token contracts altogether, but this does limit
9 // user error and mitigate some forms of malicious contract behavior.
すべて表示
コピー

L2 のトークンアドレスが間違っており、検知可能なエラーが発生した場合、入金をキャンセルし、トークンを L1 に返却する必要があります。 これを L2 から実行する唯一の方法は、不正申し立て期間が経過してからメッセージを送信することですが、それでも、トークンを永遠に失うよりは、ユーザーにとってははるかにベターな選択でしょう。

1 bytes memory message = abi.encodeWithSelector(
2 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,
3 _l1Token,
4 _l2Token,
5 _to, // switched the _to and _from here to bounce back the deposit to the sender
6 _from,
7 _amount,
8 _data
9 );
10
11 // Send message up to L1 bridge
12 // slither-disable-next-line reentrancy-events
13 sendCrossDomainMessage(l1TokenBridge, 0, message);
14 // slither-disable-next-line reentrancy-events
15 emit DepositFailed(_l1Token, _l2Token, _from, _to, _amount, _data);
16 }
17 }
18}
すべて表示
コピー

まとめ

標準ブリッジは、アセットを移転する上で最も柔軟なメカニズムです。 しかし、汎用性が高いため、必ずしも使いやすいメカニズムではない場合もあります。 特に出金については、大部分のユーザーは、異議申し立て期間が存在せず、出金を最終確認するために Merkle プルーフを必要としないサードパーティーのブリッジ(opens in a new tab)を使いたいと考えるでしょう。

サードパーティのブリッジは通常、少額の手数料(標準ブリッジの出金にかかるガス代よりも安価な場合が多いです)で、L1 上でアセットを保持する機能を提供します。 ブリッジ(または、ブリッジを稼働するスタッフ)が L1 上での資産不足を予想する場合、L2 から必要な資産が移転されます。 これらは非常に大規模な出金になるため、出金コストは高額を対象として償却され、手数料が全体に占める割合が非常に低くなります。

この記事が、レイヤー 2 の仕組みと、明確で安全な Solidity コードの書き方について、より理解を深めることに役立つことを願っています。

最終編集者: @HiroyukiNaito(opens in a new tab), 2023年12月8日

このチュートリアルは役に立ちましたか?