Skip to content

fix(swap_reserve): reject reserves that over-commit source balance (#…#307

Open
thomascolden585-svg wants to merge 1 commit intoentrius:testfrom
thomascolden585-svg:fix/295-swap-reserve-concurrent-reservations
Open

fix(swap_reserve): reject reserves that over-commit source balance (#…#307
thomascolden585-svg wants to merge 1 commit intoentrius:testfrom
thomascolden585-svg:fix/295-swap-reserve-concurrent-reservations

Conversation

@thomascolden585-svg
Copy link
Copy Markdown

@thomascolden585-svg thomascolden585-svg commented May 10, 2026

Problem

Closes #295

handle_swap_reserve validated the requestor's source-chain balanceagainst the current reserve request only. The smart contract(smart-contracts/ink/lib.rs::reservations) keys reservations byminer hotkey and exposes no source-address index, so neither sideconsidered the user's other still-live reservations from the samesource address.
A user with funds for a single swap could therefore reserve multipleminers in parallel, locking honest capacity behind reservations thatcould never all settle and starving real liquidity.

Fix

Introduce a validator-side mirror of the on-chain reservations tableand consult it during reserve so the cumulative committed amount per source address is bounded by the user's visible balance.

  • New allways/validator/reservation_index.py — thread-safe (miner_hotkey -> Reservation) mirror with
    committed_amount_for_address(from_address, from_chain, current_block, exclude_miner)
    that filters by source chain (BTC funds don't back a TAO reserve) and ignores expired rows.
  • event_watcher.py — keep the index in sync incrementally:
    • MinerReserved → fetch the full Reservation row via contract_client.get_reservation(miner)andupsert`.
    • ReservationCancelledremove.
    • SwapInitiatedremove (reservation consumed into a swap;
      contract clears it in clear_confirmed_reservation).
    • Bootstrap path hydrates the index from the metagraph hotkeys
      once at startup so the first reserve check is already accurate.
  • axon_handlers.py::handle_swap_reserve — after the per-request balance/collateral gates, additionally reject when
    balance < committed + synapse.from_amount, with the rejection message naming the amount already
    committed across other live reservations.
  • neurons/validator.py — construct ReservationIndex and inject it (plus the contract_client)
    into ContractEventWatcher so the handler and watcher share the same instance.

The handler degrades gracefully when no index is wired (legacy /test paths) — the original per-request check still runs.

Files changed

File Change
allways/validator/reservation_index.py new — index, hydrate, query
allways/validator/event_watcher.py wire index, handle MinerReserved / ReservationCancelled / drop on SwapInitiated, hydrate at bootstrap
allways/validator/axon_handlers.py aggregate-balance gate in handle_swap_reserve
neurons/validator.py construct and inject ReservationIndex
tests/test_reservation_index.py new — 19 unit + integration tests

Test plan

  • uv run pytest tests/test_reservation_index.py — 19 new tests
    covering:
    • committed_amount_for_address: sums across miners for the same(from_addr, from_chain),
      ignores other addresses, other chains,expired rows, and the exclude_miner self-row; returns 0 for
      empty address/chain.
    • remove clears entries; hydrate_from_contract loads only live rows and swallows per-hotkey RPC errors.
    • Watcher event handling: MinerReserved upserts via contract read (and survives a failed read),
      ReservationCancelled and SwapInitiated drop the row.
    • handle_swap_reserve end-to-end: rejects when other live
      reservations would exhaust the balance, accepts when the total fits, ignores commitments from other addresses
      or expired reservations, and falls through cleanly when no index is attached.
  • uv run pytest tests/ — full suite green.
  • uv run ruff check allways/ neurons/ — clean.
  • uv run pre-commit run --all-files — clean.

Risk / rollout

  • New rejection path only fires when the same source address alreadyholds at least one other live reservation,
    so steady- state minersee no behavior change.
  • Index is in-memory and rebuilt at validator startup from contract reads; no migration needed.
  • contract_client and reservation_index are both Optional on ContractEventWatcher, so existing callers/tests
    that don't pass them continue to work.

@xiao-xiao-mao xiao-xiao-mao Bot added the bug Something isn't working label May 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SwapReserve lets one source balance back multiple simultaneous reservations

1 participant