-
EndpointV2Mock.sol: a mock LayerZero V2 Endpoint contract, meant for local testing of LayerZero message passing. -
TestHelper.sol: an extensive LayerZero V2 testing framework, designed for simulating LayerZero state changes and contract interactions.
test command using your package manager:
Report incorrect code
Copy
Ask AI
pnpm test
Example Hardhat Test
Report incorrect code
Copy
Ask AI
import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers';
import {expect} from 'chai';
import {Contract, ContractFactory} from 'ethers';
import {deployments, ethers} from 'hardhat';
import {Options} from '@layerzerolabs/lz-v2-utilities';
describe('MyOFT Test', function () {
// Constant representing a mock Endpoint ID for testing purposes
const eidA = 1;
const eidB = 2;
// Declaration of variables to be used in the test suite
let MyOFT: ContractFactory;
let EndpointV2Mock: ContractFactory;
let ownerA: SignerWithAddress;
let ownerB: SignerWithAddress;
let endpointOwner: SignerWithAddress;
let myOFTA: Contract;
let myOFTB: Contract;
let mockEndpointA: Contract;
let mockEndpointB: Contract;
// Before hook for setup that runs once before all tests in the block
before(async function () {
// Contract factory for our tested contract
MyOFT = await ethers.getContractFactory('MyOFT');
// Fetching the first three signers (accounts) from Hardhat's local Ethereum network
const signers = await ethers.getSigners();
ownerA = signers.at(0)!;
ownerB = signers.at(1)!;
endpointOwner = signers.at(2)!;
// The EndpointV2Mock contract comes from @layerzerolabs/test-devtools-evm-hardhat package
// and its artifacts are connected as external artifacts to this project
//
// Unfortunately, hardhat itself does not yet provide a way of connecting external artifacts
// so we rely on hardhat-deploy to create a ContractFactory for EndpointV2Mock
//
// See https://github.com/NomicFoundation/hardhat/issues/1040
const EndpointV2MockArtifact = await deployments.getArtifact('EndpointV2Mock');
EndpointV2Mock = new ContractFactory(
EndpointV2MockArtifact.abi,
EndpointV2MockArtifact.bytecode,
endpointOwner,
);
});
// beforeEach hook for setup that runs before each test in the block
beforeEach(async function () {
// Deploying a mock LZEndpoint with the given Endpoint ID
mockEndpointA = await EndpointV2Mock.deploy(eidA);
mockEndpointB = await EndpointV2Mock.deploy(eidB);
// Deploying two instances of MyOFT contract with different identifiers and linking them to the mock LZEndpoint
myOFTA = await MyOFT.deploy('aOFT', 'aOFT', mockEndpointA.address, ownerA.address);
myOFTB = await MyOFT.deploy('bOFT', 'bOFT', mockEndpointB.address, ownerB.address);
// Setting destination endpoints in the LZEndpoint mock for each MyOFT instance
await mockEndpointA.setDestLzEndpoint(myOFTB.address, mockEndpointB.address);
await mockEndpointB.setDestLzEndpoint(myOFTA.address, mockEndpointA.address);
// Setting each MyOFT instance as a peer of the other in the mock LZEndpoint
await myOFTA.connect(ownerA).setPeer(eidB, ethers.utils.zeroPad(myOFTB.address, 32));
await myOFTB.connect(ownerB).setPeer(eidA, ethers.utils.zeroPad(myOFTA.address, 32));
});
// A test case to verify token transfer functionality
it('should send a token from A address to B address via each OFT', async function () {
// Minting an initial amount of tokens to ownerA's address in the myOFTA contract
const initialAmount = ethers.utils.parseEther('100');
await myOFTA.mint(ownerA.address, initialAmount);
// Defining the amount of tokens to send and constructing the parameters for the send operation
const tokensToSend = ethers.utils.parseEther('1');
const sendParam = [eidB, ethers.utils.zeroPad(ownerB.address, 32), tokensToSend, tokensToSend];
// Defining extra message execution options for the send operation
const options = Options.newOptions().addExecutorLzReceiveOption(200000, 0).toHex().toString();
// Fetching the native fee for the token send operation
const [nativeFee] = await myOFTA.quoteSend(sendParam, options, false, `0x`, `0x`);
// Executing the send operation from myOFTA contract
await myOFTA.send(sendParam, options, [nativeFee, 0], ownerA.address, '0x', '0x', {
value: nativeFee,
});
// Fetching the final token balances of ownerA and ownerB
const finalBalanceA = await myOFTA.balanceOf(ownerA.address);
const finalBalanceB = await myOFTB.balanceOf(ownerB.address);
// Asserting that the final balances are as expected after the send operation
expect(finalBalanceA.eq(initialAmount.sub(tokensToSend))).to.be.true;
expect(finalBalanceB.eq(tokensToSend)).to.be.true;
});
});
Example Foundry Test
Report incorrect code
Copy
Ask AI
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
// Mock imports
import { OFTMock } from "../mocks/OFTMock.sol";
import { ERC20Mock } from "../mocks/ERC20Mock.sol";
import { OFTComposerMock } from "../mocks/OFTComposerMock.sol";
// OApp imports
import { IOAppOptionsType3, EnforcedOptionParam } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol";
import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol";
// OFT imports
import { IOFT, SendParam, OFTReceipt } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol";
import { MessagingFee, MessagingReceipt } from "@layerzerolabs/oft-evm/contracts/OFTCore.sol";
import { OFTMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTMsgCodec.sol";
import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol";
// OZ imports
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
// Forge imports
import "forge-std/console.sol";
// DevTools imports
import { TestHelperOz5 } from "@layerzerolabs/test-devtools-evm-foundry/contracts/TestHelperOz5.sol";
contract MyOFTTest is TestHelperOz5 {
using OptionsBuilder for bytes;
uint32 private aEid = 1;
uint32 private bEid = 2;
OFTMock private aOFT;
OFTMock private bOFT;
address private userA = address(0x1);
address private userB = address(0x2);
uint256 private initialBalance = 100 ether;
function setUp() public virtual override {
// Provide initial Ether balances to users for testing purposes
vm.deal(userA, 1000 ether);
vm.deal(userB, 1000 ether);
// Call the base setup function from the TestHelperOz5 contract
super.setUp();
// Initialize 2 endpoints, using UltraLightNode as the library type
setUpEndpoints(2, LibraryType.UltraLightNode);
// Deploy two instances of OFTMock for testing, associating them with respective endpoints
aOFT = OFTMock(
_deployOApp(type(OFTMock).creationCode, abi.encode("aOFT", "aOFT", address(endpoints[aEid]), address(this)))
);
bOFT = OFTMock(
_deployOApp(type(OFTMock).creationCode, abi.encode("bOFT", "bOFT", address(endpoints[bEid]), address(this)))
);
// Configure and wire the OFTs together
address[] memory ofts = new address[](2);
ofts[0] = address(aOFT);
ofts[1] = address(bOFT);
this.wireOApps(ofts);
// Mint initial tokens for userA and userB
aOFT.mint(userA, initialBalance);
bOFT.mint(userB, initialBalance);
}
// Test the constructor to ensure initial setup and state are correct
function test_constructor() public {
// Check that the contract owner is correctly set
assertEq(aOFT.owner(), address(this));
assertEq(bOFT.owner(), address(this));
// Verify initial token balances for userA and userB
assertEq(aOFT.balanceOf(userA), initialBalance);
assertEq(bOFT.balanceOf(userB), initialBalance);
// Verify that the token address is correctly set to the respective OFT instances
assertEq(aOFT.token(), address(aOFT));
assertEq(bOFT.token(), address(bOFT));
}
// Test sending OFT tokens from one user to another
function test_send_oft() public {
uint256 tokensToSend = 1 ether;
// Build options for the send operation
bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0);
// Set up parameters for the send operation
SendParam memory sendParam = SendParam(
bEid,
addressToBytes32(userB),
tokensToSend,
tokensToSend,
options,
"",
""
);
// Quote the fee for sending tokens
MessagingFee memory fee = aOFT.quoteSend(sendParam, false);
// Verify initial balances before the send operation
assertEq(aOFT.balanceOf(userA), initialBalance);
assertEq(bOFT.balanceOf(userB), initialBalance);
// Perform the send operation
vm.prank(userA);
aOFT.send{ value: fee.nativeFee }(sendParam, fee, payable(address(this)));
// Verify that the packets were correctly sent to the destination chain.
// @param _dstEid The endpoint ID of the destination chain.
// @param _dstAddress The OApp address on the destination chain.
verifyPackets(bEid, addressToBytes32(address(bOFT)));
// Check balances after the send operation
assertEq(aOFT.balanceOf(userA), initialBalance - tokensToSend);
assertEq(bOFT.balanceOf(userB), initialBalance + tokensToSend);
}
// Test sending OFT tokens with a composed message
function test_send_oft_compose_msg() public {
uint256 tokensToSend = 1 ether;
// Create an instance of the OFTComposerMock contract
OFTComposerMock composer = new OFTComposerMock();
// Build options for the send operation with a composed message
bytes memory options = OptionsBuilder
.newOptions()
.addExecutorLzReceiveOption(200000, 0)
.addExecutorLzComposeOption(0, 500000, 0);
bytes memory composeMsg = hex"1234";
// Set up parameters for the send operation
SendParam memory sendParam = SendParam(
bEid,
addressToBytes32(address(composer)),
tokensToSend,
tokensToSend,
options,
composeMsg,
""
);
// Quote the fee for sending tokens
MessagingFee memory fee = aOFT.quoteSend(sendParam, false);
// Verify initial balances before the send operation
assertEq(aOFT.balanceOf(userA), initialBalance);
assertEq(bOFT.balanceOf(address(composer)), 0);
// Perform the send operation
vm.prank(userA);
(MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) = aOFT.send{ value: fee.nativeFee }(
sendParam,
fee,
payable(address(this))
);
// Verify that the packets were correctly sent to the destination chain.
// @param _dstEid The endpoint ID of the destination chain.
// @param _dstAddress The OApp address on the destination chain.
verifyPackets(bEid, addressToBytes32(address(bOFT)));
// Set up parameters for the composed message
uint32 dstEid_ = bEid;
address from_ = address(bOFT);
bytes memory options_ = options;
bytes32 guid_ = msgReceipt.guid;
address to_ = address(composer);
bytes memory composerMsg_ = OFTComposeMsgCodec.encode(
msgReceipt.nonce,
aEid,
oftReceipt.amountReceivedLD,
abi.encodePacked(addressToBytes32(userA), composeMsg)
);
// Execute the composed message
this.lzCompose(dstEid_, from_, options_, guid_, to_, composerMsg_);
// Check balances after the send operation
assertEq(aOFT.balanceOf(userA), initialBalance - tokensToSend);
assertEq(bOFT.balanceOf(address(composer)), tokensToSend);
// Verify the state of the composer contract
assertEq(composer.from(), from_);
assertEq(composer.guid(), guid_);
assertEq(composer.message(), composerMsg_);
assertEq(composer.executor(), address(this));
assertEq(composer.extraData(), composerMsg_); // default to setting the extraData to the message as well to test
}
}