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; pub 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#[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
79pub struct Fees {
81 pub taker_fee: u64,
83 pub protocol_fee: u64,
85 pub maker_broker_fee: u64,
87 pub taker_broker_fee: u64,
89}
90
91pub 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 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 let total_fee = unwrap_checked!({
114 (amount)
115 .checked_mul(total_fee_bps)?
116 .checked_div(HUNDRED_PCT_BPS)
117 });
118
119 let broker_fees = unwrap_checked!({
121 total_fee
122 .checked_mul(broker_fee_pct)?
123 .checked_div(HUNDRED_PCT)
124 });
125
126 let protocol_fee = unwrap_checked!({ total_fee.checked_sub(broker_fees) });
128
129 let maker_broker_fee = unwrap_checked!({
131 broker_fees
132 .checked_mul(maker_broker_pct)?
133 .checked_div(HUNDRED_PCT)
134 });
135
136 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 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 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
182pub 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
193pub 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 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 pub share: u8,
232}
233
234impl 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
245impl 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")]
257impl 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 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 creators: &'a Vec<TCreator>,
288 creator_accounts: &mut Iter<AccountInfo<'info>>,
289 creator_fee: u64,
290 mode: &'a CreatorFeeMode<'a, 'info>,
292) -> Result<u64> {
293 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 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 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 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 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 require!(
391 SPL_TOKEN_IDS.contains(creator_ta_info.owner),
392 ErrorCode::InvalidProgramId
393 );
394 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 ¤cy.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 Ok(unwrap_int!(creator_fee.checked_sub(remaining_fee)))
446}
447
448pub fn close_account(
451 pda_to_close: &mut AccountInfo,
452 sol_destination: &mut AccountInfo,
453) -> Result<()> {
454 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
464pub fn transfer_lamports<'info>(
467 from: &AccountInfo<'info>,
468 to: &AccountInfo<'info>,
469 lamports: u64,
470) -> Result<()> {
471 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
483pub 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 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
506pub 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 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}