跳至主要內容

Optimism 標準跨鏈橋合約演練

Solidity
跨鏈橋
第二層 (L2)
中階
奧里·波梅蘭茨
2022年3月30日
47 分鐘閱讀

Optimism (在新分頁開啟) 是一個樂觀 Rollup。 樂觀 Rollup 處理交易的價格比以太坊主網(也稱為第一層 (L1))低得多,因為交易僅由少數節點處理,而不是網路上的每個節點。 同時,所有資料都會寫入 L1,因此可以利用主網的所有完整性和可用性保證來證明和重建一切。

要在 Optimism(或任何其他 L2)上使用 L1 資產,這些資產需要被橋接。 實現此目的的一種方法是讓使用者在 L1 上鎖定資產(ETH 和 ERC-20 代幣是最常見的),並接收等值的資產以在 L2 上使用。 最終,無論誰獲得了這些資產,都可能希望將它們橋接回 L1。 執行此操作時,資產會在 L2 上被銷毀,然後在 L1 上釋放回給使用者。

這就是 Optimism 標準跨鏈橋 (在新分頁開啟)的運作方式。 在本文中,我們將檢視該跨鏈橋的原始碼以了解其運作方式,並將其作為編寫良好的 Solidity 程式碼範例進行研究。

控制流程

跨鏈橋有兩個主要流程:

  • 存款(從 L1 到 L2)
  • 提款(從 L2 到 L1)

存款流程

第一層 (L1)

  1. 如果存入 ERC-20,存款人會給予跨鏈橋花費所存入金額的授權額度
  2. 存款人呼叫 L1 跨鏈橋(depositERC20depositERC20TodepositETHdepositETHTo
  3. L1 跨鏈橋取得橋接資產的所有權
    • ETH:資產由存款人作為呼叫的一部分進行轉帳
    • ERC-20:跨鏈橋使用存款人提供的授權額度將資產轉帳給自己
  4. L1 跨鏈橋使用跨域訊息機制呼叫 L2 跨鏈橋上的 finalizeDeposit

第二層 (L2)

  1. L2 跨鏈橋驗證對 finalizeDeposit 的呼叫是否合法:
    • 來自跨域訊息合約
    • 最初來自 L1 上的跨鏈橋
  2. L2 跨鏈橋檢查 L2 上的 ERC-20 代幣合約是否正確:
  3. 如果 L2 合約正確,則呼叫它以向適當的地址鑄造適當數量的代幣。如果不正確,則啟動提款流程,允許使用者在 L1 上申領代幣。

提款流程

第二層 (L2)

  1. 提款人呼叫 L2 跨鏈橋(withdrawwithdrawTo
  2. L2 跨鏈橋銷毀屬於 msg.sender 的適當數量的代幣
  3. L2 跨鏈橋使用跨域訊息機制呼叫 L1 跨鏈橋上的 finalizeETHWithdrawalfinalizeERC20Withdrawal

第一層 (L1)

  1. L1 跨鏈橋驗證對 finalizeETHWithdrawalfinalizeERC20Withdrawal 的呼叫是否合法:
    • 來自跨域訊息機制
    • 最初來自 L2 上的跨鏈橋
  2. L1 跨鏈橋將適當的資產(ETH 或 ERC-20)轉帳到適當的地址

第一層 (L1) 程式碼

這是執行在 L1(以太坊主網)上的程式碼。

IL1ERC20Bridge

此介面定義於此 (在新分頁開啟)。 它包含橋接 ERC-20 代幣所需的函式和定義。

// SPDX-License-Identifier: MIT

Optimism 的大部分程式碼都是在 MIT 授權條款下發布的 (在新分頁開啟)

pragma solidity >0.5.0 <0.9.0;

在撰寫本文時,Solidity 的最新版本是 0.8.12。 在 0.9.0 版本發布之前,我們不知道這段程式碼是否與其相容。

在 Optimism 跨鏈橋術語中,存款 (deposit) 表示從 L1 轉帳到 L2,而 提款 (withdrawal) 表示從 L2 轉帳到 L1。

        address indexed _l1Token,
        address indexed _l2Token,

在大多數情況下,L1 上的 ERC-20 地址與 L2 上等效的 ERC-20 地址不同。 您可以在此處查看代幣地址列表 (在新分頁開啟)chainId 為 1 的地址位於 L1(主網)上,而 chainId 為 10 的地址位於 L2(Optimism)上。 另外兩個 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。 它在這裡是為了與 L2 跨鏈橋保持對稱,因為 L1 跨鏈橋的地址_並非_顯而易見。

_l2Gas 參數是交易允許花費的 L2 燃料數量。 在達到某個(較高的)限制之前,這是免費的 (在新分頁開啟),因此除非 ERC-20 合約在鑄造時執行了非常奇怪的操作,否則這不應該是個問題。 此函式處理常見的場景,即使用者將資產橋接到不同區塊鏈上的相同地址。

此函式與 depositERC20 幾乎相同,但它允許您將 ERC-20 傳送到不同的地址。

Optimism 中的提款(以及從 L2 到 L1 的其他訊息)是一個兩步流程:

  1. 在 L2 上發起交易。
  2. 在 L1 上完成或申領交易。 此交易需要在 L2 交易的錯誤挑戰期 (在新分頁開啟)結束後進行。

IL1StandardBridge

此介面定義於此 (在新分頁開啟)。 此檔案包含 ETH 的事件和函式定義。 這些定義與上面 IL1ERC20Bridge 中為 ERC-20 定義的內容非常相似。

跨鏈橋介面被劃分在兩個檔案中,因為某些 ERC-20 代幣需要自訂處理,無法由標準跨鏈橋處理。 這樣一來,處理此類代幣的自訂跨鏈橋就可以實作 IL1ERC20Bridge,而無需同時橋接 ETH。

此事件與 ERC-20 版本(ERC20DepositInitiated)幾乎相同,只是沒有 L1 和 L2 代幣地址。 其他事件和函式也是如此。

CrossDomainEnabled

此合約 (在新分頁開啟)由兩個跨鏈橋(L1L2)繼承,用於向另一層發送訊息。

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

/* 介面匯入 */
import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";

此介面 (在新分頁開啟)告訴合約如何使用跨域通訊器向另一層發送訊息。 這個跨域通訊器是另一個完整的系統,值得專門寫一篇文章,我希望將來能寫。

合約需要知道的一個參數是該層上跨域通訊器的地址。 此參數在建構函式中設定一次,並且永遠不會改變。

跨域訊息傳遞可由其執行所在區塊鏈(以太坊主網或 Optimism)上的任何合約存取。 但我們需要兩側的跨鏈橋_僅_信任來自另一側跨鏈橋的特定訊息。

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

只有來自適當跨域通訊器(messenger,如下所示)的訊息才是可信的。


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

跨域通訊器提供向另一層發送訊息的地址的方式是透過 .xDomainMessageSender() 函式 (在新分頁開啟)。 只要它在由訊息發起的交易中被呼叫,它就可以提供此資訊。

我們需要確保收到的訊息來自另一個跨鏈橋。

此函式回傳跨域通訊器。 我們使用函式而不是變數 messenger,以允許繼承自此合約的合約使用演算法來指定要使用的跨域通訊器。

最後,向另一層發送訊息的函式。

    ) internal {
        // 斯立瑟-disable-next-line 重入-事件, 重入-benign

斯立瑟 (在新分頁開啟) 是 Optimism 在每個合約上執行的靜態分析器,用於尋找漏洞和其他潛在問題。 在這種情況下,以下程式碼行會觸發兩個漏洞:

  1. 重入事件 (在新分頁開啟)
  2. 良性重入 (在新分頁開啟)
        getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
    }
}

在這種情況下,我們不擔心重入,因為我們知道 getCrossDomainMessenger() 會回傳一個可信的地址,即使斯立瑟無法知道這一點。

L1 跨鏈橋合約

此合約的原始碼在此 (在新分頁開啟)

// 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";

此介面 (在新分頁開啟)讓我們建立訊息來控制 L2 上的標準跨鏈橋。

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

此介面 (在新分頁開啟)讓我們控制 ERC-20 合約。 您可以在此處閱讀更多相關資訊

/* 函式庫匯入 */
import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";

如上所述,此合約用於層間訊息傳遞。

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

Lib_PredeployAddresses (在新分頁開啟) 包含始終具有相同地址的 L2 合約地址。這包括 L2 上的標準跨鏈橋。

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

歐本齊柏林的 Address 實用程式 (在新分頁開啟)。它用於區分合約地址和屬於外部擁有帳戶 (EOA) 的地址。

請注意,這不是一個完美的解決方案,因為無法區分直接呼叫和從合約建構函式發出的呼叫,但至少這讓我們能夠識別並防止一些常見的使用者錯誤。

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

ERC-20 標準 (在新分頁開啟)支援合約報告失敗的兩種方式:

  1. 回滾
  2. 回傳 false

處理這兩種情況會使我們的程式碼變得更加複雜,因此我們改用 歐本齊柏林的 SafeERC20 (在新分頁開啟),它確保所有失敗都會導致回滾 (在新分頁開啟)

這行程式碼是我們指定每次使用 IERC20 介面時都要使用 SafeERC20 包裝器的方式。


    /********************************
     * 外部合約參考 *
     ********************************/

    address public l2TokenBridge;

L2StandardBridge 的地址。


    // 將第一層 (L1) 代幣映射到第二層 (L2) 代幣,再映射到已存入的第一層 (L1) 代幣餘額
    mapping(address => mapping(address => uint256)) public deposits;

像這樣的雙重對應 (mapping) (在新分頁開啟)是定義二維稀疏陣列 (在新分頁開啟)的方式。 此資料結構中的值被標識為 deposit[L1 token addr][L2 token addr]。 預設值為零。 只有設定為不同值的儲存格才會寫入儲存空間。


    /***************
     * 建構函式 *
     ***************/

    // 此合約位於代理之後,因此建構函式參數將不會被使用。
    constructor() CrossDomainEnabled(address(0)) {}

為了能夠升級此合約而無需複製儲存空間中的所有變數。 為此,我們使用 Proxy (在新分頁開啟),這是一個使用 delegatecall (在新分頁開啟) 將呼叫轉移到另一個獨立合約的代理合約,該獨立合約的地址由代理合約儲存(當您升級時,您會告訴代理合約更改該地址)。 當您使用 delegatecall 時,儲存空間仍然是_呼叫_合約的儲存空間,因此所有合約狀態變數的值都不受影響。

這種模式的一個影響是,作為 delegatecall 被呼叫方的合約的儲存空間不會被使用,因此傳遞給它的建構函式值並不重要。 這就是我們可以向 CrossDomainEnabled 建構函式提供無意義值的原因。 這也是下面的初始化與建構函式分開的原因。

斯立瑟測試 (在新分頁開啟)識別出未從合約程式碼中呼叫的函式,因此可以將其宣告為 external 而不是 publicexternal 函式的燃料成本可能較低,因為可以在呼叫資料中為它們提供參數。 宣告為 public 的函式必須可以從合約內部存取。 合約無法修改自己的呼叫資料,因此參數必須在記憶體中。 當從外部呼叫此類函式時,必須將呼叫資料複製到記憶體中,這會消耗燃料。 在這種情況下,該函式只被呼叫一次,因此效率低下對我們來說並不重要。

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

initialize 函式應該只被呼叫一次。 如果 L1 跨域通訊器或 L2 代幣跨鏈橋的地址發生變化,我們會建立一個新的代理合約和一個呼叫它的新跨鏈橋。 除非整個系統升級,否則這不太可能發生,這是一種非常罕見的情況。

請注意,此函式沒有任何機制來限制_誰_可以呼叫它。 這意味著在理論上,攻擊者可以等到我們部署代理合約和第一個版本的跨鏈橋,然後搶跑 (在新分頁開啟),在合法使用者之前到達 initialize 函式。但有兩種方法可以防止這種情況:

  1. 如果合約不是由 EOA 直接部署,而是在由另一個合約建立它們的交易中部署 (在新分頁開啟),則整個過程可以是原子性的,並在執行任何其他交易之前完成。
  2. 如果對 initialize 的合法呼叫失敗,始終可以忽略新建立的代理合約和跨鏈橋,並建立新的。
        messenger = _l1messenger;
        l2TokenBridge = _l2TokenBridge;
    }

這是跨鏈橋需要知道的兩個參數。

這就是我們需要歐本齊柏林的 Address 實用程式的原因。

此函式為測試目的而存在。 請注意,它沒有出現在介面定義中——它不是供正常使用的。

這兩個函式是 _initiateETHDeposit 的包裝器,該函式處理實際的 ETH 存款。

跨域訊息的運作方式是使用訊息作為其呼叫資料來呼叫目標合約。 Solidity 合約始終根據 ABI 規範 (在新分頁開啟)來解釋其呼叫資料。 Solidity 函式 abi.encodeWithSelector (在新分頁開啟) 建立該呼叫資料。

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

這裡的訊息是使用以下參數呼叫 finalizeDeposit 函式 (在新分頁開啟)

參數含義
_l1Tokenaddress(0)代表 L1 上 ETH(它不是 ERC-20 代幣)的特殊值
_l2TokenLib_PredeployAddresses.OVM_ETH在 Optimism 上管理 ETH 的 L2 合約,0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000(此合約僅供 Optimism 內部使用)
_from_from在 L1 上發送 ETH 的地址
_to_to在 L2 上接收 ETH 的地址
amountmsg.value發送的 Wei 數量(已經發送到跨鏈橋)
_data_data附加到存款的額外資料
        // 將呼叫資料發送到第二層 (L2)
        // 斯立瑟-disable-next-line 重入-事件
        sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

透過跨域通訊器發送訊息。

        // 斯立瑟-disable-next-line 重入-事件
        emit ETHDepositInitiated(_from, _to, msg.value, _data);
    }

觸發一個事件,以通知任何監聽此轉帳的去中心化應用程式 (dapp)。

這兩個函式是 _initiateERC20Deposit 的包裝器,該函式處理實際的 ERC-20 存款。

此函式與上面的 _initiateETHDeposit 類似,但有一些重要的區別。 第一個區別是此函式接收代幣地址和要轉帳的金額作為參數。 在 ETH 的情況下,對跨鏈橋的呼叫已經包含了將資產轉帳到跨鏈橋帳戶(msg.value)。

        // 當在第一層 (L1) 上啟動存款時,第一層 (L1) 跨鏈橋會將資金轉帳給自己,以便未來
        // 提款。safeTransferFrom 也會檢查合約是否有程式碼,因此如果
        // _from 是 EOA 或 地址(0),這將會失敗。
        // 斯立瑟-disable-next-line 重入-事件, 重入-benign
        IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);

ERC-20 代幣轉帳遵循與 ETH 不同的流程:

  1. 使用者(_from)給予跨鏈橋轉帳適當代幣的授權額度。
  2. 使用者使用代幣合約的地址、金額等呼叫跨鏈橋。
  3. 跨鏈橋將代幣轉帳(給自己)作為存款流程的一部分。

第一步可能發生在與後兩步不同的獨立交易中。 然而,搶跑不是問題,因為呼叫 _initiateERC20Deposit 的兩個函式(depositERC20depositERC20To)僅使用 msg.sender 作為 _from 參數來呼叫此函式。

將存入的代幣數量新增到 deposits 資料結構中。 L2 上可能有多個地址對應於同一個 L1 ERC-20 代幣,因此僅使用跨鏈橋的 L1 ERC-20 代幣餘額來追蹤存款是不夠的。

L2 跨鏈橋向 L2 跨域通訊器發送訊息,這會導致 L1 跨域通訊器呼叫此函式(當然,這是在 L1 上提交了完成訊息的交易 (在新分頁開啟)之後)。

    ) external onlyFromCrossDomainAccount(l2TokenBridge) {

確保這是一條_合法_的訊息,來自跨域通訊器並源自 L2 代幣跨鏈橋。 此函式用於從跨鏈橋提取 ETH,因此我們必須確保它僅由授權的呼叫者呼叫。

        // 斯立瑟-disable-next-line 重入-事件
        (bool success, ) = _to.call{ value: _amount }(new bytes(0));

轉帳 ETH 的方式是使用 msg.value 中的 Wei 數量呼叫接收者。

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

        // 斯立瑟-disable-next-line 重入-事件
        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 代幣適應標準跨鏈橋,它需要允許標準跨鏈橋,且_僅_允許標準跨鏈橋來鑄造代幣。 這是必要的,因為跨鏈橋需要確保在 Optimism 上流通的代幣數量等於鎖定在 L1 跨鏈橋合約內的代幣數量。 如果 L2 上的代幣過多,一些使用者將無法將其資產橋接回 L1。 我們將不再擁有一個可信的跨鏈橋,而是在本質上重建了部分準備金銀行制度 (在新分頁開啟)。 如果 L1 上的代幣過多,其中一些代幣將永遠鎖定在跨鏈橋合約內,因為如果不銷毀 L2 代幣,就無法釋放它們。

IL2StandardERC20

L2 上每個使用標準跨鏈橋的 ERC-20 代幣都需要提供此介面 (在新分頁開啟),其中包含標準跨鏈橋所需的函式和事件。

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

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

標準 ERC-20 介面 (在新分頁開啟)不包含 mintburn 函式。 ERC-20 標準 (在新分頁開啟)不要求這些方法,該標準未指定建立和銷毀代幣的機制。

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

ERC-165 介面 (在新分頁開啟)用於指定合約提供哪些函式。 您可以在此處閱讀該標準 (在新分頁開啟)

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

此函式提供橋接到此合約的 L1 代幣地址。 請注意,我們沒有相反方向的類似函式。 我們需要能夠橋接任何 L1 代幣,無論在實作時是否計劃了 L2 支援。


    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 介面的實作 (在新分頁開啟)。 除非您需要某種自訂邏輯,否則您應該使用這個。

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

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

歐本齊柏林 ERC-20 合約 (在新分頁開啟)。 Optimism 不主張重新發明輪子,特別是當這個輪子經過了良好的稽核,並且需要足夠可信以持有資產時。

import "./IL2StandardERC20.sol";

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

這是我們需要的兩個額外配置參數,而 ERC-20 通常不需要。

首先呼叫我們繼承的合約(ERC20(_name, _symbol))的建構函式,然後設定我們自己的變數。

這就是 ERC-165 (在新分頁開啟) 的運作方式。 每個介面都是一些受支援的函式,並被標識為這些函式的 ABI 函式選擇器 (在新分頁開啟)互斥或 (XOR) (在新分頁開啟)

L2 跨鏈橋使用 ERC-165 作為健全性檢查,以確保它發送資產的 ERC-20 合約是一個 IL2StandardERC20

注意:沒有任何機制可以防止惡意合約對 supportsInterface 提供錯誤的答案,因此這是一個健全性檢查機制,_不是_安全機制。

只有 L2 跨鏈橋被允許鑄造和銷毀資產。

_mint_burn 實際上定義在歐本齊柏林 ERC-20 合約中。 該合約只是沒有將它們暴露在外部,因為鑄造和銷毀代幣的條件與使用 ERC-20 的方式一樣多種多樣。

L2 跨鏈橋程式碼

這是執行在 Optimism 上的跨鏈橋程式碼。 此合約的原始碼在此 (在新分頁開啟)

// 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 (在新分頁開啟) 介面與我們在上面看到的 L1 等效介面非常相似。 有兩個顯著的區別:

  1. 在 L1 上,您發起存款並完成提款。 在這裡,您發起提款並完成存款。
  2. 在 L1 上,必須區分 ETH 和 ERC-20 代幣。 在 L2 上,我們可以對兩者使用相同的函式,因為在內部,Optimism 上的 ETH 餘額被作為地址為 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (在新分頁開啟) 的 ERC-20 代幣來處理。

追蹤 L1 跨鏈橋的地址。 請注意,與 L1 等效合約相反,在這裡我們_需要_這個變數。 L1 跨鏈橋的地址無法事先得知。

這兩個函式發起提款。 請注意,不需要指定 L1 代幣地址。 L2 代幣預計會告訴我們 L1 等效代幣的地址。

請注意,我們_不_依賴 _from 參數,而是依賴 msg.sender,這要難以偽造得多(據我所知,這是不可能的)。


        // 為 l1TokenBridge.finalizeERC20Withdrawal(_to, _amount) 建構呼叫資料
        // 斯立瑟-disable-next-line 重入-事件
        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 上所擁有代幣未涵蓋的代幣。

        // 檢查目標代幣是否合規且
        // 驗證在第一層 (L1) 上存入的代幣與此處的第二層 (L2) 存款代幣表示相符
        if (
            // 斯立瑟-disable-next-line 重入-事件
            ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
            _l1Token == IL2StandardERC20(_l2Token).l1Token()

健全性檢查:

  1. 支援正確的介面
  2. L2 ERC-20 合約的 L1 地址與代幣的 L1 來源相符
        ) {
            // 當存款最終確定時,我們會在第二層 (L2) 上的帳戶記入相同數量的
            // 代幣。
            // 斯立瑟-disable-next-line 重入-事件
            IL2StandardERC20(_l2Token).mint(_to, _amount);
            // 斯立瑟-disable-next-line 重入-事件
            emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);

如果健全性檢查通過,則完成存款:

  1. 鑄造代幣
  2. 觸發適當的事件

如果使用者因使用錯誤的 L2 代幣地址而犯了可檢測的錯誤,我們希望取消存款並在 L1 上退還代幣。 我們從 L2 執行此操作的唯一方法是發送一條必須等待錯誤挑戰期的訊息,但這對使用者來說比永久丟失代幣要好得多。

結論

標準跨鏈橋是資產轉移最靈活的機制。 然而,因為它非常通用,所以它並不總是最容易使用的機制。 特別是對於提款,大多數使用者更喜歡使用第三方跨鏈橋 (在新分頁開啟),這些跨鏈橋不需要等待挑戰期,也不需要默克爾證明來完成提款。

這些跨鏈橋通常透過在 L1 上擁有資產來運作,它們會立即提供這些資產並收取少量費用(通常低於標準跨鏈橋提款的燃料成本)。 當跨鏈橋(或營運它的人)預計 L1 資產短缺時,它會從 L2 轉移足夠的資產。由於這些是非常大額的提款,提款成本會分攤到大量資產上,因此所佔比例要小得多。

希望本文能幫助您更深入地了解第二層 (L2) 的運作方式,以及如何編寫清晰且安全的 Solidity 程式碼。

在此處查看我的更多作品 (在新分頁開啟)