use bytemuck::Pod;
use solana_address::Address;
use solana_program::program_error::ProgramError;
use spl_token_2022_interface::error::TokenError;
use spl_token_2022_interface::extension::{
AccountType, BaseStateWithExtensions, Extension, PodStateWithExtensions,
};
use spl_token_2022_interface::pod::PodMint;
use crate::constants::{TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID};
use crate::math::amm::MintFee;
pub use spl_token_2022_interface::extension::transfer_fee::{
TransferFee, TransferFeeConfig,
};
const TLV_HEADER_LEN: usize = 4;
pub fn mint_extensions<'a>(
data: &'a [u8],
owner: &Address,
) -> Result<Option<PodStateWithExtensions<'a, PodMint>>, ProgramError> {
if owner == &TOKEN_PROGRAM_ID {
return Ok(None);
}
if owner != &TOKEN_2022_PROGRAM_ID {
return Err(ProgramError::IncorrectProgramId);
}
PodStateWithExtensions::<PodMint>::unpack(data).map(Some)
}
pub fn extension<'a, V: Extension + Pod>(
mint: &'a PodStateWithExtensions<'_, PodMint>,
) -> Result<Option<&'a V>, ProgramError> {
match mint.get_extension::<V>() {
Ok(value) => Ok(Some(value)),
Err(error) if error == TokenError::ExtensionNotFound.into() => Ok(None),
Err(ProgramError::InvalidAccountData)
if V::TYPE.get_account_type() == AccountType::Mint
&& ends_cleanly(mint.get_tlv_data()) =>
{
Ok(None)
}
Err(error) => Err(error),
}
}
fn ends_cleanly(tlv: &[u8]) -> bool {
let mut rest = tlv;
loop {
let Some(&[type_low, type_high]) = rest.get(..2) else {
return true;
};
if u16::from_le_bytes([type_low, type_high]) == 0 {
return true;
}
let Some(&[length_low, length_high]) = rest.get(2..TLV_HEADER_LEN)
else {
return false;
};
let length = usize::from(u16::from_le_bytes([length_low, length_high]));
let Some(next) = TLV_HEADER_LEN
.checked_add(length)
.and_then(|end| rest.get(end..))
else {
return false;
};
rest = next;
}
}
#[must_use]
pub fn mint_fee(fee: &TransferFee) -> MintFee {
MintFee {
bps: fee.transfer_fee_basis_points.into(),
maximum_fee: fee.maximum_fee.into(),
}
}
pub fn mint_fee_at_epoch(
data: &[u8],
owner: &Address,
epoch: u64,
) -> Result<Option<MintFee>, ProgramError> {
let Some(mint) = mint_extensions(data, owner)? else {
return Ok(None);
};
Ok(extension::<TransferFeeConfig>(&mint)?
.map(|config| mint_fee(config.get_epoch_fee(epoch))))
}
#[cfg(test)]
mod tests {
use super::*;
use spl_token_2022_interface::extension::transfer_fee::TransferFeeAmount;
const IS_INITIALIZED_OFFSET: usize = 45;
const ACCOUNT_TYPE_OFFSET: usize = 165;
const TLV_START: usize = 166;
const FEE_TYPE: u16 = 1;
const FUTURE_TYPE: u16 = 250;
const NEWER: Option<MintFee> = Some(MintFee {
bps: 250,
maximum_fee: 2_000,
});
fn entry(epoch: u64, maximum_fee: u64, basis_points: u16) -> TransferFee {
TransferFee {
epoch: epoch.into(),
maximum_fee: maximum_fee.into(),
transfer_fee_basis_points: basis_points.into(),
}
}
fn fee_payload(authority: Option<Address>) -> Vec<u8> {
bytemuck::bytes_of(&TransferFeeConfig {
transfer_fee_config_authority: authority.try_into().unwrap(),
older_transfer_fee: entry(5, 1_000, 100),
newer_transfer_fee: entry(7, 2_000, 250),
..TransferFeeConfig::default()
})
.to_vec()
}
fn other_extensions() -> Vec<(u16, Vec<u8>)> {
vec![(18, vec![0; 64]), (19, vec![0; 90])]
}
fn tlv_image(entries: &[(u16, Vec<u8>)]) -> Vec<u8> {
let mut data = vec![0_u8; ACCOUNT_TYPE_OFFSET];
data[IS_INITIALIZED_OFFSET] = 1;
data.push(1);
for (extension_type, payload) in entries {
data.extend_from_slice(&extension_type.to_le_bytes());
data.extend_from_slice(
&u16::try_from(payload.len()).unwrap().to_le_bytes(),
);
data.extend_from_slice(payload);
}
data
}
fn fee(data: &[u8]) -> Result<Option<MintFee>, ProgramError> {
mint_fee_at_epoch(data, &TOKEN_2022_PROGRAM_ID, 7)
}
#[test]
fn the_walk_finds_the_config_wherever_it_sits() {
for at in 0..=2 {
let mut entries = other_extensions();
entries.insert(at, (FEE_TYPE, fee_payload(None)));
assert_eq!(fee(&tlv_image(&entries)), Ok(NEWER), "at {at}");
}
}
#[test]
fn the_owner_picks_the_reader() {
let image = tlv_image(&[(FEE_TYPE, fee_payload(None))]);
assert_eq!(fee(&image), Ok(NEWER));
assert_eq!(mint_fee_at_epoch(&image, &TOKEN_PROGRAM_ID, 7), Ok(None));
assert_eq!(
mint_fee_at_epoch(&image, &crate::constants::SYSTEM_PROGRAM_ID, 7),
Err(ProgramError::IncorrectProgramId)
);
}
#[test]
fn the_newer_entry_applies_from_its_own_epoch() {
let image = tlv_image(&[(FEE_TYPE, fee_payload(None))]);
assert_eq!(
mint_fee_at_epoch(&image, &TOKEN_2022_PROGRAM_ID, 6),
Ok(Some(MintFee {
bps: 100,
maximum_fee: 1_000,
}))
);
assert_eq!(fee(&image), Ok(NEWER));
}
#[test]
fn a_revoked_authority_reads_as_none() {
for authority in [None, Some(Address::new_from_array([9; 32]))] {
let image = tlv_image(&[(FEE_TYPE, fee_payload(authority))]);
let mint = mint_extensions(&image, &TOKEN_2022_PROGRAM_ID)
.unwrap()
.unwrap();
let config =
extension::<TransferFeeConfig>(&mint).unwrap().unwrap();
assert_eq!(
Option::from(config.transfer_fee_config_authority),
authority
);
}
}
#[test]
fn a_bare_mint_has_no_tlv_region_to_walk() {
let mut bare = tlv_image(&[]);
bare.truncate(82);
assert_eq!(fee(&bare), Ok(None));
bare[IS_INITIALIZED_OFFSET] = 0;
assert_eq!(fee(&bare), Err(ProgramError::UninitializedAccount));
}
#[test]
fn a_region_that_ends_cleanly_carries_no_config() {
for tail in [
vec![],
vec![0],
vec![0; 2],
vec![0; 3],
vec![0; 512],
vec![7],
] {
let mut image = tlv_image(&other_extensions());
image.extend_from_slice(&tail);
assert_eq!(fee(&image), Ok(None), "tail {tail:?}");
}
}
#[test]
fn a_cut_entry_past_the_config_is_not_read() {
let mut image = tlv_image(&[(FEE_TYPE, fee_payload(None))]);
image.extend_from_slice(&[18, 0, 64, 0]);
assert_eq!(fee(&image), Ok(NEWER));
}
#[test]
fn an_account_extension_is_not_read_off_a_mint() {
let image = tlv_image(&other_extensions());
let mint = mint_extensions(&image, &TOKEN_2022_PROGRAM_ID)
.unwrap()
.unwrap();
assert_eq!(
extension::<TransferFeeAmount>(&mint),
Err(ProgramError::InvalidAccountData)
);
}
#[test]
fn a_future_extension_type_leaves_the_mint_readable() {
let future = (FUTURE_TYPE, vec![0; 16]);
let fee_free = tlv_image(&[(18, vec![0; 64]), future.clone()]);
let mint = mint_extensions(&fee_free, &TOKEN_2022_PROGRAM_ID)
.unwrap()
.unwrap();
assert_eq!(
mint.get_extension_types(),
Err(ProgramError::InvalidAccountData)
);
assert_eq!(extension::<TransferFeeConfig>(&mint), Ok(None));
let fee_after = tlv_image(&[future, (FEE_TYPE, fee_payload(None))]);
assert_eq!(fee(&fee_after), Ok(NEWER));
}
#[test]
fn a_cut_region_is_malformed() {
let mut short = tlv_image(&[]);
short.truncate(ACCOUNT_TYPE_OFFSET);
let mut header = tlv_image(&other_extensions());
header.truncate(TLV_START + 4 + 64 + 2);
let mut overrun = tlv_image(&[(18, vec![0; 64])]);
overrun.truncate(TLV_START + 4 + 32);
let mut payload = tlv_image(&other_extensions());
payload.extend_from_slice(&[18, 0, 64, 0]);
let mut config = tlv_image(&[(FEE_TYPE, fee_payload(None))]);
config.pop();
for (label, image) in [
("no account type", short),
("cut header", header),
("length past the end", overrun),
("payload never arrives", payload),
("config payload cut", config),
] {
assert_eq!(
fee(&image),
Err(ProgramError::InvalidAccountData),
"{label}"
);
}
}
#[test]
fn a_config_payload_of_the_wrong_length_is_malformed() {
for length in [100_usize, 116] {
let image = tlv_image(&[(FEE_TYPE, vec![0; length])]);
assert_eq!(
fee(&image),
Err(ProgramError::InvalidArgument),
"payload length {length}"
);
}
}
#[test]
fn a_non_mint_account_type_is_malformed() {
let mut image = tlv_image(&[(FEE_TYPE, fee_payload(None))]);
image[ACCOUNT_TYPE_OFFSET] = 2;
assert_eq!(fee(&image), Err(ProgramError::InvalidAccountData));
}
}