From b08507cb344d0a3a6b5aea0ebe93a3fa11a40196 Mon Sep 17 00:00:00 2001 From: GoodnessJohn Date: Sun, 25 Oct 2026 17:26:20 +0100 Subject: [PATCH] docs: fix issue citations, document all contracts, add obligation ABI, extend local testing guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #524, #525, #526, #527 - GOVERNANCE.md: replace wrong #35/#43 citations for set_config timelock with the correct tracking issue #220; also fix the Reference section footer - README.md: add ### subsections for all 9 previously undocumented contracts (solver_registry, proof_registry, reputation_badge, governor, backstop_vault, fee_router, intent_periphery, timelock, treasury_streams) with full public ABI - docs/solver-registry-interface.md: document lock_obligation, release_obligation, get_open_obligations, has_obligation in §2.3 and §3; add obligation-tracking explanation paragraph - docs/local-integration-testing.md: extend build step and Deploy All Contracts script to cover all 10 crates; add Testing the Newer Contracts section with CLI examples for governor, backstop_vault, fee_router, intent_periphery, treasury_streams --- GOVERNANCE.md | 13 +- README.md | 164 ++++++++++++++++++++++ docs/local-integration-testing.md | 217 ++++++++++++++++++++++++++++++ docs/solver-registry-interface.md | 13 +- 4 files changed, 400 insertions(+), 7 deletions(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 6aa7f3a..ef78d15 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -47,9 +47,10 @@ A formal RFC proposal is required for any of the following admin actions: - `propose_add_dst_token(token)` — adds a destination token to the allowlist. - `propose_remove_dst_token(token)` — removes a destination token from the allowlist. - `set_config(...)` — any change to protocol constants (bond multipliers, TTL settings, - timelock duration, etc.). Note: `set_config` does not have an on-chain timelock yet - but will once [#35](https://github.com/stellar-vortex-protocol/vortex-contracts/issues/35) - and [#43](https://github.com/stellar-vortex-protocol/vortex-contracts/issues/43) land. + timelock duration, etc.). Note: `set_config` does not have an on-chain timelock yet; + a `propose_config` / `execute_config` pattern is tracked in + [#220](https://github.com/stellar-vortex-protocol/vortex-contracts/issues/220) + (currently open — no timelocked config path exists in the contract today). A proposal is **not** required for: @@ -248,6 +249,6 @@ allowlist and cannot be a destination. This blocks an entire category of intent. - [**`docs/114-multisig-admin-design.md`**](../docs/114-multisig-admin-design.md) — How admin privileges are structured and how multi-sig can be used. - [**`CONTRIBUTING.md`**](../CONTRIBUTING.md) — On-chain code review and PR process. -- [**Issue #35**](https://github.com/stellar-vortex-protocol/vortex-contracts/issues/35), - [**Issue #43**](https://github.com/stellar-vortex-protocol/vortex-contracts/issues/43) — - Forthcoming timelocked `set_config` and bond-multiplier changes. +- [**Issue #220**](https://github.com/stellar-vortex-protocol/vortex-contracts/issues/220) — + Tracking timelocked governance for `set_config` protocol-parameter changes + (`propose_config` / `execute_config` pattern; not yet implemented). diff --git a/README.md b/README.md index 9aa2d67..cf07305 100644 --- a/README.md +++ b/README.md @@ -344,3 +344,167 @@ never raised anywhere in `intent_settlement/src/lib.rs`. | 28 | `InvalidSrcToken` | 28 | `batch_cancel_intent`, `batch_fill_intent`, `batch_submit_intent`, `fill_intent`, `get_solver_intents`, `submit_intent` | #127: `submit_intent` was called with a `src_token` whose format does not match the conventions of the declared `src_chain`. EVM chains (ethereum, base, polygon, arbitrum, optimism): expect a `0x`-prefixed 42-character hex string (e.g. `"0xA0b86991…"`). Solana: expects a base58 string between 32 and 44 characters long with no `0x` prefix. If `src_chain` is unknown this error is never raised — unknown chains bypass token-format validation so the allowlist remains the sole gate. | | 29 | `ProofRegistryNotSet` | 30 | `batch_cancel_intent`, `batch_fill_intent`, `fill_intent`, `get_solver_intents` | docs/129 §2.4 (code 24). `fill_intent` was called with `require_proof = true` but no `DataKey::ProofRegistry` has been set by the admin. Configuration error — no slash, intent stays `Accepted`. | | 30 | `ProofNotFound` | 31 | `batch_cancel_intent`, `batch_fill_intent`, `fill_intent`, `get_solver_intents` | docs/129 §2.3 (code 25). `ProofRegistry.get_proof(intent_id)` returned `None`: no verified source-chain deposit for this intent yet. Intent stays `Accepted`; the solver should retry once the VAA is relayed, or be sl + + + + +--- + +### `solver_registry` + +Off-chain solver reputation and bonding contract (`solver_registry/src/lib.rs`). +Full ABI documented in [`docs/solver-registry-interface.md`](./docs/solver-registry-interface.md). + +- `initialize(admin, bond_token, fee_recipient)` — one-time setup; seeds the default tier table +- `register_solver(solver, bond_amount)` / `stake(solver, amount)` / `unstake(solver, amount)` / `deregister_solver(solver)` — solver bond lifecycle +- `record_fill(caller, solver, intent_id, amount)` — writer-only: increment fill count and cumulative volume +- `record_failure(caller, solver, intent_id)` — writer-only: increment failure count +- `slash(caller, solver, intent_id)` — writer-only: slash bond by tier-specific `slash_bps`, transfer to fee recipient +- `lock_obligation(caller, solver, intent_id)` — writer-only: mark an intent as open for `solver`; returns updated open-obligation count +- `release_obligation(caller, solver, intent_id)` — writer-only: clear the obligation once settled or slashed; returns updated count +- `get_open_obligations(solver)` — read-only: number of currently locked obligations for a solver +- `has_obligation(solver, intent_id)` — read-only: `true` if the given intent is still an open obligation for `solver` +- `get_tier(solver)` / `tier_for(score_bps, bond_amount)` / `get_tier_table()` — tier lookups +- `get_fill_window_bonus_pct(tier)` / `get_slash_bps(tier)` / `get_fee_rebate_bps(tier)` — per-tier perk values +- `get_reputation_score(solver)` / `compute_reputation_score(record)` — reputation score (0–10 000 bps) +- `get_solver(solver)` / `get_solver_count()` — solver record reads +- `propose_writer(new_writer)` / `execute_writer(new_writer)` / `cancel_writer()` / `get_pending_writer()` — timelocked writer rotation (48 h) +- `propose_admin(new_admin)` / `accept_admin(new_admin)` / `cancel_admin_transfer()` / `get_pending_admin()` — timelocked admin handover +- `set_tier_threshold(tier, min_bond, min_score_bps)` — admin-tunable tier gates (tiers 1–4 only) +- `is_intent_recorded(action, intent_id)` — idempotency guard for write-path calls +- `get_writer()` / `get_admin()` / `get_bond_token()` — configuration reads + +--- + +### `proof_registry` + +Cross-chain deposit proof store (`proof_registry/src/lib.rs`). Accepts Wormhole VAAs +and Axelar GMP messages, verifies emitter authorization, and persists a `ProofRecord` +keyed by `intent_id` for `intent_settlement` to query via `fill_intent`. + +- `initialize(admin, configurator, wormhole_core, axelar_gateway)` — one-time setup +- `receive_message(vaa)` — ingest a Wormhole VAA; verifies emitter, decodes the intent deposit payload, stores proof +- `receive_message_axelar(source_chain, source_address, payload)` — ingest an Axelar GMP message; same decode/store path +- `set_authorized_emitter(chain_id, emitter)` / `remove_authorized_emitter(chain_id)` / `get_authorized_emitter(chain_id)` — Wormhole emitter allowlist +- `set_authorized_axelar_source(chain_name, source_address)` / `remove_authorized_axelar_source(chain_name)` / `get_authorized_axelar_source(chain_name)` — Axelar source allowlist +- `get_wormhole_core()` / `get_axelar_gateway()` — configured bridge addresses +- `get_proof(intent_id)` / `has_proof(intent_id)` / `get_fresh_proof(intent_id)` — read the stored proof (panics on missing for `get_fresh_proof`) +- `mock_set_proof(record)` / `mock_remove_proof(intent_id)` — test-only helpers (compile-gated) + +--- + +### `reputation_badge` + +Non-transferable (soulbound) on-chain tier badge for solvers (`reputation_badge/src/lib.rs`). +Design rationale in [`docs/242-reputation-tier-badge-design.md`](./docs/242-reputation-tier-badge-design.md) (#242). + +- `initialize(admin, solver_registry)` — one-time setup; `solver_registry` is optional and can be wired later +- `set_solver_registry(solver_registry)` / `get_solver_registry()` — update the registry address permitted to drive badge changes +- `mint_badge(caller, solver, tier)` — mint or overwrite `solver`'s badge to `tier` (Bronze–Platinum); caller must be admin or registered `solver_registry` +- `burn_badge(caller, solver)` — remove badge when a solver drops to Unranked; caller must be admin or registered `solver_registry` +- `get_badge(solver)` — `Option`: `None` means Unranked + +--- + +### `governor` + +On-chain governance contract for protocol parameter changes (`governor/src/lib.rs`). +Voting power is derived from a solver's staked bond in `solver_registry`. + +- `initialize(admin, solver_registry, config)` — deploy-time setup +- `update_config(config)` / `set_config(admin, config)` / `get_config()` — manage governance parameters (quorum, voting period, timelock delay) +- `propose(caller, description, calls)` — open a governance proposal; returns `proposal_id` +- `cancel(caller, proposal_id)` — proposer or admin can cancel before execution +- `cast_vote(voter, proposal_id, vote_type, reason)` — record a For / Against / Abstain vote +- `queue(proposal_id)` — move a passed proposal into the execution queue (starts the timelock) +- `execute(proposal_id)` — execute a queued proposal once the timelock has elapsed +- `state(proposal_id)` — `ProposalState`: Pending, Active, Succeeded, Defeated, Queued, Executed, Cancelled +- `proposal(proposal_id)` / `list_proposals(start, limit)` / `get_proposal_count()` — proposal reads +- `voting_power(account, ledger)` — historical voting power at a given ledger +- `sync_power(account)` — write a checkpoint for `account`'s current bond weight + +--- + +### `backstop_vault` + +LP-share vault that holds surplus collateral backing solver slashes (`backstop_vault/src/lib.rs`). + +- `initialize(admin, bond_token, settlement, min_deposit, withdrawal_cooldown_secs)` — one-time setup +- `deposit(lp, amount)` — deposit bond token, receive proportional shares; returns shares minted +- `withdraw(lp, shares)` — queue a withdrawal (enforces `withdrawal_cooldown_secs`); returns queued bond token amount +- `process_queued_withdrawal(lp)` — permissionless: release a matured withdrawal to `lp` +- `cover(caller, amount, recipient)` — settlement-only: transfer bond token to `recipient` as slash coverage, reduce vault assets +- `receive_income(amount)` — pull income into the vault (increases share value for all LPs) +- `rescue_tokens(token, amount, recipient)` — admin-only: recover non-bond tokens sent to the vault +- `set_emergency_mode(enabled)` — admin-only: disable new deposits and queued withdrawals +- `set_config(min_deposit, withdrawal_cooldown_secs)` / `get_config()` — tunable vault parameters +- `get_vault_state()` — `(total_assets, total_shares, total_queued)` triple +- `get_lp_position(lp)` — `(shares, queued_amount, queued_at)` for a given LP +- `get_withdrawal_cooldown(lp)` — timestamp after which `process_queued_withdrawal` will succeed + +--- + +### `fee_router` + +Distributes protocol fee receipts to a configurable set of weighted sinks (`fee_router/src/lib.rs`). + +- `initialize(governance, sinks, weight_delay)` — one-time setup; `sinks` is a list of `(address, weight_bps)` pairs +- `on_fee_received(token, amount)` — called by `intent_settlement` on each fill; records fee accrual without an immediate transfer +- `distribute(token)` — permissionless: push accrued fees to each sink proportional to their weight +- `claim(sink, token)` — pull accrued share for `sink` (used when `distribute` reverts for a single sink due to fee-on-transfer tokens) +- `propose_weights(caller, sinks)` / `apply_weights(caller)` / `pending_executable_at()` — timelocked weight rotation; `apply_weights` is callable only after `weight_delay` has elapsed +- `governance()` / `weight_delay()` / `sinks()` — configuration reads +- `balance_of(token)` — vault balance of a given token held by the router +- `claimable(sink, token)` — unclaimed accrued balance for a given sink and token + +--- + +### `intent_periphery` + +Read-aggregator proxy over `intent_settlement` (`intent_periphery/src/lib.rs`). Provides +batched view calls for off-chain clients and frontends without duplicating settlement logic. + +- `initialize(admin, settlement)` — one-time setup; records the settlement contract address +- `propose_settlement(settlement)` / `accept_settlement()` — timelocked settlement pointer rotation +- `get_settlement()` / `get_pending_settlement()` / `get_interface_version()` — configuration reads +- `get_stats(solver)` / `get_solver(solver)` / `get_intent(intent_id)` / `get_fee_schedule(solver)` / `get_protocol_health()` — individual forwarded views +- `list_solvers(start, limit)` / `list_open_intents(start, limit)` / `list_intents_by_user(user, start, limit)` — paginated list views forwarded to settlement +- `get_dashboard_snapshot()` — single call that returns a map with stats, health, and top-of-book data +- `batch_get_stats_and_health()` / `batch_list_solvers_and_intents(start, limit)` / `batch_get_solver_and_fee_schedule(solver)` / `batch_get_intent_and_health(intent_id)` / `batch_two_views(view_a, view_b)` — multi-read batches to reduce round-trips + +--- + +### `timelock` + +General-purpose timelocked executor (`timelock/src/lib.rs`). Used as the execution +backend for governance proposals produced by `governor`. + +- `__constructor(min_delay, grace_period, proposers, executors, admin)` — deploy-time setup; seeds role tables +- `grant_role(role, account)` / `revoke_role(role, account)` / `has_role(role, account)` — role management (roles: `proposer`, `executor`, `canceller`, `admin`) +- `get_min_delay()` / `update_delay(new_delay)` / `get_grace_period()` — delay configuration +- `schedule(id, calls, predecessor, salt, delay)` — queue a single operation; `delay ≥ min_delay` +- `schedule_batch(calls, predecessor, salt, delay)` — queue multiple calls under one operation id +- `cancel(id)` — canceller-only: remove a pending operation +- `execute(id)` — executor-only: run a ready operation; fails if `min_delay` has not elapsed or grace period has expired +- `get_operation_state(id)` — `OperationState`: Unknown, Waiting, Ready, Done +- `is_operation_done(id)` — convenience boolean check +- `hash_operation(calls, predecessor, salt)` — pure: compute the operation id + +--- + +### `treasury_streams` + +Token vesting / streaming contract for protocol-controlled treasury disbursements +(`treasury_streams/src/lib.rs`). Governed by `governor` / `timelock`. + +- `initialize(governor)` — one-time setup; records the governing address +- `propose_governor_transfer(new_governor)` / `accept_governor_transfer()` — timelocked governor handover +- `pause()` / `unpause()` / `is_paused()` — governor-only circuit breaker +- `create_stream(recipient, token, total_amount, start, end, cliffed)` — create a linear vesting stream; returns `stream_id` +- `withdraw(stream_id)` — recipient-only: pull all vested-but-unclaimed tokens; returns amount transferred +- `cancel(stream_id)` — governor-only: cancel a stream and return unvested tokens to the treasury +- `top_up(stream_id, additional_amount)` — governor-only: add tokens to an active stream (extends total without changing the rate) +- `extend(stream_id, new_end)` — governor-only: push the stream end date out +- `balance_of(stream_id)` — currently vested and unclaimed balance +- `get_stream(stream_id)` / `get_stream_count()` — stream reads +- `list_streams_by_sender(sender, start, limit)` / `list_streams_by_recipient(recipient, start, limit)` — paginated index lookups diff --git a/docs/local-integration-testing.md b/docs/local-integration-testing.md index 6e15ff9..f2b287b 100644 --- a/docs/local-integration-testing.md +++ b/docs/local-integration-testing.md @@ -53,6 +53,12 @@ cd intent_settlement && cargo build --target wasm32-unknown-unknown --release cd ../solver_registry && cargo build --target wasm32-unknown-unknown --release cd ../proof_registry && cargo build --target wasm32-unknown-unknown --release cd ../reputation_badge && cargo build --target wasm32-unknown-unknown --release +cd ../governor && cargo build --target wasm32-unknown-unknown --release +cd ../backstop_vault && cargo build --target wasm32-unknown-unknown --release +cd ../fee_router && cargo build --target wasm32-unknown-unknown --release +cd ../intent_periphery && cargo build --target wasm32-unknown-unknown --release +cd ../timelock && cargo build --target wasm32-unknown-unknown --release +cd ../treasury_streams && cargo build --target wasm32-unknown-unknown --release ``` ### 3. Deploy and Test @@ -168,6 +174,132 @@ stellar contract invoke \ -- initialize --admin test-account --fee_recipient test-account echo "Intent Settlement Contract ID: $INTENT_ID" + +# Deploy solver_registry +SOLVER_REG_ID=$(stellar contract deploy \ + --wasm solver_registry/target/wasm32-unknown-unknown/release/vortex_solver_registry.wasm \ + --source test-account \ + --network $STELLAR_NETWORK) + +stellar contract invoke \ + --id $SOLVER_REG_ID \ + --source test-account \ + --network $STELLAR_NETWORK \ + -- initialize --admin test-account --bond_token --fee_recipient test-account + +echo "Solver Registry Contract ID: $SOLVER_REG_ID" + +# Deploy proof_registry +PROOF_REG_ID=$(stellar contract deploy \ + --wasm proof_registry/target/wasm32-unknown-unknown/release/vortex_proof_registry.wasm \ + --source test-account \ + --network $STELLAR_NETWORK) + +stellar contract invoke \ + --id $PROOF_REG_ID \ + --source test-account \ + --network $STELLAR_NETWORK \ + -- initialize --admin test-account --configurator test-account \ + --wormhole_core --axelar_gateway + +echo "Proof Registry Contract ID: $PROOF_REG_ID" + +# Deploy reputation_badge +BADGE_ID=$(stellar contract deploy \ + --wasm reputation_badge/target/wasm32-unknown-unknown/release/vortex_reputation_badge.wasm \ + --source test-account \ + --network $STELLAR_NETWORK) + +stellar contract invoke \ + --id $BADGE_ID \ + --source test-account \ + --network $STELLAR_NETWORK \ + -- initialize --admin test-account --solver_registry $SOLVER_REG_ID + +echo "Reputation Badge Contract ID: $BADGE_ID" + +# Deploy governor +GOVERNOR_ID=$(stellar contract deploy \ + --wasm governor/target/wasm32-unknown-unknown/release/vortex_governor.wasm \ + --source test-account \ + --network $STELLAR_NETWORK) + +stellar contract invoke \ + --id $GOVERNOR_ID \ + --source test-account \ + --network $STELLAR_NETWORK \ + -- initialize --admin test-account --solver_registry $SOLVER_REG_ID \ + --config '{"quorum_bps":1000,"voting_period_ledgers":17280,"timelock_delay":172800}' + +echo "Governor Contract ID: $GOVERNOR_ID" + +# Deploy backstop_vault +BACKSTOP_ID=$(stellar contract deploy \ + --wasm backstop_vault/target/wasm32-unknown-unknown/release/vortex_backstop_vault.wasm \ + --source test-account \ + --network $STELLAR_NETWORK) + +stellar contract invoke \ + --id $BACKSTOP_ID \ + --source test-account \ + --network $STELLAR_NETWORK \ + -- initialize --admin test-account --bond_token \ + --settlement $INTENT_ID --min_deposit 500000000 --withdrawal_cooldown_secs 86400 + +echo "Backstop Vault Contract ID: $BACKSTOP_ID" + +# Deploy fee_router +FEE_ROUTER_ID=$(stellar contract deploy \ + --wasm fee_router/target/wasm32-unknown-unknown/release/vortex_fee_router.wasm \ + --source test-account \ + --network $STELLAR_NETWORK) + +stellar contract invoke \ + --id $FEE_ROUTER_ID \ + --source test-account \ + --network $STELLAR_NETWORK \ + -- initialize --governance $GOVERNOR_ID \ + --sinks '[{"address":"test-account","weight_bps":10000}]' \ + --weight_delay 172800 + +echo "Fee Router Contract ID: $FEE_ROUTER_ID" + +# Deploy intent_periphery +PERIPHERY_ID=$(stellar contract deploy \ + --wasm intent_periphery/target/wasm32-unknown-unknown/release/vortex_intent_periphery.wasm \ + --source test-account \ + --network $STELLAR_NETWORK) + +stellar contract invoke \ + --id $PERIPHERY_ID \ + --source test-account \ + --network $STELLAR_NETWORK \ + -- initialize --admin test-account --settlement $INTENT_ID + +echo "Intent Periphery Contract ID: $PERIPHERY_ID" + +# Deploy timelock +TIMELOCK_ID=$(stellar contract deploy \ + --wasm timelock/target/wasm32-unknown-unknown/release/vortex_timelock.wasm \ + --source test-account \ + --network $STELLAR_NETWORK) + +# timelock uses __constructor; pass args at deploy time via --args +echo "Timelock Contract ID: $TIMELOCK_ID" + +# Deploy treasury_streams +TREASURY_ID=$(stellar contract deploy \ + --wasm treasury_streams/target/wasm32-unknown-unknown/release/vortex_treasury_streams.wasm \ + --source test-account \ + --network $STELLAR_NETWORK) + +stellar contract invoke \ + --id $TREASURY_ID \ + --source test-account \ + --network $STELLAR_NETWORK \ + -- initialize --governor $GOVERNOR_ID + +echo "Treasury Streams Contract ID: $TREASURY_ID" ``` ### Submit an Intent @@ -222,6 +354,91 @@ Ensure the WASM binary was built in release mode: cd intent_settlement && cargo build --target wasm32-unknown-unknown --release ``` +## Testing the Newer Contracts + +### `governor` + `timelock` + +The governance flow requires a live `solver_registry` (for voting power) and a deployed +`timelock`. A minimal end-to-end test on the local network: + +```bash +# 1. Register a solver so the account has voting power +stellar contract invoke --id $SOLVER_REG_ID --source test-account --network $STELLAR_NETWORK \ + -- register_solver --solver test-account --bond_amount 500000000 + +# 2. Sync voting power checkpoint +stellar contract invoke --id $GOVERNOR_ID --source test-account --network $STELLAR_NETWORK \ + -- sync_power --account test-account + +# 3. Create a proposal (description + target calls encoded as JSON) +stellar contract invoke --id $GOVERNOR_ID --source test-account --network $STELLAR_NETWORK \ + -- propose --caller test-account --description '"test proposal"' --calls '[]' + +# 4. Cast a vote (For = 0) +stellar contract invoke --id $GOVERNOR_ID --source test-account --network $STELLAR_NETWORK \ + -- cast_vote --voter test-account --proposal_id 0 --vote_type 0 --reason '"lgtm"' +``` + +### `backstop_vault` + +```bash +# Deposit bond token as an LP +stellar contract invoke --id $BACKSTOP_ID --source test-account --network $STELLAR_NETWORK \ + -- deposit --lp test-account --amount 500000000 + +# Check LP position (shares, queued_amount, queued_at) +stellar contract invoke --id $BACKSTOP_ID --source test-account --network $STELLAR_NETWORK \ + -- get_lp_position --lp test-account + +# Queue a withdrawal +stellar contract invoke --id $BACKSTOP_ID --source test-account --network $STELLAR_NETWORK \ + -- withdraw --lp test-account --shares 500000000 +``` + +### `fee_router` + +```bash +# Trigger a fee distribution for the bond token +stellar contract invoke --id $FEE_ROUTER_ID --source test-account --network $STELLAR_NETWORK \ + -- distribute --token + +# Check claimable balance for a sink +stellar contract invoke --id $FEE_ROUTER_ID --source test-account --network $STELLAR_NETWORK \ + -- claimable --sink test-account --token +``` + +### `intent_periphery` + +All calls are read-only proxies; no auth required. Wire it to the deployed settlement first: + +```bash +# Verify the periphery points at the right settlement contract +stellar contract invoke --id $PERIPHERY_ID --source test-account --network $STELLAR_NETWORK \ + -- get_settlement + +# Fetch a dashboard snapshot (stats + health in one round-trip) +stellar contract invoke --id $PERIPHERY_ID --source test-account --network $STELLAR_NETWORK \ + -- get_dashboard_snapshot +``` + +### `treasury_streams` + +```bash +# Create a 30-day linear stream for a contributor (no cliff) +stellar contract invoke --id $TREASURY_ID --source test-account --network $STELLAR_NETWORK \ + -- create_stream \ + --recipient \ + --token \ + --total_amount 10000000000 \ + --start $(date +%s) \ + --end $(($(date +%s) + 2592000)) \ + --cliffed false + +# Recipient withdraws vested tokens +stellar contract invoke --id $TREASURY_ID --source test-account --network $STELLAR_NETWORK \ + -- withdraw --stream_id 0 +``` + ## Next Steps - See [Solver Integration Guide](./solver-integration-guide.md) for building a solver bot diff --git a/docs/solver-registry-interface.md b/docs/solver-registry-interface.md index ef1aea9..25c559a 100644 --- a/docs/solver-registry-interface.md +++ b/docs/solver-registry-interface.md @@ -73,8 +73,16 @@ and currently returns 0 for every tier. | `record_fill(caller, solver, intent_id, amount)` | — | `fills_completed += 1`, `total_volume += amount`. | | `record_failure(caller, solver, intent_id)` | — | `fills_failed += 1` (no bond movement). | | `slash(caller, solver, intent_id)` | `(slash_amount: i128, new_tier: u32)` | Takes `bond * slash_bps(tier) / 10_000` (min 1), transfers it to the fee recipient, `fills_failed += 1`. | +| `lock_obligation(caller, solver, intent_id)` | `u32` (updated open count) | Marks `intent_id` as an open obligation for `solver`. Idempotent: a second call for the same `(solver, intent_id)` pair is a no-op and returns the unchanged count. `solver` must be registered (`SolverNotRegistered` otherwise). Emits `obligation_locked(solver, (intent_id, count))`. | +| `release_obligation(caller, solver, intent_id)` | `u32` (updated open count) | Clears the open obligation for `(solver, intent_id)`. Idempotent: if no obligation exists the count is returned unchanged. Emits `obligation_released(solver, (intent_id, count))`. | -Each write is **exactly once per `intent_id`** (#390): a second `record_fill`, +**Obligation tracking** is the mechanism by which `intent_settlement` prevents a solver +from deregistering or unstaking while they hold accepted intents. `lock_obligation` is +called by `intent_settlement` when a solver calls `accept_intent`; `release_obligation` +is called on `fill_intent`, `slash_solver`, or `expire_intent`. The open-obligation count +can be read at any time via the view functions below (§3). + +Each **scoring** write (`record_fill`, `record_failure`, `slash`) is **exactly once per `intent_id`** (#390): a second `record_fill`, `record_failure` or `slash` for the same intent fails with `AlreadyRecorded`, whatever the solver. Keys are per action, so a `record_failure` and a `slash` for the same intent are independent. A write that reverts (e.g. @@ -109,6 +117,9 @@ implicit-signer ambiguity. | `get_solver(solver)` | `Option`. | | `get_solver_count()` | `u32`. | | `get_writer()` / `get_admin()` / `get_bond_token()` | `Option
`. | +| `get_open_obligations(solver)` | `u32` — number of currently locked (unresolved) intent obligations for `solver`. Used by `intent_settlement` to block `deregister_solver` / `unstake` while active intents are held. | +| `has_obligation(solver, intent_id)` | `bool` — `true` if `lock_obligation` has been called for this `(solver, intent_id)` pair and `release_obligation` has not yet cleared it. | +| `is_intent_recorded(action, intent_id)` | `bool` — idempotency check; `action` is `fill`, `failure`, or `slash`. | ### `SolverRecord`