1use std::fmt;
6
7use solana_address::Address;
8
9use crate::constants::{TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID};
10use crate::math::amm::MintFee;
11
12const MINT_BASE_LEN: usize = 82;
14
15const ACCOUNT_TYPE_OFFSET: usize = 165;
17
18const ACCOUNT_TYPE_MINT: u8 = 1;
19
20const TLV_START: usize = 166;
21
22const TLV_TYPE_LEN: usize = 2;
23
24const TLV_HEADER_LEN: usize = 4;
25
26const UNINITIALIZED_TYPE: u16 = 0;
27
28const TRANSFER_FEE_CONFIG_TYPE: u16 = 1;
29
30const TRANSFER_FEE_CONFIG_LEN: usize = 108;
33
34const CONFIG_AUTHORITY_OFFSET: usize = 0;
35
36const OLDER_FEE_OFFSET: usize = 72;
37
38const NEWER_FEE_OFFSET: usize = 90;
39
40const FEE_MAXIMUM_OFFSET: usize = 8;
41
42const FEE_BASIS_POINTS_OFFSET: usize = 16;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct TransferFeeEntry {
46 pub epoch: u64,
47 pub maximum_fee: u64,
48 pub basis_points: u16,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct TransferFeeConfig {
53 pub authority: Option<Address>,
55 pub older: TransferFeeEntry,
56 pub newer: TransferFeeEntry,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum TransferFeeDecodeError {
61 UnknownOwner,
62 Malformed,
63}
64
65impl fmt::Display for TransferFeeDecodeError {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 match self {
68 Self::UnknownOwner => {
69 write!(f, "Mint is owned by neither token program")
70 }
71 Self::Malformed => write!(f, "Malformed mint extension data"),
72 }
73 }
74}
75
76impl std::error::Error for TransferFeeDecodeError {}
77
78fn read_u16(payload: &[u8], offset: usize) -> Option<u16> {
79 let end = offset.checked_add(2)?;
80 let &[low, high] = payload.get(offset..end)? else {
81 return None;
82 };
83 Some(u16::from_le_bytes([low, high]))
84}
85
86fn read_u64(payload: &[u8], offset: usize) -> Option<u64> {
87 let end = offset.checked_add(8)?;
88 let bytes: [u8; 8] = payload.get(offset..end)?.try_into().ok()?;
89 Some(u64::from_le_bytes(bytes))
90}
91
92fn read_entry(payload: &[u8], offset: usize) -> Option<TransferFeeEntry> {
93 Some(TransferFeeEntry {
94 epoch: read_u64(payload, offset)?,
95 maximum_fee: read_u64(
96 payload,
97 offset.checked_add(FEE_MAXIMUM_OFFSET)?,
98 )?,
99 basis_points: read_u16(
100 payload,
101 offset.checked_add(FEE_BASIS_POINTS_OFFSET)?,
102 )?,
103 })
104}
105
106fn read_config(payload: &[u8]) -> Option<TransferFeeConfig> {
109 if payload.len() != TRANSFER_FEE_CONFIG_LEN {
110 return None;
111 }
112
113 let end = CONFIG_AUTHORITY_OFFSET.checked_add(32)?;
114 let authority: [u8; 32] =
115 payload.get(CONFIG_AUTHORITY_OFFSET..end)?.try_into().ok()?;
116
117 Some(TransferFeeConfig {
118 authority: (authority != [0; 32])
120 .then(|| Address::new_from_array(authority)),
121 older: read_entry(payload, OLDER_FEE_OFFSET)?,
122 newer: read_entry(payload, NEWER_FEE_OFFSET)?,
123 })
124}
125
126pub fn mint_extensions<'a>(
129 data: &'a [u8],
130 owner: &Address,
131) -> Result<Vec<(u16, &'a [u8])>, TransferFeeDecodeError> {
132 if owner == &TOKEN_PROGRAM_ID {
133 return Ok(Vec::new());
134 }
135 if owner != &TOKEN_2022_PROGRAM_ID {
136 return Err(TransferFeeDecodeError::UnknownOwner);
137 }
138 if data.len() == MINT_BASE_LEN {
139 return Ok(Vec::new());
140 }
141 if data.len() < TLV_START {
142 return Err(TransferFeeDecodeError::Malformed);
143 }
144 if data.get(ACCOUNT_TYPE_OFFSET) != Some(&ACCOUNT_TYPE_MINT) {
146 return Err(TransferFeeDecodeError::Malformed);
147 }
148
149 let mut extensions = Vec::new();
150 let mut offset = TLV_START;
151 while offset < data.len() {
152 let type_end = offset
153 .checked_add(TLV_TYPE_LEN)
154 .ok_or(TransferFeeDecodeError::Malformed)?;
155 let Some(&[type_low, type_high]) = data.get(offset..type_end) else {
157 break;
158 };
159 let extension_type = u16::from_le_bytes([type_low, type_high]);
160 if extension_type == UNINITIALIZED_TYPE {
161 break;
162 }
163
164 let header_end = offset
165 .checked_add(TLV_HEADER_LEN)
166 .ok_or(TransferFeeDecodeError::Malformed)?;
167 let Some(&[length_low, length_high]) = data.get(type_end..header_end)
169 else {
170 return Err(TransferFeeDecodeError::Malformed);
171 };
172
173 let length = usize::from(u16::from_le_bytes([length_low, length_high]));
174 let payload_end = header_end
175 .checked_add(length)
176 .ok_or(TransferFeeDecodeError::Malformed)?;
177 let payload = data
178 .get(header_end..payload_end)
179 .ok_or(TransferFeeDecodeError::Malformed)?;
180 extensions.push((extension_type, payload));
181 offset = payload_end;
182 }
183
184 Ok(extensions)
185}
186
187pub fn decode_transfer_fee_config(
190 data: &[u8],
191 owner: &Address,
192) -> Result<Option<TransferFeeConfig>, TransferFeeDecodeError> {
193 mint_extensions(data, owner)?
194 .into_iter()
195 .find(|(extension_type, _)| *extension_type == TRANSFER_FEE_CONFIG_TYPE)
196 .map(|(_, payload)| {
197 read_config(payload).ok_or(TransferFeeDecodeError::Malformed)
198 })
199 .transpose()
200}
201
202#[must_use]
204pub const fn transfer_fee_at_epoch(
205 config: &TransferFeeConfig,
206 epoch: u64,
207) -> MintFee {
208 let entry = if epoch >= config.newer.epoch {
209 &config.newer
210 } else {
211 &config.older
212 };
213
214 MintFee {
215 bps: entry.basis_points,
216 maximum_fee: entry.maximum_fee,
217 }
218}
219
220pub fn mint_fee_at_epoch(
221 data: &[u8],
222 owner: &Address,
223 epoch: u64,
224) -> Result<Option<MintFee>, TransferFeeDecodeError> {
225 Ok(decode_transfer_fee_config(data, owner)?
226 .map(|config| transfer_fee_at_epoch(&config, epoch)))
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 const OLDER: TransferFeeEntry = TransferFeeEntry {
234 epoch: 5,
235 maximum_fee: 1_000,
236 basis_points: 100,
237 };
238
239 const NEWER: TransferFeeEntry = TransferFeeEntry {
240 epoch: 7,
241 maximum_fee: 2_000,
242 basis_points: 250,
243 };
244
245 const AUTHORITY: [u8; 32] = [9; 32];
246
247 fn other_extensions() -> Vec<(u16, Vec<u8>)> {
249 vec![(18, vec![0; 64]), (19, vec![0; 90])]
250 }
251
252 fn fee_payload(
253 authority: Option<[u8; 32]>,
254 older: TransferFeeEntry,
255 newer: TransferFeeEntry,
256 ) -> Vec<u8> {
257 let mut payload = Vec::with_capacity(TRANSFER_FEE_CONFIG_LEN);
258 payload.extend_from_slice(&authority.unwrap_or([0; 32]));
259 payload.extend_from_slice(&[0; 32]);
261 payload.extend_from_slice(&0_u64.to_le_bytes());
262 for entry in [older, newer] {
263 payload.extend_from_slice(&entry.epoch.to_le_bytes());
264 payload.extend_from_slice(&entry.maximum_fee.to_le_bytes());
265 payload.extend_from_slice(&entry.basis_points.to_le_bytes());
266 }
267 payload
268 }
269
270 fn default_fee_payload() -> Vec<u8> {
271 fee_payload(Some(AUTHORITY), OLDER, NEWER)
272 }
273
274 fn tlv_image(entries: &[(u16, Vec<u8>)]) -> Vec<u8> {
275 let mut data = vec![0_u8; ACCOUNT_TYPE_OFFSET];
276 data.push(ACCOUNT_TYPE_MINT);
277 for (extension_type, payload) in entries {
278 data.extend_from_slice(&extension_type.to_le_bytes());
279 data.extend_from_slice(
280 &u16::try_from(payload.len()).unwrap().to_le_bytes(),
281 );
282 data.extend_from_slice(payload);
283 }
284 data
285 }
286
287 fn decode(
288 data: &[u8],
289 ) -> Result<Option<TransferFeeConfig>, TransferFeeDecodeError> {
290 decode_transfer_fee_config(data, &TOKEN_2022_PROGRAM_ID)
291 }
292
293 fn expected() -> Option<TransferFeeConfig> {
294 Some(TransferFeeConfig {
295 authority: Some(Address::new_from_array(AUTHORITY)),
296 older: OLDER,
297 newer: NEWER,
298 })
299 }
300
301 #[test]
302 fn the_walk_finds_the_config_wherever_it_sits() {
303 let entry = (TRANSFER_FEE_CONFIG_TYPE, default_fee_payload());
304
305 let mut first = vec![entry.clone()];
306 first.extend(other_extensions());
307 assert_eq!(decode(&tlv_image(&first)), Ok(expected()));
308
309 let mut middle = other_extensions();
310 middle.insert(1, entry.clone());
311 assert_eq!(decode(&tlv_image(&middle)), Ok(expected()));
312
313 let mut last = other_extensions();
314 last.push(entry);
315 assert_eq!(decode(&tlv_image(&last)), Ok(expected()));
316 }
317
318 #[test]
319 fn a_revoked_authority_reads_as_none() {
320 let image = tlv_image(&[(
321 TRANSFER_FEE_CONFIG_TYPE,
322 fee_payload(None, OLDER, NEWER),
323 )]);
324
325 assert_eq!(
326 decode(&image),
327 Ok(Some(TransferFeeConfig {
328 authority: None,
329 older: OLDER,
330 newer: NEWER,
331 }))
332 );
333 }
334
335 #[test]
336 fn the_terminator_ends_the_walk() {
337 let mut image = tlv_image(&other_extensions());
338 image.extend(core::iter::repeat_n(0_u8, 512));
339
340 assert_eq!(decode(&image), Ok(None));
341 }
342
343 #[test]
344 fn a_region_ending_on_a_boundary_carries_no_config() {
345 assert_eq!(decode(&tlv_image(&other_extensions())), Ok(None));
346 }
347
348 #[test]
349 fn a_bare_mint_has_no_tlv_region_to_walk() {
350 assert_eq!(decode(&[0_u8; MINT_BASE_LEN]), Ok(None));
351 }
352
353 #[test]
354 fn a_truncated_header_is_malformed() {
355 let mut image = tlv_image(&other_extensions());
356 image.truncate(TLV_START + TLV_HEADER_LEN + 64 + 2);
357
358 assert_eq!(decode(&image), Err(TransferFeeDecodeError::Malformed));
359 }
360
361 #[test]
362 fn a_length_running_past_the_buffer_is_malformed() {
363 let mut image = tlv_image(&[(18, vec![0; 64])]);
364 image.truncate(TLV_START + TLV_HEADER_LEN + 32);
365
366 assert_eq!(decode(&image), Err(TransferFeeDecodeError::Malformed));
367 }
368
369 #[test]
370 fn a_config_payload_of_the_wrong_length_is_malformed() {
371 for length in [TRANSFER_FEE_CONFIG_LEN - 8, TRANSFER_FEE_CONFIG_LEN + 8]
372 {
373 let image =
374 tlv_image(&[(TRANSFER_FEE_CONFIG_TYPE, vec![0; length])]);
375 assert_eq!(
376 decode(&image),
377 Err(TransferFeeDecodeError::Malformed),
378 "payload length {length}"
379 );
380 }
381 }
382
383 #[test]
384 fn a_non_mint_account_type_is_malformed() {
385 let mut image =
386 tlv_image(&[(TRANSFER_FEE_CONFIG_TYPE, default_fee_payload())]);
387 image.splice(ACCOUNT_TYPE_OFFSET..TLV_START, core::iter::once(2_u8));
388
389 assert_eq!(decode(&image), Err(TransferFeeDecodeError::Malformed));
390 }
391
392 #[test]
393 fn a_length_short_of_the_tlv_region_is_malformed() {
394 for length in [MINT_BASE_LEN + 1, ACCOUNT_TYPE_OFFSET] {
395 assert_eq!(
396 decode(&vec![0_u8; length]),
397 Err(TransferFeeDecodeError::Malformed),
398 "account length {length}"
399 );
400 }
401 }
402
403 #[test]
404 fn an_unknown_owner_is_rejected() {
405 let image =
406 tlv_image(&[(TRANSFER_FEE_CONFIG_TYPE, default_fee_payload())]);
407
408 assert_eq!(
409 decode_transfer_fee_config(
410 &image,
411 &crate::constants::SYSTEM_PROGRAM_ID
412 ),
413 Err(TransferFeeDecodeError::UnknownOwner)
414 );
415 }
416
417 #[test]
418 fn a_classic_owner_short_circuits_the_walk() {
419 let image =
420 tlv_image(&[(TRANSFER_FEE_CONFIG_TYPE, default_fee_payload())]);
421
422 assert_eq!(
423 decode_transfer_fee_config(&image, &TOKEN_PROGRAM_ID),
424 Ok(None)
425 );
426 }
427
428 #[test]
430 fn mint_fee_at_epoch_reads_the_entry_for_the_epoch() {
431 let image =
432 tlv_image(&[(TRANSFER_FEE_CONFIG_TYPE, default_fee_payload())]);
433
434 assert_eq!(
435 mint_fee_at_epoch(&image, &TOKEN_2022_PROGRAM_ID, NEWER.epoch - 1),
436 Ok(Some(MintFee {
437 bps: OLDER.basis_points,
438 maximum_fee: OLDER.maximum_fee,
439 }))
440 );
441 assert_eq!(
442 mint_fee_at_epoch(&image, &TOKEN_2022_PROGRAM_ID, NEWER.epoch),
443 Ok(Some(MintFee {
444 bps: NEWER.basis_points,
445 maximum_fee: NEWER.maximum_fee,
446 }))
447 );
448 }
449
450 #[test]
451 fn mint_extensions_lists_every_entry_in_order() {
452 let mut entries = other_extensions();
453 entries.insert(1, (TRANSFER_FEE_CONFIG_TYPE, default_fee_payload()));
454 let mut image = tlv_image(&entries);
455 image.extend(core::iter::repeat_n(0_u8, 64));
456
457 let extensions =
458 mint_extensions(&image, &TOKEN_2022_PROGRAM_ID).unwrap();
459 let expected: Vec<(u16, &[u8])> = entries
460 .iter()
461 .map(|(extension_type, payload)| {
462 (*extension_type, payload.as_slice())
463 })
464 .collect();
465 assert_eq!(extensions, expected);
466 assert_eq!(mint_extensions(&image, &TOKEN_PROGRAM_ID), Ok(Vec::new()));
467 }
468
469 #[test]
473 fn a_short_zero_tail_ends_the_walk() {
474 for tail in 1..TLV_HEADER_LEN {
475 let mut image =
476 tlv_image(&[(TRANSFER_FEE_CONFIG_TYPE, default_fee_payload())]);
477 image.extend(core::iter::repeat_n(0_u8, tail));
478
479 assert_eq!(decode(&image), Ok(expected()), "tail of {tail}");
480 }
481 }
482
483 #[test]
485 fn a_cut_entry_after_the_config_is_malformed() {
486 let mut image =
487 tlv_image(&[(TRANSFER_FEE_CONFIG_TYPE, default_fee_payload())]);
488 image.extend_from_slice(&[18, 0, 64, 0]);
489
490 assert_eq!(decode(&image), Err(TransferFeeDecodeError::Malformed));
491 }
492}