Skip to content

feat(solana): charge churn fee on remove_quote (close-recreate bypass)#488

Closed
LandynDev wants to merge 1 commit into
contract-v2from
feat/solana-remove-quote-fee
Closed

feat(solana): charge churn fee on remove_quote (close-recreate bypass)#488
LandynDev wants to merge 1 commit into
contract-v2from
feat/solana-remove-quote-fee

Conversation

@LandynDev

Copy link
Copy Markdown
Collaborator

Closes a bypass in the #484 anti-flashing quote fee.

The hole

set_quote detects "first creation" (free) by a zeroed quote PDA. But remove_quote does close = miner — it deletes the PDA and refunds rent. So:

  1. remove_quote → PDA closed, rent back
  2. set_quote → fresh PDA → miner == defaultfee = 0

Net cost to flash-bypass ≈ tx fees + a rent round-trip (a wash). The 0.01 SOL churn fee is fully dodged.

Fix

remove_quote charges the same quote_update_fee, keyed off how long the quote stood. Disturbing a fresh quote costs the same whether you overwrite it in place or close-then-recreate; a long-standing quote still removes free.

  • RemoveQuote gains vault + system_program (mirrors SetQuote)
  • QuoteRemoved gains remove_fee

Tests

  • New test_remove_fresh_quote_charges_churn_fee: fresh remove → tier-1; stale remove → free
  • test_remove_quote_closes_and_refunds: warps past the decay window so it stays a pure rent-refund check
  • On-chain + integration remove-quote helpers updated for the new accounts

Built against contract-v2 (host cargo check clean). LiteSVM/e2e need the SBF toolchain — not run locally.

The anti-flashing fee detected "first creation" by a zeroed quote PDA, but
remove_quote closes the PDA (rent refunded). A miner could remove then re-set
to land a fresh PDA and dodge the fee — flashing quotes for ~tx-fee cost.

remove_quote now charges the same decaying quote_update_fee, keyed off how long
the quote stood. Update-in-place and close-then-recreate both cost the same to
disturb a fresh quote; a long-standing quote removes free. QuoteRemoved gains
remove_fee; RemoveQuote gains vault + system_program (mirrors SetQuote).
@anderdc

anderdc commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Closing as superseded — and thanks, this is a real catch. It's the same remove_quote + set_quote bypass we flagged in the #484 review.

We've adopted your mechanism (charge the churn fee on remove_quote, keyed on how long the quote stood; first-time creation stays free) into our follow-up PR off contract-v2 (branch solana-review-followups), which also carries the rest of the #484 review fixes. Credit to you for the remove-side approach.

One difference: that branch separates subnet revenue into a dedicated Treasury PDA — collateral is no longer commingled with fees, so nothing accrues to vault.treasury_total anymore and the remove fee routes to the new Treasury. That's also why this PR couldn't merge as-is (it adds vault/treasury_total to remove_quote, which won't exist after the split).

Same fix, same mechanism, consolidated with the treasury refactor + the other review fixes.

@anderdc anderdc closed this Jun 22, 2026
@anderdc anderdc deleted the feat/solana-remove-quote-fee branch June 22, 2026 18:32
LandynDev pushed a commit that referenced this pull request Jun 22, 2026
* fix(solana): PR review — entry over-collateral gate, busy-lock deactivation, admin cancels, shared validation

- consolidate tunables.rs into constants.rs (economic-levers section)
- open_or_request: gate on 1.10x required_collateral at pool entry so an
  under-collateralized miner can't strand a user at vote_initiate (#1)
- vote_deactivate: forbid deactivating a busy miner (busy => active invariant),
  so resolve_pool never arms a reservation on an inactive miner (#3)
- restore admin cancel_pool / cancel_reservation, clearing busy_until (#4)
- admin setters reject contradictory min/max bounds (#6)
- set_quote charges the churn fee on creation too, closing the
  remove_quote + set_quote dodge (#7)
- validate.rs: shared Config-field validators used by initialize + setters so
  the two write paths can't diverge (#8)
- DEFAULT_FULFILLMENT_TIMEOUT_SECS = 14400 (4h) canonical deploy default
- tests: 12 new (entry gate, busy deactivation, cancels, bounds, quote fee);
  LiteSVM 67/67, e2e.sh 24/24

* refactor(solana): separate subnet-revenue Treasury PDA from the collateral Vault

Collateral and subnet revenue no longer share an account. The Vault holds ONLY
miner collateral (trustless — leaves only via the owning miner's withdraw or a
slash to the wronged user); a new Treasury PDA holds ONLY subnet income.

- new Treasury { total, bump } PDA (seeds [b"treasury"]); Vault loses treasury_total
- confirm 1% fee, reservation fee, and quote churn fee all accrue to the Treasury
- timeout slash still pays the user from the Vault (never the treasury)
- withdraw_treasury drains the Treasury PDA (admin-only, caller-chosen recipient)
- split invariants: vault.lamports == rent + total_collateral;
  treasury.lamports == rent + total

Anti-flash fee follows the #488 mechanism (creation free; charge on remove_quote):
- set_quote creation is free again; updates still pay the decaying churn fee
- remove_quote charges the same decaying fee -> Treasury, closing the
  remove+recreate dodge without taxing first-time quotes

Tests updated for the split + new fee semantics. LiteSVM 67/67, e2e.sh 24/24.

* fix(solana): address pre-PR code-review findings

- cancel_reservation: require an active reservation (reserved_until != 0) so it
  can't clear busy_until on a miner whose pool is still open — that could let the
  miner be deactivated mid-contest and resolve_pool match a removed miner against
  a user (fund-safety regression caught in review)
- resolve_pool: restore the inactive-miner backstop (reset pool, never arm a
  reservation or busy lock for an inactive miner)
- vote_initiate: defensive active-miner check before initiating
- remove_quote: document the deliberate "removal can cost the churn fee" stance
- fix stale vault->treasury doc comments (confirm_swap, set_quote, lib, constants)
- tests: cancel_reservation open-pool rejection + treasury lamport conservation

LiteSVM 68/68, e2e.sh 24/24.

* refactor(solana): drop cancel_pool/cancel_reservation; rely on TTL self-expiry

No permanent stuck state exists: resolve_pool is permissionless (always progresses
an open pool) and a reservation's reserved_until is always now + reservation_ttl,
so a miner abandoned in a reservation self-frees at the TTL. The admin cancel ops
were only early-clear accelerators and were the sole paths that cleared busy_until
manually — the exact footgun behind the fund-safety bug. Removing them restores a
clean busy => active invariant without a resolve_pool backstop.

- remove cancel_pool / cancel_reservation instructions, their events + tests
- resolve_pool: no active check (documented invariant); vote_initiate keeps its
  defensive active check as the funds-commit backstop

LiteSVM 65/65, e2e.sh 24/24.
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.

2 participants