乐观解决方案标准链桥合约演示
乐观解决方案(opens in a new tab)采用乐观卷叠技术。 乐观卷叠能够以比以太坊主网(也称“第一层”)低得多的价格处理交易,因为交易只是由几个节点而非网络上的所有节点处理。 同时所有数据都已写入第一层,因此一切都能够得到证明并重建,并且具有主网的所有完整性和可用性保证。
要在乐观解决方案(或任何其他第二层)上使用第一层资产,需要桥接该资产。 实现这一点的一种方法是,用户在第一层上锁定资产(以太币和 ERC-20 代币是最常见的资产)并收到相应资产,供在第二层上使用。 最后,拥有这些资产的任何人可能想把它们桥接回第一层。 在桥接过程中,资产会在第二层销毁,然后在第一层上发放给用户。
这就是乐观解决方案标准链桥(opens in a new tab)的工作方式。 在本文中,我们将学习链桥的源代码,看看它如何工作,并将它作为精心编写的 Solidity 代码示例加以研究。
控制流通
链桥有两种主要流通方式:
- 存款(从第一层到第二层)
- 提款(从第二层到第一层)
存款流通
第一层
- 如果存入 ERC-20,存款人会给链桥一笔费用 ,这笔费用从所存入的金额中抽取
- 存款人调用第一层链桥(
depositERC20
、depositERC20To
、depositETH
或depositETHTo
) - 第一层链桥拥有桥接的资产
- 以太币:资产由存款人在调用过程中转移
- ERC-20:资产被链桥转移给链桥自身,使用的是存款人提供的费用
- 第一层链桥使用跨域信息机制调用第二层链桥上的
finalizeDeposit
二层网络
- 第二层链桥验证调用
finalizeDeposit
是否合法:- 来自交叉域信息合约
- 最初来自第一层链桥
- 第二层链桥检查第二层上的 ERC-20 代币合约是否正确:
- 第二层合约报告,对应的第一层合约与第一层上提供代币的合约相同
- 第二层合约报告它支持正确的接口(使用 ERC-165(opens in a new tab))。
- 如果第二层合约正确,请调用它以便在适当地址铸造相应数量的代币。 如果不正确,请启动提款过程让用户可以在第一层上认领代币。
提款流程
二层网络
- 提款人调用第二层链桥(
withdraw
或withdrawTo
) - 第二层链桥销毁属于
msg.sender
的适当数量代币 - 第二层链桥使用跨域信息机制调用第一层链桥上的
finalizeETHWithdrawal
或finalizeERC20Withdrawal
第一层
- 第一层链桥验证调用
finalizeETHWithdrawal
或finalizeERC20Withdrawal
是否合法:- 来自交叉域信息机制
- 最初来自第二层上的链桥
- 第一层链桥将适当资产(以太币或 ERC-20)转账到适当地址
一层网络代码
以下代码在第一层 即以太坊主网上运行。
IL1ERC20Bridge
此接口在此处定义(opens in a new tab)。 其中包括桥接 ERC-20 代币所需的函数和定义。
1/ SPDX-许可标识符: MIT2复制
大多数乐观解决方案代码都是依据 MIT 许可证发布的(opens in a new tab)。
1实用性 >0.5.0 <0.9.0;2复制
编写代码时,Solidity 最新版本为 0.8.12。 在 0.9.0 版发布之前,我们不知道这段代码是否与它兼容。
1/**2 * @title IL1ERC20Bridge3 */4接口 IL1ERC20Bridge 然后5 /**************6 * 事件 *7 **********/89 事件 ERC20DepositInitiated(10显示全部复制
在乐观解决方案链桥术语中,存款是指从第一层转账到第二层,提款是指从第二层转账到第一层。
1 地址索引_l1Token,2 地址索引_l2Token,3复制
大多数情况下,第一层上的 ERC-20 地址与第二层上对应的 ERC-20 地址不同。 可以在此处参阅代币地址列表(opens in a new tab)。 带有 chainId
1 的地址在第一层(主网),带有 chainId
10 的地址在第二层(乐观解决方案)上。 另两个 chainId
值用于 Kovan 测试网络 (42) 和乐观 Kovan 测试网络 (69)。
1 地址索引_from2 地址到3 uint256 _amounty,4 bytes _data5;6复制
可以为转账添加注解,在这种情况下,注解将被添加到报告它们的事件中。
1 事件ERC20提款已完成(2 地址索引_l1Token,3 地址索引_l2Token,4 地址索引_from5 地址_to6 uint256 _amount,7 bytes _data8);9复制
同一链桥合约处理双向转账。 就第一层链桥而言,这意味着存款的初始化和提款的终局化。
12 ****************************3 * 公共函数 *4 **********************/56 /**7 * @dev 获得相应的L2 桥合同的地址。8 * 对应的L2桥合同的@return 地址。9 */10 函数 l2TokenBridge() 外部返回 (地址);11显示全部复制
并不是真需要此函数,因为在第二层上它是一个预部署的合约,所以它总是位于地址 0x4200000000000000000000000000000000000010
处。 使用此函数是为了与第二层链桥对称,因为知道第一层桥的地址并非不重要。
1 /**2 * @dev 将ERC20的金额存入L2上来电者的余额。3 * @param _l1Token Address of the L1 ERC20 we being sording4 * @param _l2Token Address of the L1 respective L2 ERC205 * @param _amount Amount of the ERC20 to entorize6 * @param _l2Gas Gas limit required to complete on L27 * @param _data 可选数据转发到 L2。 This data is provided8 * solely as a convenience for external contracts. Aside from enforcing a maximum9 * 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 _data17 ) external;18显示全部复制
_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 depositing4 * @param _l2Token Address of the L1 respective L2 ERC205 * @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 可选数据转发到 L2。 This data is provided9 * solely as a convenience for external contracts. Aside from enforcing a maximum10 * 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 _data19 ) external;20显示全部复制
此函数基本上与 depositERC20
相同,但它允许您将 ERC-20 发送到不同的地址。
1 /*************************2 * Cross-chain Functions *3 *************************/45 /**6 * @dev Complete a withdrawal from L2 to L1, and credit funds to the recipient's balance of the7 * 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 provided16 * solely as a convenience for external contracts. Aside from enforcing a maximum17 * 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 _data26 ) external;27}28显示全部复制
乐观解决方案中的提款(以及从第二层到第一层的其他信息)是一个包含两个步骤的过程:
- 在第二层上的启动交易。
- 在第一层上完成或声明交易。 在第二层交易的缺陷质询期(opens in a new tab)结束后此交易才可以进行。
IL1StandardBridge
此接口在此处定义(opens in a new tab)。 该文件包含以太币的事件和函数定义。 这些定义与上述 IL1ERC20Bridge
中为 ERC-20 定义的定义非常相似。
链桥接口分为两个文件,因为某些 ERC-20 代币需要自定义处理,标准链桥接无法处理它们。 这样,处理此类代币的自定义链桥可以实现 IL1ERC20Bridge
,而不必再桥接以太币。
1// SPDX-License-Identifier: MIT2pragma solidity >0.5.0 <0.9.0;34import "./IL1ERC20Bridge.sol";56/**7 * @title IL1StandardBridge8 */9interface IL1StandardBridge is IL1ERC20Bridge {10 /**********11 * Events *12 **********/13 event ETHDepositInitiated(14 address indexed _from,15 address indexed _to,16 uint256 _amount,17 bytes _data18 );19显示全部复制
此事件与 ERC-20 版本 (ERC20DepositInitiated
) 几乎相同,只是没有第一层和第二层代币的地址。 其他事件和函数也是如此。
1 event ETHWithdrawalFinalized(2 .3 .4 .5 );67 /********************8 * Public Functions *9 ********************/1011 /**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;1819 /**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 _data29 ) external payable;3031 /*************************32 * Cross-chain Functions *33 *************************/3435 /**36 * @dev Complete a withdrawal from L2 to L1, and credit funds to the recipient's balance of the37 * L1 ETH token. Since only the xDomainMessenger can call this function, it will never be called38 * before the withdrawal is finalized.39 .40 .41 .42 */43 function finalizeETHWithdrawal(44 address _from,45 address _to,46 uint256 _amount,47 bytes calldata _data48 ) external;49}50显示全部复制
CrossDomainEnabled
此合约(opens in a new tab)由两个链桥(一层网络和二层网络)继承,以便向其他层发送信息。
1// SPDX-License-Identifier: MIT2pragma solidity >0.5.0 <0.9.0;34/* Interface Imports */5import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";6复制
这个接口(opens in a new tab)告诉合约如何发送使用跨域信使向另一层发送信息。 跨域信使完全是另一种系统,值得单独写一篇文章来介绍,我希望将来能写出来。
1/**2 * @title CrossDomainEnabled3 * @dev Helper contract for contracts performing cross-domain communications4 *5 * Compiler used: defined by inheriting contract6 */7contract CrossDomainEnabled {8 /*************9 * Variables *10 *************/1112 // Messenger contract used to send and receive messages from the other domain.13 address public messenger;1415 /***************16 * Constructor *17 ***************/1819 /**20 * @param _messenger Address of the CrossDomainMessenger on the current layer.21 */22 constructor(address _messenger) {23 messenger = _messenger;24 }25显示全部复制
合约需要知道的一个参数就是跨域信使在这一层的地址。 此参数在构造函数中设置一次,并且永远不会更改。
12 /**********************3 * Function Modifiers *4 **********************/56 /**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 is9 * authenticated to call this function.10 */11 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {12显示全部复制
跨域消息传递可以由运行在区块链(以太坊主网或乐观解决方案)上的任何合约使用。 但是每一层都需要链桥。如果信息来自于另一边的链桥,将只信任特定信息。
1 require(2 msg.sender == address(getCrossDomainMessenger()),3 "OVM_XCHAIN: messenger contract unauthenticated"4 );5复制
只能信任来自适当跨域信使(messenger
,如下所示)的信息。
12 require(3 getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount,4 "OVM_XCHAIN: wrong sender of cross-domain message"5 );6复制
跨域信使要提供另一层发送信息的地址,就需要用到 .xDomainMessageSender()
函数(opens in a new tab)。 只要在信息发起的交易中调用它,它就可以提供此信息。
我们需要确保我们收到的信息来自另一个链桥。
12 _;3 }45 /**********************6 * Internal Functions *7 **********************/89 /**10 * Gets the messenger, usually from storage. This function is exposed in case a child contract11 * 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 }17显示全部复制
该函数返回跨域信使。 我们使用函数而不是变量 messenger
,以允许从该函数继承的合约使用一种算法来指定要使用的跨域信使。
12 /**3 * Sends a message to an account on another domain4 * @param _crossDomainTarget The intended recipient on the destination domain5 * @param _message The data to send to the target (usually calldata to a function with6 * `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 _message13显示全部复制
最后是向另一层发送信息的函数。
1 ) internal {2 // slither-disable-next-line reentrancy-events, reentrancy-benign3复制
Slither(opens in a new tab) 是一个静态分析器,乐观解决方案在每个合约上运行它以查找漏洞和其他潜在问题。 在本例中,下面一行会触发两个漏洞:
1 getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);2 }3}4复制
在这种情况下,我们不担心重入漏洞,我们知道 getCrossDomainMessenger()
返回一个可信地址,即使 Slither 无法知道这一点。
第一层链桥合约
此合约的源代码在此处(opens in a new tab)。
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;3复制
接口可以来自其他合约,因此它们必须支持各种 Solidity 版本。 但是链桥本身属于我们的合约,我们可以严格限制它使用的 Solidity 版本。
1/* Interface Imports */2import { IL1StandardBridge } from "./IL1StandardBridge.sol";3import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol";4复制
IL1ERC20Bridge 和 IL1StandardBridge 已在上面进行了说明。
1import { IL2ERC20Bridge } from "../../L2/messaging/IL2ERC20Bridge.sol";2复制
此接口(opens in a new tab)让我们创建信息来控制第二层上的标准链桥。
1import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";2复制
此接口(opens in a new tab)让我们控制 ERC-20 合约。 您可以在此处阅读更多信息。
1/* Library Imports */2import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";3复制
如上所述,此合约用于层间信息传递。
1import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";2复制
Lib_PredeployAddresses
(opens in a new tab) 为第二层合约提供地址,这些合约始终使用相同的地址。 其中包括第二层上的标准链桥。
1import { Address } from "@openzeppelin/contracts/utils/Address.sol";2复制
OpenZeppelin 的地址工具(opens in a new tab)。 它用于区分合约地址和属于外部账户 (EOA) 的地址。
请注意,这不是一个理想的解决方案,因为无法区分直接调用和合约构造函数的调用,但至少这让我们能够识别和防止一些常见的用户错误。
1import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";2复制
ERC-20 标准(opens in a new tab)支持两种合约报告失败的方式:
- 回滚
- 返回
false
处理这两种情况会使我们的代码更加复杂,因此我们使用 OpenZeppelin 的 SafeERC20
(opens in a new tab),确保所有失败都导致回滚(opens in a new tab)。
1/**2 * @title L1StandardBridge3 * @dev The L1 ETH and ERC20 Bridge is a contract which stores deposited L1 funds and standard4 * tokens that are in use on L2. It synchronizes a corresponding L2 Bridge, informing it of deposits5 * and listening to it for newly finalized withdrawals.6 *7 */8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {9 using SafeERC20 for IERC20;10显示全部复制
此行表示我们如何指定在每次使用 IERC20
接口时使用 SafeERC20
包装器。
12 /********************************3 * External Contract References *4 ********************************/56 address public l2TokenBridge;7复制
L2StandardBridge 的地址。
12 // Maps L1 token to L2 token to balance of the L1 token deposited3 mapping(address => mapping(address => uint256)) public deposits;4复制
像这样的双重映射(opens in a new tab)是定义二维稀疏数组(opens in a new tab)的方式。 此数据结构中的值被标识为 deposit[L1 token addr][L2 token addr]
。 默认值为零。 只有设置为不同值的单元才会写入存储。
12 /***************3 * Constructor *4 ***************/56 // This contract lives behind a proxy, so the constructor parameters will go unused.7 constructor() CrossDomainEnabled(address(0)) {}8复制
希望能够升级此合约而无需复制存储中的所有变量。 为此,我们使用 Proxy
(opens in a new tab),此合约使用 delegatecall
(opens in a new tab) 将呼叫转移到地址由代理合约存储的单独联系人(当您升级时,您告诉代理更改该地址)。 当您使用 delegatecall
时,存储仍然是调用合约的存储,因此合约所有状态变量的值不受影响。
这种模式的结果是不使用 delegatecall
调用的合约的存储,因此传递给它的构造函数值无关紧要。 这就是我们可以为 CrossDomainEnabled
构造函数提供一个无意义值的原因。 这也是下面的初始化与构造函数分开的原因。
1 /******************2 * Initialization *3 ******************/45 /**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-function10显示全部复制
此 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.");3复制
initialize
函数只应调用一次。 如果第一层跨域信使或第二层代币链桥的地址发生变化,我们将创建新代理和新链桥来调用它。 这种情 况不太可能发生,除非升级整个系统,这非常罕见。
请注意,此函数没有任何机制限制谁可以调用它。 这意味着理论上,攻击者可以等到我们部署代理和第一版链桥后,抢在合法用户之前在前台运行(opens in a new tab)以使用 initialize
函数。 但是有两种方法可以防止这种情况:
- 如果合约不是由外部账户直接部署,而是在有另一个合约创建它们的交易中(opens in a new tab)部署,那么整个过程可以成为最小操作单元,并且能够在执行任何其他交易之前完成。
- 如果对
initialize
的合法调用失败,总是可以忽略新创建的代理和链桥并创建新的。
1 messenger = _l1messenger;2 l2TokenBridge = _l2TokenBridge;3 }4复制
这些是链桥需要知道的两个参数。
12 /**************3 * Depositing *4 **************/56 /** @dev Modifier requiring sender to be EOA. This check could be bypassed by a malicious7 * 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 }14显示全部复制
这就是我们需要 OpenZeppelin 的 Address
工具的原因。
1 /**2 * @dev This function can be called with no data3 * to deposit an amount of ETH to the caller's balance on L2.4 * Since the receive function doesn't take data, a conservative5 * default amount is forwarded to L2.6 */7 receive() external payable onlyEOA {8 _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));9 }10显示全部复制
此函数存在的目的是测试。 请注意,它没有出现在接口定义中 — 它不适合正常使用。
1 /**2 * @inheritdoc IL1StandardBridge3 */4 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {5 _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);6 }78 /**9 * @inheritdoc IL1StandardBridge10 */11 function depositETHTo(12 address _to,13 uint32 _l2Gas,14 bytes calldata _data15 ) external payable {16 _initiateETHDeposit(msg.sender, _to, _l2Gas, _data);17 }18显示全部复制
这两个函数是 _initiateETHDeposit
的包装器,_initiateETHDeposit 处理实际的以太币存款。
1 /**2 * @dev Performs the logic for deposits by storing the ETH and informing the L2 ETH Gateway of3 * 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 可选数据转发到 L2。 This data is provided8 * solely as a convenience for external contracts. Aside from enforcing a maximum9 * 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 _data16 ) internal {17 // Construct calldata for finalizeDeposit call18 bytes memory message = abi.encodeWithSelector(19显示全部复制
跨域信息的工作方式是将信息作为其 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 _data8 );9