跳转至主要内容

乐观解决方案标准链桥合约演示

solidity链桥二层网络
中级
Ori Pomerantz
2022年3月30日
42 分钟阅读 minute read

乐观解决方案(opens in a new tab)采用乐观卷叠技术。 乐观卷叠能够以比以太坊主网(也称“第一层”)低得多的价格处理交易,因为交易只是由几个节点而非网络上的所有节点处理。 同时所有数据都已写入第一层,因此一切都能够得到证明并重建,并且具有主网的所有完整性和可用性保证。

要在乐观解决方案(或任何其他第二层)上使用第一层资产,需要桥接该资产。 实现这一点的一种方法是,用户在一层网络上锁定资产(以太币和 ERC-20 代币是最常见的资产)并收到供在二层网络上使用的对等资产。 最后,拥有这些资产的任何人可能想把它们桥接回第一层。 在桥接过程中,资产会在第二层销毁,然后在第一层上发放给用户。

这就是乐观解决方案标准链桥(opens in a new tab)的工作方式。 在本文中,我们将学习链桥的源代码,看看它如何工作,并将它作为精心编写的 Solidity 代码示例加以研究。

控制流通

链桥有两种主要流通方式:

  • 存款(从第一层到第二层)
  • 提款(从第二层到第一层)

存款流通

第一层

  1. 如果存入 ERC-20,存款人会给链桥一笔费用,这笔费用从所存入的金额中抽取
  2. 存款人调用第一层链桥(depositERC20depositERC20TodepositETHdepositETHTo
  3. 第一层链桥拥有桥接的资产
    • 以太币:资产由存款人在调用过程中转移
    • ERC-20:资产被链桥转移给链桥自身,使用的是存款人提供的费用
  4. 第一层链桥使用跨域信息机制调用第二层链桥上的 finalizeDeposit

二层网络

  1. 第二层链桥验证调用 finalizeDeposit 是否合法:
    • 来自交叉域信息合约
    • 最初来自第一层链桥
  2. 第二层链桥检查第二层上的 ERC-20 代币合约是否正确:
  3. 如果第二层合约正确,请调用它以便在适当地址铸造相应数量的代币。 如果不正确,请启动提款过程让用户可以在第一层上认领代币。

提款流程

二层网络

  1. 提款人调用第二层链桥(withdrawwithdrawTo
  2. 第二层链桥销毁属于 msg.sender 的适当数量代币
  3. 第二层链桥使用跨域信息机制调用第一层链桥上的 finalizeETHWithdrawalfinalizeERC20Withdrawal

第一层

  1. 第一层链桥验证调用 finalizeETHWithdrawalfinalizeERC20Withdrawal 是否合法:
    • 来自交叉域信息机制
    • 最初来自第二层上的链桥
  2. 第一层链桥将适当资产(以太币或 ERC-20)转账到适当地址

一层网络代码

以下代码在第一层即以太坊主网上运行。

IL1ERC20Bridge

此接口在此处定义(opens in a new tab)。 其中包括桥接 ERC-20 代币所需的函数和定义。

1// SPDX-License-Identifier: MIT
复制

大多数乐观解决方案代码都是依据 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(
显示全部
复制

在乐观解决方案链桥术语中,存款是指从第一层转账到第二层,提款是指从第二层转账到第一层。

1 address indexed _l1Token,
2 address indexed _l2Token,
复制

大多数情况下,第一层上的 ERC-20 地址与第二层上对应的 ERC-20 地址不同。 可以在此处参阅代币地址列表(opens in a new tab)。 带有 chainId 1 的地址在第一层(主网),带有 chainId 10 的地址在第二层(乐观解决方案)上。 另两个 chainId 值用于 Kovan 测试网络 (42) 和乐观 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 );
复制

同一链桥合约处理双向转账。 就第一层链桥而言,这意味着存款的初始化和提款的终局化。

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);
显示全部
复制

并不是真需要此函数,因为在第二层上它是一个预部署的合约,所以它总是位于地址 0x4200000000000000000000000000000000000010 处。 使用此函数是为了与第二层链桥对称,因为知道第一层桥的地址并非不重要。

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 参数是指允许交易花费的第二层燃料的数量。 达到某个(上限)额度前,交易是免费的(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}
显示全部
复制

乐观解决方案中的提款(以及从第二层到第一层的其他信息)是一个包含两个步骤的过程:

  1. 在第二层上的启动交易。
  2. 在第一层上完成或声明交易。 在第二层交易的缺陷质询期(opens in a new tab)结束后此交易才可以进行。

IL1StandardBridge

此接口在此处定义(opens in a new tab)。 该文件包含以太币的事件和函数定义。 这些定义与上述 IL1ERC20Bridge 中为 ERC-20 定义的定义非常相似。

链桥接口分为两个文件,因为某些 ERC-20 代币需要自定义处理,标准链桥接无法处理它们。 这样,处理此类代币的自定义链桥可以实现 IL1ERC20Bridge,而不必再桥接以太币。

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) 几乎相同,只是没有第一层和第二层代币的地址。 其他事件和函数也是如此。

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)由两个链桥(一层网络二层网络)继承,以便向其他层发送信息。

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) {
显示全部
复制

跨域消息传递可以由运行在区块链(以太坊主网或乐观解决方案)上的任何合约使用。 但是每一层都需要链桥。如果信息来自于另一边的链桥,将信任特定信息。

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 );
复制

跨域信使要提供另一层发送信息的地址,就需要用到 .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) 是一个静态分析器,乐观解决方案在每个合约上运行它以查找漏洞和其他潜在问题。 在本例中,下面一行会触发两个漏洞:

  1. 重入事件(opens in a new tab)
  2. 良性重入(opens in a new tab)
1 getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
2 }
3}
复制

在这种情况下,我们不担心重入漏洞,我们知道 getCrossDomainMessenger() 返回一个可信地址,即使 Slither 无法知道这一点。

第一层链桥合约

此合约的源代码在此处(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)让我们创建信息来控制第二层上的标准链桥。

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) 为第二层合约提供地址,这些合约始终使用相同的地址。 其中包括第二层上的标准链桥。

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)支持两种合约报告失败的方式:

  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 接口时使用 SafeERC20 包装器。

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;
复制

像这样的双重映射(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)) {}
复制

希望能够升级此合约而无需复制存储中的所有变量。 为此,我们使用 Proxy(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)可识别不是从合约代码调用且因此可以声明为 external 而不是 public 的函数。 external 函数的燃料成本可以更低,因为可以在 calldata 中为它们提供参数。 声明为 public 的函数必须可以在合约内部访问。 合约不能修改自己的 calldata,所以参数必须位于内存中。 当外部调用这类函数时,需要将 calldata 复制到内存中,这就会消耗燃料。 在本例中,函数只被调用一次,因此效率低下对我们来说无关紧要。

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

initialize 函数只应调用一次。 如果第一层跨域信使或第二层代币链桥的地址发生变化,我们将创建新代理和新链桥来调用它。 这种情况不太可能发生,除非升级整个系统,这非常罕见。

请注意,此函数没有任何机制限制可以调用它。 这意味着理论上,攻击者可以等到我们部署代理和第一版链桥后,抢在合法用户之前在前台运行(opens in a new tab)以使用 initialize 函数。 但是有两种方法可以防止这种情况:

  1. 如果合约不是由外部帐户直接部署,而是在有另一个合约创建它们的交易中(opens in a new tab)部署,那么整个过程可以成为最小操作单元,并且能够在执行任何其他交易之前完成。
  2. 如果对 initialize 的合法调用失败,总是可以忽略新创建的代理和链桥并创建新的。
1 messenger = _l1messenger;
2 l2TokenBridge = _l2TokenBridge;
3 }
复制

这些是链桥需要知道的两个参数。

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 }
显示全部
复制

这两个函数是 _initiateETHDeposit 的包装器,_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(
显示全部
复制

跨域信息的工作方式是将信息作为其 calldata 来调用目的地合约。 Solidity 合约总是解释它们的 calldata 符合 应用程序二进制接口规范(opens in a new tab)。 Solidity 函数 abi. encodeWithSelector(opens in a new tab) 可创建该 calldata。

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)在第一层上代表以太币(不是 ERC-20 代币)的特殊值
_l2TokenLib_PredeployAddresses.OVM_ETH乐观解决方案上管理以太币的第二层合约 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000(此合约仅供乐观解决方案内部使用)
_from_from第一层上发送以太币的地址
_to_to第二层上接收以太币的地址
amountmsg.value已发送的 wei 数量(已经发送到链桥的 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 }
显示全部
复制

这两个函数是 _initiateERC20Deposit 的包装器,该函数处理实际的 ERC-20 存款。

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,但有一些重要区别。 第一个区别是此函数接收代币地址和转账金额作为参数。 对于以太币,对链桥的调用已经包括将资产转账到链桥帐户 (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 代币的转账过程不同以太币:

  1. 用户 (_from) 提供费用让链桥转移适当的代币。
  2. 用户使用代币合约的地址、金额等调用链桥。
  3. 在存款过程中,链桥转移代币(给自己)。

第一步可能和最后两步发生在不同的交易中。 但是,前台运行不是问题,因为调用 _initiateERC20Deposit 的两个函数(depositERC20depositERC20To)只将 msg.sender 作为 _from 参数调用该函数。

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 数据结构中。 第二层上可能有多个地址对应于同一个第一层 ERC-20 代币,因此仅使用链桥的第一层 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
显示全部
复制

第二层链桥向第二层跨域信使发送信息,使得第一层跨域信使调用此函数(当然是在完成信息的交易(opens in a new tab)在第一层上提交以后)。

1 ) external onlyFromCrossDomainAccount(l2TokenBridge) {
复制

确保这是一个合法信息,来自跨域信使并源自第二层代币链桥。 此函数用于从链桥中提取以太币,因此我们必须确保它仅由授权调用者调用。

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

转移以太币的方式是用 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 代币可以转移。 但是,要将以太币转账到合约,你需要得到该合约的批准,donateETH 就起到这一作用。

第二层上的 ERC-20 代币

为了使 ERC-20 代币适合标准链桥,它需要允许标准链桥并且允许标准链桥铸造代币。 这是必要的,因为链桥需要确保在乐观解决方案上流通的代币数量和锁定在第一层链桥合约内的代币数量相同。 如果第二层上的代币太多,一些用户将无法将他们的资产桥接到第一层。 我们实际上将重新建立部分准备金银行制度(opens in a new tab),而不是一个受信任的链桥。 如果第一层上的代币太多,其中一些代币将永远锁定在链桥合约中,因为不销毁第二层代币就无法释放它们。

IL2StandardERC20

第二层上使用标准链桥的每个 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)不包含 mintburn 函数。 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);
复制

此函数提供桥接到此合约的第一层代币的地址。 请注意,我们在相反方向没有类似函数。 我们需要能够桥接任何第一层代币,无论第二层支持是否在实施计划。

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}
复制

铸造(创建)和燃烧(销毁)代币的函数和事件。 链桥应该是唯一可以运行这些函数的实体,以确保代币数量正确(等于锁定在第一层上的代币数量)。

L2StandardERC20

这是我们对 IL2StandardERC20 接口的实现(opens in a new tab)。 除非你需要某种自定义逻辑,否则你应该使用它。

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)。 乐观解决方案不相信重新编写合约,尤其是合约经过严格审计并且需要足够的信任来持有资产时。

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

上面是两个额外的配置参数,我们需要它们但 ERC-20 通常不需要。

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) 的工作方式。 每个接口都是许多受支持的函数,并被标识为这些函数的应用程序二进制接口函数选择器(opens in a new tab)异或(opens in a new tab)

第二层链桥使用 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}
显示全部
复制

只允许第二层链桥铸造和销毁资产。

_mint_burn 实际上是在 OpenZeppelin ERC-20 合约中定义的。 该合约只是没有将它们暴露在外部,因为铸造和销毁代币的条件与 ERC-20 使用方式的数量一样多变。

第二层链桥代码

这是在乐观解决方案上运行链桥的代码。 此合约源自此处(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) 接口与我们上面看到的第一层等效接口非常相似。 有两个明显区别:

  1. 在第一层上你发起存款并完成提款。 在此处你发起提款并完成存款。
  2. 在第一层上,有必要区分以太币和 ERC-20 代币。 在第二层上,我们可以对两者使用相同的函数,因为在乐观解决方案上的以太币余额会作为地址为 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;
显示全部
复制

跟踪第一层链桥的地址。 请注意,与第一层对应项相比,此处我们需要该变量。 第一层链桥的地址事先不为人知。

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 }
显示全部
复制

这两个函数发起提款。 请注意,无需指定第一层代币地址。 第二层代币需要告诉我们第一层代币的地址。

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) {
复制

在第一层上,有必要区分以太币和 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 并且可用于提供链桥在第一层所拥有代币范围外的代币。

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. 第二层 ERC-20 合约的第一层地址与第一层的代币来源相符
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.
显示全部
复制

如果用户由于使用错误的第二层代币地址犯了可检测到的错误,我们希望取消存款并在第一层上返还代币。 在第二层我们可以做到这一点的唯一方法是等到缺陷质询期到来后发送一条信息,但对用户来说这要比永久失去代币好得多。

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}
显示全部
复制

总结

标准链桥是最灵活的资产转移机制。 然而,由于它非常笼统,因而并非总是可供使用的最简便机制。 特别是对于提款,大多数用户喜欢使用第三方链桥(opens in a new tab),这些链桥不用等待质询期并且不需要进行默克尔证明就能完成提款。

通常,这些链桥的工作方式是在第一层上拥有资产,而且它们会立即为这些资产提供一小笔费用(通常少于标准链桥提款的燃料费用)。 当链桥(或运行链桥的人)预计第一层资产短缺时,它将从第二层转移足够的资产。 由于这些提款的数额非常庞大,大笔的提款费用经分期摊销后,所占百分比要小得多。

希望本文能帮助你更多地了解二层网络如何工作以及如何编写清晰安全的 Solidity 代码。

上次修改时间: @lukassim(opens in a new tab), 2024年4月26日

本教程对你有帮助吗?