多链智能合约部署指南

多链智能合约部署指南

在之前的文章中,我们解析了跨链智能合约的工作原理。现在,是时候更进一步,亲自动手实现两个链之间的跨链消息传递了。启动你的代码编辑器,因为我们将深入实践实现!

跨链消息传递是使数据从一个区块链传输到另一个区块链的核心机制。可以将其视为链之间的桥梁,允许在不同生态系统之间无缝通信和操作。例如,你可以将数据从Avalanche发送到BSC(币安智能链),并在接收到数据后在BSC上触发操作。

Avalanche <=> BSC

这里有一个现实世界的用例来说明这一点:想象一个在BSC上提供比Avalanche更高回报的质押协议。使用跨链消息传递,你可以:

  1. 发送跨链消息:在Avalanche链上,发送包含必要质押数据的消息,如质押金额和任何特定参数。
  2. 接收并处理消息:在BSC端,通过跨链消息传递协议接收并验证消息。
  3. 执行质押操作:一旦消息被处理,BSC上的质押功能将使用从Avalanche接收到的数据自动调用。

这个过程允许你在多个链上最大化协议的优势,而无需手动与两者交互。让我们分解一下如何在实践中构建这一点。

现在..

我们将直接深入实现两个链之间的简单跨链消息传递,而不浪费太多时间。

在本文中,我们将使用Wormhole。Wormhole提供了用于跨链消息传递的solidity SDK。我们将利用Wormhole的跨链消息传递功能将数据从一个链发送到另一个链。

你可以使用Remix IDE来编译和部署智能合约,或者通过设置Hardhat或Foundry项目在本地机器上进行相同的操作。

让我们定义我们的合约WormholeCcm.sol。

实现跨链消息传递的第一步是定义我们的智能合约。在这种情况下,我们将其称为WormholeCcm.sol(跨链消息传递的缩写)。

pragma solidity ^0.8.13;

import "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol";  
import "wormhole-solidity-sdk/interfaces/IWormholeReceiver.sol";
contract WormholeCcm is IWormholeReceiver{
}

在这初始步骤中,我们首先定义合约结构并导入必要的Wormhole接口,这些接口将帮助我们处理跨链通信。

SPDX License Identifier:
// SPDX-License-Identifier: UNLICENSED这一行是Solidity合约中的标准惯例,用于指定代码分发的许可证。在这种情况下,它被标记为未许可,但你可以选择适合你项目的许可证。

Solidity Version:
我们将Solidity版本定义为^0.8.13,这意味着合约将兼容Solidity版本0.8.13及以上。指定正确的版本很重要,以避免与新版本的兼容性问题。

Importing Wormhole Interfaces:
我们从Wormhole SDK导入两个重要的接口:

  • IWormholeRelayer:此接口将允许我们与Wormhole的中继器交互,中继器负责发送跨链消息。
  • IWormholeReceiver:此接口用于接收来自其他链的消息。通过继承此接口,我们的合约将能够处理传入的跨链消息。

Defining the Contract:
合约WormholeCcm继承自IWormholeReceiver接口。这种继承将使我们能够实现处理传入消息的功能,但目前我们只是设置合约的基本结构。

有了这个基础,我们就可以继续前进,开始实现发送和接收跨链消息的核心功能。

定义状态变量

现在我们已经定义了WormholeCcm.sol合约的基本结构,是时候引入状态变量来存储重要数据并处理跨链消息事件了。

event GreetingReceived(string greeting, uint16 senderChain, address sender);

uint256 constant GAS_LIMIT = 200_000;  
uint16 public _srcChainId;

IWormholeRelayer public immutable wormholeRelayer;

string public latestGreeting;  
address public owner;

mapping(uint16 => bytes32) private peerContracts;

让我们分解每个部分

Greeting Received Event

event GreetingReceived(string greeting, uint16 senderChain, address sender);

  • 这个事件对于跟踪从另一条链接收到消息至关重要。每当通过跨链消息成功接收到问候时,事件将被触发,记录以下信息:
  • greeting:从另一条链发送的问候消息。
  • senderChain:发送消息的链的链ID。
  • sender:在发送链上发起消息的地址。

Gas Limit Constant:

  • uint256 constant GAS_LIMIT = 200_000;
  • 我们定义了一个常量GAS_LIMIT用于执行跨链交易。
  • 这里我们将其设置为200,000,但这个值可以根据你在链间中继的操作复杂性进行调整。

Source Chain ID:

  • uint16 public _srcChainId;
  • 合约将要部署的链的Wormhole链ID。

Wormhole Relayer Interface:

  • IWormholeRelayer public immutable wormholeRelayer;
  • 我们定义了一个状态变量wormholeRelayer,它是IWormholeRelayer接口的一个实例。这将用于与Wormhole的中继服务交互,允许我们将跨链消息从一个区块链发送到另一个区块链。
  • 它被标记为immutable,这意味着它只能在合约部署期间设置一次,之后不能更改,确保合约的完整性。

Latest Greeting:

  • string public latestGreeting;
  • 这个变量将存储从跨链消息中接收到的最新问候。通过将其设为public,我们允许任何人直接从合约中查询此数据。

Owner:

  • address public owner;
  • 这个变量将存储合约所有者的地址。

Peer Contracts mapping:

  • mapping (uint16 => bytes32) private peerContracts;
  • 这个映射将存储其他链上对等合约的地址(以bytes32格式),与该特定链的链ID相关联。
  • 它将允许我们检查是否只有对等合约被允许在链间发送和接收消息。

通过定义这些状态变量,我们现在有了存储传入数据、限制gas使用以及管理与Wormhole中继器交互的必要框架。

定义“onlyOwner”修饰符

“onlyOwner”修饰符预先检查“msg.sender”是否为合约所有者的条件。这允许我们限制某些重要功能的访问。像“setPeer”函数这样的函数将只能由所有者调用。因此我们需要使用这个修饰符。

modifier onlyOwner() {  
    require(msg.sender == owner);  
    _;  
}

定义构造函数

现在我们已经设置了状态变量,下一步是为我们的合约WormholeCcm.sol定义构造函数。构造函数负责在合约部署时用重要参数初始化合约。

constructor(address _wormholeRelayer, uint16 srcChainId){  
    wormholeRelayer = IWormholeRelayer(_wormholeRelayer);  
    _srcChainId = srcChainId;  
    owner = msg.sender;  
}

在构造函数中,我们使用提供的_wormholeRelayer地址分配wormholeRelayer状态变量。这个地址必须指向部署合约的特定区块链上的Wormhole Relayer的正确实例。你可以从wormhole docs.获取这个地址。

我们使用IWormholeRelayer(_wormholeRelayer)将地址转换为IWormholeRelayer接口,确保我们的合约可以正确地与Wormhole Relayer的函数交互。确保我们初始化wormholeRelayer是很重要的,因为它是不可变的。

我们还将“srcChainId”变量的值设置为传递的链ID值。这是当前链的wormhole链ID。

我们还通过将其设置为“msg.sender”来初始化所有者变量。这意味着部署地址将成为合约的所有者。它将只允许所有者访问像“setPeer”函数这样的函数。

“setPeer”函数

此函数负责设置与相应链ID关联的对等合约地址。

它接受两个参数:

  • ChainId:目标区块链的链ID,消息将被发送到该区块链。
  • peerContract:目标链上与提供的链ID关联的合约地址。这个地址应该是bytes32类型。
  • 将地址转换为bytes32,我们必须在地址前面添加24个零
  • 例如,“0x1502e497B95e7B01D16C9C4C8193E6C2636f98C2”在bytes32中将是“0x0000000000000000000000001502e497b95e7b01d16c9c4c8193e6c2636f98c2

该函数还检查提供的地址是否不是空地址,以避免为特定链ID设置空地址。

function setPeer(uint16 chainId, bytes32 peerContract) public onlyOwner{  
    require(peerContract != bytes32(0));  
    peerContracts[chainId] = peerContract;  
}

“sendMessage”函数

在这一步中,我们实现sendMessage函数,该函数负责使用Wormhole发送跨链消息。此函数允许我们从一个区块链向另一个区块链发送问候。

function sendMessage(
       uint16 targetChain,  
        address targetAddress,  
        string memory greeting) public payable {  
   uint256 cost = quoteCrossChainGreeting(targetChain);  
   require(msg.value == cost);  
   wormholeRelayer.sendPayloadToEvm{value: cost}(  
        targetChain,  
        targetAddress,  
        abi.encode(greeting, msg.sender),   
        0,   
        GAS_LIMIT,  
        targetChain,  
        msg.sender  
   );  
}

以下是此函数的逐步工作原理:

  1. 函数签名
  • 该函数名为sendMessage,并标记为public,意味着任何人都可以调用它。
  • 它接受三个参数:
  • targetChain:消息将被发送到的区块链的链ID。
  • targetAddress:目标链上将接收消息的合约地址。
  • greeting:要跨链发送的问候消息。

费用计算

  • uint256 cost = quoteCrossChainGreeting(targetChain);
  • 这行代码调用函数quoteCrossChainGreeting来确定发送消息到targetChain的费用。发送跨链消息需要支付中继服务的费用,这取决于链。

消息费用验证

  • require(msg.value == cost);
  • 该函数检查发送者是否提供了正确数量的以太币来支付中继费用。如果msg.value(与交易一起发送的以太币数量)不等于计算出的cost,交易将失败。

发送跨链消息

  • wormholeRelayer.sendPayloadToEvm{value: cost}(...)
    这是使用Wormhole的sendPayloadToEvm函数发送实际跨链消息的地方。函数参数包括:

**targetChain** (uint16)

  • 这是Wormhole格式的链ID,用于标识消息将被发送到的目标区块链。

**targetAddress** (address)

  • 这指定了目标链上将接收跨链消息的合约地址。它确保消息被传递到目标区块链上的正确合约。

**payload** (bytes memory)

  • payload是跨链发送的实际消息,编码为bytes
  • 在此示例中,我们使用abi.encode(greeting, msg.sender)将问候字符串和发送者的地址(msg.sender)打包成一个字节数组。此编码确保数据在链间正确传输,并且可以在接收端解码。

**receiverValue** (uint256)

  • 此参数指示在目标链上附加到交付交易的价值(例如,以太币或BNB)的数量。在这种情况下,我们传递0,意味着不发送额外的价值,因为该函数仅传递消息负载,不涉及代币转移。

**gasLimit** (uint256)

  • 这定义了目标链上交付交易的gas限制。它是执行目标链上交易所能使用的最大gas量。
  • 在此示例中,我们使用预定义的GAS_LIMIT常量(先前设置为200,000)。

**refundChainId** (uint16)

  • 这定义了剩余gas应退还的链ID。
  • 我们将其传递为目标链ID。
  • 因此退款将在目标链上接收。

**refundAddress** (address)

  • 这定义了剩余gas应退还的地址。
  • 我们将其传递为“msg.sender”,因此发送消息的用户将获得退款。

sendMessage函数封装了在区块链之间发送数据的核心逻辑。它确保用户支付正确的跨链消息费用,并使用Wormhole的中继器将编码的问候消息传输到指定的目标链和地址。

发送跨链消息的费用

发送跨链消息会产生费用。现在要在某个区块获取跨链费用,wormhole提供了一个函数来获取跨链费用。

function quoteCrossChainGreeting(  
    uint16 targetChain  
) public view returns (uint256 cost) {  
    (cost, ) = wormholeRelayer.quoteEVMDeliveryPrice(  
        targetChain,  
        0,  
        GAS_LIMIT  
    );  
}
  • 输入:它接受targetChain(目标链ID)。
  • 输出:它返回发送消息所需的cost,基于目标链和gas限制。

“receiveWormholeMessages”函数

在设置跨链消息传递后,下一个关键组件是**receiveWormholeMessages**函数。此函数负责处理通过Wormhole从其他区块链发送的传入消息。

function receiveWormholeMessages(
   bytes memory payload,  
    bytes[] memory,   
    bytes32 sender,  
    uint16 sourceChain,  
    bytes32 ) public payable override {  
    require(msg.sender == address(wormholeRelayer), "Only relayer allowed");  
    require(peerContracts[sourceChain] == sender);

    (string memory greeting, address senderAddress) = abi.decode(  
        payload,  
        (string, address)  
    );  
    latestGreeting = greeting;  
    emit GreetingReceived(latestGreeting, sourceChain, senderAddress);  
    }

以下是此函数的功能:

目的:处理从其他链接收的消息。当跨链消息到达时,此函数会解码并处理它。此函数仅由wormhole relayer合约调用。

输入

**bytes memory payload**

  • 描述:这是从源链上的源合约传递的主要消息数据。
  • 目的:它包含需要处理的实际内容。在此函数中,payload包括问候消息和发送者的地址,编码为字节。

**bytes[] memory additionalVaas**

  • 描述:这是可能包含在消息中的额外验证账户地址(VAA)的数组。这些VAA通常用于额外验证,但在此函数中未使用。
  • 目的:它们提供了可能与高级用例相关的补充数据,尽管在当前实现中未使用。

**bytes32 sender**

  • 描述:这是发送消息的源链上源合约的地址。
  • 目的:它标识了消息的来源合约。这对于验证消息的来源或将其路由到正确的合约非常有用。

**uint16 sourceChain**

  • 描述:发送消息的源链的Wormhole链ID。
  • 目的:它指定消息来自哪个链。这有助于跟踪源链,并可用于基于来源的日志记录或处理决策。

**bytes32 deliveryHash**

  • 描述:表示交付内容的唯一哈希。此哈希用于重放保护。
  • 目的:它确保每条消息都是唯一的,并有助于防止重放攻击,即同一消息可能被恶意地多次重放。

并非所有这些参数总是被使用,但我们应该了解它们。

输出

  • 状态更新:该函数使用新的问候消息更新latestGreeting状态变量。
  • 事件触发:它触发一个GreetingReceived事件,包含更新的问候消息、源链ID和发送者的地址。

安全检查

  • 该函数确保只有Wormhole relayer可以调用它,防止未经授权的访问。
  • 该函数确保只有来自其他链的对等合约发送的消息。

此函数通过处理传入消息并相应地更新合约状态,完成了跨链通信过程。它确保你的合约可以响应来自其他链的消息,并提供了一种跟踪和处理接收数据的方法。

整体合约

pragma solidity ^0.8.13;

import "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol";  
import "wormhole-solidity-sdk/interfaces/IWormholeReceiver.sol";

contract WormholeCcm is IWormholeReceiver {  
    event GreetingReceived(string greeting, uint16 senderChain, address sender);

    uint256 constant GAS_LIMIT = 200_000;  
    uint16 public _srcChainId;

    IWormholeRelayer public immutable wormholeRelayer;

    string public latestGreeting;  
    address public owner;

    mapping(uint16 => bytes32) private peerContracts;

    modifier onlyOwner() {  
        require(msg.sender == owner);  
        _;  
    }

    constructor(address _wormholeRelayer, uint16 srcChainId){  
        wormholeRelayer = IWormholeRelayer(_wormholeRelayer);  
        _srcChainId = srcChainId;  
        owner = msg.sender;  
    }

    function quoteCrossChainGreeting(        uint16 targetChain    ) public view returns (uint256 cost) {  
        (cost, ) = wormholeRelayer.quoteEVMDeliveryPrice(  
            targetChain,  
            0,  
            GAS_LIMIT  
        );  
    }

    function setPeer(uint16 chainId, bytes32 peerContract) public onlyOwner{  
        require(peerContract != bytes32(0));  
        peerContracts[chainId] = peerContract;  
    }

    function sendMessage(        uint16 targetChain,  
        address targetAddress,  
        string memory greeting    ) public payable {  
        uint256 cost = quoteCrossChainGreeting(targetChain);  
        require(msg.value == cost);  
        wormholeRelayer.sendPayloadToEvm{value: cost}(  
            targetChain,  
            targetAddress,  
            abi.encode(greeting, msg.sender),   
            0,   
            GAS_LIMIT,  
            targetChain,  
            msg.sender  
        );  
    }

    function receiveWormholeMessages(        bytes memory payload,  
        bytes[] memory,   
        bytes32 sender,  
        uint16 sourceChain,  
        bytes32     ) public payable override {  
        require(msg.sender == address(wormholeRelayer), "Only relayer allowed");  
        require(peerContracts[sourceChain] == sender);

        (string memory greeting, address senderAddress) = abi.decode(  
            payload,  
            (string, address)  
        );  
        latestGreeting = greeting;  
        emit GreetingReceived(latestGreeting, sourceChain, senderAddress);  
    }  
}

在两条链上编译和部署

让我们在remix IDE上编译上述合约并将其部署在两条链上:Avalanche fuji和BSC testnet。

编译合约

编译合约

在编辑器中打开合约的同时,转到侧边栏上的编译选项卡。点击编译按钮。成功编译后,进入下一步。

连接Metamask钱包与REMIX IDE并部署合约

切换到remix IDE上的“部署和运行交易”选项卡。在环境下拉菜单中选择注入的Metamask选项。

从wormhole文档中获取relayer地址, 在这里。在部署输入字段中输入连接链的relayer地址作为构造函数参数。

点击部署按钮并在Metamask中确认交易。以下截图显示了BSC Testnet链的部署交易。切换Metamask钱包到Avalanche fuji链后,重复相同的步骤并将合约部署到Avalanche fuji。

将合约部署到BSC

配置对等合约

要设置对等合约,你需要调用**setPeer**函数。这确保了只有相应链上的授权合约可以相互通信。

操作步骤如下:

  1. 调用 **setPeer** 函数:在目标链(例如BSC)上,调用**setPeer**函数以建立与Avalanche链上合约的连接。
  2. 传递所需参数
    • Chain ID: 使用Wormhole格式的Avalanche链,ID为6
    • Contract Address: 提供在Avalanche链上部署的合约地址,格式为bytes32。

通过设置这个对等连接,你可以确保只有Avalanche链上的授权合约可以向BSC合约发送跨链消息,反之亦然。这一步对于确保跨链合约之间的通信安全,防止未经授权的消息被处理至关重要。

确保在Avalanche Fuji链上重复相同的操作,并授权BSC测试网链合约地址。

获取发送消息所需的费用

在发送跨链消息之前,确定所需的费用是至关重要的。在我们的例子中,我们使用**quoteCrossChainGreeting**函数来获取准确的费用估算。此函数接受目标链ID,格式按照Wormhole的规范(你可以在Wormhole的文档中轻松找到链ID)。

例如,在调用该函数后,我们收到的费用估算为**2600052000000000** wei,相当于**0.002600052** AVAX。与其他跨链协议相比,Wormhole的费用非常低,使其成为跨链消息传递的经济选择。

有了这些信息,我们现在知道在调用**sendMessage**函数时,我们必须传递**0.002600052** AVAX作为所需的值,以成功发送消息。此费用确保我们的消息将在目标链上以必要的gas限制进行执行。

发送消息

现在我们已经估算了费用,是时候从AVAX Fuji测试网向BSC测试网发送跨链消息了。

为此,我们将调用**sendMessage**函数,并使用从**quoteCrossChainGreeting**函数接收到的值,即**0.002600052** AVAX

以下是逐步过程:

设置值:在值字段中输入**2600052000000000** wei(相当于**0.002600052** AVAX)。

在发起交易前传递值

调用函数:通过传递所需参数来调用**sendMessage**函数:

  • 目标链ID:从Wormhole文档中获取BSC测试网的适当链ID。
  • 目标合约地址:输入部署在BSC测试网上将接收消息的合约地址。
  • 问候消息:包括你希望跨链发送的问候消息。

提供这些参数后,跨链消息将从AVAX Fuji测试网发送到BSC测试网。

发送跨链消息

监控跨链交易状态

发送跨链消息后,监控其状态以确保成功交付非常重要。Wormhole提供了一个易于使用的工具:Wormhole Scan

要跟踪跨链交易的进度:

  1. 复制源链(例如AVAX Fuji测试网)的交易哈希
  2. 导航到Wormhole Scan
  3. 将交易哈希粘贴到搜索栏中。

Wormhole Scan将显示你的交易状态,允许你确认消息已成功跨链传递。

已经完成了!!Wormhole非常快。跨链发送数据或消息的速度取决于我们使用的协议以及该协议用于跨链数据传输的机制。

像Chainlink CCIP这样的协议可能需要更多时间来传递消息,因为它等待目标链上交易的最终确定。而Wormhole使用不同的机制,包括守护节点网络和链上中继器。你可以在这里了解更多信息。

验证目标链上的消息接收

发送跨链消息后,你可以通过读取**latestGreeting**变量来验证它是否已成功接收。此变量应更新为你发送的消息,在本例中为"Hey there"

以下是确认消息接收的方法:

在Wormhole Scan上检查

你可以通过输入交易哈希使用Wormhole Scan确认交易状态和消息接收。它将显示跨链消息的传递过程,包括其到达目标链。

在BSC测试网上读取**latestGreeting**变量

  • 将你的MetaMask钱包切换到BSC测试网链,并确保Remix IDE连接到BSC测试网链。
  • 在Remix中调用**latestGreeting** getter函数以读取合约中存储的值。

结果应返回你从Avalanche Fuji链发送的消息,即“Hey there”。这确认了跨链消息的成功传递。

这就是全部——只需两个关键函数:**sendMessage****receiveMessage**,以及配置端点和路径,你就可以开始使用Wormhole进行跨链消息传递了!但等等,还有其他提供跨链消息传递的协议吗?

别担心,我已经为你准备好了!虽然本指南侧重于Wormhole,不同的协议遵循类似的结构,但有一些变化。为了帮助你导航这些协议,这里有一个Remix工作区和一个GitHub仓库,你可以在其中探索使用各种跨链消息传递协议的合约:

这些资源将指导你使用其他协议(如LayerZero、Axelar等)设置跨链消息传递。

同样的,但不同

但我们必须分别学习每个协议吗?完全不需要。当你探索这些合约时,你会很快注意到,发送跨链消息基本上归结为三个关键步骤:

  1. 一个从源链发送消息的函数
  2. 一个在目标链上接收消息的函数
  3. 配置端点地址和对等合约,这些可能根据协议有不同的名称,但核心功能保持不变。

一旦掌握了这些基本步骤,在协议之间切换就变得简单了。无论是Wormhole、LayerZero还是其他任何协议,底层逻辑是一致的。通过这种理解,你可以自信地使用任何协议实现跨链消息传递!总之,跨链消息传递是实现区块链之间无缝通信的重要工具,使数据和交易能够在不同网络之间自由流动。无论你使用的是Wormhole、LayerZero还是其他协议,核心概念都是一致的:从源链发送消息,在目标链接收消息,并配置必要的端点和合约。

一旦掌握了这些基本步骤,你会发现实现跨链消息传递变得很简单,无论使用哪种协议。有了这些知识,你现在可以开始构建强大的跨链应用程序了。编码愉快!

我是 AI 翻译官,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~

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

0 条评论

请先 登录 后评论
Jadhav Rushikesh
Jadhav Rushikesh
江湖只有他的大名,没有他的介绍。