Skip to main content

acta_maker_sdk/chain/vault/
trading.rs

1use solana_sdk::{
2    instruction::{AccountMeta, Instruction},
3    pubkey::Pubkey,
4};
5
6use super::manager::{VaultIx, VaultManager};
7use super::{VaultChainError, pda};
8use crate::chain::ix::{
9    SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID,
10    derive_associated_token_address as ata, maker_pda_with_program_id,
11};
12use crate::orders::{OrderPreimageArgs, build_physical_order_preimage};
13use crate::quote::market_pda;
14use crate::types::{PositionType, TakerSide, VaultLeg, VaultStagingStep};
15
16/// CPI accounts per hop.
17pub const MAX_VAULT_SWAP_ROUTE_ACCOUNTS: usize = 64;
18/// Instruction account slots across all hops, duplicates included.
19pub const MAX_VAULT_SWAP_PATH_ACCOUNTS: usize = 128;
20/// Hop data bytes across all hops.
21pub const MAX_VAULT_SWAP_ROUTE_DATA: usize = 1_024;
22pub const MAX_VAULT_SWAP_HOPS: usize = 4;
23
24pub(crate) const NO_AMOUNT_OFFSET: u16 = u16::MAX;
25
26pub(crate) const INSTRUCTIONS_SYSVAR_ID: Pubkey =
27    solana_sdk::pubkey!("Sysvar1nstructions1111111111111111111111111");
28
29/// Terms of a vault option trade, as agreed in the RFQ and quote.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct VaultTradeTerms {
32    pub underlying_mint: Pubkey,
33    pub quote_mint: Pubkey,
34    pub expiry_ts: u64,
35    pub is_put: bool,
36    pub side: TakerSide,
37    pub strike: u64,
38    pub quantity: u64,
39    pub price: u64,
40    pub valid_until: u64,
41    pub nonce: u64,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct VaultPositionAddresses {
46    pub market: Pubkey,
47    pub position: Pubkey,
48    pub collateral_mint: Pubkey,
49    pub collateral_token_program: Pubkey,
50    pub collateral_ata: Pubkey,
51    pub config: Pubkey,
52    pub protocol_fee_ata: Pubkey,
53}
54
55/// A position this vault holds, as its market and order id name it.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub struct VaultPositionRef {
58    pub underlying_mint: Pubkey,
59    pub quote_mint: Pubkey,
60    pub expiry_ts: u64,
61    pub is_put: bool,
62    pub order_id: [u8; 32],
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct VaultOpenPosition {
67    pub order_id: [u8; 32],
68    pub addresses: VaultPositionAddresses,
69    pub instruction: Instruction,
70}
71
72#[derive(Debug, Clone, Copy)]
73struct Asset {
74    mint: Pubkey,
75    program: Pubkey,
76    vault_ata: Pubkey,
77}
78
79#[derive(Debug, Clone, Copy)]
80struct MarketLegs {
81    underlying: Asset,
82    quote: Asset,
83}
84
85impl MarketLegs {
86    fn bind(
87        manager: &VaultManager,
88        underlying_mint: Pubkey,
89        quote_mint: Pubkey,
90    ) -> Result<Self, VaultChainError> {
91        let main = Asset {
92            mint: manager.args.main_mint,
93            program: manager.args.main_token_program,
94            vault_ata: manager.addresses.vault_main_ata,
95        };
96        let second = Asset {
97            mint: manager.args.second_mint,
98            program: manager.args.second_token_program,
99            vault_ata: manager.addresses.vault_second_ata,
100        };
101        if main.mint == underlying_mint && second.mint == quote_mint {
102            Ok(Self {
103                underlying: main,
104                quote: second,
105            })
106        } else if main.mint == quote_mint && second.mint == underlying_mint {
107            Ok(Self {
108                underlying: second,
109                quote: main,
110            })
111        } else {
112            Err(VaultChainError::InvalidInput(
113                "market mints must be the vault's main and second mints",
114            ))
115        }
116    }
117
118    const fn collateral(self, is_put: bool) -> Asset {
119        if is_put { self.quote } else { self.underlying }
120    }
121
122    const fn settlement(self, is_put: bool) -> Asset {
123        if is_put { self.underlying } else { self.quote }
124    }
125}
126
127impl VaultTradeTerms {
128    fn validate(&self) -> Result<(), VaultChainError> {
129        if self.expiry_ts == 0
130            || self.strike == 0
131            || self.quantity == 0
132            || self.price == 0
133            || self.valid_until == 0
134        {
135            return Err(VaultChainError::InvalidInput(
136                "vault trade terms require non-zero expiry and economic fields",
137            ));
138        }
139        if self.valid_until > self.expiry_ts {
140            return Err(VaultChainError::InvalidInput(
141                "vault trade quote cannot outlive its market",
142            ));
143        }
144        Ok(())
145    }
146
147    #[must_use]
148    pub fn market(&self, program_id: &Pubkey) -> Pubkey {
149        market_pda(
150            program_id,
151            &self.underlying_mint,
152            &self.quote_mint,
153            self.expiry_ts,
154            self.is_put,
155        )
156    }
157
158    /// Order id binding `maker` and `taker` to these terms.
159    #[must_use]
160    pub fn order_id(&self, program_id: &Pubkey, maker: &Pubkey, taker: &Pubkey) -> [u8; 32] {
161        crate::orders::hash_order_preimage(&build_physical_order_preimage(
162            &OrderPreimageArgs {
163                chain_id: 0,
164                program_id: program_id.to_bytes(),
165                side: self.side,
166                position_type: if self.is_put {
167                    PositionType::CashSecuredPut
168                } else {
169                    PositionType::CoveredCall
170                },
171                market: self.market(program_id).to_bytes(),
172                strike: self.strike,
173                quantity: self.quantity,
174                gross_price: self.price,
175                valid_until: self.valid_until,
176                maker: maker.to_bytes(),
177                taker: taker.to_bytes(),
178                nonce: self.nonce,
179            },
180            self.is_put,
181        ))
182    }
183
184    fn args(&self, order_id: &[u8; 32]) -> Vec<u8> {
185        let mut data = Vec::with_capacity(73);
186        data.push(u8::from(self.is_put));
187        for value in [
188            self.strike,
189            self.quantity,
190            self.price,
191            self.valid_until,
192            self.nonce,
193        ] {
194            data.extend_from_slice(&value.to_le_bytes());
195        }
196        data.extend_from_slice(order_id);
197        data
198    }
199}
200
201/// One CPI into a registered swap venue; transit and scratch accounts must end where they started.
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct VaultSwapHop {
204    pub program: Pubkey,
205    /// Vault token account the hop spends: the input leg ATA, then the previous destination.
206    pub source: Pubkey,
207    /// Vault token account the hop fills: a transit vault ATA, the last hop the output leg.
208    pub destination: Pubkey,
209    pub accounts: Vec<AccountMeta>,
210    pub data: Vec<u8>,
211    /// Not on the first hop: spends exactly what its source received earlier in this swap.
212    pub amount_offset: Option<VaultSwapAmountOffset>,
213}
214
215/// Writes the largest `a` with `floor(a * den / num) == received` as u64 LE at `data[offset..]`.
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub struct VaultSwapAmountOffset {
218    pub offset: u16,
219    pub num: u64,
220    pub den: u64,
221}
222
223/// A vault swap as its instruction carries it.
224#[derive(Debug, Clone, PartialEq, Eq)]
225pub struct VaultSwapPlan {
226    /// Main then second route with their accounts; see [`VaultManager::price_section`].
227    pub price_section: Vec<AccountMeta>,
228    /// Most the input leg may spend; the route may spend less and the rest stays in the leg.
229    pub amount_in: u64,
230    /// Least the output leg must gain. Drift is checked on the actual spend and gain.
231    pub minimum_out: u64,
232    pub valid_until: u64,
233    /// Input leg to output leg, 1..=4 hops.
234    pub hops: Vec<VaultSwapHop>,
235}
236
237#[derive(Debug, Clone, PartialEq, Eq)]
238pub enum VaultSwapKind {
239    Rebalance {
240        second_to_main: bool,
241    },
242    Recovery,
243    Closing,
244    /// Stages SECOND during recovery, with MAIN custody and valuation before the hops.
245    RecoveryStage {
246        asset: VaultStagingAsset,
247        generation: u64,
248        main_price_section: Vec<AccountMeta>,
249    },
250    /// `VaultStagingSwap`; `generation` is `staged_generation + 1` to stage, else the open slot's.
251    Staging {
252        step: VaultStagingStep,
253        leg: VaultLeg,
254        asset: VaultStagingAsset,
255        generation: u64,
256    },
257}
258
259/// A mint with a price route the vault holds on its canonical ATA between staging halves.
260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
261pub struct VaultStagingAsset {
262    pub mint: Pubkey,
263    pub token_program: Pubkey,
264}
265
266impl VaultStagingAsset {
267    #[must_use]
268    pub fn vault_ata(&self, vault: &Pubkey) -> Pubkey {
269        ata(vault, &self.mint, &self.token_program)
270    }
271}
272
273impl VaultSwapKind {
274    pub(crate) const fn fixed_accounts(&self) -> usize {
275        match self {
276            Self::Rebalance { .. } | Self::Staging { .. } | Self::RecoveryStage { .. } => 14,
277            Self::Recovery | Self::Closing => 11,
278        }
279    }
280
281    /// Instruction data before the hops; it ends with `hop_count`.
282    pub(crate) const fn header_len(&self) -> usize {
283        match self {
284            Self::Rebalance { .. } => 27,
285            Self::Recovery | Self::Closing => 26,
286            Self::Staging { .. } | Self::RecoveryStage { .. } => 36,
287        }
288    }
289}
290
291impl VaultManager {
292    fn position_addresses(
293        &self,
294        legs: MarketLegs,
295        terms: &VaultTradeTerms,
296        order_id: &[u8; 32],
297    ) -> VaultPositionAddresses {
298        let program_id = &self.args.program_id;
299        let position = pda::position(program_id, order_id).0;
300        let config = pda::premium_config(program_id, &legs.quote.mint).0;
301        let collateral = legs.collateral(terms.is_put);
302        VaultPositionAddresses {
303            market: terms.market(program_id),
304            position,
305            collateral_mint: collateral.mint,
306            collateral_token_program: collateral.program,
307            collateral_ata: ata(&position, &collateral.mint, &collateral.program),
308            config,
309            protocol_fee_ata: ata(&config, &legs.quote.mint, &legs.quote.program),
310        }
311    }
312
313    fn market_tail(&self, legs: MarketLegs) -> [AccountMeta; 7] {
314        [
315            AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR_ID, false),
316            AccountMeta::new_readonly(legs.underlying.mint, false),
317            AccountMeta::new_readonly(legs.quote.mint, false),
318            AccountMeta::new_readonly(legs.underlying.program, false),
319            AccountMeta::new_readonly(legs.quote.program, false),
320            AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
321            AccountMeta::new_readonly(self.addresses.global_config, false),
322        ]
323    }
324
325    /// `FundPositionForVaultHolder`: funds a held position's settlement leg (call: quote, put: underlying).
326    pub fn fund_holder_position(
327        &self,
328        position: &VaultPositionRef,
329    ) -> Result<Instruction, VaultChainError> {
330        if position.expiry_ts == 0 || position.order_id == [0; 32] {
331            return Err(VaultChainError::InvalidInput(
332                "vault position requires a non-zero expiry and order id",
333            ));
334        }
335        let legs = MarketLegs::bind(self, position.underlying_mint, position.quote_mint)?;
336        let funding = legs.settlement(position.is_put);
337        let program_id = &self.args.program_id;
338        let position_pda = pda::position(program_id, &position.order_id).0;
339        let market = market_pda(
340            program_id,
341            &position.underlying_mint,
342            &position.quote_mint,
343            position.expiry_ts,
344            position.is_put,
345        );
346        let accounts = vec![
347            AccountMeta::new_readonly(self.args.actor, true),
348            AccountMeta::new_readonly(self.addresses.vault, false),
349            AccountMeta::new_readonly(self.addresses.delegate, false),
350            AccountMeta::new(position_pda, false),
351            AccountMeta::new_readonly(market, false),
352            AccountMeta::new(funding.vault_ata, false),
353            AccountMeta::new(ata(&position_pda, &funding.mint, &funding.program), false),
354            AccountMeta::new_readonly(funding.mint, false),
355            AccountMeta::new_readonly(funding.program, false),
356        ];
357        Ok(self.instruction(VaultIx::FundPositionForVaultHolder, accounts, &[]))
358    }
359
360    /// `OpenPositionForVaultWriter`, holder byte 0: this vault writes and sells to `maker_owner`.
361    pub fn open_position_as_writer(
362        &self,
363        keeper: Pubkey,
364        maker_owner: Pubkey,
365        terms: &VaultTradeTerms,
366    ) -> Result<VaultOpenPosition, VaultChainError> {
367        terms.validate()?;
368        require_side(terms.side, TakerSide::Sell)?;
369        let legs = MarketLegs::bind(self, terms.underlying_mint, terms.quote_mint)?;
370        let order_id = terms.order_id(&self.args.program_id, &maker_owner, &self.addresses.vault);
371        let addresses = self.position_addresses(legs, terms, &order_id);
372        let maker = maker_pda_with_program_id(&self.args.program_id, &maker_owner).0;
373        Ok(self.open_as_writer(
374            VaultIx::OpenPositionForVaultWriter,
375            keeper,
376            WriterHolder::Maker(maker),
377            legs,
378            order_id,
379            addresses,
380            terms.args(&order_id),
381        ))
382    }
383
384    /// `OpenPositionForVaultWriter`, holder byte 1: this vault writes what `holder` buys; the
385    /// wallet signs and pays, and the vault delegate's signature on the order id is the consent.
386    pub fn open_position_for_wallet_holder(
387        &self,
388        keeper: Pubkey,
389        holder: Pubkey,
390        terms: &VaultTradeTerms,
391    ) -> Result<VaultOpenPosition, VaultChainError> {
392        terms.validate()?;
393        require_side(terms.side, TakerSide::Buy)?;
394        let legs = MarketLegs::bind(self, terms.underlying_mint, terms.quote_mint)?;
395        let order_id = terms.order_id(&self.args.program_id, &self.addresses.vault, &holder);
396        let addresses = self.position_addresses(legs, terms, &order_id);
397        Ok(self.open_as_writer(
398            VaultIx::OpenPositionForVaultWriter,
399            keeper,
400            WriterHolder::Wallet(holder),
401            legs,
402            order_id,
403            addresses,
404            terms.args(&order_id),
405        ))
406    }
407
408    /// `OpenPositionForVaultHolder`: `taker` writes against this vault's quote; the vault holds.
409    pub fn open_position_as_holder(
410        &self,
411        keeper: Pubkey,
412        taker: Pubkey,
413        terms: &VaultTradeTerms,
414    ) -> Result<VaultOpenPosition, VaultChainError> {
415        terms.validate()?;
416        require_side(terms.side, TakerSide::Sell)?;
417        let legs = MarketLegs::bind(self, terms.underlying_mint, terms.quote_mint)?;
418        let order_id = terms.order_id(&self.args.program_id, &self.addresses.vault, &taker);
419        let addresses = self.position_addresses(legs, terms, &order_id);
420        Ok(self.open_as_holder(
421            VaultIx::OpenPositionForVaultHolder,
422            keeper,
423            taker,
424            legs,
425            order_id,
426            addresses,
427            &terms.args(&order_id),
428        ))
429    }
430
431    /// `OpenPositionBetweenVaults`: this vault writes, `holder` quoted and holds.
432    pub fn open_position_with_vault_holder(
433        &self,
434        keeper: Pubkey,
435        holder: &Self,
436        terms: &VaultTradeTerms,
437    ) -> Result<VaultOpenPosition, VaultChainError> {
438        terms.validate()?;
439        require_side(terms.side, TakerSide::Sell)?;
440        let legs = MarketLegs::bind(self, terms.underlying_mint, terms.quote_mint)?;
441        let holder_legs = MarketLegs::bind(holder, terms.underlying_mint, terms.quote_mint)?;
442        if self.addresses.vault == holder.addresses.vault
443            || self.args.program_id != holder.args.program_id
444            || holder_legs.underlying.program != legs.underlying.program
445            || holder_legs.quote.program != legs.quote.program
446        {
447            return Err(VaultChainError::InvalidInput(
448                "vault-to-vault requires two distinct vaults on the same program and token pair",
449            ));
450        }
451        let order_id = terms.order_id(
452            &self.args.program_id,
453            &holder.addresses.vault,
454            &self.addresses.vault,
455        );
456        let addresses = self.position_addresses(legs, terms, &order_id);
457        Ok(self.open_between_vaults(
458            VaultIx::OpenPositionBetweenVaults,
459            keeper,
460            holder,
461            holder_legs,
462            legs,
463            order_id,
464            addresses,
465            &terms.args(&order_id),
466        ))
467    }
468
469    /// The swap instruction (rebalance, conversion or staging half) for `plan`.
470    pub fn swap(
471        &self,
472        kind: VaultSwapKind,
473        plan: &VaultSwapPlan,
474    ) -> Result<Instruction, VaultChainError> {
475        self.validate_swap(&kind, plan)?;
476        if let VaultSwapKind::RecoveryStage {
477            asset,
478            generation,
479            main_price_section,
480        } = &kind
481        {
482            if main_price_section.is_empty()
483                || main_price_section
484                    .iter()
485                    .any(|meta| meta.is_writable || meta.is_signer)
486            {
487                return Err(VaultChainError::InvalidInput(
488                    "recovery main price section must be nonempty and read-only",
489                ));
490            }
491            let mut instruction = self.swap(
492                VaultSwapKind::Staging {
493                    step: VaultStagingStep::Stage,
494                    leg: VaultLeg::Second,
495                    asset: *asset,
496                    generation: *generation,
497                },
498                plan,
499            )?;
500            instruction.data[1] = 2;
501            let tail = [
502                AccountMeta::new(self.addresses.vault_main_ata, false),
503                AccountMeta::new_readonly(self.args.main_token_program, false),
504            ];
505            instruction.accounts.splice(
506                kind.fixed_accounts() + plan.price_section.len()
507                    ..kind.fixed_accounts() + plan.price_section.len(),
508                tail.into_iter().chain(main_price_section.iter().cloned()),
509            );
510            return Ok(instruction);
511        }
512        let a = &self.addresses;
513        let args = &self.args;
514        let actor = AccountMeta::new_readonly(args.actor, true);
515        let vault = AccountMeta::new(a.vault, false);
516        let policy = AccountMeta::new_readonly(a.policy, false);
517        let custody = [
518            AccountMeta::new(a.vault_main_ata, false),
519            AccountMeta::new(a.vault_second_ata, false),
520        ];
521        let pair = [
522            AccountMeta::new_readonly(args.main_mint, false),
523            AccountMeta::new_readonly(args.second_mint, false),
524            AccountMeta::new_readonly(args.main_token_program, false),
525            AccountMeta::new_readonly(args.second_token_program, false),
526        ];
527        let mut accounts = Vec::with_capacity(
528            kind.fixed_accounts()
529                + plan.price_section.len()
530                + plan
531                    .hops
532                    .iter()
533                    .map(|hop| 3 + hop.accounts.len())
534                    .sum::<usize>(),
535        );
536        let (ix, prefix) = match kind {
537            VaultSwapKind::RecoveryStage { .. } => unreachable!("handled above"),
538            VaultSwapKind::Rebalance { second_to_main } => {
539                accounts.extend([
540                    actor,
541                    vault,
542                    policy,
543                    AccountMeta::new_readonly(a.delegate, false),
544                    AccountMeta::new_readonly(a.global_config, false),
545                ]);
546                accounts.extend(custody);
547                accounts.extend(pair);
548                accounts.extend([
549                    AccountMeta::new_readonly(args.share_mint, false),
550                    AccountMeta::new_readonly(a.manager_stake_ata, false),
551                ]);
552                (
553                    VaultIx::VaultSpotRebalance,
554                    [Some(u8::from(second_to_main)), None],
555                )
556            }
557            VaultSwapKind::Recovery => {
558                accounts.extend([actor, vault, policy]);
559                accounts.extend(custody);
560                accounts.extend(pair);
561                accounts.push(AccountMeta::new_readonly(args.share_mint, false));
562                (VaultIx::RecoveryConvertToMain, [None, None])
563            }
564            VaultSwapKind::Closing => {
565                accounts.extend([actor, vault, policy]);
566                accounts.extend(custody);
567                accounts.extend(pair);
568                accounts.push(AccountMeta::new_readonly(a.global_config, false));
569                (VaultIx::ClosingConvertToMain, [None, None])
570            }
571            VaultSwapKind::Staging {
572                step, leg, asset, ..
573            } => {
574                let (leg_ata, leg_mint, leg_program, leg) = match leg {
575                    VaultLeg::Main => {
576                        (a.vault_main_ata, args.main_mint, args.main_token_program, 0)
577                    }
578                    VaultLeg::Second => (
579                        a.vault_second_ata,
580                        args.second_mint,
581                        args.second_token_program,
582                        1,
583                    ),
584                };
585                let step = match step {
586                    VaultStagingStep::Stage => 0,
587                    VaultStagingStep::Unstage => 1,
588                };
589                accounts.extend([
590                    actor,
591                    vault,
592                    policy,
593                    AccountMeta::new_readonly(a.delegate, false),
594                    AccountMeta::new_readonly(a.global_config, false),
595                    AccountMeta::new(leg_ata, false),
596                    AccountMeta::new_readonly(leg_mint, false),
597                    AccountMeta::new_readonly(leg_program, false),
598                    AccountMeta::new(asset.vault_ata(&a.vault), false),
599                    AccountMeta::new_readonly(asset.mint, false),
600                    AccountMeta::new_readonly(asset.token_program, false),
601                    AccountMeta::new_readonly(args.share_mint, false),
602                    AccountMeta::new_readonly(a.manager_stake_ata, false),
603                ]);
604                (VaultIx::VaultStagingSwap, [Some(step), Some(leg)])
605            }
606        };
607        accounts.push(AccountMeta::new_readonly(a.swap_venues, false));
608        accounts.extend_from_slice(&plan.price_section);
609        let mut data = Vec::with_capacity(
610            kind.header_len()
611                + plan
612                    .hops
613                    .iter()
614                    .map(|hop| {
615                        let scale = if hop.amount_offset.is_some() { 16 } else { 0 };
616                        5 + scale + hop.data.len()
617                    })
618                    .sum::<usize>(),
619        );
620        data.extend(prefix.into_iter().flatten());
621        for value in [plan.amount_in, plan.minimum_out, plan.valid_until] {
622            data.extend_from_slice(&value.to_le_bytes());
623        }
624        if let VaultSwapKind::Staging { generation, .. } = kind {
625            data.extend_from_slice(&generation.to_le_bytes());
626        }
627        // Lengths are bounded by `validate_swap`.
628        data.push(plan.hops.len() as u8);
629        for hop in &plan.hops {
630            data.push(hop.accounts.len() as u8);
631            data.extend_from_slice(&(hop.data.len() as u16).to_le_bytes());
632            match hop.amount_offset {
633                Some(amount) => {
634                    data.extend_from_slice(&amount.offset.to_le_bytes());
635                    data.extend_from_slice(&amount.num.to_le_bytes());
636                    data.extend_from_slice(&amount.den.to_le_bytes());
637                }
638                None => data.extend_from_slice(&NO_AMOUNT_OFFSET.to_le_bytes()),
639            }
640            data.extend_from_slice(&hop.data);
641            accounts.extend([
642                AccountMeta::new_readonly(hop.program, false),
643                AccountMeta::new(hop.source, false),
644                AccountMeta::new(hop.destination, false),
645            ]);
646            accounts.extend(hop.accounts.iter().map(|meta| AccountMeta {
647                is_signer: false,
648                ..meta.clone()
649            }));
650        }
651        Ok(self.instruction(ix, accounts, &data))
652    }
653
654    fn validate_swap(
655        &self,
656        kind: &VaultSwapKind,
657        plan: &VaultSwapPlan,
658    ) -> Result<(), VaultChainError> {
659        let invalid = |reason| Err(VaultChainError::InvalidInput(reason));
660        if plan.amount_in == 0 || plan.minimum_out == 0 || plan.valid_until == 0 {
661            return invalid("swap amounts and validity must be non-zero");
662        }
663        let hops = &plan.hops;
664        if hops.is_empty() || hops.len() > MAX_VAULT_SWAP_HOPS {
665            return invalid("invalid vault swap hop count");
666        }
667        let accounts: usize = hops.iter().map(|hop| hop.accounts.len() + 3).sum();
668        let data: usize = hops.iter().map(|hop| hop.data.len()).sum();
669        if accounts > MAX_VAULT_SWAP_PATH_ACCOUNTS || data > MAX_VAULT_SWAP_ROUTE_DATA {
670            return invalid("vault swap hops exceed the account or data cap");
671        }
672        let a = &self.addresses;
673        let (input, output) = &self.swap_legs(kind)?;
674        let mut source = input;
675        for (index, hop) in hops.iter().enumerate() {
676            let last = index + 1 == hops.len();
677            if hop.accounts.is_empty() || hop.accounts.len() > MAX_VAULT_SWAP_ROUTE_ACCOUNTS {
678                return invalid("invalid vault swap hop account count");
679            }
680            if hop.data.is_empty() {
681                return invalid("vault swap hop needs data");
682            }
683            if let Some(amount) = hop.amount_offset
684                && (index == 0
685                    || amount.offset == NO_AMOUNT_OFFSET
686                    || usize::from(amount.offset) + 8 > hop.data.len()
687                    || amount.num == 0
688                    || amount.den == 0)
689            {
690                return invalid(
691                    "amount_offset needs a transit source, 8 bytes inside the data and a nonzero scale",
692                );
693            }
694            if hop.source != *source
695                || (last && hop.destination != *output)
696                || (!last && (hop.destination == *input || hop.destination == *output))
697            {
698                return invalid("vault swap hops must chain input leg, transit ATAs, output leg");
699            }
700            if hop
701                .accounts
702                .iter()
703                .any(|meta| meta.is_signer && meta.pubkey != a.vault)
704            {
705                return invalid("only the vault PDA may sign a hop CPI");
706            }
707            if hop.accounts.iter().any(|meta| {
708                (meta.pubkey == *input || meta.pubkey == *output)
709                    && meta.pubkey != hop.source
710                    && meta.pubkey != hop.destination
711            }) {
712                return invalid("a leg ATA may appear only as its hop's source or destination");
713            }
714            let contains = |key: &Pubkey| hop.accounts.iter().any(|meta| meta.pubkey == *key);
715            if !contains(&a.vault) || !contains(&hop.source) || !contains(&hop.destination) {
716                return invalid("hop must contain the vault, its source and its destination");
717            }
718            source = &hop.destination;
719        }
720        Ok(())
721    }
722
723    /// Input and output token accounts of `kind`.
724    fn swap_legs(&self, kind: &VaultSwapKind) -> Result<(Pubkey, Pubkey), VaultChainError> {
725        let a = &self.addresses;
726        let (main, second) = (a.vault_main_ata, a.vault_second_ata);
727        if let VaultSwapKind::RecoveryStage {
728            asset, generation, ..
729        } = kind
730        {
731            return self.swap_legs(&VaultSwapKind::Staging {
732                step: VaultStagingStep::Stage,
733                leg: VaultLeg::Second,
734                asset: *asset,
735                generation: *generation,
736            });
737        }
738        let VaultSwapKind::Staging {
739            step, leg, asset, ..
740        } = kind
741        else {
742            return Ok(match kind {
743                VaultSwapKind::Rebalance {
744                    second_to_main: false,
745                } => (main, second),
746                _ => (second, main),
747            });
748        };
749        let args = &self.args;
750        if [args.main_mint, args.second_mint, args.share_mint].contains(&asset.mint) {
751            return Err(VaultChainError::InvalidInput(
752                "the staging mint must not be a vault mint",
753            ));
754        }
755        if asset.token_program != TOKEN_PROGRAM_ID && asset.token_program != TOKEN_2022_PROGRAM_ID {
756            return Err(VaultChainError::InvalidInput(
757                "staging token program is not SPL Token",
758            ));
759        }
760        let leg = match leg {
761            VaultLeg::Main => main,
762            VaultLeg::Second => second,
763        };
764        let staging = asset.vault_ata(&a.vault);
765        match step {
766            VaultStagingStep::Stage => Ok((leg, staging)),
767            VaultStagingStep::Unstage => Ok((staging, leg)),
768        }
769    }
770}
771
772/// Terms of a vault cash spread, as signed in the spread order layout.
773#[derive(Debug, Clone, Copy, PartialEq, Eq)]
774pub struct VaultSpreadTerms {
775    pub underlying_mint: Pubkey,
776    pub quote_mint: Pubkey,
777    pub expiry_ts: u64,
778    pub kind: crate::spreads::SpreadType,
779    pub side: TakerSide,
780    pub lower_strike: u64,
781    pub upper_strike: u64,
782    pub quantity: u64,
783    pub price: u64,
784    pub valid_until: u64,
785    pub nonce: u64,
786}
787
788/// Terms for a vault open: a physical option or a cash spread.
789#[derive(Debug, Clone, Copy, PartialEq, Eq)]
790pub enum VaultSaleTerms {
791    Physical(VaultTradeTerms),
792    Spread(VaultSpreadTerms),
793}
794
795impl VaultSpreadTerms {
796    fn validate(&self) -> Result<(), VaultChainError> {
797        crate::spreads::validate_premium(
798            self.kind,
799            self.lower_strike,
800            self.upper_strike,
801            self.quantity,
802            self.price,
803        )
804        .map_err(|_| {
805            VaultChainError::InvalidInput("vault spread requires ordered strikes, positive call lower strike, and 0 < price < width")
806        })?;
807        if self.expiry_ts == 0 || self.valid_until == 0 || self.valid_until > self.expiry_ts {
808            return Err(VaultChainError::InvalidInput(
809                "vault spread quote must be valid before its market expiry",
810            ));
811        }
812        Ok(())
813    }
814
815    #[must_use]
816    pub fn market(&self, program_id: &Pubkey) -> Pubkey {
817        market_pda(
818            program_id,
819            &self.underlying_mint,
820            &self.quote_mint,
821            self.expiry_ts,
822            self.kind == crate::spreads::SpreadType::PutSpread,
823        )
824    }
825
826    /// Spread-layout order id binding `maker` and `taker` to these terms.
827    pub fn order_id(
828        &self,
829        program_id: &Pubkey,
830        maker: &Pubkey,
831        taker: &Pubkey,
832    ) -> Result<[u8; 32], VaultChainError> {
833        crate::orders::compute_spread_order_id(&crate::orders::SpreadOrderPreimageArgs {
834            chain_id: 0,
835            program_id: program_id.to_bytes(),
836            side: self.side,
837            position_type: self.kind,
838            market: self.market(program_id).to_bytes(),
839            lower_strike: self.lower_strike,
840            upper_strike: self.upper_strike,
841            quantity: self.quantity,
842            gross_price: self.price,
843            valid_until: self.valid_until,
844            maker: maker.to_bytes(),
845            taker: taker.to_bytes(),
846            nonce: self.nonce,
847        })
848        .map_err(|_| VaultChainError::InvalidInput("invalid vault spread order"))
849    }
850
851    fn args(&self, order_id: &[u8; 32]) -> Vec<u8> {
852        let mut data = Vec::with_capacity(81);
853        data.push(self.kind as u8);
854        for value in [
855            self.lower_strike,
856            self.quantity,
857            self.price,
858            self.valid_until,
859            self.nonce,
860        ] {
861            data.extend_from_slice(&value.to_le_bytes());
862        }
863        data.extend_from_slice(order_id);
864        data.extend_from_slice(&self.upper_strike.to_le_bytes());
865        data
866    }
867}
868
869impl VaultManager {
870    /// Spread collateral is quote atoms escrowed in the position's quote ATA.
871    fn spread_addresses(
872        &self,
873        legs: MarketLegs,
874        terms: &VaultSpreadTerms,
875        order_id: &[u8; 32],
876    ) -> VaultPositionAddresses {
877        let program_id = &self.args.program_id;
878        let position = pda::position(program_id, order_id).0;
879        let config = pda::premium_config(program_id, &legs.quote.mint).0;
880        VaultPositionAddresses {
881            market: terms.market(program_id),
882            position,
883            collateral_mint: legs.quote.mint,
884            collateral_token_program: legs.quote.program,
885            collateral_ata: ata(&position, &legs.quote.mint, &legs.quote.program),
886            config,
887            protocol_fee_ata: ata(&config, &legs.quote.mint, &legs.quote.program),
888        }
889    }
890
891    /// `OpenSpreadForVaultWriter`, holder byte 0: this vault sells; `C` leaves its quote leg.
892    pub fn open_spread_as_writer(
893        &self,
894        keeper: Pubkey,
895        maker_owner: Pubkey,
896        terms: &VaultSpreadTerms,
897    ) -> Result<VaultOpenPosition, VaultChainError> {
898        terms.validate()?;
899        require_side(terms.side, TakerSide::Sell)?;
900        let legs = MarketLegs::bind(self, terms.underlying_mint, terms.quote_mint)?;
901        let order_id =
902            terms.order_id(&self.args.program_id, &maker_owner, &self.addresses.vault)?;
903        let addresses = self.spread_addresses(legs, terms, &order_id);
904        let maker = maker_pda_with_program_id(&self.args.program_id, &maker_owner).0;
905        Ok(self.open_as_writer(
906            VaultIx::OpenSpreadForVaultWriter,
907            keeper,
908            WriterHolder::Maker(maker),
909            legs,
910            order_id,
911            addresses,
912            terms.args(&order_id),
913        ))
914    }
915
916    /// `OpenSpreadForVaultWriter`, holder byte 1: this vault writes a spread `holder` buys.
917    pub fn open_spread_for_wallet_holder(
918        &self,
919        keeper: Pubkey,
920        holder: Pubkey,
921        terms: &VaultSpreadTerms,
922    ) -> Result<VaultOpenPosition, VaultChainError> {
923        terms.validate()?;
924        require_side(terms.side, TakerSide::Buy)?;
925        let legs = MarketLegs::bind(self, terms.underlying_mint, terms.quote_mint)?;
926        let order_id = terms.order_id(&self.args.program_id, &self.addresses.vault, &holder)?;
927        let addresses = self.spread_addresses(legs, terms, &order_id);
928        Ok(self.open_as_writer(
929            VaultIx::OpenSpreadForVaultWriter,
930            keeper,
931            WriterHolder::Wallet(holder),
932            legs,
933            order_id,
934            addresses,
935            terms.args(&order_id),
936        ))
937    }
938
939    /// `OpenSpreadForVaultHolder`: this vault buys; `G` leaves its quote leg.
940    pub fn open_spread_as_holder(
941        &self,
942        keeper: Pubkey,
943        taker: Pubkey,
944        terms: &VaultSpreadTerms,
945    ) -> Result<VaultOpenPosition, VaultChainError> {
946        terms.validate()?;
947        require_side(terms.side, TakerSide::Sell)?;
948        let legs = MarketLegs::bind(self, terms.underlying_mint, terms.quote_mint)?;
949        let order_id = terms.order_id(&self.args.program_id, &self.addresses.vault, &taker)?;
950        let addresses = self.spread_addresses(legs, terms, &order_id);
951        Ok(self.open_as_holder(
952            VaultIx::OpenSpreadForVaultHolder,
953            keeper,
954            taker,
955            legs,
956            order_id,
957            addresses,
958            &terms.args(&order_id),
959        ))
960    }
961
962    /// `OpenSpreadBetweenVaults`: this vault sells the spread to `holder`.
963    pub fn open_spread_with_vault_holder(
964        &self,
965        keeper: Pubkey,
966        holder: &Self,
967        terms: &VaultSpreadTerms,
968    ) -> Result<VaultOpenPosition, VaultChainError> {
969        terms.validate()?;
970        require_side(terms.side, TakerSide::Sell)?;
971        let legs = MarketLegs::bind(self, terms.underlying_mint, terms.quote_mint)?;
972        let holder_legs = MarketLegs::bind(holder, terms.underlying_mint, terms.quote_mint)?;
973        if self.addresses.vault == holder.addresses.vault
974            || self.args.program_id != holder.args.program_id
975            || holder_legs.underlying.program != legs.underlying.program
976            || holder_legs.quote.program != legs.quote.program
977        {
978            return Err(VaultChainError::InvalidInput(
979                "vault-to-vault requires two distinct vaults on the same program and token pair",
980            ));
981        }
982        let order_id = terms.order_id(
983            &self.args.program_id,
984            &holder.addresses.vault,
985            &self.addresses.vault,
986        )?;
987        let addresses = self.spread_addresses(legs, terms, &order_id);
988        Ok(self.open_between_vaults(
989            VaultIx::OpenSpreadBetweenVaults,
990            keeper,
991            holder,
992            holder_legs,
993            legs,
994            order_id,
995            addresses,
996            &terms.args(&order_id),
997        ))
998    }
999
1000    #[allow(clippy::too_many_arguments)]
1001    fn open_as_writer(
1002        &self,
1003        ix: VaultIx,
1004        keeper: Pubkey,
1005        holder: WriterHolder,
1006        legs: MarketLegs,
1007        order_id: [u8; 32],
1008        addresses: VaultPositionAddresses,
1009        mut args: Vec<u8>,
1010    ) -> VaultOpenPosition {
1011        // Slot 1 signs; slot 7 is the holder; slot 8 pays the premium.
1012        let (participant, counterparty, holder_kind) = match holder {
1013            WriterHolder::Maker(maker) => (self.args.actor, maker, 0),
1014            WriterHolder::Wallet(wallet) => (wallet, wallet, 1),
1015        };
1016        args.push(holder_kind);
1017        let a = &self.addresses;
1018        let mut accounts = vec![
1019            AccountMeta::new(keeper, true),
1020            AccountMeta::new(participant, true),
1021            AccountMeta::new(a.vault, false),
1022            AccountMeta::new_readonly(a.policy, false),
1023            AccountMeta::new_readonly(a.delegate, false),
1024            AccountMeta::new(a.vault_main_ata, false),
1025            AccountMeta::new(a.vault_second_ata, false),
1026            AccountMeta::new_readonly(counterparty, false),
1027            AccountMeta::new(
1028                ata(&counterparty, &legs.quote.mint, &legs.quote.program),
1029                false,
1030            ),
1031            AccountMeta::new(addresses.market, false),
1032            AccountMeta::new(addresses.position, false),
1033            AccountMeta::new(addresses.collateral_ata, false),
1034            AccountMeta::new_readonly(addresses.config, false),
1035            AccountMeta::new(addresses.protocol_fee_ata, false),
1036        ];
1037        accounts.extend(self.market_tail(legs));
1038        accounts.extend([
1039            AccountMeta::new_readonly(self.args.share_mint, false),
1040            AccountMeta::new_readonly(a.manager_stake_ata, false),
1041        ]);
1042        VaultOpenPosition {
1043            order_id,
1044            addresses,
1045            instruction: self.instruction(ix, accounts, &args),
1046        }
1047    }
1048
1049    #[allow(clippy::too_many_arguments)]
1050    fn open_as_holder(
1051        &self,
1052        ix: VaultIx,
1053        keeper: Pubkey,
1054        taker: Pubkey,
1055        legs: MarketLegs,
1056        order_id: [u8; 32],
1057        addresses: VaultPositionAddresses,
1058        args: &[u8],
1059    ) -> VaultOpenPosition {
1060        let a = &self.addresses;
1061        let mut accounts = vec![
1062            AccountMeta::new(keeper, true),
1063            AccountMeta::new(taker, true),
1064            AccountMeta::new(a.vault, false),
1065            AccountMeta::new_readonly(a.policy, false),
1066            AccountMeta::new_readonly(a.delegate, false),
1067            AccountMeta::new(legs.quote.vault_ata, false),
1068            AccountMeta::new(
1069                ata(&taker, &legs.underlying.mint, &legs.underlying.program),
1070                false,
1071            ),
1072            AccountMeta::new(ata(&taker, &legs.quote.mint, &legs.quote.program), false),
1073            AccountMeta::new(addresses.market, false),
1074            AccountMeta::new(addresses.position, false),
1075            AccountMeta::new(addresses.collateral_ata, false),
1076            AccountMeta::new_readonly(addresses.config, false),
1077            AccountMeta::new(addresses.protocol_fee_ata, false),
1078        ];
1079        accounts.extend(self.market_tail(legs));
1080        accounts.extend([
1081            AccountMeta::new_readonly(self.args.share_mint, false),
1082            AccountMeta::new_readonly(a.manager_stake_ata, false),
1083        ]);
1084        VaultOpenPosition {
1085            order_id,
1086            addresses,
1087            instruction: self.instruction(ix, accounts, args),
1088        }
1089    }
1090
1091    #[allow(clippy::too_many_arguments)]
1092    fn open_between_vaults(
1093        &self,
1094        ix: VaultIx,
1095        keeper: Pubkey,
1096        holder: &Self,
1097        holder_legs: MarketLegs,
1098        legs: MarketLegs,
1099        order_id: [u8; 32],
1100        addresses: VaultPositionAddresses,
1101        args: &[u8],
1102    ) -> VaultOpenPosition {
1103        let (w, h) = (&self.addresses, &holder.addresses);
1104        let mut accounts = vec![
1105            AccountMeta::new(keeper, true),
1106            AccountMeta::new(self.args.actor, true),
1107            AccountMeta::new(w.vault, false),
1108            AccountMeta::new_readonly(w.policy, false),
1109            AccountMeta::new_readonly(w.delegate, false),
1110            AccountMeta::new(w.vault_main_ata, false),
1111            AccountMeta::new(w.vault_second_ata, false),
1112            AccountMeta::new(h.vault, false),
1113            AccountMeta::new_readonly(h.policy, false),
1114            AccountMeta::new_readonly(h.delegate, false),
1115            AccountMeta::new(holder_legs.quote.vault_ata, false),
1116            AccountMeta::new(addresses.market, false),
1117            AccountMeta::new(addresses.position, false),
1118            AccountMeta::new(addresses.collateral_ata, false),
1119            AccountMeta::new_readonly(addresses.config, false),
1120            AccountMeta::new(addresses.protocol_fee_ata, false),
1121        ];
1122        accounts.extend(self.market_tail(legs));
1123        accounts.extend([
1124            AccountMeta::new_readonly(self.args.share_mint, false),
1125            AccountMeta::new_readonly(w.manager_stake_ata, false),
1126            AccountMeta::new_readonly(holder.args.share_mint, false),
1127            AccountMeta::new_readonly(h.manager_stake_ata, false),
1128        ]);
1129        VaultOpenPosition {
1130            order_id,
1131            addresses,
1132            instruction: self.instruction(ix, accounts, args),
1133        }
1134    }
1135}
1136
1137/// Who holds what the vault writes: a Maker (its PDA pays from custody) or a wallet.
1138#[derive(Clone, Copy)]
1139enum WriterHolder {
1140    Maker(Pubkey),
1141    Wallet(Pubkey),
1142}
1143
1144fn require_side(side: TakerSide, route: TakerSide) -> Result<(), VaultChainError> {
1145    if side == route {
1146        return Ok(());
1147    }
1148    Err(VaultChainError::InvalidInput(if route.is_taker_buy() {
1149        "taker_sell_unsupported"
1150    } else {
1151        "taker_buy_unsupported"
1152    }))
1153}