探索Openzeppelin 新增的跨链功能

Openzeppelin 新增了 CrossChain (跨链)功能,看看如何使用它。

Openzeppelin合约首次增加了跨链支持,特别是目前支持以下链:

  • Polygon:现在最流行的侧链之一。之前这篇文章已经讨论过它。
  • Optimism: 一个基于optimistic rollup的Layer 2。之前在这篇文章讨论过这个技术。
  • Arbitrum: 另一个是基于optimistic rollup的Layer 2。
  • AMB: Arbitrary Message Bridge是一个可以用来在两个链之间转发任何数据通用工具。

Son of a bitch Meme

跨链困难是什么?

在跨链通信中到底有哪些需要考虑的事情呢?

  • 首先是发送数据问题:你可能在以太坊主网上有治理控制的合约,比如说由一个原生的ERC-20控制。而你希望合约能够有一些功能改变,比如说升级子链上的一个合约。那么就需要一种方法来允许主网的合约以安全的方式向子链发送数据。
  • sender问题: 发送跨链消息的一个困难是确定交易发起者地址,因为从合约的角度来看,msg.sender实际上是一个子链上的地址。当然 msg.sender到底是什么取决于子链,但它不会是根链的实际交易者地址。
  • 访问控制问题: 跨链的另一个问题是一个地址有可能在根链和子链都存在,所以要注意一个相同地址的合约可能存在于根链和子链上。
  • 签名的重复使用:如果你在合约中允许任何签名,它们可能会被重复使用。这就是为什么你应该经常仔细检查chainID,或者最好使用EIP-712,它可以处理所有复杂的安全签名。

    Openzeppelin跨链支持是如何运作

    Openzeppelin已经增加了合约,以支持在Polygon、Optimism、Arbitrum和AMB中的使用。主要接口是CrossChainEnabled.sol,所有支持的四个实现都基于它。另外还有一个AccessControlCrossChain.sol,它支持跨链下使用角色进行安全访问控制。相关的概述可以参见这里

    每个链的实现都包含一个不同的机制来检索原始的跨链交易发起者,大致上看起来像这样:

function processMessageFromRoot(
    uint256, /* stateId */
    address rootMessageSender,
    bytes calldata data
  )
AMB_Bridge(bridge).messageSender()
LibArbitrumL2.crossChainSender(LibArbitrumL2.ARBSYS)
Optimism_Bridge(messenger).xDomainMessageSender()

关于完整的细节,请查看这里的合约代码,相应的文档在这里

如何使用跨链功能 -- Polygon为例

让我们以Polygon为例,深入了解一下如何实际使用Openzeppelin 跨链功能。

我们将在根链(Root chain)和子链(child chain)上创建具有安全访问控制的合约。

1. 创建根合约

首先让我们创建一个合约,并在根链上部署。在正常情况下,通常是以太坊主网。我们可以继承FxBaseRootTunnel.sol合约,并根据网络的不同传递检查点和根地址。

  • GOERLI_CHECKPOINT_MANAGER = 0x2890bA17EfE978480615e330ecB65333b880928e
  • GOERLI_FX_ROOT = 0x3d1d3E34f7fB6D26245E6640E1c50710eFFf15bA
  • MAINNET_CHECKPOINT_MANAGER = 0x86E4Dc95c7FBdBf52e33D563BbDB00823894C287
  • MAINNET_FX_ROOT = 0xfe5e5D361b2ad62c541bAb87C45a0B9B018389a2
import {FxBaseRootTunnel} from
    "fx-portal/contracts/tunnel/FxBaseRootTunnel.sol";

// see left for full addresses
address constant GOERLI_CP_MANAGER = 0x2890bA17EfE978480615e...;
address constant GOERLI_FX_ROOT = 0x3d1d3E34f7fB6D26245E6640...;

contract PolygonRoot is FxBaseRootTunnel {
    bytes public latestData;

    constructor()
        FxBaseRootTunnel(GOERLI_CP_MANAGER, GOERLI_FX_ROOT) {
    }

    function _processMessageFromChild(
        bytes memory data
    ) internal override {
        latestData = data;
    }

    function sendMessageToChild(bytes memory message) public {
        _sendMessageToChild(message);
    }
}

它有两个内部函数:

  1. _processMessageFromChild: 重写(Override)函数,以便处理从子合约发来的消息。
  2. _sendMessageToChild:调用函数来向子合约发送消息。

2. 创建子合约

然后我们在子链上创建部署子合约,在我们的案例中,子链是Polygon网络。我们可以继承Openzeppelin CrossChainEnabledPolygonChild.sol合约,并根据网络的情况传递FX Portal子合约。

  • MUMBAI_FX_CHILD = 0xCf73231F28B7331BBe3124B907840A94851f9f11
  • MAINNET_FX_CHILD = 0x8397259c983751DAf40400790063935a11afa28a

也可以使用Openzeppelin的AccessControlCrossChain.sol。只要在合约中继承它,我们就会得到通常的访问控制功能以及一个新的_crossChainRoleAlias函数。

import {CrossChainEnabledPolygonChild} from
    "oz/contracts/crosschain/polygon/CrossChainEnabledPolygonChild.sol";
import {AccessControlCrossChain} from
    "oz/contracts/access/AccessControlCrossChain.sol";

address constant MUMBAI_FX_CHILD = 0xCf73231F...; // see right

contract PolygonChild is
    CrossChainEnabledPolygonChild,
    AccessControlCrossChain
{
    event MessageSent(bytes message);
    uint256 public myNumber = 12;

    constructor(
        address rootParent
    ) CrossChainEnabledPolygonChild(MUMBAI_FX_CHILD) {
        _grantRole(
            _crossChainRoleAlias(DEFAULT_ADMIN_ROLE),
            rootParent
        );
    }

    function setNumberForParentChain(
        uint256 newNumber
    ) external onlyRole(DEFAULT_ADMIN_ROLE) {
        myNumber = newNumber;
    }

    function _sendMessageToRoot(
        bytes memory message
    ) internal {
        emit MessageSent(message);
    }
}

在示例中,在部署时,我们将把先前部署的根合约地址传到这里,并立即授予它管理角色,但由于这实际上是一个跨链通信,它的工作方式有点不同:

  1. 当然onlyRole修改器不能只检查msg.sender,它使用CrossChainEnabled.sol接口来确定实际的跨链交易发起者。
  2. 而为了进一步防止与根链中的地址相同,而在子链的合约访问,我们需要区分交易发起者是直接来自msg.sender还是来自CrossChain的发起者。为此,我们使用_crossChainRoleAlias来授予角色权限。

然后我们添加一个测试函数setNumberForParentChain,只允许CrossChain的根合约调用。

为了完整起见,如果你想把信息发回给根,可以在Polygon中通过发出MessageSent事件来实现。

3. 获取编码数据帮助方法

这个部署是可选的,但是处于我们的测试需要,你可以添加一个额外的函数,就像这样:

function getEncodedSetNumberData(uint256 newNumber) external pure returns (bytes memory) {
    return abi.encodeWithSelector(PolygonChild.setNumberForParentChain.selector, newNumber);
}

基本上这个函数就是返回要调用函数的编码数据,如果你想调用setNumberForParentChain函数,你需要获得它的编码数据,你需要通过sendMessageToChild在根合约中发送这个数据。当然,在大多数环境中,你只是在使用Web3.js或任何你所使用的前端框架来获得编码数据。

4. 在Remix上测试跨链转账

好了,现在让我们把它部署到测试网,我们这里使用:

  • Goerli网络作为根链,这是最新的以太坊测试网
  • Mumbai:Polygon测试网,作为子链

对比部署到正式主网,流程是相同的,只是使用的地址不同。

所以我们可以直接把这里的全部代码复制到Remix中:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.13;

import {CrossChainEnabledPolygonChild} from "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/crosschain/polygon/CrossChainEnabledPolygonChild.sol";
import {AccessControlCrossChain} from "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/AccessControlCrossChain.sol";

import {FxBaseRootTunnel} from "https://github.com/fx-portal/contracts/blob/main/contracts/tunnel/FxBaseRootTunnel.sol";

address constant MUMBAI_FX_CHILD = 0xCf73231F28B7331BBe3124B907840A94851f9f11;
address constant GOERLI_CHECKPOINT_MANAGER = 0x2890bA17EfE978480615e330ecB65333b880928e;
address constant GOERLI_FX_ROOT = 0x3d1d3E34f7fB6D26245E6640E1c50710eFFf15bA;

address constant MAINNET_FX_CHILD = 0x8397259c983751DAf40400790063935a11afa28a;
address constant MAINNET_CHECKPOINT_MANAGER = 0x86E4Dc95c7FBdBf52e33D563BbDB00823894C287;
address constant MAINNET_FX_ROOT = 0xfe5e5D361b2ad62c541bAb87C45a0B9B018389a2;

contract PolygonChild is CrossChainEnabledPolygonChild, AccessControlCrossChain {
    uint256 public myNumber;

    constructor(address rootParent) CrossChainEnabledPolygonChild(MUMBAI_FX_CHILD) {
        _grantRole(_crossChainRoleAlias(DEFAULT_ADMIN_ROLE), rootParent);
        myNumber = 12;
    }

    function setNumberForParentChain(uint256 newNumber) external onlyRole(DEFAULT_ADMIN_ROLE) {
        myNumber = newNumber;
    }

    function getEncodedSetNumberData(uint256 newNumber) external pure returns (bytes memory) {
        return abi.encodeWithSelector(PolygonChild.setNumberForParentChain.selector, newNumber);
    }
}

contract PolygonRoot is FxBaseRootTunnel {
    bytes public latestData;

    constructor() FxBaseRootTunnel(GOERLI_CHECKPOINT_MANAGER, GOERLI_FX_ROOT) {}

    function _processMessageFromChild(bytes memory data) internal override {
        latestData = data;
    }

    function sendMessageToChild(bytes memory message) public {
        _sendMessageToChild(message);
    }
}

现在你可以:

  1. 将MetaMask切换到Goerli并部署PolygonRoot。
  2. 将MetaMask切换到Mumbai,复制地址并部署PolygonChild作为构造函数的输入参数。
  3. 将MetaMask切换到Goerli,在PolygonRoot上调用 setFxChildTunnel,传递子合约地址。
  4. 对你要发送的数据进行编码。例如,将setNumberForParentChain的数字设置为42,编码后的数据是:0x21148d91000000000000000000000000000000000000000000000000000000000000002a. 最简单的方法是通过getEncodedSetNumberData帮方法来获得。
  5. 调用sendMessageToChild并把编码数据作为。这将启动跨链调用。这可能需要一些时间。
  6. 等待一写时间....,同时你可以在这里检查FX_CHILD的事件或者简单地检查零地址的转账这里

Mumbai Zero

Mumbai FX Child

这些交易看起来可能让你感到困惑,但它并不奇怪。零地址是Polygon的一个特殊系统地址,用于提交CrossChain调用。

在我的测试中,直到Polygon上的CrossChain转账完成,需要2到25分钟。

跨链调用到这里就完成了,如果一切都正确,你可以切换回Mumbai测试网,并读取新设置的数字,它应该已经变成了42。


本翻译由 Duet Protocol 赞助支持。

本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 2022-05-23 15:49
  • 阅读 ( 821 )
  • 学分 ( 18 )
  • 分类:智能合约

0 条评论

请先 登录 后评论
翻译小组
翻译小组

首席翻译官

150 篇文章, 25357 学分