Testing Framework
How to use it
First, ensure you have installed hyper-evm-lib
in your foundry project. Instructions here
Then, in your test file, import CoreSimulatorLib
as shown in the example, and call CoreSimulatorLib.init()
in the setUp()
.
Then, call CoreSimulatorLib.nextBlock()
in your tests to simulate moving to the next block and execute any queued CoreWriter (or token bridging) actions.
Example:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Test} from "forge-std/Test.sol";
import {CoreSimulatorLib} from "@hyper-evm-lib/test/simulation/CoreSimulatorLib.sol";
contract ExampleTest is Test {
function setUp() public {
vm.createSelectFork("https://rpc.hyperliquid.xyz/evm");
// initialize the HyperCore simulator
CoreSimulatorLib.init();
}
function test() public {
// Make any smart contract calls,
// all CoreWriter and token bridging actions will be queued
...
// move to the next block,
// performing all queued CoreWriter and bridging actions
CoreSimulatorLib.nextBlock();
// Now, all precompiles calls will be
//updated to account for the above executed actions
}
}
Alternatively, you could inherit BaseSimulatorTest
for your tests, which sets up the CoreSimulatorLib
in it's setUp()
function
Test utility functions
These functions in CoreSimulatorLib
work similar to Foundry's cheatcodes and allow you to manipulate state and configuration for testing purposes.
Test Config
// force activate a Core account (without having to transfer tokens and pay the $1 fee)
function forceAccountActivation(address account) internal;
Price Setters
// set the mark price of a perp market
function setMarkPx(uint32 perp, uint64 markPx) internal;
// set the mark price by specifying a price difference in bps
function setMarkPx(uint32 perp, uint64 priceDiffBps, bool isIncrease) internal;
// set the price of a spot market
function setSpotPx(uint32 spotMarketId, uint64 spotPx) internal;
// set the spot price by specifying a price difference in bps
function setSpotPx(uint32 spotMarketId, uint64 priceDiffBps, bool isIncrease) internal;
// scale vault equity to simulate profit/loss scenarios
function setVaultMultiplier(address vault, uint64 multiplier) internal;
Balance Setters
// set an account's spot balance for a token
function forceSpotBalance(address account, uint64 token, uint64 _wei) internal;
// set an account's withdrawable perp USD
function forcePerpBalance(address account, uint64 usd) internal;
// set an account's staking balance
function forceStakingBalance(address account, uint64 _wei) internal;
// set an account's delegation amount and locked until timestamp for a validator
function forceDelegation(address account, address validator, uint64 amount, uint64 lockedUntilTimestamp) internal;
// set an account's vault equity and lock timestamp for a vault
function forceVaultEquity(address account, address vault, uint64 usd, uint64 lockedUntilTimestamp) internal;
How it works
HyperCore
replicates HyperCore state and behaviour for tests. It holds all state (accounts, spot balances, perp positions and margin, staking, vault equity) and exposes read functions used by precompiles.
Precompile calls are handled by PrecompileSim.sol
, which is etched at each precompile address. When a precompile is called, its fallback forwards the read into HyperCore
via the matching CoreView
function. CoreView
functions check if the value is already initialized in local state and returns that; if not, it performs a live RPC read via RealL1Read.sol
and returns the onchain value.
CoreWriter actions are handled through CoreWriterSim.sol
. It stores all incoming CoreWriter calls in a queue. When CoreSimulatorLib.nextBlock()
is called, it executes the queued actions by calling the relevant execute functions on HyperCore
and applies their state changes. Before applying each write, CoreExecution runs initializers to initialise the relevant state by fetching current onchain values. Then, the local state is updated, and subsequent precompile reads will use the stored state.
Check out these test contracts here for more info.