diff --git a/src/layerzero-v2/LayerZeroV2Helper.sol b/src/layerzero-v2/LayerZeroV2Helper.sol new file mode 100644 index 0000000..a1b66b7 --- /dev/null +++ b/src/layerzero-v2/LayerZeroV2Helper.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +struct Packet { + uint64 nonce; + uint32 srcEid; + address sender; + uint32 dstEid; + bytes32 receiver; + bytes32 guid; + bytes message; +} + +struct Origin { + uint32 srcEid; + bytes32 sender; + uint64 nonce; +} + +interface ILayerzeroV2Receiver { + function lzReceive( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) external payable; +} + +contract LayerZeroV2Helper is Test { + bytes32 constant PACKET_SELECTOR = 0x1ab700d4ced0c005b164c0f789fd09fcbb0156d4c2041b8a3bfbcd961cd1567f; + + function help(address endpoint, uint256 forkId, Vm.Log[] calldata logs) external { + _help(endpoint, forkId, PACKET_SELECTOR, logs); + } + + function _help(address endpoint, uint256 forkId, bytes32 eventSelector, Vm.Log[] memory logs) internal { + uint256 prevForkId = vm.activeFork(); + + for (uint256 i; i < logs.length; i++) { + Vm.Log memory log = logs[i]; + + if (log.topics[0] == eventSelector) { + (bytes memory payload,,) = abi.decode(log.data, (bytes, bytes, address)); + Packet memory packet = this.decodePacket(payload); + address receiver = address(uint160(uint256(packet.receiver))); + + vm.selectFork(forkId); + vm.prank(endpoint); + ILayerzeroV2Receiver(receiver).lzReceive( + Origin(packet.srcEid, bytes32(uint256(uint160(packet.sender))), packet.nonce), + packet.guid, + packet.message, + address(0), + bytes("") + ); + vm.selectFork(prevForkId); + } + } + } + + function decodePacket(bytes calldata encodedPacket) public pure returns (Packet memory) { + // Decode the packet header + uint8 version = uint8(encodedPacket[0]); + uint64 nonce = toUint64(encodedPacket, 1); + uint32 srcEid = toUint32(encodedPacket, 9); + address sender = toAddress(encodedPacket, 13); + uint32 dstEid = toUint32(encodedPacket, 45); + bytes32 receiver = toBytes32(encodedPacket, 49); + + // Decode the payload + bytes32 guid = toBytes32(encodedPacket, 81); + bytes memory message = encodedPacket[113:]; + + return Packet({ + nonce: nonce, + srcEid: srcEid, + sender: sender, + dstEid: dstEid, + receiver: receiver, + guid: guid, + message: message + }); + } + + function toUint64(bytes calldata data, uint256 offset) internal pure returns (uint64) { + require(offset + 8 <= data.length, "toUint64: out of bounds"); + return uint64(bytes8(data[offset:offset + 8])); + } + + function toUint32(bytes calldata data, uint256 offset) internal pure returns (uint32) { + require(offset + 4 <= data.length, "toUint32: out of bounds"); + return uint32(bytes4(data[offset:offset + 4])); + } + + function toAddress(bytes calldata data, uint256 offset) internal pure returns (address) { + require(offset + 20 <= data.length, "toAddress: out of bounds"); + return address(uint160(bytes20(data[offset + 12:offset + 32]))); + } + + function toBytes32(bytes calldata data, uint256 offset) internal pure returns (bytes32) { + require(offset + 32 <= data.length, "toBytes32: out of bounds"); + return bytes32(data[offset:offset + 32]); + } +} diff --git a/test/LayerZeroV2.t.sol b/test/LayerZeroV2.t.sol new file mode 100644 index 0000000..cd4675b --- /dev/null +++ b/test/LayerZeroV2.t.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import "forge-std/Test.sol"; + +import {LayerZeroV2Helper} from "src/layerzero-v2/LayerZeroV2Helper.sol"; + +struct MessagingParams { + uint32 dstEid; + bytes32 receiver; + bytes message; + bytes options; + bool payInLzToken; +} + +struct MessagingReceipt { + bytes32 guid; + uint64 nonce; + MessagingFee fee; +} + +struct MessagingFee { + uint256 nativeFee; + uint256 lzTokenFee; +} + +struct Origin { + uint32 srcEid; + bytes32 sender; + uint64 nonce; +} + +interface ILayerZeroV2Endpoint { + function send(MessagingParams calldata _params, address _refundAddress) + external + payable + returns (MessagingReceipt memory); + + function quote(MessagingParams calldata _params, address _sender) external view returns (MessagingFee memory); +} + +contract Target { + uint256 public value; + + function lzReceive( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) external payable { + value = abi.decode(_message, (uint256)); + } +} + +contract LayerZeroV2HelperTest is Test { + LayerZeroV2Helper lzHelper; + Target target; + + uint256 L1_FORK_ID; + uint256 POLYGON_FORK_ID; + + uint32 constant L1_ID = 30101; + uint32 constant POLYGON_ID = 30109; + + address constant lzV2Endpoint = 0x1a44076050125825900e736c501f859c50fE728c; + + string RPC_ETH_MAINNET = vm.envString("ETH_MAINNET_RPC_URL"); + string RPC_POLYGON_MAINNET = vm.envString("POLYGON_MAINNET_RPC_URL"); + + function setUp() external { + L1_FORK_ID = vm.createSelectFork(RPC_ETH_MAINNET, 19_730_754); + lzHelper = new LayerZeroV2Helper(); + + POLYGON_FORK_ID = vm.createSelectFork(RPC_POLYGON_MAINNET, 56_652_576); + target = new Target(); + } + + function testSimpleLzV2() external { + console.log(address(this)); + vm.selectFork(L1_FORK_ID); + + vm.recordLogs(); + _someCrossChainFunctionInYourContract(); + Vm.Log[] memory logs = vm.getRecordedLogs(); + lzHelper.help(lzV2Endpoint, POLYGON_FORK_ID, logs); + + vm.selectFork(POLYGON_FORK_ID); + assertEq(target.value(), 420); + } + + function _someCrossChainFunctionInYourContract() internal { + MessagingParams memory params = MessagingParams( + POLYGON_ID, + bytes32(uint256(uint160(address(target)))), + abi.encode(420), + abi.encodePacked(uint16(1), uint256(200_000)), + false + ); + + uint256 fees = ILayerZeroV2Endpoint(lzV2Endpoint).quote(params, address(this)).nativeFee; + ILayerZeroV2Endpoint(lzV2Endpoint).send{value: fees}(params, address(this)); + } +}