Skip to content

fix(perps-controller): keep the terminal TWAP record when a completing fill ties lastUpdated - #10122

Open
abretonc7s wants to merge 2 commits into
mainfrom
fix/perps-twap-terminal-record-dedupe
Open

fix(perps-controller): keep the terminal TWAP record when a completing fill ties lastUpdated#10122
abretonc7s wants to merge 2 commits into
mainfrom
fix/perps-twap-terminal-record-dedupe

Conversation

@abretonc7s

@abretonc7s abretonc7s commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Explanation

A finished HyperLiquid TWAP could stay listed as live indefinitely.

The venue reports one lifecycle as several twapHistory entries — an activation plus a terminal record — and lastUpdated is max(startedAt, normalizeTwapHistoryTimestamp(entry.time), ...fills). Slice fills are looked up by twapId, so every entry of a schedule receives the same fills, while entry.time is a whole-second value. When the final fill is what completed the schedule, that fill's millisecond timestamp outranks both entries' own timestamps and both entries derive an identical lastUpdated.

Collapsing with order.lastUpdated >= existing.lastUpdated admits that tie. The venue returns the terminal record first, so the activation was iterated last and overwrote it. resolveTwapOrderStatus maps activatedActive, so the schedule was reported live, carrying the activation's executedSz: 0, and clients offered a cancel affordance the venue can no longer act on.

Schedules that were terminated stop seconds after their last fill, so they never tie and were unaffected — which is why this presents as an intermittent subset rather than every schedule.

Observed on a real mainnet account: of 6 schedules, the 4 terminated ones resolved correctly and both finished ones were reported active, 74 and 18 days after they ended.

Fix

supersedesCollapsedTwapOrder decides terminality first, then compares lastUpdated strictly so an equal timestamp keeps the record already collapsed. Active is the only non-terminal PerpsTwapLifecycleStatus, so "has a terminal record" is exactly status !== Active.

This keeps the module's existing stance that a schedule is terminal only when the venue says so — it never infers termination from elapsed time, so collateral still cannot be reclaimed from a live TWAP.

The polled read (getTwapOrders) and the subscription adapter (subscribeToTwapOrders) shared the comparison; both are corrected.

References

Surfaced while investigating the mobile symptom where expired TWAPs render in the Active tab.

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, README.md) for new or updated code as appropriate
  • I've highlighted breaking changes using the "BREAKING" category above as appropriate
  • I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes

Note

Medium Risk
Changes TWAP lifecycle presentation and cancel affordances for a real edge case; logic is localized to history collapse but affects polled and streamed TWAP state.

Overview
Fixes finished HyperLiquid TWAPs staying in the Active list when the activation and terminal twapHistory rows share the same lastUpdated after the completing slice fill.

Collapse logic no longer uses lastUpdated >=; supersedesCollapsedTwapOrder prefers a terminal venue status over Active, then uses strict lastUpdated so a tie keeps the record already merged. getTwapOrders and subscribeToTwapOrders both use this rule. Tests cover both history orderings when activation and terminal entries tie on lastUpdated.

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

The venue reports one TWAP lifecycle as several history entries — an
activation plus a terminal record — and `lastUpdated` folds in slice fills
that every entry sharing a `twapId` receives. A schedule finished by its
final fill therefore derives the same `lastUpdated` on both entries, because
that fill outranks each entry's own whole-second timestamp.

Collapsing entries with `order.lastUpdated >= existing.lastUpdated` admits
that tie. The venue returns the terminal record first, so the activation was
iterated last and overwrote it, and `resolveTwapOrderStatus` then mapped
`activated` to `Active`. A finished schedule stayed listed as live
indefinitely, reporting the activation's zero executed size and offering a
cancel affordance the venue can no longer act on.

Decide terminality first and compare `lastUpdated` strictly, so an equal
timestamp keeps the record already collapsed. Both the polled read and the
subscription adapter shared the comparison and are both corrected.

Co-authored-by: Cursor <cursoragent@cursor.com>
@abretonc7s
abretonc7s requested a review from a team as a code owner September 5, 2026 10:02
@abretonc7s
abretonc7s deployed to default-branch September 5, 2026 10:02 — with GitHub Actions Active
Co-authored-by: Cursor <cursoragent@cursor.com>
@abretonc7s
abretonc7s requested a review from a team as a code owner September 5, 2026 10:03
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.

1 participant