Skip to content

feat: block unsupported Hyperliquid multisig accounts - #10693

Open
geositta wants to merge 9 commits into
mainfrom
TAT-3752-account-support
Open

geositta wants to merge 9 commits into
mainfrom
TAT-3752-account-support

Conversation

@geositta

@geositta geositta commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Expose provider-owned account support checks and prevent unsupported accounts from signing Perps trades or withdrawals.

Explanation

Failed probes are deliberately not cached. This preserves existing fail-open behavior while allowing later checks to recover from transient network failures.

The existing EXCHANGE_MULTI_SIG_REQUIRED error is reused for hard enforcement so current consumers retain their established error handling.

The changes are contained within @metamask/perps-controller.

Mobile integration will be handled separately after this package is released.

No dependencies were upgraded.

References

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Note

High Risk
Changes gate all Hyperliquid signing and trading setup on new account-support and lifecycle checks; incorrect caching or stale-context handling could block legitimate users or allow writes on the wrong account.

Overview
Adds getAccountSupport on the Perps controller, messenger, aggregated provider, and optional provider contract so UIs can ask whether the active account can trade on a route. Providers without the hook still report supported.

Hyperliquid implements the check by probing native multi-sig (userToMultiSigSigners), coalescing and caching successful results per account/network for the session, and treating transient probe failures as fail-open (retryable, not cached). Exchange rejections and confirmed multi-sig state update the cache and block exchange writes (orders, withdrawals, margin, DEX transfers, unified-account migration, builder fee/referral setup, etc.) with EXCHANGE_MULTI_SIG_REQUIRED before signing.

Trading paths now carry an AccountSupportContext (account, network, lifecycle generation) so actions reject PROVIDER_LIFECYCLE_STALE if the wallet or network changes mid-flow; internal HIP-3 transfers use a context-bound transfer helper without changing the public API shape.

Reviewed by Cursor Bugbot for commit 78e0dc4. Bugbot is set up for automated code reviews on this repo. Configure here.

@geositta
geositta marked this pull request as ready for review October 8, 2026 23:53
@geositta
geositta requested review from a team as code owners October 8, 2026 23:53
@geositta
geositta deployed to default-branch October 8, 2026 23:53 — with GitHub Actions Active
@geositta
geositta force-pushed the TAT-3752-account-support branch from 7c458e4 to 1255079 Compare October 8, 2026 23:59

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/perps-controller/src/providers/HyperLiquidProvider.ts

@abretonc7s abretonc7s left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static Perps review

VERDICT: REQUEST_CHANGES
COMMIT: 7356d4c
BASE: 54294ae

Summary

Four major findings require changes: an account switch can bypass the guard, margin/DEX-transfer writes bypass the guard, a rejected probe can poison the cache after WebSocket recovery, and an authoritative multisig rejection does not correct cached support. Routing and optional-provider compatibility look correct in the frozen Core source. Mobile/Extension compatibility remains unchecked.

Request and provenance

Review #10693, "feat: block unsupported Hyperliquid multisig accounts", in Core, package @metamask/perps-controller. The supplied description asks for provider-owned account support and rejection before trade or withdrawal signing. It explicitly allows transient probes to fail open, requires failed probes to remain retryable, reuses EXCHANGE_MULTI_SIG_REQUIRED, and defers Mobile integration. No separate acceptance criteria or prior review findings were supplied.

The checkout HEAD matches the frozen head. Exact comparison is the supplied three-dot diff. Initial git status has only the pre-existing untracked .cursor/skills/ directory. Source remains read-only. No fetch, install, build, tests, app launch, runtime harness, or publication is allowed or performed.

Frozen support digest 4565f019308972b456cd0509b7b81e8d3df3dd8b47e0fc25d38f44a823e1a939. Review skill revision 672608ec56d399376a4fbdf38b187aa3087e9515; library revision f9f8cb914c4c0fd1f7a592b9b4507b410a5d8651; harness skill revision 3f1567f019c18c938185cadfcc38dd20db64eee6, runtime package 0.85.0. The frozen instruction manifest has no additional instruction files. Task inputs provide no client source snapshots, client revisions, prior review, test execution evidence, or runtime evidence.

Changed files and callers

All 11 changed files are within packages/perps-controller:

  • CHANGELOG.md
  • src/PerpsController-method-action-types.ts
  • src/PerpsController.ts
  • src/index.ts
  • src/providers/AggregatedPerpsProvider.ts
  • src/providers/HyperLiquidProvider.ts
  • src/types/index.ts
  • tests/helpers/providerMocks.ts
  • tests/src/PerpsController.operations.test.ts
  • tests/src/providers/AggregatedPerpsProvider.test.ts
  • tests/src/providers/HyperLiquidProvider.account-mode.test.ts

Affected flow is messenger to controller.getAccountSupport, active provider route resolution, aggregated provider delegation, Hyperliquid readiness and native multisig probing, account/network-keyed cache, trading readiness and withdrawal guards, unified account migration, and disconnect. Existing Lighter implementations may omit the optional support hook.

Applicable shared families are controller portability, constants, protocol abstraction, connection lifecycle, data flow/state, trade execution, test coverage, and existing migration metrics/error handling. Numeric data, Pro UI, locales, embedded signers and new tracing instrumentation are outside this diff. Shared-package consumer comparison is applicable but unavailable. The shared child finished before the Core overlay was registered. The Core outcomes follow below.

Criteria ledger

Every applicable rule has PASS, FINDING, NOT_APPLICABLE or NOT_CHECKED plus frozen file/line evidence. PASS describes source inspection only, never execution success. Excluded families receive a scope reason. Findings, client evidence gaps, and Run QA questions will be retained in the final report.

Setup criterion Outcome Evidence
Request, exact revisions, supplied criteria, reference provenance, re-review history PASS inputs/review-subject.json, inputs/review-support.json, TASK.md; HEAD matches COMMIT; no previous findings supplied.
Changed file inventory and affected callers PASS Frozen diff has 11 files, 464 insertions, 19 deletions; affected flow and excluded families above.
Rule-level ledger with prose constraints PASS This ledger uses explicit outcomes and will retain client and execution gaps.

Unchecked areas

Mobile/Extension compatibility and all runtime behavior are NOT_CHECKED. Test code was inspected; no tests were executed.

Shared base review

Criterion Outcome Evidence
Caller to observable behavior, normal/error/empty/boundary/cleanup paths FINDING F1-F4 Controller:2896 delegates after route check; aggregation:316 uses default/explicit route. Hyperliquid:2870 maps null to supported and signer sets to unsupported; network failure logs and returns undefined. Cache:2914 keys lowercase account/network, :2933 evicts transient results. Guards:2950, :3611 and :15030 reject with the existing error. Migration:3158 uses captured address/network/info client, :3177 records attempted and disabled. Disconnect:16472 clears cache. F1 concerns the signing context after awaiting support; F2 concerns sibling mutation paths; F3 and F4 concern failed and contradicted cache entries.
Meaningful tests and regressions FINDING F1-F4; NOT_CHECKED execution Inspected controller operations:583, :599; aggregation tests:648, :663, :683; Hyperliquid account-mode tests:343-457 cover normal, multisig, transient failure, coalescing/cache, retry, blocked order and blocked withdraw. New captured-migration test:1654 protects the initiating migration account. It does not exercise an account switch during the new action guard. No new unsupported-account tests for updateMargin/transferBetweenDexs or network/disconnect during support. Existing T:1743 covers stale null followed by authoritative multisig rejection but never checks support or subsequent actions. Reader acquisition failure after reconnect is also untested. No tests executed.
Permissions, secrets/user data, dependencies, flags/locales/telemetry wiring PASS within Core; NOT_CHECKED clients Optional types:2477 and exported action:PerpsController-method-action-types.ts:85 only add read support; no state schema or secret material. Probe sends a public address to the existing info client. Frozen diff contains no package.json, lockfile, locale, flag or signer edits. Existing error code perpsErrorCodes.ts:109 is reused. See consumer gap below.
Signal over noise, comments, unused code, swallowed errors FINDING F3 Added JSDoc explains optional fallback and session/retry policy, Hyperliquid:2894; guard is shared by trading and withdrawal. Probe failure logs at :2880 before explicit fail-open. The secondary cleanup catch at :2941 leaves rejected support promises cached; see F3. No new TODO, ticket key, ad hoc debug log, commented code or unused helper. Pre-existing ticket comments remain outside the changed lines.

Shared rule ledger

File references in this section are under packages/perps-controller. H means src/providers/HyperLiquidProvider.ts; C means src/PerpsController.ts; A means src/providers/AggregatedPerpsProvider.ts; T means tests/src/providers/HyperLiquidProvider.account-mode.test.ts. Each grouped NOT_APPLICABLE row lists the excluded rule labels and the common scope reason.

Family and rule label Outcome Frozen evidence or scope reason
Controller portability: platform import in controller PASS Added imports are package-local types, C:100 and H:106; services remain injected through existing infrastructure.
Controller portability: deep import from app code NOT_CHECKED No app code in frozen input; src/index.ts:84 and :327 publicly export the new action/types for consumer use.
Controller portability: DEV or platform globals PASS No new platform globals in the exact diff; support uses wallet/client services at H:2908.
Controller portability: new dependency outside DI PASS No new dependency or host service; wallet/client/debug dependencies already exist, H:2870, :2908.
Controller portability: publisher contract and both consumers PASS Core; NOT_CHECKED consumers Optional provider hook/types at src/types/index.ts:2444 and :2477; messenger registration C:969 and action union :1612; root exports complete. Both client revisions absent.
Missing numeric data: all four rules NOT_APPLICABLE No changed numeric producer, conversion, calculation, numeric state, or display placeholder. New support values are a boolean discriminated union.
Constants: inline timeout/delay NOT_APPLICABLE No timing values added.
Constants: hardcoded slippage NOT_APPLICABLE Slippage calculations and params unchanged.
Constants: hardcoded leverage fallback NOT_APPLICABLE No leverage values added.
Constants: hardcoded precision NOT_APPLICABLE No precision changes.
Constants: hardcoded API URLs NOT_APPLICABLE Existing infoClient is reused; no URLs added.
Constants: hardcoded provider name PASS New route is typed providerId, C:2900, A:319; no provider ID literal in production additions.
Constants: hardcoded validation thresholds NOT_APPLICABLE Guard tests a boolean support result, H:2952.
Constants: hardcoded cache durations NOT_APPLICABLE Session cache has no added TTL; cleared on disconnect, H:16472.
Protocol: execution identity inferred from display fields NOT_APPLICABLE No fill/history identity or deduplication change.
Protocol: provider identity lost during transformation PASS Support delegates only to the route-owning provider, A:319; existing explicit/default write selection is reused. No provider removed.
Protocol: hardcoded provider API outside abstraction PASS Public callers route C:2899 to A:319; the native multisig endpoint remains inside the Hyperliquid adapter H:2875.
Protocol: provider-specific UI branching NOT_APPLICABLE No UI or hooks; provider-owned reason union is public at types/index.ts:2447.
Protocol: inconsistent provider error boundaries PASS Aggregated support delegates without provider-specific catches, A:320. Hyperliquid's documented transient fail-open is handled at its normalization boundary, H:2879; existing write mapping remains.
Protocol: hardcoded market symbols NOT_APPLICABLE Probe is account-scoped, no production market literals added.
Protocol: provider-native precision/decimals NOT_APPLICABLE No numeric normalization change.
Protocol: detailedOrderType rendered directly NOT_APPLICABLE No order-display changes.
Pro UI gating: all four rules NOT_APPLICABLE No Pro/Lite flags, tabs, UI, form helpers or runtime fixture claims changed.
Metrics: magic string property keys PASS Changed migration branch retains PERPS_EVENT_PROPERTY.STATUS and ERROR_MESSAGE at H:3169-3172.
Metrics: magic string values PASS Existing typed STATUS.NOT_APPLICABLE remains; existing multi_sig_account error reason is preserved at H:3172. No new event value introduced.
Metrics: new event instead of property PASS Existing PerpsAnalyticsEvent.AccountSetup at H:3168 is reused.
Metrics: screen source, reusable component source, new screen tracking NOT_APPLICABLE No screen/component changes.
Metrics: transaction completion_duration NOT_APPLICABLE No transaction metrics changed; shared TradingService paths remain intact.
Sentry: unbounded background trace volume NOT_APPLICABLE No spans, trace names, polling loop, reconnect instrumentation or sampling added. Support calls add an awaited provider probe, coalesced per context at H:2915.
Sentry: client-named async performance flow and matching end calls NOT_CHECKED Core adds a query and guards but no client trace references/snapshots are supplied. Existing action tracing remains outside the diff.
Connection: cleanup owner before in-flight setup FINDING F3; NOT_CHECKED pending lifecycle execution Map stores support promise immediately at H:2930; migration binds its initiating context at H:3158; disconnect clears H:16472. No late promise callback reinserts a completed probe, but a rejected entry survives automatic/manual WebSocket reconnect; H:2932/:16655 and HyperLiquidClientService.ts:1460. Pending disconnect/network/account cases have no supplied execution evidence.
Connection: second lifecycle owner NOT_APPLICABLE No client connection owner changes. Public support uses existing readiness H:13434.
Connection: unthrottled WS to state NOT_APPLICABLE No stream or state-update changes.
Connection: per-component WS subscription NOT_APPLICABLE No new subscription.
Connection: WS subscription leak NOT_APPLICABLE No subscription changes.
Connection: stale data after async gap FINDING F1 H:2951 accepts the awaited result without revalidating the account; wallet service:251 selects the signing account again.
Connection: static WebView work coupled to live ticks NOT_APPLICABLE No WebView/chart code.
Connection: missing post-mutation cache invalidation PASS New guard aborts before writes; successful action paths remain unchanged. TradingService.ts:2705 still invalidates position/account caches for margin.
Data flow: delayed balance tracker initiating context NOT_APPLICABLE No balance observer or delayed credit tracker.
Data flow: changed classification leaves old priority rules NOT_APPLICABLE No UI validation ranking/advisory conversion.
Data flow: state persists outside rendered control NOT_APPLICABLE No controls, gestures or keypad state.
Data flow: React persistence vs WebView synchronization NOT_APPLICABLE No charts.
Data flow: old context remains actionable FINDING F1 Context-keyed cache at H:2914 isolates stored entries, but the void guard H:2950 does not bind the result to subsequent current-account signing.
Data flow: unknown balance treated as usable NOT_APPLICABLE No balance handling changed. Documented probe fail-open is an explicit support policy, not a numeric fallback.
Data flow: late defaults overwrite user choice NOT_APPLICABLE No form state or metadata default changes.
Data flow: direct controller call from component NOT_CHECKED No client component source provided; C:969 exposes messenger action.
Data flow: missing accountState check NOT_APPLICABLE No new balance/position dereference; new query only reads support.
Data flow: derived flag structural lifecycle FINDING F3, F4 No persisted structural flag added. Typed support cached by account/network at H:2914 and removed on failed probe/disconnect at :2938/:16472. Rejected probes are not evicted, and definitive multisig write rejection H:3270 updates only TradingReadinessCache. Transition tests absent.
Data flow: unknown async value treated as absent blocker NOT_APPLICABLE No CTA/alert gate. Network failure deliberately returns supported and retries, as supplied criteria require, H:2889/:2935.
Data flow: async cleanup ownership FINDING F3 Session map clear and promise-identity check at H:2936 prevent an older transient cleanup from deleting a newer entry. Rejection handler fails to clear the cache. No timers/listeners added.
Data flow: mutation lock replaces accepted outcomes NOT_APPLICABLE New map coalesces read probes; no mutation reconciliation lock changed.
Data flow: stale position after close NOT_APPLICABLE Guard stops unsupported closes; successful close reconciliation unchanged.
Data flow: preload data not seeded NOT_APPLICABLE No hook/preload changes.
Data flow: order state race NOT_APPLICABLE No new post-order reads.
Data flow: leverage/validation bypass FINDING F2 New support precondition is absent from sibling margin/transfer writes H:12123/:12173 and :15217/:15231. Existing leverage/balance checks remain otherwise unchanged.
Trade flow: signed RoE bounds collapsed NOT_APPLICABLE No RoE/clamp/math changes.
Trade flow: shared pre-trade checks missing FINDING F2 Shared readiness H:3611 covers orders, cancellation and closes, while updateMargin and transferBetweenDexs bypass it.
Trade flow: post-trade state refresh PASS Existing successful controller/service paths preserved; unsupported writes return errors before mutation. No cache/stream update removed.
Trade flow: user slippage/order params PASS Readiness inserted without changing order params at H:6583; slippage normalization remains H:6604.
Locale coverage: all four rules NOT_APPLICABLE No rendered copy, locale JSON, strings calls or locale-helper deletions.
Test layer: degenerate fixture/formula branches NOT_APPLICABLE No formula change. Normal/null, signer set and network exception are distinct mocked provider outcomes at T:343-406.
Test layer: clock control/timer restore NOT_APPLICABLE No new timers/clock mocks.
Test layer: numeric control units NOT_APPLICABLE No numeric controls.
Test layer: assertions miss behavior PASS covered cases; FINDING gaps F1-F4 T:431/:456 assert SDK mutations never run for fixed multisig account. These assertions would fail if the relevant guard were removed. They do not cover the bypass paths, selected-account transition, reader-acquisition rejection or authoritative multisig cache correction.
Test layer: correct unit/contract layer and regression retention PASS static; NOT_CHECKED execution Controller, aggregated route and provider unit tests inspect returned support/error and SDK non-calls. Existing account-mode tests retained. No user-visible UI claim made here.
Embedded signer boundaries: both rules and key handoff prose NOT_APPLICABLE No WebView, transport, signing bridge, key material or outbound policy changes.
Cross-repo parity: screens/hooks/formatters/shared behavior NOT_CHECKED Read frozen references/parity.md; account-support behavior changes both consumers, but neither client checkout/revision is supplied. Mobile integration is explicitly deferred.
Shared package: public imports/compatibility/migration across Core and both consumers PASS Core; NOT_CHECKED Mobile/Extension Read references/shared-packages.md and owned-paths.json. Optional hook preserves old providers; C:969 and index exports provide additive contract. No version/dependency upgrade. Neither client imports, UI error handling, package adoption nor compilation can be checked offline.

Shared findings

  1. F1, major, H:2951. Bind the support result to the account that will sign. If withdrawal starts for supported account A, A's userToMultiSigSigners response is delayed, and selection changes to multisig account B before A resolves with null, this guard accepts A's result. With unified migration disabled or B already compatible, withdrawal continues to withdraw3, while the wallet adapter reads B at signing time. B therefore signs without its support being checked. Capture the account/network/lifecycle and reject a changed context before signing; cover this transition with a delayed-probe test.

    Evidence is a static call trace from guard H:2950 to withdrawal H:15030, current-account balance H:15039, SDK call H:15095 and wallet selection HyperLiquidWalletService.ts:251. No runtime reproduction claimed.

  2. F2, major, H:3611. Apply the account-support guard to the remaining exchange mutations. updateMargin only calls #ensureReady() before updateIsolatedMargin at line 12173, and transferBetweenDexs likewise reaches sendAsset at line 15231 without this trading-readiness helper. A known multisig account with an isolated position can still request a margin adjustment through PerpsController.updateMargin and reach signing instead of returning EXCHANGE_MULTI_SIG_REQUIRED. Guard these paths and test that unsupported accounts never call either SDK mutation.

    Reachable path is C:3587 to TradingService.ts:2679, A:904 and H:12123/:12173. transferBetweenDexs also signs at H:15231 without readiness-for-trading. No runtime reproduction claimed.

  3. F3, major, H:2932. Evict rejected probes as well as probes that resolve undefined. getInfoClient() is evaluated in #isHyperliquidMultiSigAccount's default argument outside its try, so it rejects while reconnect temporarily removes WebSocket clients. The rejected support promise is stored here, but this cleanup only deletes on resolved undefined and its catch leaves the entry intact. Once reconnect succeeds, later support checks, trades and withdrawals keep returning the cached CLIENT_NOT_INITIALIZED rejection until a full disconnect. Handle reader acquisition inside the fail-open boundary and remove failed entries; test a first probe during reconnect followed by recovery.

    Evidence is H:2872 default reader evaluation, client ensureInitialized at HyperLiquidClientService.ts:472 and getInfoClient at :555, temporary WS client removal at :1460, completed readiness reuse H:3364, and reconnect without support-cache clear H:16655. No runtime reproduction claimed.

  4. F4, major, H:2930. Replace cached support when an exchange write authoritatively reports Multi-sig required. The existing account-mode test at lines 1743-1789 covers a stale null probe followed by that migration rejection. This new cache retains isSupported:true, while the migration catch only updates TradingReadinessCache. Later getAccountSupport and action guards therefore keep treating the known multisig account as supported, and migration skips its next probe because attempted is already cached. Record unsupported support for the captured account/network on that rejection and extend the test to cover support plus a later order or withdrawal.

    Existing fixture T:1743-1789 supplies the race. The new supported result persists at H:2930; definitive classification H:3270 updates only readiness at :3283. Later action-time migration exits on attempted readiness H:2997. No runtime reproduction claimed.

Run QA handoff

Run QA requires a separate authorized task with client revisions and the provenance-locked harness. It should establish:

  • Standard vs native multisig account behavior for order, close, cancellation, TP/SL, margin, collateral transfer and withdrawal; assert the expected error and zero SDK/signature calls for unsupported accounts.
  • Hold a support response for account A, select B, then release it. Cover both supported-to-multisig and multisig-to-supported, plus same address across network changes and disconnect/reconnect.
  • Probe failure followed by recovery, including reader acquisition during WebSocket reconnect, coalesced concurrent calls, cache isolation by normalized address/network, and disconnect invalidation.
  • A stale null probe followed by an authoritative Multi-sig required migration error. Verify support turns unsupported and subsequent trade/withdrawal makes no signing attempt.
  • Messenger dispatch and conflicting/unknown explicit provider route. Preserve optional-provider fallback and Lighter route behavior.
  • Client localization/error presentation, supported account success behavior, and signing prompts using recorded Mobile/Extension revisions.

No runtime evidence or compiled-client result is established by this review.

Core contract impact matrix

Contract Impact Evidence
State No state shape or persisted metadata change Exact diff C only adds method/import/registration; support cache is a provider-private Map.
Methods Add getAccountSupport with optional providerId and Promise of discriminated union C:2896; src/types/index.ts:2444-2486. Existing method signatures remain.
Messenger Add PerpsController:getAccountSupport and union membership C:969; PerpsController-method-action-types.ts:84/:1612.
Events No event name/payload shape change Migration retains AccountSetup/typed STATUS/ERROR_MESSAGE, H:3168.
Exports Add GetAccountSupportParams, PerpsAccountSupport, PerpsAccountUnsupportedReason and action type src/index.ts:84/:327-329.
Constants/dependencies/version No new constants or dependency/version edit; package remains 20.0.0 package.json:3 and exact changed-file inventory. Unreleased changelog records addition at CHANGELOG.md:10.

Core criterion outcomes

Family and rule Outcome Evidence or gap
Public contract: state shape without client migration NOT_APPLICABLE No state shape change.
Public contract: method signatures and compatibility plan PASS Core; NOT_CHECKED consumers Additive method and optional provider hook preserve existing implementations; controller operations tests:599 and aggregation tests:683 check fallback. PR text defers Mobile integration, but no Mobile/Extension revision or paired PR is supplied.
Public contract: event name/payload drift PASS Existing AccountSetup event/constants retained H:3168-3172, matching previous branch.
Public contract: package export changes and consumer-style tests NOT_CHECKED Export declarations are present at index.ts:84/:327, but existing tests/public-api.test.ts:18-36 and tests/public-api-types.ts:1-47 do not import/assert the new action/support types. Added operational tests exercise internal imports. Package entrypoint consumption and compilation need explicit contract evidence.
Release metadata: API/state change without changelog PASS CHANGELOG.md:10 documents new support/controller/messenger, caching, guard and retry policy.
Release metadata: breaking release as minor/patch NOT_APPLICABLE No release bump in frozen diff. Optional hook and additive exports do not require source migrations in existing provider implementations. A release should carry an additive feature version. Runtime guard change is documented.
Release metadata: controller/client integration out of sync NOT_CHECKED Package remains 20.0.0 with Unreleased entry. Mobile integration explicitly deferred. No released version, Extension adoption, consumer revision, compatibility proof or bump PR supplied.
Multisig: catch classifier without proactive probe FINDING F2 updateMargin H:12123 to :12173 and transferBetweenDexs H:15217 to :15231 bypass support readiness.
Multisig: proactive probe without catch classifier PASS existing migration/error classification; FINDING F4 consistency Native write catch H:3270 still classifies Multi-sig required and stops migration. Shared map H:1702 maps exchange message to the existing error. New support cache ignores the definitive classification, F4.
Multisig: probe too early, before compatible/defer/unknown short-circuits PASS migration placement; FINDING F1 action identity Migration support probe stays immediately before the attempted write at H:3158, after compatible :3097, defer :3132 and unknown-mode :3144. Captured migration context is tested T:1654. The action guard lacks account revalidation, F1; these are separate conditions.
Unified setup: permanent account shape cached as retryable PASS legacy readiness; FINDING F4 support cache Detected native multisig sets attempted true/enabled false H:3177; catch fallback does likewise H:3283. Retry flag starts false H:2985. Authoritative fallback does not update new support cache.
Unified setup: retryable flag plus permanent condition PASS existing readiness logic; FINDING F3 failed support Transient signer failure sets retry and completes lock H:3232-3238; multisig sets only permanent readiness H:3270-3288. Failed support rejection remains cached outside that readiness policy, F3.
Unified setup: deferred/skipped path treated as failure PASS Feature disabled H:2987, compatible H:3097, defer H:3132 and unknown H:3144 remain before migration probe. Deferred/disabled/unknown paths do not mark attempted failure.
Metered benefits: strict improvement/equality/quantization NOT_APPLICABLE No fee resolver, benefit-consumption marker, allowance or economic predicate change.
Read-only guard: exchange/order/cancel/withdraw inventory FINDING F1-F4 Inventory below checks mutation boundaries and the routes that bypass support. Authenticated read/setup unchanged.
Read-only guard: registered venue-key paths NOT_APPLICABLE to changes; NOT_CHECKED host adoption H has no approveAgent registration call; C:setAgentSigner at :6606 binds a host-provided signer without an exchange write. No host credential/venue-key implementation or client revision supplied. Existing main/agent adapter still chooses selected account at WalletService:251, relevant to F1.
Read-only guard: missing public export PASS All four new public type/action exports exist at index.ts:84/:327-329; public consumption test gap recorded separately.
Expected evidence: contract impact matrix PASS reviewer reconstruction; NOT_CHECKED supplied PR matrix Matrix above reconstructs exact contract impact. Frozen PR body does not carry a matrix.
Expected evidence: Mobile/Extension note/paired PRs NOT_CHECKED Mobile follow-up is mentioned, Extension compatibility and paired revisions are absent. No fetched or substitute client sources used.
Expected evidence: provider abstraction and fallback tests PASS static; NOT_CHECKED execution A:319 uses the existing explicit/default route; tests aggregation:648-695 cover default/explicit/omitted hook. C tests:583-612 cover return/fallback. Unknown/conflicting new query route and messenger dispatch are execution questions.
Expected evidence: grep for client imports/environment globals PASS in changed code rg across changed production files finds Engine/Sentry/react-native words only in pre-existing comments/JSDoc, no new import or environment access. Added imports are package-local types. Exact diff contains no dependency/lockfile change.

Exchange write inventory

These are static paths, not successful execution claims. References use H as defined above.

Write group Boundary and support coverage
Unified migration agentSetAbstraction H:3204 follows captured migration support at :3158, with catch classifier :3270. F4 leaves support true on authoritative rejection.
Builder approval/referral for trading approveBuilderFee H:5233 and setReferrer :17075 follow shared trading setup :3611/:3645. prepareTradingWallet :15615 uses that readiness; existing context checks :15602 provide an example for F1's correction.
Order, leverage and strategy submission updateLeverage :6229, order :6384/:7361/:7883 and twapOrder :7210 are reached after action/trading readiness, including :6583/:7039. Retained lifecycle/strategy helpers are unchanged. F1/F4 still compromise the support decision.
TWAP rollback/cancel and Chase cancel twapCancel :7246 and cancel :8571 remain downstream of guarded action setup or an existing strategy session; no new lifecycle owner added. Same-context support assumption shares F1/F4 limits.
Public cancel/edit/position close/TP-SL cancelByCloid :9446, cancel :9535/:10288/:10417, modify :10173 and order :10754 use trading readiness, with existing error/result handling retained.
Margin mutation updateIsolatedMargin :12173 uses only ensureReady :12123. No support guard, F2. Controller/service/aggregated reachability at C:3587, TradingService:2679, A:904.
Collateral transfer sendAsset :15231 uses only ensureReady :15217. No support guard, F2. Transfer helper is also used for terminal strategy rebalance.
Withdrawal withdraw3 :15095 follows new support guard :15030, then awaits migration/balance reads; signer reselects main account, F1.
Legacy subscription-builder approval approveBuilderFee :5389 remains in an existing deprecated provider method. Public controller action is already a no-op C:6727. It is not a new active production route in this change; client/direct-provider callers are NOT_CHECKED.
Raw SDK exposure getExchangeClient H:16397 exposes a client for existing fixture/provider consumers. No new export or use added; downstream external callers are NOT_CHECKED.

Acceptance and changed-file reconciliation

Supplied claim Outcome Evidence
Provider-owned support exposed through controller/messenger/aggregation PASS source; NOT_CHECKED consumer dispatch/import C:2896/:969, action types:84/:1612, A:316, public types/exports.
Omitted provider hook remains supported PASS static C:2903, A:320, corresponding controller/aggregation tests.
Unsupported Hyperliquid accounts blocked before trade/withdrawal signing FINDING F1, F2, F4 Fixed-context order/withdraw paths covered, but signing context, sibling mutation coverage and stale authoritative support are incomplete.
Failed probes uncached and retryable PASS ordinary network failure; FINDING F3 T:390 covers an info-call rejection handled as undefined; reader acquisition rejects outside catch and poisons cache.
Successful support coalesced/cached per account/network session PASS normal keying/coalescing; FINDING F4 invalidation H:2914/:2930; T:376. Full disconnect clears :16472. Authoritative contrary exchange result never replaces supported.
Existing EXCHANGE_MULTI_SIG_REQUIRED error reused PASS H:2953 and perpsErrorCodes.ts:109.
Package-only change, no dependency upgrade, Mobile integration deferred PASS scope; NOT_CHECKED client compatibility All 11 files within package. No version/dependency/lockfile change; supplied PR body explicitly defers Mobile.

All 11 changed files map to the contract matrix, provider behavior, changelog, mocks, or inspected tests. All applicable shared and Core rules have outcomes. No prior review findings were supplied, so there is no re-review disposition to invent.

Limitations and recommended action

REQUEST_CHANGES for F1-F4. Bind support to the operation context through signing, protect the remaining mutation paths, evict rejected probes, and promote authoritative multisig write rejection into the account/network support cache. Add targeted regressions for those failures and consumer-style assertions for the new public types/action.

Mobile/Extension source, recorded revisions, release adoption, UI/locale/error handling, client tracing and package compilation are NOT_CHECKED. No tests, builds, runtime recipes, or app flows ran. Run QA questions are separate from the static findings above. Neither client compatibility nor runtime success is implied by completed checklist rows.

Source remained unchanged, with the same initial untracked .cursor/skills/ entry. No commit, publication or PR comment was made.

* signer set for the active account.
*/
async #assertAccountSupported(): Promise<void> {
const support = await this.#getAccountSupportForContext();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major. Bind the support result to the account that will sign. If withdrawal starts for supported account A, A's userToMultiSigSigners response is delayed, and selection changes to multisig account B before A resolves with null, this guard accepts A's result. With unified migration disabled or B already compatible, withdrawal continues to withdraw3, while the wallet adapter reads B at signing time. B therefore signs without its support being checked. Capture the account/network/lifecycle and reject a changed context before signing; cover this transition with a delayed-probe test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated and fixed in fe0e7c91ea. The guard now captures the account, network, and provider lifecycle, revalidates them after the delayed support probe, and checks the captured context again before withdrawal dispatch. The regression switches accounts while the probe is pending and verifies that withdraw3 is never called.

}): Promise<BuilderFeeSetupContext | undefined> {
// First ensure basic initialization is complete
await this.#ensureReady();
await this.#assertAccountSupported();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major. Apply the account-support guard to the remaining exchange mutations. updateMargin only calls #ensureReady() before updateIsolatedMargin at line 12173, and transferBetweenDexs likewise reaches sendAsset at line 15231 without this trading-readiness helper. A known multisig account with an isolated position can still request a margin adjustment through PerpsController.updateMargin and reach signing instead of returning EXCHANGE_MULTI_SIG_REQUIRED. Guard these paths and test that unsupported accounts never call either SDK mutation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated and fixed in fe0e7c91ea. updateMargin and transferBetweenDexs now run the account-support guard and revalidate the captured context immediately before their SDK mutations. The regression verifies unsupported accounts never call updateIsolatedMargin or sendAsset.

);
this.#accountSupportByContext.set(cacheKey, support);

probe

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major. Evict rejected probes as well as probes that resolve undefined. getInfoClient() is evaluated in #isHyperliquidMultiSigAccount's default argument outside its try, so it rejects while reconnect temporarily removes WebSocket clients. The rejected support promise is stored here, but this cleanup only deletes on resolved undefined and its catch leaves the entry intact. Once reconnect succeeds, later support checks, trades and withdrawals keep returning the cached CLIENT_NOT_INITIALIZED rejection until a full disconnect. Handle reader acquisition inside the fail-open boundary and remove failed entries; test a first probe during reconnect followed by recovery.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated and fixed in fe0e7c91ea. Info-client acquisition now occurs inside the probe’s fail-open boundary, and both undefined and rejected probes evict their exact cache entry. The recovery regression verifies an unavailable reader fails open and the next check re-probes successfully.

? { isSupported: false, reason: 'multi_sig_account' }
: { isSupported: true },
);
this.#accountSupportByContext.set(cacheKey, support);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major. Replace cached support when an exchange write authoritatively reports Multi-sig required. The existing account-mode test at lines 1743-1789 covers a stale null probe followed by that migration rejection. This new cache retains isSupported:true, while the migration catch only updates TradingReadinessCache. Later getAccountSupport and action guards therefore keep treating the known multisig account as supported, and migration skips its next probe because attempted is already cached. Record unsupported support for the captured account/network on that rejection and extend the test to cover support plus a later order or withdrawal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated and fixed in fe0e7c91ea. An authoritative Multi-sig required migration rejection now replaces the account/network support cache entry with unsupported. The existing stale-probe regression now verifies getAccountSupport is corrected and a later withdrawal is blocked before withdraw3.

@geositta
geositta requested a review from abretonc7s October 9, 2026 16:24
@geositta
geositta force-pushed the TAT-3752-account-support branch from fe0e7c9 to 1ef0f7d Compare October 9, 2026 21:21

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/perps-controller/src/providers/HyperLiquidProvider.ts Outdated

@abretonc7s abretonc7s left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed 1ef0f7d..0ceb168. The deferred migration rejection is fixed: the new cached-support fence at HyperLiquidProvider.ts:3013-3025 runs after migration and before withdraw3, and account-mode.test.ts:604-647 covers it. My four earlier comments are still addressed.

One issue is still open: the account context isn't carried to every signing boundary (HyperLiquidProvider.ts:3777). The captured context stays local to the readiness check (:3706) and is dropped on return (:3786), but migration (:3295), referral (:17182), builder (:5336), updateLeverage (:6332) and order submission (:6487) can all sign after a delayed read. Example: account A passes the support check, userAbstraction is delayed, and the selection switches to B. agentSetAbstraction then signs with B before the new check rejects the action. A switch during resolveDefaultMarginMode can also reach updateLeverage and the final order unchecked. That can trigger a signing prompt for, or mutate, an account whose support was never checked. Please pass the captured account/network/lifecycle through to each signing call and reject on mismatch, with a test for a switch during the delayed read.

@geositta

Copy link
Copy Markdown
Contributor Author

Addressed the remaining account-context signing-boundary finding from this review in 39841885dd.

Trading readiness now returns and threads the checked account/network/lifecycle context through setup and subsequent signing boundaries, including migration, referral, builder approval, leverage, orders, strategy/cancel/TP-SL paths, Chase sessions, and HIP-3 transfers. Added regressions for account switches during delayed abstraction and margin-mode reads; both reject with PROVIDER_LIFECYCLE_STALE before any write.

Verified with the full @metamask/perps-controller test suite, package build, focused Oxlint, formatting, and changelog validation.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3984188. Configure here.

Comment thread packages/perps-controller/src/providers/HyperLiquidProvider.ts
Comment thread packages/perps-controller/src/providers/HyperLiquidProvider.ts
@geositta

Copy link
Copy Markdown
Contributor Author

Follow-up hardening for the account-context fix is in e497ae1950. Persisted HIP-3 TWAP cleanup now retains its original account/network scope while rebinding the transfer to the current provider lifecycle after reconnect. The transfer boundary still revalidates account, network, lifecycle, and support immediately before signing. Added a reconnect regression that first failed with the stale lifecycle and now verifies retry/cleanup with the fresh context.

Verified with the full @metamask/perps-controller suite, package build, focused Oxlint, formatting, and changelog validation.

geositta and others added 9 commits October 9, 2026 21:49
Expose provider-owned account support checks and prevent unsupported accounts from signing Perps trades or withdrawals.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@geositta
geositta force-pushed the TAT-3752-account-support branch from e497ae1 to 78e0dc4 Compare October 10, 2026 02:54
@geositta
geositta enabled auto-merge October 10, 2026 03:06
@geositta
geositta requested a review from abretonc7s October 10, 2026 03:39

This branch was successfully deployed

1 active (outdated) deployment
default-branch — 7c458e4b Deployed Oct 8, 2026 by geositta via Determine whether this PR is a release PR #5313
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants