Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/cyan-squids-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@layerzerolabs/hyperliquid-composer": patch
"@layerzerolabs/oft-hyperliquid-example": patch
---

change fork fuzzing to works off a forked anvil
3 changes: 3 additions & 0 deletions .github/workflows/reusable-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ jobs:
# LZ_DEVTOOLS_ENABLE_SOLANA_TESTS: 1
RPC_URL_SOLANA_MAINNET: ${{ secrets.RPC_URL_SOLANA_MAINNET || 'https://rpc.ankr.com/solana' }}
RPC_URL_SOLANA_TESTNET: ${{ secrets.RPC_URL_SOLANA_TESTNET || 'https://api.devnet.solana.com' }}
FOUNDRY_PROFILE: "ci"
RPC_URL_HYPERLIQUID: "http://127.0.0.1:8546"
RPC_URL_HYPERLIQUID_UPSTREAM: ${{ secrets.RPC_URL_HYPERLIQUID_UPSTREAM || 'https://rpc.hyperliquid.xyz/evm' }}

# We'll collect the docker compose logs from all containers on failure
- name: Collect docker logs on failure
Expand Down
12 changes: 12 additions & 0 deletions Dockerfile.hyperliquid
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# syntax=docker/dockerfile:1.4
FROM ghcr.io/foundry-rs/foundry:latest

WORKDIR /anvil

# Install curl for healthcheck
USER root
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

ENTRYPOINT ["sh", "-c", "anvil --fork-url $FORK_URL --host $HOST --port $PORT"]

EXPOSE 8545
4 changes: 4 additions & 0 deletions docker-compose.local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,7 @@ services:
network-tango:
ports:
- "10003:8545"

network-hyperliquid:
ports:
- "8546:8546"
28 changes: 28 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,29 @@ services:
file: docker-compose.templates.yaml
service: ton-node

# ~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~
#
# The HyperLiquid local fork node
#
# .oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.
network-hyperliquid:
build:
context: .
dockerfile: Dockerfile.hyperliquid
environment:
- FORK_URL=${RPC_URL_HYPERLIQUID_UPSTREAM:-https://rpc.hyperliquid.xyz/evm}
- HOST=0.0.0.0
- PORT=8545
- FOUNDRY_DISABLE_NIGHTLY_WARNING=1
ports:
- "8546:8545"
healthcheck:
test: ["CMD", "sh", "-c", "curl -X POST -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"method\":\"eth_chainId\",\"params\":[],\"id\":1}' http://localhost:8545"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s

# ~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~
#
# The actual tests
Expand All @@ -69,6 +92,8 @@ services:
condition: service_healthy
network-ton:
condition: service_healthy
network-hyperliquid:
condition: service_healthy
logging:
driver: none
# The default containerized setup will specify the network URLs
Expand All @@ -85,11 +110,14 @@ services:
- NETWORK_URL_VENGABOYS=http://network-vengaboys:8545
- NETWORK_URL_TANGO=http://network-tango:8545
- NETWORK_URL_TON=http://network-ton:8081/jsonRPC
- NETWORK_URL_HYPERLIQUID=http://network-hyperliquid:8546
- DOCKER_COMPOSE_RUN_TESTS_TURBO_ARGS=$DOCKER_COMPOSE_RUN_TESTS_TURBO_ARGS
# Passing environment variables that control the solana tests
- RPC_URL_SOLANA_MAINNET=${RPC_URL_SOLANA_MAINNET}
- RPC_URL_SOLANA_TESTNET=${RPC_URL_SOLANA_TESTNET}
- LZ_DEVTOOLS_ENABLE_SOLANA_TESTS=${LZ_DEVTOOLS_ENABLE_SOLANA_TESTS}
- FOUNDRY_PROFILE=${FOUNDRY_PROFILE}
- RPC_URL_HYPERLIQUID=${RPC_URL_HYPERLIQUID}
volumes:
- ./node_modules/.cache/turbo:/app/node_modules/.cache/turbo
# Hardhat has an issue with caching compilers inside a docker container,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,57 +5,21 @@ import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTCom

import { HyperLiquidComposerCodec } from "@layerzerolabs/hyperliquid-composer/contracts/library/HyperLiquidComposerCodec.sol";

import { IHyperLiquidComposerErrors, ErrorMessagePayload } from "@layerzerolabs/hyperliquid-composer/contracts/interfaces/IHyperLiquidComposerErrors.sol";
import { IHyperAsset, IHyperLiquidComposerCore } from "@layerzerolabs/hyperliquid-composer/contracts/interfaces/IHyperLiquidComposerCore.sol";
import { IHyperLiquidComposerErrors } from "@layerzerolabs/hyperliquid-composer/contracts/interfaces/IHyperLiquidComposerErrors.sol";

import { HyperLiquidComposer } from "@layerzerolabs/hyperliquid-composer/contracts/HyperLiquidComposer.sol";
import { MyOFT } from "../../../contracts/MyOFT.sol";

import { Test, console } from "forge-std/Test.sol";
import { HyperliquidBaseTest } from "@layerzerolabs/hyperliquid-composer/test/HyperliquidBase.t.sol";

contract ComposeMessageTest is Test {
import { console } from "forge-std/Test.sol";

contract ComposeMessageTest is HyperliquidBaseTest {
using HyperLiquidComposerCodec for bytes;

uint256 public constant DEFAULT_AMOUNT = 1e18;

IHyperAsset public ALICE;
IHyperAsset public HYPE;
address public constant HL_LZ_ENDPOINT_V2 = 0xf9e1815F151024bDE4B7C10BAC10e8Ba9F6b53E1;

MyOFT public oft;
HyperLiquidComposer public hyperLiquidComposer;

address public sender;

function setUp() public {
// Skip test if fork fails
try vm.createSelectFork("https://rpc.hyperliquid-testnet.xyz/evm") {} catch {
console.log("Forking testnet https://rpc.hyperliquid-testnet.xyz/evm failed");
vm.skip(true);
}

sender = makeAddr("sender");

ALICE = IHyperAsset({
assetBridgeAddress: HyperLiquidComposerCodec.into_assetBridgeAddress(1231),
coreIndexId: 1231,
decimalDiff: 18 - 6
});

HYPE = IHyperAsset({
assetBridgeAddress: 0x2222222222222222222222222222222222222222,
coreIndexId: 1105,
decimalDiff: 18 - 10
});

oft = new MyOFT("test", "test", HL_LZ_ENDPOINT_V2, msg.sender);

hyperLiquidComposer = new HyperLiquidComposer(
HL_LZ_ENDPOINT_V2,
address(oft),
ALICE.coreIndexId,
ALICE.decimalDiff
);
function setUp() public override {
super.setUp();
}

function test_validateAndDecodeMessage_EncodingVariants(address _receiver, bool _useEncodePacked) public view {
Expand All @@ -77,7 +41,7 @@ contract ComposeMessageTest is Test {
uint256 _amount,
bytes memory _noise
) internal view returns (bytes memory) {
bytes memory payload = bytes.concat(addressToBytes32(sender), _receiver, _noise);
bytes memory payload = bytes.concat(addressToBytes32(userA), _receiver, _noise);
return OFTComposeMsgCodec.encode(0, 0, _amount, payload);
}

Expand Down
137 changes: 43 additions & 94 deletions examples/oft-hyperliquid/test/foundry/HyperLiquidComposer.t.sol
Original file line number Diff line number Diff line change
@@ -1,134 +1,83 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import { Test, console } from "forge-std/Test.sol";

import { ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";
import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import { HyperLiquidComposer, IHyperAsset } from "@layerzerolabs/hyperliquid-composer/contracts/HyperLiquidComposer.sol";
import { ICoreWriter } from "@layerzerolabs/hyperliquid-composer/contracts/interfaces/ICoreWriter.sol";

import { HyperLiquidComposerCodec } from "@layerzerolabs/hyperliquid-composer/contracts/library/HyperLiquidComposerCodec.sol";
import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol";

import { IHyperAsset } from "@layerzerolabs/hyperliquid-composer/contracts/interfaces/IHyperLiquidComposerCore.sol";
import { IHYPEPrecompile } from "@layerzerolabs/hyperliquid-composer/contracts/interfaces/IHYPEPrecompile.sol";
import { HypePrecompileMock } from "@layerzerolabs/hyperliquid-composer/test/mocks/HypePrecompileMock.sol";
import { SpotBalancePrecompileMock } from "@layerzerolabs/hyperliquid-composer/test/mocks/SpotBalancePrecompileMock.sol";
import { ICoreWriter } from "@layerzerolabs/hyperliquid-composer/contracts/interfaces/ICoreWriter.sol";

import { MyOFT } from "../../contracts/MyOFT.sol";
import { SpotBalancePrecompileMock } from "@layerzerolabs/hyperliquid-composer/test/mocks/SpotBalancePrecompileMock.sol";

contract HyperLiquidComposerTest is Test {
IHyperAsset public ALICE;
IHyperAsset public HYPE;
address public constant HL_LZ_ENDPOINT_V2 = 0xf9e1815F151024bDE4B7C10BAC10e8Ba9F6b53E1;

address public constant HL_LZ_ENDPOINT_V2_TESTNET = 0xf9e1815F151024bDE4B7C10BAC10e8Ba9F6b53E1;
address public constant HL_LZ_ENDPOINT_V2_MAINNET = 0x3A73033C0b1407574C76BdBAc67f126f6b4a9AA9;

address public constant HLP_CORE_WRITER = 0x3333333333333333333333333333333333333333;
address public constant HLP_PRECOMPILE_READ_SPOT_BALANCE = 0x0000000000000000000000000000000000000801;
// Ethereum Sepolia
uint32 public constant SRC_EID = 40161;
// HyperLiquid Testnet
uint32 public DST_EID;

MyOFT public oft;
HyperLiquidComposer public hyperLiquidComposer;

address public userA = makeAddr("userA");
address public userB = makeAddr("userB");

uint64 public constant AMOUNT_TO_SEND = 1e18;
uint64 public constant AMOUNT_TO_FUND = 100 gwei;
uint64 public constant DUST = 1 wei;

function setUp() public {
// Skip test if fork fails
try vm.createSelectFork("https://rpc.hyperliquid-testnet.xyz/evm") {} catch {
console.log("Forking testnet https://rpc.hyperliquid-testnet.xyz/evm failed");
vm.skip(true);
}

ALICE = IHyperAsset({
assetBridgeAddress: HyperLiquidComposerCodec.into_assetBridgeAddress(1231),
coreIndexId: 1231,
decimalDiff: 18 - 6
});

HYPE = IHyperAsset({
assetBridgeAddress: 0x2222222222222222222222222222222222222222,
coreIndexId: 1105,
decimalDiff: 18 - 10
});

vm.etch(HLP_PRECOMPILE_READ_SPOT_BALANCE, address(new SpotBalancePrecompileMock()).code);
SpotBalancePrecompileMock(HLP_PRECOMPILE_READ_SPOT_BALANCE).setSpotBalance(
ALICE.assetBridgeAddress,
ALICE.coreIndexId,
type(uint64).max
);

SpotBalancePrecompileMock(HLP_PRECOMPILE_READ_SPOT_BALANCE).setSpotBalance(
HYPE.assetBridgeAddress,
HYPE.coreIndexId,
type(uint64).max
);

oft = new MyOFT("test", "test", HL_LZ_ENDPOINT_V2, msg.sender);
import { HyperLiquidComposer } from "@layerzerolabs/hyperliquid-composer/contracts/HyperLiquidComposer.sol";

hyperLiquidComposer = new HyperLiquidComposer(
HL_LZ_ENDPOINT_V2,
address(oft),
ALICE.coreIndexId,
ALICE.decimalDiff
);
DST_EID = oft.endpoint().eid();
import { HyperliquidBaseTest } from "@layerzerolabs/hyperliquid-composer/test/HyperliquidBase.t.sol";
import { console } from "forge-std/Test.sol";

vm.deal(HL_LZ_ENDPOINT_V2, 100 ether);
contract HyperLiquidComposerTest is HyperliquidBaseTest {
function setUp() public override {
super.setUp();
}

function test_deployment() public view {
IHyperAsset memory hypeAsset = hyperLiquidComposer.getHypeAsset();
assertEq(hypeAsset.assetBridgeAddress, 0x2222222222222222222222222222222222222222);
assertEq(hypeAsset.coreIndexId, 1105);
uint256 expectedHypeCoreIndexId = block.chainid == 998 ? 1105 : 150;
assertEq(hypeAsset.coreIndexId, expectedHypeCoreIndexId);
assertEq(hypeAsset.decimalDiff, 10);

IHyperAsset memory oftAsset = hyperLiquidComposer.getOFTAsset();
assertEq(oftAsset.assetBridgeAddress, ALICE.assetBridgeAddress);
assertEq(oftAsset.coreIndexId, ALICE.coreIndexId);
assertEq(oftAsset.decimalDiff, ALICE.decimalDiff);
assertEq(oftAsset.assetBridgeAddress, ERC20.assetBridgeAddress);
assertEq(oftAsset.coreIndexId, ERC20.coreIndexId);
assertEq(oftAsset.decimalDiff, ERC20.decimalDiff);
}

function test_hypeIndexByChainId_testnet() public {
try vm.createSelectFork("https://rpc.hyperliquid-testnet.xyz/evm") {} catch {
console.log("Forking testnet https://rpc.hyperliquid-testnet.xyz/evm failed");
string memory rpcUrl = "https://rpc.hyperliquid-testnet.xyz/evm";
try vm.envString("RPC_URL_HYPERLIQUID_TESTNET") returns (string memory _rpcUrl) {
rpcUrl = _rpcUrl;
} catch {
console.log("Using default mainnet RPC URL");
}
// Skip test if fork fails
try vm.createSelectFork(rpcUrl) {} catch {
console.log("Forking testnet ", rpcUrl, " failed");
vm.skip(true);
}

MyOFT oftTestnet = new MyOFT("test", "test", HL_LZ_ENDPOINT_V2_TESTNET, msg.sender);
HyperLiquidComposer hypeComposerTestnet = new HyperLiquidComposer(
HL_LZ_ENDPOINT_V2_TESTNET,
address(oftTestnet),
ALICE.coreIndexId,
ALICE.decimalDiff
ERC20.coreIndexId,
ERC20.decimalDiff
);

assertEq(hypeComposerTestnet.hypeIndexByChainId(998), 1105);
}

function test_hypeIndexByChainId_mainnet() public {
try vm.createSelectFork("https://rpc.hyperliquid.xyz/evm") {} catch {
console.log("Forking mainnet https://rpc.hyperliquid.xyz/evm failed");
string memory rpcUrl = "https://rpc.hyperliquid.xyz/evm";
try vm.envString("RPC_URL_HYPERLIQUID_MAINNET") returns (string memory _rpcUrl) {
rpcUrl = _rpcUrl;
} catch {
console.log("Using default mainnet RPC URL");
}
// Skip test if fork fails
try vm.createSelectFork(rpcUrl) {} catch {
console.log("Forking mainnet ", rpcUrl, " failed");
vm.skip(true);
}

MyOFT oftMainnet = new MyOFT("test", "test", HL_LZ_ENDPOINT_V2_MAINNET, msg.sender);
HyperLiquidComposer hypeComposerMainnet = new HyperLiquidComposer(
HL_LZ_ENDPOINT_V2_MAINNET,
address(oftMainnet),
ALICE.coreIndexId,
ALICE.decimalDiff
ERC20.coreIndexId,
ERC20.decimalDiff
);

assertEq(hypeComposerMainnet.hypeIndexByChainId(999), 150);
Expand All @@ -140,7 +89,7 @@ contract HyperLiquidComposerTest is Test {
// Build composerMsg similar to the outcome of OFTCore.send()
bytes memory composerMsg_ = OFTComposeMsgCodec.encode(
0,
SRC_EID,
ETH_EID,
AMOUNT_TO_SEND + DUST,
abi.encodePacked(addressToBytes32(userA), composeMsg)
);
Expand All @@ -151,10 +100,10 @@ contract HyperLiquidComposerTest is Test {

// Expect the Transfer event to be emitted
vm.expectEmit(address(oft));
emit IERC20.Transfer(address(hyperLiquidComposer), ALICE.assetBridgeAddress, AMOUNT_TO_SEND);
emit IERC20.Transfer(address(hyperLiquidComposer), ERC20.assetBridgeAddress, AMOUNT_TO_SEND);

uint64 coreAmount = hyperLiquidComposer.quoteHyperCoreAmount(AMOUNT_TO_SEND, true).core;
bytes memory action = abi.encode(userB, ALICE.coreIndexId, coreAmount);
bytes memory action = abi.encode(userB, ERC20.coreIndexId, coreAmount);
bytes memory payload = abi.encodePacked(hyperLiquidComposer.SPOT_SEND_HEADER(), action);
vm.expectEmit(HLP_CORE_WRITER);
emit ICoreWriter.RawAction(address(hyperLiquidComposer), payload);
Expand All @@ -175,7 +124,7 @@ contract HyperLiquidComposerTest is Test {
// Build composerMsg similar to the outcome of OFTCore.send()
bytes memory composerMsg_ = OFTComposeMsgCodec.encode(
0,
SRC_EID,
ETH_EID,
AMOUNT_TO_SEND,
abi.encodePacked(addressToBytes32(userA), composeMsg)
);
Expand All @@ -189,10 +138,10 @@ contract HyperLiquidComposerTest is Test {

// Expect the Transfer event to be emitted
vm.expectEmit(address(oft));
emit IERC20.Transfer(address(hyperLiquidComposer), ALICE.assetBridgeAddress, AMOUNT_TO_SEND);
emit IERC20.Transfer(address(hyperLiquidComposer), ERC20.assetBridgeAddress, AMOUNT_TO_SEND);

uint64 coreAmount = hyperLiquidComposer.quoteHyperCoreAmount(AMOUNT_TO_SEND, true).core;
bytes memory action = abi.encode(userB, ALICE.coreIndexId, coreAmount);
bytes memory action = abi.encode(userB, ERC20.coreIndexId, coreAmount);
bytes memory payload = abi.encodePacked(hyperLiquidComposer.SPOT_SEND_HEADER(), action);
vm.expectEmit(HLP_CORE_WRITER);
emit ICoreWriter.RawAction(address(hyperLiquidComposer), payload);
Expand Down

Large diffs are not rendered by default.

Loading