|
| 1 | +# Vault Strategy, Quasar port |
| 2 | + |
| 3 | +A tokenized multi-asset vault. A manager assembles a basket of whitelisted |
| 4 | +assets at target weights; anyone can deposit USDC and receive shares priced at |
| 5 | +the vault's net asset value, and each deposit is immediately deployed into the |
| 6 | +basket by swapping USDC into every asset at its weight. Withdrawals burn shares |
| 7 | +and redeem a proportional slice of every vault, paid in kind. This is the shape |
| 8 | +of an onchain index fund or a managed ETF. |
| 9 | + |
| 10 | +This is a [Quasar](https://github.com/blueshift-gg/quasar) port of the Anchor |
| 11 | +example in [`../anchor`](../anchor). It contains two programs, each its own |
| 12 | +Quasar project: |
| 13 | + |
| 14 | +- `vault-strategy/` - the vault itself (program ID |
| 15 | + `VLT5W7bqhRN4nCdRpXm8UfHRxZd9EuZGqiSAkGHQfGh`). |
| 16 | +- `mock-swap-router/` - a stand-in constant-rate swap venue the vault trades |
| 17 | + through (program ID `SWPR8Rk3aq3DrDGLdaANq7xCMnXoUFUJWJJmCWxc8Jm`). |
| 18 | + |
| 19 | +Both share the same program IDs as the Anchor build. The mock router mints an |
| 20 | +asset against USDC at an admin-set fixed rate, standing in for a real AMM or |
| 21 | +aggregator so the example is self-contained. |
| 22 | + |
| 23 | +## How it works |
| 24 | + |
| 25 | +A separate protocol authority curates a registry of assets, binding each |
| 26 | +approved mint to its official price feed. This authority is deliberately not the |
| 27 | +strategy manager: it vets which real assets and feeds are safe, and the manager |
| 28 | +only chooses among them, so a manager can never list a token they mint |
| 29 | +themselves or pair a real mint with a feed they control. |
| 30 | + |
| 31 | +- `initialize_registry` opens the registry; `whitelist_asset` approves a mint |
| 32 | + and records its price feed. |
| 33 | +- A manager opens a basket with `initialize_strategy` (choosing a management fee |
| 34 | + and a slippage tolerance), then adds whitelisted assets with `add_asset`, each |
| 35 | + at a target weight in basis points. The weights must sum to 100% before the |
| 36 | + vault accepts deposits, so every deposit is fully invested. `set_weight` |
| 37 | + retunes a weight or retires an asset by setting it to zero. |
| 38 | +- `deposit` prices the incoming USDC against the vault's net asset value (the |
| 39 | + USDC vault plus every asset vault valued at its oracle price), mints shares for |
| 40 | + that fraction of the vault, and deploys the deposit across the basket by |
| 41 | + swapping a weight-sized slice into each asset through the router. The first |
| 42 | + deposit into an empty vault mints shares one-to-one. |
| 43 | +- `withdraw` burns shares and pays out a proportional slice of the USDC vault and |
| 44 | + every asset vault, in kind. |
| 45 | +- `rebalance` lets the manager sell one asset for USDC and buy another with it, |
| 46 | + keeping holdings near their targets as prices drift. Both legs are floored to |
| 47 | + the oracle price so a bad swap route reverts. |
| 48 | +- `collect_fees` accrues the time-based management fee by minting fresh shares to |
| 49 | + the manager, diluting holders at the configured annual rate. |
| 50 | + |
| 51 | +Every swap and rebalance leg is bounded by the registered price feed: the |
| 52 | +program computes the oracle-implied output and rejects any swap that falls short |
| 53 | +by more than the strategy's slippage tolerance. |
| 54 | + |
| 55 | +## Accounts and PDAs |
| 56 | + |
| 57 | +- **Registry** `["registry", authority]` and **WhitelistEntry** |
| 58 | + `["whitelist", registry, mint]` - the curated asset set and each approved |
| 59 | + mint's price feed. |
| 60 | +- **Strategy** `["strategy", index]` - one basket, addressed by a counter. Holds |
| 61 | + the manager, registry, share mint, USDC mint, router, fee, slippage, total |
| 62 | + shares, and running weight sum. The Strategy PDA is the authority of the share |
| 63 | + mint and every vault, so the program signs all mints and payouts. |
| 64 | +- **AssetConfig** `["asset", strategy, index]` - one basket asset (mint, copied |
| 65 | + price feed, vault, target weight). The set is the contiguous range |
| 66 | + `0..asset_count`, so a valuation can re-derive every asset and refuse to |
| 67 | + proceed if one is missing. |
| 68 | +- **Share mint** `["share_mint", strategy]`, **USDC vault** |
| 69 | + `["usdc_vault", strategy]`, and per-asset vaults `["asset_vault", strategy, index]`. |
| 70 | + |
| 71 | +`deposit` and `withdraw` reference every asset at once, so the client passes the |
| 72 | +per-asset accounts as remaining accounts (five per asset for deposit, four for |
| 73 | +withdraw), in index order. |
| 74 | + |
| 75 | +## Safety and custody |
| 76 | + |
| 77 | +- Deposited USDC and every asset sit in program-owned vaults whose authority is |
| 78 | + the Strategy PDA; only the deployed program can move them, and it does so only |
| 79 | + along deposit, withdraw, and rebalance. There is no manager path to withdraw |
| 80 | + holdings, only to trade them within the basket or collect the configured fee. |
| 81 | +- The share supply is updated before any mint or burn (checks-effects- |
| 82 | + interactions), and value computations use u128 intermediates with checked |
| 83 | + arithmetic, flooring in the vault's favour. |
| 84 | +- The management fee is capped (10% per year) and the slippage tolerance is |
| 85 | + capped (10%), so neither can be configured to drain the vault. |
| 86 | +- Price feeds are validated against the address recorded on the asset config and |
| 87 | + rejected if stale or non-positive. |
| 88 | + |
| 89 | +## What the Quasar port does differently |
| 90 | + |
| 91 | +The valuation, fee, and swap-floor math are identical to the Anchor build. The |
| 92 | +differences follow from Quasar's model: |
| 93 | + |
| 94 | +- **The cross-program swap is built by hand.** Anchor generates a typed CPI |
| 95 | + client (`mock_swap_router::cpi::*`); Quasar has no such generation, so each |
| 96 | + swap is a `CpiDynamic` call whose account list and instruction data the vault |
| 97 | + constructs directly. The router's instruction wire format (a one-byte |
| 98 | + discriminator plus two little-endian u64s) is encoded inline. |
| 99 | +- **The oracle and foreign token accounts are read by raw byte offset** through |
| 100 | + `UncheckedAccount` views, the same field layout the Anchor build parses. |
| 101 | +- **Pool vaults are program-derived token accounts** rather than associated |
| 102 | + token accounts, matching the other Quasar finance examples. |
| 103 | +- **The share mint carries no freeze authority** (it is never used); the Anchor |
| 104 | + build sets it to the strategy PDA. |
| 105 | + |
| 106 | +## Building and testing |
| 107 | + |
| 108 | +Requires the [Solana toolchain](https://docs.anza.xyz/cli/install) and the |
| 109 | +[Quasar CLI](https://github.com/blueshift-gg/quasar). Build both programs before |
| 110 | +testing, because the deposit test loads the router's compiled `.so`: |
| 111 | + |
| 112 | +```sh |
| 113 | +cargo install --git https://github.com/blueshift-gg/quasar quasar-cli --locked |
| 114 | +(cd mock-swap-router && quasar build) |
| 115 | +(cd vault-strategy && quasar build) |
| 116 | +(cd mock-swap-router && cargo test) |
| 117 | +(cd vault-strategy && cargo test) |
| 118 | +``` |
| 119 | + |
| 120 | +The router suite (`mock-swap-router/src/tests.rs`) exercises initialize, set-rate, |
| 121 | +and a USDC-for-asset swap. The vault suite (`vault-strategy/src/tests.rs`) drives |
| 122 | +the manager setup (registry, whitelist, strategy, add asset) and a two-program |
| 123 | +deposit that deploys USDC into the basket through the router CPI, asserting share |
| 124 | +minting, vault balances, and treasury flow. |
| 125 | + |
| 126 | +## Extending |
| 127 | + |
| 128 | +- Multiple assets per basket (up to the 16-asset cap) with a v0 transaction plus |
| 129 | + an Address Lookup Table for the larger account list. |
| 130 | +- A real AMM or aggregator in place of the mock router. |
| 131 | +- Deposit and withdraw fees in addition to the time-based management fee. |
| 132 | +- Rebalance automation driven by weight drift beyond a threshold. |
0 commit comments