Skip to content

fix: enforce CEI ordering in register_solver, deregister_solver, fill… - #152

Open
ironclad-x wants to merge 2 commits into
stellar-vortex-protocol:mainfrom
ironclad-x:fix/deregister-solver-ordering
Open

fix: enforce CEI ordering in register_solver, deregister_solver, fill…#152
ironclad-x wants to merge 2 commits into
stellar-vortex-protocol:mainfrom
ironclad-x:fix/deregister-solver-ordering

Conversation

@ironclad-x

Copy link
Copy Markdown

fix: submit_intent silently overwrites an existing intent record on
collision

Closes #25
Closes #26
Closes #27
Closes #28

What changed

submit_intent — intent_id collision overwrites existing record

(#25)

compute_intent_id hashed only (user, src_chain, src_amount, timestamp).
Two submit_intent calls from the same user with identical args in the
same
ledger close produced the same intent_id, and the second write
silently
overwrote the first IntentRecord — the original intent was lost with
no
error.

Fix:

  • Added DataKey::UserNonce(Address) — a per-user submit counter that
    increments on every submit_intent call and is included in the
    compute_intent_id preimage, making same-ledger collisions
    impossible.
  • Added Error::IntentAlreadyExists (variant 22) with a .has() guard
    in
    submit_intent that rejects rather than silently overwrites on any
    hash
    collision.

deregister_solver — bond returned before record removed (#28)

The SolverRecord was removed from storage after the bond transfer
back to
the solver. A re-entrant or sequenced double-call could find the record
still
present and trigger a second refund.

Fix: Remove the record and decrement TotalSolvers first — any
repeat
call hits SolverNotRegistered before reaching the transfer.

register_solver — bond pulled in before record persisted (#27)

client.transfer() pulled the bond in before the SolverRecord was
written
to storage. A failure between those two steps would leave the contract
holding
funds with no record of whose bond they are.

Fix: Persist the record (and increment TotalSolvers) before the
transfer so storage is always consistent with actual holdings.

fill_intent — token transfers before state persisted (#26)

dst_client.transfer() to the user and fee recipient ran before the
intent
state was set to Filled and before solver/protocol stats were written.
A
hostile SEP-41 token that re-entered fill_intent or slash_solver
mid-transfer would have seen the intent still in Accepted state.

Fix: Write all state changes (intent state, solver stats, protocol
volume)
to storage before any token transfer executes.

Tests added

Test Covers
same_ledger_identical_intents_produce_distinct_ids #25 — exact
reproduction case; second submit no longer overwrites first
nonce_increments_per_user_across_submissions #25 — three
back-to-back submits all get distinct ids
different_users_same_params_same_ledger_produce_distinct_ids #25
different users on nonce 0 still diverge
double_deregister_rejected_cleanly #28 — second deregister returns
SolverNotRegistered; no double-refund
solver_record_consistent_with_token_balances_after_register #27
record.bond_amount always equals tokens held by contract
fill_intent_state_committed_before_transfer_and_double_fill_rejected
#26 — state is Filled before transfers; second fill returns
IntentAlreadyFilled

…_intent

- deregister_solver: remove SolverRecord and decrement TotalSolvers before
  the bond transfer so a re-entrant or double-call sees SolverNotRegistered
  rather than a live record eligible for a second refund. Closes stellar-vortex-protocol#28

- register_solver: persist SolverRecord and increment TotalSolvers before
  client.transfer() pulls funds in so storage is always consistent with
  actual contract holdings. Closes stellar-vortex-protocol#27

- fill_intent: write intent state (Filled), filled_at, fill_amount, solver
  stats, and protocol volume to storage before any dst_client.transfer()
  executes. A hostile SEP-41 token that re-enters mid-transfer will see the
  intent already Filled and be rejected. Closes stellar-vortex-protocol#26

Regression tests added for each fix:
- double_deregister_rejected_cleanly
- solver_record_consistent_with_token_balances_after_register
- fill_intent_state_committed_before_transfer_and_double_fill_rejected
…ollision

compute_intent_id previously hashed only (user, src_chain, src_amount,
timestamp). Two submit_intent calls from the same user with identical args
in the same ledger close produced the same intent_id, and the second write
silently overwrote the first IntentRecord.

Changes:
- Add DataKey::UserNonce(Address) — a per-user submit counter stored in
  instance storage alongside Admin/FeeRecipient etc.
- Increment the nonce on every submit_intent call and include it in the
  compute_intent_id preimage, making same-ledger collisions impossible in
  practice.
- Add Error::IntentAlreadyExists (variant 22) and a .has() guard in
  submit_intent that rejects rather than silently overwrites on any
  (extremely unlikely) hash collision.

Regression tests added:
- same_ledger_identical_intents_produce_distinct_ids
- nonce_increments_per_user_across_submissions
- different_users_same_params_same_ledger_produce_distinct_ids

Closes stellar-vortex-protocol#25
@drips-wave

drips-wave Bot commented Jul 26, 2026

Copy link
Copy Markdown

@ironclad-x Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment