Testing Contracts
The LayerZero sample project supports unit testing using both the hardhat and foundry forge development framework, with specific test helpers for each:
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.
To run your unit tests for both Hardhat and Foundry, run the test
command using your package manager:
pnpm test
Example Hardhat Test
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
// 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
}
}