跳至主要內容

Optimism 標準跨鏈橋合約演練

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

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

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

這就是 Optimism 標準跨鏈橋 (opens in a new tab)的運作方式。 在本文中,我們將檢視該跨鏈橋的原始碼以了解其運作方式,並將其作為編寫良好的 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

此介面定義於此 (opens in a new tab)。 它包含橋接 ERC-20 代幣所需的函式和定義。

// SPDX-License-Identifier: MIT

Optimism 的大部分程式碼都是在 MIT 授權條款下發布的 (opens in a new tab)

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 地址不同。 您可以在此處查看代幣地址列表 (opens in a new tab)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 燃料數量。 在達到某個(較高的)限制之前,這是免費的 (opens in a new tab),因此除非 ERC-20 合約在鑄造時執行了非常奇怪的操作,否則這不應該是個問題。 此函式處理常見的場景,即使用者將資產橋接到不同區塊鏈上的相同地址。

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

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

  1. 在 L2 上發起交易。
  2. 在 L1 上完成或申領交易。 此交易需要在 L2 交易的錯誤挑戰期 (opens in a new tab)結束後進行。

IL1StandardBridge

此介面定義於此 (opens in a new tab)。 此檔案包含 ETH 的事件和函式定義。 這些定義與上面 IL1ERC20Bridge 中為 ERC-20 定義的內容非常相似。

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

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

CrossDomainEnabled

此合約 (opens in a new tab)由兩個跨鏈橋(L1L2)繼承,用於向另一層發送訊息。

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

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

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

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

跨域訊息傳遞可由其執行所在區塊鏈(以太坊主網或 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() 函式 (opens in a new tab)。 只要它在由訊息發起的交易中被呼叫,它就可以提供此資訊。

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

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

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

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

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

  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)支援合約報告失敗的兩種方式:

  1. 回滾
  2. 回傳 false

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

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


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

    address public l2TokenBridge;

L2StandardBridge 的地址。


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

像這樣的雙重對應 (mapping) (opens in a new tab)是定義二維稀疏陣列 (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 時,儲存空間仍然是_呼叫_合約的儲存空間,因此所有合約狀態變數的值都不受影響。

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

斯立瑟測試 (opens in a new tab)識別出未從合約程式碼中呼叫的函式,因此可以將其宣告為 external 而不是 publicexternal 函式的燃料成本可能較低,因為可以在呼叫資料中為它們提供參數。 宣告為 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 函式。但有兩種方法可以防止這種情況:

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

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

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

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

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

跨域訊息的運作方式是使用訊息作為其呼叫資料來呼叫目標合約。 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在 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 上提交了完成訊息的交易 (opens in a new tab)之後)。

    ) 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。 我們將不再擁有一個可信的跨鏈橋,而是在本質上重建了部分準備金銀行制度 (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)不包含 mintburn 函式。 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 代幣地址。 請注意,我們沒有相反方向的類似函式。 我們需要能夠橋接任何 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 介面的實作 (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)。 Optimism 不主張重新發明輪子,特別是當這個輪子經過了良好的稽核,並且需要足夠可信以持有資產時。

import "./IL2StandardERC20.sol";

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

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

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

這就是 ERC-165 (opens in a new tab) 的運作方式。 每個介面都是一些受支援的函式,並被標識為這些函式的 ABI 函式選擇器 (opens in a new tab)互斥或 (XOR) (opens in a new tab)

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

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

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

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

L2 跨鏈橋程式碼

這是執行在 Optimism 上的跨鏈橋程式碼。 此合約的原始碼在此 (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 等效介面非常相似。 有兩個顯著的區別:

  1. 在 L1 上,您發起存款並完成提款。 在這裡,您發起提款並完成存款。
  2. 在 L1 上,必須區分 ETH 和 ERC-20 代幣。 在 L2 上,我們可以對兩者使用相同的函式,因為在內部,Optimism 上的 ETH 餘額被作為地址為 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (opens in a new tab) 的 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 執行此操作的唯一方法是發送一條必須等待錯誤挑戰期的訊息,但這對使用者來說比永久丟失代幣要好得多。

結論

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

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

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

在此處查看我的更多作品 (opens in a new tab)