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

オプティミズムの標準ブリッジコントラクトの解説

Solidity
ブリッジ
レイヤー2
中級
オリ・ポメランツ
2022年3月30日
54 分で読めます

オプティミズム (opens in a new tab)オプティミスティック・ロールアップです。 オプティミスティック・ロールアップは、ネットワーク上のすべてのノードではなく、少数のノードのみがトランザクションを処理するため、イーサリアム・メインネット(レイヤー1 (L1) とも呼ばれます)よりもはるかに低価格でトランザクションを処理できます。 同時に、データはすべてL1に書き込まれるため、メインネットの完全性と可用性の保証をすべて備えた状態で、すべてを証明および再構築できます。

オプティミズム(またはその他のL2)でL1の資産を使用するには、資産をブリッジする必要があります。 これを実現する1つの方法は、ユーザーがL1で資産(ETHとERC-20トークンが最も一般的です)をロックし、L2で使用するための同等の資産を受け取ることです。 最終的に、それらを手にした人は、それらをL1にブリッジして戻したいと思うかもしれません。 これを行う際、資産はL2でバーンされ、その後L1でユーザーに返還されます。

これがオプティミズムの標準ブリッジ (opens in a new tab)の仕組みです。 この記事では、そのブリッジのソースコードを見て仕組みを確認し、よく書かれたSolidityコードの例として学習します。

制御フロー

ブリッジには2つの主なフローがあります。

  • 入金(L1からL2へ)
  • 引き出し(L2からL1へ)

入金フロー

レイヤー1

  1. ERC-20を入金する場合、入金者はブリッジに入金される金額を消費するためのアローワンスを与えます。
  2. 入金者はL1ブリッジを呼び出します(depositERC20depositERC20TodepositETH、またはdepositETHTo)。
  3. L1ブリッジはブリッジされた資産を所有します。
    • ETH: 資産は呼び出しの一部として入金者によって送金されます。
    • ERC-20: 資産は、入金者によって提供されたアローワンスを使用して、ブリッジ自身によって送金されます。
  4. L1ブリッジはクロスドメインメッセージメカニズムを使用して、L2ブリッジのfinalizeDepositを呼び出します。

レイヤー2

  1. L2ブリッジは、finalizeDepositへの呼び出しが正当であることを検証します。
    • クロスドメインメッセージコントラクトから来たものであること。
    • 元々はL1のブリッジからのものであること。
  2. L2ブリッジは、L2のERC-20トークンコントラクトが正しいものであるかを確認します。
    • L2コントラクトは、そのL1の対応物が、L1でトークンが送られてきたものと同じであると報告します。
    • L2コントラクトは、正しいインターフェースをサポートしていると報告します(ERC-165を使用 (opens in a new tab))。
  3. L2コントラクトが正しいものである場合、それを呼び出して適切な数のトークンを適切なアドレスにミントします。そうでない場合は、ユーザーがL1でトークンを請求できるように引き出しプロセスを開始します。

引き出しフロー

レイヤー2

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

レイヤー1

  1. L1ブリッジは、finalizeETHWithdrawalまたはfinalizeERC20Withdrawalへの呼び出しが正当であることを検証します。
    • クロスドメインメッセージメカニズムから来たものであること。
    • 元々はL2のブリッジからのものであること。
  2. L1ブリッジは、適切な資産(ETHまたはERC-20)を適切なアドレスに送金します。

レイヤー1のコード

これはL1、つまりイーサリアム・メインネットで実行されるコードです。

IL1ERC20Bridge

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

// SPDX-License-Identifier: MIT

オプティミズムのコードの大部分はMITライセンスの下でリリースされています (opens in a new tab)

pragma solidity >0.5.0 <0.9.0;

執筆時点でのSolidityの最新バージョンは0.8.12です。 バージョン0.9.0がリリースされるまで、このコードがそれと互換性があるかどうかはわかりません。

オプティミズムのブリッジの用語では、_入金(deposit)_はL1からL2への送金を意味し、_引き出し(withdrawal)_はL2からL1への送金を意味します。

        address indexed _l1Token,
        address indexed _l2Token,

ほとんどの場合、L1上のERC-20のアドレスは、L2上の同等のERC-20のアドレスと同じではありません。 トークンアドレスのリストはここで確認できます (opens in a new tab)chainIdが1のアドレスはL1(メインネット)上にあり、chainIdが10のアドレスはL2(オプティミズム)上にあります。 他の2つのchainIdの値は、Kovanテストネットワーク(42)とOptimistic Kovanテストネットワーク(69)のためのものです。

        address indexed _from,
        address _to,
        uint256 _amount,
        bytes _data
    );

送金にメモを追加することが可能であり、その場合、それらは送金を報告するイベントに追加されます。

    event ERC20WithdrawalFinalized(
        address indexed _l1Token,
        address indexed _l2Token,
        address indexed _from,
        address _to,
        uint256 _amount,
        bytes _data
    );

同じブリッジコントラクトが双方向の送金を処理します。 L1ブリッジの場合、これは入金の初期化と引き出しの完了を意味します。

L2では事前にデプロイされたコントラクトであり、常にアドレス0x4200000000000000000000000000000000000010にあるため、この関数は実際には必要ありません。 L1ブリッジのアドレスを知ることは簡単では_ない_ため、L2ブリッジとの対称性のためにここにあります。

_l2Gasパラメータは、トランザクションが消費を許可されているL2ガスの量です。 特定の(高い)制限までは無料 (opens in a new tab)であるため、ERC-20コントラクトがミント時に本当に奇妙なことをしない限り、問題にはならないはずです。 この関数は、ユーザーが異なるブロックチェーン上の同じアドレスに資産をブリッジするという一般的なシナリオを処理します。

この関数はdepositERC20とほぼ同じですが、ERC-20を別のアドレスに送信することができます。

オプティミズムでの引き出し(およびL2からL1へのその他のメッセージ)は、2段階のプロセスです。

  1. L2での開始トランザクション。
  2. L1での完了または請求トランザクション。 このトランザクションは、L2トランザクションのフォールトチャレンジ期間 (opens in a new tab)が終了した後に発生する必要があります。

IL1StandardBridge

このインターフェースはここで定義されています (opens in a new tab)。 このファイルには、ETHのイベントと関数の定義が含まれています。 これらの定義は、上記のERC-20用に定義されたIL1ERC20Bridgeと非常によく似ています。

一部のERC-20トークンはカスタム処理を必要とし、標準ブリッジでは処理できないため、ブリッジインターフェースは2つのファイルに分割されています。 これにより、そのようなトークンを処理するカスタムブリッジはIL1ERC20Bridgeを実装でき、ETHもブリッジする必要がなくなります。

このイベントは、L1およびL2のトークンアドレスがないことを除いて、ERC-20バージョン(ERC20DepositInitiated)とほぼ同じです。 他のイベントや関数についても同様です。

CrossDomainEnabled

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

// SPDX-License-Identifier: MIT
pragma solidity >0.5.0 <0.9.0;

/* インターフェースのインポート */
import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";

このインターフェース (opens in a new tab)は、クロスドメインメッセンジャーを使用して他のレイヤーにメッセージを送信する方法をコントラクトに伝えます。 このクロスドメインメッセンジャーはまったく別のシステムであり、それ自体で記事にする価値があるため、将来書きたいと思っています。

コントラクトが知る必要がある1つのパラメータは、このレイヤー上のクロスドメインメッセンジャーのアドレスです。 このパラメータはコンストラクタで一度だけ設定され、変更されることはありません。

クロスドメインメッセージングは、それが実行されているブロックチェーン(イーサリアム・メインネットまたはオプティミズムのいずれか)上の任意のコントラクトからアクセスできます。 しかし、各側のブリッジは、反対側のブリッジから来た特定のメッセージ_のみ_を信頼する必要があります。

        require(
            msg.sender == address(getCrossDomainMessenger()),
            "OVM_XCHAIN: messenger contract unauthenticated"
        );

適切なクロスドメインメッセンジャー(以下に示すようにmessenger)からのメッセージのみを信頼できます。


        require(
            getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount,
            "OVM_XCHAIN: wrong sender of cross-domain message"
        );

クロスドメインメッセンジャーが他のレイヤーにメッセージを送信したアドレスを提供する方法は、.xDomainMessageSender()関数 (opens in a new tab)です。 メッセージによって開始されたトランザクションで呼び出される限り、この情報を提供できます。

受信したメッセージが他のブリッジから来たものであることを確認する必要があります。

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

最後に、他のレイヤーにメッセージを送信する関数です。

    ) internal {
        // slither-disable-next-line reentrancy-events, reentrancy-benign

スリザー (opens in a new tab)は、オプティミズムがすべてのコントラクトで実行し、脆弱性やその他の潜在的な問題を探す静的アナライザーです。 この場合、次の行が2つの脆弱性を引き起こします。

  1. リエントランシーイベント (opens in a new tab)
  2. 良性リエントランシー (opens in a new tab)
        getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
    }
}

この場合、スリザーがそれを知る方法がなくても、getCrossDomainMessenger()が信頼できるアドレスを返すことがわかっているため、リエントランシーについて心配する必要はありません。

L1ブリッジコントラクト

このコントラクトのソースコードはここにあります (opens in a new tab)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

インターフェースは他のコントラクトの一部になる可能性があるため、幅広いSolidityバージョンをサポートする必要があります。 しかし、ブリッジ自体は私たちのコントラクトであり、どのSolidityバージョンを使用するかについて厳密にすることができます。

/* インターフェースのインポート */
import { IL1StandardBridge } from "./IL1StandardBridge.sol";
import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol";

IL1ERC20BridgeIL1StandardBridgeについては上記で説明しました。

import { IL2ERC20Bridge } from "../../L2/messaging/IL2ERC20Bridge.sol";

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

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

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

/* ライブラリのインポート */
import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";

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

import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";

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

import { Address } from "@openzeppelin/contracts/utils/Address.sol";

オープンツェッペリンのAddressユーティリティ (opens in a new tab)。これは、コントラクトアドレスと外部所有アカウント(EOA)に属するアドレスを区別するために使用されます。

直接の呼び出しとコントラクトのコンストラクタからの呼び出しを区別する方法がないため、これは完璧な解決策ではありませんが、少なくともこれにより、一般的なユーザーエラーを特定して防ぐことができます。

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

ERC-20標準 (opens in a new tab)は、コントラクトが失敗を報告するための2つの方法をサポートしています。

  1. リバート
  2. falseを返す

両方のケースを処理するとコードが複雑になるため、代わりにオープンツェッペリンのSafeERC20 (opens in a new tab)を使用します。これにより、すべての失敗がリバートになる (opens in a new tab)ことが保証されます。

この行は、IERC20インターフェースを使用するたびにSafeERC20ラッパーを使用するように指定する方法です。


    /********************************
     * 外部コントラクトの参照 *
     ********************************/

    address public l2TokenBridge;

L2StandardBridgeのアドレス。


    // レイヤー1 (L1)トークンをレイヤー2 (L2)トークンにマッピングし、入金されたレイヤー1 (L1)トークンの残高にマッピングします
    mapping(address => mapping(address => uint256)) public deposits;

このような二重のマッピング (opens in a new tab)は、2次元の疎配列 (opens in a new tab)を定義する方法です。 このデータ構造内の値はdeposit[L1 token addr][L2 token addr]として識別されます。 デフォルト値はゼロです。 異なる値に設定されたセルのみがストレージに書き込まれます。


    /***************
     * コンストラクタ *
     ***************/

    // このコントラクトはプロキシの背後に存在するため、コンストラクタのパラメータは使用されません。
    constructor() CrossDomainEnabled(address(0)) {}

ストレージ内のすべての変数をコピーすることなく、このコントラクトをアップグレードできるようにしたいと考えています。 そのために、Proxy (opens in a new tab)を使用します。これは、delegatecall (opens in a new tab)を使用して、プロキシ・コントラクトによってアドレスが保存されている別のコントラクトに呼び出しを転送するコントラクトです(アップグレード時に、そのアドレスを変更するようにプロキシに指示します)。 delegatecallを使用すると、ストレージは_呼び出し元_のコントラクトのストレージのままになるため、すべてのコントラクトの状態変数の値は影響を受けません。

このパターンの1つの効果は、delegatecallの_呼び出し先_であるコントラクトのストレージが使用されないため、それに渡されるコンストラクタの値が重要ではないことです。 これが、CrossDomainEnabledコンストラクタに無意味な値を提供できる理由です。 また、以下の初期化がコンストラクタから分離されている理由でもあります。

このスリザーテスト (opens in a new tab)は、コントラクトコードから呼び出されないため、publicの代わりにexternalとして宣言できる関数を特定します。 external関数のガスコストは低くなる可能性があります。なぜなら、コールデータでパラメータを提供できるからです。 publicとして宣言された関数は、コントラクト内からアクセス可能でなければなりません。 コントラクトは自身のコールデータを変更できないため、パラメータはメモリ内にある必要があります。 そのような関数が外部から呼び出される場合、コールデータをメモリにコピーする必要があり、これにはガスがかかります。 この場合、関数は一度しか呼び出されないため、非効率性は問題になりません。

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

initialize関数は一度だけ呼び出されるべきです。 L1クロスドメインメッセンジャーまたはL2トークンブリッジのいずれかのアドレスが変更された場合、新しいプロキシとそれを呼び出す新しいブリッジを作成します。 これは、システム全体がアップグレードされる場合を除いて起こりそうになく、非常にまれな出来事です。

この関数には、_誰が_それを呼び出すことができるかを制限するメカニズムがないことに注意してください。 これは、理論的には、攻撃者がプロキシとブリッジの最初のバージョンをデプロイするまで待ち、その後フロントランニング (opens in a new tab)を行って、正当なユーザーよりも先にinitialize関数に到達できることを意味します。しかし、これを防ぐための2つの方法があります。

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

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

これが、オープンツェッペリンのAddressユーティリティが必要だった理由です。

この関数はテスト目的で存在します。 インターフェース定義には表示されていないことに注意してください。これは通常の使用を目的としていません。

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

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

            IL2ERC20Bridge.finalizeDeposit.selector,
            address(0),
            Lib_PredeployAddresses.OVM_ETH,
            _from,
            _to,
            msg.value,
            _data
        );

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

パラメータ意味
_l1Tokenaddress(0)L1上のETH(ERC-20トークンではない)を表す特別な値
_l2TokenLib_PredeployAddresses.OVM_ETHオプティミズム上でETHを管理するL2コントラクト、0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000(このコントラクトはオプティミズムの内部使用専用です)
_from_fromETHを送信するL1上のアドレス
_to_toETHを受信するL2上のアドレス
amountmsg.value送信されたWeiの量(すでにブリッジに送信されています)
_data_data入金に添付する追加データ
        // レイヤー2 (L2)にコールデータを送信します
        // slither-disable-next-line reentrancy-events
        sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

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

        // slither-disable-next-line reentrancy-events
        emit ETHDepositInitiated(_from, _to, msg.value, _data);
    }

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

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

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

        // レイヤー1 (L1)で入金が開始されると、レイヤー1 (L1)ブリッジは将来の
        // 引き出しのために資金を自身に送金します。safeTransferFromはコントラクトにコードがあるかどうかもチェックするため、
        // _fromがEOAまたはアドレス(0)の場合は失敗します。
        // slither-disable-next-line reentrancy-events, reentrancy-benign
        IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);

ERC-20トークンの送金は、ETHとは異なるプロセスに従います。

  1. ユーザー(_from)は、適切なトークンを送金するためのアローワンスをブリッジに与えます。
  2. ユーザーは、トークンコントラクトのアドレス、量などを指定してブリッジを呼び出します。
  3. ブリッジは、入金プロセスの一部としてトークンを(自身に)送金します。

最初のステップは、最後の2つとは別のトランザクションで発生する場合があります。 ただし、_initiateERC20Depositを呼び出す2つの関数(depositERC20depositERC20To)は、_fromパラメータとしてmsg.senderを使用してのみこの関数を呼び出すため、フロントランニングは問題になりません。

入金されたトークンの量をdepositsデータ構造に追加します。 同じL1 ERC-20トークンに対応するL2上のアドレスが複数存在する可能性があるため、入金を追跡するためにL1 ERC-20トークンのブリッジの残高を使用するだけでは不十分です。

L2ブリッジはL2クロスドメインメッセンジャーにメッセージを送信し、それによりL1クロスドメインメッセンジャーがこの関数を呼び出します(もちろん、メッセージを完了するトランザクション (opens in a new tab)がL1で送信された後です)。

    ) external onlyFromCrossDomainAccount(l2TokenBridge) {

これがクロスドメインメッセンジャーから来ており、L2トークンブリッジを送信元とする_正当な_メッセージであることを確認します。 この関数はブリッジからETHを引き出すために使用されるため、承認された呼び出し元によってのみ呼び出されることを確認する必要があります。

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

ETHを送金する方法は、msg.valueにWeiの量を指定して受信者を呼び出すことです。

        require(success, "TransferHelper::safeTransferETH: ETH transfer failed");

        // slither-disable-next-line reentrancy-events
        emit ETHWithdrawalFinalized(_from, _to, _amount, _data);

引き出しに関するイベントを発行します。

この関数は上記のfinalizeETHWithdrawalに似ていますが、ERC-20トークンに必要な変更が加えられています。

        deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] - _amount;

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

ブリッジの以前の実装がありました。 その実装からこの実装に移行したとき、すべての資産を移動する必要がありました。 ERC-20トークンは単に移動させることができます。 しかし、コントラクトにETHを送金するには、そのコントラクトの承認が必要であり、それがdonateETHが提供するものです。

L2上のERC-20トークン

ERC-20トークンが標準ブリッジに適合するためには、標準ブリッジに、そして標準ブリッジに_のみ_、トークンをミントすることを許可する必要があります。 これは、オプティミズム上で流通しているトークンの数が、L1ブリッジコントラクト内にロックされているトークンの数と等しいことをブリッジが保証する必要があるため必要です。 L2にトークンが多すぎる場合、一部のユーザーは資産をL1にブリッジして戻すことができなくなります。 信頼できるブリッジの代わりに、本質的に部分準備銀行制度 (opens in a new tab)を再現することになります。 L1にトークンが多すぎる場合、L2トークンをバーンせずにそれらを解放する方法がないため、それらのトークンの一部は永遠にブリッジコントラクト内にロックされたままになります。

IL2StandardERC20

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

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

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

import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";

ERC-165インターフェース (opens in a new tab)は、コントラクトが提供する関数を指定するために使用されます。 標準はこちらで読むことができます (opens in a new tab)

interface IL2StandardERC20 is IERC20, IERC165 {
    function l1Token() external returns (address);

この関数は、このコントラクトにブリッジされているL1トークンのアドレスを提供します。 逆方向の同様の関数はないことに注意してください。 実装時にL2サポートが計画されていたかどうかに関係なく、任意のL1トークンをブリッジできるようにする必要があります。


    function mint(address _to, uint256 _amount) external;

    function burn(address _from, uint256 _amount) external;

    event Mint(address indexed _account, uint256 _amount);
    event Burn(address indexed _account, uint256 _amount);
}

トークンをミント(作成)およびバーン(破棄)するための関数とイベント。 トークンの数が正しい(L1でロックされているトークンの数と等しい)ことを保証するために、ブリッジがこれらの関数を実行できる唯一のエンティティであるべきです。

L2StandardERC20

これはIL2StandardERC20インターフェースの私たちの実装です (opens in a new tab)。 何らかのカスタムロジックが必要ない限り、これを使用する必要があります。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

オープンツェッペリンのERC-20コントラクト (opens in a new tab)。 オプティミズムは車輪の再発明を信じていません。特に、その車輪が十分に監査されており、資産を保持するのに十分な信頼性が必要な場合はなおさらです。

import "./IL2StandardERC20.sol";

contract L2StandardERC20 is IL2StandardERC20, ERC20 {
    address public l1Token;
    address public l2Bridge;

これらは、私たちが必要とし、ERC-20が通常は必要としない2つの追加の構成パラメータです。

最初に継承元のコントラクト(ERC20(_name, _symbol))のコンストラクタを呼び出し、次に独自の変数を設定します。

これがERC-165 (opens in a new tab)の仕組みです。 すべてのインターフェースはサポートされている関数の数であり、それらの関数のABI関数セレクタ (opens in a new tab)排他的論理和 (opens in a new tab)として識別されます。

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

注: 悪意のあるコントラクトがsupportsInterfaceに誤った回答を提供するのを防ぐものは何もないため、これは健全性チェックのメカニズムであり、セキュリティメカニズムでは_ありません_。

L2ブリッジのみが資産をミントおよびバーンすることを許可されています。

_mint_burnは、実際にはオープンツェッペリンのERC-20コントラクトで定義されています。 トークンをミントおよびバーンする条件は、ERC-20の使用方法の数と同じくらい多様であるため、そのコントラクトはそれらを外部に公開していないだけです。

L2ブリッジコード

これはオプティミズム上でブリッジを実行するコードです。 このコントラクトのソースはここにあります (opens in a new tab)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

/* インターフェースのインポート */
import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";
import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";
import { IL2ERC20Bridge } from "./IL2ERC20Bridge.sol";

IL2ERC20Bridge (opens in a new tab)インターフェースは、上記で見たL1の同等物と非常によく似ています。 2つの重要な違いがあります。

  1. L1では、入金を開始し、引き出しを完了します。 ここでは、引き出しを開始し、入金を完了します。
  2. L1では、ETHとERC-20トークンを区別する必要があります。 L2では、オプティミズム上のETH残高は内部的にアドレス0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (opens in a new tab)のERC-20トークンとして処理されるため、両方に同じ関数を使用できます。

L1ブリッジのアドレスを追跡します。 L1の同等物とは対照的に、ここではこの変数が_必要_であることに注意してください。 L1ブリッジのアドレスは事前にはわかりません。

これら2つの関数は引き出しを開始します。 L1トークンアドレスを指定する必要はないことに注意してください。 L2トークンは、L1の同等物のアドレスを教えてくれることが期待されています。

_fromパラメータに依存しているの_ではなく_、偽造がはるかに困難な(私の知る限り不可能な)msg.senderに依存していることに注意してください。


        // l1TokenBridge.finalizeERC20Withdrawal(_to, _amount)のためのコールデータを構築します
        // slither-disable-next-line reentrancy-events
        address l1Token = IL2StandardERC20(_l2Token).l1Token();
        bytes memory message;

        if (_l2Token == Lib_PredeployAddresses.OVM_ETH) {

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

この関数はL1StandardBridgeによって呼び出されます。

    ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {

メッセージの送信元が正当であることを確認します。 この関数は_mintを呼び出し、ブリッジがL1で所有するトークンでカバーされていないトークンを与えるために使用される可能性があるため、これは重要です。

        // ターゲットトークンが準拠しているかチェックし、
        // レイヤー1 (L1)で入金されたトークンが、ここでのレイヤー2 (L2)の入金されたトークンの表現と一致するか検証します
        if (
            // slither-disable-next-line reentrancy-events
            ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
            _l1Token == IL2StandardERC20(_l2Token).l1Token()

健全性チェック:

  1. 正しいインターフェースがサポートされていること
  2. L2 ERC-20コントラクトのL1アドレスが、トークンのL1送信元と一致すること
        ) {
            // 入金がファイナライズされると、レイヤー2 (L2)のアカウントに同額の
            // トークンをクレジットします。
            // slither-disable-next-line reentrancy-events
            IL2StandardERC20(_l2Token).mint(_to, _amount);
            // slither-disable-next-line reentrancy-events
            emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);

健全性チェックに合格した場合、入金を完了します。

  1. トークンをミントする
  2. 適切なイベントを発行する

ユーザーが間違ったL2トークンアドレスを使用するという検出可能なエラーを犯した場合、入金をキャンセルしてL1でトークンを返還したいと考えます。 L2からこれを行う唯一の方法は、フォールトチャレンジ期間を待たなければならないメッセージを送信することですが、これはユーザーがトークンを永久に失うよりもはるかに優れています。

結論

標準ブリッジは、資産の送金において最も柔軟なメカニズムです。 しかし、非常に汎用的であるため、常に最も使いやすいメカニズムであるとは限りません。 特に引き出しの場合、ほとんどのユーザーは、チャレンジ期間を待たず、引き出しを完了するためにマークル証明を必要としないサードパーティのブリッジ (opens in a new tab)を使用することを好みます。

これらのブリッジは通常、L1に資産を持ち、少額の手数料(多くの場合、標準ブリッジの引き出しのガスコストよりも安い)ですぐに提供することで機能します。 ブリッジ(またはそれを運営する人々)がL1の資産が不足すると予想した場合、L2から十分な資産を送金します。これらは非常に大きな引き出しであるため、引き出しコストは大量の資産に分散され、はるかに小さな割合になります。

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

私の他の作品についてはこちらをご覧ください (opens in a new tab)