Skip to content

让合约支持跨链

如果你的合约需要使用多个链的上下文,你需要一些特殊工具来定义和运行这些跨链操作。
OpenZeppelin提供了CrossChainEnabled抽象合约,它包含了这些专用的内部方法。
在这篇教程中,我们会通过一个例子:如何建立一个由另外一个链控制的可升级,可挖矿的ERC20 token。

首先,ERC20 合约

首先,我们来建立一个简单的ERC20合约,可以通过向导来创建,它继承了我们的合约,并且具备挖矿的能力。请注意,出于演示目的,我们没有使用内置的Ownable合约。

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

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

contract MyToken is Initializable, ERC20Upgradeable, UUPSUpgradeable {
    address public owner;

    modifier onlyOwner() {
        require(owner == _msgSender(), "Not authorized");
        _;
    }

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() initializer {}

    function initialize(address initialOwner) initializer public {
        __ERC20_init("MyToken", "MTK");
        __UUPSUpgradeable_init();

        owner = initialOwner;
    }

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {
    }
}

token可以被合约的拥有者进行挖矿和升级。

准备合约的跨链操作

我们想象一下这个合约在一个区块链上运行,但是我们希望通过另一个区块链上的管理合约来控制它挖矿和升级。
例如,我们的token在xDai,而我们的管理合约在主网,或者我们的token在主网,而管理合约在optimism。
如何实现呢,我们首先要为合约添加CrossChainEnabled。你会发现这个合约现在是抽象的。因为CrossChainEnabled合约是抽象合约:它不会绑定任何一个指定的链,并且它是通过抽象的方式来实现跨链操作的。这使我们可以轻松的在不同链中重用代码。稍后我们将通过继承一个跨链的抽象实现类来实现它。

solidity
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/crosschain/CrossChainEnabled.sol";

-contract MyToken is Initializable, ERC20Upgradeable, UUPSUpgradeable {
+abstract contract MyTokenCrossChain is Initializable, ERC20Upgradeable, UUPSUpgradeable, CrossChainEnabled {

当这样修改后,我们可以使用onlyCrossChainSender修饰符,用CrossChainEnabled来保护挖矿和升级操作。

solidity
-    function mint(address to, uint256 amount) public onlyOwner {
+    function mint(address to, uint256 amount) public onlyCrossChainSender(owner) {

-    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {
+    function _authorizeUpgrade(address newImplementation) internal override onlyCrossChainSender(owner) {

这个修改可以让另一条链上的owner有效的限制挖矿和升级操作。

特定链上的实现程序

当抽象的跨链版本token完成后,我们就可以实现特定链上的合约,或者更准确地说我们想要依赖的桥梁系统。
完成这个工作需要我们使用一些CrossChainEnabled的实现类。
例如,如果我们的token在xDai上,我们的管理合约在主网,我们可以使用xDai上的AMB进行桥接,它的地址是0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59

solidity
[...]

import "@openzeppelin/contracts-upgradeable/crosschain/amb/CrossChainEnabledAMB.sol";

contract MyTokenXDAI is
    MyTokenCrossChain,
    CrossChainEnabledAMB(0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59)
{}

混合跨域地址是很危险的

在设计具有跨链支持的合约时,了解可能的回退和正在做出的安全假设至关重要。
在本篇教程中,我们特别关注对一些特定调用者的访问限制。这通常(上面的示例)是通过msg.sender_msgSender()来实现。当准备开始进行跨链时,并不是这么简单。除了考虑桥接问题,更重要的是考虑在跨链空间中,相同的地址可能会很不一样的实体。EOA钱包只有在钱包的私钥签署交易时才能执行操作。据我们所知,所有的EVM链都是如此,因此,来自此类钱包的跨链消息,就等同于同一个钱包的非跨链消息。然而,智能合约的情况却不大相同。
由于智能合约地址的计算方式,以及不同的链上智能合约都是独立存在的,你可能有两个不同的智能合约在不同的链上,但是地址却是一样的。你可以想象成两个不同链上的多重签名钱包,但是却拥有一样的地址。你也可以发现一个链上的智能钱包地址,与另一个链上的很成熟的管理合约地址一样。因此,你在给某个地址授权时要非常小心,你需要控制这个地址是哪个链发出的。

进一步的访问控制

在上述的示例中,我们使用了onlyOwner修饰符和onlyCrossChainSender(owner)机制。我们没有使用Ownable模式,因为其所有权的传递机制并不是为了跨链实体所有者所设计的。和Ownable不同,AccessControl对于跨链的智能合约来说,会更有效率的感知各种细节。
可以使用AccessControlCrossChain,其包含了AccessControl的核心功能和CrossChainEnabled抽象合约。它还包含了一些兼容跨链操作的角色管理。
mint函数中,调用者必须是MINTER_ROLE角色,并且是同一个链发出的调用。如果调用者来自于另一条链,那么调用者就不是MINTER_ROLE角色,而是另外一个'别名'版本(MINTER_ROLE ^ CROSSCHAIN_ALIAS)。这样就降低了上述示例中通过严格区分本地地址和另外一条链上的地址的危险。更多的细节参考AccessControlCrossChain

solidity
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts-upgradeable/access/AccessControlCrossChainUpgradeable.sol";

-abstract contract MyTokenCrossChain is Initializable, ERC20Upgradeable, UUPSUpgradeable, CrossChainEnabled {
+abstract contract MyTokenCrossChain is Initializable, ERC20Upgradeable, UUPSUpgradeable, AccessControlCrossChainUpgradeable {

-    address public owner;
-    modifier onlyOwner() {
-        require(owner == _msgSender(), "Not authorized");
-        _;
-    }

+    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
+    bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");

    function initialize(address initialOwner) initializer public {
        __ERC20_init("MyToken", "MTK");
        __UUPSUpgradeable_init();
+        __AccessControl_init();
+        _grantRole(_crossChainRoleAlias(DEFAULT_ADMIN_ROLE), initialOwner); // initialOwner is on a remote chain
-        owner = initialOwner;
    }

-    function mint(address to, uint256 amount) public onlyCrossChainSender(owner) {
+    function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {

-    function _authorizeUpgrade(address newImplementation) internal override onlyCrossChainSender(owner) {
+    function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) {

下面是最终的代码:

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

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlCrossChainUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

abstract contract MyTokenCrossChain is Initializable, ERC20Upgradeable, AccessControlCrossChainUpgradeable, UUPSUpgradeable {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() initializer {}

    function initialize(address initialOwner) initializer public {
        __ERC20_init("MyToken", "MTK");
        __AccessControl_init();
        __UUPSUpgradeable_init();

        _grantRole(_crossChainRoleAlias(DEFAULT_ADMIN_ROLE), initialOwner); // initialOwner is on a remote chain
    }

    function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }

    function _authorizeUpgrade(address newImplementation) internal onlyRole(UPGRADER_ROLE) override {
    }
}

import "@openzeppelin/contracts-upgradeable/crosschain/amb/CrossChainEnabledAMB.sol";

contract MyTokenXDAI is
    MyTokenCrossChain,
    CrossChainEnabledAMB(0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59)
{}

import "@openzeppelin/contracts-upgradeable/crosschain/optimismCrossChainEnabledOptimism.sol";

contract MyTokenOptimism is
    MyTokenCrossChain,
    CrossChainEnabledOptimism(0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1)
{}

Released under the MIT License.