diff --git a/.changeset/honest-rats-cry.md b/.changeset/honest-rats-cry.md new file mode 100644 index 000000000..6f8b6430e --- /dev/null +++ b/.changeset/honest-rats-cry.md @@ -0,0 +1,5 @@ +--- +"@layerzerolabs/onft721-example": patch +--- + +Added unit tests for `MyONFT721.sol` example diff --git a/examples/onft721/package.json b/examples/onft721/package.json index d2db4855f..77b205af2 100644 --- a/examples/onft721/package.json +++ b/examples/onft721/package.json @@ -11,7 +11,7 @@ "lint:fix": "eslint --fix '**/*.{js,ts,json}' && prettier --write . && solhint 'contracts/**/*.sol' --fix --noPrompt", "lint:js": "eslint '**/*.{js,ts,json}' && prettier --check .", "lint:sol": "solhint 'contracts/**/*.sol'", - "test": "echo 'No tests yet'", + "test": "$npm_execpath run test:forge && $npm_execpath run test:hardhat", "test:forge": "forge test", "test:hardhat": "hardhat test" }, diff --git a/examples/onft721/test/foundry/MyONFT721.t.sol b/examples/onft721/test/foundry/MyONFT721.t.sol new file mode 100644 index 000000000..5cbe102ea --- /dev/null +++ b/examples/onft721/test/foundry/MyONFT721.t.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +// Mock imports +import { ONFT721Mock } from "../mocks/ONFT721Mock.sol"; +import { ONFT721ComposerMock } from "../mocks/ONFT721ComposerMock.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 { IONFT721, SendParam } from "@layerzerolabs/onft-evm/contracts/onft721/interfaces/IONFT721.sol"; +import { MessagingFee, MessagingReceipt } from "@layerzerolabs/onft-evm/contracts/onft721/ONFT721Core.sol"; +import { ONFT721MsgCodec } from "@layerzerolabs/onft-evm/contracts/onft721/libs/ONFT721MsgCodec.sol"; +import { ONFTComposeMsgCodec } from "@layerzerolabs/onft-evm/contracts/libs/ONFTComposeMsgCodec.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 MyONFT721Test is TestHelperOz5 { + using OptionsBuilder for bytes; + + uint32 private aEid = 1; + uint32 private bEid = 2; + + ONFT721Mock private aONFT721; + ONFT721Mock private bONFT721; + + address private userA = address(0x1); + address private userB = address(0x2); + uint256 private initialBalance = 100 ether; + + function setUp() public virtual override { + vm.deal(userA, 1000 ether); + vm.deal(userB, 1000 ether); + + super.setUp(); + setUpEndpoints(2, LibraryType.UltraLightNode); + + aONFT721 = ONFT721Mock( + _deployOApp( + type(ONFT721Mock).creationCode, + abi.encode("aONFT721", "aONFT721", address(endpoints[aEid]), address(this)) + ) + ); + + bONFT721 = ONFT721Mock( + _deployOApp( + type(ONFT721Mock).creationCode, + abi.encode("bONFT721", "bONFT721", address(endpoints[bEid]), address(this)) + ) + ); + + // config and wire the onfts + address[] memory onfts = new address[](2); + onfts[0] = address(aONFT721); + onfts[1] = address(bONFT721); + this.wireOApps(onfts); + + // mint tokens + aONFT721.mint(userA, 0); + } + + function test_constructor() public { + assertEq(aONFT721.owner(), address(this)); + assertEq(bONFT721.owner(), address(this)); + + assertEq(aONFT721.balanceOf(userA), 1); + assertEq(bONFT721.balanceOf(userB), 0); + + assertEq(aONFT721.token(), address(aONFT721)); + assertEq(bONFT721.token(), address(bONFT721)); + } + + function test_send_onft721() public { + uint256 tokenId = 0; + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); + SendParam memory sendParam = SendParam(bEid, addressToBytes32(userB), tokenId, options, "", ""); + MessagingFee memory fee = aONFT721.quoteSend(sendParam, false); + + assertEq(aONFT721.balanceOf(userA), 1); + assertEq(bONFT721.balanceOf(userB), 0); + + vm.prank(userA); + aONFT721.send{ value: fee.nativeFee }(sendParam, fee, payable(address(this))); + verifyPackets(bEid, addressToBytes32(address(bONFT721))); + + assertEq(aONFT721.balanceOf(userA), 0); + assertEq(bONFT721.balanceOf(userB), 1); + } + + function test_send_oft_compose_msg() public { + uint256 tokenIdToSend = 0; + + ONFT721ComposerMock composer = new ONFT721ComposerMock(); + + bytes memory options = OptionsBuilder + .newOptions() + .addExecutorLzReceiveOption(200000, 0) + .addExecutorLzComposeOption(0, 500000, 0); + bytes memory composeMsg = hex"1234"; + SendParam memory sendParam = SendParam( + bEid, + addressToBytes32(address(composer)), + tokenIdToSend, + options, + composeMsg, + "" + ); + MessagingFee memory fee = aONFT721.quoteSend(sendParam, false); + + assertEq(aONFT721.balanceOf(userA), 1); + assertEq(bONFT721.balanceOf(address(composer)), 0); + + vm.prank(userA); + MessagingReceipt memory msgReceipt = aONFT721.send{ value: fee.nativeFee }( + sendParam, + fee, + payable(address(this)) + ); + verifyPackets(bEid, addressToBytes32(address(bONFT721))); + + // lzCompose params + uint32 dstEid_ = bEid; + address from_ = address(bONFT721); + bytes memory options_ = options; + bytes32 guid_ = msgReceipt.guid; + address to_ = address(composer); + bytes memory composerMsg_ = ONFTComposeMsgCodec.encode( + msgReceipt.nonce, + aEid, + abi.encodePacked(addressToBytes32(userA), composeMsg) + ); + this.lzCompose(dstEid_, from_, options_, guid_, to_, composerMsg_); + + assertEq(aONFT721.balanceOf(userA), 0); + assertEq(bONFT721.balanceOf(address(composer)), 1); + + 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 + } +} diff --git a/examples/onft721/test/hardhat/MyONFT721.test.ts b/examples/onft721/test/hardhat/MyONFT721.test.ts new file mode 100644 index 000000000..eba0bb1a0 --- /dev/null +++ b/examples/onft721/test/hardhat/MyONFT721.test.ts @@ -0,0 +1,92 @@ +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('MyONFT721 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 MyONFT721: ContractFactory + let EndpointV2Mock: ContractFactory + let ownerA: SignerWithAddress + let ownerB: SignerWithAddress + let endpointOwner: SignerWithAddress + let myONFT721A: Contract + let myONFT721B: Contract + let mockEndpointV2A: Contract + let mockEndpointV2B: Contract + + // Before hook for setup that runs once before all tests in the block + before(async function () { + // Contract factory for our tested contract + // + // We are using a derived contract that exposes a mint() function for testing purposes + MyONFT721 = await ethers.getContractFactory('MyONFT721Mock') + + // 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 + mockEndpointV2A = await EndpointV2Mock.deploy(eidA) + mockEndpointV2B = await EndpointV2Mock.deploy(eidB) + + // Deploying two instances of MyOFT contract with different identifiers and linking them to the mock LZEndpoint + myONFT721A = await MyONFT721.deploy('aONFT721', 'aONFT721', mockEndpointV2A.address, ownerA.address) + myONFT721B = await MyONFT721.deploy('bONFT721', 'bONFT721', mockEndpointV2B.address, ownerB.address) + + // Setting destination endpoints in the LZEndpoint mock for each MyONFT721 instance + await mockEndpointV2A.setDestLzEndpoint(myONFT721B.address, mockEndpointV2B.address) + await mockEndpointV2B.setDestLzEndpoint(myONFT721A.address, mockEndpointV2A.address) + + // Setting each MyONFT721 instance as a peer of the other in the mock LZEndpoint + await myONFT721A.connect(ownerA).setPeer(eidB, ethers.utils.zeroPad(myONFT721B.address, 32)) + await myONFT721B.connect(ownerB).setPeer(eidA, ethers.utils.zeroPad(myONFT721A.address, 32)) + }) + + // A test case to verify token transfer functionality + it('should send a token from A address to B address', async function () { + // Minting an initial amount of tokens to ownerA's address in the myONFT721A contract + const initialTokenId = 0 + await myONFT721A.mint(ownerA.address, initialTokenId) + + // Defining extra message execution options for the send operation + const options = Options.newOptions().addExecutorLzReceiveOption(200000, 0).toHex().toString() + + const sendParam = [eidB, ethers.utils.zeroPad(ownerB.address, 32), initialTokenId, options, '0x', '0x'] + + // Fetching the native fee for the token send operation + const [nativeFee] = await myONFT721A.quoteSend(sendParam, false) + + // Executing the send operation from myONFT721A contract + await myONFT721A.send(sendParam, [nativeFee, 0], ownerA.address, { value: nativeFee }) + + // Fetching the final token balances of ownerA and ownerB + const finalBalanceA = await myONFT721A.balanceOf(ownerA.address) + const finalBalanceB = await myONFT721B.balanceOf(ownerB.address) + + // Asserting that the final balances are as expected after the send operation + expect(finalBalanceA).eql(ethers.BigNumber.from(0)) + expect(finalBalanceB).eql(ethers.BigNumber.from(1)) + }) +}) diff --git a/examples/onft721/test/mocks/ONFT721ComposerMock.sol b/examples/onft721/test/mocks/ONFT721ComposerMock.sol new file mode 100644 index 000000000..a17caade1 --- /dev/null +++ b/examples/onft721/test/mocks/ONFT721ComposerMock.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { IOAppComposer } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppComposer.sol"; + +contract ONFT721ComposerMock is IOAppComposer { + // default empty values for testing a lzCompose received message + address public from; + bytes32 public guid; + bytes public message; + address public executor; + bytes public extraData; + + function lzCompose( + address _from, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata /*_extraData*/ + ) external payable { + from = _from; + guid = _guid; + message = _message; + executor = _executor; + extraData = _message; + } +} diff --git a/examples/onft721/test/mocks/ONFT721Mock.sol b/examples/onft721/test/mocks/ONFT721Mock.sol new file mode 100644 index 000000000..fbaafc407 --- /dev/null +++ b/examples/onft721/test/mocks/ONFT721Mock.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { ONFT721 } from "@layerzerolabs/onft-evm/contracts/onft721/ONFT721.sol"; +import { SendParam } from "@layerzerolabs/onft-evm/contracts/onft721/interfaces/IONFT721.sol"; + +contract ONFT721Mock is ONFT721 { + constructor( + string memory _name, + string memory _symbol, + address _lzEndpoint, + address _delegate + ) ONFT721(_name, _symbol, _lzEndpoint, _delegate) {} + + function mint(address _to, uint256 _tokenId) public { + _mint(_to, _tokenId); + } + + // @dev expose internal functions for testing purposes + function debit(uint256 _tokenId, uint32 _dstEid) public { + _debit(msg.sender, _tokenId, _dstEid); + } + + function credit(address _to, uint256 _tokenId, uint32 _srcEid) public { + _credit(_to, _tokenId, _srcEid); + } + + function buildMsgAndOptions( + SendParam calldata _sendParam + ) public view returns (bytes memory message, bytes memory options) { + return _buildMsgAndOptions(_sendParam); + } +}