tensor_toolbox/token_2022/
wns.rs

1//! WNS types and functions for the SPL Token 2022 program.
2//!
3//! This module provides types and functions to interact with the WNS program
4//! while there is no WNS crate available.
5//!
6//! TODO: This can be removed once the WNS crate is available and our programs are compatible with Solana v1.18.
7
8use anchor_lang::{
9    solana_program::{
10        account_info::AccountInfo,
11        instruction::{AccountMeta, Instruction},
12        msg,
13        program::invoke_signed,
14        program_error::ProgramError,
15        program_option::COption,
16        pubkey::Pubkey,
17        rent::Rent,
18        sysvar::Sysvar,
19    },
20    Key, Result,
21};
22use anchor_spl::token_interface::spl_token_2022::{
23    extension::{
24        metadata_pointer::MetadataPointer, transfer_hook::TransferHook, BaseStateWithExtensions,
25        StateWithExtensions,
26    },
27    state::Mint,
28};
29use spl_token_metadata_interface::state::TokenMetadata;
30use std::str::FromStr;
31use tensor_vipers::{unwrap_checked, unwrap_int};
32
33use super::extension::{get_extension, get_variable_len_extension};
34
35anchor_lang::declare_id!("wns1gDLt8fgLcGhWi5MqAqgXpwEP1JftKE9eZnXS1HM");
36
37pub const ROYALTY_BASIS_POINTS_FIELD: &str = "royalty_basis_points";
38
39pub const APPROVE_LEN: usize = 8 + 8;
40
41/// WNS manager account.
42const MANAGER_PUBKEY: Pubkey = Pubkey::new_from_array([
43    125, 100, 129, 23, 165, 236, 2, 226, 233, 63, 107, 17, 242, 89, 72, 105, 75, 145, 77, 172, 118,
44    210, 188, 66, 171, 78, 251, 66, 86, 35, 201, 190,
45]);
46
47/// Accounts for the `wns_approve` function.
48pub struct ApproveAccounts<'info> {
49    pub wns_program: AccountInfo<'info>,
50    pub payer: AccountInfo<'info>,
51    pub authority: AccountInfo<'info>,
52    pub mint: AccountInfo<'info>,
53    pub approve_account: AccountInfo<'info>,
54    // Defaults to default pubkey, which is the System Program ID.
55    pub payment_mint: Option<AccountInfo<'info>>,
56    // Anchor Optional account--defaults to WNS program ID.
57    pub distribution_token_account: Option<AccountInfo<'info>>,
58    // Anchor Optional account--defaults to WNS program ID.
59    pub authority_token_account: Option<AccountInfo<'info>>,
60    pub distribution_account: AccountInfo<'info>,
61    pub system_program: AccountInfo<'info>,
62    pub distribution_program: AccountInfo<'info>,
63    pub token_program: AccountInfo<'info>,
64    pub payment_token_program: Option<AccountInfo<'info>>,
65}
66
67impl<'info> ApproveAccounts<'info> {
68    pub fn to_account_infos(self) -> Vec<AccountInfo<'info>> {
69        // Account Infos: order doesn't matter.
70        let mut accounts = vec![
71            self.wns_program,
72            self.payer,
73            self.authority,
74            self.mint,
75            self.approve_account,
76            self.distribution_account,
77            self.system_program,
78            self.distribution_program,
79            self.token_program,
80        ];
81
82        if let Some(distribution_token_account) = self.distribution_token_account {
83            accounts.push(distribution_token_account);
84        };
85
86        if let Some(authority_token_account) = self.authority_token_account {
87            accounts.push(authority_token_account);
88        }
89
90        if let Some(payment_mint) = self.payment_mint {
91            accounts.push(payment_mint);
92        }
93
94        if let Some(payment_token_program) = self.payment_token_program {
95            accounts.push(payment_token_program);
96        }
97
98        accounts
99    }
100
101    pub fn to_account_metas(&self) -> Vec<AccountMeta> {
102        vec![
103            AccountMeta::new(*self.payer.key, true),
104            AccountMeta::new(*self.authority.key, true),
105            AccountMeta::new_readonly(*self.mint.key, false),
106            AccountMeta::new(*self.approve_account.key, false),
107            AccountMeta::new_readonly(
108                self.payment_mint
109                    .as_ref()
110                    .map_or(*self.system_program.key, |account| *account.key),
111                false,
112            ),
113            // Anchor optional accounts, so either the token account or the WNS program.
114            if let Some(distribution_token_account) = &self.distribution_token_account {
115                AccountMeta::new(*distribution_token_account.key, false)
116            } else {
117                AccountMeta::new_readonly(*self.wns_program.key, false)
118            },
119            if let Some(authority_token_account) = &self.authority_token_account {
120                AccountMeta::new(*authority_token_account.key, false)
121            } else {
122                AccountMeta::new_readonly(*self.wns_program.key, false)
123            },
124            AccountMeta::new(*self.distribution_account.key, false),
125            AccountMeta::new_readonly(*self.system_program.key, false),
126            AccountMeta::new_readonly(*self.distribution_program.key, false),
127            AccountMeta::new_readonly(*self.token_program.key, false),
128            if let Some(payment_token_program) = &self.payment_token_program {
129                AccountMeta::new_readonly(*payment_token_program.key, false)
130            } else {
131                AccountMeta::new_readonly(*self.wns_program.key, false)
132            },
133        ]
134    }
135}
136
137/// Validates a WNS Token 2022 non-fungible mint account.
138///
139/// For non-fungibles assets, the validation consists of checking that the mint:
140/// - has no more than 1 supply
141/// - has 0 decimals
142/// - [TODO] has no mint authority (currently not possible to check)
143/// - `ExtensionType::MetadataPointer` is present and points to the mint account
144/// - `ExtensionType::TransferHook` is present and program id equals to WNS program
145pub fn validate_mint(mint_info: &AccountInfo) -> Result<u16> {
146    let mint_data = &mint_info.data.borrow();
147    let mint = StateWithExtensions::<Mint>::unpack(mint_data)?;
148
149    if !mint.base.is_initialized {
150        msg!("Mint is not initialized");
151        return Err(ProgramError::UninitializedAccount.into());
152    }
153
154    if mint.base.decimals != 0 {
155        msg!("Mint decimals must be 0");
156        return Err(ProgramError::InvalidAccountData.into());
157    }
158
159    if mint.base.supply != 1 {
160        msg!("Mint supply must be 1");
161        return Err(ProgramError::InvalidAccountData.into());
162    }
163
164    if mint.base.mint_authority.is_some()
165        && mint.base.mint_authority != COption::Some(MANAGER_PUBKEY)
166    {
167        msg!("Mint authority must be none or the WNS manager account");
168        return Err(ProgramError::InvalidAccountData.into());
169    }
170
171    if let Ok(extension) = get_extension::<MetadataPointer>(mint.get_tlv_data()) {
172        let metadata_address: Option<Pubkey> = extension.metadata_address.into();
173        if metadata_address != Some(mint_info.key()) {
174            msg!("Metadata pointer extension: metadata address should be the mint itself");
175            return Err(ProgramError::InvalidAccountData.into());
176        }
177    } else {
178        msg!("Missing metadata pointer extension");
179        return Err(ProgramError::InvalidAccountData.into());
180    }
181
182    if let Ok(extension) = get_extension::<TransferHook>(mint.get_tlv_data()) {
183        let program_id: Option<Pubkey> = extension.program_id.into();
184        if program_id != Some(super::wns::ID) {
185            msg!("Transfer hook extension: program id mismatch");
186            return Err(ProgramError::InvalidAccountData.into());
187        }
188    } else {
189        msg!("Missing transfer hook extension");
190        return Err(ProgramError::InvalidAccountData.into());
191    }
192
193    let metadata = get_variable_len_extension::<TokenMetadata>(mint.get_tlv_data())?;
194    let royalty_basis_points = metadata
195        .additional_metadata
196        .iter()
197        .find(|(key, _)| key == super::wns::ROYALTY_BASIS_POINTS_FIELD)
198        .map(|(_, value)| value)
199        .map(|value| u16::from_str(value).unwrap())
200        .unwrap_or(0);
201
202    Ok(royalty_basis_points)
203}
204
205/// Parameters for the `Approve` helper function.
206pub struct ApproveParams<'a> {
207    pub price: u64,
208    pub royalty_fee: u64,
209    pub signer_seeds: &'a [&'a [&'a [u8]]],
210}
211
212impl<'a> ApproveParams<'a> {
213    /// Creates a new `ApproveParams` instance for no royalties for wallet signer.
214    pub fn no_royalties() -> Self {
215        Self {
216            price: 0,
217            royalty_fee: 0,
218            signer_seeds: &[],
219        }
220    }
221    /// Creates a new `ApproveParams` instance for no royalties for PDA signer.
222    pub fn no_royalties_with_signer_seeds(signer_seeds: &'a [&'a [&'a [u8]]]) -> Self {
223        Self {
224            price: 0,
225            royalty_fee: 0,
226            signer_seeds,
227        }
228    }
229}
230
231/// Approves a WNS token transfer.
232///
233/// This needs to be called before any attempt to transfer a WNS token. For transfers
234/// that do not involve royalties payment, set the `price` and `royalty_fee` to `0`.
235///
236/// The current implementation "manually" creates the instruction data and invokes the
237/// WNS program. This is necessary because there is no WNS crate available.
238pub fn approve(accounts: super::wns::ApproveAccounts, params: ApproveParams) -> Result<()> {
239    let ApproveParams {
240        price,
241        royalty_fee,
242        signer_seeds,
243    } = params;
244
245    // instruction data (the instruction was renamed to `ApproveTransfer`)
246    let mut data = vec![198, 217, 247, 150, 208, 60, 169, 244];
247    data.extend(price.to_le_bytes());
248
249    let approve_ix = Instruction {
250        program_id: super::wns::ID,
251        accounts: accounts.to_account_metas(),
252        data,
253    };
254
255    let payer = accounts.payer.clone();
256    let approve = accounts.approve_account.clone();
257
258    // store the previous values for the assert
259    let initial_payer_lamports = payer.lamports();
260    let initial_approve_rent = approve.lamports();
261
262    // delegate the fee payment to WNS
263    let result = invoke_signed(&approve_ix, &accounts.to_account_infos(), signer_seeds)
264        .map_err(|error| error.into());
265
266    let ending_payer_lamports = payer.lamports();
267
268    // want to account for potential amount paid in rent.
269    // in case WNS tries to drain to approve account, we cap
270    // the rent difference to the minimum rent.
271    let rent_difference = unwrap_int!(std::cmp::max(
272        Rent::get()?.minimum_balance(APPROVE_LEN),
273        initial_approve_rent,
274    )
275    .checked_sub(initial_approve_rent));
276    // distribution account gets realloced based on creators potentially: overestimate here.
277    let dist_realloc_fee = Rent::get()?.minimum_balance(1024);
278
279    let payer_difference = unwrap_int!(initial_payer_lamports.checked_sub(ending_payer_lamports));
280    let expected_fee = unwrap_checked!({
281        royalty_fee
282            .checked_add(rent_difference)?
283            .checked_add(dist_realloc_fee)
284    });
285
286    // assert that payer was charged the expected fee: rent + any royalty fee.
287    if payer_difference > expected_fee {
288        msg!(
289            "Unexpected lamports change: expected {} but got {}",
290            expected_fee,
291            payer_difference
292        );
293        return Err(ProgramError::InvalidAccountData.into());
294    }
295
296    result
297}