testing/SKILL.md
Smart contract testing for HyperEVM with Foundry/Hardhat — unit tests, fuzz testing, testnet fork testing. What to test, what not to test, and what LLMs get wrong on Hyperliquid.
npx skillsauth add cloudzombie/liquidskills testingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You test getters and trivial functions. Testing that name() returns the name is worthless. Test edge cases, failure modes, and economic invariants — the things that lose money when they break.
You don't fuzz. forge test finds the bugs you thought of. Fuzzing finds the ones you didn't. If your contract does math (especially bonding curves, AMMs), fuzz it. If it handles HYPE amounts, fuzz it.
You don't test against testnet state. HyperEVM testnet (chain ID 998, https://rpc.hyperliquid-testnet.xyz/evm) is live. Use it. Deploy there before mainnet.
You forget HyperEVM ≠ standard Ethereum. Priority fees are burned (not to validators). Cancun hardfork without blobs. No blobs means no blob-based gas strategies. EIP-1559 is enabled. Test with these constraints in mind.
You skip HyperCore integration testing. If your contract interacts with HyperCore assets (via the system precompile or bridge), mock it in tests but also validate on testnet where HyperCore state is real.
forge init my-hyperliquid-project
cd my-hyperliquid-project
Configure foundry.toml for HyperEVM:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
evm_version = "cancun"
[rpc_endpoints]
hyperliquid_mainnet = "https://rpc.hyperliquid.xyz/evm"
hyperliquid_testnet = "https://rpc.hyperliquid-testnet.xyz/evm"
// test/BondingCurve.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/BondingCurve.sol";
contract BondingCurveTest is Test {
BondingCurve curve;
address user = makeAddr("user");
function setUp() public {
curve = new BondingCurve();
vm.deal(user, 100 ether); // Fund with HYPE
}
function test_BuyIncreasesPrice() public {
uint256 priceBefore = curve.getCurrentPrice();
vm.prank(user);
curve.buy{value: 1 ether}(0); // 0 = min tokens out
uint256 priceAfter = curve.getCurrentPrice();
assertGt(priceAfter, priceBefore, "Price should increase after buy");
}
function test_SellDecreasesPrice() public {
// Buy first
vm.startPrank(user);
curve.buy{value: 5 ether}(0);
uint256 tokens = curve.balanceOf(user);
uint256 priceBefore = curve.getCurrentPrice();
curve.sell(tokens / 2, 0); // sell half, 0 = min HYPE out
uint256 priceAfter = curve.getCurrentPrice();
vm.stopPrank();
assertLt(priceAfter, priceBefore, "Price should decrease after sell");
}
function test_RevertOnZeroBuy() public {
vm.prank(user);
vm.expectRevert("Amount must be > 0");
curve.buy{value: 0}(0);
}
}
Always fuzz bonding curves, AMMs, and anything that moves value.
function testFuzz_BuyAndSellRoundTrip(uint96 hypeAmount) public {
// Bound to reasonable HYPE amounts (0.001 to 10 HYPE)
hypeAmount = uint96(bound(hypeAmount, 0.001 ether, 10 ether));
vm.deal(user, hypeAmount);
vm.startPrank(user);
uint256 tokensBefore = curve.totalSupply();
curve.buy{value: hypeAmount}(0);
uint256 tokensReceived = curve.balanceOf(user);
// Sell all tokens
curve.sell(tokensReceived, 0);
uint256 hypeReceived = user.balance;
vm.stopPrank();
// After round trip, supply should be back to original
assertEq(curve.totalSupply(), tokensBefore, "Supply should be unchanged");
// User loses only fees (1%)
assertGe(hypeReceived, (hypeAmount * 98) / 100, "Should recover at least 98% due to fees");
}
function testFuzz_XYKInvariant(uint96 buyAmount, uint96 sellAmount) public {
buyAmount = uint96(bound(buyAmount, 0.01 ether, 50 ether));
sellAmount = uint96(bound(sellAmount, 0.001 ether, 5 ether));
vm.deal(user, buyAmount);
vm.prank(user);
curve.buy{value: buyAmount}(0);
uint256 k_before = curve.reserveHype() * curve.reserveTokens();
uint256 tokens = uint256(bound(sellAmount, 0, curve.balanceOf(user)));
if (tokens > 0) {
vm.prank(user);
curve.sell(tokens, 0);
}
uint256 k_after = curve.reserveHype() * curve.reserveTokens();
// k should only increase (fees go in)
assertGe(k_after, k_before, "k should never decrease");
}
For stateful contracts (vaults, AMMs, bonding curves):
contract BondingCurveInvariantTest is Test {
BondingCurve curve;
function setUp() public {
curve = new BondingCurve();
// Seed with initial state
vm.deal(address(this), 1000 ether);
curve.buy{value: 10 ether}(0);
// Target the curve for invariant testing
targetContract(address(curve));
}
// Invariant: contract HYPE balance always >= reported reserve
function invariant_BalanceMatchesReserve() public view {
assertGe(
address(curve).balance,
curve.reserveHype(),
"Contract balance must cover reserve"
);
}
// Invariant: total supply = sum of all balances (ERC-20)
function invariant_TotalSupplyConsistent() public view {
// Foundry tracks callers automatically
assertEq(
curve.totalSupply(),
curve.reserveTokens() + curve.graduationPool(),
"Total supply must equal reserves + graduation pool"
);
}
}
Test against real HyperEVM state:
# Fork testnet
forge test --fork-url https://rpc.hyperliquid-testnet.xyz/evm --fork-block-number latest -vvv
contract ForkTest is Test {
// Real HyperSwap V2 router on testnet (verify address before use)
address constant HYPERSWAP_ROUTER = 0x...; // check docs for current address
function setUp() public {
// Fork runs against real testnet state
vm.createSelectFork("hyperliquid_testnet");
}
function test_HyperSwapRouterExists() public view {
assertTrue(HYPERSWAP_ROUTER.code.length > 0, "Router must be deployed");
}
function test_BridgeAddressIsSystem() public view {
// 0x2222...2222 is the HYPE bridge system address
address bridge = 0x2222222222222222222222222222222222222222;
// It exists as a special system contract
assertTrue(bridge != address(0));
}
}
If using Hardhat instead of Foundry:
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
hardhat.config.cjs:
require("@nomicfoundation/hardhat-toolbox");
module.exports = {
solidity: "0.8.20",
networks: {
hyperliquid_testnet: {
url: "https://rpc.hyperliquid-testnet.xyz/evm",
chainId: 998,
accounts: [process.env.PRIVATE_KEY],
},
hyperliquid_mainnet: {
url: "https://rpc.hyperliquid.xyz/evm",
chainId: 999,
accounts: [process.env.PRIVATE_KEY],
},
},
};
// test/BondingCurve.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("BondingCurve", function () {
let curve, user;
beforeEach(async function () {
[, user] = await ethers.getSigners();
const BondingCurve = await ethers.getContractFactory("BondingCurve");
curve = await BondingCurve.deploy();
});
it("buy increases price", async function () {
const priceBefore = await curve.getCurrentPrice();
await curve.connect(user).buy(0, { value: ethers.parseEther("1") });
const priceAfter = await curve.getCurrentPrice();
expect(priceAfter).to.be.gt(priceBefore);
});
it("graduation happens at 100 HYPE", async function () {
// Fund user
await ethers.provider.send("hardhat_setBalance", [
user.address,
"0x" + (200n * 10n**18n).toString(16)
]);
// Buy until graduation
await curve.connect(user).buy(0, { value: ethers.parseEther("100") });
expect(await curve.graduated()).to.be.true;
});
});
Always test:
minOut parameters)Don't bother testing:
[ ] All unit tests pass locally
[ ] Fuzz tests run with at least 10,000 runs (FOUNDRY_FUZZ_RUNS=10000)
[ ] Invariant tests find no violations
[ ] Contract compiles without warnings
[ ] No hardcoded addresses — use constructor params or config files
[ ] Deployment script tested locally with `--dry-run`
[ ] Contract verified on testnet explorer after deploy
[ ] Manual end-to-end test on testnet (buy, sell, graduate if applicable)
# Foundry
forge script script/Deploy.s.sol \
--rpc-url https://rpc.hyperliquid-testnet.xyz/evm \
--private-key $PRIVATE_KEY \
--broadcast \
--verify \
--verifier-url https://explorer.hyperliquid-testnet.xyz/api \
-vvvv
# Hardhat
npx hardhat run scripts/deploy.js --network hyperliquid_testnet
development
Why build on Hyperliquid. HyperBFT consensus, native orderbook, speed, AI agent angle, honest tradeoffs. Use when someone asks "should I build on Hyperliquid?", "why not Ethereum?", or when an agent needs to understand what makes Hyperliquid unique.
development
Wallets on Hyperliquid — MetaMask + chain ID 999 setup, HyperCore API wallets, agent wallet patterns, EIP-712 signing for exchange actions. Essential for any agent that needs to interact with Hyperliquid.
tools
Development tools for Hyperliquid — Foundry, Hardhat, viem, wagmi for HyperEVM; Python SDK and TypeScript SDK for HyperCore API. What works, what to use, how to set up.
tools
Token standards on Hyperliquid — ERC-20 on HyperEVM, HIP-1/HIP-2/HIP-3 native token standards on HyperCore. When to use each, how they work, key interfaces.