1use crate::{account::AccountView, address::Address, error::ProgramError, result::ProgramResult};
47
48pub const EXT_UNINITIALIZED: u16 = 0;
51pub const EXT_TRANSFER_FEE_CONFIG: u16 = 1;
52pub const EXT_TRANSFER_FEE_AMOUNT: u16 = 2;
53pub const EXT_MINT_CLOSE_AUTHORITY: u16 = 3;
54pub const EXT_CONFIDENTIAL_TRANSFER_MINT: u16 = 4;
55pub const EXT_CONFIDENTIAL_TRANSFER_ACCOUNT: u16 = 5;
56pub const EXT_DEFAULT_ACCOUNT_STATE: u16 = 6;
57pub const EXT_IMMUTABLE_OWNER: u16 = 7;
58pub const EXT_MEMO_TRANSFER: u16 = 8;
59pub const EXT_NON_TRANSFERABLE: u16 = 9;
60pub const EXT_INTEREST_BEARING_CONFIG: u16 = 10;
61pub const EXT_CPI_GUARD: u16 = 11;
62pub const EXT_PERMANENT_DELEGATE: u16 = 12;
63pub const EXT_NON_TRANSFERABLE_ACCOUNT: u16 = 13;
64pub const EXT_TRANSFER_HOOK: u16 = 14;
65pub const EXT_TRANSFER_HOOK_ACCOUNT: u16 = 15;
66pub const EXT_CONFIDENTIAL_TRANSFER_FEE_CONFIG: u16 = 16;
67pub const EXT_CONFIDENTIAL_TRANSFER_FEE_AMOUNT: u16 = 17;
68pub const EXT_METADATA_POINTER: u16 = 18;
69pub const EXT_TOKEN_METADATA: u16 = 19;
70pub const EXT_GROUP_POINTER: u16 = 20;
71pub const EXT_TOKEN_GROUP: u16 = 21;
72pub const EXT_GROUP_MEMBER_POINTER: u16 = 22;
73pub const EXT_TOKEN_GROUP_MEMBER: u16 = 23;
74pub const EXT_SCALED_UI_AMOUNT_CONFIG: u16 = 24;
75pub const EXT_PAUSABLE_CONFIG: u16 = 25;
76pub const EXT_PAUSABLE_ACCOUNT: u16 = 26;
77
78pub const ACCOUNT_TYPE_MINT: u8 = 0x01;
80pub const ACCOUNT_TYPE_TOKEN: u8 = 0x02;
82
83pub const BASE_MINT_LEN: usize = 82;
90pub const BASE_TOKEN_LEN: usize = 165;
96pub const ACCOUNT_TYPE_OFFSET: usize = BASE_TOKEN_LEN;
99pub const TLV_OFFSET: usize = ACCOUNT_TYPE_OFFSET + 1;
102pub const MINT_EXTENSION_PADDING_START: usize = BASE_MINT_LEN;
106pub const MINT_EXTENSION_PADDING_END: usize = ACCOUNT_TYPE_OFFSET;
108
109#[inline]
125pub fn find_extension<'a>(tlv_bytes: &'a [u8], ext_type: u16) -> Option<&'a [u8]> {
126 let mut cursor = 0usize;
127 while cursor + 4 <= tlv_bytes.len() {
128 let t = u16::from_le_bytes([tlv_bytes[cursor], tlv_bytes[cursor + 1]]);
129 let len = u16::from_le_bytes([tlv_bytes[cursor + 2], tlv_bytes[cursor + 3]]) as usize;
130 let data_start = cursor + 4;
131 let data_end = data_start + len;
132 if data_end > tlv_bytes.len() {
133 return None;
134 }
135 if t == ext_type {
136 return Some(&tlv_bytes[data_start..data_end]);
137 }
138 if t == EXT_UNINITIALIZED {
139 return None;
144 }
145 cursor = data_end;
146 }
147 None
148}
149
150#[inline]
173pub fn mint_tlv_region(data: &[u8]) -> Option<&[u8]> {
174 if data.len() <= TLV_OFFSET {
175 return None;
176 }
177 let kind = data[ACCOUNT_TYPE_OFFSET];
178 if kind != ACCOUNT_TYPE_MINT && kind != 0 {
179 return None;
180 }
181 Some(&data[TLV_OFFSET..])
182}
183
184#[inline]
192pub fn token_account_tlv_region(data: &[u8]) -> Option<&[u8]> {
193 if data.len() <= TLV_OFFSET {
194 return None;
195 }
196 let kind = data[ACCOUNT_TYPE_OFFSET];
197 if kind != ACCOUNT_TYPE_TOKEN && kind != 0 {
198 return None;
199 }
200 Some(&data[TLV_OFFSET..])
201}
202
203#[derive(Clone, Copy, Debug, PartialEq, Eq)]
210pub struct ExtensionPolicy<'a> {
211 pub required: &'a [u16],
212 pub forbidden: &'a [u16],
213}
214
215impl<'a> ExtensionPolicy<'a> {
216 #[inline]
217 pub const fn new(required: &'a [u16], forbidden: &'a [u16]) -> Self {
218 Self {
219 required,
220 forbidden,
221 }
222 }
223}
224
225#[inline]
226pub fn require_extension(tlv: &[u8], ext_type: u16) -> ProgramResult {
227 if find_extension(tlv, ext_type).is_some() {
228 Ok(())
229 } else {
230 Err(ProgramError::InvalidAccountData)
231 }
232}
233
234#[inline]
235pub fn forbid_extension(tlv: &[u8], ext_type: u16) -> ProgramResult {
236 if find_extension(tlv, ext_type).is_none() {
237 Ok(())
238 } else {
239 Err(ProgramError::InvalidAccountData)
240 }
241}
242
243#[inline]
244pub fn validate_extension_policy(tlv: &[u8], policy: &ExtensionPolicy<'_>) -> ProgramResult {
245 for ext_type in policy.required {
246 require_extension(tlv, *ext_type)?;
247 }
248 for ext_type in policy.forbidden {
249 forbid_extension(tlv, *ext_type)?;
250 }
251 Ok(())
252}
253
254#[inline]
260pub fn require_non_transferable(mint: &AccountView) -> ProgramResult {
261 let data = mint
262 .try_borrow()
263 .map_err(|_| ProgramError::AccountBorrowFailed)?;
264 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
265 if find_extension(tlv, EXT_NON_TRANSFERABLE).is_some() {
266 Ok(())
267 } else {
268 Err(ProgramError::InvalidAccountData)
269 }
270}
271
272#[inline]
274pub fn require_mint_close_authority(mint: &AccountView, expected: &Address) -> ProgramResult {
275 let data = mint
276 .try_borrow()
277 .map_err(|_| ProgramError::AccountBorrowFailed)?;
278 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
279 let ext =
280 find_extension(tlv, EXT_MINT_CLOSE_AUTHORITY).ok_or(ProgramError::InvalidAccountData)?;
281 if ext.len() < 32 {
282 return Err(ProgramError::InvalidAccountData);
283 }
284 if &ext[..32] == expected.as_array() {
285 Ok(())
286 } else {
287 Err(ProgramError::IncorrectAuthority)
288 }
289}
290
291#[inline]
293pub fn require_permanent_delegate(mint: &AccountView, expected: &Address) -> ProgramResult {
294 let data = mint
295 .try_borrow()
296 .map_err(|_| ProgramError::AccountBorrowFailed)?;
297 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
298 let ext =
299 find_extension(tlv, EXT_PERMANENT_DELEGATE).ok_or(ProgramError::InvalidAccountData)?;
300 if ext.len() < 32 {
301 return Err(ProgramError::InvalidAccountData);
302 }
303 if &ext[..32] == expected.as_array() {
304 Ok(())
305 } else {
306 Err(ProgramError::IncorrectAuthority)
307 }
308}
309
310#[inline]
316pub fn require_transfer_hook_program(mint: &AccountView, expected: &Address) -> ProgramResult {
317 let data = mint
318 .try_borrow()
319 .map_err(|_| ProgramError::AccountBorrowFailed)?;
320 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
321 let ext = find_extension(tlv, EXT_TRANSFER_HOOK).ok_or(ProgramError::InvalidAccountData)?;
322 if ext.len() < 64 {
323 return Err(ProgramError::InvalidAccountData);
324 }
325 if &ext[32..64] == expected.as_array() {
326 Ok(())
327 } else {
328 Err(ProgramError::IncorrectProgramId)
329 }
330}
331
332#[inline]
334pub fn require_transfer_hook_authority(mint: &AccountView, expected: &Address) -> ProgramResult {
335 let data = mint
336 .try_borrow()
337 .map_err(|_| ProgramError::AccountBorrowFailed)?;
338 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
339 let ext = find_extension(tlv, EXT_TRANSFER_HOOK).ok_or(ProgramError::InvalidAccountData)?;
340 if ext.len() < 32 {
341 return Err(ProgramError::InvalidAccountData);
342 }
343 if &ext[..32] == expected.as_array() {
344 Ok(())
345 } else {
346 Err(ProgramError::IncorrectAuthority)
347 }
348}
349
350#[inline]
354pub fn require_metadata_pointer_address(mint: &AccountView, expected: &Address) -> ProgramResult {
355 let data = mint
356 .try_borrow()
357 .map_err(|_| ProgramError::AccountBorrowFailed)?;
358 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
359 let ext = find_extension(tlv, EXT_METADATA_POINTER).ok_or(ProgramError::InvalidAccountData)?;
360 if ext.len() < 64 {
361 return Err(ProgramError::InvalidAccountData);
362 }
363 if &ext[32..64] == expected.as_array() {
364 Ok(())
365 } else {
366 Err(ProgramError::InvalidAccountData)
367 }
368}
369
370#[inline]
372pub fn require_metadata_pointer_authority(mint: &AccountView, expected: &Address) -> ProgramResult {
373 let data = mint
374 .try_borrow()
375 .map_err(|_| ProgramError::AccountBorrowFailed)?;
376 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
377 let ext = find_extension(tlv, EXT_METADATA_POINTER).ok_or(ProgramError::InvalidAccountData)?;
378 if ext.len() < 32 {
379 return Err(ProgramError::InvalidAccountData);
380 }
381 if &ext[..32] == expected.as_array() {
382 Ok(())
383 } else {
384 Err(ProgramError::IncorrectAuthority)
385 }
386}
387
388#[inline]
390pub fn require_immutable_owner(token_account: &AccountView) -> ProgramResult {
391 let data = token_account
392 .try_borrow()
393 .map_err(|_| ProgramError::AccountBorrowFailed)?;
394 let tlv = token_account_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
395 require_extension(tlv, EXT_IMMUTABLE_OWNER)
396}
397
398#[inline]
400pub fn require_cpi_guard(token_account: &AccountView) -> ProgramResult {
401 let data = token_account
402 .try_borrow()
403 .map_err(|_| ProgramError::AccountBorrowFailed)?;
404 let tlv = token_account_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
405 require_extension(tlv, EXT_CPI_GUARD)
406}
407
408#[inline]
410pub fn require_confidential_transfer_mint(mint: &AccountView) -> ProgramResult {
411 let data = mint
412 .try_borrow()
413 .map_err(|_| ProgramError::AccountBorrowFailed)?;
414 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
415 require_extension(tlv, EXT_CONFIDENTIAL_TRANSFER_MINT)
416}
417
418#[inline]
420pub fn require_confidential_transfer_account(token_account: &AccountView) -> ProgramResult {
421 let data = token_account
422 .try_borrow()
423 .map_err(|_| ProgramError::AccountBorrowFailed)?;
424 let tlv = token_account_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
425 require_extension(tlv, EXT_CONFIDENTIAL_TRANSFER_ACCOUNT)
426}
427
428#[inline]
430pub fn require_scaled_ui_amount_config(mint: &AccountView) -> ProgramResult {
431 let data = mint
432 .try_borrow()
433 .map_err(|_| ProgramError::AccountBorrowFailed)?;
434 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
435 require_extension(tlv, EXT_SCALED_UI_AMOUNT_CONFIG)
436}
437
438#[inline]
442pub fn require_default_account_state(mint: &AccountView, expected: u8) -> ProgramResult {
443 let data = mint
444 .try_borrow()
445 .map_err(|_| ProgramError::AccountBorrowFailed)?;
446 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
447 let ext =
448 find_extension(tlv, EXT_DEFAULT_ACCOUNT_STATE).ok_or(ProgramError::InvalidAccountData)?;
449 if ext.is_empty() {
450 return Err(ProgramError::InvalidAccountData);
451 }
452 if ext[0] == expected {
453 Ok(())
454 } else {
455 Err(ProgramError::InvalidAccountData)
456 }
457}
458
459#[inline]
463pub fn require_interest_bearing_authority(mint: &AccountView, expected: &Address) -> ProgramResult {
464 let data = mint
465 .try_borrow()
466 .map_err(|_| ProgramError::AccountBorrowFailed)?;
467 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
468 let ext =
469 find_extension(tlv, EXT_INTEREST_BEARING_CONFIG).ok_or(ProgramError::InvalidAccountData)?;
470 if ext.len() < 32 {
471 return Err(ProgramError::InvalidAccountData);
472 }
473 if &ext[..32] == expected.as_array() {
474 Ok(())
475 } else {
476 Err(ProgramError::IncorrectAuthority)
477 }
478}
479
480#[inline]
484pub fn require_transfer_fee_config_authority(
485 mint: &AccountView,
486 expected: &Address,
487) -> ProgramResult {
488 let data = mint
489 .try_borrow()
490 .map_err(|_| ProgramError::AccountBorrowFailed)?;
491 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
492 let ext =
493 find_extension(tlv, EXT_TRANSFER_FEE_CONFIG).ok_or(ProgramError::InvalidAccountData)?;
494 if ext.len() < 32 {
495 return Err(ProgramError::InvalidAccountData);
496 }
497 if &ext[..32] == expected.as_array() {
498 Ok(())
499 } else {
500 Err(ProgramError::IncorrectAuthority)
501 }
502}
503
504#[inline]
506pub fn require_transfer_fee_withdraw_authority(
507 mint: &AccountView,
508 expected: &Address,
509) -> ProgramResult {
510 let data = mint
511 .try_borrow()
512 .map_err(|_| ProgramError::AccountBorrowFailed)?;
513 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
514 let ext =
515 find_extension(tlv, EXT_TRANSFER_FEE_CONFIG).ok_or(ProgramError::InvalidAccountData)?;
516 if ext.len() < 64 {
517 return Err(ProgramError::InvalidAccountData);
518 }
519 if &ext[32..64] == expected.as_array() {
520 Ok(())
521 } else {
522 Err(ProgramError::IncorrectAuthority)
523 }
524}
525
526#[cfg(test)]
527mod tests {
528 extern crate alloc;
529 use super::*;
530
531 fn mint_with_exts(exts: &[(u16, &[u8])]) -> alloc::vec::Vec<u8> {
544 let mut v = alloc::vec![0u8; ACCOUNT_TYPE_OFFSET];
546 v.push(ACCOUNT_TYPE_MINT);
547 for (ty, payload) in exts {
548 v.extend_from_slice(&ty.to_le_bytes());
549 v.extend_from_slice(&(payload.len() as u16).to_le_bytes());
550 v.extend_from_slice(payload);
551 }
552 debug_assert!(v.len() > TLV_OFFSET);
553 v
554 }
555
556 fn one_ext_mint(ext_type: u16, payload: &[u8]) -> alloc::vec::Vec<u8> {
558 mint_with_exts(&[(ext_type, payload)])
559 }
560
561 fn token_account_with_exts(exts: &[(u16, &[u8])]) -> alloc::vec::Vec<u8> {
564 let mut v = alloc::vec![0u8; BASE_TOKEN_LEN];
565 v.push(ACCOUNT_TYPE_TOKEN);
566 for (ty, payload) in exts {
567 v.extend_from_slice(&ty.to_le_bytes());
568 v.extend_from_slice(&(payload.len() as u16).to_le_bytes());
569 v.extend_from_slice(payload);
570 }
571 v
572 }
573
574 #[test]
577 fn offset_constants_match_authoritative_spec() {
578 assert_eq!(BASE_MINT_LEN, 82);
580 assert_eq!(BASE_TOKEN_LEN, 165);
581 assert_eq!(ACCOUNT_TYPE_OFFSET, 165);
582 assert_eq!(TLV_OFFSET, 166);
583 assert_eq!(MINT_EXTENSION_PADDING_START, 82);
584 assert_eq!(MINT_EXTENSION_PADDING_END, 165);
585 assert_eq!(ACCOUNT_TYPE_MINT, 0x01);
586 assert_eq!(ACCOUNT_TYPE_TOKEN, 0x02);
587 }
588
589 #[test]
590 fn real_layout_mint_tlv_region_starts_at_166() {
591 let data = one_ext_mint(EXT_NON_TRANSFERABLE, &[]);
594 let tlv = mint_tlv_region(&data).expect("extended mint must yield TLV region");
595 assert_eq!(tlv.as_ptr() as usize - data.as_ptr() as usize, TLV_OFFSET);
597 assert_eq!(u16::from_le_bytes([tlv[0], tlv[1]]), EXT_NON_TRANSFERABLE);
599 assert_eq!(u16::from_le_bytes([tlv[2], tlv[3]]), 0);
600 }
601
602 #[test]
603 fn real_layout_mint_padding_is_not_treated_as_tlv() {
604 let data = one_ext_mint(EXT_TRANSFER_HOOK, &[0u8; 64]);
611 let tlv = mint_tlv_region(&data).expect("tlv region");
612 assert!(find_extension(tlv, EXT_TRANSFER_HOOK).is_some());
613 }
614
615 #[test]
618 fn find_extension_returns_payload_slice() {
619 let data = one_ext_mint(EXT_NON_TRANSFERABLE, &[]);
620 let tlv = mint_tlv_region(&data).unwrap();
621 assert!(find_extension(tlv, EXT_NON_TRANSFERABLE).is_some());
622 }
623
624 #[test]
625 fn find_extension_returns_none_when_absent() {
626 let data = one_ext_mint(EXT_NON_TRANSFERABLE, &[]);
627 let tlv = mint_tlv_region(&data).unwrap();
628 assert!(find_extension(tlv, EXT_TRANSFER_HOOK).is_none());
629 }
630
631 #[test]
632 fn find_extension_bails_on_malformed_length() {
633 let mut data = alloc::vec![0u8; ACCOUNT_TYPE_OFFSET];
637 data.push(ACCOUNT_TYPE_MINT);
638 data.extend_from_slice(&EXT_TRANSFER_HOOK.to_le_bytes());
639 data.extend_from_slice(&999u16.to_le_bytes());
640 let tlv = mint_tlv_region(&data).unwrap();
641 assert!(find_extension(tlv, EXT_TRANSFER_HOOK).is_none());
642 }
643
644 #[test]
645 fn find_extension_finds_second_entry() {
646 let data = mint_with_exts(&[
647 (EXT_METADATA_POINTER, &[1u8; 64]),
648 (EXT_PERMANENT_DELEGATE, &[2u8; 32]),
649 ]);
650 let tlv = mint_tlv_region(&data).unwrap();
651 let perm = find_extension(tlv, EXT_PERMANENT_DELEGATE).unwrap();
652 assert_eq!(perm, &[2u8; 32]);
653 }
654
655 #[test]
656 fn extension_policy_requires_and_forbids_extensions() {
657 let data = mint_with_exts(&[
658 (EXT_CONFIDENTIAL_TRANSFER_MINT, &[0u8; 1]),
659 (EXT_SCALED_UI_AMOUNT_CONFIG, &[0u8; 1]),
660 ]);
661 let tlv = mint_tlv_region(&data).unwrap();
662 let policy = ExtensionPolicy::new(
663 &[EXT_CONFIDENTIAL_TRANSFER_MINT, EXT_SCALED_UI_AMOUNT_CONFIG],
664 &[EXT_TRANSFER_HOOK],
665 );
666
667 validate_extension_policy(tlv, &policy).unwrap();
668
669 let rejected = ExtensionPolicy::new(
670 &[EXT_CONFIDENTIAL_TRANSFER_MINT],
671 &[EXT_SCALED_UI_AMOUNT_CONFIG],
672 );
673 assert_eq!(
674 validate_extension_policy(tlv, &rejected),
675 Err(ProgramError::InvalidAccountData)
676 );
677 }
678
679 #[test]
680 fn token_account_policy_sees_cpi_guard_and_confidential_account() {
681 let data = token_account_with_exts(&[
682 (EXT_CPI_GUARD, &[]),
683 (EXT_CONFIDENTIAL_TRANSFER_ACCOUNT, &[0u8; 1]),
684 ]);
685 let tlv = token_account_tlv_region(&data).unwrap();
686
687 validate_extension_policy(
688 tlv,
689 &ExtensionPolicy::new(&[EXT_CPI_GUARD, EXT_CONFIDENTIAL_TRANSFER_ACCOUNT], &[]),
690 )
691 .unwrap();
692 }
693
694 #[test]
697 fn mint_tlv_region_rejects_short_account() {
698 let data = alloc::vec![0u8; 40];
700 assert!(mint_tlv_region(&data).is_none());
701 let data = alloc::vec![0u8; TLV_OFFSET];
702 assert!(mint_tlv_region(&data).is_none());
703 }
704
705 #[test]
706 fn mint_tlv_region_rejects_wrong_account_kind() {
707 let mut data = alloc::vec![0u8; ACCOUNT_TYPE_OFFSET];
710 data.push(ACCOUNT_TYPE_TOKEN);
711 data.push(0); assert!(mint_tlv_region(&data).is_none());
713 }
714
715 #[test]
716 fn mint_tlv_region_accepts_zero_kind_byte() {
717 let mut data = alloc::vec![0u8; ACCOUNT_TYPE_OFFSET];
720 data.push(0u8);
721 data.push(0); assert!(mint_tlv_region(&data).is_some());
723 }
724
725 #[test]
726 fn token_account_tlv_region_accepts_zero_kind_byte() {
727 let mut data = alloc::vec![0u8; BASE_TOKEN_LEN];
728 data.push(0u8);
729 data.push(0); assert!(token_account_tlv_region(&data).is_some());
731 }
732
733 #[test]
734 fn token_account_tlv_region_rejects_mint_kind() {
735 let mut data = alloc::vec![0u8; BASE_TOKEN_LEN];
736 data.push(ACCOUNT_TYPE_MINT);
737 data.push(0);
738 assert!(token_account_tlv_region(&data).is_none());
739 }
740
741 #[test]
742 fn token_account_tlv_region_returns_real_tlv() {
743 let data = token_account_with_exts(&[(EXT_IMMUTABLE_OWNER, &[])]);
744 let tlv = token_account_tlv_region(&data).unwrap();
745 assert_eq!(tlv.as_ptr() as usize - data.as_ptr() as usize, TLV_OFFSET);
746 assert!(find_extension(tlv, EXT_IMMUTABLE_OWNER).is_some());
747 }
748}