Skip to content

[simplex, docs]: bind source confirmations to the scanned placement block - #1409

Open
seunlanlege wants to merge 1 commit into
mainfrom
simplex/confirmations-bound-to-placement-block
Open

seunlanlege wants to merge 1 commit into
mainfrom
simplex/confirmations-bound-to-placement-block

Conversation

@seunlanlege

Copy link
Copy Markdown
Member

Why

For a cross-chain order the filler read the source escrow once, on a single endpoint, then waited for confirmations of the placement transaction hash (QuorumPublicClient.getTransactionConfirmations). The quorum grouped receipts by (blockHash, blockNumber) but returned only the depth, and nothing re-read the escrow before executeOrder bid.

IntentGatewayV2.placeOrder sets order.nonce = _nonce++, and the commitment hashes the whole order. A source reorg that re-mines the placement in a different position, for example behind another placement made in that window, keeps the transaction hash but changes the nonce and so the commitment. The wait kept counting confirmations of the re-mined transaction. The filler then bid on the stale commitment, which the placer can select with its session key; the destination paid the output, and redeeming on source reverted UnknownOrder, losing one full output. Exploiting it needs a reorg deeper than the confirmation curve during the wait (Polygon: 2–5 blocks).

What changes

  • The scanned block reaches the filler. EventMonitor passes the blockHash already on ScannedOrder through newOrder into IntentFiller.handleNewOrder.
  • Confirmations are bound to that block. New QuorumPublicClient.getTransactionInclusion returns the blockHash, blockNumber and confirmations a quorum agreed on (getTransactionConfirmations is now its depth; aggregation moved to aggregateInclusion). Every poll, including the first, compares the agreed block with the scanned one. On a mismatch the wait stops and the order is skipped with orderSkipped reason Source placement reorged.
  • The escrow is re-read on a quorum before bidding. New QuorumPublicClient.readWithQuorum(label, read) runs a read on every endpoint and returns the value a quorum agrees on; failing endpoints don't vote, as on every quorum path. After the wait, verifyOrderOnSource(order, sourceQuorumClient) reads every leg's escrow through it, so each endpoint votes on the whole vector. An empty or unreadable escrow skips the order (Source escrow missing after confirmations).
  • Fail closed without a block. A cross-chain order that needs confirmations but arrives without a scanned block hash is skipped (Placement block unknown). The built-in ChainScanner always supplies one from mined logs.
  • Unchanged: same-chain orders and zero-confirmation curve points make no quorum read, as before.
  • Trade-offs:
    • A placement re-mined into a different block is skipped even when its nonce did not change. That is a missed fill, not a loss.
    • Cross-chain bids wait for one more quorum round, the escrow read.
  • Docs: sdk/packages/simplex/docs/ai/changelog/2026-10-16-confirmations-bound-to-the-placement-block.md, one sentence in the order-intake flow, and the "Quorum-checked confirmations" section of docs/content/developers/evm/simplex/confirmations.mdx. No version bumps.

Validation

  • vitest --watch=false src/tests/quorum-public-client.test.ts src/tests/quorum-failure-handling.test.ts src/tests/core src/tests/activity-recorder.test.ts src/tests/scanner: 24 files, 221 passed, 5 skipped (the network-gated quorum tests).
  • New src/tests/core/source-reorg.test.ts (11 cases) drives handleNewOrder through the wait:
    • a placement that moves blocks on the first read or mid-wait is skipped;
    • an empty, failing or partially empty escrow (two legs) on the quorum read blocks the bid, and the real per-leg readLegEscrow callback runs against a stub client;
    • a missing block hash fails closed;
    • same-chain and zero-confirmation cross-chain orders make no quorum call.
      Most cases fail on the old code.
  • New quorum-public-client.test.ts cases for aggregateInclusion, getTransactionInclusion (including a quorum agreeing on a re-mined block) and readWithQuorum (divergent minority, failing endpoints including a synchronous throw, bigint vectors, no quorum). chain-lifecycle.test.ts checks that newOrder carries blockHash.
  • tsc --noEmit: no errors beyond the 11 already on main. Biome on the changed files: no new diagnostics.
  • Not tested against a live chain reorg; the reorg paths are exercised with stubbed quorum clients.

…lock

A cross-chain order's confirmation wait now counts depth only while a quorum
places the placement transaction in the block its OrderPlaced log was scanned
from, and the source escrow is re-read through the quorum right before bidding.
A reorg that re-mines the placement under a new nonce keeps the transaction
hash but changes the commitment; the filler no longer bids on the stale one.

This branch has not been deployed

No deployments
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