Skip to content

fix(polymarket): a withdrawal that may have moved money is never signed twice - #174

Merged
VickyXAI merged 4 commits into
mainfrom
fix/withdraw-idempotency
Oct 8, 2026
Merged

VickyXAI merged 4 commits into
mainfrom
fix/withdraw-idempotency

Conversation

@VickyXAI

@VickyXAI VickyXAI commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • A retry that resolves the earlier withdrawal ends the call. Settled, failed or expired: the outcome is reported and nothing new is signed. Before this, the guard cleared itself and the same confirm:true call signed a second transfer.
  • EOA (sigType 0) withdrawals have no deadline. The transfer is signed locally, and its hash, nonce and signed bytes are persisted before the broadcast. Only a receipt resolves it. Until then, a retry re-broadcasts the same bytes, so it can execute at most once.
    • The old 300s expiry is gone. So is any reading of an advanced nonce plus an unknown hash as "dropped".
    • The guard is released before a receipt only on an outright node rejection that the RPC doesn't know. "nonce too low" and "already known" keep it.
  • The relayer withdrawal is recorded before submit, not only in the catch, so a process killed mid-POST still leaves the guard. A definite 4xx still releases it.
  • HTTP 408 is now an unknown outcome in isDefiniteRejection, which is shared by the CLOB submit, the relayer batch and fund.
  • Bumps to 0.54.2 with a CHANGELOG entry.

Test plan

  • npm test: 1394/1394
  • npm run build
  • New tests: test/polymarket-withdraw-eoa.test.ts and test/polymarket-definite-rejection.test.ts. The withdraw and relayer tests were updated for the new lifecycle.
  • Mutation-checked: I reverted each of six fixes and confirmed its tests fail.
    1. Resolution continues into a new withdrawal.
    2. EOA time expiry.
    3. "nonce too low" releases the guard.
    4. Fresh signature on retry.
    5. Relayer guard armed only after the submit.
    6. 408 treated as definite.

…ed twice

A retry that resolved the earlier withdrawal now ends the call instead of
signing another. The EOA rail signs locally and persists hash, nonce and
bytes before the broadcast; only a receipt resolves it, and a retry
re-broadcasts the same bytes. No time expiry and no nonce inference for a
plain transaction. The relayer withdrawal is recorded before the submit.
HTTP 408 is an unknown outcome, not a definite rejection.
The public reader and the write RPC can disagree on the pending pool; a
reader nonce could collide with a transaction only the write node has seen
and then wedge the guard behind a 'nonce too low'. prepareTransactionRequest
now fills the nonce through the wallet's own transport, and that nonce is
what gets persisted.
@VickyXAI
VickyXAI merged commit e9b2bd5 into main Oct 8, 2026
3 checks passed
@VickyXAI
VickyXAI deleted the fix/withdraw-idempotency branch October 8, 2026 02:54
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