diff --git a/contracts/XCMTeleportComposer.sol b/contracts/XCMTeleportComposer.sol new file mode 100644 index 0000000..a898d00 --- /dev/null +++ b/contracts/XCMTeleportComposer.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import { IOAppComposer } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppComposer.sol"; +import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol"; + +import { IXCMTeleport } from "./interfaces/IXCMTeleport.sol"; + +/** + * @notice The message expected by the Composer. + */ +struct ComposerMessage { + bytes32 receiver; + uint256 deliveryFeeLimit; +} + +/** + * @title XCMTeleportComposer + * @notice Demonstrates the minimum `IOAppComposer` interface necessary to receive composed messages via LayerZero. + * @dev Implements the `lzCompose` function to process incoming composed messages. + */ +contract XCMTeleportComposer is IOAppComposer { + /** + * @notice Address of the LayerZero Endpoint. + */ + address public immutable endpoint; + + /** + * @notice Address of the OFT contract that is sending the composed message. + */ + address public immutable oft; + + /** + * @notice Address of the XCM teleport precompile. + */ + IXCMTeleport public immutable xcmTeleportPrecompile; + + /** + * @notice Constructs the contract and initializes state variables. + * @dev Stores the LayerZero Endpoint and OApp addresses. + * + * @param _endpoint The address of the LayerZero Endpoint. + * @param _oft The address of the OFT contract that is sending composed messages. + * @param _xcmTeleportPrecompile The address of the XCM teleport precompile contract. + */ + constructor(address _endpoint, address _oft, address _xcmTeleportPrecompile) { + endpoint = _endpoint; + oft = _oft; + xcmTeleportPrecompile = IXCMTeleport(_xcmTeleportPrecompile); + } + + receive() external payable { + require(msg.sender == oft, "XCMTeleportComposer: Does not accept ether"); + } + + /** + * @notice Handles incoming composed messages from LayerZero. + * @dev Ensures the message comes from the correct OApp and is sent through the authorized endpoint. + * + * @param _oft The address of the OFT contract that is sending the composed message. + */ + function lzCompose( + address _oft, + bytes32, //_guid + bytes calldata _message, + address, //_executor + bytes calldata //_extraData + ) external payable override { + // Ensure the composed message comes from the correct OApp. + require(_oft == oft, "XCMTeleportComposer: Invalid OApp"); + require(msg.sender == endpoint, "XCMTeleportComposer: Unauthorized sender"); + + // Decode the amount in local decimals being transferred. + uint256 _amountLD = OFTComposeMsgCodec.amountLD(_message); + + // Decode the actual `composeMsg` payload. + bytes memory _actualComposeMsg = OFTComposeMsgCodec.composeMsg(_message); + ComposerMessage memory _composerMessage = abi.decode(_actualComposeMsg, (ComposerMessage)); + bytes32 _receiver = _composerMessage.receiver; + uint256 _deliveryFeeLimit = _composerMessage.deliveryFeeLimit; + + // Call the XCM precompile to get the XCM delivery fee. + uint256 _deliveryFee = xcmTeleportPrecompile.deliveryFee(_receiver, _amountLD + msg.value); + require(_deliveryFee <= _deliveryFeeLimit + msg.value, "XCMTeleportComposer: XCM fee limit exceeded"); + + // Call the XCM precompile to execute the teleport. + xcmTeleportPrecompile.teleportToRelayChain(_receiver, _amountLD + msg.value - _deliveryFee); + } +} diff --git a/contracts/interfaces/IXCMTeleport.sol b/contracts/interfaces/IXCMTeleport.sol new file mode 100644 index 0000000..8788fd3 --- /dev/null +++ b/contracts/interfaces/IXCMTeleport.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +interface IXCMTeleport { + function teleportToRelayChain(bytes32 _receiver, uint256 _amountLD) external; + function deliveryFee(bytes32 _receiver, uint256 _amountLD) external returns (uint256); +} diff --git a/contracts/mocks/XCMTeleportPrecompileMock.sol b/contracts/mocks/XCMTeleportPrecompileMock.sol new file mode 100644 index 0000000..e145755 --- /dev/null +++ b/contracts/mocks/XCMTeleportPrecompileMock.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import { IXCMTeleport } from "../interfaces/IXCMTeleport.sol"; + +// @dev WARNING: This is for testing purposes only +contract XCMTeleportPrecompileMock is IXCMTeleport { + bytes32 public receiver; + uint256 public amountLD; + uint256 public currentDeliveryFee; + + function teleportToRelayChain(bytes32 _receiver, uint256 _amountLD) external override { + receiver = _receiver; + amountLD = _amountLD; + } + + function deliveryFee(bytes32 _receiver, uint256 _amountLD) external override returns (uint256) { + receiver = _receiver; + amountLD = _amountLD; + return currentDeliveryFee; + } + + function setDeliveryFee(uint256 _deliveryFee) external { + currentDeliveryFee = _deliveryFee; + } +} diff --git a/deploy/XCMTeleportComposer.ts b/deploy/XCMTeleportComposer.ts new file mode 100644 index 0000000..8497ff2 --- /dev/null +++ b/deploy/XCMTeleportComposer.ts @@ -0,0 +1,54 @@ +import assert from 'assert' + +import { type DeployFunction } from 'hardhat-deploy/types' + +const contractName = 'XCMTeleportComposer' + +const deploy: DeployFunction = async (hre) => { + const { getNamedAccounts, deployments } = hre + + const { deploy } = deployments + const { deployer } = await getNamedAccounts() + + assert(deployer, 'Missing named deployer account') + + console.log(`Network: ${hre.network.name}`) + console.log(`Deployer: ${deployer}`) + + // This is an external deployment pulled in from @layerzerolabs/lz-evm-sdk-v2 + // + // @layerzerolabs/toolbox-hardhat takes care of plugging in the external deployments + // from @layerzerolabs packages based on the configuration in your hardhat config + // + // For this to work correctly, your network config must define an eid property + // set to `EndpointId` as defined in @layerzerolabs/lz-definitions + // + // For example: + // + // networks: { + // fuji: { + // ... + // eid: EndpointId.AVALANCHE_V2_TESTNET + // } + // } + const endpointV2Deployment = await hre.deployments.get('EndpointV2') + const zkVerifyOFTAdapterDeployment = await hre.deployments.get('ZkVerifyOFTAdapter') + const xcmTeleportPrecompileAddress = '0x000000000000000000000000000000000000080C' + + const { address } = await deploy(contractName, { + from: deployer, + args: [ + endpointV2Deployment.address, // LayerZero's EndpointV2 address + zkVerifyOFTAdapterDeployment.address, // ZkVerifyOFTAdapter address + xcmTeleportPrecompileAddress, // XCM Teleport precompile address + ], + log: true, + skipIfAlreadyDeployed: false, + }) + + console.log(`Deployed contract: ${contractName}, network: ${hre.network.name}, address: ${address}`) +} + +deploy.tags = [contractName] + +export default deploy diff --git a/test/foundry/XCMTeleportComposer.t.sol b/test/foundry/XCMTeleportComposer.t.sol new file mode 100644 index 0000000..2b79661 --- /dev/null +++ b/test/foundry/XCMTeleportComposer.t.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +// Mock imports +import { ZkVerifyOFTAdapterMock } from "../../contracts/mocks/ZkVerifyOFTAdapterMock.sol"; +import { ZkVerifyTokenMock } from "../../contracts/mocks/ZkVerifyTokenMock.sol"; +import { XCMTeleportPrecompileMock } from "../../contracts/mocks/XCMTeleportPrecompileMock.sol"; + +// Contract imports +import { XCMTeleportComposer } from "../../contracts/XCMTeleportComposer.sol"; + +// OApp imports +import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; + +// OFT imports +import { SendParam, OFTReceipt } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol"; +import { MessagingFee, MessagingReceipt } from "@layerzerolabs/oft-evm/contracts/OFTCore.sol"; +import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol"; + +// Forge imports +import "forge-std/console.sol"; + +// DevTools imports +import { TestHelperOz5 } from "@layerzerolabs/test-devtools-evm-foundry/contracts/TestHelperOz5.sol"; + +contract XCMTeleportComposerTest is TestHelperOz5 { + using OptionsBuilder for bytes; + + ZkVerifyOFTAdapterMock private nativeOFTAdapter; + ZkVerifyTokenMock private extOFT; + XCMTeleportPrecompileMock private xcmTeleportPrecompileMock; + XCMTeleportComposer private xcmTeleportComposer; + + uint32 private constant vflowEid = 1; + uint32 private constant extEid = 2; + address private constant userA = address(0x1); + address private constant userB = address(0x2); + uint256 private constant initialNativeBalance = 1000 ether; + + function setUp() public virtual override { + vm.deal(userA, initialNativeBalance); + vm.deal(userB, initialNativeBalance); + + super.setUp(); + setUpEndpoints(2, LibraryType.UltraLightNode); + + nativeOFTAdapter = ZkVerifyOFTAdapterMock( + _deployOApp( + type(ZkVerifyOFTAdapterMock).creationCode, + abi.encode(18, address(endpoints[vflowEid]), address(this)) + ) + ); + + extOFT = ZkVerifyTokenMock( + _deployOApp( + type(ZkVerifyTokenMock).creationCode, + abi.encode("Token", "TKN", address(endpoints[extEid]), address(this)) + ) + ); + + xcmTeleportPrecompileMock = new XCMTeleportPrecompileMock(); + + xcmTeleportComposer = new XCMTeleportComposer( + address(endpoints[vflowEid]), + address(nativeOFTAdapter), + address(xcmTeleportPrecompileMock) + ); + + // config and wire + address[] memory ofts = new address[](2); + ofts[0] = address(nativeOFTAdapter); + ofts[1] = address(extOFT); + this.wireOApps(ofts); + + bridge_from_vflow_to_ext(userA, userB, 100 ether); + } + + function bridge_from_vflow_to_ext(address from, address to, uint256 tokensToSend) public { + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); + SendParam memory sendParam = SendParam( + extEid, + addressToBytes32(to), + tokensToSend, + tokensToSend, + options, + "", + "" + ); + MessagingFee memory fee = nativeOFTAdapter.quoteSend(sendParam, false); + + vm.prank(from); + nativeOFTAdapter.send{ value: fee.nativeFee + tokensToSend }(sendParam, fee, payable(address(this))); + verifyPackets(extEid, addressToBytes32(address(extOFT))); + } + + function test_send_native_oft_adapter_compose_msg() public { + bytes memory options = OptionsBuilder + .newOptions() + .addExecutorLzReceiveOption(100_000, 0) + .addExecutorLzComposeOption(0, 100_000, 0); + bytes32 receiver = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + uint256 maxFee = 1 ether; + bytes memory composeMsg = abi.encode(receiver, maxFee); + SendParam memory sendParam = SendParam( + vflowEid, + addressToBytes32(address(xcmTeleportComposer)), + 1 ether, + 1 ether, + options, + composeMsg, + "" + ); + MessagingFee memory fee = extOFT.quoteSend(sendParam, false); + + assertEq(userB.balance, initialNativeBalance); + assertEq(extOFT.balanceOf(userB), 100 ether); + assertEq(address(xcmTeleportComposer).balance, 0 ether); + + vm.prank(userB); + (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) = extOFT.send{ value: fee.nativeFee }( + sendParam, + fee, + payable(address(this)) + ); + verifyPackets(vflowEid, addressToBytes32(address(nativeOFTAdapter))); + + // lzCompose params + bytes memory composerMsg_ = OFTComposeMsgCodec.encode( + msgReceipt.nonce, + extEid, + oftReceipt.amountReceivedLD, + abi.encodePacked(addressToBytes32(userB), composeMsg) + ); + this.lzCompose( + vflowEid, + address(nativeOFTAdapter), + options, + msgReceipt.guid, + address(xcmTeleportComposer), + composerMsg_ + ); + + assertEq(userB.balance, initialNativeBalance - fee.nativeFee); + assertEq(extOFT.balanceOf(userB), 99 ether); + assertEq(address(xcmTeleportComposer).balance, 1 ether); + assertEq(xcmTeleportPrecompileMock.receiver(), receiver); + assertEq(xcmTeleportPrecompileMock.amountLD(), 1 ether); + } +}