Skip to content

vault-strategy: index-seeded strategies, auto-deploying deposits, and set_weight#82

Merged
mikemaccana merged 7 commits into
mainfrom
claude/vault-strategy-indexing-jw34h7
Jul 1, 2026
Merged

vault-strategy: index-seeded strategies, auto-deploying deposits, and set_weight#82
mikemaccana merged 7 commits into
mainfrom
claude/vault-strategy-indexing-jw34h7

Conversation

@mikemaccana

@mikemaccana mikemaccana commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

Reworks the vault-strategy example with three changes plus a cleanup:

  • Strategies are addressed by a caller-chosen index instead of the manager's key.
  • Deposits are deployed into the basket at their target weights in the same transaction, and a strategy must be fully allocated (weights sum to 100%) before it accepts deposits.
  • A new set_weight instruction lets the manager retune or retire an asset.
  • The now-redundant invest handler is removed.

Key Changes

  • Strategy PDA seed: ["strategy", index] (a u64 chosen at init) instead of ["strategy", manager_pubkey], so a manager can run more than one strategy. The manager is still stored on the Strategy account and used for has_one authorization.
  • Auto-deploying deposits: deposit values 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. Its remaining_accounts are now [asset_config, vault, mint, rate, price_feed] per asset, plus the router accounts. No USDC is left sitting idle.
  • Full-allocation invariant: a strategy accepts deposits only once its target weights sum to exactly 10,000 bps; deposit reverts with StrategyNotFullyAllocated otherwise. add_asset and set_weight keep the <= 10,000 cap 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 contiguous 0..asset_count range 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 with rebalance.
  • Removed invest: once deposits fully deploy and rebalance covers asset-to-asset moves (including liquidating a retired asset), a "deploy idle USDC" handler has no honest role and contradicts the full-allocation invariant.
  • Basket cap: MAX_ASSETS raised from 8 to 16.
  • Docs: README.md and CHANGELOG.md updated to match. (VIDEO_SCRIPT.md is intentionally not part of this PR.)

Implementation Details

  • The strategy index is stored on the account and used (as to_le_bytes()) to re-derive the PDA for signer seeds across deposit, collect_fees, rebalance, withdraw, and add_asset.
  • deposit deploys usdc_amount * weight_bps / 10000 per asset via mock_swap_router::cpi::swap_usdc_for_asset with the strategy PDA signing; retired assets (weight 0) are skipped, and only sub-cent rounding dust can remain as USDC.
  • Referencing every asset bounds basket size: deposit uses 14 + 5N accounts and withdraw 10 + 4N (N = asset count). At MAX_ASSETS = 16 that is 94 accounts for deposit, 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 the MAX_ASSETS comment).
  • Tested with 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, the MAX_ASSETS boundary (fill to 16, reject the 17th), and the rejection paths (under-allocated strategy, non-manager set_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

claude added 5 commits June 29, 2026 18:34
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.
@mikemaccana mikemaccana changed the title Change Strategy PDA seed from manager key to index vault-strategy: index-seeded strategies, auto-deploying deposits, and set_weight Jul 1, 2026
claude added 2 commits July 1, 2026 20:10
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.
@mikemaccana mikemaccana merged commit 3db1571 into main Jul 1, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants