hopper_runtime/
token_2022_ext.rs1use crate::{error::ProgramError, result::ProgramResult, address::Address, account::AccountView};
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#[inline]
209pub fn require_non_transferable(mint: &AccountView) -> ProgramResult {
210 let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
211 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
212 if find_extension(tlv, EXT_NON_TRANSFERABLE).is_some() {
213 Ok(())
214 } else {
215 Err(ProgramError::InvalidAccountData)
216 }
217}
218
219#[inline]
221pub fn require_mint_close_authority(
222 mint: &AccountView,
223 expected: &Address,
224) -> ProgramResult {
225 let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
226 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
227 let ext = find_extension(tlv, EXT_MINT_CLOSE_AUTHORITY).ok_or(ProgramError::InvalidAccountData)?;
228 if ext.len() < 32 {
229 return Err(ProgramError::InvalidAccountData);
230 }
231 if &ext[..32] == expected.as_array() {
232 Ok(())
233 } else {
234 Err(ProgramError::IncorrectAuthority)
235 }
236}
237
238#[inline]
240pub fn require_permanent_delegate(
241 mint: &AccountView,
242 expected: &Address,
243) -> ProgramResult {
244 let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
245 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
246 let ext = find_extension(tlv, EXT_PERMANENT_DELEGATE).ok_or(ProgramError::InvalidAccountData)?;
247 if ext.len() < 32 {
248 return Err(ProgramError::InvalidAccountData);
249 }
250 if &ext[..32] == expected.as_array() {
251 Ok(())
252 } else {
253 Err(ProgramError::IncorrectAuthority)
254 }
255}
256
257#[inline]
263pub fn require_transfer_hook_program(
264 mint: &AccountView,
265 expected: &Address,
266) -> ProgramResult {
267 let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
268 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
269 let ext = find_extension(tlv, EXT_TRANSFER_HOOK).ok_or(ProgramError::InvalidAccountData)?;
270 if ext.len() < 64 {
271 return Err(ProgramError::InvalidAccountData);
272 }
273 if &ext[32..64] == expected.as_array() {
274 Ok(())
275 } else {
276 Err(ProgramError::IncorrectProgramId)
277 }
278}
279
280#[inline]
282pub fn require_transfer_hook_authority(
283 mint: &AccountView,
284 expected: &Address,
285) -> ProgramResult {
286 let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
287 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
288 let ext = find_extension(tlv, EXT_TRANSFER_HOOK).ok_or(ProgramError::InvalidAccountData)?;
289 if ext.len() < 32 {
290 return Err(ProgramError::InvalidAccountData);
291 }
292 if &ext[..32] == expected.as_array() {
293 Ok(())
294 } else {
295 Err(ProgramError::IncorrectAuthority)
296 }
297}
298
299#[inline]
303pub fn require_metadata_pointer_address(
304 mint: &AccountView,
305 expected: &Address,
306) -> ProgramResult {
307 let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
308 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
309 let ext = find_extension(tlv, EXT_METADATA_POINTER).ok_or(ProgramError::InvalidAccountData)?;
310 if ext.len() < 64 {
311 return Err(ProgramError::InvalidAccountData);
312 }
313 if &ext[32..64] == expected.as_array() {
314 Ok(())
315 } else {
316 Err(ProgramError::InvalidAccountData)
317 }
318}
319
320#[inline]
322pub fn require_metadata_pointer_authority(
323 mint: &AccountView,
324 expected: &Address,
325) -> ProgramResult {
326 let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
327 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
328 let ext = find_extension(tlv, EXT_METADATA_POINTER).ok_or(ProgramError::InvalidAccountData)?;
329 if ext.len() < 32 {
330 return Err(ProgramError::InvalidAccountData);
331 }
332 if &ext[..32] == expected.as_array() {
333 Ok(())
334 } else {
335 Err(ProgramError::IncorrectAuthority)
336 }
337}
338
339#[inline]
341pub fn require_immutable_owner(token_account: &AccountView) -> ProgramResult {
342 let data = token_account
343 .try_borrow()
344 .map_err(|_| ProgramError::AccountBorrowFailed)?;
345 let tlv = token_account_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
346 if find_extension(tlv, EXT_IMMUTABLE_OWNER).is_some() {
347 Ok(())
348 } else {
349 Err(ProgramError::InvalidAccountData)
350 }
351}
352
353#[inline]
357pub fn require_default_account_state(mint: &AccountView, expected: u8) -> ProgramResult {
358 let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
359 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
360 let ext = find_extension(tlv, EXT_DEFAULT_ACCOUNT_STATE)
361 .ok_or(ProgramError::InvalidAccountData)?;
362 if ext.is_empty() {
363 return Err(ProgramError::InvalidAccountData);
364 }
365 if ext[0] == expected {
366 Ok(())
367 } else {
368 Err(ProgramError::InvalidAccountData)
369 }
370}
371
372#[inline]
376pub fn require_interest_bearing_authority(
377 mint: &AccountView,
378 expected: &Address,
379) -> ProgramResult {
380 let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
381 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
382 let ext = find_extension(tlv, EXT_INTEREST_BEARING_CONFIG)
383 .ok_or(ProgramError::InvalidAccountData)?;
384 if ext.len() < 32 {
385 return Err(ProgramError::InvalidAccountData);
386 }
387 if &ext[..32] == expected.as_array() {
388 Ok(())
389 } else {
390 Err(ProgramError::IncorrectAuthority)
391 }
392}
393
394#[inline]
398pub fn require_transfer_fee_config_authority(
399 mint: &AccountView,
400 expected: &Address,
401) -> ProgramResult {
402 let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
403 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
404 let ext = find_extension(tlv, EXT_TRANSFER_FEE_CONFIG)
405 .ok_or(ProgramError::InvalidAccountData)?;
406 if ext.len() < 32 {
407 return Err(ProgramError::InvalidAccountData);
408 }
409 if &ext[..32] == expected.as_array() {
410 Ok(())
411 } else {
412 Err(ProgramError::IncorrectAuthority)
413 }
414}
415
416#[inline]
418pub fn require_transfer_fee_withdraw_authority(
419 mint: &AccountView,
420 expected: &Address,
421) -> ProgramResult {
422 let data = mint.try_borrow().map_err(|_| ProgramError::AccountBorrowFailed)?;
423 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
424 let ext = find_extension(tlv, EXT_TRANSFER_FEE_CONFIG)
425 .ok_or(ProgramError::InvalidAccountData)?;
426 if ext.len() < 64 {
427 return Err(ProgramError::InvalidAccountData);
428 }
429 if &ext[32..64] == expected.as_array() {
430 Ok(())
431 } else {
432 Err(ProgramError::IncorrectAuthority)
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 extern crate alloc;
439 use super::*;
440
441 fn mint_with_exts(exts: &[(u16, &[u8])]) -> alloc::vec::Vec<u8> {
454 let mut v = alloc::vec![0u8; ACCOUNT_TYPE_OFFSET];
456 v.push(ACCOUNT_TYPE_MINT);
457 for (ty, payload) in exts {
458 v.extend_from_slice(&ty.to_le_bytes());
459 v.extend_from_slice(&(payload.len() as u16).to_le_bytes());
460 v.extend_from_slice(payload);
461 }
462 debug_assert!(v.len() > TLV_OFFSET);
463 v
464 }
465
466 fn one_ext_mint(ext_type: u16, payload: &[u8]) -> alloc::vec::Vec<u8> {
468 mint_with_exts(&[(ext_type, payload)])
469 }
470
471 fn token_account_with_exts(exts: &[(u16, &[u8])]) -> alloc::vec::Vec<u8> {
474 let mut v = alloc::vec![0u8; BASE_TOKEN_LEN];
475 v.push(ACCOUNT_TYPE_TOKEN);
476 for (ty, payload) in exts {
477 v.extend_from_slice(&ty.to_le_bytes());
478 v.extend_from_slice(&(payload.len() as u16).to_le_bytes());
479 v.extend_from_slice(payload);
480 }
481 v
482 }
483
484 #[test]
487 fn offset_constants_match_authoritative_spec() {
488 assert_eq!(BASE_MINT_LEN, 82);
490 assert_eq!(BASE_TOKEN_LEN, 165);
491 assert_eq!(ACCOUNT_TYPE_OFFSET, 165);
492 assert_eq!(TLV_OFFSET, 166);
493 assert_eq!(MINT_EXTENSION_PADDING_START, 82);
494 assert_eq!(MINT_EXTENSION_PADDING_END, 165);
495 assert_eq!(ACCOUNT_TYPE_MINT, 0x01);
496 assert_eq!(ACCOUNT_TYPE_TOKEN, 0x02);
497 }
498
499 #[test]
500 fn real_layout_mint_tlv_region_starts_at_166() {
501 let data = one_ext_mint(EXT_NON_TRANSFERABLE, &[]);
504 let tlv = mint_tlv_region(&data).expect("extended mint must yield TLV region");
505 assert_eq!(tlv.as_ptr() as usize - data.as_ptr() as usize, TLV_OFFSET);
507 assert_eq!(u16::from_le_bytes([tlv[0], tlv[1]]), EXT_NON_TRANSFERABLE);
509 assert_eq!(u16::from_le_bytes([tlv[2], tlv[3]]), 0);
510 }
511
512 #[test]
513 fn real_layout_mint_padding_is_not_treated_as_tlv() {
514 let data = one_ext_mint(EXT_TRANSFER_HOOK, &[0u8; 64]);
521 let tlv = mint_tlv_region(&data).expect("tlv region");
522 assert!(find_extension(tlv, EXT_TRANSFER_HOOK).is_some());
523 }
524
525 #[test]
528 fn find_extension_returns_payload_slice() {
529 let data = one_ext_mint(EXT_NON_TRANSFERABLE, &[]);
530 let tlv = mint_tlv_region(&data).unwrap();
531 assert!(find_extension(tlv, EXT_NON_TRANSFERABLE).is_some());
532 }
533
534 #[test]
535 fn find_extension_returns_none_when_absent() {
536 let data = one_ext_mint(EXT_NON_TRANSFERABLE, &[]);
537 let tlv = mint_tlv_region(&data).unwrap();
538 assert!(find_extension(tlv, EXT_TRANSFER_HOOK).is_none());
539 }
540
541 #[test]
542 fn find_extension_bails_on_malformed_length() {
543 let mut data = alloc::vec![0u8; ACCOUNT_TYPE_OFFSET];
547 data.push(ACCOUNT_TYPE_MINT);
548 data.extend_from_slice(&EXT_TRANSFER_HOOK.to_le_bytes());
549 data.extend_from_slice(&999u16.to_le_bytes());
550 let tlv = mint_tlv_region(&data).unwrap();
551 assert!(find_extension(tlv, EXT_TRANSFER_HOOK).is_none());
552 }
553
554 #[test]
555 fn find_extension_finds_second_entry() {
556 let data = mint_with_exts(&[
557 (EXT_METADATA_POINTER, &[1u8; 64]),
558 (EXT_PERMANENT_DELEGATE, &[2u8; 32]),
559 ]);
560 let tlv = mint_tlv_region(&data).unwrap();
561 let perm = find_extension(tlv, EXT_PERMANENT_DELEGATE).unwrap();
562 assert_eq!(perm, &[2u8; 32]);
563 }
564
565 #[test]
568 fn mint_tlv_region_rejects_short_account() {
569 let data = alloc::vec![0u8; 40];
571 assert!(mint_tlv_region(&data).is_none());
572 let data = alloc::vec![0u8; TLV_OFFSET];
573 assert!(mint_tlv_region(&data).is_none());
574 }
575
576 #[test]
577 fn mint_tlv_region_rejects_wrong_account_kind() {
578 let mut data = alloc::vec![0u8; ACCOUNT_TYPE_OFFSET];
581 data.push(ACCOUNT_TYPE_TOKEN);
582 data.push(0); assert!(mint_tlv_region(&data).is_none());
584 }
585
586 #[test]
587 fn mint_tlv_region_accepts_zero_kind_byte() {
588 let mut data = alloc::vec![0u8; ACCOUNT_TYPE_OFFSET];
591 data.push(0u8);
592 data.push(0); assert!(mint_tlv_region(&data).is_some());
594 }
595
596 #[test]
597 fn token_account_tlv_region_accepts_zero_kind_byte() {
598 let mut data = alloc::vec![0u8; BASE_TOKEN_LEN];
599 data.push(0u8);
600 data.push(0); assert!(token_account_tlv_region(&data).is_some());
602 }
603
604 #[test]
605 fn token_account_tlv_region_rejects_mint_kind() {
606 let mut data = alloc::vec![0u8; BASE_TOKEN_LEN];
607 data.push(ACCOUNT_TYPE_MINT);
608 data.push(0);
609 assert!(token_account_tlv_region(&data).is_none());
610 }
611
612 #[test]
613 fn token_account_tlv_region_returns_real_tlv() {
614 let data = token_account_with_exts(&[(EXT_IMMUTABLE_OWNER, &[])]);
615 let tlv = token_account_tlv_region(&data).unwrap();
616 assert_eq!(tlv.as_ptr() as usize - data.as_ptr() as usize, TLV_OFFSET);
617 assert!(find_extension(tlv, EXT_IMMUTABLE_OWNER).is_some());
618 }
619}