tensor_toolbox/token_2022/
mod.rs

1pub mod extension;
2pub mod token;
3pub mod transfer;
4pub mod wns;
5
6use anchor_lang::{
7    solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey},
8    Result,
9};
10use anchor_spl::{
11    token_2022::spl_token_2022::extension::transfer_hook::TransferHook,
12    token_interface::spl_token_2022::{
13        extension::{BaseStateWithExtensions, StateWithExtensions},
14        state::Mint,
15    },
16};
17use spl_token_metadata_interface::state::TokenMetadata;
18use std::str::FromStr;
19
20use self::extension::{get_extension, get_variable_len_extension};
21
22// Prefix used by Libreplex to identify royalty accounts.
23const LIBREPLEX_RO: &str = "_ro_";
24
25// Libreplex transfer hook program id: CZ1rQoAHSqWBoAEfqGsiLhgbM59dDrCWk3rnG5FXaoRV.
26const LIBREPLEX_TRANSFER_HOOK: Pubkey = Pubkey::new_from_array([
27    171, 164, 26, 246, 200, 121, 33, 135, 216, 50, 55, 114, 165, 1, 182, 24, 180, 164, 102, 111, 3,
28    53, 2, 250, 50, 121, 61, 15, 194, 104, 5, 76,
29]);
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct RoyaltyInfo {
33    /// Royalties fee basis points.
34    pub seller_fee: u16,
35
36    /// List of creators (pubkey, share).
37    pub creators: Vec<(Pubkey, u8)>,
38}
39
40/// Validates a "vanilla" Token 2022 non-fungible mint account.
41///
42/// For non-fungibles assets, the validation consists of checking that the mint:
43/// - has no more than 1 supply
44/// - has 0 decimals
45/// - has no mint authority
46///
47/// It also supports Libreplex royalty enforcement by looking for the metadata extension
48/// to retrieve the seller fee basis points and creators.
49pub fn validate_mint(mint_info: &AccountInfo) -> Result<Option<RoyaltyInfo>> {
50    let mint_data = &mint_info.data.borrow();
51    let mint = StateWithExtensions::<Mint>::unpack(mint_data)?;
52
53    if !mint.base.is_initialized {
54        msg!("Mint is not initialized");
55        return Err(ProgramError::UninitializedAccount.into());
56    }
57
58    if mint.base.decimals != 0 {
59        msg!("Mint decimals must be 0");
60        return Err(ProgramError::InvalidAccountData.into());
61    }
62
63    if mint.base.supply != 1 {
64        msg!("Mint supply must be 1");
65        return Err(ProgramError::InvalidAccountData.into());
66    }
67
68    if mint.base.mint_authority.is_some() {
69        msg!("Mint authority must be none");
70        return Err(ProgramError::InvalidAccountData.into());
71    }
72
73    let hook_program: Option<Pubkey> =
74        if let Ok(extension) = get_extension::<TransferHook>(mint.get_tlv_data()) {
75            extension.program_id.into()
76        } else {
77            None
78        };
79
80    // currently only Libreplex is supported, but this can be expanded to include
81    // other standards in the future; only looks for the metadata if the correct
82    // hook is in place to avoid parsing the metadata unnecessarily (or match on a
83    // value that it is not intended to be used)
84    if hook_program == Some(LIBREPLEX_TRANSFER_HOOK) {
85        if let Ok(metadata) = get_variable_len_extension::<TokenMetadata>(mint.get_tlv_data()) {
86            let royalties = metadata
87                .additional_metadata
88                .iter()
89                .find(|(key, _)| key.starts_with(LIBREPLEX_RO));
90
91            if let Some((destination, seller_fee)) = royalties {
92                let seller_fee: u16 = seller_fee.parse().map_err(|_error| {
93                    msg!("[ERROR] Could not parse seller fee");
94                    ProgramError::InvalidAccountData
95                })?;
96
97                if seller_fee > 10000 {
98                    msg!("[ERROR] Seller fee must be less than or equal to 10000");
99                    return Err(ProgramError::InvalidAccountData.into());
100                }
101
102                let destination = Pubkey::from_str(destination.trim_start_matches(LIBREPLEX_RO))
103                    .map_err(|_error| {
104                        msg!("[ERROR] Could not parse destination address");
105                        ProgramError::InvalidAccountData
106                    })?;
107
108                return Ok(Some(RoyaltyInfo {
109                    seller_fee,
110                    creators: vec![(destination, 100)],
111                }));
112            }
113        }
114    }
115
116    Ok(None)
117}