356 lines
14 KiB
Rust
356 lines
14 KiB
Rust
//! 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 {
|
|
borsh::BorshDeserialize,
|
|
solana_program::{
|
|
account_info::next_account_info,
|
|
account_info::AccountInfo,
|
|
entrypoint::ProgramResult,
|
|
msg,
|
|
program::{invoke, invoke_signed},
|
|
program_error::ProgramError,
|
|
program_pack::Pack,
|
|
pubkey::Pubkey,
|
|
system_instruction, system_program,
|
|
},
|
|
};
|
|
|
|
/// Program state handler.
|
|
pub struct Processor {}
|
|
impl Processor {
|
|
/// Processes [Instruction](enum.Instruction.html).
|
|
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
|
|
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,
|
|
has_metadata,
|
|
maker_size,
|
|
taker_size,
|
|
bump_seed,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn process_accept_offer(
|
|
program_id: &Pubkey,
|
|
accounts: &[AccountInfo],
|
|
has_metadata: bool,
|
|
maker_size: u64,
|
|
taker_size: u64,
|
|
bump_seed: u8,
|
|
) -> ProgramResult {
|
|
let account_info_iter = &mut accounts.iter();
|
|
let maker_wallet = next_account_info(account_info_iter)?;
|
|
let taker_wallet = next_account_info(account_info_iter)?;
|
|
let maker_src_account = next_account_info(account_info_iter)?;
|
|
let maker_dst_account = next_account_info(account_info_iter)?;
|
|
let taker_src_account = next_account_info(account_info_iter)?;
|
|
let taker_dst_account = next_account_info(account_info_iter)?;
|
|
let maker_src_mint = next_account_info(account_info_iter)?;
|
|
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())?;
|
|
// Ensure that the delegated amount is exactly equal to the maker_size
|
|
msg!(
|
|
"Delegate {}",
|
|
maker_src_token_account
|
|
.delegate
|
|
.unwrap_or(*maker_wallet.key)
|
|
);
|
|
msg!(
|
|
"Delegated Amount {}",
|
|
maker_src_token_account.delegated_amount
|
|
);
|
|
if maker_src_token_account.delegated_amount != maker_pay_size {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
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");
|
|
if maker_src_token_account.delegate != COption::Some(authority_key) {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
msg!("Delegate matches");
|
|
assert_keys_equal(spl_token::id(), *token_program_info.key)?;
|
|
// 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)?;
|
|
invoke_signed(
|
|
&spl_token::instruction::transfer(
|
|
token_program_info.key,
|
|
maker_src_account.key,
|
|
taker_dst_account.key,
|
|
transfer_authority.key,
|
|
&[],
|
|
maker_pay_size,
|
|
)?,
|
|
&[
|
|
maker_src_account.clone(),
|
|
taker_dst_account.clone(),
|
|
transfer_authority.clone(),
|
|
token_program_info.clone(),
|
|
],
|
|
&[seeds],
|
|
)?;
|
|
msg!("done tx from maker to taker {}", maker_pay_size);
|
|
if *taker_src_mint.key == spl_token::native_mint::id() {
|
|
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)?;
|
|
invoke(
|
|
&spl_token::instruction::transfer(
|
|
token_program_info.key,
|
|
taker_src_account.key,
|
|
maker_dst_account.key,
|
|
taker_wallet.key,
|
|
&[],
|
|
taker_pay_size,
|
|
)?,
|
|
&[
|
|
taker_src_account.clone(),
|
|
maker_dst_account.clone(),
|
|
taker_wallet.clone(),
|
|
token_program_info.clone(),
|
|
],
|
|
)?;
|
|
}
|
|
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<AccountInfo<'a>>,
|
|
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<u64, ProgramError> {
|
|
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)?)
|
|
}
|