Skip to content

feat(intent_settlement): proportional slashing (#193), bid window (#191), dispute flow (#188), multi-bond-token (#187) - #314

Open
iam-mercy wants to merge 1 commit into
stellar-vortex-protocol:mainfrom
iam-mercy:feat/settlement-hardening-187-188-191-193
Open

feat(intent_settlement): proportional slashing (#193), bid window (#191), dispute flow (#188), multi-bond-token (#187)#314
iam-mercy wants to merge 1 commit into
stellar-vortex-protocol:mainfrom
iam-mercy:feat/settlement-hardening-187-188-191-193

Conversation

@iam-mercy

Copy link
Copy Markdown

Summary

Implements four High-priority settlement-hardening issues in one change set.
They all touch the IntentState enum, slash_solver, fill_intent and the
solver bond model, so splitting them would mean four heavily-conflicting stacked
PRs; they're delivered together instead.

Issue Feature
#193 Dynamic, proportional bond slashing
#191 Competitive bid-window solver bidding
#188 Dispute-resolution state machine (Filling → Disputed → Resolved)
#187 Multi-bond-token support with per-token accounting

closes #193
closes #191
closes #188
closes #187


⚠️ Base branch does not compile

main (c5da0fd) fails cargo check with 67 errors — a prior bad merge
dropped ~15 DataKey variants, ~7 Error variants and ~12 constants, left
duplicate Error discriminants, and triplicated the fill_intent transfer
block. This is pre-existing and identical on origin/main and
stellar-vortex-protocol/main.

This PR adds zero new compile errors (still exactly 67, all pre-existing and
all outside the code added here), but CI will stay red until main is
repaired separately.
Because the crate can't build, the new tests could not be
executed and the wasm/proptest/clippy jobs cannot pass from this base. Every
addition here was written to be internally consistent and to match house style;
it needs a green main to be fully verified.


#193 — Proportional slashing

slash_solver no longer takes a flat 10 % of the bond. New helper
compute_slash_amount(bond, unfilled_amount):

exposure     = min(unfilled_amount, bond)          // same-token comparability
proportional = exposure / 10                        // 10% of what was at stake
cap          = bond * 1000 / 10_000                 // never worse than flat 10%
slash        = clamp(proportional, 1, min(cap, bond))
  • Integer-only, cannot panic (all operands ≥ 0, no division by zero).
  • Floor of 1 stroop preserves issue Add a minimum slash amount floor so tiny bonds round to a zero-value slash #32's guarantee.
  • A 500k-USDC-bond solver failing a 10-USDC intent now loses ~1 USDC, not
    50 000. A minimally-bonded solver failing a huge intent is still capped at
    10 % of bond (and at 100 % of bond via exposure ≤ bond).
  • SECURITY.md "Known Limitations" updated; solver_slashed event unchanged.

#191 — Competitive bid window

  • Storage-key collision fixed: is_bid_window_enabled now reads a dedicated
    DataKey::BidWindowEnabled instead of borrowing DstAllowlistEnabled "as a
    placeholder". set_dst_allowlist_enabled and set_bid_window_enabled are now
    fully independent (regression test included).
  • bid_intent(solver, intent_id, quoted_dst_amount) — reuses
    is_solver_eligible; records the bid only if strictly higher; tie-break:
    the first solver to a given amount keeps the lead
    .
  • settle_bids(intent_id) — permissionless. Winner → Accepted with a fresh
    FILL_WINDOW and accept_intent's bookkeeping (active_intents,
    OpenIntents). No usable bid (nobody bid, or the leader's bond dropped
    below the floor since bidding) → re-opened as Open with a fresh
    INTENT_EXPIRY, matching expire_intent's permissionless-materialisation
    pattern rather than getting stuck in Bidding.
  • get_best_bid view added.

#188 — Dispute-resolution state machine

Per docs/dispute-resolution-design.md. New states Filling, Disputed,
Resolved; new enum DisputeResolution { Upheld, Dismissed }; new
IntentRecord fields dispute_deadline, dispute_raised_at, resolution.

Entrypoint Who Effect
begin_fill assigned solver escrows a completing fill in the contract, opens DISPUTE_WINDOW (1 h)
dispute_fill intent user Filling → Disputed within the window
resolve_dispute arbiter (set_arbiter, defaults to admin) Upheld → full escrow to user + proportional slash; Dismissed → escrow − fee to user, no slash
release_fill anyone Filling + window elapsed → clean release (fee taken); Disputed + ARBITER_WINDOW (24 h) elapsed → full escrow to user, no slash, resolution == None marks the timeout

Escrowed tokens are held by the contract, never the solver; all state is
committed before any transfer (CEI). Every new error path has a
uniquely-discriminated Error variant (30–39).

#187 — Multi-bond-token support

Per docs/60-multi-bond-token-design.md, implemented additively (the design
doc's "remove bond_amount" schema would break every existing test and the
bond-conservation proptest with no way to verify the refactor against a
non-compiling base):

  • DataKey::SolverBond(solver, token) holds per-token balances.
    SolverRecord.bond_amount is kept as the default-token mirror so pre-[High] Implement multi-bond-token support with per-token accounting #187
    readers keep working; SolverRecord.bond_tokens: Vec<Address> enumerates the
    rest (cap MAX_BOND_TOKENS = 8).
  • register_solver / withdraw_bond / accept_intent keep their signatures
    (pinned to the default token) and gain register_solver_with_token /
    withdraw_bond_token / accept_intent_with_bond siblings ("Option A" from
    the design doc §7.2).
  • add_allowed_bond_token / remove_allowed_bond_token / set_bond_token_min /
    get_bond_token_min / get_solver_bond / get_solver_bonds.
  • IntentRecord.bond_token records which token backs each accepted intent;
    slash_solver slashes and pays out in that token. deregister_solver
    refunds every token the solver holds.
  • Per-token minimums: DataKey::MinBond(token), falling back to
    ProtocolConfig.min_bond for the default token and MIN_BOND otherwise.
  • New errors BondTokenNotAllowed = 40, TooManyBondTokens = 41 (discriminants
    differ from the design doc's §6 to avoid colliding with existing variants).
  • CHANGELOG.md [Unreleased] notes the storage-layout change and its
    upgrade behaviour.

Also

fill_intent's output/fee transfer block had been written three times by a
bad merge (paying the user 3× and the fee 2×). De-duplicated to a single
transfer after all state is committed.

Tests

25 new cases in test.rs:

proptest_bond.rs's AcceptAndSlash step now asserts the exact proportional
formula instead of the flat 10 %.

…low, multi-bond-token

Implements four High-priority settlement-hardening issues in one change set.
They share the IntentState enum, slash_solver, fill_intent and the solver
bond model, so they are delivered together.

stellar-vortex-protocol#193 — Dynamic, proportional bond slashing
  slash_solver no longer takes a flat 10% of the bond. compute_slash_amount
  returns min(unfilled_output, bond) / 10, capped at 10% of the bond (never
  more punitive than the old baseline) and floored at 1 stroop (keeps issue
  stellar-vortex-protocol#32's non-zero-slash guarantee). Integer-only, cannot panic, caps at 100%
  of bond when the intent dwarfs it. SECURITY.md "Known Limitations" updated.

stellar-vortex-protocol#191 — Competitive bid window
  Dedicated DataKey::BidWindowEnabled replaces the placeholder that reused
  DstAllowlistEnabled (a real storage-key collision — set_dst_allowlist_enabled
  could silently toggle bidding). New entrypoints: bid_intent (strictly-higher
  quotes only; ties keep the incumbent) and settle_bids (permissionless —
  promotes the winner to Accepted with a fresh fill window, or re-opens the
  intent as Open when no usable bid exists, mirroring expire_intent's
  materialisation pattern). set_bid_window_enabled / is_bid_window_enabled /
  get_best_bid round it out.

stellar-vortex-protocol#188 — Dispute-resolution state machine (Filling -> Disputed -> Resolved)
  Per docs/dispute-resolution-design.md. begin_fill escrows a completing fill
  in the contract and opens DISPUTE_WINDOW; dispute_fill lets the user contest;
  resolve_dispute (arbiter-only, set_arbiter / get_arbiter, defaults to admin)
  rules Upheld (proportional slash, full escrow to user, no fee) or Dismissed
  (fee taken, no slash); release_fill is the permissionless clean-release and
  arbiter-timeout path. Escrow is always custodied by the contract, never the
  solver. New states Filling/Disputed/Resolved and enum DisputeResolution;
  every new error path has a uniquely-discriminated Error variant.

stellar-vortex-protocol#187 — Multi-bond-token support with per-token accounting
  Per docs/60-multi-bond-token-design.md, implemented additively:
  DataKey::SolverBond(solver, token) holds per-token balances; SolverRecord
  keeps bond_amount as the default-token mirror (pre-stellar-vortex-protocol#187 readers and the
  bond-conservation proptest keep working) and gains bond_tokens: Vec<Address>.
  register_solver / withdraw_bond / accept_intent keep their signatures and
  gain *_with_token / *_token siblings. add_allowed_bond_token /
  set_bond_token_min / get_solver_bond(s) manage and expose it. slash_solver
  now slashes — and pays out — in intent.bond_token. deregister_solver refunds
  every token. Cap of MAX_BOND_TOKENS (8) per solver.

Also de-duplicates fill_intent, whose output/fee transfer block had been
written three times by a bad merge; it now transfers once, after state is
committed (CEI).

Tests: 25 new cases in test.rs covering all four features (proportional-slash
disproportion scenarios + floor; no-bid/single-bid/multi-bid + allowlist
collision regression; dispute happy/upheld/dismissed/timeout + escrow custody;
multi-token register/slash/withdraw/deregister/cap/legacy). proptest_bond.rs's
AcceptAndSlash step now asserts the proportional formula.

Docs: README lifecycle diagram + function list, SECURITY.md, CHANGELOG.md
[Unreleased] (storage-layout change noted), and both design docs' status.

NOTE: main (c5da0fd) does not compile — a prior bad merge dropped ~15 DataKey
variants, ~7 Error variants and ~12 constants, and left duplicate Error
discriminants. This change adds zero new compile errors (still 67, all
pre-existing) but CI stays red until main is repaired separately.

closes stellar-vortex-protocol#193
closes stellar-vortex-protocol#191
closes stellar-vortex-protocol#188
closes stellar-vortex-protocol#187
@drips-wave

drips-wave Bot commented Aug 28, 2026

Copy link
Copy Markdown

@iam-mercy 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

Labels

None yet

Projects

None yet

1 participant