Hyperliquid: HyperEVM precompiles, CoreWriter, and HyperCore bridge

In Part 1, we established the core architectural tradeoff behind Hyperliquid: a fully on-chain order book cannot behave like a centralized exchange if every order, cancel, and liquidation has to fight through a generic VM.
In Part 2, we went one level deeper. We looked at HyperBFT, spot vs perp accounting, sub-accounts, and the lifecycle of a trade from click to L1 state transition.
Now we reach the layer that matters most for builders: how does Solidity actually touch this specialized state machine?
HyperEVM is not just an EVM chain deployed next to HyperCore. It has native read and write lanes into HyperCore. The read lane is exposed through precompiles. The write lane is exposed through the CoreWriter system contract.
This is where Hyperliquid becomes interesting for protocol developers. A Solidity contract can read L1 order book state, oracle prices, spot balances, margin summaries, vault equity, and user positions without waiting for a third-party oracle. It can also send certain actions back into HyperCore by emitting a specially encoded system log.
In this post, we are going to unpack that bridge. We will query real HyperCore state from HyperEVM using raw eth_call, decode the integer pricing model, build Solidity wrappers around the precompiles, and then inspect how CoreWriter turns bytes into HyperCore actions.
Why HyperEVM needs native HyperCore reads
On a normal EVM chain, a smart contract only knows about EVM state.
If a lending protocol needs the BTC price, it calls an oracle. If a vault wants to know a user’s position on an exchange, it needs an off-chain keeper, a Merkle root, a signed message, or a bridge message. If a strategy contract wants to reason about an order book, it usually cannot read the order book directly because the order book lives somewhere else.
That separation creates a familiar latency stack:
- The exchange state changes.
- An off-chain process observes the change.
- The process pushes the update to a contract or oracle.
- The consuming contract reads the delayed representation.
This is fine for many DeFi applications, but it is a poor fit for an exchange-native chain. Hyperliquid has a different constraint. HyperCore already owns the financial state: orders, positions, spot balances, margin, vault equity, oracle prices, and liquidations. HyperEVM exists next to that state. So the obvious question is:
How can Solidity read HyperCore without pretending that HyperCore is an external chain? The answer is precompiles.
Precompiles are special addresses where the EVM does not execute deployed bytecode. Instead, the client recognizes the address and runs native logic. Ethereum already uses this model. HyperEVM extends the idea to expose HyperCore state.
The important architectural point is this:
HyperEVM contracts do not read an oracle copy of HyperCore. They call native precompiles that return HyperCore state as of the EVM block construction point.
That means the bridge is not a third-party data feed. It is part of the node’s execution environment.
This is the same shift we saw in Solana Token-2022 extensions. Instead of pushing meaning into external conventions, the protocol exposes explicit state through the execution layer itself.
The read precompile map
HyperEVM read precompiles live in a compact address range starting at:
0x0000000000000000000000000000000000000800
Each address corresponds to one native HyperCore query. Some of the most important ones are:
0x0000000000000000000000000000000000000800 position
0x0000000000000000000000000000000000000801 spotBalance
0x0000000000000000000000000000000000000802 userVaultEquity
0x0000000000000000000000000000000000000803 withdrawable
0x0000000000000000000000000000000000000806 markPx
0x0000000000000000000000000000000000000807 oraclePx
0x0000000000000000000000000000000000000808 spotPx
0x0000000000000000000000000000000000000809 l1BlockNumber
0x000000000000000000000000000000000000080a perpAssetInfo
0x000000000000000000000000000000000000080b spotInfo
0x000000000000000000000000000000000000080c tokenInfo
0x000000000000000000000000000000000000080e bbo
0x000000000000000000000000000000000000080f accountMarginSummary
0x0000000000000000000000000000000000000810 coreUserExists
The full list with struct definitions is in Hyperliquid’s L1Read.sol reference on the Interacting with HyperCore docs page.
The first thing to notice is that these are not normal Solidity functions.
If you write an interface like this:
interface WrongOracle {
function oraclePx(uint32 index) external view returns (uint64);
}
and call it at 0x0000000000000000000000000000000000000807, the EVM will send a 4-byte function selector before the argument. That is not what the precompile expects.
The precompile expects raw ABI-encoded input only. For oraclePx(uint32 index), the calldata is just:
abi.encode(uint32(index))

No selector. No method ID. Just the argument payload. That is why the official helper contract wraps the call using low-level staticcall.
The pattern looks like this:
library HyperCoreRead {
address constant ORACLE_PX = 0x0000000000000000000000000000000000000807;
function oraclePx(uint32 index) internal view returns (uint64) {
(bool ok, bytes memory out) = ORACLE_PX.staticcall(abi.encode(index));
require(ok, "oraclePx precompile failed");
return abi.decode(out, (uint64));
}
}
This is a subtle but important difference from a normal contract interface.
Note. A precompile is not a normal Solidity contract with a function dispatcher. It is a reserved address where the VM/client runs native protocol logic. In a normal contract, the first 4 bytes of calldata tell the contract which function to execute. In a precompile, the address itself already selects the native operation. The calldata format is whatever that precompile defines. For Hyperliquid read precompiles, that format is raw ABI-encoded arguments, so the helper uses low-level
staticcallto send exactly that payload.
Price reads: oracle price and BBO
Let’s query a real precompile directly. We will use testnet and read the oracle price for perp asset index 3.
The calldata is a single ABI-encoded uint32:
0x0000000000000000000000000000000000000000000000000000000000000003
Now call the oracle price precompile:
curl -s -X POST https://rpc.hyperliquid-testnet.xyz/evm \
-H "Content-Type: application/json" \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_call",
"params": [
{
"to": "0x0000000000000000000000000000000000000807",
"data": "0x0000000000000000000000000000000000000000000000000000000000000003"
},
"latest"
]
}'
The output at the time of testing was:
{
"jsonrpc": "2.0",
"id": 1,
"result": "0x000000000000000000000000000000000000000000000000000000000009a255"
}
Decode the result:
0x9a255 = 631381
At first glance, this looks wrong. BTC is not trading at 631381.
This is where the integer model matters. HyperCore does not return floating point numbers. It returns scaled integers. For perps, the Hyperliquid docs define the conversion as:
human price = raw price / 10^(6 - szDecimals)
The /info endpoint tells us that testnet perp index 3 is BTC and has:
{
"name": "BTC",
"szDecimals": 5,
"maxLeverage": 40,
"marginTableId": 54
}
You can check that with:
curl -s -X POST https://api.hyperliquid-testnet.xyz/info \
-H "Content-Type: application/json" \
--data '{"type":"meta"}'
So the divisor is:
10^(6 - 5) = 10
The human price is:
631381 / 10 = 63,138.1
Now the value makes sense.
The key idea is that the precompile is returning the L1 integer representation, not a frontend-formatted decimal string.

If you build a Solidity protocol on top of these values, this is the exact place where bugs happen. A contract that assumes all prices are 1e8 scaled will silently misprice assets with different szDecimals.
Reading the best bid and offer
The same pattern applies to the best bid and offer precompile.
The precompile lives at:
0x000000000000000000000000000000000000080e
It takes an asset index and returns two uint64 values:
struct Bbo {
uint64 bid;
uint64 ask;
}

All relevant structs are in L1Read.sol from the Hyperliquid docs.
Let’s query the same BTC perp index:
curl -s -X POST https://rpc.hyperliquid-testnet.xyz/evm \
-H "Content-Type: application/json" \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_call",
"params": [
{
"to": "0x000000000000000000000000000000000000080e",
"data": "0x0000000000000000000000000000000000000000000000000000000000000003"
},
"latest"
]
}'
The output was:
{
"jsonrpc": "2.0",
"id": 1,
"result": "0x000000000000000000000000000000000000000000000000000000000009a33a000000000000000000000000000000000000000000000000000000000009a42a"
}
This is two ABI words packed together:
bid raw = 0x9a33a = 631610
ask raw = 0x9a42a = 631850
Using the same BTC divisor:
bid = 631610 / 10 = 63161.0
ask = 631850 / 10 = 63185.0
The spread is visible directly from HyperCore state. Just an EVM static call into the native order book state.
Here is a Solidity wrapper:
library HyperCoreBbo {
address constant BBO = 0x000000000000000000000000000000000000080e;
struct Bbo {
uint64 bid;
uint64 ask;
}
function bbo(uint32 asset) internal view returns (Bbo memory) {
(bool ok, bytes memory out) = BBO.staticcall(abi.encode(asset));
require(ok, "bbo precompile failed");
return abi.decode(out, (Bbo));
}
}
Note. Under the hood, this is exactly the same ABI behavior as the raw curl call. Solidity just makes the decode more readable.
Account state: balances, positions, and margin
Prices are only the first layer. The more interesting precompiles expose account-level state.
For example, the spot balance precompile returns:
struct SpotBalance {
uint64 total;
uint64 hold;
uint64 entryNtl;
}
This maps directly to what we saw in Part 2. When a user places a resting spot buy order, their entire balance does not disappear. Some of it is moved into hold. The UI then shows “available to trade” by subtracting the locked capital.
In Solidity, the read looks like this:
library HyperCoreBalances {
address constant SPOT_BALANCE = 0x0000000000000000000000000000000000000801;
struct SpotBalance {
uint64 total;
uint64 hold;
uint64 entryNtl;
}
function spotBalance(address user, uint64 token) internal view returns (SpotBalance memory) {
(bool ok, bytes memory out) = SPOT_BALANCE.staticcall(abi.encode(user, token));
require(ok, "spotBalance precompile failed");
return abi.decode(out, (SpotBalance));
}
}
A protocol can now reason about the difference between total balance and usable balance.
That matters for several use cases:
- A vault contract can check if a user still has enough idle spot collateral.
- A liquidation helper can verify whether a user is already locked into open orders.
- A structured product can gate deposits based on Core balances instead of asking the user to move everything into an ERC20 wrapper first.
Perp positions expose another layer:
struct Position {
int64 szi;
uint64 entryNtl;
int64 isolatedRawUsd;
uint32 leverage;
bool isIsolated;
}
The sign of szi tells you the direction:
szi > 0 long
szi < 0 short
szi = 0 no position
The margin mode is also explicit. isIsolated tells you whether the position is using isolated margin. isolatedRawUsd only makes sense in that context.
This is very different from a normal EVM integration where the exchange state must be reconstructed from logs or queried from an off-chain API. With HyperEVM precompiles, the contract is not parsing history. It is reading the current native state.
The account margin summary precompile goes even higher level:
struct AccountMarginSummary {
int64 accountValue;
uint64 marginUsed;
uint64 ntlPos;
int64 rawUsd;
}
This is the kind of primitive that normally lives inside a centralized exchange risk engine. On Hyperliquid, it is exposed to the EVM.
That creates a different design space:
- A contract can require a minimum account value before allowing an action.
- A vault can observe margin usage without mirroring every fill.
- A liquidation or insurance mechanism can compare user risk against live HyperCore values.
- A strategy contract can check both market price and user state inside one EVM transaction.
The important limitation is that all of this is read-only.
Precompiles let Solidity observe HyperCore. They do not mutate it. For mutation, HyperEVM uses a different path, and that is CoreWriter.
CoreWriter: the write lane back into HyperCore
Read precompiles are synchronous. You call them, the EVM returns data, and your contract continues execution. Writes are different. The full CoreWriter.sol reference is on the Interacting with HyperCore docs page.

HyperCore is not an ERC20 contract. You cannot call placeOrder() on a Solidity order book. The order book is the native L1 state machine.
So HyperEVM writes use a system contract:
0x3333333333333333333333333333333333333333
The deployed CoreWriter contract on HyperEVMScan exposes a single conceptual operation:
interface ICoreWriter {
function sendRawAction(bytes calldata data) external;
}
The contract emits a log. HyperCore later reads that log and processes the encoded action. This is a very different mental model from calling another EVM contract.
When you call CoreWriter, you are not directly mutating the order book inside the same EVM frame. You are enqueuing a HyperCore action through an EVM-originated event.
The action bytes have a small header:
byte 0 encoding version
bytes 1-3 action id, big-endian
bytes 4+ raw ABI-encoded action fields
In Solidity zero-indexed terms:
data[0] = 0x01; // version 1
data[1] = 0x00;
data[2] = 0x00;
data[3] = actionId;
data[4..] = abi.encode(...);
For example, the USD class transfer action has action ID 7 and fields (uint64 ntl, bool toPerp). The full list of actions and their IDs is in the Interacting with HyperCore docs.
A minimal helper looks like this:
interface ICoreWriter {
function sendRawAction(bytes calldata data) external;
}
contract UsdClassTransfer {
address constant CORE_WRITER = 0x3333333333333333333333333333333333333333;
function transferUsdClass(uint64 ntl, bool toPerp) external {
bytes memory payload = abi.encode(ntl, toPerp);
bytes memory data = bytes.concat(hex"01000007", payload);
ICoreWriter(CORE_WRITER).sendRawAction(data);
}
}
This is where the architecture becomes visible.
The EVM contract does not know how to update HyperCore balances directly. It only knows how to emit a correctly encoded instruction. HyperCore remains the authority that validates and executes the state transition.
The same pattern applies to other CoreWriter actions.
For a limit order, the action ID is 1, and the fields are:
(
uint32 asset,
bool isBuy,
uint64 limitPx,
uint64 sz,
bool reduceOnly,
uint8 encodedTif,
uint128 cloid
)
The time-in-force encoding is:
1 = ALO
2 = GTC
3 = IOC
The docs specify that limitPx and sz are sent as 10^8 * human readable value for this CoreWriter action. That is another place where developers can get burned. The read precompile price scaling and the write action encoding are not the same thing.

An order helper:
contract HyperCoreLimitOrder {
address constant CORE_WRITER = 0x3333333333333333333333333333333333333333;
uint8 constant TIF_ALO = 1;
uint8 constant TIF_GTC = 2;
uint8 constant TIF_IOC = 3;
function placeLimitOrder(
uint32 asset,
bool isBuy,
uint64 limitPx,
uint64 sz,
bool reduceOnly,
uint8 tif,
uint128 cloid
) external {
bytes memory payload = abi.encode(asset, isBuy, limitPx, sz, reduceOnly, tif, cloid);
bytes memory data = bytes.concat(hex"01000001", payload);
ICoreWriter(CORE_WRITER).sendRawAction(data);
}
}
This is intentionally low-level. In production you would not expose raw limitPx, sz, and tif without validation.
Timing and safety traps
The hardest part of this design is not the byte encoding. It is timing.
HyperEVM does not replace HyperCore. It runs under the same umbrella L1, but EVM blocks and HyperCore state transitions are distinct phases.
On an L1 block that produces a HyperEVM block, the ordering is:
- The L1 block is built.
- The EVM block is built.
- EVM to Core transfers are processed.
- CoreWriter actions are processed.
The full block-phase description is in the HyperCore ↔ HyperEVM transfers docs.
This has a concrete consequence. If a contract tries to initialize a Core account through an EVM to Core transfer and then use CoreWriter from that same account in the same block, the CoreWriter action can still fail. The account has to exist on HyperCore before the EVM block is built.
This is the kind of bug that looks impossible if you only think in EVM terms.
In Solidity, you are used to synchronous composition:
call A
call B
state from A is visible to B
But CoreWriter crosses an execution boundary. It emits an action that HyperCore processes after the EVM block phase.
So the mental model should be:
- EVM execution emits intent.
- HyperCore later validates intent.
- HyperCore mutates native financial state.
There is also a deliberate fairness rule:
Order actions and vault transfers sent through CoreWriter are delayed onchain for a few seconds. The reason is to prevent contracts from using HyperEVM as a backdoor to skip the normal L1 mempool path and gain a latency advantage.
This means a CoreWriter order is not a free high-frequency trading primitive.
It is better understood as a composability primitive.
Practical safety checklist
Before building on these primitives, there are a few rules worth making explicit.
Treat invalid precompile inputs as expensive
The read precompiles can fail on invalid inputs, such as invalid asset IDs or invalid vault addresses. When that happens, the failed call can consume all gas passed into that precompile call frame.
Do not forward unbounded gas into arbitrary user-selected precompile calls.
Wrap reads behind allowlists:
require(asset < maxKnownAsset, "invalid asset");
For vault reads, validate vault addresses before calling.
Keep decimal conversion close to the read
Do not scatter raw price conversion across the codebase.
If oraclePx returns raw integers, create one helper that takes szDecimals and returns the normalized value.
Example:
function normalizePerpPrice(uint64 raw, uint8 szDecimals) internal pure returns (uint256) {
uint8 divisorDecimals = 6 - szDecimals;
return uint256(raw) * 1e18 / (10 ** divisorDecimals);
}
Now every downstream contract consumes 1e18 scaled values instead of trying to remember HyperCore’s raw format.
Separate read scaling from write scaling
Read precompile price scaling is based on asset metadata.
CoreWriter limit order fields use their own action encoding, where limitPx and sz are encoded as 10^8 * human readable value.
Do not reuse one scaling helper for both directions unless it explicitly distinguishes read normalization from write encoding.
Remember that CoreWriter actions are emitted by msg.sender
When a contract calls CoreWriter, the action is on behalf of the contract address, not the EOA that called your contract. That means your contract address itself must be meaningful on HyperCore for the action you are sending.
This is different from the API wallet model where an agent signs on behalf of a master account.
Build for asynchronous confirmation
A CoreWriter call only proves that the EVM transaction emitted the action. It does not prove that HyperCore accepted and executed the action. Your application should track the later HyperCore execution result.
A clean architecture looks like:
- EVM contract emits local event with action details.
- Indexer watches the EVM event.
- Indexer watches L1 explorer or HyperCore state for execution.
- UI shows “enqueued” first, then “executed” or “rejected”.
This matches the actual architecture instead of pretending the write path is synchronous.
Summary
HyperEVM precompiles and CoreWriter are the bridge between two very different execution models.
The read side is synchronous and contract-friendly. A Solidity contract can call native precompiles to observe HyperCore prices, balances, positions, vault equity, staking state, and margin summaries. The data comes back as raw ABI-encoded integers and structs, which means the developer has to handle scaling and decoding correctly.
The write side is intentionally different. CoreWriter does not expose a friendly order book contract. It emits a raw action log with a version byte, an action ID, and ABI-encoded fields. HyperCore later consumes that action and decides whether it is valid.
This split preserves the architectural boundary we have been discussing since Part 1.
HyperCore remains the high-performance financial state machine. HyperEVM gives builders a programmable surface over that state machine. The precompiles let contracts see the native exchange. CoreWriter lets contracts express intent back to it.
The most important mental shift is this: HyperEVM composability is not about porting a CLOB into Solidity. It is about giving Solidity controlled read and write access to a CLOB that already exists at the L1 level.
FAQ
HyperEVM precompiles are reserved addresses starting at 0x0000000000000000000000000000000000000800 where the node runs native logic instead of executing deployed bytecode. They give Solidity contracts direct read access to HyperCore state — oracle prices, spot balances, perp positions, vault equity, and margin summaries — with no third-party oracle in between. The full list of precompiles and their return types is defined in Hyperliquid’s L1Read.sol reference.
CoreWriter is a system contract at 0x3333333333333333333333333333333333333333 that lets HyperEVM smart contracts send actions to HyperCore. It exposes a single function, sendRawAction(bytes calldata data), which emits a log that HyperCore later reads and executes. CoreWriter is the write lane back into HyperCore for actions such as limit orders, USD class transfers, vault deposits, and staking operations.
Yes. A staticcall to the right precompile address returns HyperCore state as raw ABI-encoded data, guaranteed to match HyperCore state at the time the EVM block is constructed. There is no external oracle in the loop — the data comes from the node’s execution environment itself. Contracts can read positions, spot balances, prices, vault equity, margin summaries, and more without waiting for a keeper or bridge message.
No. A CoreWriter call in the EVM transaction only emits an action log. HyperCore processes that action in a later phase, after the EVM block is built and after EVM-to-Core transfers are processed. This means a successful CoreWriter call proves that the action was enqueued, not that HyperCore accepted or executed it. Applications should track the eventual HyperCore execution result separately — for example, by watching L1 explorer events or reading updated precompile state on a subsequent block.
Order actions and vault transfers sent through CoreWriter are intentionally delayed on-chain for a few seconds. The reason is fairness: without the delay, HyperEVM contracts could bypass the normal L1 mempool path and gain a latency advantage over traders submitting orders directly. This means a CoreWriter order should be understood as a composability primitive, not a high-frequency trading primitive. Regular CoreWriter actions that are not order-related, such as USD class transfers or staking deposits, are not subject to this delay.
oraclePx look wrong? The precompile returns the L1 integer representation, not a human-readable decimal. For perps, divide the raw value by 10^(6 - szDecimals) to get the human-readable price. For spot markets, divide by 10^(8 - base asset szDecimals). Applying the wrong scaling factor — or worse, assuming all prices are 1e8 scaled — is one of the most common bugs when building on top of HyperCore precompiles. Read perpAssetInfo or spotInfo for the correct szDecimals before scaling.
