Reverse Engineering Hyperliquid ft. SetYesterdayUserVlm
What happens when you take IDA Pro to a $30B “decentralized” exchange?
Hyperliquid markets itself as a “fully on-chain order book perpetual exchange.” $1 trillion in trading volume. $30 billion valuation. Crypto Twitter calls it the future of DeFi.
I decided to see what the hype was and googled “hyperliquid github”.
To my surprise, there was no source code. The repository tells you to download this hl-visor blob, run it, and let it download more blobs to execute for you.
Say what now?
How did we go from “code is law” and people measuring each other’s Nakamoto coefficients to whatever the hell this is? The idea of a closed-source blockchain was very intriguing (because why would you do that if you are not hiding anything?) so as you can imagine I decided to take a look.
Hyperliquid is a centralized exchange (if not worse than that due to the lack of financial audits & regulation) masquerading as a blockchain:
- A “CoreWriter” godmode that can mint tokens, move user funds without signatures, crash random validators and basically do whatever it wants
- Transaction types that do exactly what they say:
TestnetSetYesterdayUserVlm, retroactive volume manipulation shipped in the mainnet binary - A “scheduled freeze” mechanism with no unfreeze capability
- Only 8 broadcaster addresses can submit transactions: everyone else routes through them. They live in on-chain state, are governance-modifiable, and are undocumented.
- Oracle price override capabilities with no timelocks, bounds, or multi-sig requirements
- Bridge withdrawals can be censored forever: no timeout, no escape hatch
- Governance proposals are stored but effectively unqueryable: users see votes happened, not what was proposed
- $362M accounting gap: user claims exceed bridge balance, while HLP shows gains that should imply trader losses
- Undisclosed shadow-banking system: a fully deployed lending protocol (BOLE) hidden from users
Every claim includes addresses into the binary and L1 state that you can verify yourself.
Shall we?
0x0: The $362M Gap
The on-chain ledger shows $362M more in user claims than exists in the bridge. If everyone tried to withdraw, someone wouldn’t get paid.
User claims exceed the bridge by $362,454,741.25. At the same snapshot, HLP’s on-chain account value is $377.69M against $2.51M net deposits, implying $375.17M in gains. HLP is documented as the protocol’s backstop counterparty: “When a trader’s order cannot find an immediate match, the HLP vault can take the opposite side of the trade”. During the JELLY incident, “HLP acts as the counterparty for all trades on the platform” absorbed the toxic position.
If HLP is the primary counterparty, you’d expect aggregate trader PnL and HLP PnL to be strongly anti-correlated absent external hedging or off-ledger loss absorption. The observed state (trader claims +$362M above bridge, HLP gains +$375M) contradicts that expectation unless: (1) other market makers absorb losses off-ledger, (2) minted USDC (SystemAlignedQuoteSupplyDelta) inflates balances, (3) total_net_deposit includes non-withdrawable components (fees, unrealized PnL adjustments), or (4) HLP’s gains came from liquidation penalties and fees rather than counterparty PnL. External market makers certainly exist; the problem is opacity, which makes the actual loss distribution unverifiable.
Expected state:
- Traders deposit $X via bridge
- Traders collectively lose $Y to HLP
- HLP account: +$Y
- Trader claims: $X - $Y
- Bridge balance: $X
- Claims should be ≤ bridge
Observed state (2025-12-19T08:04:17.669123161):
| Metric | Value |
|---|---|
Bridge balance (bridge_config.bal, 6 decimals) | $4,011,884,542.58 |
Perp net deposit (perp_dex_meta.total_net_deposit) | $3,728,669,722.77 |
Spot USDC (spot_user_states, token 0) | $645,669,561.06 |
| Total claims (perp + spot) | $4,374,339,283.83 |
| Excess (claims − bridge) | $362,454,741.25 |
HLP vault state (0xdfc24b077bc1425ad1dea75bcb6f8158e10df303):
| Metric | Value |
|---|---|
HLP deposits (vaults: 0xdfc24b..., user_states.n) | $2,511,622.12 |
HLP start_of_day_account_value | $377,686,137.67 |
| HLP “gains” (value − deposits) | $375,174,515.54 |
Perpetual trading is zero-sum for realized PnL between counterparties, but fees, liquidations, and external hedging can move the numbers. Treat this as an invariant check, not a standalone proof of insolvency. However, the on-chain solvency test of “can the bridge cover all claims?” fails.
(My bet would be that YOU would not get paid, given all the locking mechanisms in place to prevent withdrawals.)
0x1: The “Testnet” Function
The genesis airdrop allocated 31% of supply (~310M tokens) to ~94k users based on testnet activity. The binary contains
TestnetSetYesterdayUserVlm: retroactive volume manipulation. This function’s existance at any point in time destroys integrity / community-ownership claims.
The most damning evidence sits in the binary’s string table, indexed like a feature list. Hyperliquid’s developers built infrastructure to fabricate trading volume retroactively, named it after the capability, and shipped it in the mainnet binary.
TestnetSetYesterdayUserVlm
The deserializer 0x1F40200 contains a little branch that handles the following Rust structure:
1enum VoteGlobalAction {
2 // ...
3 // 0x57:
4 TestnetSetYesterdayUserVlm {
5 user: [u8; 20], // Target wallet
6 wei: u64, // Perpetual volume
7 cross: u64, // Cross margin volume
8 spot_add: u64, // Spot addition volume
9 spot_cross: u64, // Spot cross volume
10 }
11}
Read that struct again: TestnetSetYesterdayUserVlm. Not “testing infrastructure” or “debugging tools.” The literal function is to set yesterday’s user volume.
Volume Storage Structure
Volume data is stored in BTreeMap structures optimized for daily aggregation:
1// DailyScaledVlms structure (referenced at 0x2aa3f60, 0x2eefa90)
2struct DailyScaledVlms {
3 day_to_user_vlm: BTreeMap<u32, BTreeMap<Address, UserVolume>>,
4 // Indexed by (day, user address) for efficient range queries
5}
6
7struct UserVolume {
8 wei_scaled: u64, // Perpetual volume in wei units
9 cross_scaled: u64, // Cross margin volume
10 spot_add_scaled: u64, // Spot addition volume
11 spot_cross_scaled: u64, // Spot cross volume
12}
These structures persist to RocksDB under column families in hyperliquid_data/. Volume queries aggregate across day ranges to compute cumulative metrics for fee tiers and any volume-gated programs.
Here’s the exact code, which first checks the network type:
1; Handler for case 0x57 (TestnetSetYesterdayUserVlm)
20x1f3638d: cmp byte ptr [r13+3505h], 3 ; r13 = state pointer
3 ; offset 0x3505 = network_type field
4 ; value 3 = mainnet discriminant
50x1f36395: jz loc_1F39041 ; Jump to error if mainnet
6...
After passing the check, the handler executes two state mutations:
1; From continuation at 0x1f3bb28 after mainnet check passes
20x1f3bb28: call cs:off_48499B0 ; update_daily_scaled_vlm_for_user
3 ; Resolved to 0x2aa3f60
4 ; Args: (state, day, user, wei_scaled, cross_scaled)
5
60x1f3bb2e: lea rdx, [rsp+918h+var_3A8]
70x1f3bb36: mov rdi, r12 ; state pointer
80x1f3bb39: mov esi, ebx ; day value
90x1f3bb3b: mov rcx, r14 ; user address
100x1f3bb3e: mov r8, r13 ; spot volumes
11
120x1f3bb41: call cs:off_48499B8 ; update_user_volume_state
13 ; Resolved to 0x2eefa90
14 ; Args: (state, day, user, spot_add_scaled, spot_cross_scaled)
These functions perform direct BTreeMap state writes without intermediate validation:
- 0x2aa3f60: Inserts/updates
(day, user) -> (wei_scaled, cross_scaled)
1unsigned __int8 __fastcall update_daily_scaled_vlm_for_user(_QWORD *a1, int a2, _DWORD *a3, __int64 a4, __int64 a5)
2{
3 // a1 = state pointer
4 // a2 = day (u32)
5 // a3 = user address (20 bytes)
6 // a4, a5 = wei_scaled and cross_scaled volumes (u64 each)
7
8 v12 = (_QWORD *)sub_2C32C70(a1 + 34, &v36); // BTreeMap lookup at state+272
9
10 // Binary search through BTreeMap by day
11 while ( v14 ) {
12 v11 = 4 * (unsigned int)*(unsigned __int16 *)(v14 + 230);
13 // Compare day values in sorted map
14 v15 = (a2 > *(_DWORD *)(v14 + 4 * v9 + 188)) - (a2 < *(_DWORD *)(v14 + 4 * v9 + 188));
15 // ...
16 }
17
18 // Insert/update entry
19 v19 = (_QWORD *)insert_or_update_btreemap_entry(&v36, v9, v10, v14, v11);
20 *v19 = a4; // Write wei_scaled volume
21 v19[1] = a5; // Write cross_scaled volume
22
23 return result;
24}
- 0x2eefa90: Inserts/updates
(day, user) -> (spot_add_scaled, spot_cross_scaled)
1_QWORD *__fastcall update_user_volume_state(__int64 a1, int a2, __int64 a3, __int64 a4, __int64 a5)
2{
3 // a1 = state pointer
4 // a2 = day (u32)
5 // a3 = user address
6 // a4, a5 = spot_add_scaled and spot_cross_scaled volumes
7
8 v9 = sub_2C33590(a1 + 288, a3); // BTreeMap lookup at state+288
9 v11 = v9 + 40;
10 v12 = *(_QWORD *)(v9 + 40);
11
12 // Binary search similar to above
13 // ...
14
15 result = (_QWORD *)insert_or_update_btreemap_entry(&v17, v8, v12, v11, v10);
16 *result = a4; // Write spot_add_scaled
17 result[1] = a5; // Write spot_cross_scaled
18
19 return result;
20}
Both functions follow identical patterns: lookup the user’s entry in a nested BTreeMap, binary search by day, then directly write the volume values. No validation, no bounds checking, no authorization beyond the mainnet network check at the handler level.
What administrative use case do you think this code was intended for?
ModifyNonCirculatingSupply
Fake volume can generate disproportionate token allocation if rewards depend on volume. Those tokens can be hidden from circulating supply metrics via the ModifyNonCirculatingSupply governance action.
1enum VoteGlobalAction {
2 // ...
3 // 0x25:
4 ModifyNonCirculatingSupply {
5 token: u32, // Token index
6 add: u64, // Amount to mark as non-circulating
7 remove: u64, // Amount to return to circulation
8 }
9}
If volume-based rewards generate token emissions, insiders could:
- Generate tokens via fabricated volume
- Use ModifyNonCirculatingSupply to classify those tokens as “non-circulating”
- Public APIs would report
circulatingSupply = totalSupply - nonCirculatingSupply - Apparent token supply appears lower, potentially supporting higher prices
The Complete Testnet Toolkit
Few other testnet actions are available:
TestnetAddMainnetUsersaction (discriminant0x58) copies user state between environments. If testnet volumes are inflated viaTestnetSetYesterdayUserVlm, and those volume fields are migrated, fabricated metrics persist as “legitimate” history.TestnetDepositForcreates USDC deposits for arbitrary addresses without requiring Ethereum bridge activity.TestnetFixUsdcadjusts balances directly.TestnetSpotSendFortransfers tokens on behalf of users without their signature.
A Hypothetical
Let’s entertain a hypothetical sequence of events:
Testnet phase: Create sybil wallets, fund via
TestnetDepositFor(0x52), execute wash trades. UseTestnetSetYesterdayUserVlm(0x57) to retroactively inflate volume metrics without market risk.Migration: Use
TestnetAddMainnetUsers(0x58) to copy inflated accounts to mainnet. The attacker-controlled wallets now show artificially high activity. When airdrops are distributed proportional to reported volume, these insiders claim a disproportionate share.Supply manipulation: Adjust non-circulating supply via
ModifyNonCirculatingSupply(0x25) to hide token allocations, if volume rewards generate emissions.
Proving this actually happened would require pre-migration testnet state or comprehensive forensic reconstruction: data not made public.
Why This Matters
The mainnet guard is not as innocent as it seems here. This code was meant for testnet, and testnet data is what drove allocation.
You couldn’t even join the “pretend validator set” (we’ll get to that) until a few months ago, so it’s not like we can investigate how or why they were used. The existence of TestnetSetYesterdayUserVlm in the binary however, is like finding a steal_user_funds function in a bank’s server code. The function’s presence is the problem. Its execution doesn’t need to be proven when the capability alone violates the trust model claimed in marketing materials.
Legitimate protocols don’t ship retroactive volume manipulation. They don’t maintain “unrewarded user” exclusion lists. They don’t vote to manipulate supply calculations. They don’t name functions after retroactive fraud operations.
The “testnet-to-mainnet” narrative was a story about fair distribution. The code tells a different story.
0x2: The Gatekeepers
8 undisclosed addresses control all transaction submission. If they censor you, there’s no alternative, no L1 to appeal to.
Every blockchain needs transaction submission. Ethereum and Bitcoin have thousands of nodes accepting transactions. Hyperliquid has 8 broadcaster addresses in the on-chain allowlist (snapshot 2025-12-19). They are governance-modifiable, undocumented, and match no publicly known wallets.
The lifecycle of a transaction looks like this:
1┌──────────────────┐
2│ User signs order │ <─── You control this
3└────────┬─────────┘
4 ▼
5┌──────────────────────────────┐
6│ API wraps in │
7│ SignedActionWrapper { │
8│ broadcaster: 0x??????... │ <─── One of 8 addresses
9│ broadcaster_nonce: 12345 │
10│ } │
11└────────┬─────────────────────┘
12 ▼
13┌──────────────────────────────┐
14│ Validator checks: │
15│ 1. broadcaster ∈ whitelist? │ <─── TxUnexpectedBroadcaster
16│ 2. nonce valid? │ <─── TxInvalidBroadcasterNonce
17│ 3. signature valid? │
18└────────┬─────────────────────┘
19 ▼
20┌──────────────────┐
21│ Execute on-chain │
22└──────────────────┘
All user transactions must be wrapped in a SignedActionWrapper with a broadcaster field. Submit directly to validators and your transaction is rejected with error TxUnexpectedBroadcaster (case 0x25 at 0x39EA1DA):
1struct SignedActionWrapper {
2 signed_actions: Vec<SignedAction>, // Your cryptographically signed actions
3 broadcaster_nonce: u64, // Replay protection (offset +0x18)
4 broadcaster: [u8; 20], // 20-byte address (offset +0x20)
5}
Three checks: broadcaster in whitelist, nonce valid, EIP-712 signature valid. All three must pass. Your cryptographically perfect transaction gets rejected at the protocol level if it doesn’t route through one of 8 mystery addresses; no public disclosure names the 8 wallets.
0x1e9b90ab34427807dc25c7266beb188e86af7ed60x2d9d6ae54b069fd372401b71dc4843d85babe3ea0x67e451964e0421f6e7d07be784f35c530667c2b30x76d335fbd515969ed5facf98611ca6e3ba87ff010x90eaf322d6e39adbdca7b632ec2436719a99fcd00x940e4f78cfb16e07e1e2ef0994e186bde7e6478c0xf70a9d9a56fe5c75815a9eae6a8593bc59cb6a060xffbb4dfc9455f0df2e973d7a371d8ad994264aa6
The Liquidation Cartel
Liquidations work the same way. The binary reveals allowed_liquidators, a HashSet<Address> in the Clearinghouse structure. Only whitelisted addresses can liquidate; attempting without membership hits InvalidLiquidator. Global across perpetuals, and all margin types.
Normal trades pay 2.5-5 basis points. Whitelist-only access eliminates competition and creates structural advantage for whitelisted liquidators. Two entities bypass the whitelist: defaultLiquidator and BackstopLiquidator. If a single entity controls both, it can delay at the whitelist tier and execute at worse prices via backstop.
Governance Control
Both whitelists are stored in runtime state and modifiable via governance actions. VoteGlobalAction::ModifyBroadcaster (discriminant 0x26) takes a single 20-byte address as payload to modify the broadcaster whitelist. VoteGlobalAction::UserCanLiquidate (discriminant 0x1C) does the same for liquidators.
Both execute via the VoteGlobalAction handler at 0x1F36320 (12KB, 89 total governance actions). Analysis shows these handlers execute without per-action authorization checks beyond the validator consensus vote. The threshold percentage is not visible in the binary; 2/3 is a common BFT default, but it’s not proven here.
No timelock observed: modifications execute at the next block after consensus. If any single entity controls ≥67% of voting power, they can unilaterally modify both whitelists at any block. No transparency mechanism for whitelist state changes.
The Trust Model
Hyperliquid combines:
- Permissioned access (whitelist only)
- Fee exemption (massive profit boost vs competitors)
- Validator co-location (first look at liquidatable positions)
- Governance control (can trigger liquidations via parameter changes)
- Concentrated governance (single entity can change whitelists if it reaches threshold)
The architecture creates a privileged MEV extraction position for insiders.
| Protocol | Liquidator Access | Fee Structure |
|---|---|---|
| Compound | Permissionless (anyone) | 0% (liquidator keeps spread) |
| Aave | Permissionless (anyone) | 0% (liquidator keeps spread) |
| dYdX v4 | Permissionless (validators) | 0% (validator profit) |
| Hyperliquid | Whitelisted addresses | 0% for fwiends 💓 |
Real-World Exercise
On July 29, 2025, Hyperliquid’s API servers collapsed: 27-37 minute freeze. Users couldn’t open or close trades, exit positions, or withdraw. Liquidations still execute at the protocol layer, so positions remain exposed during outages.
Block production continued uninterrupted. The broadcaster monopoly meant users had no alternative access path. This repeated on July 23, 2025: 37 minutes of downtime.
Optimism’s sequencer is centralized, but it’s an L2; force-include via Ethereum L1. Hyperliquid is an L1. If the 8 broadcasters censor you, there’s no higher authority.
0x3: The Trapdoor
Users can deposit instantly but may never withdraw. The code to freeze your assets exists. The code to unfreeze them does not.
Cancelling Withdrawals
Validators can cancel pending withdrawals via Case 7 (discriminant 0x07).
Handler at 0x1F3634D:
1case 7u: // invalidateWithdrawals
2 // Validate withdrawal count ≤ 10000
3 // Iterate over each withdrawal ID
4 // Lookup in bridge state at a3+11128
5 sub_1E3F170(v477, dest); // STATE WRITE at 0x1f3ada9
Bridge state writer at 0x1E3F170:
1unsigned __int64 __fastcall sub_1E3F170(__int64 a1, __int64 *a2)
2{
3 v10 = *a2; // withdrawal count
4 result = a2[1]; // withdrawal array pointer
5 v3 = a2[2]; // array length
6
7 if ( v3 )
8 {
9 v4 = (__int64 *)a2[1];
10 v5 = result + 16 * v3;
11 v12 = *(_QWORD *)(a1 + 11136); // Bridge state base pointer
12 v6 = *(_QWORD *)(a1 + 11144); // Number of buckets
13 do
14 {
15 v7 = *v4; // Withdrawal ID (global index)
16 v8 = *v4 / 0x2710uLL; // Bucket index = ID / 10000
17 if ( v8 >= v6 )
18 {
19 LOWORD(v13[0]) = 312; // Error: bucket out of range
20 core::result::unwrap_failed();
21 }
22 v9 = *((_DWORD *)v4 + 2); // Withdrawal index within bucket
23 a2 = (__int64 *)(v12 + 800 * v8); // Calculate bucket address
24
25 // Modify withdrawal state in BTreeMap
26 result = sub_2ABF640(v14, a2, v7, v9);
27
28 v4 += 2; // Next withdrawal (16-byte stride)
29 }
30 while ( v4 != (__int64 *)v5 );
31 }
32 return result;
33}
BTreeMap mutation at 0x2ABF640:
1_WORD *__fastcall sub_2ABF640(_WORD *a1, _QWORD *a2, unsigned __int64 a3, unsigned int a4)
2{
3 v5 = a3 / 0x2710; // Validate bucket
4 if ( v5 != a2[6] || (v6 = a3 - 10000 * v5, v6 >= a2[2]) )
5 sub_121EFD0(&off_47EC130); // Panic: invalid withdrawal ID
6
7 v7 = a2[1];
8 v8 = 48 * v6; // 48 bytes per withdrawal record
9
10 // THE WRITE:
11 v9 = *(_DWORD *)(v7 + v8 + 24); // Read old index at offset +24
12 *(_DWORD *)(v7 + v8 + 24) = a4; // WRITE new index at offset +24
13
14 *v69 = 352; // Success
15 return v69;
16}
The withdrawal record at bucket_base + 48*local_idx + 24 is mutated by governance via invalidateWithdrawals.
Authorization in execution path: none. Relies entirely on VoteGlobalAction consensus reaching this code.
Changing the Bridge Validator Set
Bridge validator set is governance-controlled via Case 17 (discriminant 0x11) at 0x1F37D9C:
1; Case 17 - AllowedBridgeValidators
21f37d9c add r15, 8 ; Skip discriminant, point to payload
31f37da8 call sub_1BE9E60 ; Deserialize Vec<[u8;32]>
41f37db2 cmp eax, 160h ; Check success
51f37db7 jnz loc_1F397F1 ; Error if failed
6
71f37e11 lea rbx, [r13+3200h] ; Bridge validator state base
81f37e3f call sub_1C23160 ; Mutate validator set
9
10; Binary search using memcmp on 32-byte keys
111f37f09 mov edx, 20h ; Compare 32 bytes
121f37f16 call cs:memcmp_ptr
State written to r13+0x3200. Each validator is 32 bytes. The insert function at 0x1C23160:
1char __fastcall sub_1C23160(__int64 *a1, const void *a2)
2{
3 v2 = *a1; // Root node pointer
4 if ( !*a1 )
5 return 0; // Empty set
6
7 v14 = a1;
8 v13 = a1[1]; // Tree depth
9
10LABEL_3:
11 v15 = *(unsigned __int16 *)(v2 + 362); // Entries in node
12 v3 = -1;
13 v4 = 0;
14
15 do
16 {
17 if ( 32 * (_DWORD)v15 == v4 ) // End of node
18 {
19 v3 = v15;
20LABEL_9:
21 if ( !v13 )
22 return 0; // Not found, at leaf
23 --v13;
24 v2 = *(_QWORD *)(v2 + 8 * v3 + 368); // Follow child pointer
25 goto LABEL_3;
26 }
27
28 // Binary search: compare 32-byte validator keys
29 v5 = memcmp(a2, (const void *)(v2 + v4), 0x20u);
30 v6 = (v5 > 0) - ((__int64)v5 < 0);
31 v4 += 32; // Next entry
32 ++v3;
33 }
34 while ( v6 == 1 );
35
36 if ( v6 )
37 goto LABEL_9;
38
39 // Found match - update set
40 v16[0] = v2;
41 v16[1] = v13;
42 v16[2] = v3;
43 v7 = v14;
44 v16[3] = v14;
45 sub_1729780(v17, v16, &v12);
46 --v7[2];
47
48 return 1;
49}
| Property | Finding |
|---|---|
| Authorization | None in handler |
| Validation | Deserialize only |
| Application | Instant (no delay) |
If a single entity controls ≥2/3 of voting power, they can replace all bridge validators in a single block.
Freezing the entire chain permanently
Exhaustive enumeration of all 89 VoteGlobalAction variants (0x00-0x58) from deserializer at 0x1F40200 shows FreezeChain exists at discriminant 0x20. No UnfreezeChain variant exists.
Other governance actions are reversible via boolean toggles:
HaltPerpTrading(0x22):is_halted: boolDisableCValidator(0x33):disable: boolSetEvmEnabled(0x38): boolean flag
FreezeChain takes only a height parameter with no toggle:
1FreezeChain {
2 height: u64, // Block height at which to freeze
3}
State structure:
1struct VisorAbciState {
2 // ... fields ...
3 scheduled_freeze_height: Option<u64>, // Offset 0x4C8 (discriminant), 0x4D0 (value)
4 // ... more fields ...
5 current_height: u64, // Offset 0x2F80
6 // ... more fields ...
7}
From block_proposal_validator at 0x3B8A452:
1v12 = *(_QWORD *)(a2 + 1224); // Load freeze discriminant
2v183 = *(_QWORD *)(a2 + 1232); // Load freeze_height
3
4if ( v12 == 1 ) { // Freeze is scheduled
5 if ( v183 < *(_QWORD *)(a2 + 12160) ) // scheduled_height < current_height
6 core::panicking::panic(...); // Cannot schedule freeze in past
7
8 if ( v183 == *(_QWORD *)(a2 + 12160) ) // AT freeze height
9 core::panicking::panic(...); // assertion failed: !self.state.is_frozen()
10}
When current_height == scheduled_freeze_height, panic aborts block validation. No blocks can be proposed or accepted. Chain cannot advance past freeze height.
From check_and_handle_scheduled_freeze at 0x1C899C0:
1v1 = *(uint64_t *)(a1 + 0x2F80); // Load current_height
2if ( *(uint32_t *)(a1 + 0x4C8) == 1 ) { // Option discriminant == Some
3 v2 = *(uint64_t *)(a1 + 0x4D0); // Load freeze_height value
4 if ( v2 <= v1 ) {
5 bool exact_match = (v2 == v1);
6 bool periodic = (ROR8(0xD288CE703AFB7E91LL * v1, 4) < 0x68DB8BAC710CCuLL);
7
8 v3 = exact_match | periodic;
9 if ( exact_match ) {
10 v5 = "freeze";
11 write_freeze_marker("/hyperliquid_data/freeze_abci_height");
12 } else {
13 v5 = "periodic";
14 }
15 log_freeze_event(a1, v5, ...);
16 save_checkpoint(state);
17 }
18}
Freezing requires a governance vote. Unfreezing requires manual operator intervention: stopping validators, deleting /hyperliquid_data/freeze_abci_height, patching state databases, coordinating restart. No on-chain evidence, no audit trail.
The POPCAT Incident (November 2025)
On November 12, 2025, Hyperliquid froze all Arbitrum deposits and withdrawals:
- Attacker created ~$20-30M leveraged long on POPCAT
- Placed massive buy orders to prop up price
- Withdrew orders, triggering cascading liquidations
- HLP vault absorbed $4.9M in bad debt
Response: froze all deposits and withdrawals platform-wide, labeled as “maintenance,” resumed ~2 hours later. Third manipulation attack in 2025.
Sounds familiar?
0x4: The Magic 8 Ball
A single compromised address can set arbitrary prices on all markets: instant liquidation cascades, no timelock, no deviation limits, no multi-sig.
Derivatives exchanges live or die by their oracles. Get the price wrong and positions liquidate instantly. Get it maliciously wrong: billions evaporate in seconds.
The Single Point of Total Failure
The oracle_updater field is a single privileged address controlling price feeds for the entire exchange. Not a multi-sig. Not a decentralized oracle network. One wallet’s private key.
The Hip3Deploy governance action at 0x1F28050 contains variant 2: SetOracle. Let’s trace what happens when it executes.
Execution Path
Entry: VoteGlobalAction dispatcher at 0x1F36320 (180KB function, 89 variants)
Oracle Dispatcher:
1__int64 sub_3DDC500(__int64 result, __int64 *state_tree,
2 __int128 *perp_config, __int64 action_data,
3 char should_validate)
4{
5 // Extract market key (compared as 20 bytes via memcmp below)
6 s1 = *(_OWORD *)(action_data + 32);
7
8 // Binary search BTree for market at state+272
9 v6 = state_tree[3];
10 v7 = (char *)(v6 + 272);
11 do {
12 v11 = memcmp(&s1, v7, 0x14u); // Compare 20-byte addresses
13 v12 = (v11 > 0) - ((__int64)v11 < 0);
14 v7 += 20;
15 } while ( v12 == 1 );
16
17 // CRITICAL: Validation is OPTIONAL
18 if ( should_validate && market_found ) {
19 sub_303AE10(result, &s1, 0, timestamp, state_ptr);
20 if ( result_code != 352 ) {
21 handle_vote_global_action_freeze_chain(&s1, error_ctx, ...);
22 return error;
23 }
24 }
25 // If should_validate = 0, the handler skips the sub_303AE10 validation step
26 return success;
27}
See that should_validate parameter? When false, oracle prices write directly to state. No checks whatsoever.
Oracle State Writer: sub_303AE10 at 0x303AE10
1__int64 sub_303AE10(__int64 result, unsigned int *timestamp,
2 int is_testnet, unsigned __int64 current_time,
3 __int64 *market_state)
4{
5 // Rate limit based on environment
6 if ( is_testnet == 0 )
7 rate_limit = 100; // 100ms vs 400ms
8 else
9 rate_limit = 400;
10
11 // Check rate limit
12 if ( market_state[2] >= rate_limit ) {
13 if ( current_time < last_update_time ) {
14 *(_WORD *)result = 195; // "Oracle price update too often"
15 return result;
16 }
17 }
18
19 // Timestamp bounds (within 24h window)
20 sub_11C4770(&calc, timestamp + 1, 86400, 0);
21 v21 = 86400LL * (date_calc) + v14;
22 v23 = 1000 * v21 + v22 - 62135683200000LL; // Epoch ms
23
24 // No future timestamps
25 if ( current_time > v23 ) {
26 *(_WORD *)result = 195;
27 return result;
28 }
29
30 // No duplicate timestamps
31 v38 = *market_state;
32 if ( v38 ) {
33 do {
34 if ( current_time == existing_timestamp ) {
35 *(_WORD *)result = 196;
36 return result;
37 }
38 } while ( searching );
39 }
40
41 // SUCCESS - caller proceeds to write price to state
42 *(_WORD *)result = 352;
43 return result;
44}
What gets checked:
- Rate limit: ≥100ms (testnet) or ≥400ms (production) between updates
- Timestamp in valid date range
- No future timestamps
- No duplicate timestamps
What doesn’t get checked:
- Price magnitude
- Price deviation from previous values
- Validator signatures
- Authorization
The struct from protocol analysis:
1/// Hip3DeployAction variant 2: SetOracle
2/// Handler: 0x1F28050, case 2
3SetOracle {
4 /// +0x38: Address of the oracle updater
5 oracle_addr: [u8; 20],
6
7 /// +0x08: Vec of signed 64-bit prices
8 /// NO maximum/minimum bounds
9 oracle_pxs: Vec<i64>,
10
11 /// +0x50: Oracle metadata
12 oracle_data: OracleData,
13
14 /// +0x20: External perpetual prices
15 external_perp_px: ExternalPerpPx,
16}
No execution_time, no delay_blocks, no timelock_expiry. Prices write to state in the same block.
Governance Overrides
Even if oracle_updater is honest, governance bypasses every safety check.
OverrideMaxSignedDistancesFromOracle: Normal behavior: if oracle reports ETH at $3,000 but mark price is $3,500, reject. With override: governance sets deviation limit to any value. Want 1000% deviation from market reality? Done.
OverrideIsolatedSpotDistances & OverrideIsolatedExternalDistances control liquidation thresholds for isolated margin. Governance can reduce safety margins to zero: a 5% move required to liquidate a 20x position becomes 0.1%.
OverrideFundingImpactUsd: Funding rates keep perp prices anchored to spot. Override to zero and massive open interest imbalances produce no funding payments. Open 100,000 ETH long, manipulate oracle to $10,000, close for $650M profit, pay zero funding.
Industry Comparison
| Protocol | Oracle Sources | Update Delay | Deviation Cap | Multi-sig | Override |
|---|---|---|---|---|---|
| Hyperliquid | 1 (oracle_updater) | None | Bypassable | No (single addr) | Unlimited, instant |
| dYdX v4 | 5 feeds | 1 hour | ±10% | 3-of-5 | Capped, 7-day vote |
| GMX v2 | Chainlink | N/A | ±2.5% | 2-of-3 | None exists |
| Synthetix | Chainlink + Pyth | 15 min | ±10% circuit | 4-of-7 | 48h timelock |
| Aave v3 | Chainlink + fallback | Grace period | Staleness check | Guardian | 24h+ delay |
Every legitimate protocol learned from Mango Markets ($110M, Oct 2022) and Cream Finance ($130M, Oct 2021). Industry standard: multiple independent oracles, timelocks, deviation caps, multi-sig, circuit breakers.
Hyperliquid lacks these safeguards.
Attack Surface
Primary: Compromise oracle_updater private key. Phishing, infrastructure breach, insider. Instant arbitrary prices on all markets.
Secondary: Governance. If a single entity controls the voting threshold, it can replace oracle_updater or submit SetOracle directly.
Tertiary: Combine with liquidation cartel. Whitelisted liquidators co-located with validators see liquidatable positions before oracle update broadcasts, pre-position to capture, zero fees maximize extraction.
Real-World Exercise: JELLY (March 2025)
Everything documented above was exercised in production on March 26, 2025.
A trader exploited the oracle system via JELLYJELLY: deposited $7.17M, opened $6M short (20x on a $20M market cap token), pumped spot on Bybit to distort the oracle feed, forced self-liquidation. HLP absorbed $12M unrealized losses.
Hyperliquid’s response:
- Unilaterally delisted JELLYJELLY: no visible governance vote
- Overrode oracle price to $0.0095 (vs $0.50 market)
- Force-settled all positions at validator-chosen price
- Froze attacker’s withdrawals ($6.26M of $7.17M escaped before freeze)
The attacker manipulated external markets; Hyperliquid manipulated their own oracle. Same capability, different beneficiary.
Arthur Hayes commented: “Let’s stop pretending Hyperliquid is decentralized.”
Bitget CEO Gracy Chen called it “potentially FTX 2.0” (referring to centralized failure modes, not criminal fraud).
Every position on Hyperliquid is one compromised private key away from forced liquidation. Your 2x leveraged long with 50% safety margin can be liquidated at an artificial price. You can’t withdraw during the attack (3-second window), can’t prove manipulation (it’s in consensus state), can’t appeal (liquidations are final), and can’t mitigate it via decentralization if governance is concentrated.
One key. One block. One irreversible cascade.
0x5: The Black Hole
Governance proposals are stored but unqueryable. Users see votes happened but cannot verify what was proposed. The most destructive operations leave no audit trail.
Hyperliquid’s most powerful governance actions leave no trace. Validators vote on proposals users cannot read, execute changes users cannot see, and suppress warnings users have no way to verify.
VoteGlobalAction Effects Are Invisible
Governance execution bypasses the LedgerUpdate logging system. While the binary includes logging infrastructure (forward_ledger_update_from_state at 0x38de5f0) that emits events for user actions (discriminants 0x00–0x16), VoteGlobalAction execution (handler at 0x1f36320) directly modifies consensus state without calling logging functions. The --write-system-and-core-writer-actions flag enables logging for CoreWriter privileged actions (System actions 0x37–0x46), but VoteGlobalAction variants (89 types including QuarantineUser, FreezeChain, ModifyBroadcaster, SetReserveAccumulator) execute through the consensus vote path and leave no LedgerUpdate entries.
The platform’s most destructive operations leave no record. Minting is traceable. Freezing accounts is not. Changing interest rate accumulators is not. Modifying the broadcaster whitelist is not. State changes are permanent and invisible to the public userNonFundingLedgerUpdates API.
System Actions and Silent Balance Modifications
System-initiated balance changes occur through two distinct mechanisms with different visibility:
User-triggered internal transfers: Operations like withdrawals or cross-margin adjustments emit internalTransfer LedgerUpdates (type 0x0A), which appear in the public userNonFundingLedgerUpdates API. The effect is visible, and the cause is a user-signed action.
System action balance modifications: Privileged System actions (L1 action types 0x37–0x46, including SystemUsdClassTransfer at 0x39) directly modify account balances without emitting ANY LedgerUpdate events. These operations bypass the logging infrastructure entirely. Balance changes occur with no corresponding entry in userNonFundingLedgerUpdates and no logged cause.
Users querying the API will see balance discrepancies with no explanation when System actions execute. The --write-system-and-core-writer-actions flag can log some execution to disk, but pretty much no documentation exists about it, nor about what it would log in the first place.
0x6: The Shadow Bank
Hidden leverage risk. Users trading perpetuals unknowingly interact with undisclosed lending infrastructure that could amplify systemic risk during market stress.
You thought you were trading perpetual futures. Turns out, you’ve been sitting on top of a fully-deployed, undisclosed lending protocol the entire time: complete with borrow/supply/repay/withdraw operations, interest rate curves, governance-controlled reserve parameters, and integrated liquidation infrastructure.
Update (Dec 22): This section originally characterized BOLE as an undisclosed protocol. As @hermithype pointed out, a lending feature called “BLP” was publicly tested on testnet during pre-alpha. The core concern remains: this infrastructure is live on mainnet with $1M+ in deposits, active API endpoints returning real user data, and no prominent documentation informing current users that a borrow/lend system operates alongside the perpetual exchange in mainnet.
Discovery
Buried in the binary is a structure definition: struct BolePool with 8 elements. Cross-referencing reveals the complete infrastructure of an Aave-style borrow/lend protocol internally codenamed “Bole” with kink model interest rate curves.
The governance action SetBole (VoteGlobalAction discriminant 0x50) controls the entire system:
1/// VoteGlobalAction case 0x50
2/// Serialized as "setBole" in governance payloads
3SetBole {
4 pool_config: BolePoolConfig,
5 interest_rate_model: KinkModel,
6 reserve_factor: u64,
7 liquidation_bonus: u64,
8}
The API Is Live
The protocol is live on mainnet with active pools holding real user funds:
1# Query all borrow/lend pool states
2curl -s https://api.hyperliquid.xyz/info \
3 -H "Content-Type: application/json" \
4 -d '{"type":"allBorrowLendReserveStates"}'
5
6# Response (December 2025):
7[
8 [0, {
9 "totalSupplied": "1002666.00863692",
10 "totalBorrowed": "497.8765043",
11 "borrowYearlyRate": "0.05",
12 "supplyYearlyRate": "0.0000223449",
13 "utilization": "0.0004965527",
14 "oraclePx": "1.0",
15 "ltv": "0.0"
16 }],
17 [150, {
18 "totalSupplied": "45.91215326",
19 "totalBorrowed": "0.0",
20 "borrowYearlyRate": "0.05",
21 "supplyYearlyRate": "0.0",
22 "balance": "45.91215326",
23 "utilization": "0.0",
24 "oraclePx": "24.355",
25 "ltv": "0.5"
26 }]
27]
Pool 0 (USDC): Over $1M supplied, with active borrows ($497.88) accruing interest at 5% APY. Pool 150: $1,100+ in collateral (45.91 tokens @ $24.36/unit) with 50% LTV ratio configured.
User-facing L1 action exists (L1 ActionType discriminant 0x49, “BorrowLend”) with operations for supply/borrow/repay/withdraw. API handlers exposed: borrowLendUserState (info type case 0x27) returns per-user positions, allBorrowLendReserveStates (info type case 0x29) returns all pool states.
Why This Matters
Undisclosed Risk: Users depositing into HLP or trading perpetuals don’t know their counterparty might be leveraged through an internal lending systemDocumentation Gap: While early testnet participants may have been aware of BLP, the feature’s evolution to “BOLE” and its current mainnet deployment aren’t reflected in user-facing documentation. The API endpoints (
allBorrowLendReserveStates,borrowLendUserState) are live and returning production dataHidden Leverage Risk: Mainnet data shows $1M+ in supplied assets with configured LTV ratios (50% for token 150). If BOLE collateral integrates with perp margin calculations, users could:
- Supply token A to BOLE pool
- Borrow USDC at 50% LTV
- Trade perps with borrowed USDC
- Effectively leverage spot holdings into perp exposure
This creates systemic amplification: market crash in token A triggers BOLE liquidation, forced USDC repayment, perp position liquidation, and cascade across both systems. Oracle price feeds (
oraclePxfield in API response) create shared dependency between lending LTV calculations and perp margin requirements.No Audit: The Zellic bridge audit covers the Ethereum bridge contract, not the L1 lending system
Governance Control: Governance can modify interest rates, LTV ratios, and liquidation parameters via
SetBole(VoteGlobalAction 0x50) votes. If a single entity controls the voting threshold, it can:- Lower liquidation thresholds (increase LTV)
- Reduce interest rates (encourage borrowing)
- Modify oracle feeds (separate VoteGlobalAction)
- Amplify systemic leverage without user notification
This isn’t a prototype or testing infrastructure. It’s a production-ready lending protocol running alongside the perpetual exchange, with no public documentation, no user disclosure, and no external audit.
0x7: Closing Thoughts
I expected the usual DeFi compromises: admin keys, upgrades, emergency controls. Every protocol has them. I think what I found was a bit different.
Every finding required binary reverse engineering. If the source were open, the TestnetSetYesterdayUserVlm transaction type, freeze-without-unfreeze asymmetry, 8-wallet broadcaster monopoly, hidden lending protocol – all would be public.
Not open-sourcing a decentralized protocol goes against the entire ethos of it. It is not about security. It is about hiding what the system actually does.
Now that we established that the trust assumptions look like this:
- The
execution_senderkey holder won’t mint tokens or steal funds - The Foundation won’t vote to freeze the chain and vanish
- The 8 broadcasters won’t censor transactions
- The whitelisted liquidators won’t front-run positions
- The oracle updater won’t manipulate prices
- Bridge withdrawals won’t face indefinite censorship
- Accrued interest won’t vanish via accumulator override
- Governance proposals match claims (unverifiable)
- The $362M accounting gap has a benign explanation (unverifiable)
- The hidden BOLE lending protocol won’t amplify systemic risk
If you have funds on Hyperliquid, I’d like to ask you dear reader, did you even know any of this?
Draw your own conclusions.
All findings are based on analysis of the Hyperliquid validator binary and L1 state snapshots. Addresses and state queries provided for independent verification.
Appendix
Sources
I’ve spent this entire post documenting how Hyperliquid fails to be verifiable where it matters, but credit where it’s due: they do use cryptography in one place that matters.
The hl-node binary is GPG-signed by Hyperliquid, which is distributed via their validator onboarding process. That signature cryptographically proves they shipped TestnetSetYesterdayUserVlm.
For once, good use of crypto!
| File | Description |
|---|---|
hl-node-v76 | Validator binary (72.4 MiB, ELF x86-64, signed Dec 2025) |
hl-node-v76.sig | Detached GPG signature from Hyperliquid Foundation |
hyperfoundation-pubkey.asc | Hyperliquid’s GPG public key (CF2C2EA3) |
Binary hash: SHA256: 1c2911eda8d82b8b6d0569676e0afa841c76976db4acedd29674b15a3349ee1a
All addresses referenced in this post are for hl-node-v76. Addresses may differ across releases.
1export GNUPGHOME=$(mktemp -d)
2gpg --import hyperfoundation-pubkey.asc
3gpg --verify hl-node-v76.sig hl-node-v76
Reproducing the accounting gap
Export the snapshot and convert to SQLite with the provided script state_to_sqlite.py:
1python3 state_to_sqlite.py abci_state.rmp state.db
Spot balances are stored as JSON in spot_user_states.data. The structure is {"b": [[token_id, {"t": total, "e": escrow}], ...]}. Token 0 is USDC:
1import json, sqlite3
2
3conn = sqlite3.connect("state.db")
4cur = conn.cursor()
5
6# Get vault addresses to exclude (vaults are separate from user claims)
7vaults = {row[0] for row in cur.execute("SELECT address FROM vaults")}
8
9# System/internal addresses
10exclude = vaults | {
11 "0x2000000000000000000000000000000000000000", # system spot
12 "0x0000000000000000000000000000000000000000", # zero address
13 "0xffffffffffffffffffffffffffffffffffffffff", # max address
14}
15
16total = 0
17for addr, data in cur.execute("SELECT address, data FROM spot_user_states"):
18 if addr in exclude:
19 continue
20 state = json.loads(data)
21 for entry in state.get("b", []): # "b" = balances array
22 if isinstance(entry, list) and len(entry) >= 2:
23 token_id, bal = entry[0], entry[1]
24 if token_id == 0: # USDC (8 decimals in spot)
25 total += int(bal.get("t", 0)) # "t" = total
26
27print(f"Spot USDC: ${total / 1e8:,.2f}")
Double-counting check: Perp total_net_deposit is an aggregate counter, not derived from individual positions. Spot balances in spot_user_states are separate from perp margin. Verify by sampling: users with spot USDC should not have that same balance counted in their perp margin.
All Transaction Actions
Every user-initiated operation on Hyperliquid corresponds to one of these action types. Extracted from binary analysis of hl-node.
Trading Actions
| Action | Fields | Description |
|---|---|---|
Order | orders, grouping, builder? | Place orders (limit, market, trigger) |
BatchModify | (nested) | Batch order modifications |
Cancel | cancels: [{a, o}] | Cancel by asset+oid |
CancelByCloid | cancels: [{asset, cloid}] | Cancel by client order ID |
Liquidate | liquidator_to_assets, defaultLiquidator, requests | Liquidation action |
TwapOrder | twap params | Time-weighted average price order |
TwapCancel | twapId, a | Cancel TWAP order |
UpdateUserLeverage | asset, isCross, leverage | Change leverage |
UpdateIsolatedMargin | asset, isBuy, ntli | Update isolated margin |
TopUpStrictIsolatedMargin | asset, ntl | Top up strict isolated margin |
ModifyNetChildVaultPositions | childVaultAddresses, assets | Vault position netting |
ScheduleCancel | (trigger params) | Schedule order cancellation |
ApproveBuilderFee | builder, maxFeeRate | Approve builder fee |
StartFeeTrial | perpAddBaseRate, perpCrossBaseRate, spotAddBaseRate, spotCrossBaseRate | Start fee trial |
Transfer Actions
| Action | Fields | Description |
|---|---|---|
UsdSend | destination, amount, signatureChainId | Send USDC |
SpotSend | destination, token, amount | Send spot token |
SendAsset | destination, sourceDex, destinationDex, token, amount, fromSubAccount, nonce, maxFeeRate, builder | Cross-DEX asset send |
SubAccountTransfer | subAccountUser, isDeposit, usd | Transfer to/from sub-account |
SubAccountSpotTransfer | subAccountUser, token, amount, isDeposit | Spot token sub-account transfer |
UsdClassTransfer | (params) | Move USDC between perp/spot |
SystemSpotSend | destination, token, ntl, toPerp | System spot send |
SystemSendAsset | destination, sourceDexOrSpot, destinationDexOrSpot, wei, isMint, … | System asset send |
SystemUsdClassTransfer | ntl, (params) | System USD class transfer |
Account Management
| Action | Fields | Description |
|---|---|---|
ApproveAgent3 | agentAddress, agentName, nonce, expiry? | Approve trading agent |
CreateSubAccount | name | Create sub-account |
SetDisplayName | displayName | Set user display name |
SetReferrer | code | Set referrer code |
RegisterReferrer | code | Register as referrer |
LinkStakingUser | (fields) | Link trading to staking user |
ConvertToMultiSigUser | signers, threshold, nonce | Convert to multisig |
Vault Operations
| Action | Fields | Description |
|---|---|---|
CreateVault | name, description, initialUsd, frozenUser? | Create HLP vault |
VaultTransfer | vaultAddress, isDeposit, usd | Deposit/withdraw vault |
VaultDistribute | vaultAddress, usd | Distribute vault profits |
VaultModify | vaultAddress, allowDeposits, alwaysCloseOnWithdraw | Modify vault settings |
VoteGlobal | (vault modification) | Vote on vault parameters |
ModifyNetChildVaultPositions | childVaultAddresses, assets | Modify child vault positions |
Staking (C-Layer)
| Action | Fields | Description |
|---|---|---|
TokenDelegate | validator, wei, isUndelegate | Delegate/undelegate HYPE |
ClaimRewards | (none) | Claim staking rewards |
CWithdraw | destination, wei, usd, builder? | Withdraw from staking |
CUserModify | extendLongTermStaking, profile | Modify staking user |
CSigner | (signer action) | C-signer operations |
Governance
| Action | Fields | Description |
|---|---|---|
GovPropose | title, description, category, … | Create governance proposal |
GovVote | id, choice | Vote on proposal |
VoteGlobal | (params) | Vote on global parameters |
Validator Operations
| Action | Fields | Description |
|---|---|---|
RegisterValidator | validator, signature, coldUserSignature, … | Register as validator |
CValidator | Register/ChangeProfile/Unregister | Validator profile management |
ValidatorL1Vote | (vote params) | L1 validator vote |
ValidatorL1Stream | riskFreeRate | L1 stream action |
SignValidatorSetUpdate | validatorSet, signature | Sign validator set update |
VoteAppHash | height, appHash | Vote on app hash |
ForceIncreaseEpoch | validatorsAndSignatures, newActiveEpoch | Force epoch increase |
VoteEthDeposit | ethId, (deposit details) | Vote on ETH deposit |
VoteEthFinalizedWithdrawal | ethTxHash, (withdrawal details) | Vote on finalized withdrawal |
VoteEthFinalizedValidatorSetUpdate | header, validatorSet | Vote on validator set update |
ValidatorSignWithdrawal | destination, usd, vaultAddress?, builder? | Validator sign withdrawal |
Bridge (Ethereum L1)
| Action | Fields | Description |
|---|---|---|
Withdraw3 | destination, usd, nonce, signatureChainId | Withdraw to L1 |
VoteEthDeposit | ethId, (deposit details) | Vote on ETH deposit |
VoteEthFinalizedWithdrawal | ethTxHash, (withdrawal details) | Vote on finalized withdrawal |
EVM Operations
| Action | Fields | Description |
|---|---|---|
EvmRawTx | raw (hex tx) | Submit raw EVM transaction |
FinalizeEvmContract | (contract params) | Finalize EVM contract |
EvmUserModify | usingBigBlocks, input | Modify EVM user settings |
SendToEvmWithData | destinationRecipient, addressEncoding, destinationChainId, gasLimit, wei, … | Send to EVM with calldata |
DeployerSendToEvmForFrozenUser | frozenUser, wei | Deployer send for frozen user |
Spot/Token Operations
| Action | Fields | Description |
|---|---|---|
SpotDeploy | RegisterToken/RegisterToken2/RegisterSpot/RegisterHyperliquidity/… | Deploy spot markets |
SpotSend | destination, token, amount | Send spot token |
Hip3Deploy | collateralToken, oracleUpdater, … | HIP-3 deployment |
SpotDeploy Variants (17)
| Variant | Description |
|---|---|
RegisterToken | Register new token |
RegisterToken2 | Register token v2 |
MaxGas | Set max gas |
UserGenesis | User genesis |
ExistingTokenAndWei | Existing token deposit |
BlacklistUsers | Blacklist users |
Genesis | Token genesis |
RegisterSpot | Register spot market |
RegisterHyperliquidity | Register hyperliquidity pool |
RequestEvmContract | Request EVM contract |
SetFullName | Set token full name |
SetDeployerTradingFeeShare | Set deployer fee share |
EnableFreezePrivilege | Enable freeze privilege |
FreezeUser | Freeze user |
RevokeFreezePrivilege | Revoke freeze privilege |
EnableQuoteToken | Enable as quote token |
EnableAlignedQuoteToken | Enable as aligned quote token |
DEX Abstraction
| Action | Fields | Description |
|---|---|---|
AgentEnableDexAbstraction | enabled | Enable DEX abstraction for agent |
UserDexAbstraction | enabled, … | Enable DEX abstraction for user |
UserPortfolioMargin | (margin params) | Portfolio margin settings |
BoleAction | operation (Borrow/Supply/Repay), token, amount | Borrow/lend operations |
Reserve/Borrow-Lend
| Action | Fields | Description |
|---|---|---|
ReserveRequestWeight | weight | Request reserve weight |
System/Internal
| Action | Fields | Description |
|---|---|---|
SetGlobal | pxs, externalPerpPxs, usdtUsdcPx | Set global prices |
Noop | (none) | No operation |
ReassessFees | (none) | Reassess fees |
SystemApproveBuilderFee | (params) | System approve builder fee |
SystemAlignedQuoteSupplyDelta | token, wei, isMint | Aligned quote supply delta |
All governance actions
89 total variants extracted from hl-node. All execute immediately upon validator consensus: no timelocks, no user consent, no reversal mechanisms.
Fee & Trading Parameters (0x00–0x07)
| Code | Action | Description |
|---|---|---|
| 0x00 | FeeSchedule | Modify fee structure (Vec, max 10k entries) |
| 0x01 | ReferralBucketMillis | Referral bucket timing |
| 0x02 | OverrideFundingImpactUsd | Override funding impact per asset |
| 0x03 | FundingClamp | Set funding rate bounds per asset |
| 0x04 | DefaultImpactUsd | Default price impact threshold |
| 0x05 | MaxOrderDistanceFromAnchor | Max order distance from oracle (0.2–0.99) |
| 0x06 | InsertMarginTable | Insert margin requirement table |
| 0x07 | SetMarginTableIds | Assign margin tables to assets |
System Control (0x08–0x0C)
| Code | Action | Description |
|---|---|---|
| 0x08 | SimulateCrashHeight | Simulate consensus crash at height |
| 0x09 | NewSzDecimals | Update size decimal precision |
| 0x0A | PerformAutoDeleveraging | Trigger ADL |
| 0x0B | AssetsAtOpenInterestCap | Mark assets at OI cap |
| 0x0C | AdlShortfallRemaining | ADL shortfall tracking |
Token & Bridge Restrictions (0x0D–0x11)
| Code | Action | Description |
|---|---|---|
| 0x0D | HlOnlyCoins | Restrict tokens to Hyperliquid only (trap funds) |
| 0x0E | StakingEpochDurationSeconds | Staking epoch duration (no bounds check) |
| 0x0F | StrictIsolatedCoins | Force isolated margin mode (can trigger liquidations) |
| 0x10 | Bridge2WithdrawFee | Bridge withdrawal fee |
| 0x11 | AllowedBridgeValidators | Control bridge validator set (max 10k) |
Validator Management (0x12–0x13)
| Code | Action | Description |
|---|---|---|
| 0x12 | AllowedCValidator | Whitelist validator address |
| 0x13 | ForceRecomputeValidatorState | Force state recomputation |
Signature & Margin Overrides (0x14–0x17)
| Code | Action | Description |
|---|---|---|
| 0x14 | OverrideMaxSignatureValiditySeconds | Override signature validity |
| 0x15 | OverrideIsolatedMaxLeverage | Override max leverage per asset |
| 0x16 | OverrideIsolatedMarginRequirement | Override margin requirement |
| 0x17 | CanonicalTokens | Set canonical token routing |
User Control (0x18–0x1D)
| Code | Action | Description |
|---|---|---|
| 0x18 | DisableSpot | Disable spot trading |
| 0x19 | MaxHlpWithdrawPerSecond | Rate limit HLP withdrawals (DOS vector) |
| 0x1A | QuarantineUser | Freeze user account (no undo mechanism) |
| 0x1B | CancelUserOrders | Force cancel all user orders |
| 0x1C | UserCanLiquidate | Toggle user’s liquidation privileges |
| 0x1D | UserIsUnrewarded | Exclude user from rewards |
Market & Asset Control (0x1E–0x25)
| Code | Action | Description |
|---|---|---|
| 0x1E | AllowDuplicateTokenNames | Allow duplicate token names |
| 0x1F | DeployGasAuctionChange | Modify gas auction parameters |
| 0x20 | FreezeChain | Halt chain at specified height (within 50k blocks) |
| 0x21 | ModifyVault | Arbitrary vault configuration |
| 0x22 | HaltPerpTrading | Instant trading halt per asset |
| 0x23 | RegisterAsset2 | Register new tradeable asset |
| 0x24 | ModifyReferrer | Override user’s referrer (fee extraction) |
| 0x25 | ModifyNonCirculatingSupply | Modify circulating supply accounting |
Network & Validator Operations (0x26–0x34)
| Code | Action | Description |
|---|---|---|
| 0x26 | ModifyBroadcaster | Control broadcaster set |
| 0x27 | ModifyCStakingPeriods | Modify staking periods |
| 0x28 | CValidator | Validator profile (register/change/unregister) |
| 0x29 | UnjailAll | Mass amnesty: unjail all validators |
| 0x2A | MaxNValidators | Set max validator count (centralization) |
| 0x2B | SetReserveAccumulator | Direct reserve value manipulation |
| 0x2C | UnjailSigner | Unjail specific signer |
| 0x2D | RefundCSignerRequestWeights | Refund signer request weights |
| 0x2E | TestnetSpotDeploy | [TESTNET] Spot deployment |
| 0x2F | TestnetHip3Deploy | [TESTNET] HIP-3 deployment |
| 0x30 | SetNativeToken | Set native token (one-time, “HYPE”) |
| 0x31 | SetSelfDelegationRequirement | Self-delegation minimum (0 allows Sybil) |
| 0x32 | SetAllowAllValidators | Permissionless validator mode |
| 0x33 | DisableCValidator | Disable specific validator |
| 0x34 | DisableNodeIp | Ban node by IP |
Staking & Rewards (0x35–0x37)
| Code | Action | Description |
|---|---|---|
| 0x35 | SetStakesDecaySeconds | Stake decay duration |
| 0x36 | SetDistributeRewardBucketSeconds | Reward distribution timing |
| 0x37 | SetMaxOiPerSecond | Max OI change rate per market |
EVM Control (0x38–0x3C)
| Code | Action | Description |
|---|---|---|
| 0x38 | SetEvmEnabled | EVM kill switch |
| 0x39 | SetPartialLiquidationCooldown | Partial liquidation cooldown |
| 0x3A | SetEvmBlockDuration | EVM block duration (timing attacks) |
| 0x3B | SetEvmPrecompileEnabled | Toggle precompiles (disable ecrecover breaks all sigs) |
| 0x3C | SetEvmL1TransfersEnabled | Trap funds in EVM |
Action Delays & Quote Tokens (0x3D–0x42)
| Code | Action | Description |
|---|---|---|
| 0x3D | SetMaxWithdrawLeverage | Max leverage for withdrawals |
| 0x3E | SetActionDelayerEnabled | Bypass timelocks |
| 0x3F | SetActionDelayedActionsRefund | Delayed action refund policy |
| 0x40 | AllowQuoteToken | Toggle quote token (instant market shutdown) |
| 0x41 | ValidatorL1VoteEnabled | Toggle L1 validator voting |
| 0x42 | SetCheckAllUsersCollateralizedCross | Cross-collateral check toggle |
PerpDex Parameters (0x43–0x4D)
| Code | Action | Description |
|---|---|---|
| 0x43 | SetPerpDexTransferBounds | Transfer bounds per DEX |
| 0x44 | SetPerpDexOpenInterestFundingCap | OI funding cap |
| 0x45 | SetPerpDexOpenInterestLimit | OI limit |
| 0x46 | SetPerpDexDailyInterestLimit | Daily interest limit |
| 0x47 | SetPerpDexMaxNotionalTransferredTotal | Max notional transfer |
| 0x48 | SetPageStatus | Page status flag |
| 0x49 | SetLiquidBaseTokens | Liquid base token set |
| 0x4A | SetLiquidQuoteTokens | Liquid quote token set |
| 0x4B | SetCoreWriterActionEnabled | Core writer action toggle |
| 0x4C | SetPerpDexsLocked | Lock perp DEXs |
| 0x4D | SetHip3NoCross | HIP-3 cross-margin restriction |
USDC & DEX Abstraction (0x4E–0x4F)
| Code | Action | Description |
|---|---|---|
| 0x4E | SetUsdcEvmContract | USDC contract control (6 subcases: SetAddress, SetEnabled, MigrateBalances, DeployEvmContract, ClearContracts, SetFlag) |
| 0x4F | SetDexAbstractionEnabled | DEX abstraction toggle |
Bole/Lending Protocol (0x50)
| Code | Action | Description |
|---|---|---|
| 0x50 | SetBole | Lending protocol control (7 subcases: EnableBole, SetPoolConfig, SetUtilizationRange, SetBorrowRange, SetAutoRepayEnabled, SetReserveAccumulator, TestnetAction) |
Testnet-Only (0x51–0x58)
| Code | Action | Description |
|---|---|---|
| 0x51 | TestnetFixUsdc | Arbitrary USDC balance rewrite |
| 0x52 | TestnetDepositFor | Mint USDC to any address (max 920M per call) |
| 0x53 | TestnetSpotSendFor | Transfer tokens from any user |
| 0x54 | TestnetDisableFeeTrial | Disable fee trial |
| 0x55 | TestnetChangeTokenDeployer | Hijack token contracts |
| 0x56 | TestnetChangePerpDexDeployer | Hijack perp DEX control |
| 0x57 | TestnetSetYesterdayUserVlm | Override historical volume |
| 0x58 | TestnetAddMainnetUsers | Migrate mainnet users |