Skip to content

Commit a7dc393

Browse files
committed
Address vault Strategy PDA by index instead of manager key
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
1 parent 0d1c060 commit a7dc393

12 files changed

Lines changed: 47 additions & 25 deletions

File tree

finance/vault-strategy/VIDEO_SCRIPT.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Custody is the whole game, so let us name the boxes before we move money.
2929

3030
First, the word vault, because it gets overloaded. By the common standard a vault holds a single asset: you put one kind of token in, you get shares out. A managed mix of several assets is not one vault; it lives in several vaults, one per asset, and is usually called a basket or a fund. Symmetry calls its multi-asset products baskets. We will keep it simple: a vault is one single-asset token account, and the strategy is the whole construct that owns them. So vault strategy reads literally, a strategy built from vaults.
3131

32-
The center of everything is the `Strategy` account, whose address is a PDA derived from the seeds `"strategy"` plus Maria's public key. A PDA is an address with no private key: it is found deliberately off the signing curve, so no key can sign for it and only the program can, by supplying the seeds. The strategy PDA is the authority over the USDC vault, every asset vault, and the share mint. Each vault is an associated token account owned by the strategy PDA and holds exactly one asset. The share mint's address is also a PDA, seeds `"share_mint"` plus the strategy address, so it is deterministic, one share mint per strategy, with the strategy PDA as its mint authority.
32+
The center of everything is the `Strategy` account, whose address is a PDA derived from the seeds `"strategy"` plus an index: zero for the first strategy, one for the next, and so on. A PDA is an address with no private key: it is found deliberately off the signing curve, so no key can sign for it and only the program can, by supplying the seeds. The strategy PDA is the authority over the USDC vault, every asset vault, and the share mint. Each vault is an associated token account owned by the strategy PDA and holds exactly one asset. The share mint's address is also a PDA, seeds `"share_mint"` plus the strategy address, so it is deterministic, one share mint per strategy, with the strategy PDA as its mint authority.
3333

3434
The asset set is not fixed. Each asset the strategy holds gets its own small account, an `AssetConfig`, whose address is a PDA seeded by the strategy and an index: zero, one, two, and so on. That indexing matters later: the assets are exactly the range zero up to the count, so any handler that values the whole strategy can re-derive every one and refuse to run if a single asset account is missing.
3535

@@ -39,7 +39,7 @@ ON SCREEN:
3939

4040
```
4141
Registry [off curve - PDA, seeds: "registry" + authority] owner = curator, not a manager
42-
Strategy [off curve - PDA, seeds: "strategy" + manager]
42+
Strategy [off curve - PDA, seeds: "strategy" + index] manager stored as a field, not a seed
4343
authority over: vault_usdc, every asset vault, share_mint
4444
share_mint [off curve - PDA, seeds: "share_mint" + strategy] authority = Strategy PDA
4545
AssetConfig #i [off curve - PDA, seeds: "asset" + strategy + index] one per asset
@@ -72,15 +72,15 @@ Fee generated: none
7272

7373
NARRATION:
7474

75-
Maria is our portfolio manager, and she wants to run the basket and earn the fee. She calls `initialize_strategy`, binding her strategy to Victor's registry. She sets two numbers and no assets yet: a fee of one hundred basis points, which is one percent a year, and a maximum slippage of one hundred basis points, which we will use when she trades. Both are capped in code, the fee at ten percent and the slippage tolerance at ten percent, and both are fixed here at creation with no setter to change them later.
75+
Maria is our portfolio manager, and she wants to run the basket and earn the fee. She calls `initialize_strategy`, picking index zero, which fixes her strategy's address at the seeds `"strategy"` plus zero, and binding it to Victor's registry. Her own key is still recorded on the strategy as the manager, the account that holds her management powers; it is just no longer part of the address. She sets two numbers and no assets yet: a fee of one hundred basis points, which is one percent a year, and a maximum slippage of one hundred basis points, which we will use when she trades. Both are capped in code, the fee at ten percent and the slippage tolerance at ten percent, and both are fixed here at creation with no setter to change them later.
7676

7777
The fee cap exists because the fee is paid by minting new shares to the manager; an uncapped fee would let a manager dilute depositors to nothing by configuration alone.
7878

7979
ON SCREEN:
8080

8181
```
82-
ADDED - Strategy [off curve - PDA]
83-
manager: Maria registry: Victor's registry
82+
ADDED - Strategy [off curve - PDA, seeds: "strategy" + 0]
83+
index: 0 manager: Maria registry: Victor's registry
8484
fee_bps: 100 max_slippage_bps: 100 total_shares: 0
8585
asset_count: 0 total_weight_bps: 0 last_fee_accrual_timestamp: now
8686

finance/vault-strategy/anchor/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ An [in-kind distribution](https://www.investopedia.com/terms/i/in-kind.asp) retu
7878

7979
### Step 2 - Maria initializes the strategy
8080

81-
`initialize_strategy(fee_bps=100, max_slippage_bps=100, swap_router)` creates the `Strategy` PDA (`["strategy", maria]`), the share mint, and the USDC vault, binding the strategy to Victor's registry. No assets yet.
81+
`initialize_strategy(index=0, fee_bps=100, max_slippage_bps=100, swap_router)` creates the `Strategy` PDA (`["strategy", 0]`), the share mint, and the USDC vault, binding the strategy to Victor's registry. The strategy is addressed by a caller-chosen index (`"strategy" + 0`, `"strategy" + 1`, …) rather than the manager's key. No assets yet.
8282

8383
### Step 3 - Maria adds assets
8484

finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/add_asset.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pub struct AddAssetAccountConstraints<'info> {
1616
mut,
1717
has_one = manager,
1818
has_one = registry @ VaultError::InvalidRegistry,
19-
seeds = [b"strategy", strategy.manager.as_ref()],
19+
seeds = [b"strategy", strategy.index.to_le_bytes().as_ref()],
2020
bump = strategy.bump
2121
)]
2222
pub strategy: Box<Account<'info, Strategy>>,

finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/collect_fees.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub struct CollectFeesAccountConstraints<'info> {
1717
#[account(
1818
mut,
1919
has_one = manager,
20-
seeds = [b"strategy", strategy.manager.as_ref()],
20+
seeds = [b"strategy", strategy.index.to_le_bytes().as_ref()],
2121
bump = strategy.bump
2222
)]
2323
pub strategy: Account<'info, Strategy>,
@@ -57,7 +57,7 @@ pub fn handle_collect_fees(context: Context<CollectFeesAccountConstraints>) -> R
5757
let elapsed_seconds = (current_ts - last_ts) as u64;
5858
let total_shares = context.accounts.strategy.total_shares;
5959
let fee_bps = context.accounts.strategy.fee_bps;
60-
let manager_key = context.accounts.strategy.manager;
60+
let strategy_index = context.accounts.strategy.index;
6161
let strategy_bump = context.accounts.strategy.bump;
6262

6363
// fee_shares = total_shares * fee_bps * elapsed / (10_000 * SECONDS_PER_YEAR)
@@ -86,7 +86,8 @@ pub fn handle_collect_fees(context: Context<CollectFeesAccountConstraints>) -> R
8686
.ok_or(VaultError::MathOverflow)?;
8787

8888
// Mint fee shares to manager - strategy PDA signs
89-
let signer_seeds: &[&[&[u8]]] = &[&[b"strategy", manager_key.as_ref(), &[strategy_bump]]];
89+
let index_bytes = strategy_index.to_le_bytes();
90+
let signer_seeds: &[&[&[u8]]] = &[&[b"strategy", index_bytes.as_ref(), &[strategy_bump]]];
9091

9192
let mint_accounts = MintTo {
9293
mint: context.accounts.share_mint.to_account_info(),

finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/deposit.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub struct DepositAccountConstraints<'info> {
1818
#[account(
1919
mut,
2020
has_one = usdc_mint @ VaultError::InvalidUsdcMint,
21-
seeds = [b"strategy", strategy.manager.as_ref()],
21+
seeds = [b"strategy", strategy.index.to_le_bytes().as_ref()],
2222
bump = strategy.bump
2323
)]
2424
pub strategy: Box<Account<'info, Strategy>>,
@@ -74,7 +74,7 @@ pub fn handle_deposit<'info>(
7474
let vault_usdc_amount = context.accounts.vault_usdc.amount;
7575
let total_shares = context.accounts.strategy.total_shares;
7676
let usdc_decimals = context.accounts.usdc_mint.decimals;
77-
let manager_key = context.accounts.strategy.manager;
77+
let strategy_index = context.accounts.strategy.index;
7878
let strategy_bump = context.accounts.strategy.bump;
7979
let strategy_key = context.accounts.strategy.key();
8080
let asset_count = context.accounts.strategy.asset_count as usize;
@@ -146,7 +146,8 @@ pub fn handle_deposit<'info>(
146146
let cpi_ctx = CpiContext::new(context.accounts.token_program.key(), transfer_accounts);
147147
transfer_checked(cpi_ctx, usdc_amount, usdc_decimals)?;
148148

149-
let signer_seeds: &[&[&[u8]]] = &[&[b"strategy", manager_key.as_ref(), &[strategy_bump]]];
149+
let index_bytes = strategy_index.to_le_bytes();
150+
let signer_seeds: &[&[&[u8]]] = &[&[b"strategy", index_bytes.as_ref(), &[strategy_bump]]];
150151
let mint_accounts = MintTo {
151152
mint: context.accounts.share_mint.to_account_info(),
152153
to: context.accounts.depositor_share_account.to_account_info(),

finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/initialize_strategy.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub const MAX_FEE_BPS: u16 = 1_000;
2020
pub const MAX_SLIPPAGE_BPS: u16 = 1_000;
2121

2222
#[derive(Accounts)]
23+
#[instruction(index: u64)]
2324
pub struct InitializeStrategyAccountConstraints<'info> {
2425
#[account(mut)]
2526
pub manager: Signer<'info>,
@@ -33,7 +34,7 @@ pub struct InitializeStrategyAccountConstraints<'info> {
3334
init,
3435
payer = manager,
3536
space = Strategy::DISCRIMINATOR.len() + Strategy::INIT_SPACE,
36-
seeds = [b"strategy", manager.key().as_ref()],
37+
seeds = [b"strategy", index.to_le_bytes().as_ref()],
3738
bump
3839
)]
3940
pub strategy: Box<Account<'info, Strategy>>,
@@ -67,6 +68,7 @@ pub struct InitializeStrategyAccountConstraints<'info> {
6768

6869
pub fn handle_initialize_strategy(
6970
context: Context<InitializeStrategyAccountConstraints>,
71+
index: u64,
7072
fee_bps: u16,
7173
max_slippage_bps: u16,
7274
swap_router: Pubkey,
@@ -80,6 +82,7 @@ pub fn handle_initialize_strategy(
8082
let clock = Clock::get()?;
8183

8284
context.accounts.strategy.set_inner(Strategy {
85+
index,
8386
manager: context.accounts.manager.key(),
8487
registry: context.accounts.registry.key(),
8588
share_mint: context.accounts.share_mint.key(),

finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/invest.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub struct InvestAccountConstraints<'info> {
2020
mut,
2121
has_one = manager,
2222
has_one = usdc_mint @ VaultError::InvalidUsdcMint,
23-
seeds = [b"strategy", strategy.manager.as_ref()],
23+
seeds = [b"strategy", strategy.index.to_le_bytes().as_ref()],
2424
bump = strategy.bump
2525
)]
2626
pub strategy: Box<Account<'info, Strategy>>,
@@ -86,7 +86,7 @@ pub struct InvestAccountConstraints<'info> {
8686

8787
pub fn handle_invest(context: Context<InvestAccountConstraints>, usdc_amount: u64) -> Result<()> {
8888
let strategy = &context.accounts.strategy;
89-
let manager_key = strategy.manager;
89+
let strategy_index = strategy.index;
9090
let strategy_bump = strategy.bump;
9191
let max_slippage_bps = strategy.max_slippage_bps;
9292

@@ -113,7 +113,8 @@ pub fn handle_invest(context: Context<InvestAccountConstraints>, usdc_amount: u6
113113
.try_into()
114114
.map_err(|_| VaultError::MathOverflow)?;
115115

116-
let signer_seeds: &[&[&[u8]]] = &[&[b"strategy", manager_key.as_ref(), &[strategy_bump]]];
116+
let index_bytes = strategy_index.to_le_bytes();
117+
let signer_seeds: &[&[&[u8]]] = &[&[b"strategy", index_bytes.as_ref(), &[strategy_bump]]];
117118

118119
let cpi_accounts = RouterSwapAccounts {
119120
caller: context.accounts.strategy.to_account_info(),

finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/rebalance.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub struct RebalanceAccountConstraints<'info> {
2020
mut,
2121
has_one = manager,
2222
has_one = usdc_mint @ VaultError::InvalidUsdcMint,
23-
seeds = [b"strategy", strategy.manager.as_ref()],
23+
seeds = [b"strategy", strategy.index.to_le_bytes().as_ref()],
2424
bump = strategy.bump
2525
)]
2626
pub strategy: Box<Account<'info, Strategy>>,
@@ -116,7 +116,7 @@ pub fn handle_rebalance(
116116
);
117117

118118
let strategy = &context.accounts.strategy;
119-
let manager_key = strategy.manager;
119+
let strategy_index = strategy.index;
120120
let strategy_bump = strategy.bump;
121121
let slip = (10_000 - strategy.max_slippage_bps) as u128;
122122

@@ -160,7 +160,8 @@ pub fn handle_rebalance(
160160
.try_into()
161161
.map_err(|_| VaultError::MathOverflow)?;
162162

163-
let signer_seeds: &[&[&[u8]]] = &[&[b"strategy", manager_key.as_ref(), &[strategy_bump]]];
163+
let index_bytes = strategy_index.to_le_bytes();
164+
let signer_seeds: &[&[&[u8]]] = &[&[b"strategy", index_bytes.as_ref(), &[strategy_bump]]];
164165

165166
// Step 1: sell basket token -> USDC
166167
let sell_cpi_accounts = RouterSellAccounts {

finance/vault-strategy/anchor/programs/vault-strategy/src/instructions/withdraw.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub struct WithdrawAccountConstraints<'info> {
1818
#[account(
1919
mut,
2020
has_one = usdc_mint @ VaultError::InvalidUsdcMint,
21-
seeds = [b"strategy", strategy.manager.as_ref()],
21+
seeds = [b"strategy", strategy.index.to_le_bytes().as_ref()],
2222
bump = strategy.bump
2323
)]
2424
pub strategy: Box<Account<'info, Strategy>>,
@@ -77,7 +77,7 @@ pub fn handle_withdraw<'info>(
7777

7878
let vault_usdc_amount = context.accounts.vault_usdc.amount;
7979
let usdc_decimals = context.accounts.usdc_mint.decimals;
80-
let manager_key = context.accounts.strategy.manager;
80+
let strategy_index = context.accounts.strategy.index;
8181
let strategy_bump = context.accounts.strategy.bump;
8282
let strategy_key = context.accounts.strategy.key();
8383
let user_key = context.accounts.user.key();
@@ -104,7 +104,8 @@ pub fn handle_withdraw<'info>(
104104
.checked_sub(shares_to_burn)
105105
.ok_or(VaultError::MathOverflow)?;
106106

107-
let signer_seeds: &[&[&[u8]]] = &[&[b"strategy", manager_key.as_ref(), &[strategy_bump]]];
107+
let index_bytes = strategy_index.to_le_bytes();
108+
let signer_seeds: &[&[&[u8]]] = &[&[b"strategy", index_bytes.as_ref(), &[strategy_bump]]];
108109

109110
// Hoist owned account-info handles for every CPI up front, so the asset loop
110111
// can borrow remaining_accounts without also re-borrowing `context.accounts`

finance/vault-strategy/anchor/programs/vault-strategy/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,18 @@ pub mod vault_strategy {
2929
instructions::whitelist_asset::handle_whitelist_asset(context, price_feed)
3030
}
3131

32+
/// Open a strategy at a caller-chosen index, e.g. index 0 derives the PDA
33+
/// from seeds `"strategy" + 0`. Manager pays and becomes the strategy's manager.
3234
pub fn initialize_strategy(
3335
context: Context<InitializeStrategyAccountConstraints>,
36+
index: u64,
3437
fee_bps: u16,
3538
max_slippage_bps: u16,
3639
swap_router: Pubkey,
3740
) -> Result<()> {
3841
instructions::initialize_strategy::handle_initialize_strategy(
3942
context,
43+
index,
4044
fee_bps,
4145
max_slippage_bps,
4246
swap_router,

0 commit comments

Comments
 (0)