WEB3 SOFTWARE ANALYSIS

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:

  1. A “CoreWriter” godmode that can mint tokens, move user funds without signatures, crash random validators and basically do whatever it wants
  2. Transaction types that do exactly what they say: TestnetSetYesterdayUserVlm, retroactive volume manipulation shipped in the mainnet binary
  3. A “scheduled freeze” mechanism with no unfreeze capability
  4. Only 8 broadcaster addresses can submit transactions: everyone else routes through them. They live in on-chain state, are governance-modifiable, and are undocumented.
  5. Oracle price override capabilities with no timelocks, bounds, or multi-sig requirements
  6. Bridge withdrawals can be censored forever: no timeout, no escape hatch
  7. Governance proposals are stored but effectively unqueryable: users see votes happened, not what was proposed
  8. $362M accounting gap: user claims exceed bridge balance, while HLP shows gains that should imply trader losses
  9. 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):

MetricValue
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):

MetricValue
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:

  1. 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}
  1. 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:

  1. Generate tokens via fabricated volume
  2. Use ModifyNonCirculatingSupply to classify those tokens as “non-circulating”
  3. Public APIs would report circulatingSupply = totalSupply - nonCirculatingSupply
  4. Apparent token supply appears lower, potentially supporting higher prices

The Complete Testnet Toolkit

Few other testnet actions are available:

  • TestnetAddMainnetUsers action (discriminant 0x58) copies user state between environments. If testnet volumes are inflated via TestnetSetYesterdayUserVlm, and those volume fields are migrated, fabricated metrics persist as “legitimate” history.
  • TestnetDepositFor creates USDC deposits for arbitrary addresses without requiring Ethereum bridge activity.
  • TestnetFixUsdc adjusts balances directly.
  • TestnetSpotSendFor transfers 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. Use TestnetSetYesterdayUserVlm (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└────────┬─────────────────────┘
1213┌──────────────────────────────┐
14│ Validator checks:            │
15│ 1. broadcaster ∈ whitelist?  │ <─── TxUnexpectedBroadcaster
16│ 2. nonce valid?              │ <─── TxInvalidBroadcasterNonce
17│ 3. signature valid?          │
18└────────┬─────────────────────┘
1920┌──────────────────┐
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.

  • 0x1e9b90ab34427807dc25c7266beb188e86af7ed6
  • 0x2d9d6ae54b069fd372401b71dc4843d85babe3ea
  • 0x67e451964e0421f6e7d07be784f35c530667c2b3
  • 0x76d335fbd515969ed5facf98611ca6e3ba87ff01
  • 0x90eaf322d6e39adbdca7b632ec2436719a99fcd0
  • 0x940e4f78cfb16e07e1e2ef0994e186bde7e6478c
  • 0xf70a9d9a56fe5c75815a9eae6a8593bc59cb6a06
  • 0xffbb4dfc9455f0df2e973d7a371d8ad994264aa6

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.

ProtocolLiquidator AccessFee Structure
CompoundPermissionless (anyone)0% (liquidator keeps spread)
AavePermissionless (anyone)0% (liquidator keeps spread)
dYdX v4Permissionless (validators)0% (validator profit)
HyperliquidWhitelisted addresses0% 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}
PropertyFinding
AuthorizationNone in handler
ValidationDeserialize only
ApplicationInstant (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: bool
  • DisableCValidator (0x33): disable: bool
  • SetEvmEnabled (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:

  1. Attacker created ~$20-30M leveraged long on POPCAT
  2. Placed massive buy orders to prop up price
  3. Withdrew orders, triggering cascading liquidations
  4. 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

ProtocolOracle SourcesUpdate DelayDeviation CapMulti-sigOverride
Hyperliquid1 (oracle_updater)NoneBypassableNo (single addr)Unlimited, instant
dYdX v45 feeds1 hour±10%3-of-5Capped, 7-day vote
GMX v2ChainlinkN/A±2.5%2-of-3None exists
SynthetixChainlink + Pyth15 min±10% circuit4-of-748h timelock
Aave v3Chainlink + fallbackGrace periodStaleness checkGuardian24h+ 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:

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

  1. Undisclosed Risk: Users depositing into HLP or trading perpetuals don’t know their counterparty might be leveraged through an internal lending system

    Documentation 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 data

  2. Hidden 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 (oraclePx field in API response) create shared dependency between lending LTV calculations and perp margin requirements.

  3. No Audit: The Zellic bridge audit covers the Ethereum bridge contract, not the L1 lending system

  4. 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:

  1. The execution_sender key holder won’t mint tokens or steal funds
  2. The Foundation won’t vote to freeze the chain and vanish
  3. The 8 broadcasters won’t censor transactions
  4. The whitelisted liquidators won’t front-run positions
  5. The oracle updater won’t manipulate prices
  6. Bridge withdrawals won’t face indefinite censorship
  7. Accrued interest won’t vanish via accumulator override
  8. Governance proposals match claims (unverifiable)
  9. The $362M accounting gap has a benign explanation (unverifiable)
  10. 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!

FileDescription
hl-node-v76Validator binary (72.4 MiB, ELF x86-64, signed Dec 2025)
hl-node-v76.sigDetached GPG signature from Hyperliquid Foundation
hyperfoundation-pubkey.ascHyperliquid’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

ActionFieldsDescription
Orderorders, grouping, builder?Place orders (limit, market, trigger)
BatchModify(nested)Batch order modifications
Cancelcancels: [{a, o}]Cancel by asset+oid
CancelByCloidcancels: [{asset, cloid}]Cancel by client order ID
Liquidateliquidator_to_assets, defaultLiquidator, requestsLiquidation action
TwapOrdertwap paramsTime-weighted average price order
TwapCanceltwapId, aCancel TWAP order
UpdateUserLeverageasset, isCross, leverageChange leverage
UpdateIsolatedMarginasset, isBuy, ntliUpdate isolated margin
TopUpStrictIsolatedMarginasset, ntlTop up strict isolated margin
ModifyNetChildVaultPositionschildVaultAddresses, assetsVault position netting
ScheduleCancel(trigger params)Schedule order cancellation
ApproveBuilderFeebuilder, maxFeeRateApprove builder fee
StartFeeTrialperpAddBaseRate, perpCrossBaseRate, spotAddBaseRate, spotCrossBaseRateStart fee trial

Transfer Actions

ActionFieldsDescription
UsdSenddestination, amount, signatureChainIdSend USDC
SpotSenddestination, token, amountSend spot token
SendAssetdestination, sourceDex, destinationDex, token, amount, fromSubAccount, nonce, maxFeeRate, builderCross-DEX asset send
SubAccountTransfersubAccountUser, isDeposit, usdTransfer to/from sub-account
SubAccountSpotTransfersubAccountUser, token, amount, isDepositSpot token sub-account transfer
UsdClassTransfer(params)Move USDC between perp/spot
SystemSpotSenddestination, token, ntl, toPerpSystem spot send
SystemSendAssetdestination, sourceDexOrSpot, destinationDexOrSpot, wei, isMint, …System asset send
SystemUsdClassTransferntl, (params)System USD class transfer

Account Management

ActionFieldsDescription
ApproveAgent3agentAddress, agentName, nonce, expiry?Approve trading agent
CreateSubAccountnameCreate sub-account
SetDisplayNamedisplayNameSet user display name
SetReferrercodeSet referrer code
RegisterReferrercodeRegister as referrer
LinkStakingUser(fields)Link trading to staking user
ConvertToMultiSigUsersigners, threshold, nonceConvert to multisig

Vault Operations

ActionFieldsDescription
CreateVaultname, description, initialUsd, frozenUser?Create HLP vault
VaultTransfervaultAddress, isDeposit, usdDeposit/withdraw vault
VaultDistributevaultAddress, usdDistribute vault profits
VaultModifyvaultAddress, allowDeposits, alwaysCloseOnWithdrawModify vault settings
VoteGlobal(vault modification)Vote on vault parameters
ModifyNetChildVaultPositionschildVaultAddresses, assetsModify child vault positions

Staking (C-Layer)

ActionFieldsDescription
TokenDelegatevalidator, wei, isUndelegateDelegate/undelegate HYPE
ClaimRewards(none)Claim staking rewards
CWithdrawdestination, wei, usd, builder?Withdraw from staking
CUserModifyextendLongTermStaking, profileModify staking user
CSigner(signer action)C-signer operations

Governance

ActionFieldsDescription
GovProposetitle, description, category, …Create governance proposal
GovVoteid, choiceVote on proposal
VoteGlobal(params)Vote on global parameters

Validator Operations

ActionFieldsDescription
RegisterValidatorvalidator, signature, coldUserSignature, …Register as validator
CValidatorRegister/ChangeProfile/UnregisterValidator profile management
ValidatorL1Vote(vote params)L1 validator vote
ValidatorL1StreamriskFreeRateL1 stream action
SignValidatorSetUpdatevalidatorSet, signatureSign validator set update
VoteAppHashheight, appHashVote on app hash
ForceIncreaseEpochvalidatorsAndSignatures, newActiveEpochForce epoch increase
VoteEthDepositethId, (deposit details)Vote on ETH deposit
VoteEthFinalizedWithdrawalethTxHash, (withdrawal details)Vote on finalized withdrawal
VoteEthFinalizedValidatorSetUpdateheader, validatorSetVote on validator set update
ValidatorSignWithdrawaldestination, usd, vaultAddress?, builder?Validator sign withdrawal

Bridge (Ethereum L1)

ActionFieldsDescription
Withdraw3destination, usd, nonce, signatureChainIdWithdraw to L1
VoteEthDepositethId, (deposit details)Vote on ETH deposit
VoteEthFinalizedWithdrawalethTxHash, (withdrawal details)Vote on finalized withdrawal

EVM Operations

ActionFieldsDescription
EvmRawTxraw (hex tx)Submit raw EVM transaction
FinalizeEvmContract(contract params)Finalize EVM contract
EvmUserModifyusingBigBlocks, inputModify EVM user settings
SendToEvmWithDatadestinationRecipient, addressEncoding, destinationChainId, gasLimit, wei, …Send to EVM with calldata
DeployerSendToEvmForFrozenUserfrozenUser, weiDeployer send for frozen user

Spot/Token Operations

ActionFieldsDescription
SpotDeployRegisterToken/RegisterToken2/RegisterSpot/RegisterHyperliquidity/…Deploy spot markets
SpotSenddestination, token, amountSend spot token
Hip3DeploycollateralToken, oracleUpdater, …HIP-3 deployment
SpotDeploy Variants (17)
VariantDescription
RegisterTokenRegister new token
RegisterToken2Register token v2
MaxGasSet max gas
UserGenesisUser genesis
ExistingTokenAndWeiExisting token deposit
BlacklistUsersBlacklist users
GenesisToken genesis
RegisterSpotRegister spot market
RegisterHyperliquidityRegister hyperliquidity pool
RequestEvmContractRequest EVM contract
SetFullNameSet token full name
SetDeployerTradingFeeShareSet deployer fee share
EnableFreezePrivilegeEnable freeze privilege
FreezeUserFreeze user
RevokeFreezePrivilegeRevoke freeze privilege
EnableQuoteTokenEnable as quote token
EnableAlignedQuoteTokenEnable as aligned quote token

DEX Abstraction

ActionFieldsDescription
AgentEnableDexAbstractionenabledEnable DEX abstraction for agent
UserDexAbstractionenabled, …Enable DEX abstraction for user
UserPortfolioMargin(margin params)Portfolio margin settings
BoleActionoperation (Borrow/Supply/Repay), token, amountBorrow/lend operations

Reserve/Borrow-Lend

ActionFieldsDescription
ReserveRequestWeightweightRequest reserve weight

System/Internal

ActionFieldsDescription
SetGlobalpxs, externalPerpPxs, usdtUsdcPxSet global prices
Noop(none)No operation
ReassessFees(none)Reassess fees
SystemApproveBuilderFee(params)System approve builder fee
SystemAlignedQuoteSupplyDeltatoken, wei, isMintAligned 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)

CodeActionDescription
0x00FeeScheduleModify fee structure (Vec, max 10k entries)
0x01ReferralBucketMillisReferral bucket timing
0x02OverrideFundingImpactUsdOverride funding impact per asset
0x03FundingClampSet funding rate bounds per asset
0x04DefaultImpactUsdDefault price impact threshold
0x05MaxOrderDistanceFromAnchorMax order distance from oracle (0.2–0.99)
0x06InsertMarginTableInsert margin requirement table
0x07SetMarginTableIdsAssign margin tables to assets

System Control (0x08–0x0C)

CodeActionDescription
0x08SimulateCrashHeightSimulate consensus crash at height
0x09NewSzDecimalsUpdate size decimal precision
0x0APerformAutoDeleveragingTrigger ADL
0x0BAssetsAtOpenInterestCapMark assets at OI cap
0x0CAdlShortfallRemainingADL shortfall tracking

Token & Bridge Restrictions (0x0D–0x11)

CodeActionDescription
0x0DHlOnlyCoinsRestrict tokens to Hyperliquid only (trap funds)
0x0EStakingEpochDurationSecondsStaking epoch duration (no bounds check)
0x0FStrictIsolatedCoinsForce isolated margin mode (can trigger liquidations)
0x10Bridge2WithdrawFeeBridge withdrawal fee
0x11AllowedBridgeValidatorsControl bridge validator set (max 10k)

Validator Management (0x12–0x13)

CodeActionDescription
0x12AllowedCValidatorWhitelist validator address
0x13ForceRecomputeValidatorStateForce state recomputation

Signature & Margin Overrides (0x14–0x17)

CodeActionDescription
0x14OverrideMaxSignatureValiditySecondsOverride signature validity
0x15OverrideIsolatedMaxLeverageOverride max leverage per asset
0x16OverrideIsolatedMarginRequirementOverride margin requirement
0x17CanonicalTokensSet canonical token routing

User Control (0x18–0x1D)

CodeActionDescription
0x18DisableSpotDisable spot trading
0x19MaxHlpWithdrawPerSecondRate limit HLP withdrawals (DOS vector)
0x1AQuarantineUserFreeze user account (no undo mechanism)
0x1BCancelUserOrdersForce cancel all user orders
0x1CUserCanLiquidateToggle user’s liquidation privileges
0x1DUserIsUnrewardedExclude user from rewards

Market & Asset Control (0x1E–0x25)

CodeActionDescription
0x1EAllowDuplicateTokenNamesAllow duplicate token names
0x1FDeployGasAuctionChangeModify gas auction parameters
0x20FreezeChainHalt chain at specified height (within 50k blocks)
0x21ModifyVaultArbitrary vault configuration
0x22HaltPerpTradingInstant trading halt per asset
0x23RegisterAsset2Register new tradeable asset
0x24ModifyReferrerOverride user’s referrer (fee extraction)
0x25ModifyNonCirculatingSupplyModify circulating supply accounting

Network & Validator Operations (0x26–0x34)

CodeActionDescription
0x26ModifyBroadcasterControl broadcaster set
0x27ModifyCStakingPeriodsModify staking periods
0x28CValidatorValidator profile (register/change/unregister)
0x29UnjailAllMass amnesty: unjail all validators
0x2AMaxNValidatorsSet max validator count (centralization)
0x2BSetReserveAccumulatorDirect reserve value manipulation
0x2CUnjailSignerUnjail specific signer
0x2DRefundCSignerRequestWeightsRefund signer request weights
0x2ETestnetSpotDeploy[TESTNET] Spot deployment
0x2FTestnetHip3Deploy[TESTNET] HIP-3 deployment
0x30SetNativeTokenSet native token (one-time, “HYPE”)
0x31SetSelfDelegationRequirementSelf-delegation minimum (0 allows Sybil)
0x32SetAllowAllValidatorsPermissionless validator mode
0x33DisableCValidatorDisable specific validator
0x34DisableNodeIpBan node by IP

Staking & Rewards (0x35–0x37)

CodeActionDescription
0x35SetStakesDecaySecondsStake decay duration
0x36SetDistributeRewardBucketSecondsReward distribution timing
0x37SetMaxOiPerSecondMax OI change rate per market

EVM Control (0x38–0x3C)

CodeActionDescription
0x38SetEvmEnabledEVM kill switch
0x39SetPartialLiquidationCooldownPartial liquidation cooldown
0x3ASetEvmBlockDurationEVM block duration (timing attacks)
0x3BSetEvmPrecompileEnabledToggle precompiles (disable ecrecover breaks all sigs)
0x3CSetEvmL1TransfersEnabledTrap funds in EVM

Action Delays & Quote Tokens (0x3D–0x42)

CodeActionDescription
0x3DSetMaxWithdrawLeverageMax leverage for withdrawals
0x3ESetActionDelayerEnabledBypass timelocks
0x3FSetActionDelayedActionsRefundDelayed action refund policy
0x40AllowQuoteTokenToggle quote token (instant market shutdown)
0x41ValidatorL1VoteEnabledToggle L1 validator voting
0x42SetCheckAllUsersCollateralizedCrossCross-collateral check toggle

PerpDex Parameters (0x43–0x4D)

CodeActionDescription
0x43SetPerpDexTransferBoundsTransfer bounds per DEX
0x44SetPerpDexOpenInterestFundingCapOI funding cap
0x45SetPerpDexOpenInterestLimitOI limit
0x46SetPerpDexDailyInterestLimitDaily interest limit
0x47SetPerpDexMaxNotionalTransferredTotalMax notional transfer
0x48SetPageStatusPage status flag
0x49SetLiquidBaseTokensLiquid base token set
0x4ASetLiquidQuoteTokensLiquid quote token set
0x4BSetCoreWriterActionEnabledCore writer action toggle
0x4CSetPerpDexsLockedLock perp DEXs
0x4DSetHip3NoCrossHIP-3 cross-margin restriction

USDC & DEX Abstraction (0x4E–0x4F)

CodeActionDescription
0x4ESetUsdcEvmContractUSDC contract control (6 subcases: SetAddress, SetEnabled, MigrateBalances, DeployEvmContract, ClearContracts, SetFlag)
0x4FSetDexAbstractionEnabledDEX abstraction toggle

Bole/Lending Protocol (0x50)

CodeActionDescription
0x50SetBoleLending protocol control (7 subcases: EnableBole, SetPoolConfig, SetUtilizationRange, SetBorrowRange, SetAutoRepayEnabled, SetReserveAccumulator, TestnetAction)

Testnet-Only (0x51–0x58)

CodeActionDescription
0x51TestnetFixUsdcArbitrary USDC balance rewrite
0x52TestnetDepositForMint USDC to any address (max 920M per call)
0x53TestnetSpotSendForTransfer tokens from any user
0x54TestnetDisableFeeTrialDisable fee trial
0x55TestnetChangeTokenDeployerHijack token contracts
0x56TestnetChangePerpDexDeployerHijack perp DEX control
0x57TestnetSetYesterdayUserVlmOverride historical volume
0x58TestnetAddMainnetUsersMigrate mainnet users
Can Bölük

Can Bölük

Security researcher and reverse engineer. Interested in Windows kernel development, low-level programming, static program analysis and cryptography.