hopper_runtime/
token_2022_ext.rs1use 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#[inline]
209pub fn require_non_transferable(mint: &AccountView) -> ProgramResult {
210 let data = mint
211 .try_borrow()
212 .map_err(|_| ProgramError::AccountBorrowFailed)?;
213 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
214 if find_extension(tlv, EXT_NON_TRANSFERABLE).is_some() {
215 Ok(())
216 } else {
217 Err(ProgramError::InvalidAccountData)
218 }
219}
220
221#[inline]
223pub fn require_mint_close_authority(mint: &AccountView, expected: &Address) -> ProgramResult {
224 let data = mint
225 .try_borrow()
226 .map_err(|_| ProgramError::AccountBorrowFailed)?;
227 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
228 let ext =
229 find_extension(tlv, EXT_MINT_CLOSE_AUTHORITY).ok_or(ProgramError::InvalidAccountData)?;
230 if ext.len() < 32 {
231 return Err(ProgramError::InvalidAccountData);
232 }
233 if &ext[..32] == expected.as_array() {
234 Ok(())
235 } else {
236 Err(ProgramError::IncorrectAuthority)
237 }
238}
239
240#[inline]
242pub fn require_permanent_delegate(mint: &AccountView, expected: &Address) -> ProgramResult {
243 let data = mint
244 .try_borrow()
245 .map_err(|_| ProgramError::AccountBorrowFailed)?;
246 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
247 let ext =
248 find_extension(tlv, EXT_PERMANENT_DELEGATE).ok_or(ProgramError::InvalidAccountData)?;
249 if ext.len() < 32 {
250 return Err(ProgramError::InvalidAccountData);
251 }
252 if &ext[..32] == expected.as_array() {
253 Ok(())
254 } else {
255 Err(ProgramError::IncorrectAuthority)
256 }
257}
258
259#[inline]
265pub fn require_transfer_hook_program(mint: &AccountView, expected: &Address) -> ProgramResult {
266 let data = mint
267 .try_borrow()
268 .map_err(|_| ProgramError::AccountBorrowFailed)?;
269 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
270 let ext = find_extension(tlv, EXT_TRANSFER_HOOK).ok_or(ProgramError::InvalidAccountData)?;
271 if ext.len() < 64 {
272 return Err(ProgramError::InvalidAccountData);
273 }
274 if &ext[32..64] == expected.as_array() {
275 Ok(())
276 } else {
277 Err(ProgramError::IncorrectProgramId)
278 }
279}
280
281#[inline]
283pub fn require_transfer_hook_authority(mint: &AccountView, expected: &Address) -> ProgramResult {
284 let data = mint
285 .try_borrow()
286 .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(mint: &AccountView, expected: &Address) -> ProgramResult {
304 let data = mint
305 .try_borrow()
306 .map_err(|_| ProgramError::AccountBorrowFailed)?;
307 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
308 let ext = find_extension(tlv, EXT_METADATA_POINTER).ok_or(ProgramError::InvalidAccountData)?;
309 if ext.len() < 64 {
310 return Err(ProgramError::InvalidAccountData);
311 }
312 if &ext[32..64] == expected.as_array() {
313 Ok(())
314 } else {
315 Err(ProgramError::InvalidAccountData)
316 }
317}
318
319#[inline]
321pub fn require_metadata_pointer_authority(mint: &AccountView, expected: &Address) -> ProgramResult {
322 let data = mint
323 .try_borrow()
324 .map_err(|_| ProgramError::AccountBorrowFailed)?;
325 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
326 let ext = find_extension(tlv, EXT_METADATA_POINTER).ok_or(ProgramError::InvalidAccountData)?;
327 if ext.len() < 32 {
328 return Err(ProgramError::InvalidAccountData);
329 }
330 if &ext[..32] == expected.as_array() {
331 Ok(())
332 } else {
333 Err(ProgramError::IncorrectAuthority)
334 }
335}
336
337#[inline]
339pub fn require_immutable_owner(token_account: &AccountView) -> ProgramResult {
340 let data = token_account
341 .try_borrow()
342 .map_err(|_| ProgramError::AccountBorrowFailed)?;
343 let tlv = token_account_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
344 if find_extension(tlv, EXT_IMMUTABLE_OWNER).is_some() {
345 Ok(())
346 } else {
347 Err(ProgramError::InvalidAccountData)
348 }
349}
350
351#[inline]
355pub fn require_default_account_state(mint: &AccountView, expected: u8) -> ProgramResult {
356 let data = mint
357 .try_borrow()
358 .map_err(|_| ProgramError::AccountBorrowFailed)?;
359 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
360 let ext =
361 find_extension(tlv, EXT_DEFAULT_ACCOUNT_STATE).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(mint: &AccountView, expected: &Address) -> ProgramResult {
377 let data = mint
378 .try_borrow()
379 .map_err(|_| ProgramError::AccountBorrowFailed)?;
380 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
381 let ext =
382 find_extension(tlv, EXT_INTEREST_BEARING_CONFIG).ok_or(ProgramError::InvalidAccountData)?;
383 if ext.len() < 32 {
384 return Err(ProgramError::InvalidAccountData);
385 }
386 if &ext[..32] == expected.as_array() {
387 Ok(())
388 } else {
389 Err(ProgramError::IncorrectAuthority)
390 }
391}
392
393#[inline]
397pub fn require_transfer_fee_config_authority(
398 mint: &AccountView,
399 expected: &Address,
400) -> ProgramResult {
401 let data = mint
402 .try_borrow()
403 .map_err(|_| ProgramError::AccountBorrowFailed)?;
404 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
405 let ext =
406 find_extension(tlv, EXT_TRANSFER_FEE_CONFIG).ok_or(ProgramError::InvalidAccountData)?;
407 if ext.len() < 32 {
408 return Err(ProgramError::InvalidAccountData);
409 }
410 if &ext[..32] == expected.as_array() {
411 Ok(())
412 } else {
413 Err(ProgramError::IncorrectAuthority)
414 }
415}
416
417#[inline]
419pub fn require_transfer_fee_withdraw_authority(
420 mint: &AccountView,
421 expected: &Address,
422) -> ProgramResult {
423 let data = mint
424 .try_borrow()
425 .map_err(|_| ProgramError::AccountBorrowFailed)?;
426 let tlv = mint_tlv_region(&data).ok_or(ProgramError::InvalidAccountData)?;
427 let ext =
428 find_extension(tlv, EXT_TRANSFER_FEE_CONFIG).ok_or(ProgramError::InvalidAccountData)?;
429 if ext.len() < 64 {
430 return Err(ProgramError::InvalidAccountData);
431 }
432 if &ext[32..64] == expected.as_array() {
433 Ok(())
434 } else {
435 Err(ProgramError::IncorrectAuthority)
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 extern crate alloc;
442 use super::*;
443
444 fn mint_with_exts(exts: &[(u16, &[u8])]) -> alloc::vec::Vec<u8> {
457 let mut v = alloc::vec![0u8; ACCOUNT_TYPE_OFFSET];
459 v.push(ACCOUNT_TYPE_MINT);
460 for (ty, payload) in exts {
461 v.extend_from_slice(&ty.to_le_bytes());
462 v.extend_from_slice(&(payload.len() as u16).to_le_bytes());
463 v.extend_from_slice(payload);
464 }
465 debug_assert!(v.len() > TLV_OFFSET);
466 v
467 }
468
469 fn one_ext_mint(ext_type: u16, payload: &[u8]) -> alloc::vec::Vec<u8> {
471 mint_with_exts(&[(ext_type, payload)])
472 }
473
474 fn token_account_with_exts(exts: &[(u16, &[u8])]) -> alloc::vec::Vec<u8> {
477 let mut v = alloc::vec![0u8; BASE_TOKEN_LEN];
478 v.push(ACCOUNT_TYPE_TOKEN);
479 for (ty, payload) in exts {
480 v.extend_from_slice(&ty.to_le_bytes());
481 v.extend_from_slice(&(payload.len() as u16).to_le_bytes());
482 v.extend_from_slice(payload);
483 }
484 v
485 }
486
487 #[test]
490 fn offset_constants_match_authoritative_spec() {
491 assert_eq!(BASE_MINT_LEN, 82);
493 assert_eq!(BASE_TOKEN_LEN, 165);
494 assert_eq!(ACCOUNT_TYPE_OFFSET, 165);
495 assert_eq!(TLV_OFFSET, 166);
496 assert_eq!(MINT_EXTENSION_PADDING_START, 82);
497 assert_eq!(MINT_EXTENSION_PADDING_END, 165);
498 assert_eq!(ACCOUNT_TYPE_MINT, 0x01);
499 assert_eq!(ACCOUNT_TYPE_TOKEN, 0x02);
500 }
501
502 #[test]
503 fn real_layout_mint_tlv_region_starts_at_166() {
504 let data = one_ext_mint(EXT_NON_TRANSFERABLE, &[]);
507 let tlv = mint_tlv_region(&data).expect("extended mint must yield TLV region");
508 assert_eq!(tlv.as_ptr() as usize - data.as_ptr() as usize, TLV_OFFSET);
510 assert_eq!(u16::from_le_bytes([tlv[0], tlv[1]]), EXT_NON_TRANSFERABLE);
512 assert_eq!(u16::from_le_bytes([tlv[2], tlv[3]]), 0);
513 }
514
515 #[test]
516 fn real_layout_mint_padding_is_not_treated_as_tlv() {
517 let data = one_ext_mint(EXT_TRANSFER_HOOK, &[0u8; 64]);
524 let tlv = mint_tlv_region(&data).expect("tlv region");
525 assert!(find_extension(tlv, EXT_TRANSFER_HOOK).is_some());
526 }
527
528 #[test]
531 fn find_extension_returns_payload_slice() {
532 let data = one_ext_mint(EXT_NON_TRANSFERABLE, &[]);
533 let tlv = mint_tlv_region(&data).unwrap();
534 assert!(find_extension(tlv, EXT_NON_TRANSFERABLE).is_some());
535 }
536
537 #[test]
538 fn find_extension_returns_none_when_absent() {
539 let data = one_ext_mint(EXT_NON_TRANSFERABLE, &[]);
540 let tlv = mint_tlv_region(&data).unwrap();
541 assert!(find_extension(tlv, EXT_TRANSFER_HOOK).is_none());
542 }
543
544 #[test]
545 fn find_extension_bails_on_malformed_length() {
546 let mut data = alloc::vec![0u8; ACCOUNT_TYPE_OFFSET];
550 data.push(ACCOUNT_TYPE_MINT);
551 data.extend_from_slice(&EXT_TRANSFER_HOOK.to_le_bytes());
552 data.extend_from_slice(&999u16.to_le_bytes());
553 let tlv = mint_tlv_region(&data).unwrap();
554 assert!(find_extension(tlv, EXT_TRANSFER_HOOK).is_none());
555 }
556
557 #[test]
558 fn find_extension_finds_second_entry() {
559 let data = mint_with_exts(&[
560 (EXT_METADATA_POINTER, &[1u8; 64]),
561 (EXT_PERMANENT_DELEGATE, &[2u8; 32]),
562 ]);
563 let tlv = mint_tlv_region(&data).unwrap();
564 let perm = find_extension(tlv, EXT_PERMANENT_DELEGATE).unwrap();
565 assert_eq!(perm, &[2u8; 32]);
566 }
567
568 #[test]
571 fn mint_tlv_region_rejects_short_account() {
572 let data = alloc::vec![0u8; 40];
574 assert!(mint_tlv_region(&data).is_none());
575 let data = alloc::vec![0u8; TLV_OFFSET];
576 assert!(mint_tlv_region(&data).is_none());
577 }
578
579 #[test]
580 fn mint_tlv_region_rejects_wrong_account_kind() {
581 let mut data = alloc::vec![0u8; ACCOUNT_TYPE_OFFSET];
584 data.push(ACCOUNT_TYPE_TOKEN);
585 data.push(0); assert!(mint_tlv_region(&data).is_none());
587 }
588
589 #[test]
590 fn mint_tlv_region_accepts_zero_kind_byte() {
591 let mut data = alloc::vec![0u8; ACCOUNT_TYPE_OFFSET];
594 data.push(0u8);
595 data.push(0); assert!(mint_tlv_region(&data).is_some());
597 }
598
599 #[test]
600 fn token_account_tlv_region_accepts_zero_kind_byte() {
601 let mut data = alloc::vec![0u8; BASE_TOKEN_LEN];
602 data.push(0u8);
603 data.push(0); assert!(token_account_tlv_region(&data).is_some());
605 }
606
607 #[test]
608 fn token_account_tlv_region_rejects_mint_kind() {
609 let mut data = alloc::vec![0u8; BASE_TOKEN_LEN];
610 data.push(ACCOUNT_TYPE_MINT);
611 data.push(0);
612 assert!(token_account_tlv_region(&data).is_none());
613 }
614
615 #[test]
616 fn token_account_tlv_region_returns_real_tlv() {
617 let data = token_account_with_exts(&[(EXT_IMMUTABLE_OWNER, &[])]);
618 let tlv = token_account_tlv_region(&data).unwrap();
619 assert_eq!(tlv.as_ptr() as usize - data.as_ptr() as usize, TLV_OFFSET);
620 assert!(find_extension(tlv, EXT_IMMUTABLE_OWNER).is_some());
621 }
622}