Skip to content

twap: grant one next-block retry before dropping on InvalidEip1271Signature#473

Open
mfw78 wants to merge 1 commit into
feat/m4-conditional-order-removedfrom
feat/m4-eip1271-one-block-retry
Open

twap: grant one next-block retry before dropping on InvalidEip1271Signature#473
mfw78 wants to merge 1 commit into
feat/m4-conditional-order-removedfrom
feat/m4-eip1271-one-block-retry

Conversation

@mfw78

@mfw78 mfw78 commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

What

Adds RetryAction::DropOnRepeat: the first InvalidEip1271Signature refusal for a watch retries on the next block instead of dropping; a repeat refusal on a later block drops as before. Tracked via a refused: block marker on the watch, cleared on an accepted submit. Reclassifies the InvalidEip1271Signature row in classification.toml/classification_data.rs from drop to drop-on-repeat. The denied wire error stays coarse, so cow_venue::classify_denied re-derives the action from the errorType prefix in the detail string on both the videre-sdk keeper sweep and the legacy composable-cow sweep.

Why

A first-time TWAP user's Safe wiring and ComposableCoW.create() can land in the same block as the indexed event. Shepherd polls and submits inside that block, faster than an orderbook node can verify against its own head, so a signature that is valid one block later is rejected and the watch is dropped permanently. Treating only a repeated refusal as permanent absorbs the one-block indexing race while still dropping genuinely broken signatures.

Closes #320

Testing

nexum-sdk and videre-sdk keeper unit tests cover the marker set/clear/repeat-drop lifecycle; cow-venue adapter and classification tests cover the drop-on-repeat row and classify_denied prefix recovery; composable-cow sweep tests cover the marker clear on acceptance.

AI Assistance

Implemented with AI assistance under human direction and review.

…nature

A first-time user's Safe wiring and ComposableCoW create() land in one
block, so the orderbook rejects the first submission against its own
head and the keeper dropped a valid watch. The classification table
gains a drop-on-repeat action, the retry ledger records the refused
block and gates one next-block retry, and a denied refusal re-enters
the table by its errorType prefix so the grace survives the coarse
venue-error collapse. A repeat on a later block still drops.

@lgahdl lgahdl left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The marker lifecycle, classification table wiring, and string-prefix matching are all solid where they were actually applied: Retrier::apply's DropOnRepeat arm correctly distinguishes "later block" from "same block" (verified: tick.block > block -> drop, tick.block <= block -> no-op), clearing on accept is idempotent and WatchSet::remove also deletes the marker so nothing orphans, classification.toml's row moved cleanly to drop-on-repeat with no dangling reference to the old drop behavior, and classify_denied's prefix match is anchored (split_once(':') then exact table lookup, not a substring/contains search) so there's no false-positive risk from an unrelated error's text.

But the PR's central claim — that this logic is applied on "both the videre-sdk keeper sweep and the legacy composable-cow sweep" — is false as shipped. I checked crates/videre-sdk/src/keeper.rs directly at this commit: retry_action's Denied arm is completely unchanged by this diff and still reads:

VenueFault::UnknownVenue
| VenueFault::InvalidBody(_)
| VenueFault::Unsupported
| VenueFault::Denied(_) => RetryAction::Drop,

(crates/videre-sdk/src/keeper.rs:194-197). Only crates/composable-cow/src/sweep.rs:64-67 was actually edited to special-case VenueFault::Denied(detail) => classify_denied(detail). That means twap-monitor — which per #469 runs on the generic videre_sdk::Keeper::sweep path, and which is the exact scenario this PR's "Why" section describes ("a first-time TWAP user's Safe wiring... lands in the same block") — still drops the watch permanently on the FIRST InvalidEip1271Signature refusal. Only stop-loss (composable-cow's legacy path, per #471) actually gets the one-block grace this PR adds. The bug this PR closes #320 for is still live for its primary named beneficiary.

Fix: add the same VenueFault::Denied(detail) => classify_denied(detail) branch inside videre_sdk::keeper::retry_action's Denied arm (or fold the re-derivation into retry_action itself so both sweeps call through one place, closing the duplication risk at the same time).

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