vault-strategy: index-seeded strategies, auto-deploying deposits, and set_weight#82
Merged
Merged
Conversation
The Strategy PDA was derived from seeds "strategy" + manager pubkey. Switch to a simpler caller-chosen index, e.g. "strategy" + 0, so strategies are addressed by a counter rather than the manager's key. - Add `index: u64` to the Strategy account, set at creation and used to re-derive the PDA wherever it signs for the vaults and share mint. - `initialize_strategy` now takes an `index` argument used in the PDA seeds. - deposit, invest, rebalance, collect_fees, withdraw, add_asset re-derive the strategy PDA from the stored index; the manager is still recorded as a field and keeps its manager-only powers, it is just no longer part of the address. - Update tests, README, and the video script to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RajngzX57RGaQx5sKPbysZ
Keep VIDEO_SCRIPT.md as-is on this branch; the index-seed program change stands on its own. The updated script is delivered separately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RajngzX57RGaQx5sKPbysZ
deposit now invests each depositor's USDC into the basket at its target weights in the same transaction, routing each weight-sized slice through the registered swap router with the same oracle-computed slippage floor invest uses. The USDC vault no longer holds idle deposits; only the unallocated remainder (when the weights sum below 10000) stays as USDC. Its remaining_accounts are now [asset_config, vault, mint, rate, price_feed] per asset, plus the router accounts. Add set_weight, a manager-only instruction that changes an asset's target weight, including setting it to zero to retire it. The asset's index is preserved so the contiguous 0..asset_count range the valuation handlers depend on stays intact; the manager sells the retired asset's holdings out with rebalance. Rework the tests around the new behavior: a full-lifecycle test (deposit and auto-deploy, a price move, a rebalance back to target, a second depositor priced at the new NAV, a year's fee, in-kind withdrawal), focused per-handler tests, set_weight retire and rejection paths, and oracle-bounded slippage on both deposit and invest. Also fixes two InitializeStrategy constructions left without the index field. 20 tests pass under cargo build-sbf + cargo test. Update README and CHANGELOG to match.
A strategy now accepts deposits only once its target weights sum to exactly 10,000 bps. deposit reverts with StrategyNotFullyAllocated otherwise, so a strategy is either still being configured or fully allocated and live, and every accepted deposit is fully invested (bar sub-cent rounding dust). There is no idle-cash mode. add_asset and set_weight keep the <= 10,000 cap, so the manager can still pass through intermediate states while reconfiguring. Remove invest. Once deposits fully deploy and rebalance covers every asset-to-asset move (including liquidating a retired asset), a "deploy idle USDC" handler has no honest job and contradicts the full-allocation invariant. The router CPI stays; deposit and rebalance still use it. Tests: add test_deposit_rejects_underallocated and the retire-then-reallocate path in test_set_weight_retire; move router-mismatch coverage onto deposit; drop the invest tests. 19 tests pass under cargo build-sbf + cargo test. Update README and CHANGELOG.
MAX_ASSETS is bounded by how many accounts deposit and withdraw pull into one instruction, not by storage. Spell out the arithmetic (deposit 14 + 5N accounts, withdraw 10 + 4N) and note that at the cap of 8, deposit's 54 accounts stay within the 128-account transaction lock limit but exceed the 1232-byte legacy transaction size beyond ~3 assets, so large baskets require a v0 transaction with an Address Lookup Table. Comment plus a README note; no behavior change.
Bump the basket cap from 8 to 16 (used only in add_asset's TooManyAssets guard; assets are separate PDAs, so no layout impact). Update the worked account-count numbers in the MAX_ASSETS comment and README: at 16 assets deposit references 94 accounts and withdraw 74, still within the 128-account transaction lock limit but past the 1232-byte legacy size, so large baskets need a v0 transaction with an Address Lookup Table. Add test_add_asset_enforces_max_assets: fill a strategy to 16 assets (625 bps each) and assert the 17th reverts with TooManyAssets. 20 tests pass.
Apply rustfmt to the deposit handler and the tests; the CI Rustfmt check runs `cargo fmt --check`. No behavior change.
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.
Summary
Reworks the
vault-strategyexample with three changes plus a cleanup:set_weightinstruction lets the manager retune or retire an asset.investhandler is removed.Key Changes
["strategy", index](au64chosen at init) instead of["strategy", manager_pubkey], so a manager can run more than one strategy. The manager is still stored on theStrategyaccount and used forhas_oneauthorization.depositvalues every asset for NAV, mints shares, then swaps each depositor's USDC into the basket at its target weights through the registered router, in one transaction, under an oracle-computed slippage floor. Itsremaining_accountsare now[asset_config, vault, mint, rate, price_feed]per asset, plus the router accounts. No USDC is left sitting idle.depositreverts withStrategyNotFullyAllocatedotherwise.add_assetandset_weightkeep the<= 10,000cap so the manager can pass through intermediate states while configuring.set_weight(new, manager-only): changes an asset's target weight, including to zero to retire it. The asset's index is preserved, so the contiguous0..asset_countrange the valuation handlers depend on stays intact. Retiring an asset then means reassigning its weight to another asset to return to 100% and selling the holdings out withrebalance.invest: once deposits fully deploy andrebalancecovers asset-to-asset moves (including liquidating a retired asset), a "deploy idle USDC" handler has no honest role and contradicts the full-allocation invariant.MAX_ASSETSraised from 8 to 16.README.mdandCHANGELOG.mdupdated to match. (VIDEO_SCRIPT.mdis intentionally not part of this PR.)Implementation Details
indexis stored on the account and used (asto_le_bytes()) to re-derive the PDA for signer seeds acrossdeposit,collect_fees,rebalance,withdraw, andadd_asset.depositdeploysusdc_amount * weight_bps / 10000per asset viamock_swap_router::cpi::swap_usdc_for_assetwith the strategy PDA signing; retired assets (weight 0) are skipped, and only sub-cent rounding dust can remain as USDC.deposituses14 + 5Naccounts andwithdraw10 + 4N(N = asset count). AtMAX_ASSETS = 16that is 94 accounts fordeposit, within Solana's 128-account transaction lock limit but past the 1232-byte legacy transaction size (~3 assets), so a large basket needs a v0 transaction with an Address Lookup Table (documented in the README and theMAX_ASSETScomment).cargo build-sbf+cargo test(LiteSVM), 20 tests: the full lifecycle (deposit + auto-deploy, a price move, rebalance back to target, a second depositor priced at the new NAV, a year's fee, in-kind withdrawal), retire-then-reallocate, theMAX_ASSETSboundary (fill to 16, reject the 17th), and the rejection paths (under-allocated strategy, non-managerset_weight, unregistered router, incomplete asset accounts, oracle-bounded slippage, over-cap fee/slippage, non-whitelisted asset, weight overflow).Note on large baskets: the suite exercises deposits with 2 assets over legacy transactions. A deposit into a near-cap basket must be sent as a v0 transaction with an Address Lookup Table and runs ~N router swap CPIs, whose compute-unit cost is not yet covered by a test.
https://claude.ai/code/session_01RajngzX57RGaQx5sKPbysZ
🤖 Generated with Claude Code