From c8963e8348f42541270ca4cd99e0a76f0e41fd8b Mon Sep 17 00:00:00 2001 From: Jarry Xiao <61092285+jarry-xiao@users.noreply.github.com> Date: Wed, 20 Oct 2021 19:51:38 -0500 Subject: [PATCH] Updating Stateless Offer to Optionally Include Fees (#2507) * Updating Stateless Offer PDA seeds to only require main wallet and mint types * Added optional payment of creator fees for NFTs * Addressed formatting issues * Fixed bugs in fee paying code, removed logs to lower compute limit * Add in check to make sure the proper metadata is passed in * Added workflow for taker posting NFT (maker pays fees) --- Cargo.lock | 31 +++ stateless-asks/program/Cargo.toml | 1 + stateless-asks/program/src/error.rs | 2 + stateless-asks/program/src/instruction.rs | 65 ++++- stateless-asks/program/src/processor.rs | 278 ++++++++++++++++++---- 5 files changed, 328 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ece475e14eb..3f15a99248c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1732,6 +1732,36 @@ dependencies = [ "autocfg", ] +[[package]] +name = "metaplex-token-metadata" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abcc939f0afdc6db054b9998a1292d0a016244b382462e61cfc7c570624982cb" +dependencies = [ + "arrayref", + "borsh", + "metaplex-token-vault", + "num-derive", + "num-traits", + "solana-program", + "spl-token 3.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror", +] + +[[package]] +name = "metaplex-token-vault" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5211991ba3273df89cd5e0f6f558bc8d7453c87c0546f915b4a319e1541df33" +dependencies = [ + "borsh", + "num-derive", + "num-traits", + "solana-program", + "spl-token 3.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror", +] + [[package]] name = "mime" version = "0.3.16" @@ -4037,6 +4067,7 @@ name = "stateless-asks" version = "0.1.0" dependencies = [ "borsh", + "metaplex-token-metadata", "solana-program", "solana-program-test", "solana-sdk", diff --git a/stateless-asks/program/Cargo.toml b/stateless-asks/program/Cargo.toml index bf05c561732..12642bb4e2e 100644 --- a/stateless-asks/program/Cargo.toml +++ b/stateless-asks/program/Cargo.toml @@ -15,6 +15,7 @@ borsh = "0.9.1" solana-program = "1.8.0" spl-token = { version = "3.2", path = "../../token/program", features = ["no-entrypoint"] } spl-associated-token-account = {version = "1.0.3", features = ["no-entrypoint"]} +metaplex-token-metadata = { version = "0.0.1", features = ["no-entrypoint"] } thiserror = "1.0" [dev-dependencies] diff --git a/stateless-asks/program/src/error.rs b/stateless-asks/program/src/error.rs index e1397181138..ef35e575c3c 100644 --- a/stateless-asks/program/src/error.rs +++ b/stateless-asks/program/src/error.rs @@ -18,6 +18,8 @@ pub enum UtilError { StatementFalse, #[error("NotRentExempt")] NotRentExempt, + #[error("NumericalOverflow")] + NumericalOverflow, } impl From for ProgramError { diff --git a/stateless-asks/program/src/instruction.rs b/stateless-asks/program/src/instruction.rs index c4426bd354e..c46f215dba4 100644 --- a/stateless-asks/program/src/instruction.rs +++ b/stateless-asks/program/src/instruction.rs @@ -1,9 +1,11 @@ //! Instruction types + use { borsh::{BorshDeserialize, BorshSchema, BorshSerialize}, solana_program::{ instruction::{AccountMeta, Instruction}, pubkey::Pubkey, + system_program, }, }; @@ -24,6 +26,8 @@ pub enum StatelessOfferInstruction { /// Bob (or anyone) executes AcceptOffer /// AcceptOffer { + #[allow(dead_code)] + has_metadata: bool, #[allow(dead_code)] maker_size: u64, #[allow(dead_code)] @@ -47,17 +51,69 @@ pub fn accept_offer( taker_mint: &Pubkey, authority: &Pubkey, token_program_id: &Pubkey, + is_native: bool, + maker_size: u64, + taker_size: u64, + bump_seed: u8, +) -> Instruction { + let init_data = StatelessOfferInstruction::AcceptOffer { + has_metadata: false, + maker_size, + taker_size, + bump_seed, + }; + let data = init_data.try_to_vec().unwrap(); + let mut accounts = vec![ + AccountMeta::new_readonly(*maker_wallet, false), + AccountMeta::new_readonly(*taker_wallet, true), + AccountMeta::new(*maker_src_account, false), + AccountMeta::new(*maker_dst_account, false), + AccountMeta::new(*taker_src_account, false), + AccountMeta::new(*taker_dst_account, false), + AccountMeta::new_readonly(*maker_mint, false), + AccountMeta::new_readonly(*taker_mint, false), + AccountMeta::new_readonly(*authority, false), + AccountMeta::new_readonly(*token_program_id, false), + ]; + if is_native { + accounts.push(AccountMeta::new_readonly(system_program::id(), false)); + } + Instruction { + program_id: *program_id, + accounts, + data, + } +} + +/// Creates an 'initialize' instruction. +#[allow(clippy::too_many_arguments)] +pub fn accept_offer_with_metadata( + program_id: &Pubkey, + maker_wallet: &Pubkey, + taker_wallet: &Pubkey, + maker_src_account: &Pubkey, + maker_dst_account: &Pubkey, + taker_src_account: &Pubkey, + taker_dst_account: &Pubkey, + maker_mint: &Pubkey, + taker_mint: &Pubkey, + authority: &Pubkey, + token_program_id: &Pubkey, + metadata: &Pubkey, + creators: &[&Pubkey], + is_native: bool, maker_size: u64, taker_size: u64, bump_seed: u8, ) -> Instruction { let init_data = StatelessOfferInstruction::AcceptOffer { + has_metadata: true, maker_size, taker_size, bump_seed, }; let data = init_data.try_to_vec().unwrap(); - let accounts = vec![ + let mut accounts = vec![ AccountMeta::new_readonly(*maker_wallet, false), AccountMeta::new_readonly(*taker_wallet, true), AccountMeta::new(*maker_src_account, false), @@ -69,6 +125,13 @@ pub fn accept_offer( AccountMeta::new_readonly(*authority, false), AccountMeta::new_readonly(*token_program_id, false), ]; + if is_native { + accounts.push(AccountMeta::new_readonly(system_program::id(), false)); + } + accounts.push(AccountMeta::new_readonly(*metadata, false)); + for creator in creators.iter() { + accounts.push(AccountMeta::new(**creator, false)); + } Instruction { program_id: *program_id, accounts, diff --git a/stateless-asks/program/src/processor.rs b/stateless-asks/program/src/processor.rs index f70fd943de2..20561146636 100644 --- a/stateless-asks/program/src/processor.rs +++ b/stateless-asks/program/src/processor.rs @@ -1,7 +1,10 @@ //! Program state processor +use metaplex_token_metadata::state::Metadata; use solana_program::program_option::COption; +use std::slice::Iter; +use crate::error::UtilError; use crate::instruction::StatelessOfferInstruction; use crate::validation_utils::{assert_is_ata, assert_keys_equal}; use { @@ -27,12 +30,20 @@ impl Processor { let instruction = StatelessOfferInstruction::try_from_slice(input)?; match instruction { StatelessOfferInstruction::AcceptOffer { + has_metadata, maker_size, taker_size, bump_seed, } => { msg!("Instruction: accept offer"); - process_accept_offer(program_id, accounts, maker_size, taker_size, bump_seed) + process_accept_offer( + program_id, + accounts, + has_metadata, + maker_size, + taker_size, + bump_seed, + ) } } } @@ -41,6 +52,7 @@ impl Processor { fn process_accept_offer( program_id: &Pubkey, accounts: &[AccountInfo], + has_metadata: bool, maker_size: u64, taker_size: u64, bump_seed: u8, @@ -56,9 +68,80 @@ fn process_accept_offer( let taker_src_mint = next_account_info(account_info_iter)?; let transfer_authority = next_account_info(account_info_iter)?; let token_program_info = next_account_info(account_info_iter)?; + let mut system_program_info: Option<&AccountInfo> = None; + let is_native = *taker_src_mint.key == spl_token::native_mint::id(); + if is_native { + assert_keys_equal(*taker_wallet.key, *taker_src_account.key)?; + assert_keys_equal(*maker_wallet.key, *maker_dst_account.key)?; + system_program_info = Some(next_account_info(account_info_iter)?); + } + let seeds = &[ + b"stateless_offer", + maker_wallet.key.as_ref(), + maker_src_mint.key.as_ref(), + taker_src_mint.key.as_ref(), + &maker_size.to_le_bytes(), + &taker_size.to_le_bytes(), + &[bump_seed], + ]; + let (maker_pay_size, taker_pay_size) = if has_metadata { + let metadata_info = next_account_info(account_info_iter)?; + let (maker_metadata_key, _) = Pubkey::find_program_address( + &[ + b"metadata", + metaplex_token_metadata::id().as_ref(), + maker_src_mint.key.as_ref(), + ], + &metaplex_token_metadata::id(), + ); + let (taker_metadata_key, _) = Pubkey::find_program_address( + &[ + b"metadata", + metaplex_token_metadata::id().as_ref(), + taker_src_mint.key.as_ref(), + ], + &metaplex_token_metadata::id(), + ); + if *metadata_info.key == maker_metadata_key { + msg!("Taker pays for fees"); + let taker_remaining_size = pay_creator_fees( + account_info_iter, + metadata_info, + taker_src_account, + taker_wallet, + token_program_info, + system_program_info, + taker_src_mint, + taker_size, + is_native, + &[], + )?; + (maker_size, taker_remaining_size) + } else if *metadata_info.key == taker_metadata_key { + msg!("Maker pays for fees"); + let maker_remaining_size = pay_creator_fees( + account_info_iter, + metadata_info, + maker_src_account, + transfer_authority, // Delegate signs for transfer + token_program_info, + system_program_info, + maker_src_mint, + maker_size, + is_native, + seeds, + )?; + (maker_remaining_size, taker_size) + } else { + msg!("Neither maker nor taker metadata keys match"); + return Err(ProgramError::InvalidAccountData); + } + } else { + (maker_size, taker_size) + }; + let maker_src_token_account: spl_token::state::Account = spl_token::state::Account::unpack(&maker_src_account.data.borrow())?; - msg!("Processed Accounts"); // Ensure that the delegated amount is exactly equal to the maker_size msg!( "Delegate {}", @@ -70,20 +153,10 @@ fn process_accept_offer( "Delegated Amount {}", maker_src_token_account.delegated_amount ); - if maker_src_token_account.delegated_amount != maker_size { + if maker_src_token_account.delegated_amount != maker_pay_size { return Err(ProgramError::InvalidAccountData); } - msg!("Delegated Amount matches"); - let seeds = &[ - b"stateless_offer", - maker_src_account.key.as_ref(), - maker_dst_account.key.as_ref(), - taker_src_mint.key.as_ref(), - &maker_size.to_le_bytes(), - &taker_size.to_le_bytes(), - &[bump_seed], - ]; - let authority_key = Pubkey::create_program_address(seeds, program_id).unwrap(); + let authority_key = Pubkey::create_program_address(seeds, program_id)?; assert_keys_equal(authority_key, *transfer_authority.key)?; // Ensure that authority is the delegate of this token account msg!("Authority key matches"); @@ -92,19 +165,12 @@ fn process_accept_offer( } msg!("Delegate matches"); assert_keys_equal(spl_token::id(), *token_program_info.key)?; - msg!("start"); // Both of these transfers will fail if the `transfer_authority` is the delegate of these ATA's // One consideration is that the taker can get tricked in the case that the maker size is greater than // the token amount in the maker's ATA, but these stateless offers should just be invalidated in // the client. assert_is_ata(maker_src_account, maker_wallet.key, maker_src_mint.key)?; assert_is_ata(taker_dst_account, taker_wallet.key, maker_src_mint.key)?; - msg!( - "Transferring {} from {} to {}", - maker_src_mint.key, - maker_wallet.key, - taker_wallet.key - ); invoke_signed( &spl_token::instruction::transfer( token_program_info.key, @@ -112,7 +178,7 @@ fn process_accept_offer( taker_dst_account.key, transfer_authority.key, &[], - maker_size, + maker_pay_size, )?, &[ maker_src_account.clone(), @@ -122,34 +188,29 @@ fn process_accept_offer( ], &[seeds], )?; - msg!("done tx from maker to taker {}", maker_size); + msg!("done tx from maker to taker {}", maker_pay_size); if *taker_src_mint.key == spl_token::native_mint::id() { - msg!( - "Transferring lamports from {} to {}", - taker_wallet.key, - maker_wallet.key - ); - assert_keys_equal(*taker_wallet.key, *taker_src_account.key)?; - assert_keys_equal(*maker_wallet.key, *maker_dst_account.key)?; - let system_program_info = next_account_info(account_info_iter)?; - assert_keys_equal(system_program::id(), *system_program_info.key)?; - invoke( - &system_instruction::transfer(taker_src_account.key, maker_dst_account.key, taker_size), - &[ - taker_src_account.clone(), - maker_dst_account.clone(), - system_program_info.clone(), - ], - )?; + match system_program_info { + Some(sys_program_info) => { + assert_keys_equal(system_program::id(), *sys_program_info.key)?; + invoke( + &system_instruction::transfer( + taker_src_account.key, + maker_dst_account.key, + taker_pay_size, + ), + &[ + taker_src_account.clone(), + maker_dst_account.clone(), + sys_program_info.clone(), + ], + )?; + } + _ => return Err(ProgramError::InvalidAccountData), + } } else { assert_is_ata(maker_dst_account, maker_wallet.key, taker_src_mint.key)?; assert_is_ata(taker_src_account, taker_wallet.key, taker_src_mint.key)?; - msg!( - "Transferring {} from {} to {}", - taker_src_mint.key, - taker_wallet.key, - maker_wallet.key - ); invoke( &spl_token::instruction::transfer( token_program_info.key, @@ -157,7 +218,7 @@ fn process_accept_offer( maker_dst_account.key, taker_wallet.key, &[], - taker_size, + taker_pay_size, )?, &[ taker_src_account.clone(), @@ -167,7 +228,128 @@ fn process_accept_offer( ], )?; } - msg!("done tx from taker to maker {}", taker_size); + msg!("done tx from taker to maker {}", taker_pay_size); msg!("done!"); Ok(()) } + +#[allow(clippy::too_many_arguments)] +fn pay_creator_fees<'a>( + account_info_iter: &mut Iter>, + metadata_info: &AccountInfo<'a>, + src_account_info: &AccountInfo<'a>, + src_authority_info: &AccountInfo<'a>, + token_program_info: &AccountInfo<'a>, + system_program_info: Option<&AccountInfo<'a>>, + fee_mint: &AccountInfo<'a>, + size: u64, + is_native: bool, + seeds: &[&[u8]], +) -> Result { + let metadata = Metadata::from_account_info(metadata_info)?; + let fees = metadata.data.seller_fee_basis_points; + let total_fee = (fees as u64) + .checked_mul(size) + .ok_or(UtilError::NumericalOverflow)? + .checked_div(10000) + .ok_or(UtilError::NumericalOverflow)?; + let mut remaining_fee = total_fee; + let remaining_size = size + .checked_sub(total_fee) + .ok_or(UtilError::NumericalOverflow)?; + match metadata.data.creators { + Some(creators) => { + for creator in creators { + let pct = creator.share as u64; + let creator_fee = pct + .checked_mul(total_fee) + .ok_or(UtilError::NumericalOverflow)? + .checked_div(100) + .ok_or(UtilError::NumericalOverflow)?; + remaining_fee = remaining_fee + .checked_sub(creator_fee) + .ok_or(UtilError::NumericalOverflow)?; + let current_creator_info = next_account_info(account_info_iter)?; + assert_keys_equal(creator.address, *current_creator_info.key)?; + if !is_native { + let current_creator_token_account_info = next_account_info(account_info_iter)?; + assert_is_ata( + current_creator_token_account_info, + current_creator_info.key, + fee_mint.key, + )?; + if creator_fee > 0 { + if seeds.is_empty() { + invoke( + &spl_token::instruction::transfer( + token_program_info.key, + src_account_info.key, + current_creator_token_account_info.key, + src_authority_info.key, + &[], + creator_fee, + )?, + &[ + src_account_info.clone(), + current_creator_token_account_info.clone(), + src_authority_info.clone(), + token_program_info.clone(), + ], + )?; + } else { + invoke_signed( + &spl_token::instruction::transfer( + token_program_info.key, + src_account_info.key, + current_creator_token_account_info.key, + src_authority_info.key, + &[], + creator_fee, + )?, + &[ + src_account_info.clone(), + current_creator_token_account_info.clone(), + src_authority_info.clone(), + token_program_info.clone(), + ], + &[seeds], + )?; + } + } + } else if creator_fee > 0 { + if !seeds.is_empty() { + msg!("Maker cannot pay with native SOL"); + return Err(ProgramError::InvalidAccountData); + } + match system_program_info { + Some(sys_program_info) => { + invoke( + &system_instruction::transfer( + src_account_info.key, + current_creator_info.key, + creator_fee, + ), + &[ + src_account_info.clone(), + current_creator_info.clone(), + sys_program_info.clone(), + ], + )?; + } + None => { + msg!("Invalid System Program Info"); + return Err(ProgramError::IncorrectProgramId); + } + } + } + } + } + None => { + msg!("No creators found in metadata"); + } + } + // Any dust is returned to the party posting the NFT + Ok(remaining_size + .checked_add(remaining_fee) + .ok_or(UtilError::NumericalOverflow)?) +}