skip to content
TTL ZERO
Table of Contents

調査時点: 2026-08-30(JST)

根拠の扱い: 本稿では、hl-nodeの静的解析で確認した実装事実、testnet transactionおよびsnapshotで確認した状態、公式Docs、外部観測を区別して記す。strip済みバイナリからsource上の型名や仕様上の意図を推測して断定することは避ける。

HyperliquidのHIP-3 DEXには、特定のアドレスだけに通常注文を許可する「Stars」と呼ばれる仕組みが実装されている。公開Docsではまだ詳細なschemaが説明されていない一方、現在のhl-nodeバイナリにはHip3DeployAction::StarmodifyApprovalsHip3StarStateUser 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はactivatemodifyApprovalspaの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 + 0x780/1/2DeployerState.disabled_state、別経路のdisabled/reduceOnly/enabled enumは、それぞれ別の概念である。

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-node
version: 6df83ed3250019f0cf5286b9ef214493d12ce157
build time: 2026-08-28 11:25:59 +0000
sha256: 9004d9c4c425e003dc657af542862eda67d9e3bfb936459f483e13c00e00c26
compiler: 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_02a854c0Hip3DeployActionの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 = 2

Hip3StarOperationのobject-key parserはFUN_01d7cdf0FUN_01dcce70にあり、次の対応が一致する。

JSON key internal tag
activate 0
modifyApprovals 1
pa 2

generated serializer/parserのcase 16とtestnet explorerのtxDetailsから、outer payloadのfieldはdexoperationで確定した。実例は次の通りである。

{"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をdecode
  • FUN_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がremoveinsertを選ぶための入力値であり、適用後は消える。

serialized stateのss.a

MainOrHip3::Hip3のgenerated codecはlimitsschemastatessdfを別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上でaapproved_usersallowlistなどの別名があるかは、strip済みバイナリからは分からない。

また、調査時点のperpDexsmetametaAndAssetCtxsssaを返さない。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 users

cap内なら、falseのaddressをsub_27612C0でremoveし、trueのaddressをsub_2854440でinsertする。

activateが初期化するもの

sub_24B01B0のoperation tag 0、つまりactivateは、対象DEXのrecord + 0x78が未activate状態のとき、次のwriteを行う。

record + 0x78 = 1
record + 0x80 = 0 // BTree root
record + 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前のmodifyApprovalsNot activatedで拒否されるため、通常のsequenceは次のようになる。

registerAsset2
-> star.activate
-> star.modifyApprovals

testnetのktobでも、この順序で成功したtransactionを確認できる。

注文受付側のallowlist判定

更新handlerだけでなく、注文処理側のcall chainも特定できた。

sub_2482980
-> sub_29994D0
-> sub_21AE390

sub_21AE390は選択したDEX recordのtagと、record + 0x80にあるBTreeを参照する。Star有効化済みのnamed HIP-3 DEXでは、callerの20-byte addressをsetから検索する。

  • 一致: success
  • root不在または不一致: User requires approval
  • callerのreduceOnly flagが立つ: 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状態、2Main variantのtag/nicheである可能性が高い。

一方、名前付きのstate fieldは別のrecord + 0x128にあり、5 fieldを持つDeployerStateとしてserializeされる。

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

disabled_stateにはNaUserDisabledValidatorDisabledmの4 variantがある。さらに、別のvote/global action経路にはdisabledreduceOnlyenabledの3値enumも存在する。しかし、これらをrecord + 0x78やStar activation gateへ対応付ける根拠はない。

つまり、次の3つは分けて扱う必要がある。

record + 0x78 Star/MainOrHip3のoperational tag
DeployerState.disabled_state DEX deployer state内の別field
disabled/reduceOnly/enabled 別のgenerated/vote action enum

pa proxy action

FUN_01d7dd50FUN_01dcee00Hip3StarProxyAction parserで、paproxyAction 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には少なくともnanormalTpslpositionTpslがある。

signed L1 actionの位置づけ

Starはsign_l1_actionを使うL1 actionであり、通常のdeployer actionはvaultAddress=nullexpiresAfter=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で署名し、rsvを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。
  • MainOrHip3 stateがrestart時にlive runtime viewへinstallされるsource-level assignment。
  • 個別transaction errorに対するclone/revert境界。
  • AllowHip3GrowthMode vote成立後の最終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を確認してほしい。