Learning HyperEVM by building a perp whose pool takes the other side of every trade and hedges traders' net position with its own position on HyperCore, sent through CoreWriter. The third build after hyperevm-amm (an exchange that trades on HyperCore) and hyperevm-lend (a lending market that reads HyperCore prices).
Status (2026-10-19): a learning project, not audited, not for real funds. It has only been
run on testnet; HedgedPerp itself is not restricted to any chain (only the testnet copy is),
so do not deploy it with real money.
- Codex reviewed it adversarially six times (r6 on the testnet-only copy, r6b APPROVE). The hedge's "is my order done?" took four rounds and ended as a keeper acknowledgement, below.
- One hedged round trip ran on testnet with real prices: open, hedge on Core, close, unwind the
hedge, every USDC back (
docs/step3-testnet.md). - Liquidation, a margin-rejected hedge and an IOC that found nothing ran on testnet with a
testnet-only copy whose price the keeper sets (
docs/step4-liquidation-and-rejections.md). - An order recorder (
recorder/orders.py, reviews rec1–rec3 APPROVE) is built but has not been run on testnet yet. - Step 5 is specified and built, and testnet session 1 ran (2026-10-29; spec s5h APPROVE,
implementation s5i5, amendments s5i7b APPROVE): G1 failed (historical
0x800reads return the current state), and session 1 stopped at cycle 22 of 120 when the testnet book drifted over 1% from the oracle: how stale a contract's view of its own Core position is, and how quickly and reliably it can flatten that position with reduce-only orders sent through CoreWriter (docs/step5-close-from-evm-spec.md). The testnet copy gainedrawIocReduceOnly; the recorder'sunwindis now reduce-only.
For a library that wraps CoreWriter and the precompiles, use hyper-evm-lib; to read HL prices safely, hl-price-guard. This repo is a worked example: code to copy from, and what happened when it ran on testnet.
| I want to… | Look at |
|---|---|
| hedge a pool's net exposure on HyperCore from a contract | HedgedPerp.hedge: read the Core position, send one IOC for the gap |
| know whether a CoreWriter order is done | "Why the hedge's acknowledgement is off-chain" below: three on-chain answers that broke, and acknowledge(cloid) from the info API |
| see every outcome a CoreWriter perp order can have, with timings | docs/step4-liquidation-and-rejections.md (filled, iocCancelRejected, perpMarginRejected) |
| settle trades so neither trader nor keeper gets a free option | requestOpen / settleOpen: anyone settles 2–15 s later at the checked oracle |
| liquidate without letting a wide book block it | HedgedPerp.liquidationPrice: the price better for the trader of oracle and mark |
| record CoreWriter orders on testnet and fail closed | recorder/orders.py |
| deploy a contract over the 2M small-block gas limit | script/big_blocks.py (big blocks on, deploy, off) |
git clone --recurse-submodules https://github.com/psyto/hyperevm-perp && cd hyperevm-perp
forge test46 tests: HyperCore precompiles and CoreWriter are mocked at their real addresses, so nothing touches a network.
- A CoreWriter perp order is found in
orderStatusby its cloid:unknownOidfor under a second, thenfilled,iocCancelRejectedorperpMarginRejected. A margin-rejected order is registered, not missing. - A contract's Core account starts with no perp margin; an order then is rejected
(
perpMarginRejected), not dropped silently. - Deploying a ~14 KB contract needs big blocks (
evmUserModify); turn them off afterwards, since every transaction from the address then waits for a big block. - Basis shows up even on a quiet book: trader entry at the oracle 82,874.0, hedge fill 82,904.0. Core's taker fee was 4.5 bp a side, against the 10 bp charged to traders.
- USDC EVM → Core through
CoreDepositWalletdid not arrive for an EOA either on 2026-10-19 (it did on 2026-10-17). HYPE's bridge worked, so USDC was made on Core by selling HYPE on spot.
- One market, BTC on testnet. Traders post USDC margin (up to 20x) and go long or short. The pool is every trader's counterparty.
- Two-step trades, settled by anyone. The trader requests; anyone settles at the checked oracle 2–15 s later. Within one block the price is one value, so when the trader and the keeper are both live, whoever likes that block's price settles it: neither gets a free option. After 15 s the trader can cancel.
- The hedge.
hedge()reads the pool's Core position (0x800) and sends one IOC for the gap to traders' net position, with a fresh cloid. Nothing else goes out until the keeperacknowledges that cloid after reading a final status from the info API (filled,iocCancelRejected,perpMarginRejected, …), never onunknownOid. The contract keeps no record of what the hedge holds; it reads Core each time. - Liquidation never refuses a price. Liquidatable when equity is below 2.5% of notional at the price better for the trader: the higher of oracle and mark for a long, the lower for a short. One feed moving alone liquidates no one, and a wide or empty book does not block it; a move counts only once both show it (see Known limits). The equity left goes to the caller; a shortfall is the pool's.
- Payouts never depend on Core. Profits are capped at 5× margin, and a position opens only while the pool's EVM USDC alone covers every open position's cap. Core money is the hedge's and is not counted. Payouts are credits, claimed from the EVM balance.
| Parameter | Value |
|---|---|
| Max leverage | 20x |
| Fee | 10 bp of notional at open and at close |
| Liquidatable below | 2.5% of notional (−2.5% at 20x) |
| Profit cap | 5× margin |
| Settle window | 2–15 s after the request |
| Hedge order | one IOC, limit 30 bp beyond the touch and within 1% of the oracle; at most maxHedgeLots; at least $12 |
| Price checks for settles | constructor bounds on mark/oracle, mid/oracle, spread |
- The keeper is trusted for the hedge. It is also the only liquidity provider. An early or wrong acknowledgement can put two orders on Core; that risks only Core money, never what traders are owed.
- The hedge lags. Between two hedges the Core position trails traders' exposure by whatever settled, with no bound in size or time but the keeper's pace. On top of that, the one outstanding order can run after exposure moved and point the wrong way, by at most its size. Core liquidations or ADL of the hedge move the Core position on their own; the next hedge orders the gap from what Core shows.
- Liquidation can be held back. A move against a trader counts only once both the oracle and the mark show it. The mark follows HL's book, so a trader (or anyone) who pushes the BTC book their way keeps the liquidation price on their side for as long as they pay to hold the push; a real move the mark lags waits the same way. Meanwhile the loss can pass the margin, and the pool pays the shortfall. Neither delay is measured or bounded here (hl-price-guard docs/safe-limits.md section 5).
- Capital-hungry. Because payouts do not count Core money, the EVM must hold 5× every open margin.
- Not handled: funding (traders pay none; the hedge pays or receives Core's), basis (traders settle at the oracle, the hedge fills at the book: 3.6 bp in step 3), gaps under $12, other liquidity providers (needs share pricing against open PnL), Core HYPE (gas) cannot be sent back to the EVM.
A CoreWriter action runs after the EVM block, and the EVM is never told how it ended. Reviews r3–r5 broke three on-chain answers to "is my order done?":
| Tried | Broke because |
|---|---|
| a timer between orders (r3) | while Core was slow, three calls sent three identical orders |
| "the Core position moved" (r4) | Core liquidations and ADL move it too; an order can net back to its start; a timeout does not cancel a queued action |
| acknowledging "never registered" orders (r5) | orderStatus answers unknownOid for those and for an order still inside CoreWriter's delay alike |
On testnet every order then showed unknownOid for under a second and a final status within
about 2 s.
| Path | What |
|---|---|
src/HedgedPerp.sol |
the market |
src/testing/TestnetPricedPerp.sol |
testnet only (refuses any chain but 998): keeper-set price for liquidation, settles within 1% of the real oracle, a raw IOC (and its reduce-only twin) for measuring orderStatus and closes |
test/ |
46 tests; precompiles and CoreWriter mocked at their real addresses |
docs/design.md |
the design and what is left |
docs/step3-testnet.md |
the hedged round trip on testnet |
docs/step4-liquidation-and-rejections.md |
liquidation, perpMarginRejected, iocCancelRejected; all four order outcomes and their timings |
docs/step5-close-from-evm-spec.md |
spec only: precompile visibility of a contract's Core position, reduce-only close latency and outcomes, a read-only mainnet book estimate |
recorder/orders.py |
testnet order recorder: sends raw IOCs one at a time, records every orderStatus change by cloid, fails closed; step 5 commands g1, step5, lag, and book (mainnet, read only) |
docs/reviews/ |
Codex review rounds: payloads, outputs, PoCs, and what was fixed (r1.md–r6.md; recorder rec1–rec3; step 5 spec s5–s5h) |
script/big_blocks.py |
big blocks on/off for the deployer (deployment needs ~2.8M gas, over the 2M small block) |
lib/hyperevm-amm, lib/hl-price-guard |
pinned git submodules: CoreWriter/precompile helpers, guarded price reads (hl-price-guard v0.3.1; tests set prices with its HLTest) |
Codex, adversarial, a failing test for every finding (docs/reviews/).
| Round | Scope | Findings | Outcome |
|---|---|---|---|
| 1 | whole market | 2 high, 1 medium | capital is EVM USDC only; anyone settles within 2–15 s; no hedge without a Core account |
| 2 | r1 fixes | 1 high | the documented hedge bound was wrong; corrected |
| 3 | r2 fixes | 1 high, 1 medium | a timer let orders pile up; rebuilt as one unconfirmed order |
| 4 | the rebuild | 1 high, 1 medium | a position change is no acknowledgement; keeper acknowledges by cloid; settles never wait |
| 5 | acknowledgement | 1 high (procedure) | never acknowledge on unknownOid |
| 6, 6b | the testnet-only copy | 1 medium, 1 low; then APPROVE | the raw IOC takes the one order slot; settle price within 1% of the oracle |
| rec1–rec3 | the order recorder | CHANGES twice; then APPROVE | fails closed on any surprise; status and unwind for manual recovery |
| s5–s5h | step 5 spec | 4 high, 16 medium, 3 low over s5–s5g; s5h APPROVE | reduce-only for every close and recovery; a gate on whether historical 0x800 reads work; local-clock brackets only; per-order acknowledge table |
| s5i1–s5i5 | step 5 implementation | 5 medium, 5 low over s5i1–s5i4; s5i5 APPROVE | G1 strict per block and bound to its pool; exact price and size conversion; read pace with degraded brackets; slot re-read before every acknowledgement; base fee schedule recorded |
| s5i6, s5r1, s5i7–s5i7b | signing fix; testnet session 1 | s5i6 APPROVE; s5r1 and s5i7 CHANGES, s5i7b APPROVE | --private-key for cast 1.7; opens 2% through (the testnet book drifted >1%); ackopen for a stopped open; first-read matches are upper bounds; every session ends on record |
- Step 5 on testnet: sessions 2 and 3 of
step5on other days (98 cycles left),lag; the week of mainnetbookruns until 2026-11-05 (read only); write it up. - A full arithmetic audit round, as hyperevm-lend's r8 did.
- Funding passed through to traders, accrued per second.
- A keeper that polls
orderStatusand acknowledges by itself (by hand on testnet so far). - A way to send the pool's Core HYPE back to the EVM.
Apache-2.0