1use solana_program_error::{ProgramError, ProgramResult};
4use solana_pubkey::Pubkey;
5use wincode::{deserialize, SchemaRead, SchemaWrite};
6
7use crate::{
8 access::verify_access_merkle_proof, error::RoshiError, math::validate_percentage_bps,
9 oracle::OracleConfig, state::VAULT_ACCOUNT_TAG, ID,
10};
11
12const FLAG_FALSE: u8 = 0;
13const FLAG_TRUE: u8 = 1;
14
15const fn flag(value: bool) -> u8 {
16 value as u8
17}
18
19fn bool_flag(flag: u8) -> Result<bool, ProgramError> {
20 match flag {
21 FLAG_FALSE => Ok(false),
22 FLAG_TRUE => Ok(true),
23 _ => Err(RoshiError::InvalidVaultState.into()),
24 }
25}
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub enum Role {
29 Admin,
30 Strategist,
31 SwapAuthority,
32 NavAuthority,
33 WithdrawalAuthority,
34}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq, SchemaWrite, SchemaRead)]
37#[wincode(assert_zero_copy)]
38#[repr(C)]
39pub struct Vault {
40 pub base_oracle: OracleConfig,
41 pub total_assets: u64,
42 pub external_assets: u64,
43 pub pending_withdrawal_assets: u64,
44 pub fees_payable: u64,
45 pub high_watermark: u64,
46 pub report_epoch: u64,
47 pub requested_withdrawal_shares: u64,
48 pub last_update_ts: i64,
49 pub tag: [u8; 32],
50 pub admin: [u8; 32],
51 pub strategist: [u8; 32],
52 pub swap_authority: [u8; 32],
53 pub nav_authority: [u8; 32],
54 pub withdrawal_authority: [u8; 32],
55 pub base_mint: [u8; 32],
56 pub share_mint: [u8; 32],
57 pub treasury: [u8; 32],
58 pub last_report_hash: [u8; 32],
59 pub access_merkle_root: [u8; 32],
60 pub performance_fee_bps: u16,
61 pub withdrawal_buffer_bps: u16,
62 pub tag_len: u8,
63 pub base_decimals: u8,
64 pub deposit_sub_account: u8,
65 pub withdraw_sub_account: u8,
66 deposits_paused_flag: u8,
67 withdrawals_paused_flag: u8,
68 manage_paused_flag: u8,
69 private_flag: u8,
70 external_enabled_flag: u8,
71 pub bump: u8,
72 _padding: [u8; 2],
73}
74
75impl Vault {
76 pub const SEED: &'static [u8] = b"vault";
77 pub const MAX_TAG_LEN: usize = 32;
78 pub const SPACE: usize = std::mem::size_of::<Self>() + 1;
79
80 #[allow(clippy::too_many_arguments)]
81 pub fn new(
82 tag: &[u8],
83 admin: [u8; 32],
84 strategist: [u8; 32],
85 swap_authority: [u8; 32],
86 nav_authority: [u8; 32],
87 withdrawal_authority: [u8; 32],
88 base_mint: [u8; 32],
89 share_mint: [u8; 32],
90 base_decimals: u8,
91 base_oracle: OracleConfig,
92 deposit_sub_account: u8,
93 withdraw_sub_account: u8,
94 treasury: [u8; 32],
95 performance_fee_bps: u16,
96 withdrawal_buffer_bps: u16,
97 private: bool,
98 access_merkle_root: [u8; 32],
99 bump: u8,
100 ) -> Result<Self, ProgramError> {
101 Self::validate_config(
102 base_mint,
103 share_mint,
104 performance_fee_bps,
105 withdrawal_buffer_bps,
106 )?;
107 base_oracle
108 .validate()
109 .map_err(|_| ProgramError::from(RoshiError::InvalidVaultState))?;
110
111 let (tag, tag_len) = Self::pack_tag(tag)?;
112
113 Ok(Self {
114 base_oracle,
115 total_assets: 0,
116 external_assets: 0,
117 pending_withdrawal_assets: 0,
118 fees_payable: 0,
119 high_watermark: 0,
120 report_epoch: 0,
121 requested_withdrawal_shares: 0,
122 last_update_ts: 0,
123 tag,
124 admin,
125 strategist,
126 swap_authority,
127 nav_authority,
128 withdrawal_authority,
129 base_mint,
130 share_mint,
131 treasury,
132 last_report_hash: [0; 32],
133 access_merkle_root,
134 performance_fee_bps,
135 withdrawal_buffer_bps,
136 tag_len,
137 base_decimals,
138 deposit_sub_account,
139 withdraw_sub_account,
140 deposits_paused_flag: flag(false),
141 withdrawals_paused_flag: flag(false),
142 manage_paused_flag: flag(false),
143 private_flag: flag(private),
144 external_enabled_flag: flag(false),
145 bump,
146 _padding: [0; 2],
147 })
148 }
149
150 pub fn from_account_data(data: &[u8]) -> Result<Self, ProgramError> {
153 let (&tag, rest) = data
154 .split_first()
155 .ok_or(ProgramError::from(RoshiError::InvalidVaultAccount))?;
156 if tag != VAULT_ACCOUNT_TAG {
157 return Err(RoshiError::InvalidVaultAccount.into());
158 }
159 let vault: Self =
160 deserialize(rest).map_err(|_| ProgramError::from(RoshiError::InvalidVaultAccount))?;
161 vault.validate_state()?;
162 Ok(vault)
163 }
164
165 pub fn validate_config(
166 base_mint: [u8; 32],
167 share_mint: [u8; 32],
168 performance_fee_bps: u16,
169 withdrawal_buffer_bps: u16,
170 ) -> ProgramResult {
171 validate_percentage_bps(performance_fee_bps)?;
172 validate_percentage_bps(withdrawal_buffer_bps)?;
173
174 if base_mint == share_mint {
175 return Err(ProgramError::InvalidArgument);
176 }
177
178 Ok(())
179 }
180
181 pub fn pack_tag(tag: &[u8]) -> Result<([u8; Self::MAX_TAG_LEN], u8), ProgramError> {
182 Self::validate_tag(tag)?;
183
184 let mut packed_tag = [0; Self::MAX_TAG_LEN];
185 packed_tag[..tag.len()].copy_from_slice(tag);
186
187 Ok((packed_tag, tag.len() as u8))
188 }
189
190 pub fn unpack_tag(tag: &[u8; Self::MAX_TAG_LEN], tag_len: u8) -> Result<&[u8], ProgramError> {
191 let tag_len = usize::from(tag_len);
192 let tag = tag
193 .get(..tag_len)
194 .ok_or(ProgramError::from(RoshiError::InvalidVaultTag))?;
195 Self::validate_tag(tag)?;
196
197 Ok(tag)
198 }
199
200 pub fn tag_seed(&self) -> Result<&[u8], ProgramError> {
201 Self::unpack_tag(&self.tag, self.tag_len)
202 }
203
204 pub fn find_address(tag: &[u8], base_mint: &Pubkey) -> Result<(Pubkey, u8), ProgramError> {
205 Self::validate_tag(tag)?;
206
207 Ok(Pubkey::find_program_address(
208 &[Self::SEED, tag, base_mint.as_ref()],
209 &ID,
210 ))
211 }
212
213 fn validate_tag(tag: &[u8]) -> ProgramResult {
214 if tag.is_empty() || tag.len() > Self::MAX_TAG_LEN {
215 return Err(RoshiError::InvalidVaultTag.into());
216 }
217
218 Ok(())
219 }
220
221 pub fn authority_for_role(&self, role: Role) -> Pubkey {
222 match role {
223 Role::Admin => Pubkey::from(self.admin),
224 Role::Strategist => Pubkey::from(self.strategist),
225 Role::SwapAuthority => Pubkey::from(self.swap_authority),
226 Role::NavAuthority => Pubkey::from(self.nav_authority),
227 Role::WithdrawalAuthority => Pubkey::from(self.withdrawal_authority),
228 }
229 }
230
231 pub fn has_role(&self, role: Role, signer: &Pubkey) -> bool {
232 self.authority_for_role(role) == *signer
233 }
234
235 pub fn verify_address(&self, vault_key: &Pubkey) -> ProgramResult {
237 let base_mint = Pubkey::from(self.base_mint);
238 let (expected_vault_key, expected_bump) = Self::find_address(self.tag_seed()?, &base_mint)?;
239
240 if vault_key != &expected_vault_key || self.bump != expected_bump {
241 return Err(ProgramError::InvalidSeeds);
242 }
243
244 Ok(())
245 }
246
247 pub fn economic_share_supply(&self, active_share_supply: u64) -> Result<u64, ProgramError> {
250 active_share_supply
251 .checked_add(self.requested_withdrawal_shares)
252 .ok_or(ProgramError::from(RoshiError::Overflow))
253 }
254
255 pub fn verify_idle_sub_account(&self, sub_account: u8) -> ProgramResult {
266 if sub_account == self.deposit_sub_account || sub_account == self.withdraw_sub_account {
267 return Ok(());
268 }
269
270 Err(RoshiError::InvalidSubAccount.into())
271 }
272
273 pub fn verify_manage_enabled(&self) -> ProgramResult {
274 if self.manage_paused()? {
275 return Err(RoshiError::VaultPaused.into());
276 }
277
278 Ok(())
279 }
280
281 pub fn allows_depositor(&self, depositor: &Pubkey, proof: &[[u8; 32]]) -> bool {
282 match self.private() {
283 Ok(false) => true,
284 Ok(true) => verify_access_merkle_proof(depositor, &self.access_merkle_root, proof),
285 Err(_) => false,
286 }
287 }
288
289 pub fn deposits_paused(&self) -> Result<bool, ProgramError> {
290 bool_flag(self.deposits_paused_flag)
291 }
292
293 pub fn withdrawals_paused(&self) -> Result<bool, ProgramError> {
294 bool_flag(self.withdrawals_paused_flag)
295 }
296
297 pub fn manage_paused(&self) -> Result<bool, ProgramError> {
298 bool_flag(self.manage_paused_flag)
299 }
300
301 pub fn private(&self) -> Result<bool, ProgramError> {
302 bool_flag(self.private_flag)
303 }
304
305 pub fn external_enabled(&self) -> Result<bool, ProgramError> {
306 bool_flag(self.external_enabled_flag)
307 }
308
309 pub fn set_deposits_paused(&mut self, deposits_paused: bool) {
310 self.deposits_paused_flag = flag(deposits_paused);
311 }
312
313 pub fn set_withdrawals_paused(&mut self, withdrawals_paused: bool) {
314 self.withdrawals_paused_flag = flag(withdrawals_paused);
315 }
316
317 pub fn set_manage_paused(&mut self, manage_paused: bool) {
318 self.manage_paused_flag = flag(manage_paused);
319 }
320
321 pub fn set_private(&mut self, private: bool) {
322 self.private_flag = flag(private);
323 }
324
325 pub fn set_external_enabled(&mut self, external_enabled: bool) {
326 self.external_enabled_flag = flag(external_enabled);
327 }
328
329 pub fn validate_state(&self) -> ProgramResult {
330 Self::unpack_tag(&self.tag, self.tag_len)?;
331 Self::validate_config(
332 self.base_mint,
333 self.share_mint,
334 self.performance_fee_bps,
335 self.withdrawal_buffer_bps,
336 )?;
337 self.base_oracle
338 .validate()
339 .map_err(|_| ProgramError::from(RoshiError::InvalidVaultState))?;
340 bool_flag(self.deposits_paused_flag)?;
341 bool_flag(self.withdrawals_paused_flag)?;
342 bool_flag(self.manage_paused_flag)?;
343 bool_flag(self.private_flag)?;
344 bool_flag(self.external_enabled_flag)?;
345 Ok(())
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352 use crate::access::{access_merkle_leaf, access_merkle_node};
353 use wincode::{config::DefaultConfig, serialize, SchemaRead, SchemaWrite, TypeMeta};
354
355 fn assert_zero_copy<T>()
356 where
357 T: wincode::ZeroCopy,
358 T: for<'de> SchemaRead<'de, DefaultConfig> + SchemaWrite<DefaultConfig>,
359 {
360 assert_eq!(
361 <T as SchemaRead<'_, DefaultConfig>>::TYPE_META,
362 TypeMeta::Static {
363 size: core::mem::size_of::<T>(),
364 zero_copy: true,
365 }
366 );
367 assert_eq!(
368 <T as SchemaWrite<DefaultConfig>>::TYPE_META,
369 TypeMeta::Static {
370 size: core::mem::size_of::<T>(),
371 zero_copy: true,
372 }
373 );
374 }
375
376 pub(crate) fn new_test_vault(private: bool, access_merkle_root: [u8; 32]) -> Vault {
377 let admin = Pubkey::new_unique();
378 let base_mint = Pubkey::new_unique();
379 let (_, bump) = Vault::find_address(b"test", &base_mint).unwrap();
380
381 Vault::new(
382 b"test",
383 admin.to_bytes(),
384 [2; 32],
385 [3; 32],
386 [4; 32],
387 [5; 32],
388 base_mint.to_bytes(),
389 Pubkey::new_unique().to_bytes(),
390 6,
391 OracleConfig::default(),
392 7,
393 8,
394 [9; 32],
395 100,
396 250,
397 private,
398 access_merkle_root,
399 bump,
400 )
401 .unwrap()
402 }
403
404 #[test]
405 fn new_initializes_default_accounting_and_config() {
406 let vault = new_test_vault(true, [10; 32]);
407
408 assert_eq!(vault.tag_seed().unwrap(), b"test");
409 assert_eq!(vault.strategist, [2; 32]);
410 assert_eq!(vault.swap_authority, [3; 32]);
411 assert_eq!(vault.nav_authority, [4; 32]);
412 assert_eq!(vault.withdrawal_authority, [5; 32]);
413 assert_eq!(vault.base_decimals, 6);
414 assert_eq!(vault.deposit_sub_account, 7);
415 assert_eq!(vault.withdraw_sub_account, 8);
416 assert_eq!(vault.treasury, [9; 32]);
417 assert_eq!(vault.total_assets, 0);
418 assert_eq!(vault.external_assets, 0);
419 assert_eq!(vault.pending_withdrawal_assets, 0);
420 assert_eq!(vault.fees_payable, 0);
421 assert_eq!(vault.high_watermark, 0);
422 assert_eq!(vault.report_epoch, 0);
423 assert_eq!(vault.requested_withdrawal_shares, 0);
424 assert_eq!(vault.performance_fee_bps, 100);
425 assert_eq!(vault.withdrawal_buffer_bps, 250);
426 assert_eq!(vault.last_update_ts, 0);
427 assert_eq!(vault.deposits_paused(), Ok(false));
428 assert_eq!(vault.withdrawals_paused(), Ok(false));
429 assert_eq!(vault.manage_paused(), Ok(false));
430 assert_eq!(vault.private(), Ok(true));
431 assert_eq!(vault.external_enabled(), Ok(false));
432 assert_eq!(vault.access_merkle_root, [10; 32]);
433 }
434
435 #[test]
436 fn from_account_data_round_trips_a_tagged_vault() {
437 let vault = new_test_vault(false, [0; 32]);
438 let mut data = vec![VAULT_ACCOUNT_TAG];
439 data.extend_from_slice(&serialize(&vault).unwrap());
440
441 assert_eq!(Vault::from_account_data(&data).unwrap(), vault);
442 }
443
444 #[test]
445 fn from_account_data_rejects_wrong_tag() {
446 let vault = new_test_vault(false, [0; 32]);
447 let mut data = vec![VAULT_ACCOUNT_TAG + 1];
448 data.extend_from_slice(&serialize(&vault).unwrap());
449
450 assert_eq!(
451 Vault::from_account_data(&data),
452 Err(ProgramError::from(RoshiError::InvalidVaultAccount))
453 );
454 }
455
456 #[test]
457 fn vault_is_zero_copy_with_explicit_padding() {
458 assert_zero_copy::<Vault>();
459 assert_eq!(core::mem::size_of::<Vault>(), 600);
460 assert_eq!(Vault::SPACE, 601);
461 let vault = new_test_vault(false, [0; 32]);
462 assert_eq!(
463 serialize(&vault).unwrap().len(),
464 core::mem::size_of::<Vault>()
465 );
466 }
467
468 #[test]
469 fn pause_and_access_flags_use_typed_accessors() {
470 let mut vault = new_test_vault(false, [0; 32]);
471
472 assert_eq!(vault.deposits_paused(), Ok(false));
473 assert_eq!(vault.withdrawals_paused(), Ok(false));
474 assert_eq!(vault.manage_paused(), Ok(false));
475 assert_eq!(vault.private(), Ok(false));
476 assert_eq!(vault.external_enabled(), Ok(false));
477
478 vault.set_deposits_paused(true);
479 vault.set_withdrawals_paused(true);
480 vault.set_manage_paused(true);
481 vault.set_private(true);
482 vault.set_external_enabled(true);
483
484 assert_eq!(vault.deposits_paused(), Ok(true));
485 assert_eq!(vault.withdrawals_paused(), Ok(true));
486 assert_eq!(vault.manage_paused(), Ok(true));
487 assert_eq!(vault.private(), Ok(true));
488 assert_eq!(vault.external_enabled(), Ok(true));
489 }
490
491 #[test]
492 fn verify_manage_enabled_rejects_paused_vault() {
493 let mut vault = new_test_vault(false, [0; 32]);
494
495 vault.set_manage_paused(true);
496
497 assert_eq!(
498 vault.verify_manage_enabled(),
499 Err(ProgramError::from(RoshiError::VaultPaused))
500 );
501 }
502
503 #[test]
504 fn unpack_tag_rejects_invalid_tags() {
505 let (tag, _) = Vault::pack_tag(b"test").unwrap();
506
507 assert!(matches!(
508 Vault::unpack_tag(&tag, 0),
509 Err(error) if error == ProgramError::from(RoshiError::InvalidVaultTag)
510 ));
511 assert!(matches!(
512 Vault::unpack_tag(&tag, 33),
513 Err(error) if error == ProgramError::from(RoshiError::InvalidVaultTag)
514 ));
515 }
516
517 #[test]
518 fn validate_config_rejects_invalid_bps() {
519 assert!(matches!(
520 Vault::validate_config([1; 32], [2; 32], 10_001, 0),
521 Err(error) if error == ProgramError::from(RoshiError::InvalidBps)
522 ));
523 }
524
525 #[test]
526 fn validate_config_rejects_matching_base_and_share_mints() {
527 assert!(matches!(
528 Vault::validate_config([1; 32], [1; 32], 0, 0),
529 Err(ProgramError::InvalidArgument)
530 ));
531 }
532
533 #[test]
534 fn from_account_data_rejects_invalid_vault_flags() {
535 let mut vault = new_test_vault(false, [0; 32]);
536 vault.manage_paused_flag = 255;
537 let mut data = vec![VAULT_ACCOUNT_TAG];
538 data.extend_from_slice(&serialize(&vault).unwrap());
539
540 assert_eq!(
541 Vault::from_account_data(&data),
542 Err(ProgramError::from(RoshiError::InvalidVaultState))
543 );
544 }
545
546 #[test]
547 fn from_account_data_rejects_invalid_base_oracle_kind() {
548 let vault = new_test_vault(false, [0; 32]);
549 let mut data = vec![VAULT_ACCOUNT_TAG];
550 data.extend_from_slice(&serialize(&vault).unwrap());
551 let oracle_kind_offset = 1
552 + core::mem::size_of::<crate::oracle::SwitchboardOracleConfig>()
553 + core::mem::size_of::<crate::oracle::PythOracleConfig>();
554 data[oracle_kind_offset] = 255;
555
556 assert_eq!(
557 Vault::from_account_data(&data),
558 Err(ProgramError::from(RoshiError::InvalidVaultState))
559 );
560 }
561
562 #[test]
563 fn public_vault_allows_any_depositor_without_proof() {
564 let vault = new_test_vault(false, [0; 32]);
565
566 assert!(vault.allows_depositor(&Pubkey::new_unique(), &[]));
567 assert!(vault.allows_depositor(&Pubkey::new_unique(), &[[7; 32]]));
568 }
569
570 #[test]
571 fn private_vault_accepts_valid_access_proof() {
572 let allowed = Pubkey::new_unique();
573 let sibling = access_merkle_leaf(&Pubkey::new_unique());
574 let root = access_merkle_node(&access_merkle_leaf(&allowed), &sibling);
575 let vault = new_test_vault(true, root);
576
577 assert!(vault.allows_depositor(&allowed, &[sibling]));
578 }
579
580 #[test]
581 fn private_vault_rejects_missing_or_wrong_access_proof() {
582 let allowed = Pubkey::new_unique();
583 let sibling = access_merkle_leaf(&Pubkey::new_unique());
584 let root = access_merkle_node(&access_merkle_leaf(&allowed), &sibling);
585 let vault = new_test_vault(true, root);
586
587 assert!(!vault.allows_depositor(&allowed, &[]));
588 assert!(!vault.allows_depositor(&Pubkey::new_unique(), &[sibling]));
589 assert!(!vault.allows_depositor(&allowed, &[[9; 32]]));
590 }
591}