跨链消息传递

构建合约的开发者可能需要跨链功能。为此,多个协议已经实现了它们自己跨链处理操作的方式。

这些桥的多样性在 @norswap跨链互操作性报告 中有所概述,该报告提出了 7 种桥类别分类法。由于缺乏可移植性,这种多样性使得开发者难以设计跨链应用。

本指南将教你如何遵循 ERC-7786 来建立跨链消息传递网关,而无论底层的桥是什么。开发者可以实现处理跨链消息的网关合约,并连接他们想要的任何跨链协议(或自己实现)。

ERC-7786 网关

为了以一种简单且不带偏见的方式解决可组合性的缺乏,ERC-7786 提出了一个用于实现将消息中继到其他链的网关的标准。这种通用的方法具有足够的表达力,可以启用新型的应用,并且可以通过标准化的属性适用于任何桥分类法或特定的桥接口。

消息传递概述

该 ERC 定义了一个源网关和一个目标网关。两者都是合约,它们实现了一个协议来发送消息并分别处理其接收。这两个过程由 ERC-7786 规范明确标识,因为它们定义了两个网关的最低要求。

  • 源链 上,合约实现了一个标准的 sendMessage 函数,并发出一个 MessagePosted 事件,以表明该消息应由底层协议中继。

  • 目标链 上,网关接收消息并通过调用 executeMessage 函数将其传递给接收者合约。

智能合约开发者只需要担心实现 IERC7786GatewaySource 接口以在源链上发送消息,以及实现 IERC7786GatewaySourceIERC7786Receiver 接口以在目标链上接收此类消息。

开始使用 Axelar Network

为了开始发送跨链消息,开发者可以从由 Axelar Network 提供支持的双工网关开始。这将允许合约利用 Axelar 中继器在目标链上的自动执行来发送或接收跨链消息。

Unresolved include directive in modules/ROOT/pages/crosschain.adoc - include::api:example$crosschain/MyCustomAxelarGatewayDuplex.sol[]

有关双工网关如何工作的更多细节,请参见下面的 如何使用 Axelar Network 发送和接收消息

开发者可以使用 registerChainEquivalenceregisterRemoteGateway 函数注册支持的链和目标网关。

跨链通信

发送消息

源网关的接口足够通用,它允许包装自定义协议来验证消息。根据用例,开发者可以实现任何链下机制来读取标准的 MessagePosted 事件并将其传递到目标链上的接收者。

Unresolved include directive in modules/ROOT/pages/crosschain.adoc - include::api:example$crosschain/MyERC7786GatewaySource.sol[]
该标准使用 CAIP-2 标识符表示链,并使用 CAIP-10 标识符表示帐户,以增强与非 EVM 链的互操作性。 考虑在合约库中使用 Strings 库来处理这些标识符。

接收消息

为了成功处理目标链上的消息,需要一个目标网关。尽管 ERC-7786 没有为目标网关定义标准接口,但它要求在接收到消息时调用 executeMessage

每个跨链消息协议已经提供了一种通过规范桥或中间合约接收消息的方式。开发者可以轻松地将接收合约包装到网关中,该网关调用 ERC 要求的 executeMessage 函数。

为了在自定义智能合约上接收消息,OpenZeppelin Community Contracts 为开发者提供了一个 ERC7786Receiver 实现以供继承。 这样,你的合约就可以接收通过已知的目标网关中继的跨链消息。

Unresolved include directive in modules/ROOT/pages/crosschain.adoc - include::api:example$crosschain/MyERC7786ReceiverContract.sol[]

标准接收接口抽象了底层协议。 这样,合约就可以通过符合 ERC-7786 的网关(或通过适配器)发送消息,并在目标链上接收到该消息,而无需担心协议的实现细节。

Axelar Network

除了 AxelarGatewayDuplex 之外,该库还提供了一个名为 AxelarGatewaySourceIERC7786GatewaySource 接口的实现,该接口用作发送符合 ERC-7786 的消息的适配器。

该实现采用了一个本地网关地址,该地址必须对应于 Axelar 的原生网关,并且具有以下机制:

  • 跟踪 Axelar 链名称和 CAIP-2 标识符之间的等价关系

  • 使用其 CAIP-2 标识符记录每个网络的目标网关

AxelarGatewaySource 实现可以开箱即用

Unresolved include directive in modules/ROOT/pages/crosschain.adoc - include::api:example$crosschain/MyCustomAxelarGatewaySource.sol[]

对于目标网关,该库提供了一个 AxelarExecutable 接口的适配器,用于接收消息并将它们中继到 IERC7786Receiver

Unresolved include directive in modules/ROOT/pages/crosschain.adoc - include::api:example$crosschain/MyCustomAxelarGatewayDestination.sol[]

Open Bridge

ERC7786OpenBridge 是一个特殊的网关,它实现了 IERC7786GatewaySourceIERC7786Receiver 接口。 它提供了一种同时跨多个桥发送消息的方式,并确保通过基于阈值的确认系统传递消息。

该桥维护一个已知的网关列表和一个确认阈值。 发送消息时,它会广播到所有注册的网关,接收消息时,它需要最少数量的确认才能执行消息。 这种方法通过确保跨多个桥正确传递和验证消息来提高可靠性。

发送消息时,桥会跟踪来自每个网关的消息 ID,以维护消息跨不同桥的传输记录:

function sendMessage(
    string calldata destinationChain,
    string memory receiver,
    bytes memory payload,
    bytes[] memory attributes
) public payable virtual whenNotPaused returns (bytes32 outboxId) {

    // ... initializes variables and prepares the payload ...
    // ... 初始化变量并准备 payload ...

    // Posts on all gateways
    // 在所有网关上发布
    Outbox[] memory outbox = new Outbox[](_gateways.length());
    bool needsId = false;
    for (uint256 i = 0; i < outbox.length; ++i) {
        address gateway = _gateways.at(i);
        // Sends message
        // 发送消息
        bytes32 id = IERC7786GatewaySource(gateway).sendMessage(
            destinationChain,
            bridge,
            wrappedPayload,
            attributes
        );
        // Tracks the id, if any
        // 如果有 ID,则跟踪它
        if (id != bytes32(0)) {
            outbox[i] = Outbox(gateway, id);
            needsId = true;
        }
    }

    // ... 处理消息跟踪并返回值 ...
    // ... process message tracking and return the value ...
}

在接收端,桥实现了一个基于阈值的确认系统。 仅在收到来自网关的足够确认后才执行消息,从而确保消息的有效性并防止重复执行。 executeMessage 函数处理此过程:

function executeMessage(
    string calldata /*messageId*/, // Gateway specific, empty or unique
    string calldata sourceChain, // CAIP-2 chain identifier
    string calldata sender, // CAIP-10 account address (without chain identifier)
    bytes calldata payload,
    bytes[] calldata attributes
) public payable virtual whenNotPaused returns (bytes4) {

    // ... 验证消息格式并提取消息 ID ...
    // ... Validate message format and extract the message id ...

    // If the call firstly comes from a trusted gateway
    // 如果调用首先来自可信网关
    if (_gateways.contains(msg.sender) && !tracker.receivedBy[msg.sender]) {
        // Count how many times it has been received
        // 统计接收到的次数
        tracker.receivedBy[msg.sender] = true;
        ++tracker.countReceived;
        emit Received(id, msg.sender);

        // If it has been executed, exits normally
        // 如果已执行,则正常退出
        if (tracker.executed) return IERC7786Receiver.executeMessage.selector;
    } else if (tracker.executed) {
        revert ERC7786OpenBridgeAlreadyExecuted();
    }

    // ... 验证发件人并准备执行负载 ...
    // ... validate sender and prepare the execution load ...

    // If it is ready to execute and not already executed
    // 如果准备好执行,并且尚未执行
    if (tracker.countReceived >= getThreshold()) {
        // Prevents reentrancy
        // 防止重入
        tracker.executed = true;

        // ... 准备执行上下文并验证状态 ...
        // ... prepare execution context and validate status ...
        bytes memory call = abi.encodeCall(
            IERC7786Receiver.executeMessage,
            (uint256(id).toHexString(32), sourceChain, originalSender, unwrappedPayload, attributes)
        );

        (bool success, bytes memory returndata) = receiver.parseAddress().call(call);

        // ... 处理结果 ...
        // ... Process results ...
    }

    return IERC7786Receiver.executeMessage.selector;
}

该桥被设计为可配置的。 作为一个 Ownable 合约,它允许所有者管理可信网关的列表并调整确认阈值。 _gateways 列表和阈值最初是在合约部署期间使用 _addGateway_setThreshold 函数设置的。 所有者可以根据需要更新这些设置,以适应不断变化的需求或添加新的网关。