Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions contracts/XCMTeleportComposer.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
7 changes: 7 additions & 0 deletions contracts/interfaces/IXCMTeleport.sol
Original file line number Diff line number Diff line change
@@ -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);
}
26 changes: 26 additions & 0 deletions contracts/mocks/XCMTeleportPrecompileMock.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
54 changes: 54 additions & 0 deletions deploy/XCMTeleportComposer.ts
Original file line number Diff line number Diff line change
@@ -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
149 changes: 149 additions & 0 deletions test/foundry/XCMTeleportComposer.t.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}