From 380021ef9ada4b5a0f12db88761af772435b989c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 22:43:17 +0000 Subject: [PATCH 1/2] vault-strategy: rename WhitelistEntry to ApprovedAsset Name the account after what it is, one curator-approved asset bound to its official price feed, rather than after the list mechanic. whitelist_asset becomes approve_asset, the PDA seed "whitelist" becomes "approved_asset", and the unused AssetNotWhitelisted error goes away (approval is checked by the ApprovedAsset account's existence, so no error path ever fired). Also correct the Registry's description in doc comments, README, and CHANGELOG: the Registry account is the curator record at the root of the approved set, not the list itself. It stores only the authority; the set is the collection of ApprovedAsset accounts derived from it. Fix two stale MAX_ASSETS (8) references the cap raise missed. 20 tests pass under cargo build-sbf + cargo test; fmt and clippy clean. --- finance/vault-strategy/anchor/CHANGELOG.md | 4 +- finance/vault-strategy/anchor/README.md | 16 ++-- .../programs/vault-strategy/src/error.rs | 2 - .../src/instructions/add_asset.rs | 12 +-- .../{whitelist_asset.rs => approve_asset.rs} | 18 ++--- .../src/instructions/initialize_strategy.rs | 2 +- .../vault-strategy/src/instructions/mod.rs | 4 +- .../anchor/programs/vault-strategy/src/lib.rs | 11 +-- .../vault-strategy/src/state/registry.rs | 25 +++--- .../vault-strategy/src/state/strategy.rs | 4 +- .../vault-strategy/tests/vault_strategy.rs | 76 ++++++++++--------- 11 files changed, 92 insertions(+), 82 deletions(-) rename finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/{whitelist_asset.rs => approve_asset.rs} (57%) diff --git a/finance/vault-strategy/anchor/CHANGELOG.md b/finance/vault-strategy/anchor/CHANGELOG.md index 4ec33328..1565a379 100644 --- a/finance/vault-strategy/anchor/CHANGELOG.md +++ b/finance/vault-strategy/anchor/CHANGELOG.md @@ -4,8 +4,8 @@ ### Added -- **Curated asset registry.** A `Registry` plus per-mint `WhitelistEntry` accounts, maintained by a protocol authority separate from strategy managers. Each entry binds an approved mint to its official Pyth price feed. New instructions: `initialize_registry`, `whitelist_asset`. -- **Dynamic assets.** A strategy now grows its portfolio with `add_asset`, which registers a whitelisted mint at the next index as an `AssetConfig` PDA (`["asset", strategy, index]`) and creates its vault. Assets occupy the contiguous range `0..asset_count`, up to `MAX_ASSETS` (8). Replaces the previous fixed two-asset layout. +- **Curated asset registry.** A `Registry` account naming a curator authority separate from strategy managers, plus one `ApprovedAsset` account per approved mint (`["approved_asset", registry, mint]`), each binding the mint to its official Pyth price feed. Approval is checked by the account's existence. New instructions: `initialize_registry`, `approve_asset`. +- **Dynamic assets.** A strategy now grows its portfolio with `add_asset`, which registers an approved mint at the next index as an `AssetConfig` PDA (`["asset", strategy, index]`) and creates its vault. Assets occupy the contiguous range `0..asset_count`, up to `MAX_ASSETS` (16). Replaces the previous fixed two-asset layout. - **Oracle-bounded slippage.** `deposit` and `rebalance` compute each swap's minimum output from the Pyth price and a strategy-level `max_slippage_bps` (capped at `MAX_SLIPPAGE_BPS` = 10%), instead of trusting a caller-supplied minimum. Set at creation via `initialize_strategy`. - **Full-allocation invariant with immediate deployment.** A strategy accepts deposits only once its weights sum to exactly 10,000 bps (`deposit` reverts with `StrategyNotFullyAllocated` otherwise). `deposit` then swaps each depositor's USDC into the basket at its target weights through the registered router in the same transaction, so every deposit is fully invested (bar sub-cent rounding dust) and the USDC vault holds no idle cash. - **Retirable assets.** `set_weight(weight_bps)` changes an asset's target weight after creation, including setting it to zero to retire it (reassign that weight to another asset to reach 100% and reopen deposits; `rebalance` liquidates the retired holdings). The asset's index is preserved, so the `0..asset_count` range the valuation handlers depend on stays contiguous. diff --git a/finance/vault-strategy/anchor/README.md b/finance/vault-strategy/anchor/README.md index 512c1705..f0c43d3f 100644 --- a/finance/vault-strategy/anchor/README.md +++ b/finance/vault-strategy/anchor/README.md @@ -1,6 +1,6 @@ # Solana Vault Strategy (Anchor) -A manager-run investment vault on Solana. Users deposit [USDC](https://www.investopedia.com/terms/u/usd-coin-usdc.asp) and receive shares representing proportional ownership of a portfolio of assets. The manager adds assets from a curated whitelist and sets their target weights; each deposit is deployed across those assets at its weights in the same transaction. The manager rebalances as prices drift, earns a fee, and depositors withdraw their proportional slice in kind when they choose. +A manager-run investment vault on Solana. Users deposit [USDC](https://www.investopedia.com/terms/u/usd-coin-usdc.asp) and receive shares representing proportional ownership of a portfolio of assets. The manager adds assets a curator has approved and sets their target weights; each deposit is deployed across those assets at its weights in the same transaction. The manager rebalances as prices drift, earns a fee, and depositors withdraw their proportional slice in kind when they choose. The example uses two stocks as the portfolio assets: **TSLAx** (Tesla) and **NVDAx** (NVIDIA) - [xStocks](https://backed.fi/xstocks) issued on Solana by Backed Finance. In tests these are mock [tokens](https://solana.com/docs/terminology#token). @@ -10,7 +10,7 @@ A note on the word **vault**: by the common standard (ERC-4626) a vault holds a ## Programs -- **`vault-strategy`**: Registry/whitelist, strategy creation, asset registration, deposits, share minting, fee accrual, rebalancing, withdrawals +- **`vault-strategy`**: Registry and approved assets, strategy creation, asset registration, deposits, share minting, fee accrual, rebalancing, withdrawals - **`mock-swap-router`**: Test-only fake Jupiter. Stores exchange rates, mints/burns basket tokens for USDC. Replaced by real [Jupiter](https://jup.ag) in production. --- @@ -72,9 +72,9 @@ An [in-kind distribution](https://www.investopedia.com/terms/i/in-kind.asp) retu `Maria` and `Victor` are stored as plain `Pubkey`s and may each be a [Squads](https://squads.so/) multisig; the program only checks the signature. -### Victor creates the registry and whitelists assets +### Victor creates the registry and approves assets -`initialize_registry()` creates a `Registry` PDA (`["registry", victor]`) owned by Victor. `whitelist_asset(price_feed)` then creates one `WhitelistEntry` PDA (`["whitelist", registry, mint]`) per approved mint, binding it to its official Pyth feed. Only Victor can do this. This separation is the anti-fraud core: a manager can only ever add assets Victor approved, and the feed comes from the registry, so a manager cannot list a token they mint themselves or pair a real mint with a feed they control. +`initialize_registry()` creates a `Registry` PDA (`["registry", victor]`) owned by Victor. The registry holds no list; it only names the curator. The approved set is the collection of `ApprovedAsset` accounts under it: `approve_asset(price_feed)` creates one `ApprovedAsset` PDA (`["approved_asset", registry, mint]`) per approved mint, binding it to its official Pyth feed, and an asset counts as approved exactly when that account exists. Only Victor can create them. This separation is the anti-fraud core: a manager can only ever add assets Victor approved, and the feed comes from the registry, so a manager cannot list a token they mint themselves or pair a real mint with a feed they control. ### Maria initializes the strategy @@ -82,7 +82,7 @@ An [in-kind distribution](https://www.investopedia.com/terms/i/in-kind.asp) retu ### Maria adds assets -`add_asset(weight_bps)`, once per asset, creates an `AssetConfig` at `["asset", strategy, index]` (index = current `asset_count`), copies the official feed from the whitelist entry, and creates that asset's vault. TSLAx at index 0 (4000 bps), NVDAx at index 1 (6000 bps). Rejected if the mint is not whitelisted, if the weights would exceed 10,000 bps, or once `MAX_ASSETS` (8) is reached. Deposits stay closed until the weights sum to exactly 10,000. +`add_asset(weight_bps)`, once per asset, creates an `AssetConfig` at `["asset", strategy, index]` (index = current `asset_count`), copies the official feed from the ApprovedAsset account, and creates that asset's vault. TSLAx at index 0 (4000 bps), NVDAx at index 1 (6000 bps). Rejected if the mint is not approved (its ApprovedAsset account does not exist), if the weights would exceed 10,000 bps, or once `MAX_ASSETS` (16) is reached. Deposits stay closed until the weights sum to exactly 10,000. ### Alice deposits, and her money is deployed at once @@ -122,7 +122,7 @@ The `mock-swap-router` exists only for testing: it stores a `usdc_per_token` rat The strategy PDA holds all assets; no instruction moves a vault's tokens to the manager. The manager's powers are fenced: -- **Assets** are limited to mints whitelisted by the registry authority, with the price feed taken from the registry, not the manager. +- **Assets** are limited to mints approved by the registry authority, with the price feed taken from the registry, not the manager. - **Swaps** go only through the one router registered at creation, and each leg's minimum output is computed from the oracle, not supplied by the manager. - **The fee** is fixed at creation and capped at 10%, paid only in minted shares. @@ -151,13 +151,13 @@ cargo build-sbf --manifest-path programs/vault-strategy/Cargo.toml cargo test --manifest-path programs/vault-strategy/Cargo.toml ``` -Tests live in `programs/vault-strategy/tests/vault_strategy.rs` and use [LiteSVM](https://github.com/LiteSVM/litesvm). Both `.so` files are loaded from `target/deploy/`, so build before testing. The suite covers the full lifecycle end to end (deposit with auto-deployment, a price move, rebalance back to target, a second depositor priced at the new NAV, a year's fee, in-kind withdrawal), retiring an asset with `set_weight` and reallocating to reopen deposits, and the rejection paths: non-whitelisted asset, weight overflow, over-cap fee and slippage, oracle-bounded deposit slippage, an under-allocated strategy, non-manager `set_weight`, unregistered router, and incomplete asset accounts on deposit. +Tests live in `programs/vault-strategy/tests/vault_strategy.rs` and use [LiteSVM](https://github.com/LiteSVM/litesvm). Both `.so` files are loaded from `target/deploy/`, so build before testing. The suite covers the full lifecycle end to end (deposit with auto-deployment, a price move, rebalance back to target, a second depositor priced at the new NAV, a year's fee, in-kind withdrawal), retiring an asset with `set_weight` and reallocating to reopen deposits, and the rejection paths: unapproved asset, weight overflow, over-cap fee and slippage, oracle-bounded deposit slippage, an under-allocated strategy, non-manager `set_weight`, unregistered router, and incomplete asset accounts on deposit. ## FAQ ### How do I build an onchain investment fund on Solana? -A manager creates a strategy with `initialize_strategy`, registers whitelisted assets with `add_asset` at target weights, and investors `deposit` USDC for shares. Each deposit is deployed across the basket in the same transaction, and `withdraw` redeems a proportional slice of every vault in kind. +A manager creates a strategy with `initialize_strategy`, registers curator-approved assets with `add_asset` at target weights, and investors `deposit` USDC for shares. Each deposit is deployed across the basket in the same transaction, and `withdraw` redeems a proportional slice of every vault in kind. ### How are share prices calculated? diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/src/error.rs b/finance/vault-strategy/anchor/programs/vault-strategy/src/error.rs index 306afada..c80b5a70 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/src/error.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/src/error.rs @@ -12,8 +12,6 @@ pub enum VaultError { SlippageConfigTooHigh, #[msg("Asset mint is not part of this strategy")] AssetNotFound, - #[msg("Asset mint is not whitelisted in the registry")] - AssetNotWhitelisted, #[msg("Strategy already holds the maximum number of assets")] TooManyAssets, #[msg("Asset is already part of this strategy")] diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/add_asset.rs b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/add_asset.rs index bc1e3575..dc21ac1d 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/add_asset.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/add_asset.rs @@ -5,7 +5,7 @@ use anchor_spl::{ }; use crate::error::VaultError; -use crate::state::{AssetConfig, Registry, Strategy, WhitelistEntry, MAX_ASSETS}; +use crate::state::{ApprovedAsset, AssetConfig, Registry, Strategy, MAX_ASSETS}; #[derive(Accounts)] pub struct AddAssetAccountConstraints<'info> { @@ -26,12 +26,12 @@ pub struct AddAssetAccountConstraints<'info> { pub asset_mint: Box>, /// Proof the mint is approved, and the source of its official price feed. - /// Seeds tie it to this registry and this mint; existence means whitelisted. + /// Seeds tie it to this registry and this mint; existence means approved. #[account( - seeds = [b"whitelist", registry.key().as_ref(), asset_mint.key().as_ref()], - bump = whitelist_entry.bump + seeds = [b"approved_asset", registry.key().as_ref(), asset_mint.key().as_ref()], + bump = approved_asset.bump )] - pub whitelist_entry: Box>, + pub approved_asset: Box>, #[account( init, @@ -77,7 +77,7 @@ pub fn handle_add_asset( index, mint: context.accounts.asset_mint.key(), // Copied from the registry entry, never supplied by the manager. - price_feed: context.accounts.whitelist_entry.price_feed, + price_feed: context.accounts.approved_asset.price_feed, vault: context.accounts.vault_asset.key(), weight_bps, bump: context.bumps.asset_config, diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/whitelist_asset.rs b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/approve_asset.rs similarity index 57% rename from finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/whitelist_asset.rs rename to finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/approve_asset.rs index 7436b359..0dd06af7 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/whitelist_asset.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/approve_asset.rs @@ -1,10 +1,10 @@ use anchor_lang::prelude::*; use anchor_spl::token_interface::Mint; -use crate::state::{Registry, WhitelistEntry}; +use crate::state::{ApprovedAsset, Registry}; #[derive(Accounts)] -pub struct WhitelistAssetAccountConstraints<'info> { +pub struct ApproveAssetAccountConstraints<'info> { #[account(mut)] pub authority: Signer<'info>, @@ -20,24 +20,24 @@ pub struct WhitelistAssetAccountConstraints<'info> { #[account( init, payer = authority, - space = WhitelistEntry::DISCRIMINATOR.len() + WhitelistEntry::INIT_SPACE, - seeds = [b"whitelist", registry.key().as_ref(), asset_mint.key().as_ref()], + space = ApprovedAsset::DISCRIMINATOR.len() + ApprovedAsset::INIT_SPACE, + seeds = [b"approved_asset", registry.key().as_ref(), asset_mint.key().as_ref()], bump )] - pub whitelist_entry: Account<'info, WhitelistEntry>, + pub approved_asset: Account<'info, ApprovedAsset>, pub system_program: Program<'info, System>, } -pub fn handle_whitelist_asset( - context: Context, +pub fn handle_approve_asset( + context: Context, price_feed: Pubkey, ) -> Result<()> { - context.accounts.whitelist_entry.set_inner(WhitelistEntry { + context.accounts.approved_asset.set_inner(ApprovedAsset { registry: context.accounts.registry.key(), mint: context.accounts.asset_mint.key(), price_feed, - bump: context.bumps.whitelist_entry, + bump: context.bumps.approved_asset, }); Ok(()) } diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/initialize_strategy.rs b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/initialize_strategy.rs index 7e9f665f..0cf96dd4 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/initialize_strategy.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/initialize_strategy.rs @@ -27,7 +27,7 @@ pub struct InitializeStrategyAccountConstraints<'info> { pub usdc_mint: InterfaceAccount<'info, Mint>, - /// The whitelist this strategy will draw its assets from. + /// Registry whose approved assets this strategy may hold. pub registry: Account<'info, Registry>, #[account( diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/mod.rs b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/mod.rs index 66e9eb18..be9eb51e 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/mod.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/mod.rs @@ -1,19 +1,19 @@ pub mod add_asset; +pub mod approve_asset; pub mod collect_fees; pub mod deposit; pub mod initialize_registry; pub mod initialize_strategy; pub mod rebalance; pub mod set_weight; -pub mod whitelist_asset; pub mod withdraw; pub use add_asset::*; +pub use approve_asset::*; pub use collect_fees::*; pub use deposit::*; pub use initialize_registry::*; pub use initialize_strategy::*; pub use rebalance::*; pub use set_weight::*; -pub use whitelist_asset::*; pub use withdraw::*; diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/src/lib.rs b/finance/vault-strategy/anchor/programs/vault-strategy/src/lib.rs index 53b11305..4773503c 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/src/lib.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/src/lib.rs @@ -14,7 +14,8 @@ declare_id!("VLT5W7bqhRN4nCdRpXm8UfHRxZd9EuZGqiSAkGHQfGh"); pub mod vault_strategy { use super::*; - /// Create a curated whitelist of assets, owned by `authority` (not a manager). + /// Create the curator record for an approved-asset set, owned by `authority` + /// (not a manager). The set itself lives in per-asset ApprovedAsset accounts. pub fn initialize_registry( context: Context, ) -> Result<()> { @@ -22,11 +23,11 @@ pub mod vault_strategy { } /// Approve a mint and bind it to its official price feed. Registry authority only. - pub fn whitelist_asset( - context: Context, + pub fn approve_asset( + context: Context, price_feed: Pubkey, ) -> Result<()> { - instructions::whitelist_asset::handle_whitelist_asset(context, price_feed) + instructions::approve_asset::handle_approve_asset(context, price_feed) } /// Open a strategy at a caller-chosen index, e.g. index 0 derives the PDA @@ -47,7 +48,7 @@ pub mod vault_strategy { ) } - /// Add a whitelisted asset to the strategy at the next index. Manager only. + /// Add a curator-approved asset to the strategy at the next index. Manager only. pub fn add_asset(context: Context, weight_bps: u16) -> Result<()> { instructions::add_asset::handle_add_asset(context, weight_bps) } diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/src/state/registry.rs b/finance/vault-strategy/anchor/programs/vault-strategy/src/state/registry.rs index 9c814637..ffee0bb7 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/src/state/registry.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/src/state/registry.rs @@ -1,10 +1,14 @@ use anchor_lang::prelude::*; -/// A curated set of assets that strategies may hold. Maintained by a protocol -/// authority that is deliberately not the strategy manager: the authority vets -/// which real assets (and which official price feed) are safe, and the manager -/// only chooses among them. This is what stops a manager from listing a token -/// they mint themselves, or pairing a real mint with a feed they control. +/// The curator record at the root of an approved-asset set. This account holds no +/// list: it only names the authority allowed to approve assets under it. The set +/// itself is the collection of `ApprovedAsset` accounts derived from this registry, +/// and an asset counts as approved exactly when its `ApprovedAsset` account exists. +/// +/// The authority is deliberately not a strategy manager: the curator vets which +/// real assets (and which official price feed) are safe, and a manager only +/// chooses among them. This is what stops a manager from listing a token they +/// mint themselves, or pairing a real mint with a feed they control. #[account] #[derive(InitSpace)] pub struct Registry { @@ -12,12 +16,15 @@ pub struct Registry { pub bump: u8, } -/// One approved mint, binding it to its official Pyth PriceUpdateV2 feed. -/// Created only by the registry authority; add_asset copies `price_feed` from -/// here so the manager never supplies the feed. +/// One curator-approved asset, binding its mint to its official Pyth +/// PriceUpdateV2 feed. Its address is a PDA seeded by the registry and the mint, +/// so existence of this account is the approval check: add_asset derives the +/// address and fails if no account is there. Created only by the registry +/// authority; add_asset copies `price_feed` from here so the manager never +/// supplies the feed. #[account] #[derive(InitSpace)] -pub struct WhitelistEntry { +pub struct ApprovedAsset { pub registry: Pubkey, pub mint: Pubkey, pub price_feed: Pubkey, diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/src/state/strategy.rs b/finance/vault-strategy/anchor/programs/vault-strategy/src/state/strategy.rs index 62eae6bd..05dc893a 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/src/state/strategy.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/src/state/strategy.rs @@ -21,7 +21,7 @@ pub struct Strategy { /// Index used as the PDA seed, e.g. 0 for the first strategy. pub index: u64, pub manager: Pubkey, - /// Whitelist this strategy draws assets from. add_asset only accepts mints + /// Registry whose curator approves assets. add_asset only accepts mints /// approved in this registry. pub registry: Pubkey, pub share_mint: Pubkey, @@ -52,7 +52,7 @@ pub struct AssetConfig { pub strategy: Pubkey, pub index: u8, pub mint: Pubkey, - /// Pyth PriceUpdateV2 account, copied from the registry whitelist entry at + /// Pyth PriceUpdateV2 account, copied from the registry's ApprovedAsset at /// add time so the manager cannot substitute a feed they control. pub price_feed: Pubkey, /// Strategy-owned associated token account holding this asset. diff --git a/finance/vault-strategy/anchor/programs/vault-strategy/tests/vault_strategy.rs b/finance/vault-strategy/anchor/programs/vault-strategy/tests/vault_strategy.rs index 96c13690..469eb2b4 100644 --- a/finance/vault-strategy/anchor/programs/vault-strategy/tests/vault_strategy.rs +++ b/finance/vault-strategy/anchor/programs/vault-strategy/tests/vault_strategy.rs @@ -105,8 +105,8 @@ struct TestContext { strategy_pda: Pubkey, share_mint_pda: Pubkey, registry_pda: Pubkey, - whitelist_tsla: Pubkey, - whitelist_nvda: Pubkey, + approved_tsla: Pubkey, + approved_nvda: Pubkey, router_config_pda: Pubkey, router_authority_pda: Pubkey, tsla_rate_pda: Pubkey, @@ -130,7 +130,7 @@ impl TestContext { } /// Mints, router (config + rates + treasury), Pyth feeds, a registry with TSLAx -/// and NVDAx whitelisted, and all derived PDAs. Does not create the strategy. +/// and NVDAx approved, and all derived PDAs. Does not create the strategy. fn setup_full() -> TestContext { let vault_program_id = vault_strategy::id(); let router_program_id = mock_swap_router::id(); @@ -192,12 +192,12 @@ fn setup_full() -> TestContext { Pubkey::find_program_address(&[b"share_mint", strategy_pda.as_ref()], &vault_program_id); let (registry_pda, _) = Pubkey::find_program_address(&[b"registry", payer.pubkey().as_ref()], &vault_program_id); - let (whitelist_tsla, _) = Pubkey::find_program_address( - &[b"whitelist", registry_pda.as_ref(), tsla_mint.as_ref()], + let (approved_tsla, _) = Pubkey::find_program_address( + &[b"approved_asset", registry_pda.as_ref(), tsla_mint.as_ref()], &vault_program_id, ); - let (whitelist_nvda, _) = Pubkey::find_program_address( - &[b"whitelist", registry_pda.as_ref(), nvda_mint.as_ref()], + let (approved_nvda, _) = Pubkey::find_program_address( + &[b"approved_asset", registry_pda.as_ref(), nvda_mint.as_ref()], &vault_program_id, ); let (router_config_pda, _) = @@ -271,7 +271,7 @@ fn setup_full() -> TestContext { ) .unwrap(); - // Registry with both basket assets whitelisted, bound to their feeds. + // Registry with both basket assets approved, bound to their feeds. let init_registry_ix = Instruction::new_with_bytes( vault_program_id, &vault_strategy::instruction::InitializeRegistry {}.data(), @@ -291,17 +291,17 @@ fn setup_full() -> TestContext { .unwrap(); for (mint, feed, entry) in [ - (tsla_mint, price_feed_tsla, whitelist_tsla), - (nvda_mint, price_feed_nvda, whitelist_nvda), + (tsla_mint, price_feed_tsla, approved_tsla), + (nvda_mint, price_feed_nvda, approved_nvda), ] { let ix = Instruction::new_with_bytes( vault_program_id, - &vault_strategy::instruction::WhitelistAsset { price_feed: feed }.data(), - vault_strategy::accounts::WhitelistAssetAccountConstraints { + &vault_strategy::instruction::ApproveAsset { price_feed: feed }.data(), + vault_strategy::accounts::ApproveAssetAccountConstraints { authority: payer.pubkey(), registry: registry_pda, asset_mint: mint, - whitelist_entry: entry, + approved_asset: entry, system_program: system_program::id(), } .to_account_metas(None), @@ -321,8 +321,8 @@ fn setup_full() -> TestContext { strategy_pda, share_mint_pda, registry_pda, - whitelist_tsla, - whitelist_nvda, + approved_tsla, + approved_nvda, router_config_pda, router_authority_pda, tsla_rate_pda, @@ -372,7 +372,7 @@ fn add_asset( ctx: &mut TestContext, index: u8, mint: Pubkey, - whitelist_entry: Pubkey, + approved_asset: Pubkey, vault: Pubkey, weight_bps: u16, ) -> Result<(), solana_kite::SolanaKiteError> { @@ -385,7 +385,7 @@ fn add_asset( strategy: ctx.strategy_pda, registry: ctx.registry_pda, asset_mint: mint, - whitelist_entry, + approved_asset, asset_config, vault_asset: vault, associated_token_program: ata_program_id(), @@ -406,9 +406,9 @@ fn add_asset( fn standard_strategy(ctx: &mut TestContext) { let router = ctx.router_program_id; init_strategy(ctx, FEE_BPS, SLIPPAGE_BPS, router); - let (tm, wt, vt) = (ctx.tsla_mint, ctx.whitelist_tsla, ctx.vault_tsla); + let (tm, wt, vt) = (ctx.tsla_mint, ctx.approved_tsla, ctx.vault_tsla); add_asset(ctx, 0, tm, wt, vt, 4000).unwrap(); - let (nm, wn, vn) = (ctx.nvda_mint, ctx.whitelist_nvda, ctx.vault_nvda); + let (nm, wn, vn) = (ctx.nvda_mint, ctx.approved_nvda, ctx.vault_nvda); add_asset(ctx, 1, nm, wn, vn, 6000).unwrap(); } @@ -585,7 +585,7 @@ fn set_weight( fn tsla_only_strategy(ctx: &mut TestContext) { let router = ctx.router_program_id; init_strategy(ctx, FEE_BPS, SLIPPAGE_BPS, router); - let (tm, wt, vt) = (ctx.tsla_mint, ctx.whitelist_tsla, ctx.vault_tsla); + let (tm, wt, vt) = (ctx.tsla_mint, ctx.approved_tsla, ctx.vault_tsla); add_asset(ctx, 0, tm, wt, vt, 4000).unwrap(); } @@ -746,21 +746,25 @@ fn test_initialize_and_add_assets() { } #[test] -fn test_add_asset_rejects_non_whitelisted() { +fn test_add_asset_rejects_unapproved() { let mut ctx = setup_full(); let router = ctx.router_program_id; init_strategy(&mut ctx, FEE_BPS, SLIPPAGE_BPS, router); - // A mint that was never whitelisted: its whitelist_entry PDA does not exist. + // A mint that was never approved: its approved_asset PDA does not exist. let rogue_mint = create_token_mint(&mut ctx.svm, &ctx.payer, TOKEN_DECIMALS, None).unwrap(); let (rogue_entry, _) = Pubkey::find_program_address( - &[b"whitelist", ctx.registry_pda.as_ref(), rogue_mint.as_ref()], + &[ + b"approved_asset", + ctx.registry_pda.as_ref(), + rogue_mint.as_ref(), + ], &ctx.vault_program_id, ); let rogue_vault = derive_ata(&ctx.strategy_pda, &rogue_mint); let result = add_asset(&mut ctx, 0, rogue_mint, rogue_entry, rogue_vault, 5000); - assert!(result.is_err(), "adding a non-whitelisted mint must fail"); + assert!(result.is_err(), "adding an unapproved mint must fail"); } #[test] @@ -768,33 +772,33 @@ fn test_add_asset_rejects_weight_overflow() { let mut ctx = setup_full(); let router = ctx.router_program_id; init_strategy(&mut ctx, FEE_BPS, SLIPPAGE_BPS, router); - let (tm, wt, vt) = (ctx.tsla_mint, ctx.whitelist_tsla, ctx.vault_tsla); + let (tm, wt, vt) = (ctx.tsla_mint, ctx.approved_tsla, ctx.vault_tsla); add_asset(&mut ctx, 0, tm, wt, vt, 6000).unwrap(); - let (nm, wn, vn) = (ctx.nvda_mint, ctx.whitelist_nvda, ctx.vault_nvda); + let (nm, wn, vn) = (ctx.nvda_mint, ctx.approved_nvda, ctx.vault_nvda); let result = add_asset(&mut ctx, 1, nm, wn, vn, 6000); assert!(result.is_err(), "weights over 10000 bps must fail"); } -/// Create a fresh mint and whitelist it in the registry. The bound price feed is an +/// Create a fresh mint and approve it in the registry. The bound price feed is an /// arbitrary pubkey: callers that never value this asset (e.g. the cap boundary test) /// do not need a real feed account. -fn create_and_whitelist_mint(ctx: &mut TestContext) -> (Pubkey, Pubkey) { +fn create_and_approve_mint(ctx: &mut TestContext) -> (Pubkey, Pubkey) { let mint = create_token_mint(&mut ctx.svm, &ctx.payer, TOKEN_DECIMALS, None).unwrap(); let (entry, _) = Pubkey::find_program_address( - &[b"whitelist", ctx.registry_pda.as_ref(), mint.as_ref()], + &[b"approved_asset", ctx.registry_pda.as_ref(), mint.as_ref()], &ctx.vault_program_id, ); let ix = Instruction::new_with_bytes( ctx.vault_program_id, - &vault_strategy::instruction::WhitelistAsset { + &vault_strategy::instruction::ApproveAsset { price_feed: Keypair::new().pubkey(), } .data(), - vault_strategy::accounts::WhitelistAssetAccountConstraints { + vault_strategy::accounts::ApproveAssetAccountConstraints { authority: ctx.payer.pubkey(), registry: ctx.registry_pda, asset_mint: mint, - whitelist_entry: entry, + approved_asset: entry, system_program: system_program::id(), } .to_account_metas(None), @@ -812,7 +816,7 @@ fn test_add_asset_enforces_max_assets() { // Fill the basket to the cap: 16 assets at 625 bps each = 10000. for index in 0..16u8 { - let (mint, entry) = create_and_whitelist_mint(&mut ctx); + let (mint, entry) = create_and_approve_mint(&mut ctx); let vault = derive_ata(&ctx.strategy_pda, &mint); add_asset(&mut ctx, index, mint, entry, vault, 625).unwrap(); } @@ -821,7 +825,7 @@ fn test_add_asset_enforces_max_assets() { assert_eq!(strategy.total_weight_bps, 10_000); // The 17th asset must be rejected. Weight 0 so the cap, not the weight sum, trips. - let (mint, entry) = create_and_whitelist_mint(&mut ctx); + let (mint, entry) = create_and_approve_mint(&mut ctx); let vault = derive_ata(&ctx.strategy_pda, &mint); let result = add_asset(&mut ctx, 16, mint, entry, vault, 0); assert!(result.is_err(), "adding beyond MAX_ASSETS must revert"); @@ -986,9 +990,9 @@ fn test_deposit_rejects_unregistered_router() { // Register a different router than the deployed mock, then fully allocate 40/60. let bogus_router = Pubkey::new_unique(); init_strategy(&mut ctx, FEE_BPS, SLIPPAGE_BPS, bogus_router); - let (tm, wt, vt) = (ctx.tsla_mint, ctx.whitelist_tsla, ctx.vault_tsla); + let (tm, wt, vt) = (ctx.tsla_mint, ctx.approved_tsla, ctx.vault_tsla); add_asset(&mut ctx, 0, tm, wt, vt, 4000).unwrap(); - let (nm, wn, vn) = (ctx.nvda_mint, ctx.whitelist_nvda, ctx.vault_nvda); + let (nm, wn, vn) = (ctx.nvda_mint, ctx.approved_nvda, ctx.vault_nvda); add_asset(&mut ctx, 1, nm, wn, vn, 6000).unwrap(); let user = fund_user(&mut ctx, 10_000_000); From 18968a05b5256570cdcd3da94e83751945e5980c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 15:29:26 +0000 Subject: [PATCH 2/2] vault-strategy: apply the ApprovedAsset rename to the Quasar port Bring the Quasar port in line with the Anchor version: WhitelistEntry becomes ApprovedAsset, whitelist_asset becomes approve_asset, the PDA seed "whitelist" becomes "approved_asset", and the unused AssetNotWhitelisted error is removed. Instruction and account discriminators are explicit in Quasar, so the wire format is otherwise unchanged. Registry doc comments and the README now state that the Registry account is the curator record at the root of the approved set, not the list itself. Also restore the anchor CHANGELOG's merged entry to describe what shipped on 2026-07-01 under a dated heading (correcting its stale MAX_ASSETS figure), and record the rename as a new dated entry in both changelogs. Verified with quasar build + cargo test on both programs (router 2 tests, vault 3 tests, all passing); the renamed seed is exercised end to end. --- finance/vault-strategy/anchor/CHANGELOG.md | 10 +- finance/vault-strategy/quasar/CHANGELOG.md | 4 + finance/vault-strategy/quasar/README.md | 17 +-- .../src/instructions/set_rate.rs | 4 +- .../src/instructions/swap_asset_for_usdc.rs | 4 +- .../src/instructions/swap_usdc_for_asset.rs | 9 +- .../quasar/mock-swap-router/src/lib.rs | 2 +- .../quasar/mock-swap-router/src/tests.rs | 11 +- .../quasar/vault-strategy/src/errors.rs | 1 - .../src/instructions/add_asset.rs | 10 +- .../{whitelist_asset.rs => approve_asset.rs} | 18 +-- .../src/instructions/deposit.rs | 28 +++- .../vault-strategy/src/instructions/mod.rs | 4 +- .../src/instructions/withdraw.rs | 18 ++- .../quasar/vault-strategy/src/lib.rs | 16 +-- .../quasar/vault-strategy/src/oracle.rs | 6 +- .../vault-strategy/src/state/registry.rs | 22 +-- .../vault-strategy/src/state/strategy.rs | 2 +- .../quasar/vault-strategy/src/tests.rs | 127 +++++++++++++----- 19 files changed, 216 insertions(+), 97 deletions(-) rename finance/vault-strategy/quasar/vault-strategy/src/instructions/{whitelist_asset.rs => approve_asset.rs} (57%) diff --git a/finance/vault-strategy/anchor/CHANGELOG.md b/finance/vault-strategy/anchor/CHANGELOG.md index 1565a379..182761d0 100644 --- a/finance/vault-strategy/anchor/CHANGELOG.md +++ b/finance/vault-strategy/anchor/CHANGELOG.md @@ -1,11 +1,15 @@ # Changelog -## Unreleased +## 2026-07-20 + +- **`WhitelistEntry` renamed `ApprovedAsset`** (and `whitelist_asset` renamed `approve_asset`, PDA seed `"whitelist"` renamed `"approved_asset"`), naming the account after what it is: one curator-approved asset bound to its official price feed. The unused `AssetNotWhitelisted` error is removed; approval is checked by the `ApprovedAsset` account's existence. Doc comments and README now state that the `Registry` account is the curator record at the root of the approved set, not the list itself. + +## 2026-07-01 ### Added -- **Curated asset registry.** A `Registry` account naming a curator authority separate from strategy managers, plus one `ApprovedAsset` account per approved mint (`["approved_asset", registry, mint]`), each binding the mint to its official Pyth price feed. Approval is checked by the account's existence. New instructions: `initialize_registry`, `approve_asset`. -- **Dynamic assets.** A strategy now grows its portfolio with `add_asset`, which registers an approved mint at the next index as an `AssetConfig` PDA (`["asset", strategy, index]`) and creates its vault. Assets occupy the contiguous range `0..asset_count`, up to `MAX_ASSETS` (16). Replaces the previous fixed two-asset layout. +- **Curated asset registry.** A `Registry` plus per-mint `WhitelistEntry` accounts, maintained by a protocol authority separate from strategy managers. Each entry binds an approved mint to its official Pyth price feed. New instructions: `initialize_registry`, `whitelist_asset`. +- **Dynamic assets.** A strategy now grows its portfolio with `add_asset`, which registers a whitelisted mint at the next index as an `AssetConfig` PDA (`["asset", strategy, index]`) and creates its vault. Assets occupy the contiguous range `0..asset_count`, up to `MAX_ASSETS` (16). Replaces the previous fixed two-asset layout. - **Oracle-bounded slippage.** `deposit` and `rebalance` compute each swap's minimum output from the Pyth price and a strategy-level `max_slippage_bps` (capped at `MAX_SLIPPAGE_BPS` = 10%), instead of trusting a caller-supplied minimum. Set at creation via `initialize_strategy`. - **Full-allocation invariant with immediate deployment.** A strategy accepts deposits only once its weights sum to exactly 10,000 bps (`deposit` reverts with `StrategyNotFullyAllocated` otherwise). `deposit` then swaps each depositor's USDC into the basket at its target weights through the registered router in the same transaction, so every deposit is fully invested (bar sub-cent rounding dust) and the USDC vault holds no idle cash. - **Retirable assets.** `set_weight(weight_bps)` changes an asset's target weight after creation, including setting it to zero to retire it (reassign that weight to another asset to reach 100% and reopen deposits; `rebalance` liquidates the retired holdings). The asset's index is preserved, so the `0..asset_count` range the valuation handlers depend on stays contiguous. diff --git a/finance/vault-strategy/quasar/CHANGELOG.md b/finance/vault-strategy/quasar/CHANGELOG.md index 4f116388..44cb79ce 100644 --- a/finance/vault-strategy/quasar/CHANGELOG.md +++ b/finance/vault-strategy/quasar/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2026-07-20 + +- **`WhitelistEntry` renamed `ApprovedAsset`** (and `whitelist_asset` renamed `approve_asset`, PDA seed `"whitelist"` renamed `"approved_asset"`), naming the account after what it is: one curator-approved asset bound to its official price feed. The unused `AssetNotWhitelisted` error is removed; approval is checked by the `ApprovedAsset` account's existence. Doc comments and README now state that the `Registry` account is the curator record at the root of the approved set, not the list itself. + ## 2026-07-07 Added this changelog. Changes prior to this date were tracked in git history only. diff --git a/finance/vault-strategy/quasar/README.md b/finance/vault-strategy/quasar/README.md index 09175a5f..7ef78d1f 100644 --- a/finance/vault-strategy/quasar/README.md +++ b/finance/vault-strategy/quasar/README.md @@ -1,6 +1,6 @@ # Solana Vault Strategy (Quasar) -A tokenized multi-asset vault on Solana, written with Quasar. A manager assembles a basket of whitelisted +A multi-asset vault on Solana, written with Quasar. A manager assembles a basket of curator-approved assets at target weights; anyone can deposit USDC and receive shares priced at the vault's net asset value, and each deposit is immediately deployed into the basket by swapping USDC into every asset at its weight. Withdrawals burn shares @@ -28,10 +28,11 @@ strategy manager: it vets which real assets and feeds are safe, and the manager only chooses among them, so a manager can never list a token they mint themselves or pair a real mint with a feed they control. -- `initialize_registry` opens the registry; `whitelist_asset` approves a mint - and records its price feed. +- `initialize_registry` records the curator; `approve_asset` approves a mint + and records its price feed. The registry holds no list: an asset is approved + exactly when its `ApprovedAsset` account exists. - A manager opens a basket with `initialize_strategy` (choosing a management fee - and a slippage tolerance), then adds whitelisted assets with `add_asset`, each + and a slippage tolerance), then adds approved assets with `add_asset`, each at a target weight in basis points. The weights must sum to 100% before the vault accepts deposits, so every deposit is fully invested. `set_weight` retunes a weight or retires an asset by setting it to zero. @@ -54,9 +55,9 @@ by more than the strategy's slippage tolerance. ## Accounts and PDAs -- **Registry** `["registry", authority]` and **WhitelistEntry** - `["whitelist", registry, mint]` - the curated asset set and each approved - mint's price feed. +- **Registry** `["registry", authority]` and **ApprovedAsset** + `["approved_asset", registry, mint]` - the curator record and, one account per + approved mint, the asset set with each mint's price feed. - **Strategy** `["strategy", index]` - one basket, addressed by a counter. Holds the manager, registry, share mint, USDC mint, router, fee, slippage, total shares, and running weight sum. The Strategy PDA is the authority of the share @@ -119,7 +120,7 @@ cargo install --git https://github.com/blueshift-gg/quasar quasar-cli --locked The router suite (`mock-swap-router/src/tests.rs`) exercises initialize, set-rate, and a USDC-for-asset swap. The vault suite (`vault-strategy/src/tests.rs`) drives -the manager setup (registry, whitelist, strategy, add asset) and a two-program +the manager setup (registry, approve asset, strategy, add asset) and a two-program deposit that deploys USDC into the basket through the router CPI, asserting share minting, vault balances, and treasury flow. diff --git a/finance/vault-strategy/quasar/mock-swap-router/src/instructions/set_rate.rs b/finance/vault-strategy/quasar/mock-swap-router/src/instructions/set_rate.rs index f72f7120..0fd3edea 100644 --- a/finance/vault-strategy/quasar/mock-swap-router/src/instructions/set_rate.rs +++ b/finance/vault-strategy/quasar/mock-swap-router/src/instructions/set_rate.rs @@ -1,9 +1,7 @@ use quasar_lang::prelude::*; use quasar_spl::prelude::*; -use crate::state::{ - AssetRate, AssetRateInner, RouterAuthorityPda, RouterConfig, TreasuryPda, -}; +use crate::state::{AssetRate, AssetRateInner, RouterAuthorityPda, RouterConfig, TreasuryPda}; #[derive(Accounts)] pub struct SetRateAccountConstraints { diff --git a/finance/vault-strategy/quasar/mock-swap-router/src/instructions/swap_asset_for_usdc.rs b/finance/vault-strategy/quasar/mock-swap-router/src/instructions/swap_asset_for_usdc.rs index d0f89564..4d66e2eb 100644 --- a/finance/vault-strategy/quasar/mock-swap-router/src/instructions/swap_asset_for_usdc.rs +++ b/finance/vault-strategy/quasar/mock-swap-router/src/instructions/swap_asset_for_usdc.rs @@ -2,7 +2,9 @@ use quasar_lang::prelude::*; use quasar_spl::prelude::*; use crate::errors::RouterError; -use crate::state::{AssetRate, RouterAuthorityPda, RouterConfig, TreasuryPda, ROUTER_AUTHORITY_SEED}; +use crate::state::{ + AssetRate, RouterAuthorityPda, RouterConfig, TreasuryPda, ROUTER_AUTHORITY_SEED, +}; #[derive(Accounts)] pub struct SwapAssetForUsdcAccountConstraints { diff --git a/finance/vault-strategy/quasar/mock-swap-router/src/instructions/swap_usdc_for_asset.rs b/finance/vault-strategy/quasar/mock-swap-router/src/instructions/swap_usdc_for_asset.rs index 88bb4a21..f3108122 100644 --- a/finance/vault-strategy/quasar/mock-swap-router/src/instructions/swap_usdc_for_asset.rs +++ b/finance/vault-strategy/quasar/mock-swap-router/src/instructions/swap_usdc_for_asset.rs @@ -2,7 +2,9 @@ use quasar_lang::prelude::*; use quasar_spl::prelude::*; use crate::errors::RouterError; -use crate::state::{AssetRate, RouterAuthorityPda, RouterConfig, TreasuryPda, ROUTER_AUTHORITY_SEED}; +use crate::state::{ + AssetRate, RouterAuthorityPda, RouterConfig, TreasuryPda, ROUTER_AUTHORITY_SEED, +}; #[derive(Accounts)] pub struct SwapUsdcForAssetAccountConstraints { @@ -56,7 +58,10 @@ pub fn handle_swap_usdc_for_asset( .ok_or(RouterError::MathOverflow)? .try_into() .map_err(|_| RouterError::MathOverflow)?; - require!(asset_out >= minimum_asset_out, RouterError::SlippageExceeded); + require!( + asset_out >= minimum_asset_out, + RouterError::SlippageExceeded + ); // USDC from caller to the router treasury (caller signs). accounts diff --git a/finance/vault-strategy/quasar/mock-swap-router/src/lib.rs b/finance/vault-strategy/quasar/mock-swap-router/src/lib.rs index f0e75ffb..a2cd4cf0 100644 --- a/finance/vault-strategy/quasar/mock-swap-router/src/lib.rs +++ b/finance/vault-strategy/quasar/mock-swap-router/src/lib.rs @@ -14,7 +14,7 @@ mod tests; declare_id!("SWPR8Rk3aq3DrDGLdaANq7xCMnXoUFUJWJJmCWxc8Jm"); /// A mock constant-rate swap router used by the vault-strategy example. It -/// swaps a whitelisted asset against USDC at an admin-set fixed rate: buying an +/// swaps an approved asset against USDC at an admin-set fixed rate: buying an /// asset mints it against USDC paid into the treasury; selling burns it and /// pays USDC out. Stand-in for a real AMM or aggregator so the vault-strategy /// example is self-contained. diff --git a/finance/vault-strategy/quasar/mock-swap-router/src/tests.rs b/finance/vault-strategy/quasar/mock-swap-router/src/tests.rs index 01285c1c..ad74d82e 100644 --- a/finance/vault-strategy/quasar/mock-swap-router/src/tests.rs +++ b/finance/vault-strategy/quasar/mock-swap-router/src/tests.rs @@ -230,11 +230,18 @@ fn test_initialize_and_swap_usdc_for_asset() { ]; let result = svm.process_instruction_chain(&instructions, &accounts); - assert!(result.is_ok(), "router chain failed: {:?}", result.raw_result); + assert!( + result.is_ok(), + "router chain failed: {:?}", + result.raw_result + ); // Caller paid all USDC and received the minted asset; treasury holds the USDC. assert_eq!(token_amount(result.account(&caller_usdc).unwrap()), 0); - assert_eq!(token_amount(result.account(&caller_asset).unwrap()), ASSET_OUT); + assert_eq!( + token_amount(result.account(&caller_asset).unwrap()), + ASSET_OUT + ); assert_eq!(token_amount(result.account(&treasury).unwrap()), USDC_IN); println!(" ROUTER SWAP CU: {}", result.compute_units_consumed); diff --git a/finance/vault-strategy/quasar/vault-strategy/src/errors.rs b/finance/vault-strategy/quasar/vault-strategy/src/errors.rs index 4cbaeba3..0c5b5e20 100644 --- a/finance/vault-strategy/quasar/vault-strategy/src/errors.rs +++ b/finance/vault-strategy/quasar/vault-strategy/src/errors.rs @@ -8,7 +8,6 @@ pub enum VaultError { SwapSlippageExceeded, SlippageConfigTooHigh, AssetNotFound, - AssetNotWhitelisted, TooManyAssets, DuplicateAsset, WeightOverflow, diff --git a/finance/vault-strategy/quasar/vault-strategy/src/instructions/add_asset.rs b/finance/vault-strategy/quasar/vault-strategy/src/instructions/add_asset.rs index 7f22d08e..de3f23ff 100644 --- a/finance/vault-strategy/quasar/vault-strategy/src/instructions/add_asset.rs +++ b/finance/vault-strategy/quasar/vault-strategy/src/instructions/add_asset.rs @@ -3,8 +3,8 @@ use quasar_spl::prelude::*; use crate::errors::VaultError; use crate::state::{ - snapshot_strategy, AssetConfig, AssetConfigInner, AssetVaultPda, Registry, Strategy, - WhitelistEntry, MAX_ASSETS, + snapshot_strategy, ApprovedAsset, AssetConfig, AssetConfigInner, AssetVaultPda, Registry, + Strategy, MAX_ASSETS, }; #[derive(Accounts)] @@ -25,8 +25,8 @@ pub struct AddAssetAccountConstraints { pub asset_mint: Account, /// Proof the mint is approved and the source of its official price feed. - #[account(address = WhitelistEntry::seeds(registry.address(), asset_mint.address()))] - pub whitelist_entry: Account, + #[account(address = ApprovedAsset::seeds(registry.address(), asset_mint.address()))] + pub approved_asset: Account, #[account( init, @@ -73,7 +73,7 @@ pub fn handle_add_asset( index, mint: *accounts.asset_mint.address(), // Copied from the registry entry, never supplied by the manager. - price_feed: accounts.whitelist_entry.price_feed, + price_feed: accounts.approved_asset.price_feed, vault: *accounts.vault_asset.address(), weight_bps, bump: bumps.asset_config, diff --git a/finance/vault-strategy/quasar/vault-strategy/src/instructions/whitelist_asset.rs b/finance/vault-strategy/quasar/vault-strategy/src/instructions/approve_asset.rs similarity index 57% rename from finance/vault-strategy/quasar/vault-strategy/src/instructions/whitelist_asset.rs rename to finance/vault-strategy/quasar/vault-strategy/src/instructions/approve_asset.rs index f116b5bd..9efba644 100644 --- a/finance/vault-strategy/quasar/vault-strategy/src/instructions/whitelist_asset.rs +++ b/finance/vault-strategy/quasar/vault-strategy/src/instructions/approve_asset.rs @@ -1,10 +1,10 @@ use quasar_lang::prelude::*; use quasar_spl::prelude::*; -use crate::state::{Registry, WhitelistEntry, WhitelistEntryInner}; +use crate::state::{ApprovedAsset, ApprovedAssetInner, Registry}; #[derive(Accounts)] -pub struct WhitelistAssetAccountConstraints { +pub struct ApproveAssetAccountConstraints { #[account(mut)] pub authority: Signer, @@ -16,25 +16,25 @@ pub struct WhitelistAssetAccountConstraints { #[account( init, payer = authority, - address = WhitelistEntry::seeds(registry.address(), asset_mint.address()), + address = ApprovedAsset::seeds(registry.address(), asset_mint.address()), )] - pub whitelist_entry: Account, + pub approved_asset: Account, pub rent: Sysvar, pub system_program: Program, } #[inline(always)] -pub fn handle_whitelist_asset( - accounts: &mut WhitelistAssetAccountConstraints, +pub fn handle_approve_asset( + accounts: &mut ApproveAssetAccountConstraints, price_feed: Address, - bumps: &WhitelistAssetAccountConstraintsBumps, + bumps: &ApproveAssetAccountConstraintsBumps, ) -> Result<(), ProgramError> { - accounts.whitelist_entry.set_inner(WhitelistEntryInner { + accounts.approved_asset.set_inner(ApprovedAssetInner { registry: *accounts.registry.address(), mint: *accounts.asset_mint.address(), price_feed, - bump: bumps.whitelist_entry, + bump: bumps.approved_asset, }); Ok(()) } diff --git a/finance/vault-strategy/quasar/vault-strategy/src/instructions/deposit.rs b/finance/vault-strategy/quasar/vault-strategy/src/instructions/deposit.rs index 2f3101fa..e988e199 100644 --- a/finance/vault-strategy/quasar/vault-strategy/src/instructions/deposit.rs +++ b/finance/vault-strategy/quasar/vault-strategy/src/instructions/deposit.rs @@ -117,9 +117,20 @@ pub fn handle_deposit( let feed_view = get_view(&remaining, index * ACCOUNTS_PER_ASSET + 4)?; let config = load_asset_config(&config_view)?; - require_keys_eq!(config.strategy, strategy_key, VaultError::InvalidAssetAccount); - require!(config.index as usize == index, VaultError::InvalidAssetAccount); - require_keys_eq!(*vault_view.address(), config.vault, VaultError::InvalidAssetAccount); + require_keys_eq!( + config.strategy, + strategy_key, + VaultError::InvalidAssetAccount + ); + require!( + config.index as usize == index, + VaultError::InvalidAssetAccount + ); + require_keys_eq!( + *vault_view.address(), + config.vault, + VaultError::InvalidAssetAccount + ); let price = load_price(&feed_view, &config.price_feed, now)?; let amount = read_token_amount(&vault_view)?; @@ -140,7 +151,10 @@ pub fn handle_deposit( .try_into() .map_err(|_| VaultError::MathOverflow)? }; - require!(shares_to_mint >= minimum_shares, VaultError::SlippageTooHigh); + require!( + shares_to_mint >= minimum_shares, + VaultError::SlippageTooHigh + ); let mut strategy = snapshot_strategy(&accounts.strategy); strategy.total_shares = total_shares @@ -180,7 +194,11 @@ pub fn handle_deposit( let feed_view = get_view(&remaining, index * ACCOUNTS_PER_ASSET + 4)?; let config = load_asset_config(&config_view)?; - require_keys_eq!(*mint_view.address(), config.mint, VaultError::InvalidAssetAccount); + require_keys_eq!( + *mint_view.address(), + config.mint, + VaultError::InvalidAssetAccount + ); if config.weight_bps == 0 { continue; diff --git a/finance/vault-strategy/quasar/vault-strategy/src/instructions/mod.rs b/finance/vault-strategy/quasar/vault-strategy/src/instructions/mod.rs index 66e9eb18..be9eb51e 100644 --- a/finance/vault-strategy/quasar/vault-strategy/src/instructions/mod.rs +++ b/finance/vault-strategy/quasar/vault-strategy/src/instructions/mod.rs @@ -1,19 +1,19 @@ pub mod add_asset; +pub mod approve_asset; pub mod collect_fees; pub mod deposit; pub mod initialize_registry; pub mod initialize_strategy; pub mod rebalance; pub mod set_weight; -pub mod whitelist_asset; pub mod withdraw; pub use add_asset::*; +pub use approve_asset::*; pub use collect_fees::*; pub use deposit::*; pub use initialize_registry::*; pub use initialize_strategy::*; pub use rebalance::*; pub use set_weight::*; -pub use whitelist_asset::*; pub use withdraw::*; diff --git a/finance/vault-strategy/quasar/vault-strategy/src/instructions/withdraw.rs b/finance/vault-strategy/quasar/vault-strategy/src/instructions/withdraw.rs index 5da87ce6..d09f0309 100644 --- a/finance/vault-strategy/quasar/vault-strategy/src/instructions/withdraw.rs +++ b/finance/vault-strategy/quasar/vault-strategy/src/instructions/withdraw.rs @@ -137,10 +137,22 @@ pub fn handle_withdraw( let user_ata_view = get_view(&remaining, i * ACCOUNTS_PER_ASSET + 3)?; let config = load_asset_config(&config_view)?; - require_keys_eq!(config.strategy, strategy_key, VaultError::InvalidAssetAccount); + require_keys_eq!( + config.strategy, + strategy_key, + VaultError::InvalidAssetAccount + ); require!(config.index as usize == i, VaultError::InvalidAssetAccount); - require_keys_eq!(*vault_view.address(), config.vault, VaultError::InvalidAssetAccount); - require_keys_eq!(*mint_view.address(), config.mint, VaultError::InvalidAssetAccount); + require_keys_eq!( + *vault_view.address(), + config.vault, + VaultError::InvalidAssetAccount + ); + require_keys_eq!( + *mint_view.address(), + config.mint, + VaultError::InvalidAssetAccount + ); let (recipient_mint, recipient_owner) = read_token_mint_and_owner(&user_ata_view)?; require_keys_eq!(recipient_owner, user_key, VaultError::InvalidRecipient); diff --git a/finance/vault-strategy/quasar/vault-strategy/src/lib.rs b/finance/vault-strategy/quasar/vault-strategy/src/lib.rs index c90b9572..da121861 100644 --- a/finance/vault-strategy/quasar/vault-strategy/src/lib.rs +++ b/finance/vault-strategy/quasar/vault-strategy/src/lib.rs @@ -15,7 +15,7 @@ mod tests; declare_id!("VLT5W7bqhRN4nCdRpXm8UfHRxZd9EuZGqiSAkGHQfGh"); /// Tokenized multi-asset vault strategy. A manager assembles a basket of -/// whitelisted assets at target weights; depositors receive shares priced at +/// curator-approved assets at target weights; depositors receive shares priced at /// net asset value, and each deposit is immediately deployed into the basket by /// swapping USDC through a registered router. Withdrawals redeem shares for a /// proportional slice of every vault. See README.md for the full walkthrough. @@ -23,7 +23,7 @@ declare_id!("VLT5W7bqhRN4nCdRpXm8UfHRxZd9EuZGqiSAkGHQfGh"); mod quasar_vault_strategy { use super::*; - /// Create a curated whitelist of assets, owned by `authority` (not a + /// Create a curated approved_asset of assets, owned by `authority` (not a /// manager). #[instruction(discriminator = 0)] pub fn initialize_registry( @@ -35,15 +35,11 @@ mod quasar_vault_strategy { /// Approve a mint and bind it to its official price feed. Registry authority /// only. #[instruction(discriminator = 1)] - pub fn whitelist_asset( - ctx: Ctx, + pub fn approve_asset( + ctx: Ctx, price_feed: Address, ) -> Result<(), ProgramError> { - instructions::whitelist_asset::handle_whitelist_asset( - &mut ctx.accounts, - price_feed, - &ctx.bumps, - ) + instructions::approve_asset::handle_approve_asset(&mut ctx.accounts, price_feed, &ctx.bumps) } /// Open a strategy at a caller-chosen index. Manager pays and becomes the @@ -66,7 +62,7 @@ mod quasar_vault_strategy { ) } - /// Add a whitelisted asset to the strategy at the next index. Manager only. + /// Add a curator-approved asset to the strategy at the next index. Manager only. #[instruction(discriminator = 3)] pub fn add_asset( ctx: Ctx, diff --git a/finance/vault-strategy/quasar/vault-strategy/src/oracle.rs b/finance/vault-strategy/quasar/vault-strategy/src/oracle.rs index 7e453346..22fbc2da 100644 --- a/finance/vault-strategy/quasar/vault-strategy/src/oracle.rs +++ b/finance/vault-strategy/quasar/vault-strategy/src/oracle.rs @@ -16,7 +16,7 @@ const MAX_PRICE_AGE_SECONDS: i64 = 60; const TOKEN_MINT_OFFSET: usize = 0; // mint: Pubkey [0..32] const TOKEN_OWNER_OFFSET: usize = 32; // owner: Pubkey [32..64] const TOKEN_AMOUNT_OFFSET: usize = 64; // amount: u64 [64..72] -// Mint layout: mint_authority option(36) + supply(8) = 44. + // Mint layout: mint_authority option(36) + supply(8) = 44. const MINT_DECIMALS_OFFSET: usize = 44; /// Borrow an account's raw data as a slice. Read-only; used for accounts that @@ -83,7 +83,9 @@ pub fn read_mint_decimals(account: &AccountView) -> Result { } /// Read the `mint` and `owner` addresses of a token account from its raw data. -pub fn read_token_mint_and_owner(account: &AccountView) -> Result<(Address, Address), ProgramError> { +pub fn read_token_mint_and_owner( + account: &AccountView, +) -> Result<(Address, Address), ProgramError> { let data = account_data(account); if data.len() < TOKEN_OWNER_OFFSET + 32 { return Err(VaultError::InvalidVaultAccount.into()); diff --git a/finance/vault-strategy/quasar/vault-strategy/src/state/registry.rs b/finance/vault-strategy/quasar/vault-strategy/src/state/registry.rs index 0dfe321c..45043d93 100644 --- a/finance/vault-strategy/quasar/vault-strategy/src/state/registry.rs +++ b/finance/vault-strategy/quasar/vault-strategy/src/state/registry.rs @@ -1,13 +1,17 @@ use quasar_lang::prelude::*; pub const REGISTRY_SEED: &[u8] = b"registry"; -pub const WHITELIST_SEED: &[u8] = b"whitelist"; +pub const APPROVED_ASSET_SEED: &[u8] = b"approved_asset"; -/// A curated set of assets that strategies may hold, maintained by a protocol -/// authority that is deliberately not the strategy manager: the authority vets -/// which real assets (and which official price feed) are safe, and the manager -/// only chooses among them. This is what stops a manager from listing a token -/// they mint themselves, or pairing a real mint with a feed they control. +/// The curator record at the root of an approved-asset set. This account holds +/// no list: it only names the authority allowed to approve assets under it. The +/// set itself is the collection of `ApprovedAsset` accounts derived from this +/// registry, and an asset counts as approved exactly when its account exists. +/// +/// The authority is deliberately not a strategy manager: the curator vets which +/// real assets (and which official price feed) are safe, and a manager only +/// chooses among them. This is what stops a manager from listing a token they +/// mint themselves, or pairing a real mint with a feed they control. /// /// PDA: `["registry", authority]`. #[account(discriminator = 1, set_inner)] @@ -21,10 +25,10 @@ pub struct Registry { /// registry authority; `add_asset` copies `price_feed` from here so the manager /// never supplies the feed. /// -/// PDA: `["whitelist", registry, mint]`. +/// PDA: `["approved_asset", registry, mint]`. #[account(discriminator = 2, set_inner)] -#[seeds(b"whitelist", registry: Address, mint: Address)] -pub struct WhitelistEntry { +#[seeds(b"approved_asset", registry: Address, mint: Address)] +pub struct ApprovedAsset { pub registry: Address, pub mint: Address, pub price_feed: Address, diff --git a/finance/vault-strategy/quasar/vault-strategy/src/state/strategy.rs b/finance/vault-strategy/quasar/vault-strategy/src/state/strategy.rs index c428e122..bad6ac6a 100644 --- a/finance/vault-strategy/quasar/vault-strategy/src/state/strategy.rs +++ b/finance/vault-strategy/quasar/vault-strategy/src/state/strategy.rs @@ -41,7 +41,7 @@ pub struct AssetConfig { pub strategy: Address, pub index: u8, pub mint: Address, - /// Price feed account, copied from the registry whitelist entry at add time + /// Price feed account, copied from the registry's ApprovedAsset at add time /// so the manager cannot substitute a feed they control. pub price_feed: Address, /// Strategy-owned token account holding this asset. diff --git a/finance/vault-strategy/quasar/vault-strategy/src/tests.rs b/finance/vault-strategy/quasar/vault-strategy/src/tests.rs index 7795d1bc..e40da13d 100644 --- a/finance/vault-strategy/quasar/vault-strategy/src/tests.rs +++ b/finance/vault-strategy/quasar/vault-strategy/src/tests.rs @@ -1,5 +1,5 @@ //! QuasarSVM integration tests. `test_strategy_setup` drives the manager-side -//! setup (registry, whitelist, strategy, add asset) and asserts state. +//! setup (registry, approve asset, strategy, add asset) and asserts state. //! `test_deposit` is a two-program test: it loads the mock swap router too, //! wires up rates and a Pyth-shaped price feed, and deposits, checking that the //! deposit is priced 1:1 on the first deposit and deployed into the basket @@ -16,7 +16,7 @@ use { std::println, }; -use crate::state::{ASSET_CONFIG_SEED, REGISTRY_SEED, STRATEGY_SEED, WHITELIST_SEED}; +use crate::state::{APPROVED_ASSET_SEED, ASSET_CONFIG_SEED, REGISTRY_SEED, STRATEGY_SEED}; const DECIMALS: u8 = 6; const FEE_BPS: u16 = 100; @@ -102,9 +102,9 @@ fn pyth_feed_account(address: Pubkey, price: i64, publish_time: i64) -> Account fn registry_pda(authority: &Pubkey) -> Pubkey { Pubkey::find_program_address(&[REGISTRY_SEED, authority.as_ref()], &program_id()).0 } -fn whitelist_pda(registry: &Pubkey, mint: &Pubkey) -> Pubkey { +fn approved_asset_pda(registry: &Pubkey, mint: &Pubkey) -> Pubkey { Pubkey::find_program_address( - &[WHITELIST_SEED, registry.as_ref(), mint.as_ref()], + &[APPROVED_ASSET_SEED, registry.as_ref(), mint.as_ref()], &program_id(), ) .0 @@ -150,7 +150,7 @@ fn router_rate_pda(mint: &Pubkey) -> Pubkey { fn init_registry_data() -> Vec { vec![0u8] } -fn whitelist_data(price_feed: &Pubkey) -> Vec { +fn approve_asset_data(price_feed: &Pubkey) -> Vec { let mut data = vec![1u8]; data.extend_from_slice(price_feed.as_ref()); data @@ -198,11 +198,11 @@ fn init_registry_ix(authority: Pubkey, registry: Pubkey) -> Instruction { } } -fn whitelist_ix( +fn approve_asset_ix( authority: Pubkey, registry: Pubkey, asset_mint: Pubkey, - whitelist: Pubkey, + approved_asset: Pubkey, price_feed: Pubkey, ) -> Instruction { Instruction { @@ -211,11 +211,11 @@ fn whitelist_ix( AccountMeta::new(authority, true), AccountMeta::new_readonly(registry, false), AccountMeta::new_readonly(asset_mint, false), - AccountMeta::new(whitelist, false), + AccountMeta::new(approved_asset, false), AccountMeta::new_readonly(rent_id(), false), AccountMeta::new_readonly(system_program_id(), false), ], - data: whitelist_data(&price_feed), + data: approve_asset_data(&price_feed), } } @@ -253,7 +253,7 @@ fn add_asset_ix( strategy: Pubkey, registry: Pubkey, asset_mint: Pubkey, - whitelist: Pubkey, + approved_asset: Pubkey, asset_config: Pubkey, vault_asset: Pubkey, weight_bps: u16, @@ -265,7 +265,7 @@ fn add_asset_ix( AccountMeta::new(strategy, false), AccountMeta::new_readonly(registry, false), AccountMeta::new_readonly(asset_mint, false), - AccountMeta::new_readonly(whitelist, false), + AccountMeta::new_readonly(approved_asset, false), AccountMeta::new(asset_config, false), AccountMeta::new(vault_asset, false), AccountMeta::new_readonly(rent_id(), false), @@ -289,7 +289,10 @@ fn read_u16(data: &[u8], offset: usize) -> u16 { #[test] fn test_strategy_setup() { let mut svm = QuasarSvm::new() - .with_program(&program_id(), &std::fs::read("target/deploy/quasar_vault_strategy.so").unwrap()) + .with_program( + &program_id(), + &std::fs::read("target/deploy/quasar_vault_strategy.so").unwrap(), + ) .with_token_program(); let authority = Pubkey::new_unique(); @@ -300,7 +303,7 @@ fn test_strategy_setup() { let swap_router = router_id(); let registry = registry_pda(&authority); - let whitelist = whitelist_pda(®istry, &asset_mint); + let approved_asset = approved_asset_pda(®istry, &asset_mint); let index = 0u64; let strategy = strategy_pda(index); let share_mint = share_mint_pda(&strategy); @@ -314,7 +317,7 @@ fn test_strategy_setup() { mint_account(usdc_mint, authority), mint_account(asset_mint, authority), empty_account(registry), - empty_account(whitelist), + empty_account(approved_asset), empty_account(strategy), empty_account(share_mint), empty_account(vault_usdc), @@ -324,22 +327,52 @@ fn test_strategy_setup() { let instructions = vec![ init_registry_ix(authority, registry), - whitelist_ix(authority, registry, asset_mint, whitelist, price_feed), - init_strategy_ix(manager, usdc_mint, registry, strategy, share_mint, vault_usdc, index, swap_router), - add_asset_ix(manager, strategy, registry, asset_mint, whitelist, asset_config, vault_asset, 10_000), + approve_asset_ix(authority, registry, asset_mint, approved_asset, price_feed), + init_strategy_ix( + manager, + usdc_mint, + registry, + strategy, + share_mint, + vault_usdc, + index, + swap_router, + ), + add_asset_ix( + manager, + strategy, + registry, + asset_mint, + approved_asset, + asset_config, + vault_asset, + 10_000, + ), ]; let result = svm.process_instruction_chain(&instructions, &accounts); - assert!(result.is_ok(), "setup chain failed: {:?}", result.raw_result); + assert!( + result.is_ok(), + "setup chain failed: {:?}", + result.raw_result + ); let strategy_data = &result.account(&strategy).unwrap().data; assert_eq!(strategy_data[0], 3, "strategy discriminator"); assert_eq!(strategy_data[STRATEGY_ASSET_COUNT_OFFSET], 1, "asset_count"); - assert_eq!(read_u16(strategy_data, STRATEGY_TOTAL_WEIGHT_OFFSET), 10_000, "total_weight_bps"); + assert_eq!( + read_u16(strategy_data, STRATEGY_TOTAL_WEIGHT_OFFSET), + 10_000, + "total_weight_bps" + ); let asset_config_data = &result.account(&asset_config).unwrap().data; assert_eq!(asset_config_data[0], 4, "asset_config discriminator"); - assert_eq!(read_u16(asset_config_data, ASSET_CONFIG_WEIGHT_OFFSET), 10_000, "weight_bps"); + assert_eq!( + read_u16(asset_config_data, ASSET_CONFIG_WEIGHT_OFFSET), + 10_000, + "weight_bps" + ); println!(" STRATEGY SETUP CU: {}", result.compute_units_consumed); } @@ -350,8 +383,14 @@ fn test_strategy_setup() { #[test] fn test_deposit() { let mut svm = QuasarSvm::new() - .with_program(&program_id(), &std::fs::read("target/deploy/quasar_vault_strategy.so").unwrap()) - .with_program(&router_id(), &std::fs::read("../mock-swap-router/target/deploy/quasar_mock_swap_router.so").unwrap()) + .with_program( + &program_id(), + &std::fs::read("target/deploy/quasar_vault_strategy.so").unwrap(), + ) + .with_program( + &router_id(), + &std::fs::read("../mock-swap-router/target/deploy/quasar_mock_swap_router.so").unwrap(), + ) .with_token_program(); svm.warp_to_timestamp(NOW); @@ -363,7 +402,7 @@ fn test_deposit() { let price_feed = Pubkey::new_unique(); let registry = registry_pda(&authority); - let whitelist = whitelist_pda(®istry, &asset_mint); + let approved_asset = approved_asset_pda(®istry, &asset_mint); let index = 0u64; let strategy = strategy_pda(index); let share_mint = share_mint_pda(&strategy); @@ -400,7 +439,7 @@ fn test_deposit() { empty_account(r_rate), // Vault-strategy accounts. empty_account(registry), - empty_account(whitelist), + empty_account(approved_asset), empty_account(strategy), empty_account(share_mint), empty_account(vault_usdc), @@ -481,19 +520,47 @@ fn test_deposit() { router_init, router_set_rate, init_registry_ix(authority, registry), - whitelist_ix(authority, registry, asset_mint, whitelist, price_feed), - init_strategy_ix(manager, usdc_mint, registry, strategy, share_mint, vault_usdc, index, router_id()), - add_asset_ix(manager, strategy, registry, asset_mint, whitelist, asset_config, vault_asset, 10_000), + approve_asset_ix(authority, registry, asset_mint, approved_asset, price_feed), + init_strategy_ix( + manager, + usdc_mint, + registry, + strategy, + share_mint, + vault_usdc, + index, + router_id(), + ), + add_asset_ix( + manager, + strategy, + registry, + asset_mint, + approved_asset, + asset_config, + vault_asset, + 10_000, + ), deposit_ix, ]; let result = svm.process_instruction_chain(&instructions, &accounts); - assert!(result.is_ok(), "deposit chain failed: {:?}", result.raw_result); + assert!( + result.is_ok(), + "deposit chain failed: {:?}", + result.raw_result + ); // First deposit mints shares 1:1 with USDC. - assert_eq!(token_amount(result.account(&depositor_share).unwrap()), DEPOSIT); + assert_eq!( + token_amount(result.account(&depositor_share).unwrap()), + DEPOSIT + ); // The deposit was deployed into the asset via the router. - assert_eq!(token_amount(result.account(&vault_asset).unwrap()), ASSET_OUT); + assert_eq!( + token_amount(result.account(&vault_asset).unwrap()), + ASSET_OUT + ); assert_eq!(token_amount(result.account(&depositor_usdc).unwrap()), 0); assert_eq!(token_amount(result.account(&r_treasury).unwrap()), DEPOSIT); // All USDC was swapped out of the vault into the asset.