Skip to content

fix: additional protection against double fuul submission#921

Open
owen-eth wants to merge 3 commits intomainfrom
fastswap-miles-fuel-idempotency
Open

fix: additional protection against double fuul submission#921
owen-eth wants to merge 3 commits intomainfrom
fastswap-miles-fuel-idempotency

Conversation

@owen-eth
Copy link
Copy Markdown
Contributor

@owen-eth owen-eth commented Apr 18, 2026

Describe your changes

Layer 1 (load-bearing): insertEvent existence check

insertEvent now SELECTs by tx_hash before inserting. If the row
exists, it falls through to a COALESCE-only UPDATE that backfills
gas_cost / block_timestamp only when previously NULL, and preserves
every other column (processed, miles, surplus_eth,
net_profit_eth, bid_cost). Block rescans — for any reason — are now
non-destructive.

Layer 2: miles-non-null idempotency check

processMiles and processERC20Miles read the miles column on every
pending row. If non-null, the row's outcome was already settled (Fuul
submitted, or a terminal no-credit path), so they skip submitToFuel
and just refresh derived columns / flip processed=true. Backstop in
case processed ever gets reset by hand. Uses the existing miles
column — no schema migration.

Layer 3: atomic saveLastBlock

Replaced the prior DELETE-then-INSERT pattern (non-atomic, could vanish
the last_block row on a pod kill mid-pair) with a single INSERT.
fastswap_miles_meta has PRIMARY KEY(k), so INSERT upserts atomically.

Layer 4: Fuul-side dedup_id

Every submitToFuel call now includes dedup_id = tx_hash. If we ever
do send the same tx twice (e.g. service crashes in the ms between
submitToFuel succeeding and markProcessed running), Fuul drops the
duplicate server-side instead of re-crediting the user.

ERC20 already-swept guard

In the ERC20 path, the idempotency check runs before batch aggregation:
rows with miles already set are excluded from the sweep batch
(otherwise the new sweep would quote for tokens already moved out of
the executor wallet, breaking pro-rata allocation for every other row
in the batch). Those rows just get processed = true flipped via
markProcessedFlagOnly — preserving the original sweep's derived
columns, which we can't reproduce.

Rollback

No DB schema changes. Roll back to the previous image and restart —
every column the old binary reads/writes still exists. Already-settled
rows are preserved by Layer 1 even if rollback happens after a rescan.
Pending rows aren't block-gated (the SELECT filters by
processed = false, not block range), so rollback drains them on the
first post-rollback cycle.

Issue ticket number and link

Fixes # (issue)

Checklist before requesting a review

  • I have added tests that prove my fix is effective or that my feature works
  • I have made corresponding changes to the documentation

@owen-eth owen-eth requested a review from harshsingh1002 April 18, 2026 19:50
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