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
16pub const MAX_VAULT_SWAP_ROUTE_ACCOUNTS: usize = 64;
18pub const MAX_VAULT_SWAP_PATH_ACCOUNTS: usize = 128;
20pub 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#[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#[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 #[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#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct VaultSwapHop {
204 pub program: Pubkey,
205 pub source: Pubkey,
207 pub destination: Pubkey,
209 pub accounts: Vec<AccountMeta>,
210 pub data: Vec<u8>,
211 pub amount_offset: Option<VaultSwapAmountOffset>,
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub struct VaultSwapAmountOffset {
218 pub offset: u16,
219 pub num: u64,
220 pub den: u64,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
225pub struct VaultSwapPlan {
226 pub price_section: Vec<AccountMeta>,
228 pub amount_in: u64,
230 pub minimum_out: u64,
232 pub valid_until: u64,
233 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 RecoveryStage {
246 asset: VaultStagingAsset,
247 generation: u64,
248 main_price_section: Vec<AccountMeta>,
249 },
250 Staging {
252 step: VaultStagingStep,
253 leg: VaultLeg,
254 asset: VaultStagingAsset,
255 generation: u64,
256 },
257}
258
259#[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 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 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 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 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 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 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 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 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 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#[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#[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 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 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 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 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 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 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 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#[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}