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