From 977eb1d6734909d7e73ab368e4a00920eb112bbf Mon Sep 17 00:00:00 2001 From: Britt Cyr Date: Tue, 8 Oct 2024 10:35:37 -0400 Subject: [PATCH] Support transfer fees and other init token extensions (#142) * support transfer fees * cargo change * mint 22 --- Cargo.lock | 1 + client/rust/src/lib.rs | 2 +- .../src/program/processor/create_market.rs | 53 ++++-- .../manifest/src/program/processor/deposit.rs | 14 +- .../src/program/processor/global_deposit.rs | 12 +- .../manifest/src/validation/token_checkers.rs | 18 +- programs/manifest/tests/cases/token22.rs | 179 +++++++++++++++++- .../manifest/tests/program_test/fixtures.rs | 2 +- .../ui-wrapper/tests/program_test/fixtures.rs | 2 +- programs/wrapper/Cargo.toml | 1 + .../wrapper/tests/program_test/fixtures.rs | 2 +- 11 files changed, 248 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f1f039b85..26f5c959d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6381,6 +6381,7 @@ dependencies = [ "solana-sdk", "solana-security-txt", "spl-token 3.5.0", + "spl-token-2022 3.0.4", "static_assertions", "thiserror", "tokio", diff --git a/client/rust/src/lib.rs b/client/rust/src/lib.rs index d182c75a5..a019b7ec5 100644 --- a/client/rust/src/lib.rs +++ b/client/rust/src/lib.rs @@ -224,7 +224,7 @@ mod test { }, }; use solana_sdk::{account::Account, account_info::AccountInfo}; - use spl_token::state::Mint; + use spl_token_2022::state::Mint; use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr}; #[test] diff --git a/programs/manifest/src/program/processor/create_market.rs b/programs/manifest/src/program/processor/create_market.rs index 400f240ee..009616611 100644 --- a/programs/manifest/src/program/processor/create_market.rs +++ b/programs/manifest/src/program/processor/create_market.rs @@ -16,9 +16,10 @@ use solana_program::{ use spl_token_2022::{ extension::{ mint_close_authority::MintCloseAuthority, permanent_delegate::PermanentDelegate, - transfer_fee::TransferFeeConfig, BaseStateWithExtensions, StateWithExtensions, + BaseStateWithExtensions, ExtensionType, PodStateWithExtensions, StateWithExtensions, }, - state::Mint, + pod::PodMint, + state::{Account, Mint}, }; pub(crate) fn process_create_market( @@ -72,13 +73,6 @@ pub(crate) fn process_create_market( ); } } - - // Transfer fees make the amounts on withdraw and deposit not match what is expected. - require!( - pool_mint.get_extension::().is_err(), - ManifestError::InvalidMint, - "Transfer fee mints are not allowed", - )?; } } @@ -98,23 +92,33 @@ pub(crate) fn process_create_market( }; let (_vault_key, bump) = get_vault_address(market.key, mint.key); - let space: usize = spl_token::state::Account::LEN; let seeds: Vec> = vec![ b"vault".to_vec(), market.key.as_ref().to_vec(), mint.key.as_ref().to_vec(), vec![bump], ]; - create_account( - payer.as_ref(), - token_account, - system_program.as_ref(), - &token_program_for_mint, - &rent, - space as u64, - seeds, - )?; + if is_mint_22 { + let mint_data: Ref<'_, &mut [u8]> = mint.data.borrow(); + let mint_with_extension: PodStateWithExtensions<'_, PodMint> = + PodStateWithExtensions::::unpack(&mint_data).unwrap(); + let mint_extensions: Vec = + mint_with_extension.get_extension_types()?; + let required_extensions: Vec = + ExtensionType::get_required_init_account_extensions(&mint_extensions); + let space: usize = + ExtensionType::try_calculate_account_len::(&required_extensions)?; + create_account( + payer.as_ref(), + token_account, + system_program.as_ref(), + &token_program_for_mint, + &rent, + space as u64, + seeds, + )?; + invoke( &spl_token_2022::instruction::initialize_account3( &token_program_for_mint, @@ -130,6 +134,17 @@ pub(crate) fn process_create_market( ], )?; } else { + let space: usize = spl_token::state::Account::LEN; + create_account( + payer.as_ref(), + token_account, + system_program.as_ref(), + &token_program_for_mint, + &rent, + space as u64, + seeds, + )?; + invoke( &spl_token::instruction::initialize_account3( &token_program_for_mint, diff --git a/programs/manifest/src/program/processor/deposit.rs b/programs/manifest/src/program/processor/deposit.rs index af0315605..ea2744d4e 100644 --- a/programs/manifest/src/program/processor/deposit.rs +++ b/programs/manifest/src/program/processor/deposit.rs @@ -30,6 +30,8 @@ pub(crate) fn process_deposit( ) -> ProgramResult { let deposit_context: DepositContext = DepositContext::load(accounts)?; let DepositParams { amount_atoms } = DepositParams::try_from_slice(data)?; + // Due to transfer fees, this might not be what you expect. + let mut deposited_amount_atoms: u64 = amount_atoms; let DepositContext { market, @@ -48,6 +50,7 @@ pub(crate) fn process_deposit( &trader_token.try_borrow_data()?[0..32] == dynamic_account.get_base_mint().as_ref(); if *vault.owner == spl_token_2022::id() { + let before_vault_balance_atoms: u64 = vault.get_balance_atoms(); invoke( &spl_token_2022::instruction::transfer_checked( token_program.key, @@ -76,9 +79,10 @@ pub(crate) fn process_deposit( ], )?; - // TODO: Check the actual amount received and use that as the - // amount_atoms, rather than what the user said because of transfer - // fees. + let after_vault_balance_atoms: u64 = vault.get_balance_atoms(); + deposited_amount_atoms = after_vault_balance_atoms + .checked_sub(before_vault_balance_atoms) + .unwrap(); } else { invoke( &spl_token::instruction::transfer( @@ -98,7 +102,7 @@ pub(crate) fn process_deposit( )?; } - dynamic_account.deposit(payer.key, amount_atoms, is_base)?; + dynamic_account.deposit(payer.key, deposited_amount_atoms, is_base)?; emit_stack(DepositLog { market: *market.key, @@ -108,7 +112,7 @@ pub(crate) fn process_deposit( } else { *dynamic_account.get_quote_mint() }, - amount_atoms, + amount_atoms: deposited_amount_atoms, })?; Ok(()) diff --git a/programs/manifest/src/program/processor/global_deposit.rs b/programs/manifest/src/program/processor/global_deposit.rs index b103deebb..f2787bad0 100644 --- a/programs/manifest/src/program/processor/global_deposit.rs +++ b/programs/manifest/src/program/processor/global_deposit.rs @@ -31,6 +31,8 @@ pub(crate) fn process_global_deposit( ) -> ProgramResult { let global_deposit_context: GlobalDepositContext = GlobalDepositContext::load(accounts)?; let GlobalDepositParams { amount_atoms } = GlobalDepositParams::try_from_slice(data)?; + // Due to transfer fees, this might not be what you expect. + let mut deposited_amount_atoms: u64 = amount_atoms; let GlobalDepositContext { payer, @@ -47,6 +49,7 @@ pub(crate) fn process_global_deposit( // Do the token transfer if *global_vault.owner == spl_token_2022::id() { + let before_vault_balance_atoms: u64 = global_vault.get_balance_atoms(); invoke( &spl_token_2022::instruction::transfer_checked( token_program.key, @@ -66,9 +69,10 @@ pub(crate) fn process_global_deposit( payer.as_ref().clone(), ], )?; - // TODO: Check the actual amount received and use that as the - // amount_atoms, rather than what the user said because of transfer - // fees. + let after_vault_balance_atoms: u64 = global_vault.get_balance_atoms(); + deposited_amount_atoms = after_vault_balance_atoms + .checked_sub(before_vault_balance_atoms) + .unwrap(); } else { invoke( &spl_token::instruction::transfer( @@ -91,7 +95,7 @@ pub(crate) fn process_global_deposit( emit_stack(GlobalDepositLog { global: *global.key, trader: *payer.key, - global_atoms: GlobalAtoms::new(amount_atoms), + global_atoms: GlobalAtoms::new(deposited_amount_atoms), })?; Ok(()) diff --git a/programs/manifest/src/validation/token_checkers.rs b/programs/manifest/src/validation/token_checkers.rs index 929fbc596..3fb109de5 100644 --- a/programs/manifest/src/validation/token_checkers.rs +++ b/programs/manifest/src/validation/token_checkers.rs @@ -1,9 +1,8 @@ use crate::require; -use solana_program::{ - account_info::AccountInfo, program_error::ProgramError, program_pack::Pack, pubkey::Pubkey, +use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; +use spl_token_2022::{ + check_spl_token_program_account, extension::StateWithExtensions, state::Mint, }; -use spl_token::state::Mint; -use spl_token_2022::check_spl_token_program_account; use std::ops::Deref; #[derive(Clone)] @@ -15,7 +14,8 @@ pub struct MintAccountInfo<'a, 'info> { impl<'a, 'info> MintAccountInfo<'a, 'info> { pub fn new(info: &'a AccountInfo<'info>) -> Result, ProgramError> { check_spl_token_program_account(info.owner)?; - let mint: Mint = Mint::unpack(&info.try_borrow_data()?)?; + + let mint: Mint = StateWithExtensions::::unpack(&info.data.borrow())?.base; Ok(Self { mint, info }) } @@ -59,6 +59,14 @@ impl<'a, 'info> TokenAccountInfo<'a, 'info> { ) } + pub fn get_balance_atoms(&self) -> u64 { + u64::from_le_bytes( + self.info.try_borrow_data().unwrap()[64..72] + .try_into() + .unwrap(), + ) + } + pub fn new_with_owner( info: &'a AccountInfo<'info>, mint: &Pubkey, diff --git a/programs/manifest/tests/cases/token22.rs b/programs/manifest/tests/cases/token22.rs index 282576cfb..3e716e270 100644 --- a/programs/manifest/tests/cases/token22.rs +++ b/programs/manifest/tests/cases/token22.rs @@ -1,10 +1,11 @@ -use std::{cell::RefCell, rc::Rc}; +use std::{cell::RefCell, rc::Rc, u64}; use manifest::{ program::{ batch_update::PlaceOrderParams, batch_update_instruction, claim_seat_instruction, create_market_instructions, deposit_instruction, swap_instruction, withdraw_instruction, }, + quantities::WrapperU64, state::{OrderType, NO_EXPIRATION_LAST_VALID_SLOT}, }; use solana_program_test::{processor, ProgramTest, ProgramTestContext}; @@ -542,3 +543,179 @@ async fn token22_quote() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test] +async fn token22_deposit_transfer_fee() -> anyhow::Result<()> { + let program_test: ProgramTest = ProgramTest::new( + "manifest", + manifest::ID, + processor!(manifest::process_instruction), + ); + solana_logger::setup_with_default(RUST_LOG_DEFAULT); + + let market_keypair: Keypair = Keypair::new(); + + let context: Rc> = + Rc::new(RefCell::new(program_test.start_with_context().await)); + + let payer_keypair: Keypair = context.borrow().payer.insecure_clone(); + let payer: &Pubkey = &payer_keypair.pubkey(); + + // For this test, usdc is old token and spl is token22. + let usdc_mint_f: MintFixture = + MintFixture::new_with_version(Rc::clone(&context), Some(6), false).await; + + // TODO: Use xfer fee extension + let spl_mint_keypair: Keypair = Keypair::new(); + let extension_types: Vec = + vec![spl_token_2022::extension::ExtensionType::TransferFeeConfig]; + let space: usize = spl_token_2022::extension::ExtensionType::try_calculate_account_len::< + spl_token_2022::state::Mint, + >(&extension_types) + .unwrap(); + // first create the mint account for the new NFT + let mint_rent: u64 = solana_program::sysvar::rent::Rent::default().minimum_balance(space); + + let init_account_ix: Instruction = create_account( + &payer, + &spl_mint_keypair.pubkey(), + mint_rent, + space as u64, + &spl_token_2022::id(), + ); + let init_mint_ix: Instruction = spl_token_2022::instruction::initialize_mint2( + &spl_token_2022::id(), + &spl_mint_keypair.pubkey(), + &payer, + None, + 6, + ) + .unwrap(); + + // 1_000 bps = 10% + let transfer_fee_ix: Instruction = + spl_token_2022::extension::transfer_fee::instruction::initialize_transfer_fee_config( + &spl_token_2022::id(), + &spl_mint_keypair.pubkey(), + None, + None, + 1_000, + u64::MAX, + ) + .unwrap(); + + send_tx_with_retry( + Rc::clone(&context), + &[init_account_ix, transfer_fee_ix, init_mint_ix], + Some(&payer), + &[&payer_keypair, &spl_mint_keypair], + ) + .await + .unwrap(); + + let spl_mint_key: Pubkey = spl_mint_keypair.pubkey(); + + // Create the market with SPL as base which is 2022, USDC as quote which is normal. + let create_market_ixs: Vec = create_market_instructions( + &market_keypair.pubkey(), + &spl_mint_key, + &usdc_mint_f.key, + payer, + ) + .unwrap(); + send_tx_with_retry( + Rc::clone(&context), + &create_market_ixs[..], + Some(&payer), + &[&payer_keypair.insecure_clone(), &market_keypair], + ) + .await?; + + // Claim seat + let claim_seat_ix: Instruction = claim_seat_instruction(&market_keypair.pubkey(), &payer); + send_tx_with_retry( + Rc::clone(&context), + &[claim_seat_ix], + Some(&payer), + &[&payer_keypair.insecure_clone()], + ) + .await?; + + // Create depositor token accounts + let spl_token_account_keypair: Keypair = Keypair::new(); + let rent: Rent = context.borrow_mut().banks_client.get_rent().await.unwrap(); + let create_spl_token_account_ix: Instruction = create_account( + payer, + &spl_token_account_keypair.pubkey(), + rent.minimum_balance(spl_token_2022::state::Account::LEN + 13), + spl_token_2022::state::Account::LEN as u64 + 13, + &spl_token_2022::id(), + ); + let init_spl_token_account_ix: Instruction = spl_token_2022::instruction::initialize_account( + &spl_token_2022::id(), + &spl_token_account_keypair.pubkey(), + &spl_mint_key, + payer, + ) + .unwrap(); + send_tx_with_retry( + Rc::clone(&context), + &[create_spl_token_account_ix, init_spl_token_account_ix], + Some(&payer), + &[ + &payer_keypair.insecure_clone(), + &spl_token_account_keypair.insecure_clone(), + ], + ) + .await?; + + // Add funds to token account. + let spl_mint_to_instruction: Instruction = spl_token_2022::instruction::mint_to( + &spl_token_2022::ID, + &spl_mint_key, + &spl_token_account_keypair.pubkey(), + &payer, + &[&payer], + 1_000_000_000_000_000, + ) + .unwrap(); + send_tx_with_retry( + Rc::clone(&context), + &[spl_mint_to_instruction], + Some(&payer), + &[&payer_keypair.insecure_clone()], + ) + .await?; + + let deposit_spl_ix: Instruction = deposit_instruction( + &market_keypair.pubkey(), + &payer, + &spl_mint_key, + 1_000_000_000, + &spl_token_account_keypair.pubkey(), + spl_token_2022::id(), + ); + send_tx_with_retry( + Rc::clone(&context), + &[deposit_spl_ix], + Some(&payer), + &[&payer_keypair.insecure_clone()], + ) + .await?; + + // TODO: Check that the balance on the seat reflects the xfer fee + let market_account: solana_sdk::account::Account = context + .borrow_mut() + .banks_client + .get_account(market_keypair.pubkey()) + .await + .unwrap() + .unwrap(); + + let market: manifest::state::MarketValue = + manifest::program::get_dynamic_value(market_account.data.as_slice()); + let balance = market.get_trader_balance(&payer); + assert_eq!(balance.0.as_u64(), 900_000_000); + + Ok(()) +} diff --git a/programs/manifest/tests/program_test/fixtures.rs b/programs/manifest/tests/program_test/fixtures.rs index 9e0e7c368..0fc8422eb 100644 --- a/programs/manifest/tests/program_test/fixtures.rs +++ b/programs/manifest/tests/program_test/fixtures.rs @@ -26,7 +26,7 @@ use solana_sdk::{ program_pack::Pack, signature::Keypair, signer::Signer, system_instruction::create_account, transaction::Transaction, }; -use spl_token::state::Mint; +use spl_token_2022::state::Mint; use std::rc::Rc; #[derive(PartialEq)] diff --git a/programs/ui-wrapper/tests/program_test/fixtures.rs b/programs/ui-wrapper/tests/program_test/fixtures.rs index ce22b0872..f46c4c7e1 100644 --- a/programs/ui-wrapper/tests/program_test/fixtures.rs +++ b/programs/ui-wrapper/tests/program_test/fixtures.rs @@ -16,7 +16,7 @@ use solana_sdk::{ signature::Keypair, signer::Signer, system_instruction::create_account, transaction::Transaction, }; -use spl_token::state::Mint; +use spl_token_2022::state::Mint; use std::rc::Rc; use ui_wrapper::{ instruction_builders::{claim_seat_instruction, create_wrapper_instructions}, diff --git a/programs/wrapper/Cargo.toml b/programs/wrapper/Cargo.toml index c137372b7..3b8551256 100644 --- a/programs/wrapper/Cargo.toml +++ b/programs/wrapper/Cargo.toml @@ -24,6 +24,7 @@ hypertree = { path = "../../lib" } # Cannot use workspace because the generator needs to see a version here shank = "0.4.2" spl-token = { workspace = true, features = ["no-entrypoint"] } +spl-token-2022 = { workspace = true, features = ["no-entrypoint"] } solana-program = { workspace = true } borsh = { workspace = true } bytemuck = { workspace = true } diff --git a/programs/wrapper/tests/program_test/fixtures.rs b/programs/wrapper/tests/program_test/fixtures.rs index 8daeb38af..477b85b5e 100644 --- a/programs/wrapper/tests/program_test/fixtures.rs +++ b/programs/wrapper/tests/program_test/fixtures.rs @@ -16,7 +16,7 @@ use solana_sdk::{ signature::Keypair, signer::Signer, system_instruction::create_account, transaction::Transaction, }; -use spl_token::state::Mint; +use spl_token_2022::state::Mint; use std::rc::Rc; use wrapper::instruction_builders::{ claim_seat_instruction, create_wrapper_instructions, deposit_instruction, withdraw_instruction,