Skip to content

feat(#197): wire solver_registry tier perks into accept_intent / slash_solver - #313

Open
ushpraise wants to merge 3 commits into
stellar-vortex-protocol:mainfrom
ushpraise:feat/197-solver-registry-tier-perks
Open

feat(#197): wire solver_registry tier perks into accept_intent / slash_solver#313
ushpraise wants to merge 3 commits into
stellar-vortex-protocol:mainfrom
ushpraise:feat/197-solver-registry-tier-perks

Conversation

@ushpraise

@ushpraise ushpraise commented Aug 28, 2026

Copy link
Copy Markdown

Closes #197

Stacked on #311. This branch builds on feat/batch-ops-solver-listing-solana-e2e because #197 needs the compiling baseline from #311 and touches the same accept_intent / slash_solver. Until #311 merges, this PR's diff against main also shows #311's two commits (321174c, b77c5b2); review only 58adfdd. GitHub will trim the diff automatically once #311 lands.

#197 depended on #1/#186 (the solver_registry contract), which did not exist. This PR builds a focused solver_registry — the tier lookup + perk schedule #197 needs — and wires it in. #186's remaining scope (score-gated promotion, staking, reputation-score port, migration) is called out and left open.


New crate: solver_registry/

Mirrors proof_registry/'s structure. Stores an admin-managed tier per solver and exposes exactly what the settlement contract and off-chain solvers need:

fn purpose
initialize(admin) one-shot
set_tier(solver, tier) / clear_tier(solver) admin-only; tier in 0..=4
get_tier(solver) -> u32 the one hot-path method intent_settlement calls; defaults to 0 (Unranked)
get_fill_window_bonus_bps(tier) / get_slash_bps(tier) the perk schedule, public so solvers can price it

Tier table = docs/solver-registry-design.md §3/§6/§7:

Tier Fill-window bonus Slash
0 Unranked +0% 10%
1 Bronze +10% 10%
2 Silver +20% 8%
3 Gold +30% 6%
4 Platinum +50% 5% (floor)

Cargo.lock is committed — a fresh resolve hits the soroban-env-host rand_core / ed25519-dalek conflict.

Still #186 (not in this PR): score-gated automatic promotion (porting compute_reputation_score, record_fill / record_failure), staking, migrate_solver. The read interface is designed to stay stable when those land.

intent_settlement wiring (58adfdd)

  • set_solver_registry(Option<Address>) / get_solver_registry() — admin links an optional registry. Unset is the default, and then every solver is Unranked: accept_intent deadline is now + fill_window and slash_solver takes bond / 10, byte-for-byte the pre-[High] Wire solver-registry tier perks into accept_intent and slash_solver #197 behaviour. All 158 pre-existing tests pass unchanged.
  • accept_intent: deadline = now + fill_window * (10_000 + TIER_FILL_WINDOW_BONUS_BPS[tier]) / 10_000.
  • slash_solver: bond * TIER_SLASH_BPS[tier] / 10_000, floored at MIN_SLASH_BPS (500 = Platinum's 5%). TIER_SLASH_BPS[0] == 1000 so Unranked is exactly bond / 10. Overflow-safe (checked_mul → flat-10% fallback); the .max(1) floor (Add a minimum slash amount floor so tiny bonds round to a zero-value slash #32) is kept.
  • Graceful degradation — the cross-contract call is env.try_invoke_contract (not a generated #[contractclient], to keep the wasm smaller). Registry unset, wrong address, trap, or an undecodable return → tier 0. accept_intent / slash_solver never hard-fail on the optional integration. Covered by a test that points the registry slot at the settlement contract itself.
  • Local perk tables (TIER_FILL_WINDOW_BONUS_BPS / TIER_SLASH_BPS in intent_settlement) rather than fetching get_fill_window_bonus/get_slash_bps per call — a deliberate deviation from design §6, flagged in the design doc: one cross-contract call per hot-path invocation instead of three, and the settlement contract still enforces the agreed schedule if the registry returns a bad value. Both copies carry "keep in sync" comments.

Tier snapshot: accept-time, not live (DoD)

The tier is read in accept_intent and stored on IntentRecord.solver_tier (a u32). slash_solver uses that snapshot. Rationale (in code comments in both functions, per the DoD, matching the deadline-inclusivity comment style): the fill window and the slash rate are both part of the deal struck at accept-time, so a mid-flight promotion can't soften an abandonment and a mid-flight demotion can't harden it. Cleared on every re-open path (partial fill, slash). Both directions are tested.

Out of scope: fee rebates (design §8)

Not implemented. It overlaps with the volume-based fee discount in #7 — the design doc now says the two should be unified in one design rather than built twice.

wasm size

#197 plus the batch/list_solvers work put the optimized intent_settlement wasm at the edge of Soroban's 64 KB limit. This PR:

  • removes the never-functional bid-window scaffolding (BID_WINDOW, BestBidRecord, is_bid_window_enabled, the dead Bidding branch — submit_intent always opened intents Open; IntentState::Bidding is kept as a reserved variant),
  • pins binaryen (version_132) in the wasm-size CI job so the measured size is reproducible,
  • raises the budget to 64 500 (optimized artifact = 63 683, ~1.8 KB under the 65 536 hard limit).

A dedicated size-reduction pass is now blocking further feature work — flagged in the workflow comment and CHANGELOG.

Tests & CI

  • solver_registry: 8 tests — tier roundtrip for every tier, get_tier default, set_tier(0) clears, tier > MAX rejected, admin auth, perk-schedule views vs the design doc (incl. the 5% floor).
  • intent_settlement: 10 new tests using an in-module mock registry (real cross-contract path): fill-window bonus for all five tiers, slash rate for all five tiers, registry-unset fallback, registry-set-but-untiered, registry pointing at a non-registry contract, mid-flight promotion and demotion, partial-fill re-open clearing the snapshot.
  • New CI job solver-registry (fmt / clippy / test / wasm build).

Local run

$ cargo fmt --all -- --check                  # both crates, clean
$ cargo clippy --all-targets -- -D warnings    # both crates, 0 warnings
$ (intent_settlement) cargo test
test result: ok. 168 passed; 0 failed
$ (solver_registry) cargo test
test result: ok. 8 passed; 0 failed
$ PROPTEST_CASES cargo test --features testutils -- bond_conservation
test result: ok. 1 passed; 0 failed
$ cargo build --target wasm32-unknown-unknown --release && wasm-opt -Oz ...
optimized = 63683  budget = 64500  (hard limit 65536)

…vortex-protocol#202, stellar-vortex-protocol#203, stellar-vortex-protocol#204)

The crate did not compile on main: lib.rs referenced ~12 undefined
constants, 9 undefined DataKey variants and 6 undefined Error variants,
the Error enum had duplicate discriminants (22 x3, 23 x2),
validate_src_token called a non-existent String::get, compute_reputation_score
was a pub contract fn taking a non-ABI &SolverRecord, and fill_intent
transferred the fill amount and fee three times each. test.rs had also
been mangled by bad merges (unclosed fns, empty fn bodies, a duplicate
test name, a stale duplicate import, a SolverRecord literal missing a
field). The proptest crate never had its dev-deps written to Cargo.lock.

Constants (stellar-vortex-protocol#202) — declared with rationale comments matching the existing
style; DEFAULT_* alias the historical hard-coded values so initialize()
behaviour is unchanged:
  DEFAULT_MIN_BOND/FILL_WINDOW/INTENT_EXPIRY/PROTOCOL_FEE_BPS  = MIN_BOND / 300 / 1800 / 5
  MAX_PROTOCOL_FEE_BPS = 1000 (10% ceiling, matches set_config doc)
  MIN_FILL_WINDOW_SECS = 60,  MIN_INTENT_EXPIRY_SECS = 300
  MIN_BOND_FLOOR = 10_000_000 (1 USDC)
  SLASH_COOLDOWN = 3600 (matches slash_cooldown_expires_after_time_window)
  CANCEL_COOLDOWN = 60,  MAX_BATCH_SIZE = 20,  MAX_EXTENSION_DURATION = 300

Error enum (stellar-vortex-protocol#203) — removed the collisions, renumbered sequentially to 36,
added AmountTooLarge, InvalidConfig, TimelockNotElapsed,
NoPendingAdminTransfer, NoPendingDstTokenChange, CancelCooldownNotExpired,
BatchTooLarge, ExtensionAlreadyGranted. Replaced the "reuse nearest"
Error::ZeroAmount placeholders in the batch guards and request_extension.

DataKey enum (stellar-vortex-protocol#204) — added Config, PendingAdmin, PendingDstTokenAdd,
PendingDstTokenRemove, AllowedDstTokenList, MinBondMultiplier, UserIntents,
CancelCooldown, ExtensionGranted.

Other baseline fixes:
- validate_src_token: rewritten over a fixed byte buffer via
  String::copy_into_slice; same accepted formats (EVM 0x+40hex, Solana
  base58 32-44).
- fill_intent: collapsed the triplicated transfer/fee blocks into one
  CEI-ordered pass (state committed, then one user transfer + one fee
  transfer).
- compute_reputation_score is now pub(crate) (not a contract entrypoint).
- added get_pending_admin view (PendingAdmin had no reader).
- test.rs: repaired the mangled functions, fixed get_stats() destructuring
  (now a 3-tuple), Vec::get Option handling, and pre-stellar-vortex-protocol#127 tests that used
  "0xabc" placeholder src_tokens; proptest advances past SLASH_COOLDOWN
  between accept+slash steps.

cargo build / clippy --all-targets -D warnings / test: green (145 passed).
…olana e2e

Closes stellar-vortex-protocol#198
Closes stellar-vortex-protocol#199
Closes stellar-vortex-protocol#201

## stellar-vortex-protocol#199 — batch_fill_intent / batch_cancel_intent

- `batch_fill_intent(solver, fills: Vec<(BytesN<32>, i128)>)` and
  `batch_cancel_intent(user, intent_ids: Vec<BytesN<32>>)`, both capped at
  MAX_BATCH_SIZE and reverting the whole batch on any failure (Soroban
  whole-transaction atomicity), matching batch_submit_intent /
  batch_accept_intent.
- Mixed outcomes in one batch_fill are fine: some pairs complete an intent
  (Filled), others only advance it (PartiallyFilled + re-opened);
  OpenIntents / active_intents bookkeeping stays correct across the mix.
- batch_cancel_intent checks and stamps the per-user CANCEL_COOLDOWN once
  for the whole call (via new cancel_intent_core / check_/stamp_ helpers),
  so a user can clear all their open intents in one tx instead of one per
  cooldown window.
- Auth fix: require_auth() is one-shot per address per invocation, so the
  loop bodies now call un-gated `*_inner` functions and each batch
  authorises the actor exactly once. This also fixes the pre-existing
  batch_submit_intent / batch_accept_intent, which failed with
  `Auth, ExistingValue` for any batch of more than one item.
- Dedicated `Error::BatchTooLarge`, checked before any auth/state change.

## stellar-vortex-protocol#198 — Paginated, enumerable solver listing

- `list_solvers(start: u32, limit: u32) -> Vec<Address>`, limit clamped to
  MAX_BATCH_SIZE, backed by a new instance-storage `DataKey::SolverList`
  kept in sync by register_solver (append-if-absent) / deregister_solver
  (remove) — mirrors the add_to/remove_from_dst_token_list pattern (stellar-vortex-protocol#117),
  including the "already present" guard so re-registration never duplicates.
- Storage-cost trade-off documented on `DataKey::SolverList` in the style of
  the OpenIntents comment.
- `indexer/reference-indexer.js`: comment noting list_solvers as the
  on-chain alternative to full solver_registered/solver_deregistered replay.

## stellar-vortex-protocol#201 — Solana as a fully-supported source chain

- validate_src_token already enforces the Solana rules (base58 alphabet
  excluding 0/O/I/l, 32-44 chars, no 0x prefix) → InvalidSrcToken. No
  logic change needed beyond the baseline compile fix; this change is docs
  + tests + removing the "planned" markers.
- docs/132-supported-chains.md: Solana promoted to "Supported"; §3.2
  rewritten with the on-chain validation rules, base58 alphabet, the
  32-byte-pubkey / bs58 verification source, a CLI example, and a
  decimals-vary warning; new §4.8 SPL-mint table (USDC 6, wSOL 9, BONK 5).
  Also noted that avalanche/bsc src_tokens are not yet format-checked.
- docs/solver-integration-guide.md: per-source-chain table for interpreting
  src_token / src_amount, incl. resolving a Solana SPL mint and reading its
  non-uniform decimals.
- README: "planned" removed from the Supported Source Chains table; Solana
  rows added to the decimal-normalization table; MAX_AMOUNT overflow note
  corrected to Error::AmountTooLarge.

## CI

- `[profile.release]` gains `lto = true` (raw wasm 95 KB -> 68 KB).
- wasm-size job now installs binaryen and measures the `wasm-opt -Oz`
  artifact (~61.4 KB) — the size that actually deploys — against a 63 000
  byte budget, under Soroban's 64 KB hard limit. The contract has grown
  close to the limit; a dedicated size-reduction pass is still owed.

## Tests

14 new tests. list_solvers register/deregister/dedupe/pagination; batch
fill mixed full+partial, atomic revert, size guard; batch cancel
one-cooldown, atomic revert, size guard; Solana end-to-end with the
allowlist enabled (EVM + Solana), out-of-range length rejection, 0x-prefix
rejection.

Local: cargo fmt --check, clippy --all-targets -D warnings, cargo test
(158 passed), PROPTEST_CASES=256 bond_conservation (passed), wasm-opt size
61 456 <= 63 000.
…to accept_intent / slash_solver

Closes stellar-vortex-protocol#197

## New crate: solver_registry

Minimal tier registry (solver_registry/): admin-managed tier per solver
(0 Unranked … 4 Platinum), `get_tier(solver) -> u32` (the one method the
settlement contract calls on the hot path), and the perk-schedule views
`get_fill_window_bonus_bps` / `get_slash_bps`. Tier table matches
docs/solver-registry-design.md §3/§6/§7. Score-gated auto-promotion,
staking, reputation-score port and migration are still stellar-vortex-protocol#186 — noted in the
crate docs and the design doc's new "Implementation status" section.
Cargo.lock is committed (a fresh resolve hits the soroban-env-host
rand_core/ed25519 conflict).

## intent_settlement wiring

- `set_solver_registry(Option<Address>)` / `get_solver_registry()` — admin
  links an optional registry. Unset (default) ⇒ every solver is Unranked,
  i.e. behaviour is byte-for-byte the pre-stellar-vortex-protocol#197 flat 10% slash and fixed
  fill window. All 158 pre-existing tests still pass unchanged.
- `accept_intent`: effective fill window =
  `fill_window * (10_000 + TIER_FILL_WINDOW_BONUS_BPS[tier]) / 10_000`
  (+0 / +10 / +20 / +30 / +50 %).
- `slash_solver`: `bond * TIER_SLASH_BPS[tier] / 10_000`, floored at
  `MIN_SLASH_BPS` (Platinum's 5%). `TIER_SLASH_BPS[0]` is 1000, so the
  Unranked path is exactly `bond / 10` as before. Overflow-safe (checked_mul
  with a flat-10% fallback); `.max(1)` floor kept (stellar-vortex-protocol#32).
- Cross-contract call is `env.try_invoke_contract` (not a generated
  `#[contractclient]`) to keep the wasm smaller; any failure — registry
  unset, wrong address, trap, bad return — falls back to tier 0. The
  contract never hard-fails on the optional integration.
- Tier snapshot: read at accept-time, stored on `IntentRecord.solver_tier`
  (a `u32`), consulted by `slash_solver`. A mid-flight promotion can't
  soften an abandonment and a demotion can't harden it — symmetric with the
  fill window, which is likewise fixed at accept-time. Documented in code
  comments in accept_intent/slash_solver (per the DoD) and in the design
  doc. Cleared on every re-open path.
- Local tier tables (not fetched per call) — deliberate deviation from
  design §6, flagged in the design doc: one cross-contract call per
  accept/slash instead of three, and settlement still enforces the schedule
  if the registry misbehaves. The two table copies carry "keep in sync"
  comments.

Out of scope (design §8): fee rebates. Overlaps with stellar-vortex-protocol#7's volume discount;
flagged in the design doc to be unified rather than built twice.

## wasm size

stellar-vortex-protocol#197 plus the earlier batch/list_solvers work pushed the optimized wasm to
the edge of Soroban's 64 KB limit. This commit:
- removes the never-functional bid-window scaffolding (`BID_WINDOW`,
  `BestBidRecord`, `is_bid_window_enabled`, the dead `Bidding` branch —
  `submit_intent` always opened intents `Open` anyway; the enum variant is
  kept as reserved),
- pins binaryen in the wasm-size CI job for a reproducible measurement,
- raises the budget to 64 500 (optimized artifact is 63 683; ~1.8 KB under
  the hard limit).

A dedicated size-reduction pass is now blocking further features — called
out in the workflow comment and CHANGELOG.

## Tests / CI

- solver_registry/src/test.rs: 8 tests — tier roundtrip for all tiers,
  get_tier default, set_tier(0) clears, tier > MAX rejected, admin auth,
  and the perk-schedule views vs the design doc (incl. 5% floor).
- intent_settlement: 10 new tests using an in-module mock registry
  (exercises the real cross-contract path): fill-window bonus for every
  tier, slash rate for every tier, registry-unset fallback, registry-set
  but solver-untiered, registry pointing at a non-registry contract
  (graceful trap), mid-flight promotion and demotion (snapshot), and
  partial-fill re-open clearing the snapshot.
- New CI job `solver-registry` (fmt / clippy / test / wasm build).

Local: fmt --check + clippy --all-targets -D warnings clean (both crates);
cargo test 168 (intent_settlement) + 8 (solver_registry); PROPTEST_CASES
bond_conservation pass; optimized wasm 63 683 <= 64 500.
@drips-wave

drips-wave Bot commented Aug 28, 2026

Copy link
Copy Markdown

@ushpraise 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

Development

Successfully merging this pull request may close these issues.

[High] Wire solver-registry tier perks into accept_intent and slash_solver

1 participant