From 73b37d02adf8559e88afdc8096c8a25b11a10151 Mon Sep 17 00:00:00 2001 From: Matthew Krak Date: Fri, 19 Jan 2024 13:45:54 -0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=9A=20OApp=20example=20(#214)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jan Nanista --- .changeset/six-masks-roll.md | 7 + .changeset/thick-moons-invite.md | 6 + .eslintignore | 1 + .../actions/setup-environment/action.yaml | 3 + .gitmodules | 4 +- examples/oapp/.eslintignore | 5 + examples/oapp/.eslintrc.js | 5 + examples/oapp/.gitignore | 16 + examples/oapp/.prettierignore | 5 + examples/oapp/.prettierrc.js | 3 + examples/oapp/README.md | 27 + examples/oapp/contracts/MyOApp.sol | 66 +++ examples/oapp/deploy/MyOApp.ts | 47 ++ examples/oapp/foundry.toml | 11 + examples/oapp/hardhat.config.ts | 19 + examples/oapp/package.json | 47 ++ examples/oapp/solhint.config.js | 1 + examples/oapp/tasks/getSigners.ts | 21 + examples/oapp/tasks/index.ts | 2 + examples/oapp/test/foundry/MyOApp.t.sol | 63 +++ examples/oapp/test/foundry/OptionsHelper.sol | 95 ++++ examples/oapp/test/foundry/TestHelper.sol | 479 ++++++++++++++++++ .../test/foundry/mocks/ExecutorFeeLibMock.sol | 13 + .../test/foundry/mocks/SendUln302Mock.sol | 31 ++ .../foundry/mocks/SimpleMessageLibMock.sol | 19 + examples/oapp/test/hardhat/OApp.test.ts | 11 + examples/oapp/tsconfig.json | 14 + examples/oft/.eslintignore | 3 +- examples/oft/.prettierignore | 3 +- examples/oft/deploy/YourOFT.ts | 1 - examples/oft/foundry.toml | 5 +- examples/oft/package.json | 10 +- examples/oft/test/hardhat/OFT.test.ts | 8 +- packages/toolbox-foundry/.eslintignore | 4 +- packages/toolbox-foundry/.gitignore | 1 + packages/toolbox-foundry/.prettierignore | 4 +- packages/toolbox-foundry/DEVELOPMENT.md | 18 + packages/toolbox-foundry/Makefile | 31 ++ packages/toolbox-foundry/README.md | 4 +- packages/toolbox-foundry/package.json | 6 +- packages/toolbox-foundry/src/ds-test | 1 + packages/toolbox-foundry/src/forge-std | 1 + packages/toolbox-foundry/turbo.json | 8 + pnpm-lock.yaml | 100 +++- turbo.json | 2 +- 45 files changed, 1210 insertions(+), 21 deletions(-) create mode 100644 .changeset/six-masks-roll.md create mode 100644 .changeset/thick-moons-invite.md create mode 100644 examples/oapp/.eslintignore create mode 100644 examples/oapp/.eslintrc.js create mode 100644 examples/oapp/.gitignore create mode 100644 examples/oapp/.prettierignore create mode 100644 examples/oapp/.prettierrc.js create mode 100644 examples/oapp/README.md create mode 100644 examples/oapp/contracts/MyOApp.sol create mode 100644 examples/oapp/deploy/MyOApp.ts create mode 100644 examples/oapp/foundry.toml create mode 100644 examples/oapp/hardhat.config.ts create mode 100644 examples/oapp/package.json create mode 100644 examples/oapp/solhint.config.js create mode 100644 examples/oapp/tasks/getSigners.ts create mode 100644 examples/oapp/tasks/index.ts create mode 100644 examples/oapp/test/foundry/MyOApp.t.sol create mode 100644 examples/oapp/test/foundry/OptionsHelper.sol create mode 100644 examples/oapp/test/foundry/TestHelper.sol create mode 100644 examples/oapp/test/foundry/mocks/ExecutorFeeLibMock.sol create mode 100644 examples/oapp/test/foundry/mocks/SendUln302Mock.sol create mode 100644 examples/oapp/test/foundry/mocks/SimpleMessageLibMock.sol create mode 100644 examples/oapp/test/hardhat/OApp.test.ts create mode 100644 examples/oapp/tsconfig.json create mode 100644 packages/toolbox-foundry/.gitignore create mode 100644 packages/toolbox-foundry/Makefile create mode 160000 packages/toolbox-foundry/src/ds-test create mode 160000 packages/toolbox-foundry/src/forge-std create mode 100644 packages/toolbox-foundry/turbo.json diff --git a/.changeset/six-masks-roll.md b/.changeset/six-masks-roll.md new file mode 100644 index 000000000..e30a05c19 --- /dev/null +++ b/.changeset/six-masks-roll.md @@ -0,0 +1,7 @@ +--- +"@layerzerolabs/toolbox-foundry": patch +"@layerzerolabs/oapp-example": patch +"@layerzerolabs/oft-example": patch +--- + +Include solidity-bytes-utils in toolbox-foundry diff --git a/.changeset/thick-moons-invite.md b/.changeset/thick-moons-invite.md new file mode 100644 index 000000000..2b292fae9 --- /dev/null +++ b/.changeset/thick-moons-invite.md @@ -0,0 +1,6 @@ +--- +"@layerzerolabs/toolbox-foundry": patch +"@layerzerolabs/oapp-example": patch +--- + +Include forgotten libs in toolbox-foundry diff --git a/.eslintignore b/.eslintignore index 2f3073e4a..a2b84b2e5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,4 +4,5 @@ node_modules *.md *.sol *.toml +Makefile pnpm-lock.yaml diff --git a/.github/workflows/actions/setup-environment/action.yaml b/.github/workflows/actions/setup-environment/action.yaml index a57c83e2c..66aa51d76 100644 --- a/.github/workflows/actions/setup-environment/action.yaml +++ b/.github/workflows/actions/setup-environment/action.yaml @@ -14,3 +14,6 @@ runs: with: node-version-file: ".nvmrc" cache: "pnpm" + + - name: Setup Foundry + uses: foundry-rs/foundry-toolchain@v1 diff --git a/.gitmodules b/.gitmodules index 672e3ee5e..403988df8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "toolbox-foundry/forge-std"] - path = packages/toolbox-foundry/lib/forge-std + path = packages/toolbox-foundry/src/forge-std url = https://github.com/foundry-rs/forge-std [submodule "toolbox-foundry/ds-test"] - path = packages/toolbox-foundry/lib/ds-test + path = packages/toolbox-foundry/src/ds-test url = https://github.com/dapphub/ds-test diff --git a/examples/oapp/.eslintignore b/examples/oapp/.eslintignore new file mode 100644 index 000000000..1f6f67ad9 --- /dev/null +++ b/examples/oapp/.eslintignore @@ -0,0 +1,5 @@ +artifacts +cache +dist +node_modules +out \ No newline at end of file diff --git a/examples/oapp/.eslintrc.js b/examples/oapp/.eslintrc.js new file mode 100644 index 000000000..e1aec060d --- /dev/null +++ b/examples/oapp/.eslintrc.js @@ -0,0 +1,5 @@ +require('@rushstack/eslint-patch/modern-module-resolution'); + +module.exports = { + extends: ['@layerzerolabs/eslint-config-next/recommended'], +}; diff --git a/examples/oapp/.gitignore b/examples/oapp/.gitignore new file mode 100644 index 000000000..1b1ba0d01 --- /dev/null +++ b/examples/oapp/.gitignore @@ -0,0 +1,16 @@ +node_modules +.env +coverage +coverage.json +typechain +typechain-types + +# Hardhat files +cache +artifacts + +# foundry test compilation files +out + +# pnpm +pnpm-error.log diff --git a/examples/oapp/.prettierignore b/examples/oapp/.prettierignore new file mode 100644 index 000000000..d5e0b7749 --- /dev/null +++ b/examples/oapp/.prettierignore @@ -0,0 +1,5 @@ +artifacts/ +cache/ +dist/ +node_modules/ +out/ \ No newline at end of file diff --git a/examples/oapp/.prettierrc.js b/examples/oapp/.prettierrc.js new file mode 100644 index 000000000..6f55b4019 --- /dev/null +++ b/examples/oapp/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('@layerzerolabs/prettier-config-next'), +}; diff --git a/examples/oapp/README.md b/examples/oapp/README.md new file mode 100644 index 000000000..5c9a23cf0 --- /dev/null +++ b/examples/oapp/README.md @@ -0,0 +1,27 @@ +

+ + LayerZero + +

+ +

@layerzerolabs/oapp-example

+ +## Template repository for getting started with LayerZero using either Hardhat or Foundry in one project. + +### Getting Started + +#### Using Foundry + +```bash +forge install +forge build +forge test +``` + +#### Using Hardhat + +```bash +pnpm +pnpm hardhat compile +pnpm hardhat test +``` diff --git a/examples/oapp/contracts/MyOApp.sol b/examples/oapp/contracts/MyOApp.sol new file mode 100644 index 000000000..b16ed1c33 --- /dev/null +++ b/examples/oapp/contracts/MyOApp.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.22; + +import { OApp, MessagingFee, Origin } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/OApp.sol"; +import { MessagingReceipt } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/OAppSender.sol"; + +contract MyOApp is OApp { + constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) {} + + string public data = "Nothing received yet."; + + /** + * @notice Sends a message from the source chain to a destination chain. + * @param _dstEid The endpoint ID of the destination chain. + * @param _message The message string to be sent. + * @param _options Additional options for message execution. + * @dev Encodes the message as bytes and sends it using the `_lzSend` internal function. + * @return receipt A `MessagingReceipt` struct containing details of the message sent. + */ + function send( + uint32 _dstEid, + string memory _message, + bytes calldata _options + ) external payable returns (MessagingReceipt memory receipt) { + bytes memory _payload = abi.encode(_message); + receipt = _lzSend(_dstEid, _payload, _options, MessagingFee(msg.value, 0), payable(msg.sender)); + } + + /** + * @notice Quotes the gas needed to pay for the full omnichain transaction in native gas or ZRO token. + * @param _dstEid Destination chain's endpoint ID. + * @param _message The message. + * @param _options Message execution options (e.g., for sending gas to destination). + * @param _payInLzToken Whether to return fee in ZRO token. + */ + function quote( + uint32 _dstEid, + string memory _message, + bytes memory _options, + bool _payInLzToken + ) public view returns (MessagingFee memory fee) { + bytes memory payload = abi.encode(_message); + fee = _quote(_dstEid, payload, _options, _payInLzToken); + } + + /** + * @dev Internal function override to handle incoming messages from another chain. + * @param _origin A struct containing information about the message sender. + * @param _guid A unique global packet identifier for the message. + * @param payload The encoded message payload being received. + * @param _executor The address of the Executor responsible for processing the message. + * @param _extraData Arbitrary data appended by the Executor to the message. + * + * Decodes the received payload and processes it as per the business logic defined in the function. + */ + function _lzReceive( + Origin calldata _origin, + bytes32 _guid, + bytes calldata payload, + address _executor, + bytes calldata _extraData + ) internal override { + data = abi.decode(payload, (string)); + } +} diff --git a/examples/oapp/deploy/MyOApp.ts b/examples/oapp/deploy/MyOApp.ts new file mode 100644 index 000000000..7851718df --- /dev/null +++ b/examples/oapp/deploy/MyOApp.ts @@ -0,0 +1,47 @@ +import { type DeployFunction } from 'hardhat-deploy/types' + +// TODO declare your contract name here +const contractName = 'MyOApp' + +const deploy: DeployFunction = async (hre) => { + const { getNamedAccounts, deployments } = hre + + const { deploy } = deployments + const { deployer } = await getNamedAccounts() + + 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 { address } = await deploy(contractName, { + from: deployer, + args: [ + endpointV2Deployment.address, // LayerZero's EndpointV2 address + deployer, // owner + ], + log: true, + skipIfAlreadyDeployed: false, + }) + + console.log(`Deployed contract: ${contractName}, network: ${hre.network.name}, address: ${address}`) +} + +deploy.tags = [contractName] +export default deploy diff --git a/examples/oapp/foundry.toml b/examples/oapp/foundry.toml new file mode 100644 index 000000000..294e8bd76 --- /dev/null +++ b/examples/oapp/foundry.toml @@ -0,0 +1,11 @@ +[profile.default] +src = 'contracts' +out = 'out' +test = 'test/foundry' +cache_path = 'cache' +libs = ['node_modules', 'node_modules/@layerzerolabs/toolbox-foundry/lib'] + +remappings = [ + '@layerzerolabs/=node_modules/@layerzerolabs', + '@openzeppelin/=node_modules/@openzeppelin/', +] diff --git a/examples/oapp/hardhat.config.ts b/examples/oapp/hardhat.config.ts new file mode 100644 index 000000000..5b0e76136 --- /dev/null +++ b/examples/oapp/hardhat.config.ts @@ -0,0 +1,19 @@ +import 'hardhat-deploy' +import 'hardhat-contract-sizer' +import '@nomiclabs/hardhat-ethers' +import '@layerzerolabs/toolbox-hardhat' +import { HardhatUserConfig } from 'hardhat/types' + +import './tasks/' + +const config: HardhatUserConfig = { + solidity: '0.8.22', + + namedAccounts: { + deployer: { + default: 0, // wallet address of index[0], of the mnemonic in .env + }, + }, +} + +export default config diff --git a/examples/oapp/package.json b/examples/oapp/package.json new file mode 100644 index 000000000..e985e1a7b --- /dev/null +++ b/examples/oapp/package.json @@ -0,0 +1,47 @@ +{ + "name": "@layerzerolabs/oapp-example", + "version": "0.0.2", + "private": true, + "license": "MIT", + "scripts": { + "compile": "$npm_execpath compile:forge && $npm_execpath compile:hardhat", + "compile:forge": "forge build", + "compile:hardhat": "$npm_execpath hardhat compile", + "lint": "$npm_execpath lint:js && $npm_execpath lint:sol", + "lint:fix": "$npm_execpath prettier --write . && solhint 'contracts/**/*.sol' --fix --noPrompt", + "lint:js": "$npm_execpath eslint '**/*.js' && $npm_execpath prettier --check .", + "lint:sol": "solhint 'contracts/**/*.sol'", + "test": "$npm_execpath test:forge && $npm_execpath test:hardhat", + "test:forge": "forge test", + "test:hardhat": "$npm_execpath hardhat test" + }, + "devDependencies": { + "@babel/core": "^7.23.7", + "@layerzerolabs/eslint-config-next": "^2.0.7", + "@layerzerolabs/lz-definitions": "~2.0.7", + "@layerzerolabs/lz-evm-messagelib-v2": "~2.0.7", + "@layerzerolabs/lz-evm-oapp-v2": "~2.0.7", + "@layerzerolabs/lz-evm-protocol-v2": "~2.0.7", + "@layerzerolabs/lz-evm-v1-0.7": "~2.0.7", + "@layerzerolabs/prettier-config-next": "^2.0.7", + "@layerzerolabs/solhint-config": "^2.0.7", + "@layerzerolabs/toolbox-foundry": "~0.0.1", + "@layerzerolabs/toolbox-hardhat": "~0.0.3", + "@nomicfoundation/hardhat-ethers": "^3.0.5", + "@nomiclabs/hardhat-ethers": "^2.2.3", + "@openzeppelin/contracts": "^4.9.5", + "@openzeppelin/contracts-upgradeable": "^4.9.5", + "@rushstack/eslint-patch": "^1.6.1", + "@types/mocha": "^10.0.6", + "ethers": "^5.7.0", + "hardhat": "^2.19.4", + "hardhat-contract-sizer": "^2.10.0", + "hardhat-deploy": "^0.11.45", + "mocha": "^10.2.0", + "prettier": "^3.1.1", + "solhint": "^4.0.0", + "solidity-bytes-utils": "^0.8.1", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + } +} diff --git a/examples/oapp/solhint.config.js b/examples/oapp/solhint.config.js new file mode 100644 index 000000000..52efe629c --- /dev/null +++ b/examples/oapp/solhint.config.js @@ -0,0 +1 @@ +module.exports = require('@layerzerolabs/solhint-config'); diff --git a/examples/oapp/tasks/getSigners.ts b/examples/oapp/tasks/getSigners.ts new file mode 100644 index 000000000..db1d0c8ea --- /dev/null +++ b/examples/oapp/tasks/getSigners.ts @@ -0,0 +1,21 @@ +import { task, types } from 'hardhat/config' +import { type ActionType } from 'hardhat/types' + +// TODO Figure out a way so this doesnt need to be defined in two places +interface TaskArguments { + n: number +} + +const action: ActionType = async (taskArgs, hre) => { + const signers = await hre.ethers.getSigners() + for (let i = 0; i < taskArgs.n; ++i) { + console.log(`${i}) ${signers[i].address}`) + } +} + +task('getSigners', 'show the signers of the current mnemonic', action).addOptionalParam( + 'n', + 'how many to show', + 3, + types.int +) diff --git a/examples/oapp/tasks/index.ts b/examples/oapp/tasks/index.ts new file mode 100644 index 000000000..abd2a95aa --- /dev/null +++ b/examples/oapp/tasks/index.ts @@ -0,0 +1,2 @@ +import './getSigners' +// TODO get rid of index.ts somehow so we only need to define it in one place diff --git a/examples/oapp/test/foundry/MyOApp.t.sol b/examples/oapp/test/foundry/MyOApp.t.sol new file mode 100644 index 000000000..b8dfb037a --- /dev/null +++ b/examples/oapp/test/foundry/MyOApp.t.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.22; + +import { Packet } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ISendLib.sol"; +import { OptionsBuilder } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/libs/OptionsBuilder.sol"; +import { MessagingFee } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/OApp.sol"; +import { MessagingReceipt } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/OAppSender.sol"; +import { MyOApp } from "../../contracts/MyOApp.sol"; +import { TestHelper } from "./TestHelper.sol"; + +import "forge-std/console.sol"; + +/// @notice Unit test for MyOApp using the TestHelper. +/// @dev Inherits from TestHelper to utilize its setup and utility functions. +contract MyOAppTest is TestHelper { + using OptionsBuilder for bytes; + + // Declaration of mock endpoint IDs. + uint16 aEid = 1; + uint16 bEid = 2; + + // Declaration of mock contracts. + MyOApp aMyOApp; // OApp A + MyOApp bMyOApp; // OApp B + + /// @notice Calls setUp from TestHelper and initializes contract instances for testing. + function setUp() public virtual override { + super.setUp(); + + // Setup function to initialize 2 Mock Endpoints with Mock MessageLib. + setUpEndpoints(2, LibraryType.UltraLightNode); + + // Initializes 2 MyOApps; one on chain A, one on chain B. + address[] memory sender = setupOApps(type(MyOApp).creationCode, 1, 2); + aMyOApp = MyOApp(payable(sender[0])); + bMyOApp = MyOApp(payable(sender[1])); + } + + /// @notice Tests the send and multi-compose functionality of MyOApp. + /// @dev Simulates message passing from A -> B and checks for data integrity. + function test_send() public { + // Setup variable for data values before calling send(). + string memory dataBefore = aMyOApp.data(); + + // Generates 1 lzReceive execution option via the OptionsBuilder library. + // STEP 0: Estimating message gas fees via the quote function. + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(150000, 0); + MessagingFee memory fee = aMyOApp.quote(bEid, "test message", options, false); + + // STEP 1: Sending a message via the _lzSend() method. + MessagingReceipt memory receipt = aMyOApp.send{ value: fee.nativeFee }(bEid, "test message", options); + + // Asserting that the receiving OApps have NOT had data manipulated. + assertEq(bMyOApp.data(), dataBefore, "shouldn't be changed until lzReceive packet is verified"); + + // STEP 2 & 3: Deliver packet to bMyOApp manually. + verifyPackets(bEid, addressToBytes32(address(bMyOApp))); + + // Asserting that the data variable has updated in the receiving OApp. + assertEq(bMyOApp.data(), "test message", "lzReceive data assertion failure"); + } +} diff --git a/examples/oapp/test/foundry/OptionsHelper.sol b/examples/oapp/test/foundry/OptionsHelper.sol new file mode 100644 index 000000000..bf227cd5d --- /dev/null +++ b/examples/oapp/test/foundry/OptionsHelper.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.0; + +import { ExecutorOptions } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/ExecutorOptions.sol"; +import { UlnOptions } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/libs/UlnOptions.sol"; + +contract UlnOptionsMock { + using UlnOptions for bytes; + + function decode( + bytes calldata _options + ) public pure returns (bytes memory executorOptions, bytes memory dvnOptions) { + return UlnOptions.decode(_options); + } +} + +contract OptionsHelper { + UlnOptionsMock ulnOptions = new UlnOptionsMock(); + + function _parseExecutorLzReceiveOption(bytes memory _options) internal view returns (uint256 gas, uint256 value) { + (bool exist, bytes memory option) = _getExecutorOptionByOptionType( + _options, + ExecutorOptions.OPTION_TYPE_LZRECEIVE + ); + require(exist, "OptionsHelper: lzReceive option not found"); + (gas, value) = this.decodeLzReceiveOption(option); + } + + function _parseExecutorNativeDropOption( + bytes memory _options + ) internal view returns (uint256 amount, bytes32 receiver) { + (bool exist, bytes memory option) = _getExecutorOptionByOptionType( + _options, + ExecutorOptions.OPTION_TYPE_NATIVE_DROP + ); + require(exist, "OptionsHelper: nativeDrop option not found"); + (amount, receiver) = this.decodeNativeDropOption(option); + } + + function _parseExecutorLzComposeOption( + bytes memory _options + ) internal view returns (uint16 index, uint256 gas, uint256 value) { + (bool exist, bytes memory option) = _getExecutorOptionByOptionType( + _options, + ExecutorOptions.OPTION_TYPE_LZCOMPOSE + ); + require(exist, "OptionsHelper: lzCompose option not found"); + return this.decodeLzComposeOption(option); + } + + function _executorOptionExists( + bytes memory _options, + uint8 _executorOptionType + ) internal view returns (bool exist) { + (exist, ) = _getExecutorOptionByOptionType(_options, _executorOptionType); + } + + function _getExecutorOptionByOptionType( + bytes memory _options, + uint8 _executorOptionType + ) internal view returns (bool exist, bytes memory option) { + (bytes memory executorOpts, ) = ulnOptions.decode(_options); + + uint256 cursor; + while (cursor < executorOpts.length) { + (uint8 optionType, bytes memory op, uint256 nextCursor) = this.nextExecutorOption(executorOpts, cursor); + if (optionType == _executorOptionType) { + return (true, op); + } + cursor = nextCursor; + } + } + + function nextExecutorOption( + bytes calldata _options, + uint256 _cursor + ) external pure returns (uint8 optionType, bytes calldata option, uint256 cursor) { + return ExecutorOptions.nextExecutorOption(_options, _cursor); + } + + function decodeLzReceiveOption(bytes calldata _option) external pure returns (uint128 gas, uint128 value) { + return ExecutorOptions.decodeLzReceiveOption(_option); + } + + function decodeNativeDropOption(bytes calldata _option) external pure returns (uint128 amount, bytes32 receiver) { + return ExecutorOptions.decodeNativeDropOption(_option); + } + + function decodeLzComposeOption( + bytes calldata _option + ) external pure returns (uint16 index, uint128 gas, uint128 value) { + return ExecutorOptions.decodeLzComposeOption(_option); + } +} diff --git a/examples/oapp/test/foundry/TestHelper.sol b/examples/oapp/test/foundry/TestHelper.sol new file mode 100644 index 000000000..07b4b09e6 --- /dev/null +++ b/examples/oapp/test/foundry/TestHelper.sol @@ -0,0 +1,479 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.18; + +import { Test } from "forge-std/Test.sol"; +import { DoubleEndedQueue } from "@openzeppelin/contracts/utils/structs/DoubleEndedQueue.sol"; + +import { UlnConfig, SetDefaultUlnConfigParam } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol"; +import { SetDefaultExecutorConfigParam, ExecutorConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/SendLibBase.sol"; +import { ReceiveUln302 } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/uln302/ReceiveUln302.sol"; +import { DVN, ExecuteParam } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/dvn/DVN.sol"; +import { DVNFeeLib } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/dvn/DVNFeeLib.sol"; +import { IExecutor } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/interfaces/IExecutor.sol"; +import { Executor } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/Executor.sol"; +import { PriceFeed } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/PriceFeed.sol"; +import { ILayerZeroPriceFeed } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/interfaces/ILayerZeroPriceFeed.sol"; +import { IReceiveUlnE2 } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/interfaces/IReceiveUlnE2.sol"; +import { ReceiveUln302 } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/uln302/ReceiveUln302.sol"; +import { IMessageLib } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLib.sol"; +import { EndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/EndpointV2.sol"; +import { ExecutorOptions } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/ExecutorOptions.sol"; +import { PacketV1Codec } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/PacketV1Codec.sol"; +import { Origin } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +import { OApp } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/OApp.sol"; +import { OptionsBuilder } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/libs/OptionsBuilder.sol"; + +import { OptionsHelper } from "./OptionsHelper.sol"; +import { SendUln302Mock as SendUln302 } from "./mocks/SendUln302Mock.sol"; +import { SimpleMessageLibMock } from "./mocks/SimpleMessageLibMock.sol"; +import "./mocks/ExecutorFeeLibMock.sol"; +import "forge-std/console.sol"; + +/** + * @title TestHelper + * @notice Helper contract for setting up and managing LayerZero test environments. + * @dev Extends Foundry's Test contract and provides utility functions for setting up mock endpoints and OApps. + */ +contract TestHelper is Test, OptionsHelper { + using OptionsBuilder for bytes; + + enum LibraryType { + UltraLightNode, + SimpleMessageLib + } + + using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; + using PacketV1Codec for bytes; + + mapping(uint32 => mapping(bytes32 => DoubleEndedQueue.Bytes32Deque)) packetsQueue; // dstEid => dstUA => guids queue + mapping(bytes32 => bytes) packets; // guid => packet bytes + mapping(bytes32 => bytes) optionsLookup; // guid => options + + mapping(uint32 => address) endpoints; // eid => endpoint + + uint256 public constant TREASURY_GAS_CAP = 1000000000000; + uint256 public constant TREASURY_GAS_FOR_FEE_CAP = 100000; + + uint128 public executorValueCap = 0.1 ether; + + /// @dev Initializes test environment setup, to be overridden by specific tests. + function setUp() public virtual {} + + /** + * @dev set executorValueCap if more than 0.1 ether is necessary + * @dev this must be called prior to setUpEndpoints() if the value is to be used + * @param _valueCap amount executor can pass as msg.value to lzReceive() + */ + function setExecutorValueCap(uint128 _valueCap) public { + executorValueCap = _valueCap; + } + /** + * @notice Sets up endpoints for testing. + * @param _endpointNum The number of endpoints to create. + * @param _libraryType The type of message library to use (UltraLightNode or SimpleMessageLib). + */ + function setUpEndpoints(uint8 _endpointNum, LibraryType _libraryType) public { + EndpointV2[] memory endpointList = new EndpointV2[](_endpointNum); + uint32[] memory eidList = new uint32[](_endpointNum); + + // deploy _excludedContracts + for (uint8 i = 0; i < _endpointNum; i++) { + uint32 eid = i + 1; + eidList[i] = eid; + endpointList[i] = new EndpointV2(eid, address(this)); + registerEndpoint(endpointList[i]); + } + + // deploy + address[] memory sendLibs = new address[](_endpointNum); + address[] memory receiveLibs = new address[](_endpointNum); + + address[] memory signers = new address[](1); + signers[0] = vm.addr(1); + + PriceFeed priceFeed = new PriceFeed(); + priceFeed.initialize(address(this)); + + for (uint8 i = 0; i < _endpointNum; i++) { + if (_libraryType == LibraryType.UltraLightNode) { + address endpointAddr = address(endpointList[i]); + + SendUln302 sendUln; + ReceiveUln302 receiveUln; + { + sendUln = new SendUln302(payable(this), endpointAddr, TREASURY_GAS_CAP, TREASURY_GAS_FOR_FEE_CAP); + receiveUln = new ReceiveUln302(endpointAddr); + endpointList[i].registerLibrary(address(sendUln)); + endpointList[i].registerLibrary(address(receiveUln)); + sendLibs[i] = address(sendUln); + receiveLibs[i] = address(receiveUln); + } + + Executor executor = new Executor(); + DVN dvn; + { + address[] memory admins = new address[](1); + admins[0] = address(this); + + address[] memory messageLibs = new address[](2); + messageLibs[0] = address(sendUln); + messageLibs[1] = address(receiveUln); + + executor.initialize( + endpointAddr, + address(0x0), + messageLibs, + address(priceFeed), + address(this), + admins + ); + ExecutorFeeLib executorLib = new ExecutorFeeLibMock(); + executor.setWorkerFeeLib(address(executorLib)); + + dvn = new DVN(i + 1, messageLibs, address(priceFeed), signers, 1, admins); + DVNFeeLib dvnLib = new DVNFeeLib(1e18); + dvn.setWorkerFeeLib(address(dvnLib)); + } + + //todo: setDstGas + uint32 endpointNum = _endpointNum; + IExecutor.DstConfigParam[] memory dstConfigParams = new IExecutor.DstConfigParam[](endpointNum); + for (uint8 j = 0; j < endpointNum; j++) { + if (i == j) continue; + uint32 dstEid = j + 1; + + address[] memory defaultDVNs = new address[](1); + address[] memory optionalDVNs = new address[](0); + defaultDVNs[0] = address(dvn); + + { + SetDefaultUlnConfigParam[] memory params = new SetDefaultUlnConfigParam[](1); + UlnConfig memory ulnConfig = UlnConfig( + 100, + uint8(defaultDVNs.length), + uint8(optionalDVNs.length), + 0, + defaultDVNs, + optionalDVNs + ); + params[0] = SetDefaultUlnConfigParam(dstEid, ulnConfig); + sendUln.setDefaultUlnConfigs(params); + } + + { + SetDefaultExecutorConfigParam[] memory params = new SetDefaultExecutorConfigParam[](1); + ExecutorConfig memory executorConfig = ExecutorConfig(10000, address(executor)); + params[0] = SetDefaultExecutorConfigParam(dstEid, executorConfig); + sendUln.setDefaultExecutorConfigs(params); + } + + { + SetDefaultUlnConfigParam[] memory params = new SetDefaultUlnConfigParam[](1); + UlnConfig memory ulnConfig = UlnConfig( + 100, + uint8(defaultDVNs.length), + uint8(optionalDVNs.length), + 0, + defaultDVNs, + optionalDVNs + ); + params[0] = SetDefaultUlnConfigParam(dstEid, ulnConfig); + receiveUln.setDefaultUlnConfigs(params); + } + + // executor config + dstConfigParams[j] = IExecutor.DstConfigParam({ + dstEid: dstEid, + baseGas: 5000, + multiplierBps: 10000, + floorMarginUSD: 1e10, + nativeCap: executorValueCap + }); + + uint128 denominator = priceFeed.getPriceRatioDenominator(); + ILayerZeroPriceFeed.UpdatePrice[] memory prices = new ILayerZeroPriceFeed.UpdatePrice[](1); + prices[0] = ILayerZeroPriceFeed.UpdatePrice( + dstEid, + ILayerZeroPriceFeed.Price(1 * denominator, 1, 1) + ); + priceFeed.setPrice(prices); + } + executor.setDstConfig(dstConfigParams); + } else if (_libraryType == LibraryType.SimpleMessageLib) { + SimpleMessageLibMock messageLib = new SimpleMessageLibMock(payable(this), address(endpointList[i])); + endpointList[i].registerLibrary(address(messageLib)); + sendLibs[i] = address(messageLib); + receiveLibs[i] = address(messageLib); + } else { + revert("invalid library type"); + } + } + + // config up + for (uint8 i = 0; i < _endpointNum; i++) { + EndpointV2 endpoint = endpointList[i]; + for (uint8 j = 0; j < _endpointNum; j++) { + if (i == j) continue; + endpoint.setDefaultSendLibrary(j + 1, sendLibs[i]); + endpoint.setDefaultReceiveLibrary(j + 1, receiveLibs[i], 0); + } + } + } + + /** + * @notice Sets up mock OApp contracts for testing. + * @param _oappCreationCode The bytecode for creating OApp contracts. + * @param _startEid The starting endpoint ID for OApp setup. + * @param _oappNum The number of OApps to set up. + * @return oapps An array of addresses for the deployed OApps. + */ + function setupOApps( + bytes memory _oappCreationCode, + uint8 _startEid, + uint8 _oappNum + ) public returns (address[] memory oapps) { + oapps = new address[](_oappNum); + for (uint8 eid = _startEid; eid < _startEid + _oappNum; eid++) { + address oapp = _deployOApp(_oappCreationCode, abi.encode(address(endpoints[eid]), address(this), true)); + oapps[eid - _startEid] = oapp; + } + // config + wireOApps(oapps); + } + /** + * @notice Configures the peers between multiple OApp instances. + * @dev Sets each OApp as a peer to every other OApp in the provided array, except itself. + * @param oapps An array of addresses representing the deployed OApp instances. + */ + function wireOApps(address[] memory oapps) public { + uint256 size = oapps.length; + for (uint256 i = 0; i < size; i++) { + OApp localOApp = OApp(payable(oapps[i])); + for (uint256 j = 0; j < size; j++) { + if (i == j) continue; + OApp remoteOApp = OApp(payable(oapps[j])); + uint32 remoteEid = (remoteOApp.endpoint()).eid(); + localOApp.setPeer(remoteEid, addressToBytes32(address(remoteOApp))); + } + } + } + + /** + * @notice Deploys an OApp contract using provided bytecode and constructor arguments. + * @dev This internal function uses low-level `create` for deploying a new contract. + * @param _oappBytecode The bytecode of the OApp contract to be deployed. + * @param _constructorArgs The encoded constructor arguments for the OApp contract. + * @return addr The address of the newly deployed OApp contract. + */ + function _deployOApp(bytes memory _oappBytecode, bytes memory _constructorArgs) internal returns (address addr) { + bytes memory bytecode = bytes.concat(abi.encodePacked(_oappBytecode), _constructorArgs); + assembly { + addr := create(0, add(bytecode, 0x20), mload(bytecode)) + if iszero(extcodesize(addr)) { + revert(0, 0) + } + } + } + + /** + * @notice Schedules a packet for delivery, storing it in the packets queue. + * @dev Adds the packet to the front of the queue and stores its options for later retrieval. + * @param _packetBytes The packet data to be scheduled. + * @param _options The options associated with the packet, used during delivery. + */ + function schedulePacket(bytes calldata _packetBytes, bytes calldata _options) public { + uint32 dstEid = _packetBytes.dstEid(); + bytes32 dstAddress = _packetBytes.receiver(); + DoubleEndedQueue.Bytes32Deque storage queue = packetsQueue[dstEid][dstAddress]; + // front in, back out + bytes32 guid = _packetBytes.guid(); + queue.pushFront(guid); + packets[guid] = _packetBytes; + optionsLookup[guid] = _options; + } + + /** + * @notice Verifies and processes packets destined for a specific chain and user address. + * @dev Calls an overloaded version of verifyPackets with default values for packet amount and composer address. + * @param _dstEid The destination chain's endpoint ID. + * @param _dstAddress The destination address in bytes32 format. + */ + function verifyPackets(uint32 _dstEid, bytes32 _dstAddress) public { + verifyPackets(_dstEid, _dstAddress, 0, address(0x0)); + } + + /** + * @dev verify packets to destination chain's OApp address. + * @param _dstEid The destination endpoint ID. + * @param _dstAddress The destination address. + */ + function verifyPackets(uint32 _dstEid, address _dstAddress) public { + verifyPackets(_dstEid, bytes32(uint256(uint160(_dstAddress))), 0, address(0x0)); + } + + /** + * @dev dst UA receive/execute packets + * @dev will NOT work calling this directly with composer IF the composed payload is different from the lzReceive msg payload + */ + function verifyPackets(uint32 _dstEid, bytes32 _dstAddress, uint256 _packetAmount, address _composer) public { + require(endpoints[_dstEid] != address(0), "endpoint not yet registered"); + + DoubleEndedQueue.Bytes32Deque storage queue = packetsQueue[_dstEid][_dstAddress]; + uint256 pendingPacketsSize = queue.length(); + uint256 numberOfPackets; + if (_packetAmount == 0) { + numberOfPackets = queue.length(); + } else { + numberOfPackets = pendingPacketsSize > _packetAmount ? _packetAmount : pendingPacketsSize; + } + while (numberOfPackets > 0) { + numberOfPackets--; + // front in, back out + bytes32 guid = queue.popBack(); + bytes memory packetBytes = packets[guid]; + this.assertGuid(packetBytes, guid); + this.validatePacket(packetBytes); + + bytes memory options = optionsLookup[guid]; + if (_executorOptionExists(options, ExecutorOptions.OPTION_TYPE_NATIVE_DROP)) { + (uint256 amount, bytes32 receiver) = _parseExecutorNativeDropOption(options); + address to = address(uint160(uint256(receiver))); + (bool sent, ) = to.call{ value: amount }(""); + require(sent, "Failed to send Ether"); + } + if (_executorOptionExists(options, ExecutorOptions.OPTION_TYPE_LZRECEIVE)) { + this.lzReceive(packetBytes, options); + } + if (_composer != address(0) && _executorOptionExists(options, ExecutorOptions.OPTION_TYPE_LZCOMPOSE)) { + this.lzCompose(packetBytes, options, guid, _composer); + } + } + } + + function lzReceive(bytes calldata _packetBytes, bytes memory _options) external payable { + EndpointV2 endpoint = EndpointV2(endpoints[_packetBytes.dstEid()]); + (uint256 gas, uint256 value) = OptionsHelper._parseExecutorLzReceiveOption(_options); + + Origin memory origin = Origin(_packetBytes.srcEid(), _packetBytes.sender(), _packetBytes.nonce()); + endpoint.lzReceive{ value: value, gas: gas }( + origin, + _packetBytes.receiverB20(), + _packetBytes.guid(), + _packetBytes.message(), + bytes("") + ); + } + + function lzCompose( + bytes calldata _packetBytes, + bytes memory _options, + bytes32 _guid, + address _composer + ) external payable { + this.lzCompose( + _packetBytes.dstEid(), + _packetBytes.receiverB20(), + _options, + _guid, + _composer, + _packetBytes.message() + ); + } + + // @dev the verifyPackets does not know the composeMsg if it is NOT the same as the original lzReceive payload + // Can call this directly from your test to lzCompose those types of packets + function lzCompose( + uint32 _dstEid, + address _from, + bytes memory _options, + bytes32 _guid, + address _to, + bytes calldata _composerMsg + ) external payable { + EndpointV2 endpoint = EndpointV2(endpoints[_dstEid]); + (uint16 index, uint256 gas, uint256 value) = _parseExecutorLzComposeOption(_options); + endpoint.lzCompose{ value: value, gas: gas }(_from, _to, _guid, index, _composerMsg, bytes("")); + } + + function validatePacket(bytes calldata _packetBytes) external { + uint32 dstEid = _packetBytes.dstEid(); + EndpointV2 endpoint = EndpointV2(endpoints[dstEid]); + (address receiveLib, ) = endpoint.getReceiveLibrary(_packetBytes.receiverB20(), _packetBytes.srcEid()); + ReceiveUln302 dstUln = ReceiveUln302(receiveLib); + + (uint64 major, , ) = IMessageLib(receiveLib).version(); + if (major == 3) { + // it is ultra light node + bytes memory config = dstUln.getConfig(_packetBytes.srcEid(), _packetBytes.receiverB20(), 2); // CONFIG_TYPE_ULN + DVN dvn = DVN(abi.decode(config, (UlnConfig)).requiredDVNs[0]); + + bytes memory packetHeader = _packetBytes.header(); + bytes32 payloadHash = keccak256(_packetBytes.payload()); + + // sign + bytes memory signatures; + bytes memory verifyCalldata = abi.encodeWithSelector( + IReceiveUlnE2.verify.selector, + packetHeader, + payloadHash, + 100 + ); + { + bytes32 hash = dvn.hashCallData(dstEid, address(dstUln), verifyCalldata, block.timestamp + 1000); + bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, ethSignedMessageHash); // matches dvn signer + signatures = abi.encodePacked(r, s, v); + } + ExecuteParam[] memory params = new ExecuteParam[](1); + params[0] = ExecuteParam(dstEid, address(dstUln), verifyCalldata, block.timestamp + 1000, signatures); + dvn.execute(params); + + // commit verification + bytes memory callData = abi.encodeWithSelector( + IReceiveUlnE2.commitVerification.selector, + packetHeader, + payloadHash + ); + { + bytes32 hash = dvn.hashCallData(dstEid, address(dstUln), callData, block.timestamp + 1000); + bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, ethSignedMessageHash); // matches dvn signer + signatures = abi.encodePacked(r, s, v); + } + params[0] = ExecuteParam(dstEid, address(dstUln), callData, block.timestamp + 1000, signatures); + dvn.execute(params); + } else { + SimpleMessageLibMock(payable(receiveLib)).validatePacket(_packetBytes); + } + } + + function assertGuid(bytes calldata packetBytes, bytes32 guid) external pure { + bytes32 packetGuid = packetBytes.guid(); + require(packetGuid == guid, "guid not match"); + } + + function registerEndpoint(EndpointV2 endpoint) public { + endpoints[endpoint.eid()] = address(endpoint); + } + + function hasPendingPackets(uint16 _dstEid, bytes32 _dstAddress) public view returns (bool flag) { + DoubleEndedQueue.Bytes32Deque storage queue = packetsQueue[_dstEid][_dstAddress]; + return queue.length() > 0; + } + + function getNextInflightPacket(uint16 _dstEid, bytes32 _dstAddress) public view returns (bytes memory packetBytes) { + DoubleEndedQueue.Bytes32Deque storage queue = packetsQueue[_dstEid][_dstAddress]; + if (queue.length() > 0) { + bytes32 guid = queue.back(); + packetBytes = packets[guid]; + } + } + + function addressToBytes32(address _addr) internal pure returns (bytes32) { + return bytes32(uint256(uint160(_addr))); + } + + receive() external payable {} +} diff --git a/examples/oapp/test/foundry/mocks/ExecutorFeeLibMock.sol b/examples/oapp/test/foundry/mocks/ExecutorFeeLibMock.sol new file mode 100644 index 000000000..a2be7f4ca --- /dev/null +++ b/examples/oapp/test/foundry/mocks/ExecutorFeeLibMock.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.22; + +import { ExecutorFeeLib } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/ExecutorFeeLib.sol"; + +contract ExecutorFeeLibMock is ExecutorFeeLib { + constructor() ExecutorFeeLib(1e18) {} + + function _isV1Eid(uint32 /*_eid*/) internal pure override returns (bool) { + return false; + } +} diff --git a/examples/oapp/test/foundry/mocks/SendUln302Mock.sol b/examples/oapp/test/foundry/mocks/SendUln302Mock.sol new file mode 100644 index 000000000..2524cfb91 --- /dev/null +++ b/examples/oapp/test/foundry/mocks/SendUln302Mock.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { Packet } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ISendLib.sol"; +import { MessagingFee } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { SendUln302 } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/uln302/SendUln302.sol"; + +import { TestHelper } from "../TestHelper.sol"; + +contract SendUln302Mock is SendUln302 { + // offchain packets schedule + TestHelper public testHelper; + + constructor( + address payable _verifyHelper, + address _endpoint, + uint256 _treasuryGasCap, + uint256 _treasuryGasForFeeCap + ) SendUln302(_endpoint, _treasuryGasCap, _treasuryGasForFeeCap) { + testHelper = TestHelper(_verifyHelper); + } + + function send( + Packet calldata _packet, + bytes calldata _options, + bool _payInLzToken + ) public override returns (MessagingFee memory fee, bytes memory encodedPacket) { + (fee, encodedPacket) = super.send(_packet, _options, _payInLzToken); + testHelper.schedulePacket(encodedPacket, _options); + } +} diff --git a/examples/oapp/test/foundry/mocks/SimpleMessageLibMock.sol b/examples/oapp/test/foundry/mocks/SimpleMessageLibMock.sol new file mode 100644 index 000000000..344832e01 --- /dev/null +++ b/examples/oapp/test/foundry/mocks/SimpleMessageLibMock.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { SimpleMessageLib } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/SimpleMessageLib.sol"; + +import { TestHelper } from "../TestHelper.sol"; + +contract SimpleMessageLibMock is SimpleMessageLib { + // offchain packets schedule + TestHelper public testHelper; + + constructor(address payable _verifyHelper, address _endpoint) SimpleMessageLib(_endpoint, address(0x0)) { + testHelper = TestHelper(_verifyHelper); + } + + function _handleMessagingParamsHook(bytes memory _encodedPacket, bytes memory _options) internal override { + testHelper.schedulePacket(_encodedPacket, _options); + } +} diff --git a/examples/oapp/test/hardhat/OApp.test.ts b/examples/oapp/test/hardhat/OApp.test.ts new file mode 100644 index 000000000..d5c783dee --- /dev/null +++ b/examples/oapp/test/hardhat/OApp.test.ts @@ -0,0 +1,11 @@ +import { before, beforeEach, describe } from 'mocha' + +describe('OApp: ', function () { + before(async function () { + //TODO + }) + + beforeEach(async function () { + //TODO + }) +}) diff --git a/examples/oapp/tsconfig.json b/examples/oapp/tsconfig.json new file mode 100644 index 000000000..eb2f10d76 --- /dev/null +++ b/examples/oapp/tsconfig.json @@ -0,0 +1,14 @@ +{ + "exclude": ["node_modules"], + "include": ["deploy", "tasks", "hardhat.config.ts"], + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "types": ["node", "mocha"] + } +} diff --git a/examples/oft/.eslintignore b/examples/oft/.eslintignore index dfa778e68..1f6f67ad9 100644 --- a/examples/oft/.eslintignore +++ b/examples/oft/.eslintignore @@ -1,4 +1,5 @@ artifacts cache dist -node_modules \ No newline at end of file +node_modules +out \ No newline at end of file diff --git a/examples/oft/.prettierignore b/examples/oft/.prettierignore index 3222379cb..d5e0b7749 100644 --- a/examples/oft/.prettierignore +++ b/examples/oft/.prettierignore @@ -1,4 +1,5 @@ artifacts/ cache/ dist/ -node_modules/ \ No newline at end of file +node_modules/ +out/ \ No newline at end of file diff --git a/examples/oft/deploy/YourOFT.ts b/examples/oft/deploy/YourOFT.ts index b80148741..3d3ad39fa 100644 --- a/examples/oft/deploy/YourOFT.ts +++ b/examples/oft/deploy/YourOFT.ts @@ -39,7 +39,6 @@ const deploy: DeployFunction = async (hre) => { deployer, // owner ], log: true, - waitConfirmations: 3, skipIfAlreadyDeployed: false, }) diff --git a/examples/oft/foundry.toml b/examples/oft/foundry.toml index 29c97e7fd..294e8bd76 100644 --- a/examples/oft/foundry.toml +++ b/examples/oft/foundry.toml @@ -5,4 +5,7 @@ test = 'test/foundry' cache_path = 'cache' libs = ['node_modules', 'node_modules/@layerzerolabs/toolbox-foundry/lib'] -# See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file +remappings = [ + '@layerzerolabs/=node_modules/@layerzerolabs', + '@openzeppelin/=node_modules/@openzeppelin/', +] diff --git a/examples/oft/package.json b/examples/oft/package.json index af19a1908..c982fcb36 100644 --- a/examples/oft/package.json +++ b/examples/oft/package.json @@ -4,12 +4,16 @@ "private": true, "license": "MIT", "scripts": { - "compile": "$npm_execpath hardhat compile", + "compile": "$npm_execpath compile:forge && $npm_execpath compile:hardhat", + "compile:forge": "forge build", + "compile:hardhat": "$npm_execpath hardhat compile", "lint": "$npm_execpath lint:js && $npm_execpath lint:sol", "lint:fix": "$npm_execpath prettier --write . && solhint 'contracts/**/*.sol' --fix --noPrompt", "lint:js": "$npm_execpath eslint '**/*.js' && $npm_execpath prettier --check .", "lint:sol": "solhint 'contracts/**/*.sol'", - "test": "$npm_execpath hardhat test --parallel" + "test": "$npm_execpath test:forge && $npm_execpath test:hardhat", + "test:forge": "forge test", + "test:hardhat": "$npm_execpath hardhat test" }, "devDependencies": { "@babel/core": "^7.23.7", @@ -25,10 +29,12 @@ "@nomiclabs/hardhat-ethers": "^2.2.3", "@openzeppelin/contracts": "^4.9.5", "@rushstack/eslint-patch": "^1.6.1", + "@types/mocha": "^10.0.6", "ethers": "^5.7.0", "hardhat": "^2.19.4", "hardhat-contract-sizer": "^2.10.0", "hardhat-deploy": "^0.11.45", + "mocha": "^10.2.0", "prettier": "^3.1.1", "solhint": "^4.0.0", "solidity-bytes-utils": "^0.8.1", diff --git a/examples/oft/test/hardhat/OFT.test.ts b/examples/oft/test/hardhat/OFT.test.ts index 3d87a4efe..d5c783dee 100644 --- a/examples/oft/test/hardhat/OFT.test.ts +++ b/examples/oft/test/hardhat/OFT.test.ts @@ -1,6 +1,6 @@ -import { before } from 'mocha' +import { before, beforeEach, describe } from 'mocha' -describe.skip('OFT: ', function () { +describe('OApp: ', function () { before(async function () { //TODO }) @@ -8,8 +8,4 @@ describe.skip('OFT: ', function () { beforeEach(async function () { //TODO }) - - it('OFT send', async function () { - //TODO - }) }) diff --git a/packages/toolbox-foundry/.eslintignore b/packages/toolbox-foundry/.eslintignore index a9f4ed545..3b4485c6a 100644 --- a/packages/toolbox-foundry/.eslintignore +++ b/packages/toolbox-foundry/.eslintignore @@ -1,2 +1,4 @@ lib -node_modules \ No newline at end of file +node_modules +src/ds-test +src/forge-std \ No newline at end of file diff --git a/packages/toolbox-foundry/.gitignore b/packages/toolbox-foundry/.gitignore new file mode 100644 index 000000000..7951405f8 --- /dev/null +++ b/packages/toolbox-foundry/.gitignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/packages/toolbox-foundry/.prettierignore b/packages/toolbox-foundry/.prettierignore index 46f10721d..675627102 100644 --- a/packages/toolbox-foundry/.prettierignore +++ b/packages/toolbox-foundry/.prettierignore @@ -1,2 +1,4 @@ lib/ -node_modules/ \ No newline at end of file +node_modules/ +src/ds-test/ +src/forge-std/ \ No newline at end of file diff --git a/packages/toolbox-foundry/DEVELOPMENT.md b/packages/toolbox-foundry/DEVELOPMENT.md index a87fb0e04..224130399 100644 --- a/packages/toolbox-foundry/DEVELOPMENT.md +++ b/packages/toolbox-foundry/DEVELOPMENT.md @@ -6,6 +6,24 @@

Development

+## Building + +This package requires `make` CLI utility to be available. Make sure that `make` is available by running: + +```bash +make --help +``` + +On MacOS, `make` is installed as a aprt of the XCode developer tools. On unix operating systems, `make` can be installed by adding the `build-essential` package: + +```bash +# On Debian +apt-get install build-essential + +# On Apline +apk add --no-cache make +``` + ## Adding libraries To install a new library to be included with this package, please follow these steps: diff --git a/packages/toolbox-foundry/Makefile b/packages/toolbox-foundry/Makefile new file mode 100644 index 000000000..cb636b13a --- /dev/null +++ b/packages/toolbox-foundry/Makefile @@ -0,0 +1,31 @@ +# None of the targets actually build any binaries so we make them all as phony +.PHONY: clean node_modules git_submodules + +clean: + rm -rf lib + +# This is the overarching target that builds the lib/ directory +lib: clean node_modules git_submodules + +# This target will get all the libraries from node_modules +# and copy them to the lib/ directory +node_modules: + # First we create the parent directory + mkdir -p lib/solidity-bytes-utils/src + + # We copy the contracts from solidity-bytes-utils + # + # FIXME Due to a weird discrepancy between foundry compilation step + # and forge test, we need to provide the contracts in both src and contracts directories + cp -R node_modules/solidity-bytes-utils/contracts lib/solidity-bytes-utils/src + cp -R node_modules/solidity-bytes-utils/contracts lib/solidity-bytes-utils + + # We also want to make sure to include the license & package.json + cp node_modules/solidity-bytes-utils/package.json node_modules/solidity-bytes-utils/LICENSE lib/solidity-bytes-utils + +# This target will get all the git submodules installed in src/ directory +# and copy them to lib/ directory +# +# At this point we only have submodules in src/ so we can just copy everything +git_submodules: + cp -R src/* lib diff --git a/packages/toolbox-foundry/README.md b/packages/toolbox-foundry/README.md index 1da08c4a1..574744966 100644 --- a/packages/toolbox-foundry/README.md +++ b/packages/toolbox-foundry/README.md @@ -26,7 +26,7 @@ To use `@layerzerolabs/toolbox-foundry` you will need to point to it in your `fo ```toml libs = [ - 'node_modules/@layerzerolabs/toolbox-foundry/lib' + 'node_modules/@layerzerolabs/toolbox-foundry/lib', # Any other library folders you need, e.g. 'node_modules' ] @@ -44,3 +44,5 @@ This package comes with support for `forge-std` out of the box so you can start import "forge-std/console.sol"; import { Test } from "forge-std/Test.sol"; ``` + +The supporting packages for `@layerzerolabs/` dependencies are also included - namely `solidity-bytes-utils`. diff --git a/packages/toolbox-foundry/package.json b/packages/toolbox-foundry/package.json index ab506b163..1d98f67b5 100644 --- a/packages/toolbox-foundry/package.json +++ b/packages/toolbox-foundry/package.json @@ -17,11 +17,15 @@ "!lib/forge-std/.gitignore", "!lib/forge-std/.gitmodules" ], - "scripts": {}, + "scripts": { + "build": "make lib", + "clean": "make clean" + }, "dependencies": {}, "devDependencies": { "@types/jest": "^29.5.11", "jest": "^29.7.0", + "solidity-bytes-utils": "^0.8.1", "ts-jest": "^29.1.1", "ts-node": "^10.9.2", "tsup": "~8.0.1", diff --git a/packages/toolbox-foundry/src/ds-test b/packages/toolbox-foundry/src/ds-test new file mode 160000 index 000000000..e282159d5 --- /dev/null +++ b/packages/toolbox-foundry/src/ds-test @@ -0,0 +1 @@ +Subproject commit e282159d5170298eb2455a6c05280ab5a73a4ef0 diff --git a/packages/toolbox-foundry/src/forge-std b/packages/toolbox-foundry/src/forge-std new file mode 160000 index 000000000..ae570fec0 --- /dev/null +++ b/packages/toolbox-foundry/src/forge-std @@ -0,0 +1 @@ +Subproject commit ae570fec082bfe1c1f45b0acca4a2b4f84d345ce diff --git a/packages/toolbox-foundry/turbo.json b/packages/toolbox-foundry/turbo.json new file mode 100644 index 000000000..dad78ed11 --- /dev/null +++ b/packages/toolbox-foundry/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "pipeline": { + "build": { + "outputs": ["lib/**"] + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a00705ae..2d0a49557 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,6 +69,90 @@ importers: specifier: 1.11.0 version: 1.11.0 + examples/oapp: + devDependencies: + '@babel/core': + specifier: ^7.23.7 + version: 7.23.7 + '@layerzerolabs/eslint-config-next': + specifier: ^2.0.7 + version: 2.0.11(typescript@5.3.3) + '@layerzerolabs/lz-definitions': + specifier: ~2.0.7 + version: 2.0.12 + '@layerzerolabs/lz-evm-messagelib-v2': + specifier: ~2.0.7 + version: 2.0.11(@axelar-network/axelar-gmp-sdk-solidity@5.6.4)(@chainlink/contracts-ccip@0.7.6)(@layerzerolabs/lz-evm-protocol-v2@2.0.11)(@layerzerolabs/lz-evm-v1-0.7@2.0.11)(@openzeppelin/contracts-upgradeable@4.9.5)(@openzeppelin/contracts@4.9.5)(hardhat-deploy@0.11.45)(solidity-bytes-utils@0.8.1) + '@layerzerolabs/lz-evm-oapp-v2': + specifier: ~2.0.7 + version: 2.0.11(@layerzerolabs/lz-evm-messagelib-v2@2.0.11)(@layerzerolabs/lz-evm-protocol-v2@2.0.11)(@layerzerolabs/lz-evm-v1-0.7@2.0.11)(@openzeppelin/contracts-upgradeable@4.9.5)(@openzeppelin/contracts@4.9.5)(hardhat-deploy@0.11.45)(solidity-bytes-utils@0.8.1) + '@layerzerolabs/lz-evm-protocol-v2': + specifier: ~2.0.7 + version: 2.0.11(@openzeppelin/contracts@4.9.5)(hardhat-deploy@0.11.45)(solidity-bytes-utils@0.8.1) + '@layerzerolabs/lz-evm-v1-0.7': + specifier: ~2.0.7 + version: 2.0.11(@openzeppelin/contracts-upgradeable@4.9.5)(@openzeppelin/contracts@4.9.5)(hardhat-deploy@0.11.45) + '@layerzerolabs/prettier-config-next': + specifier: ^2.0.7 + version: 2.0.11 + '@layerzerolabs/solhint-config': + specifier: ^2.0.7 + version: 2.0.11(typescript@5.3.3) + '@layerzerolabs/toolbox-foundry': + specifier: ~0.0.1 + version: link:../../packages/toolbox-foundry + '@layerzerolabs/toolbox-hardhat': + specifier: ~0.0.3 + version: link:../../packages/toolbox-hardhat + '@nomicfoundation/hardhat-ethers': + specifier: ^3.0.5 + version: 3.0.5(ethers@5.7.2)(hardhat@2.19.4) + '@nomiclabs/hardhat-ethers': + specifier: ^2.2.3 + version: 2.2.3(ethers@5.7.2)(hardhat@2.19.4) + '@openzeppelin/contracts': + specifier: ^4.9.5 + version: 4.9.5 + '@openzeppelin/contracts-upgradeable': + specifier: ^4.9.5 + version: 4.9.5 + '@rushstack/eslint-patch': + specifier: ^1.6.1 + version: 1.6.1 + '@types/mocha': + specifier: ^10.0.6 + version: 10.0.6 + ethers: + specifier: ^5.7.0 + version: 5.7.2 + hardhat: + specifier: ^2.19.4 + version: 2.19.4(ts-node@10.9.2)(typescript@5.3.3) + hardhat-contract-sizer: + specifier: ^2.10.0 + version: 2.10.0(hardhat@2.19.4) + hardhat-deploy: + specifier: ^0.11.45 + version: 0.11.45 + mocha: + specifier: ^10.2.0 + version: 10.2.0 + prettier: + specifier: ^3.1.1 + version: 3.1.1 + solhint: + specifier: ^4.0.0 + version: 4.0.0(typescript@5.3.3) + solidity-bytes-utils: + specifier: ^0.8.1 + version: 0.8.1(@babel/core@7.23.7) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@18.18.14)(typescript@5.3.3) + typescript: + specifier: ^5.3.3 + version: 5.3.3 + examples/oft: devDependencies: '@babel/core': @@ -110,6 +194,9 @@ importers: '@rushstack/eslint-patch': specifier: ^1.6.1 version: 1.6.1 + '@types/mocha': + specifier: ^10.0.6 + version: 10.0.6 ethers: specifier: ^5.7.0 version: 5.7.2 @@ -122,6 +209,9 @@ importers: hardhat-deploy: specifier: ^0.11.45 version: 0.11.45 + mocha: + specifier: ^10.2.0 + version: 10.2.0 prettier: specifier: ^3.1.1 version: 3.1.1 @@ -683,6 +773,9 @@ importers: jest: specifier: ^29.7.0 version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) + solidity-bytes-utils: + specifier: ^0.8.1 + version: 0.8.1(@babel/core@7.23.7) ts-jest: specifier: ^29.1.1 version: 29.1.1(@babel/core@7.23.7)(esbuild@0.19.11)(jest@29.7.0)(typescript@5.3.3) @@ -3862,6 +3955,10 @@ packages: resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} dev: true + /@types/mocha@10.0.6: + resolution: {integrity: sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==} + dev: true + /@types/node@12.20.55: resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} dev: true @@ -7384,7 +7481,6 @@ packages: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 - dev: true /glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} @@ -10777,7 +10873,7 @@ packages: resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} hasBin: true dependencies: - glob: 7.2.0 + glob: 7.2.3 /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} diff --git a/turbo.json b/turbo.json index f40fe6134..d9f89f08d 100644 --- a/turbo.json +++ b/turbo.json @@ -2,7 +2,7 @@ "$schema": "https://turbo.build/schema.json", "pipeline": { "compile": { - "outputs": ["artifacts/**", "cache/**"], + "outputs": ["artifacts/**", "cache/**", "out/**"], "dependsOn": ["^build"] }, "build": {