1use crate::{account::AccountView, address::Address, error::ProgramError, result::ProgramResult};
55
56pub const EXT_UNINITIALIZED: u16 = 0;
59pub const EXT_TRANSFER_FEE_CONFIG: u16 = 1;
60pub const EXT_TRANSFER_FEE_AMOUNT: u16 = 2;
61pub const EXT_MINT_CLOSE_AUTHORITY: u16 = 3;
62pub const EXT_CONFIDENTIAL_TRANSFER_MINT: u16 = 4;
63pub const EXT_CONFIDENTIAL_TRANSFER_ACCOUNT: u16 = 5;
64pub const EXT_DEFAULT_ACCOUNT_STATE: u16 = 6;
65pub const EXT_IMMUTABLE_OWNER: u16 = 7;
66pub const EXT_MEMO_TRANSFER: u16 = 8;
67pub const EXT_NON_TRANSFERABLE: u16 = 9;
68pub const EXT_INTEREST_BEARING_CONFIG: u16 = 10;
69pub const EXT_CPI_GUARD: u16 = 11;
70pub const EXT_PERMANENT_DELEGATE: u16 = 12;
71pub const EXT_NON_TRANSFERABLE_ACCOUNT: u16 = 13;
72pub const EXT_TRANSFER_HOOK: u16 = 14;
73pub const EXT_TRANSFER_HOOK_ACCOUNT: u16 = 15;
74pub const EXT_CONFIDENTIAL_TRANSFER_FEE_CONFIG: u16 = 16;
75pub const EXT_CONFIDENTIAL_TRANSFER_FEE_AMOUNT: u16 = 17;
76pub const EXT_METADATA_POINTER: u16 = 18;
77pub const EXT_TOKEN_METADATA: u16 = 19;
78pub const EXT_GROUP_POINTER: u16 = 20;
79pub const EXT_TOKEN_GROUP: u16 = 21;
80pub const EXT_GROUP_MEMBER_POINTER: u16 = 22;
81pub const EXT_TOKEN_GROUP_MEMBER: u16 = 23;
82pub const EXT_CONFIDENTIAL_MINT_BURN: u16 = 24;
83pub const EXT_SCALED_UI_AMOUNT_CONFIG: u16 = 25;
84pub const EXT_PAUSABLE_CONFIG: u16 = 26;
85pub const EXT_PAUSABLE_ACCOUNT: u16 = 27;
86pub const EXT_PERMISSIONED_BURN: u16 = 28;
87
88pub const MAX_KNOWN_EXTENSION_TYPE: u16 = EXT_PERMISSIONED_BURN;
94
95pub const ACCOUNT_TYPE_MINT: u8 = 0x01;
97pub const ACCOUNT_TYPE_TOKEN: u8 = 0x02;
99
100pub const BASE_MINT_LEN: usize = 82;
107pub const BASE_TOKEN_LEN: usize = 165;
113pub const TOKEN_MULTISIG_LEN: usize = 355;
118pub const ACCOUNT_TYPE_OFFSET: usize = BASE_TOKEN_LEN;
121pub const TLV_OFFSET: usize = ACCOUNT_TYPE_OFFSET + 1;
124pub const MINT_EXTENSION_PADDING_START: usize = BASE_MINT_LEN;
128pub const MINT_EXTENSION_PADDING_END: usize = ACCOUNT_TYPE_OFFSET;
130
131#[inline]
147pub fn find_extension(tlv_bytes: &[u8], ext_type: u16) -> Option<&[u8]> {
148 let mut cursor = 0usize;
149 while cursor + 4 <= tlv_bytes.len() {
150 let t = u16::from_le_bytes([tlv_bytes[cursor], tlv_bytes[cursor + 1]]);
151 let len = u16::from_le_bytes([tlv_bytes[cursor + 2], tlv_bytes[cursor + 3]]) as usize;
152 let data_start = cursor + 4;
153 let data_end = data_start + len;
154 if data_end > tlv_bytes.len() {
155 return None;
156 }
157 if t == ext_type {
158 return Some(&tlv_bytes[data_start..data_end]);
159 }
160 if t == EXT_UNINITIALIZED {
161 return None;
166 }
167 cursor = data_end;
168 }
169 None
170}
171
172pub fn validate_tlv_structure(tlv_bytes: &[u8]) -> ProgramResult {
180 let mut cursor = 0usize;
181 while cursor < tlv_bytes.len() {
182 if tlv_bytes.len() - cursor < 2 {
184 return Ok(());
185 }
186
187 let ext_type = u16::from_le_bytes([tlv_bytes[cursor], tlv_bytes[cursor + 1]]);
188 if ext_type == EXT_UNINITIALIZED {
189 return Ok(());
190 }
191 if tlv_bytes.len() - cursor < 4 {
192 return Err(ProgramError::InvalidAccountData);
193 }
194
195 let len = u16::from_le_bytes([tlv_bytes[cursor + 2], tlv_bytes[cursor + 3]]) as usize;
196 cursor = cursor
197 .checked_add(4)
198 .and_then(|start| start.checked_add(len))
199 .ok_or(ProgramError::InvalidAccountData)?;
200 if cursor > tlv_bytes.len() {
201 return Err(ProgramError::InvalidAccountData);
202 }
203 }
204 Ok(())
205}
206
207pub fn validate_extension_allowlist(tlv_bytes: &[u8], allowed: &[u16]) -> ProgramResult {
213 let mut cursor = 0usize;
214 let mut seen = 0u32;
215 while cursor < tlv_bytes.len() {
216 if tlv_bytes.len() - cursor < 2 {
217 return Ok(());
218 }
219
220 let ext_type = u16::from_le_bytes([tlv_bytes[cursor], tlv_bytes[cursor + 1]]);
221 if ext_type == EXT_UNINITIALIZED {
222 return Ok(());
223 }
224 if ext_type > MAX_KNOWN_EXTENSION_TYPE || !allowed.contains(&ext_type) {
225 return Err(ProgramError::InvalidAccountData);
226 }
227 let bit = 1u32
228 .checked_shl(ext_type as u32)
229 .ok_or(ProgramError::InvalidAccountData)?;
230 if seen & bit != 0 {
231 return Err(ProgramError::InvalidAccountData);
232 }
233 seen |= bit;
234
235 if tlv_bytes.len() - cursor < 4 {
236 return Err(ProgramError::InvalidAccountData);
237 }
238 let len = u16::from_le_bytes([tlv_bytes[cursor + 2], tlv_bytes[cursor + 3]]) as usize;
239 cursor = cursor
240 .checked_add(4)
241 .and_then(|start| start.checked_add(len))
242 .ok_or(ProgramError::InvalidAccountData)?;
243 if cursor > tlv_bytes.len() {
244 return Err(ProgramError::InvalidAccountData);
245 }
246 }
247 Ok(())
248}
249
250#[inline]
275pub fn mint_tlv_region(data: &[u8]) -> Option<&[u8]> {
276 if data.len() <= TLV_OFFSET || data.len() == TOKEN_MULTISIG_LEN {
277 return None;
278 }
279 let kind = data[ACCOUNT_TYPE_OFFSET];
280 if kind != ACCOUNT_TYPE_MINT && kind != 0 {
281 return None;
282 }
283 if data[MINT_EXTENSION_PADDING_START..MINT_EXTENSION_PADDING_END]
284 .iter()
285 .any(|byte| *byte != 0)
286 {
287 return None;
288 }
289 Some(&data[TLV_OFFSET..])
290}
291
292#[inline]
300pub fn token_account_tlv_region(data: &[u8]) -> Option<&[u8]> {
301 if data.len() <= TLV_OFFSET || data.len() == TOKEN_MULTISIG_LEN {
302 return None;
303 }
304 let kind = data[ACCOUNT_TYPE_OFFSET];
305 if kind != ACCOUNT_TYPE_TOKEN && kind != 0 {
306 return None;
307 }
308 Some(&data[TLV_OFFSET..])
309}
310
311#[derive(Clone, Copy, Debug, PartialEq, Eq)]
318pub struct ExtensionPolicy<'a> {
319 pub required: &'a [u16],
320 pub forbidden: &'a [u16],
321}
322
323impl<'a> ExtensionPolicy<'a> {
324 #[inline]
325 pub const fn new(required: &'a [u16], forbidden: &'a [u16]) -> Self {
326 Self {
327 required,
328 forbidden,
329 }
330 }
331}
332
333#[inline]
334pub fn require_extension(tlv: &[u8], ext_type: u16) -> ProgramResult {
335 validate_tlv_structure(tlv)?;
336 if find_extension(tlv, ext_type).is_some() {
337 Ok(())
338 } else {
339 Err(ProgramError::InvalidAccountData)
340 }
341}
342
343#[inline]
344pub fn forbid_extension(tlv: &[u8], ext_type: u16) -> ProgramResult {
345 validate_tlv_structure(tlv)?;
346 if find_extension(tlv, ext_type).is_none() {
347 Ok(())
348 } else {
349 Err(ProgramError::InvalidAccountData)
350 }
351}
352
353#[inline]
354pub fn validate_extension_policy(tlv: &[u8], policy: &ExtensionPolicy<'_>) -> ProgramResult {
355 validate_tlv_structure(tlv)?;
356 for ext_type in policy.required {
357 if find_extension(tlv, *ext_type).is_none() {
358 return Err(ProgramError::InvalidAccountData);
359 }
360 }
361 for ext_type in policy.forbidden {
362 if find_extension(tlv, *ext_type).is_some() {
363 return Err(ProgramError::InvalidAccountData);
364 }
365 }
366 Ok(())
367}
368
369#[inline]
375pub fn require_non_transferable(mint: &AccountView<'_>) -> ProgramResult {
376 let data = mint
377 .try_borrow()
378 .map_err(|_| ProgramError::AccountBorrowFailed)?;
379 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
380 if find_extension(tlv, EXT_NON_TRANSFERABLE).is_some() {
381 Ok(())
382 } else {
383 Err(ProgramError::InvalidAccountData)
384 }
385}
386
387#[inline]
389pub fn require_mint_close_authority(mint: &AccountView<'_>, expected: &Address) -> ProgramResult {
390 let data = mint
391 .try_borrow()
392 .map_err(|_| ProgramError::AccountBorrowFailed)?;
393 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
394 let ext =
395 find_extension(tlv, EXT_MINT_CLOSE_AUTHORITY).ok_or(ProgramError::InvalidAccountData)?;
396 if ext.len() < 32 {
397 return Err(ProgramError::InvalidAccountData);
398 }
399 if crate::address::keys_eq_bytes(&ext[..32], expected.as_array()) {
400 Ok(())
401 } else {
402 Err(ProgramError::IncorrectAuthority)
403 }
404}
405
406#[inline]
408pub fn require_permanent_delegate(mint: &AccountView<'_>, expected: &Address) -> ProgramResult {
409 let data = mint
410 .try_borrow()
411 .map_err(|_| ProgramError::AccountBorrowFailed)?;
412 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
413 let ext =
414 find_extension(tlv, EXT_PERMANENT_DELEGATE).ok_or(ProgramError::InvalidAccountData)?;
415 if ext.len() < 32 {
416 return Err(ProgramError::InvalidAccountData);
417 }
418 if crate::address::keys_eq_bytes(&ext[..32], expected.as_array()) {
419 Ok(())
420 } else {
421 Err(ProgramError::IncorrectAuthority)
422 }
423}
424
425#[inline]
431pub fn require_transfer_hook_program(mint: &AccountView<'_>, expected: &Address) -> ProgramResult {
432 let data = mint
433 .try_borrow()
434 .map_err(|_| ProgramError::AccountBorrowFailed)?;
435 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
436 let ext = find_extension(tlv, EXT_TRANSFER_HOOK).ok_or(ProgramError::InvalidAccountData)?;
437 if ext.len() < 64 {
438 return Err(ProgramError::InvalidAccountData);
439 }
440 if crate::address::keys_eq_bytes(&ext[32..64], expected.as_array()) {
441 Ok(())
442 } else {
443 Err(ProgramError::IncorrectProgramId)
444 }
445}
446
447#[inline]
449pub fn require_transfer_hook_authority(
450 mint: &AccountView<'_>,
451 expected: &Address,
452) -> ProgramResult {
453 let data = mint
454 .try_borrow()
455 .map_err(|_| ProgramError::AccountBorrowFailed)?;
456 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
457 let ext = find_extension(tlv, EXT_TRANSFER_HOOK).ok_or(ProgramError::InvalidAccountData)?;
458 if ext.len() < 32 {
459 return Err(ProgramError::InvalidAccountData);
460 }
461 if crate::address::keys_eq_bytes(&ext[..32], expected.as_array()) {
462 Ok(())
463 } else {
464 Err(ProgramError::IncorrectAuthority)
465 }
466}
467
468#[inline]
472pub fn require_metadata_pointer_address(
473 mint: &AccountView<'_>,
474 expected: &Address,
475) -> ProgramResult {
476 let data = mint
477 .try_borrow()
478 .map_err(|_| ProgramError::AccountBorrowFailed)?;
479 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
480 let ext = find_extension(tlv, EXT_METADATA_POINTER).ok_or(ProgramError::InvalidAccountData)?;
481 if ext.len() < 64 {
482 return Err(ProgramError::InvalidAccountData);
483 }
484 if crate::address::keys_eq_bytes(&ext[32..64], expected.as_array()) {
485 Ok(())
486 } else {
487 Err(ProgramError::InvalidAccountData)
488 }
489}
490
491#[inline]
493pub fn require_metadata_pointer_authority(
494 mint: &AccountView<'_>,
495 expected: &Address,
496) -> ProgramResult {
497 let data = mint
498 .try_borrow()
499 .map_err(|_| ProgramError::AccountBorrowFailed)?;
500 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
501 let ext = find_extension(tlv, EXT_METADATA_POINTER).ok_or(ProgramError::InvalidAccountData)?;
502 if ext.len() < 32 {
503 return Err(ProgramError::InvalidAccountData);
504 }
505 if crate::address::keys_eq_bytes(&ext[..32], expected.as_array()) {
506 Ok(())
507 } else {
508 Err(ProgramError::IncorrectAuthority)
509 }
510}
511
512#[inline]
514pub fn require_immutable_owner(token_account: &AccountView<'_>) -> ProgramResult {
515 let data = token_account
516 .try_borrow()
517 .map_err(|_| ProgramError::AccountBorrowFailed)?;
518 let tlv = token_account_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
519 require_extension(tlv, EXT_IMMUTABLE_OWNER)
520}
521
522#[inline]
524pub fn require_cpi_guard(token_account: &AccountView<'_>) -> ProgramResult {
525 let data = token_account
526 .try_borrow()
527 .map_err(|_| ProgramError::AccountBorrowFailed)?;
528 let tlv = token_account_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
529 require_extension(tlv, EXT_CPI_GUARD)
530}
531
532#[inline]
534pub fn require_confidential_transfer_mint(mint: &AccountView<'_>) -> ProgramResult {
535 let data = mint
536 .try_borrow()
537 .map_err(|_| ProgramError::AccountBorrowFailed)?;
538 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
539 require_extension(tlv, EXT_CONFIDENTIAL_TRANSFER_MINT)
540}
541
542#[inline]
544pub fn require_confidential_transfer_account(token_account: &AccountView<'_>) -> ProgramResult {
545 let data = token_account
546 .try_borrow()
547 .map_err(|_| ProgramError::AccountBorrowFailed)?;
548 let tlv = token_account_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
549 require_extension(tlv, EXT_CONFIDENTIAL_TRANSFER_ACCOUNT)
550}
551
552#[inline]
554pub fn require_scaled_ui_amount_config(mint: &AccountView<'_>) -> ProgramResult {
555 let data = mint
556 .try_borrow()
557 .map_err(|_| ProgramError::AccountBorrowFailed)?;
558 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
559 require_extension(tlv, EXT_SCALED_UI_AMOUNT_CONFIG)
560}
561
562#[inline]
566pub fn require_default_account_state(mint: &AccountView<'_>, expected: u8) -> ProgramResult {
567 let data = mint
568 .try_borrow()
569 .map_err(|_| ProgramError::AccountBorrowFailed)?;
570 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
571 let ext =
572 find_extension(tlv, EXT_DEFAULT_ACCOUNT_STATE).ok_or(ProgramError::InvalidAccountData)?;
573 if ext.is_empty() {
574 return Err(ProgramError::InvalidAccountData);
575 }
576 if ext[0] == expected {
577 Ok(())
578 } else {
579 Err(ProgramError::InvalidAccountData)
580 }
581}
582
583#[inline]
587pub fn require_interest_bearing_authority(
588 mint: &AccountView<'_>,
589 expected: &Address,
590) -> ProgramResult {
591 let data = mint
592 .try_borrow()
593 .map_err(|_| ProgramError::AccountBorrowFailed)?;
594 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
595 let ext =
596 find_extension(tlv, EXT_INTEREST_BEARING_CONFIG).ok_or(ProgramError::InvalidAccountData)?;
597 if ext.len() < 32 {
598 return Err(ProgramError::InvalidAccountData);
599 }
600 if crate::address::keys_eq_bytes(&ext[..32], expected.as_array()) {
601 Ok(())
602 } else {
603 Err(ProgramError::IncorrectAuthority)
604 }
605}
606
607#[inline]
611pub fn require_transfer_fee_config_authority(
612 mint: &AccountView<'_>,
613 expected: &Address,
614) -> ProgramResult {
615 let data = mint
616 .try_borrow()
617 .map_err(|_| ProgramError::AccountBorrowFailed)?;
618 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
619 let ext =
620 find_extension(tlv, EXT_TRANSFER_FEE_CONFIG).ok_or(ProgramError::InvalidAccountData)?;
621 if ext.len() < 32 {
622 return Err(ProgramError::InvalidAccountData);
623 }
624 if crate::address::keys_eq_bytes(&ext[..32], expected.as_array()) {
625 Ok(())
626 } else {
627 Err(ProgramError::IncorrectAuthority)
628 }
629}
630
631#[inline]
633pub fn require_transfer_fee_withdraw_authority(
634 mint: &AccountView<'_>,
635 expected: &Address,
636) -> ProgramResult {
637 let data = mint
638 .try_borrow()
639 .map_err(|_| ProgramError::AccountBorrowFailed)?;
640 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
641 let ext =
642 find_extension(tlv, EXT_TRANSFER_FEE_CONFIG).ok_or(ProgramError::InvalidAccountData)?;
643 if ext.len() < 64 {
644 return Err(ProgramError::InvalidAccountData);
645 }
646 if crate::address::keys_eq_bytes(&ext[32..64], expected.as_array()) {
647 Ok(())
648 } else {
649 Err(ProgramError::IncorrectAuthority)
650 }
651}
652
653#[cfg(test)]
654mod tests {
655 extern crate alloc;
656 use super::*;
657
658 fn mint_with_exts(exts: &[(u16, &[u8])]) -> alloc::vec::Vec<u8> {
671 let mut v = alloc::vec![0u8; ACCOUNT_TYPE_OFFSET];
673 v.push(ACCOUNT_TYPE_MINT);
674 for (ty, payload) in exts {
675 v.extend_from_slice(&ty.to_le_bytes());
676 v.extend_from_slice(&(payload.len() as u16).to_le_bytes());
677 v.extend_from_slice(payload);
678 }
679 debug_assert!(v.len() > TLV_OFFSET);
680 v
681 }
682
683 fn one_ext_mint(ext_type: u16, payload: &[u8]) -> alloc::vec::Vec<u8> {
685 mint_with_exts(&[(ext_type, payload)])
686 }
687
688 fn token_account_with_exts(exts: &[(u16, &[u8])]) -> alloc::vec::Vec<u8> {
691 let mut v = alloc::vec![0u8; BASE_TOKEN_LEN];
692 v.push(ACCOUNT_TYPE_TOKEN);
693 for (ty, payload) in exts {
694 v.extend_from_slice(&ty.to_le_bytes());
695 v.extend_from_slice(&(payload.len() as u16).to_le_bytes());
696 v.extend_from_slice(payload);
697 }
698 v
699 }
700
701 #[test]
704 fn offset_constants_match_authoritative_spec() {
705 assert_eq!(BASE_MINT_LEN, 82);
707 assert_eq!(BASE_TOKEN_LEN, 165);
708 assert_eq!(ACCOUNT_TYPE_OFFSET, 165);
709 assert_eq!(TLV_OFFSET, 166);
710 assert_eq!(MINT_EXTENSION_PADDING_START, 82);
711 assert_eq!(MINT_EXTENSION_PADDING_END, 165);
712 assert_eq!(TOKEN_MULTISIG_LEN, 355);
713 assert_eq!(ACCOUNT_TYPE_MINT, 0x01);
714 assert_eq!(ACCOUNT_TYPE_TOKEN, 0x02);
715 assert_eq!(EXT_CONFIDENTIAL_MINT_BURN, 24);
716 assert_eq!(EXT_SCALED_UI_AMOUNT_CONFIG, 25);
717 assert_eq!(EXT_PAUSABLE_CONFIG, 26);
718 assert_eq!(EXT_PAUSABLE_ACCOUNT, 27);
719 assert_eq!(EXT_PERMISSIONED_BURN, 28);
720 }
721
722 #[test]
723 fn real_layout_mint_tlv_region_starts_at_166() {
724 let data = one_ext_mint(EXT_NON_TRANSFERABLE, &[]);
727 let tlv = mint_tlv_region(&data).expect("extended mint must yield TLV region");
728 assert_eq!(tlv.as_ptr() as usize - data.as_ptr() as usize, TLV_OFFSET);
730 assert_eq!(u16::from_le_bytes([tlv[0], tlv[1]]), EXT_NON_TRANSFERABLE);
732 assert_eq!(u16::from_le_bytes([tlv[2], tlv[3]]), 0);
733 }
734
735 #[test]
736 fn real_layout_mint_padding_is_not_treated_as_tlv() {
737 let data = one_ext_mint(EXT_TRANSFER_HOOK, &[0u8; 64]);
744 let tlv = mint_tlv_region(&data).expect("tlv region");
745 assert!(find_extension(tlv, EXT_TRANSFER_HOOK).is_some());
746 }
747
748 #[test]
751 fn find_extension_returns_payload_slice() {
752 let data = one_ext_mint(EXT_NON_TRANSFERABLE, &[]);
753 let tlv = mint_tlv_region(&data).unwrap();
754 assert!(find_extension(tlv, EXT_NON_TRANSFERABLE).is_some());
755 }
756
757 #[test]
758 fn find_extension_returns_none_when_absent() {
759 let data = one_ext_mint(EXT_NON_TRANSFERABLE, &[]);
760 let tlv = mint_tlv_region(&data).unwrap();
761 assert!(find_extension(tlv, EXT_TRANSFER_HOOK).is_none());
762 }
763
764 #[test]
765 fn find_extension_bails_on_malformed_length() {
766 let mut data = alloc::vec![0u8; ACCOUNT_TYPE_OFFSET];
770 data.push(ACCOUNT_TYPE_MINT);
771 data.extend_from_slice(&EXT_TRANSFER_HOOK.to_le_bytes());
772 data.extend_from_slice(&999u16.to_le_bytes());
773 let tlv = mint_tlv_region(&data).unwrap();
774 assert!(find_extension(tlv, EXT_TRANSFER_HOOK).is_none());
775 assert_eq!(
776 forbid_extension(tlv, EXT_TRANSFER_HOOK),
777 Err(ProgramError::InvalidAccountData),
778 "a malformed forbidden extension must not look absent",
779 );
780 }
781
782 #[test]
783 fn allowlist_rejects_unknown_duplicate_and_truncated_entries() {
784 let allowed = [EXT_METADATA_POINTER];
785 let known = mint_with_exts(&[(EXT_METADATA_POINTER, &[0u8; 64])]);
786 validate_extension_allowlist(mint_tlv_region(&known).unwrap(), &allowed).unwrap();
787
788 let unknown = mint_with_exts(&[(MAX_KNOWN_EXTENSION_TYPE + 1, &[])]);
789 assert_eq!(
790 validate_extension_allowlist(mint_tlv_region(&unknown).unwrap(), &allowed),
791 Err(ProgramError::InvalidAccountData),
792 );
793
794 let duplicate = mint_with_exts(&[
795 (EXT_METADATA_POINTER, &[0u8; 64]),
796 (EXT_METADATA_POINTER, &[0u8; 64]),
797 ]);
798 assert_eq!(
799 validate_extension_allowlist(mint_tlv_region(&duplicate).unwrap(), &allowed),
800 Err(ProgramError::InvalidAccountData),
801 );
802
803 let truncated = [EXT_METADATA_POINTER as u8, 0, 4, 0, 1];
804 assert_eq!(
805 validate_extension_allowlist(&truncated, &allowed),
806 Err(ProgramError::InvalidAccountData),
807 );
808 }
809
810 #[test]
811 fn find_extension_finds_second_entry() {
812 let data = mint_with_exts(&[
813 (EXT_METADATA_POINTER, &[1u8; 64]),
814 (EXT_PERMANENT_DELEGATE, &[2u8; 32]),
815 ]);
816 let tlv = mint_tlv_region(&data).unwrap();
817 let perm = find_extension(tlv, EXT_PERMANENT_DELEGATE).unwrap();
818 assert_eq!(perm, &[2u8; 32]);
819 }
820
821 #[test]
822 fn extension_policy_requires_and_forbids_extensions() {
823 let data = mint_with_exts(&[
824 (EXT_CONFIDENTIAL_TRANSFER_MINT, &[0u8; 1]),
825 (EXT_SCALED_UI_AMOUNT_CONFIG, &[0u8; 1]),
826 ]);
827 let tlv = mint_tlv_region(&data).unwrap();
828 let policy = ExtensionPolicy::new(
829 &[EXT_CONFIDENTIAL_TRANSFER_MINT, EXT_SCALED_UI_AMOUNT_CONFIG],
830 &[EXT_TRANSFER_HOOK],
831 );
832
833 validate_extension_policy(tlv, &policy).unwrap();
834
835 let rejected = ExtensionPolicy::new(
836 &[EXT_CONFIDENTIAL_TRANSFER_MINT],
837 &[EXT_SCALED_UI_AMOUNT_CONFIG],
838 );
839 assert_eq!(
840 validate_extension_policy(tlv, &rejected),
841 Err(ProgramError::InvalidAccountData)
842 );
843 }
844
845 #[test]
846 fn token_account_policy_sees_cpi_guard_and_confidential_account() {
847 let data = token_account_with_exts(&[
848 (EXT_CPI_GUARD, &[]),
849 (EXT_CONFIDENTIAL_TRANSFER_ACCOUNT, &[0u8; 1]),
850 ]);
851 let tlv = token_account_tlv_region(&data).unwrap();
852
853 validate_extension_policy(
854 tlv,
855 &ExtensionPolicy::new(&[EXT_CPI_GUARD, EXT_CONFIDENTIAL_TRANSFER_ACCOUNT], &[]),
856 )
857 .unwrap();
858 }
859
860 #[test]
863 fn mint_tlv_region_rejects_short_account() {
864 let data = alloc::vec![0u8; 40];
866 assert!(mint_tlv_region(&data).is_none());
867 let data = alloc::vec![0u8; TLV_OFFSET];
868 assert!(mint_tlv_region(&data).is_none());
869 }
870
871 #[test]
872 fn mint_tlv_region_rejects_wrong_account_kind() {
873 let mut data = alloc::vec![0u8; ACCOUNT_TYPE_OFFSET];
876 data.push(ACCOUNT_TYPE_TOKEN);
877 data.push(0); assert!(mint_tlv_region(&data).is_none());
879 }
880
881 #[test]
882 fn tlv_regions_reject_multisig_collision_and_dirty_mint_padding() {
883 let mut multisig = alloc::vec![0u8; TOKEN_MULTISIG_LEN];
884 multisig[ACCOUNT_TYPE_OFFSET] = ACCOUNT_TYPE_MINT;
885 assert!(mint_tlv_region(&multisig).is_none());
886 multisig[ACCOUNT_TYPE_OFFSET] = ACCOUNT_TYPE_TOKEN;
887 assert!(token_account_tlv_region(&multisig).is_none());
888
889 let mut dirty_mint = one_ext_mint(EXT_NON_TRANSFERABLE, &[]);
890 dirty_mint[MINT_EXTENSION_PADDING_START] = 1;
891 assert!(mint_tlv_region(&dirty_mint).is_none());
892 }
893
894 #[test]
895 fn mint_tlv_region_accepts_zero_kind_byte() {
896 let mut data = alloc::vec![0u8; ACCOUNT_TYPE_OFFSET];
899 data.push(0u8);
900 data.push(0); assert!(mint_tlv_region(&data).is_some());
902 }
903
904 #[test]
905 fn token_account_tlv_region_accepts_zero_kind_byte() {
906 let mut data = alloc::vec![0u8; BASE_TOKEN_LEN];
907 data.push(0u8);
908 data.push(0); assert!(token_account_tlv_region(&data).is_some());
910 }
911
912 #[test]
913 fn token_account_tlv_region_rejects_mint_kind() {
914 let mut data = alloc::vec![0u8; BASE_TOKEN_LEN];
915 data.push(ACCOUNT_TYPE_MINT);
916 data.push(0);
917 assert!(token_account_tlv_region(&data).is_none());
918 }
919
920 #[test]
921 fn token_account_tlv_region_returns_real_tlv() {
922 let data = token_account_with_exts(&[(EXT_IMMUTABLE_OWNER, &[])]);
923 let tlv = token_account_tlv_region(&data).unwrap();
924 assert_eq!(tlv.as_ptr() as usize - data.as_ptr() as usize, TLV_OFFSET);
925 assert!(find_extension(tlv, EXT_IMMUTABLE_OWNER).is_some());
926 }
927}