tensor_toolbox/
common.rs

1#![allow(clippy::result_large_err)]
2use anchor_lang::{
3    prelude::*,
4    solana_program::{program::invoke, pubkey::Pubkey, system_instruction, system_program},
5};
6use anchor_spl::{
7    associated_token::AssociatedToken,
8    token::spl_token,
9    token_2022::spl_token_2022,
10    token_interface::{TokenAccount, TokenInterface},
11};
12use mpl_token_metadata::types::TokenStandard;
13use std::slice::Iter;
14use tensor_vipers::prelude::*;
15
16use crate::token_2022::transfer::transfer_checked as token_2022_transfer_checked;
17use crate::TensorError;
18
19pub const HUNDRED_PCT_BPS: u64 = 10000;
20pub const HUNDRED_PCT: u64 = 100;
21pub const BROKER_FEE_PCT: u64 = 50;
22pub const TNSR_DISCOUNT_BPS: u64 = 2500;
23pub const TAKER_FEE_BPS: u64 = 200;
24pub const MAKER_BROKER_PCT: u64 = 80; // Out of 100
25
26pub const fn pubkey(base58str: &str) -> Pubkey {
27    Pubkey::new_from_array(five8_const::decode_32_const(base58str))
28}
29
30pub mod escrow {
31    use super::*;
32    declare_id!("TSWAPaqyCSx2KABk68Shruf4rp7CxcNi8hAsbdwmHbN");
33
34    pub const TSWAP_SINGLETON: Pubkey = pubkey("4zdNGgAtFsW1cQgHqkiWyRsxaAgxrSRRynnuunxzjxue");
35}
36
37pub mod fees {
38    use super::*;
39    declare_id!("TFEEgwDP6nn1s8mMX2tTNPPz8j2VomkphLUmyxKm17A");
40}
41
42pub mod marketplace {
43    use super::*;
44    declare_id!("TCMPhJdwDryooaGtiocG1u3xcYbRpiJzb283XfCZsDp");
45
46    pub const TCOMP_SINGLETON: Pubkey = pubkey("q4s8z5dRAt2fKC2tLthBPatakZRXPMx1LfacckSXd4f");
47}
48
49pub mod mpl_token_auth_rules {
50    use super::*;
51    declare_id!("auth9SigNpDKz4sJJ1DfCTuZrZNSAgh9sFD3rboVmgg");
52}
53
54pub mod price_lock {
55    use super::*;
56    declare_id!("TLoCKic2wGJm7VhZKumih4Lc35fUhYqVMgA4j389Buk");
57
58    pub const TLOCK_SINGLETON: Pubkey = pubkey("CdXA5Vpg4hqvsmLSKC2cygnJVvsQTrDrrn428nAZQaKz");
59}
60
61/// Calculates fee vault shard from a given AccountInfo or Pubkey. Relies on the Anchor `Key` trait.
62#[macro_export]
63macro_rules! shard_num {
64    ($value:expr) => {
65        &$value.key().as_ref()[31].to_le_bytes()
66    };
67}
68
69pub const SPL_TOKEN_IDS: [Pubkey; 2] = [spl_token::ID, spl_token_2022::ID];
70
71pub struct CalcFeesArgs {
72    pub amount: u64,
73    pub total_fee_bps: u64,
74    pub broker_fee_pct: u64,
75    pub maker_broker_pct: u64,
76    pub tnsr_discount: bool,
77}
78
79/// Fees struct that holds the calculated fees.
80pub struct Fees {
81    /// Taker fee is the total fee sans royalties: protocol fee + broker fees.
82    pub taker_fee: u64,
83    /// Protocol fee is the fee that goes to the protocol, a percentage of the total fee determined by 1 - broker_fee_pct.
84    pub protocol_fee: u64,
85    /// Maker broker fee is the fee that goes to the maker broker: a percentage of the total broker fee.
86    pub maker_broker_fee: u64,
87    /// Taker broker fee is the fee that goes to the taker broker: the remainder of the total broker fee.
88    pub taker_broker_fee: u64,
89}
90
91// Calculate fees for a given amount.
92pub fn calc_fees(args: CalcFeesArgs) -> Result<Fees> {
93    let CalcFeesArgs {
94        amount,
95        total_fee_bps,
96        broker_fee_pct,
97        maker_broker_pct,
98        tnsr_discount,
99    } = args;
100
101    // Apply the TNSR discount if enabled.
102    let total_fee_bps = if tnsr_discount {
103        unwrap_checked!({
104            total_fee_bps
105                .checked_mul(HUNDRED_PCT_BPS - TNSR_DISCOUNT_BPS)?
106                .checked_div(HUNDRED_PCT_BPS)
107        })
108    } else {
109        total_fee_bps
110    };
111
112    // Total fee is calculated from the passed in total_fee_bps and is protocol fee + broker fees.
113    let total_fee = unwrap_checked!({
114        (amount)
115            .checked_mul(total_fee_bps)?
116            .checked_div(HUNDRED_PCT_BPS)
117    });
118
119    // Broker fees are a percentage of the total fee.
120    let broker_fees = unwrap_checked!({
121        total_fee
122            .checked_mul(broker_fee_pct)?
123            .checked_div(HUNDRED_PCT)
124    });
125
126    // Protocol fee is the remainder.
127    let protocol_fee = unwrap_checked!({ total_fee.checked_sub(broker_fees) });
128
129    // Maker broker is a percentage of the total brokers fee.
130    let maker_broker_fee = unwrap_checked!({
131        broker_fees
132            .checked_mul(maker_broker_pct)?
133            .checked_div(HUNDRED_PCT)
134    });
135
136    // Remaining broker fee is the taker broker fee.
137    let taker_broker_fee = unwrap_int!(broker_fees.checked_sub(maker_broker_fee));
138
139    Ok(Fees {
140        taker_fee: total_fee,
141        protocol_fee,
142        maker_broker_fee,
143        taker_broker_fee,
144    })
145}
146
147pub fn is_royalty_enforced(token_standard: Option<TokenStandard>) -> bool {
148    matches!(
149        token_standard,
150        Some(TokenStandard::ProgrammableNonFungible)
151            | Some(TokenStandard::ProgrammableNonFungibleEdition)
152    )
153}
154
155pub fn calc_creators_fee(
156    seller_fee_basis_points: u16,
157    amount: u64,
158    royalty_pct: Option<u16>,
159) -> Result<u64> {
160    let creator_fee_bps = if let Some(royalty_pct) = royalty_pct {
161        require!(royalty_pct <= 100, TensorError::BadRoyaltiesPct);
162
163        // If optional passed, pay optional royalties
164        unwrap_checked!({
165            (seller_fee_basis_points as u64)
166                .checked_mul(royalty_pct as u64)?
167                .checked_div(100_u64)
168        })
169    } else {
170        // Else pay 0
171        0_u64
172    };
173    let fee = unwrap_checked!({
174        creator_fee_bps
175            .checked_mul(amount)?
176            .checked_div(HUNDRED_PCT_BPS)
177    });
178
179    Ok(fee)
180}
181
182/// Transfers all lamports from a PDA (except for rent) to a destination account.
183pub fn transfer_all_lamports_from_pda<'info>(
184    from_pda: &AccountInfo<'info>,
185    to: &AccountInfo<'info>,
186) -> Result<()> {
187    let rent = Rent::get()?.minimum_balance(from_pda.data_len());
188    let to_move = unwrap_int!(from_pda.lamports().checked_sub(rent));
189
190    transfer_lamports_from_pda(from_pda, to, to_move)
191}
192
193/// Transfers specified lamports from a PDA to a destination account.
194/// Throws an error if less than rent remains in the PDA.
195pub fn transfer_lamports_from_pda<'info>(
196    from_pda: &AccountInfo<'info>,
197    to: &AccountInfo<'info>,
198    lamports: u64,
199) -> Result<()> {
200    let remaining_pda_lamports = unwrap_int!(from_pda.lamports().checked_sub(lamports));
201    // Check we are not withdrawing into our rent.
202    let rent = Rent::get()?.minimum_balance(from_pda.data_len());
203    require!(
204        remaining_pda_lamports >= rent,
205        TensorError::InsufficientBalance
206    );
207
208    **from_pda.try_borrow_mut_lamports()? = remaining_pda_lamports;
209
210    let new_to = unwrap_int!(to.lamports.borrow().checked_add(lamports));
211    **to.lamports.borrow_mut() = new_to;
212
213    Ok(())
214}
215
216pub struct FromExternal<'b, 'info> {
217    pub from: &'b AccountInfo<'info>,
218    pub sys_prog: &'b AccountInfo<'info>,
219}
220
221pub enum FromAcc<'a, 'info> {
222    Pda(&'a AccountInfo<'info>),
223    External(&'a FromExternal<'a, 'info>),
224}
225
226#[derive(AnchorSerialize, AnchorDeserialize, PartialEq, Eq, Debug, Clone)]
227pub struct TCreator {
228    pub address: Pubkey,
229    pub verified: bool,
230    // In percentages, NOT basis points ;) Watch out!
231    pub share: u8,
232}
233
234//into token meta
235impl From<TCreator> for mpl_token_metadata::types::Creator {
236    fn from(creator: TCreator) -> Self {
237        mpl_token_metadata::types::Creator {
238            address: creator.address,
239            verified: creator.verified,
240            share: creator.share,
241        }
242    }
243}
244
245//from token meta
246impl From<mpl_token_metadata::types::Creator> for TCreator {
247    fn from(creator: mpl_token_metadata::types::Creator) -> Self {
248        TCreator {
249            address: creator.address,
250            verified: creator.verified,
251            share: creator.share,
252        }
253    }
254}
255
256#[cfg(feature = "mpl-core")]
257//from token meta
258impl From<mpl_core::types::Creator> for TCreator {
259    fn from(creator: mpl_core::types::Creator) -> Self {
260        TCreator {
261            address: creator.address,
262            share: creator.percentage,
263            // mpl-core does not have a concept of "verified" creator
264            verified: false,
265        }
266    }
267}
268
269#[repr(u8)]
270pub enum CreatorFeeMode<'a, 'info> {
271    Sol {
272        from: &'a FromAcc<'a, 'info>,
273    },
274    Spl {
275        associated_token_program: &'a Program<'info, AssociatedToken>,
276        token_program: &'a Interface<'info, TokenInterface>,
277        system_program: &'a Program<'info, System>,
278        currency: &'a AccountInfo<'info>,
279        from: &'a AccountInfo<'info>,
280        from_token_acc: &'a AccountInfo<'info>,
281        rent_payer: &'a AccountInfo<'info>,
282    },
283}
284
285pub fn transfer_creators_fee<'a, 'info>(
286    //using TCreator here so that this fn is agnostic to normal NFTs and cNFTs
287    creators: &'a Vec<TCreator>,
288    creator_accounts: &mut Iter<AccountInfo<'info>>,
289    creator_fee: u64,
290    // put not-in-common args in an enum so the invoker doesn't require it
291    mode: &'a CreatorFeeMode<'a, 'info>,
292) -> Result<u64> {
293    // Send royalties: taken from AH's calculation:
294    // https://github.com/metaplex-foundation/metaplex-program-library/blob/2320b30ec91b729b153f0c0fe719f96d325b2358/auction-house/program/src/utils.rs#L366-L471
295    let mut remaining_fee = creator_fee;
296    for creator in creators {
297        let current_creator_info = next_account_info(creator_accounts)?;
298
299        require!(
300            creator.address.eq(current_creator_info.key),
301            TensorError::CreatorMismatch
302        );
303
304        let pct = creator.share as u64;
305        let creator_fee = unwrap_checked!({ pct.checked_mul(creator_fee)?.checked_div(100) });
306
307        let current_creator_ta_info = match mode {
308            CreatorFeeMode::Sol { from: _ } => {
309                // Prevents InsufficientFundsForRent, where creator acc doesn't have enough fee
310                // https://explorer.solana.com/tx/vY5nYA95ELVrs9SU5u7sfU2ucHj4CRd3dMCi1gWrY7MSCBYQLiPqzABj9m8VuvTLGHb9vmhGaGY7mkqPa1NLAFE
311                let rent = Rent::get()?.minimum_balance(current_creator_info.data_len());
312                if unwrap_int!(current_creator_info.lamports().checked_add(creator_fee)) < rent {
313                    //skip current creator, we can't pay them
314                    continue;
315                }
316                None
317            }
318            CreatorFeeMode::Spl {
319                associated_token_program: _,
320                token_program: _,
321                system_program: _,
322                currency: _,
323                from: _,
324                from_token_acc: _,
325                rent_payer: _,
326            } => {
327                // ATA validated on transfer CPI.
328                Some(next_account_info(creator_accounts)?)
329            }
330        };
331
332        remaining_fee = unwrap_int!(remaining_fee.checked_sub(creator_fee));
333
334        if creator_fee > 0 {
335            match mode {
336                CreatorFeeMode::Sol { from } => match from {
337                    FromAcc::Pda(from_pda) => {
338                        transfer_lamports_from_pda(from_pda, current_creator_info, creator_fee)?;
339                    }
340                    FromAcc::External(from_ext) => {
341                        let FromExternal { from, sys_prog } = from_ext;
342                        invoke(
343                            &system_instruction::transfer(
344                                from.key,
345                                current_creator_info.key,
346                                creator_fee,
347                            ),
348                            &[
349                                (*from).clone(),
350                                current_creator_info.clone(),
351                                (*sys_prog).clone(),
352                            ],
353                        )?;
354                    }
355                },
356
357                CreatorFeeMode::Spl {
358                    associated_token_program,
359                    token_program,
360                    system_program,
361                    currency,
362                    from,
363                    from_token_acc: from_ta,
364                    rent_payer,
365                } => {
366                    let creator_ta_info =
367                        unwrap_opt!(current_creator_ta_info, "missing creator ata");
368
369                    // Creators can change the owner of their ATA to someone else, causing the instruction calling this
370                    // function to fail.
371                    // To prevent this, we don't idempotently create the ATA. Instead we check if the passed in token
372                    // account exists, and if it is the correct mint and owner, otherwise we create the ATA.
373
374                    if creator_ta_info.data_is_empty()
375                        && creator_ta_info.owner == &system_program::ID
376                    {
377                        anchor_spl::associated_token::create(CpiContext::new(
378                            associated_token_program.to_account_info(),
379                            anchor_spl::associated_token::Create {
380                                payer: rent_payer.to_account_info(),
381                                associated_token: creator_ta_info.to_account_info(),
382                                authority: current_creator_info.to_account_info(),
383                                mint: currency.to_account_info(),
384                                system_program: system_program.to_account_info(),
385                                token_program: token_program.to_account_info(),
386                            },
387                        ))?;
388                    } else {
389                        // Validate the owner is a SPL token program.
390                        require!(
391                            SPL_TOKEN_IDS.contains(creator_ta_info.owner),
392                            ErrorCode::InvalidProgramId
393                        );
394                        // Validate the mint and owner.
395                        let creator_ta =
396                            TokenAccount::try_deserialize(&mut &creator_ta_info.data.borrow()[..])?;
397
398                        require!(creator_ta.mint == currency.key(), TensorError::InvalidMint);
399                        require!(
400                            creator_ta.owner == current_creator_info.key(),
401                            TensorError::InvalidOwner
402                        );
403                    }
404
405                    match token_program.key() {
406                        anchor_spl::token::ID => {
407                            anchor_spl::token::transfer(
408                                CpiContext::new(
409                                    token_program.to_account_info(),
410                                    anchor_spl::token::Transfer {
411                                        from: from_ta.to_account_info(),
412                                        to: creator_ta_info.to_account_info(),
413                                        authority: from.to_account_info(),
414                                    },
415                                ),
416                                creator_fee,
417                            )?;
418                        }
419                        anchor_spl::token_interface::ID => {
420                            let mint = anchor_spl::token_interface::Mint::try_deserialize(
421                                &mut &currency.data.borrow()[..],
422                            )?;
423                            token_2022_transfer_checked(
424                                CpiContext::new(
425                                    token_program.to_account_info(),
426                                    anchor_spl::token_interface::TransferChecked {
427                                        from: from_ta.to_account_info(),
428                                        mint: currency.to_account_info(),
429                                        to: creator_ta_info.to_account_info(),
430                                        authority: from.to_account_info(),
431                                    },
432                                ),
433                                creator_fee,
434                                mint.decimals,
435                            )?;
436                        }
437                        _ => return Err(ErrorCode::InvalidProgramId.into()),
438                    }
439                }
440            }
441        }
442    }
443
444    // Return the amount that was sent (minus any dust).
445    Ok(unwrap_int!(creator_fee.checked_sub(remaining_fee)))
446}
447
448// NOT: https://github.com/coral-xyz/sealevel-attacks/blob/master/programs/9-closing-accounts/secure/src/lib.rs
449// Instead: https://github.com/coral-xyz/anchor/blob/b7bada148cead931bc3bdae7e9a641e9be66e6a6/lang/src/common.rs#L6
450pub fn close_account(
451    pda_to_close: &mut AccountInfo,
452    sol_destination: &mut AccountInfo,
453) -> Result<()> {
454    // Transfer tokens from the account to the sol_destination.
455    let dest_starting_lamports = sol_destination.lamports();
456    **sol_destination.lamports.borrow_mut() =
457        unwrap_int!(dest_starting_lamports.checked_add(pda_to_close.lamports()));
458    **pda_to_close.lamports.borrow_mut() = 0;
459
460    pda_to_close.assign(&system_program::ID);
461    pda_to_close.realloc(0, false).map_err(Into::into)
462}
463
464/// Transfers lamports from one account to another, handling the cases where the account
465/// is either a PDA or a system account.
466pub fn transfer_lamports<'info>(
467    from: &AccountInfo<'info>,
468    to: &AccountInfo<'info>,
469    lamports: u64,
470) -> Result<()> {
471    // if the from account is empty, we can use the system program to transfer
472    if from.data_is_empty() && from.owner == &system_program::ID {
473        invoke(
474            &system_instruction::transfer(from.key, to.key, lamports),
475            &[from.clone(), to.clone()],
476        )
477        .map_err(Into::into)
478    } else {
479        transfer_lamports_from_pda(from, to, lamports)
480    }
481}
482
483/// Transfers lamports, skipping the transfer if the `to` account would not be rent exempt.
484///
485/// This is useful when transferring lamports to a new account that may not have been created yet
486/// and the transfer amount is less than the rent exemption.
487pub fn transfer_lamports_checked<'info, 'b>(
488    from: &'b AccountInfo<'info>,
489    to: &'b AccountInfo<'info>,
490    lamports: u64,
491) -> Result<()> {
492    let rent = Rent::get()?.minimum_balance(to.data_len());
493    if unwrap_int!(to.lamports().checked_add(lamports)) < rent {
494        // skip the transfer if the account as the account would not be rent exempt
495        msg!(
496            "Skipping transfer of {} lamports to {}: account would not be rent exempt",
497            lamports,
498            to.key
499        );
500        Ok(())
501    } else {
502        transfer_lamports(from, to, lamports)
503    }
504}
505
506/// Asserts that the account is a valid fee account: either one of the program singletons or the fee vault.
507pub fn assert_fee_account(fee_vault_info: &AccountInfo, state_info: &AccountInfo) -> Result<()> {
508    let expected_fee_vault = Pubkey::find_program_address(
509        &[
510            b"fee_vault",
511            // Use the last byte of the mint as the fee shard number
512            shard_num!(state_info),
513        ],
514        &fees::ID,
515    )
516    .0;
517
518    require!(
519        fee_vault_info.key == &expected_fee_vault
520            || &marketplace::TCOMP_SINGLETON == fee_vault_info.key,
521        TensorError::InvalidFeeAccount
522    );
523
524    Ok(())
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530
531    #[test]
532    fn pubkey_constant() {
533        let default_pubkey = pubkey("11111111111111111111111111111111");
534        assert_eq!(default_pubkey, Pubkey::default());
535
536        let p = pubkey("4zdNGgAtFsW1cQgHqkiWyRsxaAgxrSRRynnuunxzjxue");
537        assert_eq!(
538            p.to_bytes(),
539            [
540                59, 86, 73, 113, 118, 186, 131, 166, 77, 161, 204, 243, 144, 62, 8, 138, 52, 116,
541                86, 213, 41, 159, 32, 94, 252, 208, 28, 78, 220, 101, 76, 105
542            ]
543        );
544    }
545
546    #[test]
547    #[should_panic]
548    fn pubkey_constant_base58_too_short() {
549        let _p = pubkey("abc");
550    }
551
552    #[test]
553    #[should_panic]
554    fn pubkey_constant_base58_too_long() {
555        let _p = pubkey("abc123456789012345678901234567890123456789012345678901234567890123");
556    }
557
558    #[test]
559    #[should_panic]
560    fn pubkey_constant_base58_empty() {
561        let _p = pubkey("");
562    }
563}