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 @@
+
+
+
+
+
+
+@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": {