feat(solana): charge churn fee on remove_quote (close-recreate bypass)#488
feat(solana): charge churn fee on remove_quote (close-recreate bypass)#488LandynDev wants to merge 1 commit into
Conversation
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).
|
Closing as superseded — and thanks, this is a real catch. It's the same We've adopted your mechanism (charge the churn fee on One difference: that branch separates subnet revenue into a dedicated Same fix, same mechanism, consolidated with the treasury refactor + the other review fixes. |
* 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.
Closes a bypass in the #484 anti-flashing quote fee.
The hole
set_quotedetects "first creation" (free) by a zeroed quote PDA. Butremove_quotedoesclose = miner— it deletes the PDA and refunds rent. So:remove_quote→ PDA closed, rent backset_quote→ fresh PDA →miner == default→ fee = 0Net cost to flash-bypass ≈ tx fees + a rent round-trip (a wash). The 0.01 SOL churn fee is fully dodged.
Fix
remove_quotecharges the samequote_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.RemoveQuotegainsvault+system_program(mirrorsSetQuote)QuoteRemovedgainsremove_feeTests
test_remove_fresh_quote_charges_churn_fee: fresh remove → tier-1; stale remove → freetest_remove_quote_closes_and_refunds: warps past the decay window so it stays a pure rent-refund check