HyperliquidのStarsをバイナリから読み解く
/ 19 min read
Read this post in EnglishTable of Contents
調査時点: 2026-08-30(JST)
根拠の扱い: 本稿では、
hl-nodeの静的解析で確認した実装事実、testnet transactionおよびsnapshotで確認した状態、公式Docs、外部観測を区別して記す。strip済みバイナリからsource上の型名や仕様上の意図を推測して断定することは避ける。
HyperliquidのHIP-3 DEXには、特定のアドレスだけに通常注文を許可する「Stars」と呼ばれる仕組みが実装されている。公開Docsではまだ詳細なschemaが説明されていない一方、現在のhl-nodeバイナリにはHip3DeployAction::Star、modifyApprovals、Hip3StarState、User requires approvalといった具体的な実装が存在する。
解析の結果、Starsはmarket単位のaddress allowlistをBTreeSetとして保持し、通常注文の受付時にその集合を参照する仕組みだと分かった。更新はaddressとboolの組で指定し、trueで追加、falseで削除する。適用後の承認数は10,000件以下に制限される。一方、reduce-only注文は承認検査を迂回するため、未承認ユーザーのポジション縮小まで妨げる設計ではない。
概要(TL;DR)
Hip3DeployAction::StarはHIP-3 deploy actionの独立したvariantで、内部tagは0x10。- outer payloadは
{"star":{"dex":"<dex>","operation":...}}。 - operationは
activate、modifyApprovals、paの3種類。 modifyApprovalsは[[20-byte address, bool], ...]。trueは追加、falseは削除。- 保存側のallowlistは
BTreeSet<Address20>。boolは更新入力にだけ存在する。 - serialized stateでは
ss.aが承認済みaddressの配列になる。 - 最終的な承認数は10,000以下。既存addressの再追加や未登録addressの削除はno-op。
- Star有効化済みDEXでは、通常注文時にcaller addressをallowlistから検索する。reduce-only注文はこの検査をbypassする。
record + 0x78の0/1/2、DeployerState.disabled_state、別経路のdisabled/reduceOnly/enabledenumは、それぞれ別の概念である。
Starsの概念図
Starsの更新経路と注文受付経路をまとめると、次のようになる。
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重要なのは、保存側にaddress -> boolという値があるわけではない点だ。boolは変更命令の方向を示すだけで、適用後のstateには承認済みaddressだけが残る。
Starsは何をするのか
Starsは、HIP-3で作られたDEXをaddress allowlist型で運用するための機構である。testnetではktobというBTC Star DEXが確認でき、公開transactionにはregister、Starのactivate、traderのapprove、unhalt、未承認注文の失敗、承認済み注文の成功というsequenceがある。testnet上の観測
外部報告では、未承認addressでもdepositやreduce-only/closeが可能とされている。これは、注文処理側でcallerのreduceOnly flagが立つ場合にallowlist検査をbypassする実装と整合する。Starsの概要と上限に関する報告
現在の公式HIP-3 DocsはregisterAsset、oracle、margin、deployer feeなどを説明しているが、Stars、modifyApprovals、内部stateのss.aは公開schemaに掲載していない。公式HIP-3 deployer actions、公式HIP-3 proposal
解析対象
対象はRustでコンパイルされ、debug symbolsが除去されたhl-nodeである。
path: /home/dev/hl-nodeversion: 6df83ed3250019f0cf5286b9ef214493d12ce157build time: 2026-08-28 11:25:59 +0000sha256: 9004d9c4c425e003dc657af542862eda67d9e3bfb936459f483e13c00e00c26compiler: rustc 1.95.0 (59807616e 2026-04-14)GhidraとIDAで、Rust/serde由来の文字列、generated parser/serializer、jump table、stateへの書き込み、BTree helperを相互に照合した。また、testnet explorerのtransactionと、公式nodeのtranslate-abci-stateでJSON化したsnapshotを用いて、wire/storage表現を確認した。
バイナリはstrip済みなので、本稿のsub_...やFUN_...は解析ツールが付けた名前である。意味付けは関数名ではなく、call chain、比較幅、field access、embedded string、runtime dataの一致に基づく。
actionの全体像
FUN_02a854c0はHip3DeployActionのJSON object-key parserで、starをlength 4のkeyとして認識し、内部tag 0x10を生成する。
Hip3DeployAction::Star tag = 0x10 ├─ dex └─ operation: Hip3StarOperation ├─ activate tag = 0 ├─ modifyApprovals tag = 1 │ └─ [[20-byte address, bool], ...] └─ pa tag = 2Hip3StarOperationのobject-key parserはFUN_01d7cdf0とFUN_01dcce70にあり、次の対応が一致する。
| JSON key | internal tag |
|---|---|
activate |
0 |
modifyApprovals |
1 |
pa |
2 |
generated serializer/parserのcase 16とtestnet explorerのtxDetailsから、outer payloadのfieldはdexとoperationで確定した。実例は次の通りである。
{"type":"perpDeploy","star":{"dex":"ktob","operation":"activate"}}{"type":"perpDeploy","star":{"dex":"ktob","operation":{"modifyApprovals":[["0xd8cb8d9747f50be8e423c698f9104ee090540961",true]]}}}modifyApprovalsの入力形式
FUN_02b54330のarray pathはFUN_02bbfd90に入り、各要素についてFUN_01d61010を呼ぶ。
FUN_01dea8e0: JSON stringから20-byte addressをdecodeFUN_01deae80: JSON booleanを解析し、falseを0、trueを1にするFUN_01d4ba40: tupleのbracket、comma、iteratorを処理
inner shapeは次の通りである。
"modifyApprovals": [ ["0x<40 hex characters>", true], ["0x<40 hex characters>", false]]このcollectionは「現在の承認状態全体」ではなく、変更集合である。同じaddressが現在のsetに存在するかを調べ、必要な追加と削除だけを適用する。
保存側はBTreeSet<Address20>
初期解析ではnode内の領域をkeyに対応するboolと解釈し、BTreeMap<Address, bool>と考えていた。しかし、serializer、iterator、insert/remove helper、snapshotを追加で照合すると、保存側は20-byte addressだけをelementとするBTreeSetであることが分かった。
| 観測 | 意味づけ |
|---|---|
key幅 0x14 |
20-byte address |
key slot stride 0x14 |
valueを持たないset element |
node +0xe6 |
node内のkey数(16-bit) |
header +0x00 |
root pointer |
header +0x08 |
tree depth/height |
header +0x10 |
element count |
sub_27612C0は20-byte keyを削除し、sub_2854440は同じ幅のkeyを追加する。保存nodeにper-entry boolはない。modifyApprovalsに含まれるboolはhandlerがremoveとinsertを選ぶための入力値であり、適用後は消える。
serialized stateのss.a
MainOrHip3::Hip3のgenerated codecはlimits、schema、state、ss、dfを別fieldとして扱う。ssの専用serializerを追うと、現行nodeが生成・保存するStar stateは少なくとも次の形になる。
Hip3StarState { a: BTreeSet<Address20>}sub_3D50CC0はJSON object keyとしてaを出力し、その値を20-byte addressのcollection serializerへ渡す。binary/RMP serializerも同じkeyを出力する。
さらに、公式nodeのtranslate-abci-stateでtestnet snapshotをJSON化すると、ktob recordに実データとして次の構造が存在した。
{ "moh": { "Hip3": { "schema": { "name": "ktob", "full_name": "BTC Star DEX" }, "ss": { "a": [ "0xd8cb8d9747f50be8e423c698f9104ee090540961" ] } } }}従って、ss.aがserialized clearinghouse state上の承認済みaddress集合であることは高い確度で確認できた。ただし、Rust source上でaにapproved_usersやallowlistなどの別名があるかは、strip済みバイナリからは分からない。
また、調査時点のperpDexs、meta、metaAndAssetCtxsはssやaを返さない。snapshot/storageに存在することと、documented public APIで公開されていることは別である。
承認更新と10,000件制限
承認更新の中心はsub_24B01B0である。入力の各addressについて、現在の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既に承認済みのaddressを再度trueにしても、新規追加には数えない。存在しないaddressをfalseにしても、削除には数えない。
handlerは入力側の件数が10,000を超えていないかを確認したうえで、次の条件を判定する。
current_count + new_approvals <= 10000 + revocationsこれは、変更適用後のset要素数が10,000以下であることと同値である。
| 現在の件数 | 新規追加 | 削除 | 判定 |
|---|---|---|---|
| 9,999 | 1 | 0 | 許可 |
| 10,000 | 1 | 1 | 許可 |
| 10,000 | 1 | 0 | 拒否 |
上限を超えた場合のembedded stringは次の通りである。
too many approved userscap内なら、falseのaddressをsub_27612C0でremoveし、trueのaddressをsub_2854440でinsertする。
activateが初期化するもの
sub_24B01B0のoperation tag 0、つまりactivateは、対象DEXのrecord + 0x78が未activate状態のとき、次のwriteを行う。
record + 0x78 = 1record + 0x80 = 0 // BTree rootrecord + 0x90 = 0 // BTree lengthこれは既存のallowlistをclearする処理ではない。record + 0x78 == 0の間、+0x80以降は有効なBTree headerではなく、unionの重なったinactive領域である。activateはss=None相当の表現をss=Some(empty BTreeSet)相当へ切り替え、setとしてのrepresentation invariantを成立させる。
既にrecord + 0x78 == 1なら初期化分岐を通らず、既存treeは保持される。したがってactivateは実質的にidempotentである。また、activate前のmodifyApprovalsはNot activatedで拒否されるため、通常のsequenceは次のようになる。
registerAsset2 -> star.activate -> star.modifyApprovalstestnetのktobでも、この順序で成功したtransactionを確認できる。
注文受付側のallowlist判定
更新handlerだけでなく、注文処理側のcall chainも特定できた。
sub_2482980 -> sub_29994D0 -> sub_21AE390sub_21AE390は選択したDEX recordのtagと、record + 0x80にあるBTreeを参照する。Star有効化済みのnamed HIP-3 DEXでは、callerの20-byte addressをsetから検索する。
- 一致: success
- root不在または不一致:
User requires approval - callerの
reduceOnlyflagが立つ: approval検査をbypass
これにより、allowlistが状態更新側に存在するだけでなく、通常注文のadmission controlで実際に使われることまで確認できた。
reduce-only bypassは重要である。Starsは未承認addressの全操作を凍結する仕組みではなく、新規リスクを増やす通常注文を制限しつつ、既存ポジションを縮小する経路を残す。
record + 0x78を3値modeと呼ばない
record + 0x78は、単純なdisabled / reduceOnly / enabled enumではない。実装上の挙動から、現在は次のように整理するのが安全である。
| 値 | 観測された挙動 | 暫定的な意味 |
|---|---|---|
0 |
Star mutationは未activateとして拒否。注文側のapproval検査は行わない | HIP-3、ss=None |
1 |
ss payloadのBTreeを有効として扱い、通常注文を検査 |
HIP-3、ss=Some |
2 |
Star mutation対象から除外され、注文側ではapproval不要 | Main / validator-operated DEX |
この位置はMainOrHip3 unionとHip3側のoptional ss discriminantが重なる領域と考えられる。0/1はHip3側のStar activation状態、2はMain variantのtag/nicheである可能性が高い。
一方、名前付きのstate fieldは別のrecord + 0x128にあり、5 fieldを持つDeployerStateとしてserializeされる。
DeployerState ├─ n_reserve_deployments_used ├─ last_settlement_time ├─ deploy_time ├─ disabled_state └─ mdisabled_stateにはNa、UserDisabled、ValidatorDisabled、mの4 variantがある。さらに、別のvote/global action経路にはdisabled、reduceOnly、enabledの3値enumも存在する。しかし、これらをrecord + 0x78やStar activation gateへ対応付ける根拠はない。
つまり、次の3つは分けて扱う必要がある。
record + 0x78 Star/MainOrHip3のoperational tagDeployerState.disabled_state DEX deployer state内の別fielddisabled/reduceOnly/enabled 別のgenerated/vote action enumpa proxy action
FUN_01d7dd50とFUN_01dcee00はHip3StarProxyAction parserで、paはproxyAction wrapperを持つ。
| JSON key | tag |
|---|---|
cancel |
0 |
order |
1 |
sendAsset |
2 |
確認できたpayloadは次の通りである。
{"pa":{"proxyAction":{"order":{"orders":[...],"grouping":"na","builder":{"b":"0x...","f":...}}}}}{"pa":{"proxyAction":{"cancel":{"cancels":[...]}}}}{"pa":{"proxyAction":{"sendAsset":{"destination":"0x...","amount":"..."}}}}builderはoptionalで、groupingには少なくともna、normalTpsl、positionTpslがある。
signed L1 actionの位置づけ
Starはsign_l1_actionを使うL1 actionであり、通常のdeployer actionはvaultAddress=null、expiresAfter=nullとして/exchangeへ送られる。
action_hash = keccak256( msgpack.packb(action) || nonce.to_bytes(8, "big") || 0x00 // vaultAddress is None [|| 0x00 || expiresAfter(8)] // expiresAfterが存在する場合)testnetではsource bのphantom agentとしてExchange EIP-712 domainで署名し、r、s、vをenvelopeへ入れる。field orderやnumber表現がhashに影響する点は、通常のHyperliquid L1 actionと同じである。公式Signing、公式Nonces
ただし、explorerの過去transaction表示は実際のnonceやsignatureを返さない。そのため、既知のStar transactionについて完全なsigned envelopeを再構成できたわけではない。
確認できた主要関数
| address | 役割 |
|---|---|
0x02a854c0 |
Hip3DeployAction key parser。star -> tag 0x10 |
0x02aa6df0 |
Star operation parser |
0x02bbfd90 |
modifyApprovalsのaddress/bool collection parser |
0x025b01b0 |
Star operationの検証・適用handler |
0x027612c0 |
approval BTreeSet remove |
0x02854440 |
approval BTreeSet insert |
0x03d50cc0 |
Hip3StarState JSON serializer。key a |
0x03d51070 |
Hip3StarState binary/RMP serializer。key a |
0x03b110f0 |
approval address collectionのJSON serializer |
0x03e58e90 |
20-byte addressのJSON serializer |
0x021ae390 |
order admissionのapproval lookup |
0x03d282f0 |
DEX registry record lookup |
0x03c51d70 |
DEX record constructor |
0x024a9d50 |
outer perp-deploy action dispatcher |
0x01d7dd50 |
Hip3StarProxyAction parser |
公開仕様と実装の距離
今回確認したStar action、ss.a、approval lookupは、現行binaryとtestnet stateに実在する。一方、公式DocsのHIP-3 action schemaにはまだ掲載されていない。
この差は、Docs更新より先にtestnetへ実装が入った、試験的なsurfaceとして扱われている、またはbinaryとDocsのversionが一致していない、といった可能性を含む。Docsにないことだけから、非公開機能や将来仕様だと断定することはできない。
また、snapshotに保存される内部fieldがpublic API contractであるとも限らない。ss.aは現行のserialized stateで確認できるが、documented info endpointからは取得できない。内部stateのfield名を、そのまま外部クライアント向けschemaとして依存するのは危険である。
まだ分かっていないこと
- 過去Star transactionの実nonceと
r/s/vを含む完全なsigned envelope。 Hip3StarState.aのRust source上の正確なtype aliasと意味的field名。- undocumented endpointを含めたapproval setの公開範囲。
activateによるrepresentation初期化の、公開仕様上の説明とatomicity。MainOrHip3stateがrestart時にlive runtime viewへinstallされるsource-level assignment。- 個別transaction errorに対するclone/revert境界。
AllowHip3GrowthModevote成立後の最終state commit関数。
なお、handlerからaction dispatcherまではwork stateを直接in-place更新する。さらに外側にはblock-levelのclone、apply、commit境界が存在することを確認した。ただし、これだけでは個別transaction単位のrollback semanticsまでは断定できない。
まとめ
Starsの中心は、次の3点に整理できる。
1. state ss.a = BTreeSet<Address20>
2. mutation modifyApprovals = [(address, true|false), ...] true -> add false -> remove final size <= 10,000
3. admission normal order -> address membershipを検査 reduce-only order -> approval検査をbypassしたがって、Starsはmarketの取引可否を一つのbooleanで切り替える機能ではない。market stateに承認addressの集合を保持し、差分更新、件数上限、activation、注文受付時のmembership testを組み合わせるmarket-level policyである。
初期解析で考えていたBTreeMap<Address, bool>や「activate時に既存treeをclearするreduce-only遷移」という理解は、serializer、snapshot、注文処理側まで追ったことで修正された。入力のboolと保存state、Star activationと別のmode enumを分離することが、この実装を正しく読むうえで重要になる。
本稿は特定の取引、レバレッジ、署名付きactionの送信を推奨するものではない。Hyperliquidの仕様、node binary、API、testnet stateは更新され得る。再現する場合は、対象binaryのversionとSHA-256、最新の公式Docsを確認してほしい。