tokens/stop-loss-vault: permissionless Switchboard-triggered stop-loss vault#37
Open
mikemaccana wants to merge 1 commit into
Open
tokens/stop-loss-vault: permissionless Switchboard-triggered stop-loss vault#37mikemaccana wants to merge 1 commit into
mikemaccana wants to merge 1 commit into
Conversation
24f1f6a to
201dc08
Compare
…s vault A per-owner Anchor vault that holds one volatile token and permissionlessly converts it to a stable token (USDC) once a Switchboard On-Demand price feed reports a price at or below an owner-set threshold. An offchain TukTuk crank calls convert_if_triggered on a schedule; the swap routes through Jupiter v6's shared_accounts_route, with a mock-jupiter program standing in for tests. Instructions: initialize_vault, deposit, update_threshold, convert_if_triggered, withdraw_stables, withdraw_volatile. - token_interface + transfer_checked for every token move, so the vault works against the Classic and Token Extensions programs. - Oracle freshness enforced via MAX_PRICE_STALENESS_SLOTS; owner-only mutations via has_one + PDA seeds. - withdraw_volatile escape hatch so a never-triggered vault never locks funds. - mock-jupiter and mock-switchboard test programs with the real external shapes. - 8 Rust + LiteSVM scenarios; README with a finance primer and a program-flow walkthrough (Alice/Bob/Carol/Dave) naming each handler and the accounts it changes. https://claude.ai/code/session_01UXGGFcK3UWRrcv9UoW1gkJ
201dc08 to
c07aadd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
A new
tokens/stop-loss-vault/Anchor example: a per-owner vault that holds a single volatile SPL token (e.g. wSOL) and permissionlessly converts it to a single stable token (e.g. USDC) when a Switchboard On-Demand price feed reports a price at or below an owner-set threshold. The conversion runs from an offchain cranker — typically a TukTuk task — that callsconvert_if_triggeredon a schedule. The instruction reverts cheaply while the price is above the threshold and only swaps once it has actually dropped.Architecture
[b"vault", owner.key().as_ref()]. The vault owns two associated token accounts (volatile + stable) and records the oracle feed pubkey, the threshold price (in the feed's native fixed-point scale), the suggested crank cadence, and the registered TukTuk task pubkey.triggeredflag flipsfalse → trueonce a conversion fires, locking the pre-trigger actions (deposit,update_threshold,withdraw_volatile) so the post-trigger vault is simply a stable-token wallet drained viawithdraw_stables.convert_if_triggeredreads the latest price, and only if it is at or below the stored threshold CPIs the swap aggregator'sshared_accounts_routewith the vault's entire volatile balance; the vault PDA signs the CPI for itself. In production the aggregator is Jupiter v6; in tests amock-jupiterprogram with the same external instruction shape stands in.Instructions
initialize_vault(threshold_price, crank_interval_seconds, tuktuk_task)— owner creates the vault, its two ATAs, and records the threshold + scheduling hint.deposit(amount)— owner moves volatile tokens in. Refused once triggered.update_threshold(new_threshold_price?, new_crank_interval_seconds?)— owner trails the threshold and/or crank cadence. Both optional; refused once triggered.convert_if_triggered(switchboard_price_update_data)— permissionless. Swaps only when the price is at or below the threshold and fresh; otherwise reverts withPriceAboveThresholdorStalePrice.withdraw_stables(amount)— owner pulls stables out after a trigger.withdraw_volatile(amount)— owner pulls volatile tokens back out before a trigger. The escape hatch so a vault whose threshold is never reached doesn't lock the deposit forever.Token handling & safety
transfer_checkedfor every token move, so each CPI carries the mint + decimals and a wrong-mint/decimals account fails instead of silently miscalculating.anchor_spl::token_interface(InterfaceAccount,Interface<TokenInterface>) throughout, so the vault works against both the Classic Token Program and the Token Extensions Program.convert_if_triggeredreadslast_update_slotand rejects any price older thanMAX_PRICE_STALENESS_SLOTS, failing closed if the feed stops updating.has_one = owner+ PDA seeds; the crank path is the only permissionless entry point and it cannot move funds anywhere except through the swap.Why Switchboard On-Demand
On-Demand prices are pulled (not pushed) and verified onchain via Ed25519 signatures, so the price-update bytes travel as an instruction argument and the program trusts them only after verification. That fits a permissionless crank: the cranker pays for the update it wants the program to act on, and the program never trusts the cranker's identity. The teaching example uses a
mock-switchboardprogram exposing the minimum fields the vault reads (price, scale, last-update slot) so tests can drive deterministic price scenarios; production swaps it for theswitchboard-on-demandcrate andPullFeedAccountData::parse_and_verify.Why TukTuk
TukTuk is the maintained replacement for the dead Clockwork for scheduling onchain instruction handlers. The vault doesn't enforce the cadence onchain — it records
crank_interval_secondsas a hint and stores the TukTuk task pubkey for discoverability. Anyone can crank; in normal operation TukTuk runs the schedule and pays for the price update.Tests
8 Rust + LiteSVM scenarios under
programs/stop-loss-vault/tests/:StalePrice).withdraw_volatile.Run:
Scope
programs/stop-loss-vault/— the vault program and its Rust + LiteSVM tests.programs/mock-jupiter/— minimal mock with Jupiter v6'sshared_accounts_routeshape (deterministic price-multiply, checked math), tests only.programs/mock-switchboard/— minimal mock feed (price / scale / last-update slot), tests only.tokens/stop-loss-vault/README.md— architecture, lifecycle, and limitations.Limitations (documented in the README and exercised by tests)
crank_interval_secondsaccordingly.slippage_bps = 0/quoted_out_amount = 0for simplicity.mock-jupiter/mock-switchboardare test stand-ins, marked not-for-production, and thetuktuk_taskregistration is an honestly-labelledTODO(the owner pre-creates the task; the vault just records it).https://claude.ai/code/session_01UXGGFcK3UWRrcv9UoW1gkJ