1use bytemuck::Pod;
9use solana_address::Address;
10use solana_program::program_error::ProgramError;
11use spl_token_2022_interface::error::TokenError;
12use spl_token_2022_interface::extension::pausable::PausableConfig;
13use spl_token_2022_interface::extension::{
14 AccountType, BaseStateWithExtensions, Extension, PodStateWithExtensions,
15};
16use spl_token_2022_interface::pod::PodMint;
17
18use crate::constants::{TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID};
19use crate::math::amm::MintFee;
20
21pub use spl_token_2022_interface::extension::transfer_fee::{
22 TransferFee, TransferFeeConfig,
23};
24
25const TLV_HEADER_LEN: usize = 4;
27
28pub fn mint_extensions<'a>(
34 data: &'a [u8],
35 owner: &Address,
36) -> Result<Option<PodStateWithExtensions<'a, PodMint>>, ProgramError> {
37 if owner == &TOKEN_PROGRAM_ID {
38 spl_token_mint(data)?;
39 return Ok(None);
40 }
41 if owner != &TOKEN_2022_PROGRAM_ID {
42 return Err(ProgramError::IncorrectProgramId);
43 }
44 PodStateWithExtensions::<PodMint>::unpack(data).map(Some)
45}
46
47pub(crate) fn spl_token_mint(data: &[u8]) -> Result<&PodMint, ProgramError> {
50 if data.len() != size_of::<PodMint>() {
51 return Err(ProgramError::InvalidAccountData);
52 }
53 Ok(PodStateWithExtensions::<PodMint>::unpack(data)?.base)
54}
55
56pub fn extension<'a, V: Extension + Pod>(
62 mint: &'a PodStateWithExtensions<'_, PodMint>,
63) -> Result<Option<&'a V>, ProgramError> {
64 match mint.get_extension::<V>() {
65 Ok(value) => Ok(Some(value)),
66 Err(error) if error == TokenError::ExtensionNotFound.into() => Ok(None),
68 Err(ProgramError::InvalidAccountData)
72 if V::TYPE.get_account_type() == AccountType::Mint
73 && ends_cleanly(mint.get_tlv_data()) =>
74 {
75 Ok(None)
76 }
77 Err(error) => Err(error),
78 }
79}
80
81fn ends_cleanly(tlv: &[u8]) -> bool {
85 let mut rest = tlv;
86 loop {
87 let Some(&[type_low, type_high]) = rest.get(..2) else {
88 return true;
89 };
90 if u16::from_le_bytes([type_low, type_high]) == 0 {
91 return true;
92 }
93 let Some(&[length_low, length_high]) = rest.get(2..TLV_HEADER_LEN)
94 else {
95 return false;
96 };
97 let length = usize::from(u16::from_le_bytes([length_low, length_high]));
98 let Some(next) = TLV_HEADER_LEN
99 .checked_add(length)
100 .and_then(|end| rest.get(end..))
101 else {
102 return false;
103 };
104 rest = next;
105 }
106}
107
108#[must_use]
109pub fn mint_fee(fee: &TransferFee) -> MintFee {
110 MintFee {
111 bps: fee.transfer_fee_basis_points.into(),
112 maximum_fee: fee.maximum_fee.into(),
113 }
114}
115
116#[derive(Clone, Copy, Debug, PartialEq, Eq)]
118pub struct FeeSchedule {
119 pub older: MintFee,
120 pub newer: MintFee,
121 pub newer_epoch: u64,
123 pub read_epoch: u64,
125}
126
127impl FeeSchedule {
128 pub(crate) fn of(config: &TransferFeeConfig, read_epoch: u64) -> Self {
130 Self {
131 older: mint_fee(&config.older_transfer_fee),
132 newer: mint_fee(&config.newer_transfer_fee),
133 newer_epoch: config.newer_transfer_fee.epoch.into(),
134 read_epoch,
135 }
136 }
137
138 #[must_use]
140 pub const fn fee(&self) -> MintFee {
141 self.fee_at(self.read_epoch)
142 }
143
144 #[must_use]
147 pub const fn fee_at(&self, epoch: u64) -> MintFee {
148 if epoch >= self.newer_epoch {
149 self.newer
150 } else {
151 self.older
152 }
153 }
154}
155
156pub fn mint_fee_at_epoch(
159 data: &[u8],
160 owner: &Address,
161 epoch: u64,
162) -> Result<Option<MintFee>, ProgramError> {
163 let Some(mint) = mint_extensions(data, owner)? else {
164 return Ok(None);
165 };
166 Ok(extension::<TransferFeeConfig>(&mint)?
167 .map(|config| mint_fee(config.get_epoch_fee(epoch))))
168}
169
170pub fn mint_paused(data: &[u8], owner: &Address) -> Result<bool, ProgramError> {
173 let Some(mint) = mint_extensions(data, owner)? else {
174 return Ok(false);
175 };
176 Ok(extension::<PausableConfig>(&mint)?
177 .is_some_and(|config| bool::from(config.paused)))
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 use spl_token_2022_interface::extension::transfer_fee::TransferFeeAmount;
185
186 const IS_INITIALIZED_OFFSET: usize = 45;
188
189 const ACCOUNT_TYPE_OFFSET: usize = 165;
191
192 const TLV_START: usize = 166;
193
194 const FEE_TYPE: u16 = 1;
195
196 const FUTURE_TYPE: u16 = 250;
198
199 const NEWER: Option<MintFee> = Some(MintFee {
200 bps: 250,
201 maximum_fee: 2_000,
202 });
203
204 fn entry(epoch: u64, maximum_fee: u64, basis_points: u16) -> TransferFee {
205 TransferFee {
206 epoch: epoch.into(),
207 maximum_fee: maximum_fee.into(),
208 transfer_fee_basis_points: basis_points.into(),
209 }
210 }
211
212 fn fee_payload(authority: Option<Address>) -> Vec<u8> {
213 bytemuck::bytes_of(&TransferFeeConfig {
214 transfer_fee_config_authority: authority.try_into().unwrap(),
215 older_transfer_fee: entry(5, 1_000, 100),
216 newer_transfer_fee: entry(7, 2_000, 250),
217 ..TransferFeeConfig::default()
218 })
219 .to_vec()
220 }
221
222 fn other_extensions() -> Vec<(u16, Vec<u8>)> {
224 vec![(18, vec![0; 64]), (19, vec![0; 90])]
225 }
226
227 fn tlv_image(entries: &[(u16, Vec<u8>)]) -> Vec<u8> {
228 let mut data = vec![0_u8; ACCOUNT_TYPE_OFFSET];
229 data[IS_INITIALIZED_OFFSET] = 1;
230 data.push(1);
232 for (extension_type, payload) in entries {
233 data.extend_from_slice(&extension_type.to_le_bytes());
234 data.extend_from_slice(
235 &u16::try_from(payload.len()).unwrap().to_le_bytes(),
236 );
237 data.extend_from_slice(payload);
238 }
239 data
240 }
241
242 fn fee(data: &[u8]) -> Result<Option<MintFee>, ProgramError> {
243 mint_fee_at_epoch(data, &TOKEN_2022_PROGRAM_ID, 7)
244 }
245
246 #[test]
247 fn the_walk_finds_the_config_wherever_it_sits() {
248 for at in 0..=2 {
249 let mut entries = other_extensions();
250 entries.insert(at, (FEE_TYPE, fee_payload(None)));
251 assert_eq!(fee(&tlv_image(&entries)), Ok(NEWER), "at {at}");
252 }
253 }
254
255 #[test]
256 fn the_owner_picks_the_reader() {
257 let image = tlv_image(&[(FEE_TYPE, fee_payload(None))]);
258 assert_eq!(fee(&image), Ok(NEWER));
259 let mut spl_mint = [0; 82];
261 spl_mint[45] = 1;
262 assert_eq!(
263 mint_fee_at_epoch(&spl_mint, &TOKEN_PROGRAM_ID, 7),
264 Ok(None)
265 );
266 assert_eq!(
268 mint_fee_at_epoch(&image, &TOKEN_PROGRAM_ID, 7),
269 Err(ProgramError::InvalidAccountData)
270 );
271 assert_eq!(
272 mint_fee_at_epoch(&spl_mint.map(|_| 0), &TOKEN_PROGRAM_ID, 7),
273 Err(ProgramError::UninitializedAccount)
274 );
275 assert_eq!(
276 mint_fee_at_epoch(&image, &crate::constants::SYSTEM_PROGRAM_ID, 7),
277 Err(ProgramError::IncorrectProgramId)
278 );
279 }
280
281 const PAUSABLE_TYPE: u16 = 26;
282
283 fn pausable_payload(paused: bool) -> Vec<u8> {
284 bytemuck::bytes_of(&PausableConfig {
285 paused: paused.into(),
286 ..PausableConfig::default()
287 })
288 .to_vec()
289 }
290
291 #[test]
292 fn the_pause_reads_off_the_pausable_config() {
293 for paused in [false, true] {
294 let mut entries = other_extensions();
295 entries.push((PAUSABLE_TYPE, pausable_payload(paused)));
296 assert_eq!(
297 mint_paused(&tlv_image(&entries), &TOKEN_2022_PROGRAM_ID),
298 Ok(paused),
299 "paused {paused}"
300 );
301 }
302 assert_eq!(
303 mint_paused(
304 &tlv_image(&other_extensions()),
305 &TOKEN_2022_PROGRAM_ID
306 ),
307 Ok(false)
308 );
309 }
310
311 #[test]
312 fn the_owner_picks_the_pause_reader() {
313 let mut spl_mint = [0; 82];
314 spl_mint[IS_INITIALIZED_OFFSET] = 1;
315 assert_eq!(mint_paused(&spl_mint, &TOKEN_PROGRAM_ID), Ok(false));
316 let paused = tlv_image(&[(PAUSABLE_TYPE, pausable_payload(true))]);
317 assert_eq!(
318 mint_paused(&paused, &crate::constants::SYSTEM_PROGRAM_ID),
319 Err(ProgramError::IncorrectProgramId)
320 );
321 }
322
323 #[test]
324 fn the_newer_entry_applies_from_its_own_epoch() {
325 let image = tlv_image(&[(FEE_TYPE, fee_payload(None))]);
326 assert_eq!(
327 mint_fee_at_epoch(&image, &TOKEN_2022_PROGRAM_ID, 6),
328 Ok(Some(MintFee {
329 bps: 100,
330 maximum_fee: 1_000,
331 }))
332 );
333 assert_eq!(fee(&image), Ok(NEWER));
334 }
335
336 #[test]
337 fn a_revoked_authority_reads_as_none() {
338 for authority in [None, Some(Address::new_from_array([9; 32]))] {
339 let image = tlv_image(&[(FEE_TYPE, fee_payload(authority))]);
340 let mint = mint_extensions(&image, &TOKEN_2022_PROGRAM_ID)
341 .unwrap()
342 .unwrap();
343 let config =
344 extension::<TransferFeeConfig>(&mint).unwrap().unwrap();
345 assert_eq!(
346 Option::from(config.transfer_fee_config_authority),
347 authority
348 );
349 }
350 }
351
352 #[test]
353 fn a_bare_mint_has_no_tlv_region_to_walk() {
354 let mut bare = tlv_image(&[]);
355 bare.truncate(82);
356 assert_eq!(fee(&bare), Ok(None));
357 bare[IS_INITIALIZED_OFFSET] = 0;
358 assert_eq!(fee(&bare), Err(ProgramError::UninitializedAccount));
359 }
360
361 #[test]
364 fn a_region_that_ends_cleanly_carries_no_config() {
365 for tail in [
366 vec![],
367 vec![0],
368 vec![0; 2],
369 vec![0; 3],
370 vec![0; 512],
371 vec![7],
372 ] {
373 let mut image = tlv_image(&other_extensions());
374 image.extend_from_slice(&tail);
375 assert_eq!(fee(&image), Ok(None), "tail {tail:?}");
376 }
377 }
378
379 #[test]
382 fn a_cut_entry_past_the_config_is_not_read() {
383 let mut image = tlv_image(&[(FEE_TYPE, fee_payload(None))]);
384 image.extend_from_slice(&[18, 0, 64, 0]);
385 assert_eq!(fee(&image), Ok(NEWER));
386 }
387
388 #[test]
391 fn an_account_extension_is_not_read_off_a_mint() {
392 let image = tlv_image(&other_extensions());
393 let mint = mint_extensions(&image, &TOKEN_2022_PROGRAM_ID)
394 .unwrap()
395 .unwrap();
396 assert_eq!(
397 extension::<TransferFeeAmount>(&mint),
398 Err(ProgramError::InvalidAccountData)
399 );
400 }
401
402 #[test]
405 fn a_future_extension_type_leaves_the_mint_readable() {
406 let future = (FUTURE_TYPE, vec![0; 16]);
407 let fee_free = tlv_image(&[(18, vec![0; 64]), future.clone()]);
408 let mint = mint_extensions(&fee_free, &TOKEN_2022_PROGRAM_ID)
409 .unwrap()
410 .unwrap();
411 assert_eq!(
412 mint.get_extension_types(),
413 Err(ProgramError::InvalidAccountData)
414 );
415 assert_eq!(extension::<TransferFeeConfig>(&mint), Ok(None));
416 let fee_after = tlv_image(&[future, (FEE_TYPE, fee_payload(None))]);
417 assert_eq!(fee(&fee_after), Ok(NEWER));
418 }
419
420 #[test]
421 fn a_cut_region_is_malformed() {
422 let mut short = tlv_image(&[]);
423 short.truncate(ACCOUNT_TYPE_OFFSET);
424 let mut header = tlv_image(&other_extensions());
425 header.truncate(TLV_START + 4 + 64 + 2);
426 let mut overrun = tlv_image(&[(18, vec![0; 64])]);
427 overrun.truncate(TLV_START + 4 + 32);
428 let mut payload = tlv_image(&other_extensions());
429 payload.extend_from_slice(&[18, 0, 64, 0]);
430 let mut config = tlv_image(&[(FEE_TYPE, fee_payload(None))]);
431 config.pop();
432 for (label, image) in [
433 ("no account type", short),
434 ("cut header", header),
435 ("length past the end", overrun),
436 ("payload never arrives", payload),
437 ("config payload cut", config),
438 ] {
439 assert_eq!(
440 fee(&image),
441 Err(ProgramError::InvalidAccountData),
442 "{label}"
443 );
444 }
445 }
446
447 #[test]
448 fn a_config_payload_of_the_wrong_length_is_malformed() {
449 for length in [100_usize, 116] {
450 let image = tlv_image(&[(FEE_TYPE, vec![0; length])]);
451 assert_eq!(
452 fee(&image),
453 Err(ProgramError::InvalidArgument),
454 "payload length {length}"
455 );
456 }
457 }
458
459 const OLDER: MintFee = MintFee {
460 bps: 100,
461 maximum_fee: 1_000,
462 };
463
464 fn schedule(read_epoch: u64) -> FeeSchedule {
466 let config: TransferFeeConfig =
467 bytemuck::pod_read_unaligned(&fee_payload(None));
468 FeeSchedule::of(&config, read_epoch)
469 }
470
471 #[test]
472 fn a_schedule_holds_both_entries_and_the_read_epoch() {
473 assert_eq!(
474 schedule(6),
475 FeeSchedule {
476 older: OLDER,
477 newer: NEWER.unwrap(),
478 newer_epoch: 7,
479 read_epoch: 6,
480 }
481 );
482 }
483
484 #[test]
485 fn the_newer_fee_applies_from_newer_epoch() {
486 let schedule = schedule(0);
487 assert_eq!(schedule.fee_at(6), OLDER);
488 assert_eq!(schedule.fee_at(7), NEWER.unwrap());
489 assert_eq!(schedule.fee_at(8), NEWER.unwrap());
490 assert_eq!(schedule.fee_at(0), OLDER);
491 assert_eq!(schedule.fee_at(u64::MAX), NEWER.unwrap());
492 }
493
494 #[test]
495 fn fee_is_the_fee_at_the_read_epoch() {
496 for (read_epoch, fee) in [
497 (0, OLDER),
498 (6, OLDER),
499 (7, NEWER.unwrap()),
500 (8, NEWER.unwrap()),
501 ] {
502 let schedule = schedule(read_epoch);
503 assert_eq!(schedule.fee(), fee, "read at {read_epoch}");
504 assert_eq!(schedule.fee(), schedule.fee_at(read_epoch));
505 }
506 }
507
508 #[test]
510 fn fee_at_matches_get_epoch_fee() {
511 let config: TransferFeeConfig =
512 bytemuck::pod_read_unaligned(&fee_payload(None));
513 let schedule = FeeSchedule::of(&config, 0);
514 for epoch in [0, 5, 6, 7, 8, u64::MAX] {
515 assert_eq!(
516 schedule.fee_at(epoch),
517 mint_fee(config.get_epoch_fee(epoch)),
518 "epoch {epoch}"
519 );
520 }
521 }
522
523 #[test]
524 fn a_non_mint_account_type_is_malformed() {
525 let mut image = tlv_image(&[(FEE_TYPE, fee_payload(None))]);
526 image[ACCOUNT_TYPE_OFFSET] = 2;
528 assert_eq!(fee(&image), Err(ProgramError::InvalidAccountData));
529 }
530}