Skip to content

Instantly share code, notes, and snippets.

@DanL0
Created May 1, 2024 15:33
Show Gist options
  • Save DanL0/7eb57319646e4b1f3176d7723f84226e to your computer and use it in GitHub Desktop.
Save DanL0/7eb57319646e4b1f3176d7723f84226e to your computer and use it in GitHub Desktop.
## Pure Foundry
:::caution
When relying only on git submodules LayerZero's TestHelper will only work with OpenZeppelin V4 due to outdated `IExecutor` and `ILayerZeroPriceFeed` code in `LayerZero-Labs/LayerZero-v2` repository.
:::
<br/>
Start by installing LayerZero V2 contracts:
```bash
forge install LayerZero-Labs/LayerZero-v2
```
You also need to install dependencies including OpenZeppelin V5:
```bash
forge install LayerZero-Labs/LayerZero
forge install OpenZeppelin/openzeppelin-contracts
forge install GNSPS/solidity-bytes-utils
```
Now create remappings.txt file with following content:
```bash
@layerzerolabs/lz-evm-oapp-v2/=lib/LayerZero-v2/oapp/
@layerzerolabs/lz-evm-protocol-v2/=lib/LayerZero-v2/protocol/
@layerzerolabs/lz-evm-messagelib-v2/=lib/LayerZero-v2/messagelib/
@layerzerolabs/lz-evm-v1-0.7/=lib/LayerZero/
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
solidity-bytes-utils/=lib/solidity-bytes-utils/
```
Your project should now be properly configured to work with LayerZero contracts. To confirm, create example contract `src/MyOApp.sol` with following content:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
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 _delegate) OApp(_endpoint, _delegate) Ownable(_delegate) {}
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.
* @return fee A `MessagingFee` struct containing the calculated gas fee in either the native token or 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.
* @dev _origin A struct containing information about the message sender.
* @dev _guid A unique global packet identifier for the message.
* @param payload The encoded message payload being received.
*
* @dev The following params are unused in the current implementation of the OApp.
* @dev _executor The address of the Executor responsible for processing the message.
* @dev _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));
}
}
```
Now run:
```bash
forge build
```
It should return compilation success status, eg.:
```bash
➜ hello_foundry git:(master) forge build
[⠒] Compiling...
[⠆] Compiling 38 files with 0.8.23
[⠰] Solc 0.8.23 finished in 1.15s
Compiler run successful!
```
### Test Helper
Test Helper allows you to test LayerZero's protocol functionality using mocked endpoint contracts. It is the recommended way to test the behavior of your smart contracts before deploying them to the production environment. Testing in the local environment is the most convenient way to do so.
If you wish to use LayerZero's Test Helper when using pure Foundry setup you need to use OpenZeppelin V4 instead of V5. To do this you can remove OZ V5:
```bash
forge remove OpenZeppelin/openzeppelin-contracts
```
Install OpenZeppelin V4 and hardhat-deploy:
```bash
forge install OpenZeppelin/openzeppelin-contracts@v4.9.6
forge install OpenZeppelin/openzeppelin-contracts-upgradeable@v4.9.6
forge install wighawag/hardhat-deploy
```
Add following entry to remappings:
```bash
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
```
At this point your `remappings.txt` should look like this:
```
@layerzerolabs/lz-evm-oapp-v2/=lib/LayerZero-v2/oapp/
@layerzerolabs/lz-evm-protocol-v2/=lib/LayerZero-v2/protocol/
@layerzerolabs/lz-evm-messagelib-v2/=lib/LayerZero-v2/messagelib/
@layerzerolabs/lz-evm-v1-0.7/=lib/LayerZero/
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
solidity-bytes-utils/=lib/solidity-bytes-utils/
```
Because you're using OpenZeppelin V4 instead of V5 remove Ownable constructor call from `src/MyOApp.sol` example. Before:
```solidity
contract MyOApp is OApp {
constructor(address _endpoint, address _delegate) OApp(_endpoint, _delegate) Ownable(_delegate) {}
```
After:
```solidity
contract MyOApp is OApp {
constructor(address _endpoint, address _delegate) OApp(_endpoint, _delegate) {}
```
Run:
```
forge build
```
Compilation should be successful.
It is time to add first test to see if TestHelper is working. Create `test/MyOApp.t.sol` with following content:
```solidity
// 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";
// The unique path location of your OApp
import { MyOApp } from "../src/MyOApp.sol";
import { TestHelper } from "@layerzerolabs/lz-evm-oapp-v2/test/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");
}
}
```
You should see test result indicating success:
```bash
Ran 1 test for test/MyOApp.t.sol:MyOAppTest
[PASS] test_send() (gas: 696226)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.19ms (1.93ms CPU time)
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment