Skip to content

fix(perps-controller): tighten Lighter data validation - #10015

Merged
abretonc7s merged 2 commits into
mainfrom
fix/perps/lighter-data-integrity-follow-up
Aug 31, 2026
Merged

abretonc7s merged 2 commits into
mainfrom
fix/perps/lighter-data-integrity-follow-up

Conversation

@abretonc7s

@abretonc7s abretonc7s commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

Description

Reject malformed Lighter WebSocket fill fields and invalid margin fractions before they reach perps state. Adds coverage for inactive-history cursor nonprogress found during the final #9889 review.

Changelog

The existing unreleased Lighter entry already covers this pre-release correction.

Related issues

Follow-up to #9889.

Testing

  • Focused Lighter suites: 368 passed
  • Full perps package: 84 suites, 3,675 passed, 40 skipped
  • TypeScript, ESLint, Prettier, changelog validation, and diff checks
  • Root build

Note

Medium Risk
Changes leverage derivation and fill ingestion boundaries for Lighter; overly strict validation could reject otherwise-usable venue rows or block market/position reads until metadata is fixed.

Overview
Tightens Lighter venue data handling so bad margin metadata and trade payloads fail before they affect markets, leverage, or fill streams.

Max leverage is now derived through a shared deriveLighterMaxLeverage helper that requires minInitialMarginFraction to be a safe integer in 1–10,000 and rejects leverage below 1x. getMarkets, margin cache refresh, getMaxLeverage, and internal leverage lookups use this path; refresh also validates maintenance margin fractions in the same range. REST parsing swaps generic positive numbers for a MarginFractionStruct on initial/default/maintenance margin fields.

Fill adaptation (adaptFillFromLighterTrade) adds checks on trade identifiers, requires isMakerAsk to be a boolean, and parses maker/taker fees via parseLighterTradeFee so malformed counterparty fee fields invalidate the row while still allowing valid nonzero counterparty fees. Matching WebSocket fill drops and unit tests cover these cases.

Tests add rejection paths for out-of-range/non-integer margin fractions, malformed fill fields, and inactive-order history paging that advances the cursor with zero rows (TP/SL recovery fails closed).

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

@abretonc7s
abretonc7s marked this pull request as ready for review August 31, 2026 02:25
@abretonc7s
abretonc7s requested a review from a team as a code owner August 31, 2026 02:25
@abretonc7s
abretonc7s enabled auto-merge August 31, 2026 03:07
@abretonc7s
abretonc7s added this pull request to the merge queue Aug 31, 2026
Merged via the queue into main with commit c2ddaca Aug 31, 2026
47 checks passed
@abretonc7s
abretonc7s deleted the fix/perps/lighter-data-integrity-follow-up branch August 31, 2026 07:24
@abretonc7s abretonc7s mentioned this pull request Aug 31, 2026
4 tasks done
pull Bot pushed a commit to Reality2byte/core that referenced this pull request Aug 31, 2026
## Explanation

Releases `@metamask/perps-controller` **14.0.0 → 15.0.0**. The monorepo
version goes **1217.0.0 → 1218.0.0**.

No other package is being published. Perps-controller has no in-monorepo
dependents that need a workspace range bump.

The bump is **major**. Adding a new member to the public
`PerpsProviderType` union is a breaking type change for exhaustive
switches:

- **Added (breaking):** Lighter as a perps venue (initial
implementation, disabled by default)
([MetaMask#9889](MetaMask#9889),
[MetaMask#10015](MetaMask#10015)).
`PerpsProviderType` gains `'lighter'`. Enablement requires client opt-in
(`providerCredentials.lighter.enabled`, or the
`perpsLighterProviderEnabled` remote feature flag plus a client-supplied
`providerCredentials.lighter.signerBridge`). Without a bridge the
provider is read-only. Lighter is off by default, so clients that do not
opt in can upgrade without runtime behavior change; exhaustive
`PerpsProviderType` switches still need a new case to typecheck.
- **Fixed:** Preserve accepted HyperLiquid Scale orders when part of a
batch is rejected ([MetaMask#9989](MetaMask#9989)).

## Changelog

Moved Unreleased entries in `packages/perps-controller/CHANGELOG.md`
under `[15.0.0]`. Folded the pre-release Lighter validation follow-up
(MetaMask#10015) into the Lighter Added entry (as that PR intended). Breaking
entry is prefixed with `**BREAKING:**` and includes adaptation notes.

## References

- Source PRs: [MetaMask#9889](MetaMask#9889),
[MetaMask#10015](MetaMask#10015),
[MetaMask#9989](MetaMask#9989)

- No draft Mobile/Extension adoption PRs required to keep current
clients compiling: Lighter stays disabled until clients opt in. Client
work to enable Lighter (credentials, signer bridge, UI) is separate.

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [x] I've communicated my changes to consumers by [updating changelogs
for packages I've
changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md)
- [x] I've introduced [breaking
changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md)
in this PR and have prepared draft pull requests for clients and
consumer packages to resolve them


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
pull Bot pushed a commit to Reality2byte/core that referenced this pull request Sep 4, 2026
## Explanation

Lighter retains inactive markets for historical identity and can zero
their order minimums and margin fractions. The strict response decoder
rejected those retired rows, which prevented otherwise valid active
markets from loading.

This change accepts zero constraints only for inactive rows, omits
retired markets whose risk metadata cannot produce canonical market
data, and preserves inactive rows with valid metadata as delisted.
Positions left on retired markets remain readable using their current
position leverage as a non-tradable cap. Active markets remain strict
and fail closed on zero or malformed constraints.

Validated with:

- 322 focused Lighter service/provider tests
- Full `@metamask/perps-controller` test suite and Core build
- Targeted ESLint, formatting, and changelog validation
- Mobile yalc integration using package `16.1.0` (`yalc.sig`
`ee73aeec8330f58ce0a6e9c6b4cf42a3`)
- A 36/36-node iOS recipe covering Lighter and Hyperliquid on mainnet
and testnet
- Revalidated Lighter mainnet on iOS `mm-1`: live markets loaded and the
invalid inactive-market metadata error was absent

## References

- Related to https://consensyssoftware.atlassian.net/browse/TAT-3863
- Related to https://consensyssoftware.atlassian.net/browse/TAT-3766
- Unblocks MetaMask/metamask-mobile#34865
- Follow-up to MetaMask#9889 and
MetaMask#10015

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [x] I've communicated my changes to consumers by [updating changelogs
for packages I've
changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md)
- [ ] I've introduced [breaking
changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md)
in this PR and have prepared draft pull requests for clients and
consumer packages to resolve them — not applicable; this change is
non-breaking

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Scoped to Lighter public market-metadata parsing and listing; active
markets remain fail-closed on bad constraints, with no changes to
signing, deposits, or position operations.
>
> **Overview**
> Fixes Lighter market loading when the venue returns **inactive** perp
rows with zeroed minimum order sizes or margin fractions—metadata
Lighter keeps for history but that previously made the strict HTTP
decoder reject the whole response and block active markets.
>
> **`LighterClientService`** now allows zero minimums and margin
fractions only when `status === 'inactive'`, with `refine` validators
still requiring positive values for **active** markets.
**`LighterProvider`** adds `isInactiveMarketWithoutUsableRiskMetadata`
to drop those retired rows from `getMarketDataWithPrices` and margin
caching, skips them in `getMarkets` when leverage metadata is omitted
(instead of throwing), and still surfaces inactive markets with valid
risk data as **delisted**. Tests cover both omission and preservation
paths.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
445456c. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants