skip to content
TTL ZERO
Table of Contents

As of: 2026-08-30 (JST)

How evidence is handled: this post keeps four things apart — implementation facts confirmed by statically analysing hl-node, state confirmed by testnet transactions and snapshots, the official Docs, and outside observation. It avoids inferring source-level type names or intended semantics from a stripped binary and then asserting them.

Hyperliquid’s HIP-3 DEXes carry a mechanism called “Stars” that permits normal orders only from specific addresses. The public Docs do not yet explain its schema in any detail, while the current hl-node binary holds concrete implementation: Hip3DeployAction::Star, modifyApprovals, Hip3StarState, User requires approval.

The analysis showed that Stars keeps a per-market address allowlist as a BTreeSet, and consults that set when a normal order is admitted. An update is given as pairs of an address and a bool, true to add and false to remove. The number of approvals after the update is capped at 10,000. A reduce-only order, on the other hand, bypasses the approval check, so this is not a design that also stops an unapproved user from shrinking a position.

Overview (TL;DR)

  • Hip3DeployAction::Star is its own variant of the HIP-3 deploy action, with internal tag 0x10.
  • The outer payload is {"star":{"dex":"<dex>","operation":...}}.
  • There are three operations: activate, modifyApprovals and pa.
  • modifyApprovals is [[20-byte address, bool], ...]. true adds, false removes.
  • The allowlist on the stored side is a BTreeSet<Address20>. The bool exists only in the update input.
  • In the serialized state, ss.a is the array of approved addresses.
  • The final number of approvals is 10,000 or fewer. Re-adding an existing address, or removing an address that is not registered, is a no-op.
  • On a Star-activated DEX, a normal order has the caller address looked up in the allowlist. A reduce-only order bypasses that check.
  • 0/1/2 at record + 0x78, DeployerState.disabled_state, and the disabled/reduceOnly/enabled enum on a separate path are three different concepts.

Stars in one diagram

Put the update path and the order-admission path together, and Stars comes out like this.

Deployer action
{ type: "perpDeploy",
star: { dex, operation } }
|
v
+-------------------+
| Star operation |
+-------------------+
| | |
| | +---- pa: order / cancel / sendAsset
| |
| +---- modifyApprovals
| [[address, true|false], ...]
| |
| v
| calculate delta and final count
| |
| final_count <= 10,000
| |
| +------+------+
| | |
| true false
| | |
| insert remove
| +------v------+
| BTreeSet<Address20>
|
+---- activate: ss=None -> ss=Some(empty set)
User order
|
+---- reduceOnly == true ---------------------> accept
|
v
Star active?
|
+---- no -------------------------------------> normal path
|
v
caller address in BTreeSet?
|
+---- yes ------------------------------------> accept
|
+---- no -------------------------------------> User requires approval

What matters here is that there is no address -> bool value on the stored side. The bool only states the direction of a change instruction; once it has been applied, nothing but approved addresses remains in the state.

What Stars does

Stars is the machinery for running a DEX created through HIP-3 as an address allowlist. On testnet a BTC Star DEX named ktob can be confirmed, and its public transactions carry a sequence of register, Star activate, trader approve, unhalt, a failed unapproved order, and a successful approved order. Observed on testnet

Outside reports say that an unapproved address can still deposit and can still do reduce-only/close. That is consistent with an implementation which, on the order-processing side, bypasses the allowlist check when the caller’s reduceOnly flag is set. A report on Stars and its cap

The current official HIP-3 Docs explain registerAsset, the oracle, margin, the deployer fee and so on, but Stars, modifyApprovals and the internal state’s ss.a are not listed in the public schema. Official HIP-3 deployer actions, Official HIP-3 proposal

What was analysed

The target is hl-node, compiled from Rust with its debug symbols removed.

path: /home/dev/hl-node
version: 6df83ed3250019f0cf5286b9ef214493d12ce157
build time: 2026-08-28 11:25:59 +0000
sha256: 9004d9c4c425e003dc657af542862eda67d9e3bfb936459f483e13c00e00c26
compiler: rustc 1.95.0 (59807616e 2026-04-14)

With Ghidra and IDA, strings originating from Rust/serde, generated parsers and serializers, jump tables, writes into state, and BTree helpers were cross-checked against one another. Transactions from the testnet explorer, together with a snapshot turned into JSON by the official node’s translate-abci-state, were also used to confirm the wire and storage representations.

Because the binary is stripped, the sub_... and FUN_... names in this post are names the analysis tools assigned. The meanings do not come from the function names; they rest on agreement between call chains, comparison widths, field accesses, embedded strings and runtime data.

The shape of the action

FUN_02a854c0 is the JSON object-key parser for Hip3DeployAction. It recognises star as a key of length 4 and produces the internal tag 0x10.

Hip3DeployAction::Star tag = 0x10
├─ dex
└─ operation: Hip3StarOperation
├─ activate tag = 0
├─ modifyApprovals tag = 1
│ └─ [[20-byte address, bool], ...]
└─ pa tag = 2

The object-key parsers for Hip3StarOperation sit in FUN_01d7cdf0 and FUN_01dcce70, and the following correspondence matches in both.

JSON key Internal tag
activate 0
modifyApprovals 1
pa 2

From case 16 of the generated serializer/parser and from txDetails in the testnet explorer, the fields of the outer payload are settled as dex and operation. Real examples follow.

{"type":"perpDeploy","star":{"dex":"ktob","operation":"activate"}}
{"type":"perpDeploy","star":{"dex":"ktob","operation":{"modifyApprovals":[["0xd8cb8d9747f50be8e423c698f9104ee090540961",true]]}}}

The input format of modifyApprovals

The array path in FUN_02b54330 enters FUN_02bbfd90, which calls FUN_01d61010 for each element.

  • FUN_01dea8e0: decodes a 20-byte address from a JSON string
  • FUN_01deae80: parses a JSON boolean, making false 0 and true 1
  • FUN_01d4ba40: handles the tuple’s brackets, commas and iterator

The inner shape is as follows.

"modifyApprovals": [
["0x<40 hex characters>", true],
["0x<40 hex characters>", false]
]

This collection is not “the whole of the current approval state”; it is a set of changes. It checks whether each address is already in the current set, and applies only the additions and removals that are needed.

The stored side is a BTreeSet<Address20>

The first pass of the analysis read a region inside the node as a bool paired with the key, and took it for a BTreeMap<Address, bool>. Cross-checking further against the serializer, the iterator, the insert/remove helpers and the snapshot showed that the stored side is a BTreeSet whose elements are 20-byte addresses and nothing else.

Observation Interpretation
key width 0x14 a 20-byte address
key slot stride 0x14 a set element carrying no value
node +0xe6 the number of keys in the node (16-bit)
header +0x00 root pointer
header +0x08 tree depth/height
header +0x10 element count

sub_27612C0 removes a 20-byte key, and sub_2854440 adds a key of the same width. There is no per-entry bool in the stored node. The bool carried in modifyApprovals is an input value the handler uses to choose between remove and insert, and it is gone once the update is applied.

ss.a in the serialized state

The generated codec for MainOrHip3::Hip3 treats limits, schema, state, ss and df as separate fields. Following the dedicated serializer for ss, the Star state the current node produces and stores takes at least the following shape.

Hip3StarState {
a: BTreeSet<Address20>
}

sub_3D50CC0 emits a as a JSON object key and passes its value to the collection serializer for 20-byte addresses. The binary/RMP serializer emits the same key.

Turning a testnet snapshot into JSON with the official node’s translate-abci-state then showed the following structure present as real data in the ktob record.

{
"moh": {
"Hip3": {
"schema": {
"name": "ktob",
"full_name": "BTC Star DEX"
},
"ss": {
"a": [
"0xd8cb8d9747f50be8e423c698f9104ee090540961"
]
}
}
}
}

So ss.a being the set of approved addresses in the serialized clearinghouse state could be confirmed with high confidence. Whether a goes by some other name in the Rust source, such as approved_users or allowlist, is not something a stripped binary can tell.

perpDexs, meta and metaAndAssetCtxs, as of this investigation, also return neither ss nor a. Existing in a snapshot or in storage, and being exposed through a documented public API, are two different things.

Approval updates and the 10,000 cap

sub_24B01B0 is the centre of an approval update. For each address in the input, it counts the real difference against the current set.

for (address, requested_flag) in modifications:
current = approvals.contains(address)
if requested_flag == true and current == false:
new_approvals += 1
if requested_flag == false and current == true:
revocations += 1

Setting an already-approved address to true again does not count as a new addition. Setting an address that does not exist to false does not count as a removal.

Having checked that the number of entries on the input side does not exceed 10,000, the handler then evaluates the following condition.

current_count + new_approvals <= 10000 + revocations

This is equivalent to the set holding 10,000 elements or fewer once the change has been applied.

Current count New additions Removals Verdict
9,999 1 0 allowed
10,000 1 1 allowed
10,000 1 0 rejected

The embedded string for going over the cap is as follows.

too many approved users

Inside the cap, false addresses are removed with sub_27612C0 and true addresses are inserted with sub_2854440.

What activate initialises

Operation tag 0 of sub_24B01B0 — that is, activate — performs the following writes when record + 0x78 of the target DEX is in the not-yet-activated state.

record + 0x78 = 1
record + 0x80 = 0 // BTree root
record + 0x90 = 0 // BTree length

This is not a routine that clears an existing allowlist. While record + 0x78 == 0, everything from +0x80 onwards is not a valid BTree header but an inactive region overlapped by a union. activate switches a representation equivalent to ss=None over to one equivalent to ss=Some(empty BTreeSet), and thereby establishes the representation invariant of a set.

If record + 0x78 == 1 already, the initialisation branch is not taken and the existing tree is preserved. activate is therefore idempotent in effect. And because modifyApprovals before activate is rejected with Not activated, the ordinary sequence looks like this.

registerAsset2
-> star.activate
-> star.modifyApprovals

On testnet’s ktob too, transactions that succeeded in this order can be confirmed.

The allowlist check on the order-admission side

Beyond the update handler, the call chain on the order-processing side could be identified as well.

sub_2482980
-> sub_29994D0
-> sub_21AE390

sub_21AE390 consults the tag of the selected DEX record and the BTree at record + 0x80. On a Star-activated named HIP-3 DEX, it looks the caller’s 20-byte address up in the set.

  • a match: success
  • no root, or no match: User requires approval
  • the caller’s reduceOnly flag is set: the approval check is bypassed

This confirms not only that the allowlist exists on the state-update side, but that it is actually used in the admission control for normal orders.

The reduce-only bypass matters. Stars is not a mechanism that freezes every operation of an unapproved address; it restricts the normal orders that would add new risk, while leaving open a path for shrinking an existing position.

Why record + 0x78 is not called a three-valued mode

record + 0x78 is not a plain disabled / reduceOnly / enabled enum. Going by the behaviour of the implementation, the safe way to organise it for now is as follows.

Value Observed behaviour Provisional meaning
0 Star mutations are rejected as not activated. No approval check runs on the order side HIP-3, ss=None
1 The BTree in the ss payload is treated as valid, and normal orders are checked HIP-3, ss=Some
2 Excluded from Star mutations, and no approval is needed on the order side Main / validator-operated DEX

This position is thought to be the region where the MainOrHip3 union overlaps the optional ss discriminant on the Hip3 side. 0/1 are most likely the Star activation state on the Hip3 side, and 2 the tag/niche of the Main variant.

The named state field, by contrast, sits separately at record + 0x128 and is serialized as a DeployerState with 5 fields.

DeployerState
├─ n_reserve_deployments_used
├─ last_settlement_time
├─ deploy_time
├─ disabled_state
└─ m

disabled_state has 4 variants: Na, UserDisabled, ValidatorDisabled and m. A separate vote/global action path carries a three-valued enum of disabled, reduceOnly and enabled as well. But there is no basis for mapping any of these onto record + 0x78 or onto the Star activation gate.

These three, in other words, have to be handled separately.

record + 0x78 Star/MainOrHip3 operational tag
DeployerState.disabled_state a separate field inside the DEX deployer state
disabled/reduceOnly/enabled a different generated/vote action enum

The pa proxy action

FUN_01d7dd50 and FUN_01dcee00 are the Hip3StarProxyAction parser, and pa carries a proxyAction wrapper.

JSON key Tag
cancel 0
order 1
sendAsset 2

The payloads that could be confirmed are as follows.

{"pa":{"proxyAction":{"order":{"orders":[...],"grouping":"na","builder":{"b":"0x...","f":...}}}}}
{"pa":{"proxyAction":{"cancel":{"cancels":[...]}}}}
{"pa":{"proxyAction":{"sendAsset":{"destination":"0x...","amount":"..."}}}}

builder is optional, and grouping has at least na, normalTpsl and positionTpsl.

Where the signed L1 action fits

A Star is an L1 action that uses sign_l1_action, and an ordinary deployer action is sent to /exchange with vaultAddress=null and expiresAfter=null.

action_hash = keccak256(
msgpack.packb(action)
|| nonce.to_bytes(8, "big")
|| 0x00 // vaultAddress is None
[|| 0x00 || expiresAfter(8)] // if expiresAfter is present
)

On testnet it is signed under the Exchange EIP-712 domain as a phantom agent with source b, and r, s and v go into the envelope. As with any ordinary Hyperliquid L1 action, field order and number representation affect the hash. Official Signing, Official Nonces

The explorer’s display of past transactions, however, does not return the actual nonce or signature. A complete signed envelope for a known Star transaction has therefore not been reconstructed.

The main functions confirmed

Address Role
0x02a854c0 Hip3DeployAction key parser. star -> tag 0x10
0x02aa6df0 Star operation parser
0x02bbfd90 address/bool collection parser for modifyApprovals
0x025b01b0 validation and application handler for Star operations
0x027612c0 approval BTreeSet remove
0x02854440 approval BTreeSet insert
0x03d50cc0 Hip3StarState JSON serializer. key a
0x03d51070 Hip3StarState binary/RMP serializer. key a
0x03b110f0 JSON serializer for the approval address collection
0x03e58e90 JSON serializer for a 20-byte address
0x021ae390 approval lookup in order admission
0x03d282f0 DEX registry record lookup
0x03c51d70 DEX record constructor
0x024a9d50 outer perp-deploy action dispatcher
0x01d7dd50 Hip3StarProxyAction parser

The distance between the public spec and the implementation

The Star action, ss.a and the approval lookup confirmed here do exist in the current binary and in testnet state. They are not, on the other hand, listed yet in the HIP-3 action schema in the official Docs.

That gap covers possibilities such as the implementation landing on testnet ahead of a Docs update, the surface being treated as experimental, or the binary and the Docs not being at the same version. Absence from the Docs, on its own, cannot establish that this is a private feature or a future specification.

Nor is an internal field stored in a snapshot necessarily a public API contract. ss.a can be confirmed in the current serialized state, but it cannot be fetched from a documented info endpoint. Depending on the field names of internal state as though they were a schema for external clients is dangerous.

What is still unknown

  • The complete signed envelope, including the real nonce and r/s/v, of past Star transactions.
  • The exact type alias and semantic field name of Hip3StarState.a in the Rust source.
  • How widely the approval set is exposed, undocumented endpoints included.
  • The public-specification account, and the atomicity, of the representation initialisation activate performs.
  • The source-level assignment by which MainOrHip3 state is installed into the live runtime view on restart.
  • The clone/revert boundary for an error in an individual transaction.
  • The final state commit function after an AllowHip3GrowthMode vote passes.

From the handler up to the action dispatcher, work state is updated in place directly. Further out, a block-level clone, apply and commit boundary was confirmed to exist. That on its own, however, is not enough to settle rollback semantics at the level of an individual transaction.

Summary

The heart of Stars comes down to these three points.

1. state
ss.a = BTreeSet<Address20>
2. mutation
modifyApprovals = [(address, true|false), ...]
true -> add
false -> remove
final size <= 10,000
3. admission
normal order -> check address membership
reduce-only order -> bypass the approval check

Stars is therefore not a feature that flips a market’s tradability with a single boolean. It is a market-level policy that holds a set of approved addresses in market state and combines incremental updates, a cap on the count, activation, and a membership test at order admission.

The BTreeMap<Address, bool> the first pass of the analysis had in mind, and the “reduce-only transition that clears the existing tree on activate”, were corrected by following the serializer, the snapshot and the order-processing side through. Separating the input bool from the stored state, and Star activation from the other mode enums, is what matters in reading this implementation correctly.

This post does not recommend any particular trade, any leverage, or the sending of any signed action. Hyperliquid’s specification, node binary, API and testnet state can all change. If you reproduce this, check the version and SHA-256 of the target binary, and the latest official Docs.