{"content":{"title":"LayerZero 跨链通信转账 2024极简示例","body":"## 开篇\r\n\r\n由于在站内看到了2022年layerzero极简教程，但是由于layerzero版本更迭，其官方示例的封装性高又缺乏注解，故写了这篇2024版layerzero示例。\r\n\r\n本示例包含fantom testnet 跨链传递信息或native token至mumbai network的合约与脚本，希望能帮到诸位。\r\n\r\n\r\n\r\n\r\n## 初始化项目\r\n\r\n我们可以使用hardhat很轻松地创建一个solidity合约项目：\r\n\r\n```shell\r\nmkdir layerzero-tutorial\r\n\r\ncd layerzero-tutorial\r\n\r\nnpm init\r\n\r\nnpm install --save-dev hardhat\r\n\r\nnpx hardhat init\r\n```\r\n\r\n最后一项选择创建一个ts项目\r\n\r\n```\r\n👷 Welcome to Hardhat v2.20.1 👷‍\r\n\r\n? What do you want to do? …\r\n\r\n  Create a JavaScript project\r\n\r\n  Create a TypeScript project\r\n\r\n ❯ Create a TypeScript project (with Viem)\r\n\r\n  Create an empty hardhat.config.js\r\n\r\n  Quit\r\n```\r\n\r\n\r\n\r\n## 编写合约\r\n\r\n我们期待从A链发送信息或者代币至B链，需要在A/B两条链上分别部署合约，如果你的操作是对称的，即相同业务的双向通信，则可以使用同一张合约，本示例的合约也只有一张，分别部署至 fantom testnet 和 mumbai。\r\n\r\n下面是合约代码：\r\n\r\n```javascript\r\n//SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.9;\r\npragma abicoder v2;\r\n\r\nimport \"../interfaces/ILayerZeroEndpoint.sol\";\r\nimport \"../interfaces/ILayerZeroReceiver.sol\";\r\nimport \"hardhat/console.sol\";\r\n\r\ncontract LayerZeroDemo1 is ILayerZeroReceiver {\r\n    event ReceiveMsg(\r\n        uint16 _srcChainId,\r\n        address _from,\r\n        uint16 _count,\r\n        bytes _payload\r\n    );\r\n    ILayerZeroEndpoint public endpoint;\r\n    uint16 public messageCount;\r\n    bytes public message;\r\n\r\n    constructor(address _endpoint) {\r\n        endpoint = ILayerZeroEndpoint(_endpoint);\r\n    }\r\n    \r\n    function sendMsg(\r\n        uint16 _dstChainId,\r\n        address _destination,\r\n        bytes calldata payload,\r\n        bytes calldata _adapterParams\r\n    ) public payable {\r\n        require(abi.encodePacked(_destination,address(this)).length == 40, \"Invalid destination\");\r\n        endpoint.send{value: msg.value}(\r\n            _dstChainId,\r\n            abi.encodePacked(_destination,address(this)),\r\n            payload,\r\n            payable(msg.sender),\r\n            address(this),\r\n            _adapterParams\r\n        );\r\n    }\r\n\r\n    function sendNativeToken(\r\n        uint16  _dstChainId, \r\n        address  _toAddress, \r\n        uint  _amount\r\n    ) public payable {\r\n        uint dstGas = 350000;\r\n        uint16 version = 2;\r\n        bytes memory adapterParams = abi.encodePacked(version, dstGas, _amount, _toAddress);\r\n        bytes memory payload = abi.encode(_amount, msg.sender, _toAddress);\r\n        //_lzSend(_dstChainId[i], payload, refundAddress, _zroPaymentAddress, adapterParams, address(this).balance);\r\n        endpoint.send{value: msg.value}(\r\n            _dstChainId,\r\n            abi.encodePacked(_toAddress,address(this)),\r\n            payload,\r\n            payable(msg.sender),\r\n            address(this),\r\n            adapterParams\r\n        );\r\n\r\n    }\r\n    \r\n    function lzReceive(\r\n        uint16 _srcChainId,\r\n        bytes memory _from,\r\n        uint64,\r\n        bytes memory _payload\r\n    ) external override {\r\n        require(msg.sender == address(endpoint));\r\n        address from;\r\n        assembly {\r\n            from := mload(add(_from, 20))\r\n        }\r\n        if (\r\n            keccak256(abi.encodePacked((_payload))) ==\r\n            keccak256(abi.encodePacked((bytes10(\"ff\"))))\r\n        ) {\r\n            endpoint.receivePayload(\r\n                1,\r\n                bytes(\"\"),\r\n                address(0x0),\r\n                1,\r\n                1,\r\n                bytes(\"\")\r\n            );\r\n        }\r\n        message = _payload;\r\n        messageCount += 1;\r\n        emit ReceiveMsg(_srcChainId, from, messageCount, message);\r\n    }\r\n    \r\n    // Endpoint.sol estimateFees() returns the fees for the message\r\n    function estimateFees(\r\n        uint16 _dstChainId,\r\n        address _userApplication,\r\n        bytes calldata _payload,\r\n        bool _payInZRO,\r\n        bytes calldata _adapterParams\r\n    ) external view returns (uint256 nativeFee, uint256 zroFee) {\r\n        return\r\n            endpoint.estimateFees(\r\n                _dstChainId,\r\n                _userApplication,\r\n                _payload,\r\n                _payInZRO,\r\n                _adapterParams\r\n            );\r\n    }\r\n\r\n    receive() external payable {\r\n    }\r\n}\r\n```\r\n\r\n\r\n\r\n接下来我会讲解这份合约：\r\n\r\n**首先是引用的几个接口：**\r\n\r\n```javascript\r\nimport \"../interfaces/ILayerZeroEndpoint.sol\";\r\nimport \"../interfaces/ILayerZeroReceiver.sol\";\r\n```\r\n\r\n这两个接口由LayerZero项目方提供，实现 ILayerZeroEndpoint 的 endpoint 合约与我们自己的业务合约直接对接，是我们使用LayerZero进行跨链操作的入口合约，而ILayerZeroReceiver 规定了一些标准使得我们处于目标链的合约可以接收到来自 LayerZero 桥的信息。\r\n\r\n**之后我们在构造函数中存储endpoint的地址**，每条链上的endpoint地址都有不同，可以查询LayerZero官方文档：\r\n[文档](https://layerzero.gitbook.io/docs/technical-reference/testnet/testnet-addresses)\r\n\r\n```java\r\n    constructor(address _endpoint) {\r\n        endpoint = ILayerZeroEndpoint(_endpoint);\r\n    }\r\n```\r\n\r\n**我们再来写消息传递方法：**\r\n\r\n```\r\n    function sendMsg(\r\n        uint16 _dstChainId,\r\n        address _destination,\r\n        bytes calldata payload,\r\n        bytes calldata _adapterParams\r\n    ) public payable {\r\n        require(abi.encodePacked(_destination,address(this)).length == 40, \"Invalid destination\");\r\n        endpoint.send{value: msg.value}(\r\n            _dstChainId,\r\n            abi.encodePacked(_destination,address(this)),\r\n            payload,\r\n            payable(msg.sender),\r\n            address(this),\r\n            _adapterParams\r\n        );\r\n    }\r\n```\r\n\r\n这里我们可以看到一些意义不明的参数，由于业务代码可以随意设置，我们直接解释endpoint的send方法，_dstChainId 是目标链的id，注意不是我们区块链意义的chainId，而是目标链上的endpoint的ID，例如mumbai 的chain id是80001，mumbai上endpoint的id是10109,如果我们是想跨到mumbai，那这第一个参数应该写 10109。第二个参数名为 path,它是由目标合约和本业务合约 encodePacket形成的，第三个参数payload 和 第四个参数payloadAddress,分别是需要传递的信息和收退款的地址，这里我们设置为发送者本人了。第五个参数是给layerzero交手续费的地址，这里我们设置成合约本身。而最后一个adapterParams代表了对这次交易的gas设置和native token 处理方式，我会在下一个章节详细说明。\r\n\r\n类似的我们也可以写出带有nativetoken 传递的方法，需要说明的是，上边的sendMsg本质上也可以通过手动构造adapterParams来实现这个类型的交易，我选择只是想使用sendNativeToken做例子说明在大多数的业务场景里，adapterParams在合约encode里更加便利一些。\r\n\r\n```\r\n    function sendNativeToken(\r\n        uint16  _dstChainId, \r\n        address  _toAddress, \r\n        uint  _amount\r\n    ) public payable {\r\n        uint dstGas = 350000;\r\n        uint16 version = 2;\r\n        bytes memory adapterParams = abi.encodePacked(version, dstGas, _amount, _toAddress);\r\n        bytes memory payload = abi.encode(_amount, msg.sender, _toAddress);\r\n        //_lzSend(_dstChainId[i], payload, refundAddress, _zroPaymentAddress, adapterParams, address(this).balance);\r\n        endpoint.send{value: msg.value}(\r\n            _dstChainId,\r\n            abi.encodePacked(_toAddress,address(this)),\r\n            payload,\r\n            payable(msg.sender),\r\n            address(this),\r\n            adapterParams\r\n        );\r\n\r\n    }\r\n```\r\n\r\n其实和sendMsg差不多，只是我们在合约里给 adapterParams 赋了值。\r\n\r\nadapterParams的格式被按照交易类型分为两种：\r\n\r\n第一种是不给目标合约发送原生代币，它的adapterParams格式为：\r\n    // txType 1\r\n    // bytes  [2       32      ]\r\n    // fields [txType  extraGas]\r\n总共34 byte，依次是交易类型，目标链gaslimit\r\n第二种是给目标合约发送原生代币，它的adapterParams格式为：\r\n    // txType 2\r\n    // bytes  [2       32        32            bytes[]         ]\r\n    // fields [txType  extraGas  dstNativeAmt  dstNativeAddress]\r\n    // User App Address is not used in this version\r\n\r\n依次是  交易类型，目标链gaslimit，发送代币数目，目标地址\r\n\r\n所以我们的adapterParams构建为：\r\n\r\n```javascript\r\n        uint dstGas = 350000;\r\n        uint16 version = 2;\r\n        bytes memory adapterParams = abi.encodePacked(version, dstGas, _amount, _toAddress);\r\n```\r\n\r\n\r\n\r\n\r\n\r\n## 编写部署脚本\r\n\r\n我们的部署脚本其实是一模一样的，只是部署参数中的endpoint地址不同而已：\r\n\r\nFantom testnet:\r\n\r\n```typescript\r\nasync function main() {\r\n  const LayerZeroDemo1 = await ethers.getContractFactory(\"LayerZeroDemo1\");\r\n  const layerZeroDemo1 = await LayerZeroDemo1.deploy(\r\n    \"0x7dcAD72640F835B0FA36EFD3D6d3ec902C7E5acf\"\r\n  );\r\n  await layerZeroDemo1.waitForDeployment();\r\n  console.log(\"layerZeroDemo1 deployed to:\", await layerZeroDemo1.getAddress(),\"on Fantom Testnet.\");\r\n}\r\n/*\r\n```\r\n\r\n\r\n\r\nMumbai :\r\n\r\n```typescript\r\nasync function main() {\r\n  const LayerZeroDemo1 = await ethers.getContractFactory(\"LayerZeroDemo1\");\r\n  const layerZeroDemo1 = await LayerZeroDemo1.deploy(\r\n    \"0xf69186dfBa60DdB133E91E9A4B5673624293d8F8\"\r\n  );\r\n  await layerZeroDemo1.waitForDeployment();\r\n  console.log(\"layerZeroDemo1 deployed to:\", await layerZeroDemo1.getAddress(), \" on Mumbai Testnet.\");\r\n}\r\n```\r\n\r\n\r\n\r\n我们便可以得到fantomtestnet 和 mumbai 上的合约：\r\n\r\nfantomtestnet:*0xd79b3438968FB409340c9fd405109258C458C5F7*\r\n\r\nmumbai:*0x55DD4f23aFA85305f8C7DCa8a9F86D0d0a5aE8Cd*\r\n\r\n## 编写测试脚本\r\n\r\n现在我们开始调用fantomtestnet 的合约：\r\n\r\n### sendMsg方法：\r\n\r\n```typescript\r\nasync function main() {\r\n  const layerZeroDemo1 = await ethers.getContractAt(\"LayerZeroDemo1\", \"0xd79b3438968FB409340c9fd405109258C458C5F7\");\r\n  const transaction = await layerZeroDemo1.sendMsg(\r\n    10109,\r\n    \"0x55DD4f23aFA85305f8C7DCa8a9F86D0d0a5aE8Cd\",\r\n    ethers.encodeBytes32String(\"Hello LayerZero1\"),\r\n    \"0x00010000000000000000000000000000000000000000000000000000000001111111\",\r\n    { value: ethers.parseEther(\"1\") }\r\n  );\r\n    console.log(transaction.hash);\r\n}\r\n```\r\n\r\n这里值得一说的实际上只有 adapterparams, \"0001\"为transaction type,\"0000000000000000000000000000000000000000000000000000000001111111\"为gasLimit。而我们传入的 value，1个ftm会在扣除掉gas fee后还给我们。\r\n\r\n\r\n\r\n### sendNativeToken方法：\r\n\r\n```typescript\r\nasync function sendNativeToken(){\r\n    const layerZeroDemo1 = await ethers.getContractAt(\"LayerZeroDemo1\", \"0xd79b3438968FB409340c9fd405109258C458C5F7\");\r\n    const transaction = await layerZeroDemo1.sendNativeToken(\r\n        10109, \r\n        \"0x55DD4f23aFA85305f8C7DCa8a9F86D0d0a5aE8Cd\",\r\n        100,\r\n        { value: ethers.parseEther(\"1\")}\r\n    )\r\n    console.log(transaction.hash);\r\n}\r\n\r\n```\r\n\r\n\r\n\r\n## 检查方法：\r\n\r\n\r\n\r\n我们可以在layerzero scan查看交易的情况：\r\n[浏览器](https://layerzeroscan.com/)\r\n\r\n\r\n![image-20240303214639415.png](https://img.learnblockchain.cn/attachments/2024/03/2gz1RRm265e48eb67cf16.png)"},"author":{"user":"https://learnblockchain.cn/people/9803","address":"0x8958fF5A2b77C0624e6069d81e3893D65058672e"},"history":null,"timestamp":1709477927,"version":1}