From f662f67776f094bb47656ffe74a703471be91dd0 Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Tue, 5 Mar 2024 11:05:22 -0800 Subject: [PATCH 01/24] [mmm] initial commit --- pnpm-lock.yaml | 4 +- programs/mmm/src/ext_util.rs | 7 ++ .../ext_vanilla/ext_sol_deposit_sell.rs | 85 +++++++++++++++++++ .../mmm/src/instructions/ext_vanilla/mod.rs | 3 + programs/mmm/src/instructions/mod.rs | 2 + programs/mmm/src/lib.rs | 1 + 6 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 programs/mmm/src/ext_util.rs create mode 100644 programs/mmm/src/instructions/ext_vanilla/ext_sol_deposit_sell.rs create mode 100644 programs/mmm/src/instructions/ext_vanilla/mod.rs diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1178e8..e8adba3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -802,7 +802,7 @@ packages: /@metaplex-foundation/mpl-candy-machine@5.1.0(fastestsmallesttextencoderdecoder@1.0.22): resolution: {integrity: sha512-pjHpUpWVOCDxK3l6dXxfmJKNQmbjBqnm5ElOl1mJAygnzO8NIPQvrP89y6xSNyo8qZsJyt4ZMYUyD0TdbtKZXQ==} dependencies: - '@metaplex-foundation/beet': 0.7.1 + '@metaplex-foundation/beet': 0.7.2 '@metaplex-foundation/beet-solana': 0.4.1 '@metaplex-foundation/cusper': 0.0.2 '@solana/spl-token': 0.3.11(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22) @@ -1322,7 +1322,7 @@ packages: peerDependencies: '@solana/web3.js': ^1.50.1 dependencies: - '@metaplex-foundation/beet': 0.7.1 + '@metaplex-foundation/beet': 0.7.2 '@metaplex-foundation/beet-solana': 0.4.1 '@solana/web3.js': 1.89.1 bn.js: 5.2.1 diff --git a/programs/mmm/src/ext_util.rs b/programs/mmm/src/ext_util.rs new file mode 100644 index 0000000..e6b03fe --- /dev/null +++ b/programs/mmm/src/ext_util.rs @@ -0,0 +1,7 @@ +use solana_program::{account_info::AccountInfo, pubkey::Pubkey}; +use spl_token_client + +pub fn check_group_for_mint(mint_state: &AccountInfo, group_address: Pubkey) -> Result<()> { + + Ok(()) +} diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_sol_deposit_sell.rs b/programs/mmm/src/instructions/ext_vanilla/ext_sol_deposit_sell.rs new file mode 100644 index 0000000..dce5f98 --- /dev/null +++ b/programs/mmm/src/instructions/ext_vanilla/ext_sol_deposit_sell.rs @@ -0,0 +1,85 @@ +use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize}; +use anchor_spl::{ + associated_token::AssociatedToken, + token_interface::{Mint, TokenAccount, TokenInterface}, +}; + +use crate::{ + constants::*, + errors::MMMErrorCode, + state::{Pool, SellState}, + util::{check_allowlists_for_mint, log_pool}, +}; + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct ExtDepositeSellArgs { + pub asset_amount: u64, +} + +#[derive(Accounts)] +#[instruction(args: ExtDepositeSellArgs)] +pub struct ExtDepositeSell<'info> { + #[account(mut)] + pub owner: Signer<'info>, + pub cosigner: Signer<'info>, + #[account( + mut, + seeds = [POOL_PREFIX.as_bytes(), owner.key().as_ref(), pool.uuid.as_ref()], + has_one = owner @ MMMErrorCode::InvalidOwner, + has_one = cosigner @ MMMErrorCode::InvalidCosigner, + bump + )] + pub pool: Box>, + pub asset_mint: InterfaceAccount<'info, Mint>, + #[account( + mut, + associated_token::mint = asset_mint, + associated_token::authority = owner, + associated_token::token_program = token_program, + )] + pub asset_token_account: Box>, + #[account( + init_if_needed, + payer = owner, + associated_token::mint = asset_mint, + associated_token::authority = pool, + associated_token::token_program = token_program, + + )] + pub sellside_escrow_token_account: Box>, + #[account( + init_if_needed, + payer = owner, + seeds = [ + SELL_STATE_PREFIX.as_bytes(), + pool.key().as_ref(), + asset_mint.key().as_ref(), + ], + space = SellState::LEN, + bump + )] + pub sell_state: Account<'info, SellState>, + pub system_program: Program<'info, System>, + pub token_program: Interface<'info, TokenInterface>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub rent: Sysvar<'info, Rent>, +} + +pub fn handler(ctx: Context, args: ExtDepositeSellArgs) -> Result<()> { + let owner = &ctx.accounts.owner; + let asset_token_account = &ctx.accounts.asset_token_account; + let asset_mint = &ctx.accounts.asset_mint; + let sellside_escrow_token_account = &ctx.accounts.sellside_escrow_token_account; + let token_program = &ctx.accounts.token_program; + let pool = &mut ctx.accounts.pool; + let sell_state = &mut ctx.accounts.sell_state; + + if pool.using_shared_escrow() { + return Err(MMMErrorCode::InvalidPool.into()); + } + + // check group/membership to determine + // the added token belongs to the pool group + + Ok(()) +} diff --git a/programs/mmm/src/instructions/ext_vanilla/mod.rs b/programs/mmm/src/instructions/ext_vanilla/mod.rs new file mode 100644 index 0000000..29f1485 --- /dev/null +++ b/programs/mmm/src/instructions/ext_vanilla/mod.rs @@ -0,0 +1,3 @@ +pub mod ext_sol_deposit_sell; + +pub use ext_sol_deposit_sell::*; \ No newline at end of file diff --git a/programs/mmm/src/instructions/mod.rs b/programs/mmm/src/instructions/mod.rs index b81d88e..755c4a9 100644 --- a/programs/mmm/src/instructions/mod.rs +++ b/programs/mmm/src/instructions/mod.rs @@ -4,11 +4,13 @@ pub mod admin; pub mod mip1; pub mod ocp; pub mod vanilla; +pub mod ext_vanilla; pub use admin::*; pub use mip1::*; pub use ocp::*; pub use vanilla::*; +pub use ext_vanilla::*; use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize}; diff --git a/programs/mmm/src/lib.rs b/programs/mmm/src/lib.rs index 8a1f76e..5f25e24 100644 --- a/programs/mmm/src/lib.rs +++ b/programs/mmm/src/lib.rs @@ -11,6 +11,7 @@ mod errors; pub mod instructions; pub mod state; pub mod util; +mod ext_util; use instructions::*; From dcaaa5d19c3546200b99bb0d4bfd34a6687ff8df Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Wed, 6 Mar 2024 14:52:44 -0800 Subject: [PATCH 02/24] [mmm] deposit sell for T22 extension --- Cargo.lock | 1 + programs/mmm/Cargo.toml | 1 + programs/mmm/src/errors.rs | 2 + programs/mmm/src/ext_util.rs | 40 ++++++++++++- ...ol_deposit_sell.rs => ext_deposit_sell.rs} | 58 +++++++++++++++++-- .../mmm/src/instructions/ext_vanilla/mod.rs | 4 +- programs/mmm/src/state.rs | 6 +- sdk/src/idl/mmm.ts | 24 ++++++++ 8 files changed, 123 insertions(+), 13 deletions(-) rename programs/mmm/src/instructions/ext_vanilla/{ext_sol_deposit_sell.rs => ext_deposit_sell.rs} (58%) diff --git a/Cargo.lock b/Cargo.lock index 87928b3..a372544 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1313,6 +1313,7 @@ dependencies = [ "spl-associated-token-account", "spl-token", "spl-token-2022 1.0.0", + "spl-token-group-interface", ] [[package]] diff --git a/programs/mmm/Cargo.toml b/programs/mmm/Cargo.toml index add245b..cd6e360 100644 --- a/programs/mmm/Cargo.toml +++ b/programs/mmm/Cargo.toml @@ -22,6 +22,7 @@ community-managed-token = { version = "0.3.1", features = ["no-entrypoint"] } mpl-token-metadata = { version = "4.0.0" } open_creator_protocol = { version = "0.4.2", features = ["cpi"] } solana-program = "~1.17" +spl-token-group-interface = "0.1.0" spl-token = { version = "4.0.0", features = ["no-entrypoint"] } spl-associated-token-account = { version = "2.2.0", features = [ "no-entrypoint", diff --git a/programs/mmm/src/errors.rs b/programs/mmm/src/errors.rs index 4061e99..d40c38e 100644 --- a/programs/mmm/src/errors.rs +++ b/programs/mmm/src/errors.rs @@ -62,4 +62,6 @@ pub enum MMMErrorCode { UnexpectedMetadataUri, // 0x178c #[msg("Invalid remaining accounts")] InvalidRemainingAccounts, // 0x178d + #[msg("Invalid token extensions")] + InValidTokenExtension, // 0x178e } diff --git a/programs/mmm/src/ext_util.rs b/programs/mmm/src/ext_util.rs index e6b03fe..cf574aa 100644 --- a/programs/mmm/src/ext_util.rs +++ b/programs/mmm/src/ext_util.rs @@ -1,7 +1,41 @@ use solana_program::{account_info::AccountInfo, pubkey::Pubkey}; -use spl_token_client +use spl_token_2022::{ + extension::{ + group_member_pointer::GroupMemberPointer, BaseStateWithExtensions, StateWithExtensions, + }, + state::Mint as Token22Mint, +}; +use spl_token_group_interface::state::TokenGroupMember; + +use crate::state::{Allowlist, ALLOWLIST_KIND_GROUP}; + +use {crate::errors::MMMErrorCode, anchor_lang::prelude::*}; + +pub fn check_group_ext_for_mint(token_mint: &AccountInfo, allowlists: &[Allowlist]) -> Result<()> { + if token_mint.data_is_empty() { + return Err(MMMErrorCode::InvalidTokenMint.into()); + } + let borrowed_data = token_mint.data.borrow(); + let mint_deserialized = StateWithExtensions::::unpack(&borrowed_data)?; + if !mint_deserialized.base.is_initialized { + return Err(MMMErrorCode::InvalidTokenMint.into()); + } + if let Ok(group_member_ptr) = mint_deserialized.get_extension::() { + if Some(*token_mint.key) != Option::::from(group_member_ptr.member_address) { + return Err(MMMErrorCode::InValidTokenExtension.into()); + } + } + if let Ok(group_member) = mint_deserialized.get_extension::() { + let group_address = allowlists + .iter() + .find(|allowlist| allowlist.kind == ALLOWLIST_KIND_GROUP) + .map(|allowlist| allowlist.value); + if Some(group_member.group) != group_address { + return Err(MMMErrorCode::InValidTokenExtension.into()); + } + } else { + return Err(MMMErrorCode::InValidTokenExtension.into()); + } -pub fn check_group_for_mint(mint_state: &AccountInfo, group_address: Pubkey) -> Result<()> { - Ok(()) } diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_sol_deposit_sell.rs b/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs similarity index 58% rename from programs/mmm/src/instructions/ext_vanilla/ext_sol_deposit_sell.rs rename to programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs index dce5f98..c4242a2 100644 --- a/programs/mmm/src/instructions/ext_vanilla/ext_sol_deposit_sell.rs +++ b/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs @@ -3,12 +3,15 @@ use anchor_spl::{ associated_token::AssociatedToken, token_interface::{Mint, TokenAccount, TokenInterface}, }; +use solana_program::program::invoke; +use spl_token_2022::onchain::invoke_transfer_checked; use crate::{ constants::*, errors::MMMErrorCode, + ext_util::check_group_ext_for_mint, state::{Pool, SellState}, - util::{check_allowlists_for_mint, log_pool}, + util::log_pool, }; #[derive(AnchorSerialize, AnchorDeserialize)] @@ -44,7 +47,6 @@ pub struct ExtDepositeSell<'info> { associated_token::mint = asset_mint, associated_token::authority = pool, associated_token::token_program = token_program, - )] pub sellside_escrow_token_account: Box>, #[account( @@ -65,7 +67,10 @@ pub struct ExtDepositeSell<'info> { pub rent: Sysvar<'info, Rent>, } -pub fn handler(ctx: Context, args: ExtDepositeSellArgs) -> Result<()> { +pub fn handler<'info>( + ctx: Context<'_, '_, '_, 'info, ExtDepositeSell<'info>>, + args: ExtDepositeSellArgs, +) -> Result<()> { let owner = &ctx.accounts.owner; let asset_token_account = &ctx.accounts.asset_token_account; let asset_mint = &ctx.accounts.asset_mint; @@ -75,11 +80,52 @@ pub fn handler(ctx: Context, args: ExtDepositeSellArgs) -> Resu let sell_state = &mut ctx.accounts.sell_state; if pool.using_shared_escrow() { - return Err(MMMErrorCode::InvalidPool.into()); + return Err(MMMErrorCode::InvalidAccountState.into()); } - // check group/membership to determine - // the added token belongs to the pool group + check_group_ext_for_mint(&asset_mint.to_account_info(), &pool.allowlists)?; + invoke_transfer_checked( + token_program.key, + asset_token_account.to_account_info(), + asset_mint.to_account_info(), + sellside_escrow_token_account.to_account_info(), + owner.to_account_info(), + ctx.remaining_accounts, + args.asset_amount, + 0, + &[], + )?; + + if asset_token_account.amount == args.asset_amount { + invoke( + &spl_token_2022::instruction::close_account( + token_program.key, + &asset_token_account.key(), + &owner.key(), + &owner.key(), + &[], + )?, + &[ + asset_token_account.to_account_info(), + owner.to_account_info(), + ], + )?; + } + + pool.sellside_asset_amount = pool + .sellside_asset_amount + .checked_add(args.asset_amount) + .ok_or(MMMErrorCode::NumericOverflow)?; + + sell_state.pool = pool.key(); + sell_state.pool_owner = owner.key(); + sell_state.asset_mint = asset_mint.key(); + sell_state.cosigner_annotation = pool.cosigner_annotation; + sell_state.asset_amount = sell_state + .asset_amount + .checked_add(args.asset_amount) + .ok_or(MMMErrorCode::NumericOverflow)?; + log_pool("post_deposit_sell", pool)?; Ok(()) } diff --git a/programs/mmm/src/instructions/ext_vanilla/mod.rs b/programs/mmm/src/instructions/ext_vanilla/mod.rs index 29f1485..5ceb1d5 100644 --- a/programs/mmm/src/instructions/ext_vanilla/mod.rs +++ b/programs/mmm/src/instructions/ext_vanilla/mod.rs @@ -1,3 +1,3 @@ -pub mod ext_sol_deposit_sell; +pub mod ext_deposit_sell; -pub use ext_sol_deposit_sell::*; \ No newline at end of file +pub use ext_deposit_sell::*; \ No newline at end of file diff --git a/programs/mmm/src/state.rs b/programs/mmm/src/state.rs index 569fdd2..043d2c1 100644 --- a/programs/mmm/src/state.rs +++ b/programs/mmm/src/state.rs @@ -10,6 +10,7 @@ pub const ALLOWLIST_KIND_FVCA: u8 = 1; pub const ALLOWLIST_KIND_MINT: u8 = 2; pub const ALLOWLIST_KIND_MCC: u8 = 3; pub const ALLOWLIST_KIND_METADATA: u8 = 4; +pub const ALLOWLIST_KIND_GROUP: u8 = 5; // ANY nft will pass the allowlist check, please make sure to use cosigner to check NFT validity pub const ALLOWLIST_KIND_ANY: u8 = u8::MAX; @@ -25,10 +26,11 @@ impl Allowlist { // kind == 2: single mint, useful for SFT // kind == 3: verified MCC // kind == 4: metadata - // kind == 5,6,... will be supported in the future + // kind == 5: group extension + // kind == 6,7,... will be supported in the future // kind == 255: any pub fn valid(&self) -> bool { - if self.kind > ALLOWLIST_KIND_METADATA && self.kind != ALLOWLIST_KIND_ANY { + if self.kind > ALLOWLIST_KIND_GROUP && self.kind != ALLOWLIST_KIND_ANY { return false; } if self.kind != 0 && self.kind != ALLOWLIST_KIND_ANY { diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index d590021..62413ac 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -1899,6 +1899,18 @@ export type Mmm = { ] } }, + { + "name": "ExtDepositeSellArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "assetAmount", + "type": "u64" + } + ] + } + }, { "name": "SolMip1FulfillSellArgs", "type": { @@ -4155,6 +4167,18 @@ export const IDL: Mmm = { ] } }, + { + "name": "ExtDepositeSellArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "assetAmount", + "type": "u64" + } + ] + } + }, { "name": "SolMip1FulfillSellArgs", "type": { From 8b0ea095cccb015c921d3c007c57e41ad184849e Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Wed, 6 Mar 2024 15:16:22 -0800 Subject: [PATCH 03/24] fmt --- programs/mmm/src/instructions/ext_vanilla/mod.rs | 2 +- programs/mmm/src/instructions/mod.rs | 4 ++-- programs/mmm/src/lib.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/programs/mmm/src/instructions/ext_vanilla/mod.rs b/programs/mmm/src/instructions/ext_vanilla/mod.rs index 5ceb1d5..d7e1bda 100644 --- a/programs/mmm/src/instructions/ext_vanilla/mod.rs +++ b/programs/mmm/src/instructions/ext_vanilla/mod.rs @@ -1,3 +1,3 @@ pub mod ext_deposit_sell; -pub use ext_deposit_sell::*; \ No newline at end of file +pub use ext_deposit_sell::*; diff --git a/programs/mmm/src/instructions/mod.rs b/programs/mmm/src/instructions/mod.rs index 755c4a9..9c1b5a9 100644 --- a/programs/mmm/src/instructions/mod.rs +++ b/programs/mmm/src/instructions/mod.rs @@ -1,16 +1,16 @@ #![allow(missing_docs)] pub mod admin; +pub mod ext_vanilla; pub mod mip1; pub mod ocp; pub mod vanilla; -pub mod ext_vanilla; pub use admin::*; +pub use ext_vanilla::*; pub use mip1::*; pub use ocp::*; pub use vanilla::*; -pub use ext_vanilla::*; use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize}; diff --git a/programs/mmm/src/lib.rs b/programs/mmm/src/lib.rs index 5f25e24..7f157ea 100644 --- a/programs/mmm/src/lib.rs +++ b/programs/mmm/src/lib.rs @@ -8,10 +8,10 @@ declare_id!("mmm3XBJg5gk8XJxEKBvdgptZz6SgK4tXvn36sodowMc"); mod ata; mod constants; mod errors; +mod ext_util; pub mod instructions; pub mod state; pub mod util; -mod ext_util; use instructions::*; From c414ed68ab480123447d7eb5c3f0498a0be98eff Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Wed, 6 Mar 2024 17:59:27 -0800 Subject: [PATCH 04/24] add metadata check --- Cargo.lock | 1 + programs/mmm/Cargo.toml | 1 + programs/mmm/src/ext_util.rs | 83 ++++++++++++++++++- .../ext_vanilla/ext_deposit_sell.rs | 14 +++- 4 files changed, 94 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a372544..0b283a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1314,6 +1314,7 @@ dependencies = [ "spl-token", "spl-token-2022 1.0.0", "spl-token-group-interface", + "spl-token-metadata-interface", ] [[package]] diff --git a/programs/mmm/Cargo.toml b/programs/mmm/Cargo.toml index cd6e360..ac3ee6d 100644 --- a/programs/mmm/Cargo.toml +++ b/programs/mmm/Cargo.toml @@ -23,6 +23,7 @@ mpl-token-metadata = { version = "4.0.0" } open_creator_protocol = { version = "0.4.2", features = ["cpi"] } solana-program = "~1.17" spl-token-group-interface = "0.1.0" +spl-token-metadata-interface = "0.2.0" spl-token = { version = "4.0.0", features = ["no-entrypoint"] } spl-associated-token-account = { version = "2.2.0", features = [ "no-entrypoint", diff --git a/programs/mmm/src/ext_util.rs b/programs/mmm/src/ext_util.rs index cf574aa..aaa6779 100644 --- a/programs/mmm/src/ext_util.rs +++ b/programs/mmm/src/ext_util.rs @@ -1,18 +1,93 @@ +use anchor_lang::prelude::*; use solana_program::{account_info::AccountInfo, pubkey::Pubkey}; use spl_token_2022::{ extension::{ - group_member_pointer::GroupMemberPointer, BaseStateWithExtensions, StateWithExtensions, + group_member_pointer::GroupMemberPointer, metadata_pointer::MetadataPointer, + BaseStateWithExtensions, StateWithExtensions, }, state::Mint as Token22Mint, }; use spl_token_group_interface::state::TokenGroupMember; +use spl_token_metadata_interface::state::TokenMetadata; -use crate::state::{Allowlist, ALLOWLIST_KIND_GROUP}; +use crate::{errors::MMMErrorCode, state::*}; -use {crate::errors::MMMErrorCode, anchor_lang::prelude::*}; +pub fn check_allowlists_for_mint_ext( + allowlists: &[Allowlist], + token_mint: &AccountInfo, + allowlist_aux: Option, +) -> Result { + if token_mint.owner != &spl_token_2022::ID || token_mint.data_is_empty() { + return Err(MMMErrorCode::InvalidTokenMint.into()); + } + let borrowed_data = token_mint.data.borrow(); + let mint_deserialized = StateWithExtensions::::unpack(&borrowed_data)?; + if !mint_deserialized.base.is_initialized { + return Err(MMMErrorCode::InvalidTokenMint.into()); + } + if let Ok(metadata_ptr) = mint_deserialized.get_extension::() { + if Option::::from(metadata_ptr.metadata_address) != Some(*token_mint.key) { + return Err(MMMErrorCode::InValidTokenExtension.into()); + } + } + let parsed_metadata = mint_deserialized + .get_variable_len_extension::() + .unwrap(); + + if allowlists + .iter() + .any(|&val| val.kind == ALLOWLIST_KIND_METADATA) + { + // If allowlist_aux is not passed in, do not validate URI. + if let Some(ref aux_key) = allowlist_aux { + // Handle URI padding. + if !parsed_metadata.uri.trim().starts_with(aux_key) { + msg!( + "Failed metadata validation. Expected URI: |{}| but got |{}|", + *aux_key, + parsed_metadata.uri + ); + return Err(MMMErrorCode::UnexpectedMetadataUri.into()); + } + } + } + + for allowlist_val in allowlists.iter() { + match allowlist_val.kind { + ALLOWLIST_KIND_EMPTY => {} + ALLOWLIST_KIND_ANY => { + // any is a special case, we don't need to check anything else + return Ok(parsed_metadata); + } + ALLOWLIST_KIND_FVCA => { + return Err(MMMErrorCode::InvalidAllowLists.into()); + } + ALLOWLIST_KIND_MINT => { + if token_mint.key() == allowlist_val.value { + return Ok(parsed_metadata); + } + } + ALLOWLIST_KIND_MCC => { + return Err(MMMErrorCode::InvalidAllowLists.into()); + } + ALLOWLIST_KIND_METADATA | ALLOWLIST_KIND_GROUP => { + // Do not validate URI here, as we already did it above. + // Group is validated in a separate function. + // These checks are separate since allowlist values are unioned together. + continue; + } + _ => { + return Err(MMMErrorCode::InvalidAllowLists.into()); + } + } + } + + // at the end, we didn't find a match, thus return err + Err(MMMErrorCode::InvalidAllowLists.into()) +} pub fn check_group_ext_for_mint(token_mint: &AccountInfo, allowlists: &[Allowlist]) -> Result<()> { - if token_mint.data_is_empty() { + if token_mint.owner != &spl_token_2022::ID || token_mint.data_is_empty() { return Err(MMMErrorCode::InvalidTokenMint.into()); } let borrowed_data = token_mint.data.borrow(); diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs b/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs index c4242a2..fa707fb 100644 --- a/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs +++ b/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs @@ -9,7 +9,8 @@ use spl_token_2022::onchain::invoke_transfer_checked; use crate::{ constants::*, errors::MMMErrorCode, - ext_util::check_group_ext_for_mint, + ext_util::{check_allowlists_for_mint_ext, check_group_ext_for_mint}, + instructions::check_allowlists_for_mint, state::{Pool, SellState}, util::log_pool, }; @@ -17,6 +18,7 @@ use crate::{ #[derive(AnchorSerialize, AnchorDeserialize)] pub struct ExtDepositeSellArgs { pub asset_amount: u64, + pub allowlist_aux: Option, } #[derive(Accounts)] @@ -33,6 +35,10 @@ pub struct ExtDepositeSell<'info> { bump )] pub pool: Box>, + #[account( + mint::token_program = token_program, + constraint = asset_mint.supply == 1 && asset_mint.decimals == 0 @ MMMErrorCode::InvalidTokenMint, + )] pub asset_mint: InterfaceAccount<'info, Mint>, #[account( mut, @@ -83,7 +89,13 @@ pub fn handler<'info>( return Err(MMMErrorCode::InvalidAccountState.into()); } + check_allowlists_for_mint_ext( + &pool.allowlists, + &asset_mint.to_account_info(), + args.allowlist_aux, + )?; check_group_ext_for_mint(&asset_mint.to_account_info(), &pool.allowlists)?; + invoke_transfer_checked( token_program.key, asset_token_account.to_account_info(), From 58e60ed3dd824e231dca24a152da485322612960 Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Wed, 6 Mar 2024 18:07:04 -0800 Subject: [PATCH 05/24] remove mut --- programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs b/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs index fa707fb..0b3acd5 100644 --- a/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs +++ b/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs @@ -10,7 +10,6 @@ use crate::{ constants::*, errors::MMMErrorCode, ext_util::{check_allowlists_for_mint_ext, check_group_ext_for_mint}, - instructions::check_allowlists_for_mint, state::{Pool, SellState}, util::log_pool, }; @@ -41,7 +40,6 @@ pub struct ExtDepositeSell<'info> { )] pub asset_mint: InterfaceAccount<'info, Mint>, #[account( - mut, associated_token::mint = asset_mint, associated_token::authority = owner, associated_token::token_program = token_program, From 71951f522912f9ff6044aadbe345517ca0c40340 Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Wed, 6 Mar 2024 18:19:00 -0800 Subject: [PATCH 06/24] add entrypoints --- programs/mmm/src/lib.rs | 7 ++ sdk/src/idl/mmm.ts | 158 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) diff --git a/programs/mmm/src/lib.rs b/programs/mmm/src/lib.rs index 7f157ea..1480410 100644 --- a/programs/mmm/src/lib.rs +++ b/programs/mmm/src/lib.rs @@ -125,4 +125,11 @@ pub mod mmm { ) -> Result<()> { instructions::set_shared_escrow::handler(ctx, args) } + + pub fn ext_deposit_sell<'info>( + ctx: Context<'_, '_, '_, 'info, ExtDepositeSell<'info>>, + args: ExtDepositeSellArgs, + ) -> Result<()> { + instructions::ext_deposit_sell::handler(ctx, args) + } } diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index 62413ac..41f8d70 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -1601,6 +1601,74 @@ export type Mmm = { } } ] + }, + { + "name": "extDepositSell", + "accounts": [ + { + "name": "owner", + "isMut": true, + "isSigner": true + }, + { + "name": "cosigner", + "isMut": false, + "isSigner": true + }, + { + "name": "pool", + "isMut": true, + "isSigner": false + }, + { + "name": "assetMint", + "isMut": false, + "isSigner": false + }, + { + "name": "assetTokenAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "sellsideEscrowTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "sellState", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ExtDepositeSellArgs" + } + } + ] } ], "accounts": [ @@ -1907,6 +1975,12 @@ export type Mmm = { { "name": "assetAmount", "type": "u64" + }, + { + "name": "allowlistAux", + "type": { + "option": "string" + } } ] } @@ -2262,6 +2336,11 @@ export type Mmm = { "code": 6029, "name": "InvalidRemainingAccounts", "msg": "Invalid remaining accounts" + }, + { + "code": 6030, + "name": "InValidTokenExtension", + "msg": "Invalid token extensions" } ] }; @@ -3869,6 +3948,74 @@ export const IDL: Mmm = { } } ] + }, + { + "name": "extDepositSell", + "accounts": [ + { + "name": "owner", + "isMut": true, + "isSigner": true + }, + { + "name": "cosigner", + "isMut": false, + "isSigner": true + }, + { + "name": "pool", + "isMut": true, + "isSigner": false + }, + { + "name": "assetMint", + "isMut": false, + "isSigner": false + }, + { + "name": "assetTokenAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "sellsideEscrowTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "sellState", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ExtDepositeSellArgs" + } + } + ] } ], "accounts": [ @@ -4175,6 +4322,12 @@ export const IDL: Mmm = { { "name": "assetAmount", "type": "u64" + }, + { + "name": "allowlistAux", + "type": { + "option": "string" + } } ] } @@ -4530,6 +4683,11 @@ export const IDL: Mmm = { "code": 6029, "name": "InvalidRemainingAccounts", "msg": "Invalid remaining accounts" + }, + { + "code": 6030, + "name": "InValidTokenExtension", + "msg": "Invalid token extensions" } ] }; From 230df91215d7f00d51ded1ce42b109c323b5e8ca Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Thu, 7 Mar 2024 11:32:56 -0800 Subject: [PATCH 07/24] merge check fn --- programs/mmm/src/ext_util.rs | 53 +++++++++---------- .../ext_vanilla/ext_deposit_sell.rs | 19 +++---- programs/mmm/src/lib.rs | 2 +- 3 files changed, 31 insertions(+), 43 deletions(-) diff --git a/programs/mmm/src/ext_util.rs b/programs/mmm/src/ext_util.rs index aaa6779..2c65900 100644 --- a/programs/mmm/src/ext_util.rs +++ b/programs/mmm/src/ext_util.rs @@ -25,6 +25,8 @@ pub fn check_allowlists_for_mint_ext( if !mint_deserialized.base.is_initialized { return Err(MMMErrorCode::InvalidTokenMint.into()); } + + // verify metadata extension if let Ok(metadata_ptr) = mint_deserialized.get_extension::() { if Option::::from(metadata_ptr.metadata_address) != Some(*token_mint.key) { return Err(MMMErrorCode::InValidTokenExtension.into()); @@ -52,6 +54,28 @@ pub fn check_allowlists_for_mint_ext( } } + // verify group member extension + if let Ok(group_member_ptr) = mint_deserialized.get_extension::() { + if Some(*token_mint.key) != Option::::from(group_member_ptr.member_address) { + return Err(MMMErrorCode::InValidTokenExtension.into()); + } + } + if let Ok(group_member) = mint_deserialized.get_extension::() { + let group_allowlist = allowlists + .iter() + .find(|allowlist| allowlist.kind == ALLOWLIST_KIND_GROUP); + if Some(group_member.group) != group_allowlist.map(|allowlist| allowlist.value) { + return Err(MMMErrorCode::InValidTokenExtension.into()); + } + // counter spoof check + if Some(group_member.mint) != Some(*token_mint.key) { + return Err(MMMErrorCode::InValidTokenExtension.into()); + } + + } else { + return Err(MMMErrorCode::InValidTokenExtension.into()); + } + for allowlist_val in allowlists.iter() { match allowlist_val.kind { ALLOWLIST_KIND_EMPTY => {} @@ -85,32 +109,3 @@ pub fn check_allowlists_for_mint_ext( // at the end, we didn't find a match, thus return err Err(MMMErrorCode::InvalidAllowLists.into()) } - -pub fn check_group_ext_for_mint(token_mint: &AccountInfo, allowlists: &[Allowlist]) -> Result<()> { - if token_mint.owner != &spl_token_2022::ID || token_mint.data_is_empty() { - return Err(MMMErrorCode::InvalidTokenMint.into()); - } - let borrowed_data = token_mint.data.borrow(); - let mint_deserialized = StateWithExtensions::::unpack(&borrowed_data)?; - if !mint_deserialized.base.is_initialized { - return Err(MMMErrorCode::InvalidTokenMint.into()); - } - if let Ok(group_member_ptr) = mint_deserialized.get_extension::() { - if Some(*token_mint.key) != Option::::from(group_member_ptr.member_address) { - return Err(MMMErrorCode::InValidTokenExtension.into()); - } - } - if let Ok(group_member) = mint_deserialized.get_extension::() { - let group_address = allowlists - .iter() - .find(|allowlist| allowlist.kind == ALLOWLIST_KIND_GROUP) - .map(|allowlist| allowlist.value); - if Some(group_member.group) != group_address { - return Err(MMMErrorCode::InValidTokenExtension.into()); - } - } else { - return Err(MMMErrorCode::InValidTokenExtension.into()); - } - - Ok(()) -} diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs b/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs index 0b3acd5..18c6ee0 100644 --- a/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs +++ b/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs @@ -1,4 +1,4 @@ -use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize}; +use anchor_lang::{prelude::*, AnchorDeserialize}; use anchor_spl::{ associated_token::AssociatedToken, token_interface::{Mint, TokenAccount, TokenInterface}, @@ -9,19 +9,14 @@ use spl_token_2022::onchain::invoke_transfer_checked; use crate::{ constants::*, errors::MMMErrorCode, - ext_util::{check_allowlists_for_mint_ext, check_group_ext_for_mint}, + ext_util::check_allowlists_for_mint_ext, state::{Pool, SellState}, util::log_pool, + DepositSellArgs, }; -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct ExtDepositeSellArgs { - pub asset_amount: u64, - pub allowlist_aux: Option, -} - #[derive(Accounts)] -#[instruction(args: ExtDepositeSellArgs)] +#[instruction(args: DepositSellArgs)] pub struct ExtDepositeSell<'info> { #[account(mut)] pub owner: Signer<'info>, @@ -68,12 +63,11 @@ pub struct ExtDepositeSell<'info> { pub system_program: Program<'info, System>, pub token_program: Interface<'info, TokenInterface>, pub associated_token_program: Program<'info, AssociatedToken>, - pub rent: Sysvar<'info, Rent>, } pub fn handler<'info>( ctx: Context<'_, '_, '_, 'info, ExtDepositeSell<'info>>, - args: ExtDepositeSellArgs, + args: DepositSellArgs, ) -> Result<()> { let owner = &ctx.accounts.owner; let asset_token_account = &ctx.accounts.asset_token_account; @@ -92,7 +86,6 @@ pub fn handler<'info>( &asset_mint.to_account_info(), args.allowlist_aux, )?; - check_group_ext_for_mint(&asset_mint.to_account_info(), &pool.allowlists)?; invoke_transfer_checked( token_program.key, @@ -135,7 +128,7 @@ pub fn handler<'info>( .asset_amount .checked_add(args.asset_amount) .ok_or(MMMErrorCode::NumericOverflow)?; - log_pool("post_deposit_sell", pool)?; + log_pool("post_ext_deposit_sell", pool)?; Ok(()) } diff --git a/programs/mmm/src/lib.rs b/programs/mmm/src/lib.rs index 1480410..8b8e34b 100644 --- a/programs/mmm/src/lib.rs +++ b/programs/mmm/src/lib.rs @@ -128,7 +128,7 @@ pub mod mmm { pub fn ext_deposit_sell<'info>( ctx: Context<'_, '_, '_, 'info, ExtDepositeSell<'info>>, - args: ExtDepositeSellArgs, + args: DepositSellArgs, ) -> Result<()> { instructions::ext_deposit_sell::handler(ctx, args) } From bbe331d2ad7995f57439bff005641122b4f0ef3a Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Thu, 7 Mar 2024 15:53:01 -0800 Subject: [PATCH 08/24] add integration test --- Anchor.toml | 2 +- package.json | 3 +- pnpm-lock.yaml | 75 ++++++++++++++++++- sdk/src/constants.ts | 1 + sdk/src/idl/mmm.ts | 50 +------------ tests/mmm-ext-deposit.spec.ts | 112 +++++++++++++++++++++++++++++ tests/utils/nfts.ts | 131 +++++++++++++++++++++++++++++++++- 7 files changed, 320 insertions(+), 54 deletions(-) create mode 100644 tests/mmm-ext-deposit.spec.ts diff --git a/Anchor.toml b/Anchor.toml index ecd5baf..4e467d4 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -34,7 +34,7 @@ address = "6Huqrb4xxmmNA4NufYdgpmspoLmjXFd3qEfteCddLgSz" # ocp: policy (allow al mmm = "mmm3XBJg5gk8XJxEKBvdgptZz6SgK4tXvn36sodowMc" [scripts] -test = "npx ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" +test = "npx ts-mocha -p ./tsconfig.json -t 1000000 tests/**/mmm-ext-deposit.spec.ts" [toolchain] anchor_version = "0.29.0" diff --git a/package.json b/package.json index 91ace37..1a3dc79 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "@metaplex-foundation/umi-bundle-tests": "^0.8.2", "@metaplex-foundation/umi-web3js-adapters": "^0.8.2", "@project-serum/anchor": "^0.26.0", - "@solana/spl-token": "^0.3.5", + "@solana/spl-token": "^0.4.1", + "@solana/spl-token-group": "^0.0.1", "@solana/web3.js": "^1.65.0", "borsh": "^0.7.0", "old-mpl-token-metadata": "npm:@metaplex-foundation/mpl-token-metadata@2.12.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e8adba3..5996a39 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,8 +30,11 @@ dependencies: specifier: ^0.26.0 version: 0.26.0 '@solana/spl-token': - specifier: ^0.3.5 - version: 0.3.11(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22) + specifier: ^0.4.1 + version: 0.4.1(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22) + '@solana/spl-token-group': + specifier: ^0.0.1 + version: 0.0.1(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22) '@solana/web3.js': specifier: ^1.65.0 version: 1.89.1 @@ -1290,17 +1293,34 @@ packages: /@solana/codecs-core@2.0.0-experimental.8618508: resolution: {integrity: sha512-JCz7mKjVKtfZxkuDtwMAUgA7YvJcA2BwpZaA1NOLcted4OMC4Prwa3DUe3f3181ixPYaRyptbF0Ikq2MbDkYEA==} + /@solana/codecs-core@2.0.0-experimental.9741939: + resolution: {integrity: sha512-7E51aEwLW+1ta6VWbEq0CbvwSUOcIwmaQCLpkOKsF6cwSNqE5GDv/jw9zKuiYOynlBFQO1Ws59a/hlb2wwwWKg==} + dev: false + /@solana/codecs-data-structures@2.0.0-experimental.8618508: resolution: {integrity: sha512-sLpjL9sqzaDdkloBPV61Rht1tgaKq98BCtIKRuyscIrmVPu3wu0Bavk2n/QekmUzaTsj7K1pVSniM0YqCdnEBw==} dependencies: '@solana/codecs-core': 2.0.0-experimental.8618508 '@solana/codecs-numbers': 2.0.0-experimental.8618508 + /@solana/codecs-data-structures@2.0.0-experimental.9741939: + resolution: {integrity: sha512-Ghjx0pFEA22T/cpqg3zXHYlnxqKytjTr1QjgfhWvBrlVFkqr3ZQL4av9/V6sHKvpoi+JNkEwM1wE+tlpt/54CA==} + dependencies: + '@solana/codecs-core': 2.0.0-experimental.9741939 + '@solana/codecs-numbers': 2.0.0-experimental.9741939 + dev: false + /@solana/codecs-numbers@2.0.0-experimental.8618508: resolution: {integrity: sha512-EXQKfzFr3CkKKNzKSZPOOOzchXsFe90TVONWsSnVkonO9z+nGKALE0/L9uBmIFGgdzhhU9QQVFvxBMclIDJo2Q==} dependencies: '@solana/codecs-core': 2.0.0-experimental.8618508 + /@solana/codecs-numbers@2.0.0-experimental.9741939: + resolution: {integrity: sha512-VXBvw8LZdGUJGuC33EFzvaUxCvgNtr9y9Td8zjK9wJDqKSzR0+G53CJv5K4AF25LUu8du8XHBK3VEz3YHl44nQ==} + dependencies: + '@solana/codecs-core': 2.0.0-experimental.9741939 + dev: false + /@solana/codecs-strings@2.0.0-experimental.8618508(fastestsmallesttextencoderdecoder@1.0.22): resolution: {integrity: sha512-b2yhinr1+oe+JDmnnsV0641KQqqDG8AQ16Z/x7GVWO+AWHMpRlHWVXOq8U1yhPMA4VXxl7i+D+C6ql0VGFp0GA==} peerDependencies: @@ -1310,12 +1330,29 @@ packages: '@solana/codecs-numbers': 2.0.0-experimental.8618508 fastestsmallesttextencoderdecoder: 1.0.22 + /@solana/codecs-strings@2.0.0-experimental.9741939(fastestsmallesttextencoderdecoder@1.0.22): + resolution: {integrity: sha512-J1DCTJsAMhFcIIvys/yNWM2Vr4HEWi+LzFB9xxdH+FHTynApjTMd4vI7frgnckuWH7ynp8EZFPnBTje1BphQrw==} + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + dependencies: + '@solana/codecs-core': 2.0.0-experimental.9741939 + '@solana/codecs-numbers': 2.0.0-experimental.9741939 + fastestsmallesttextencoderdecoder: 1.0.22 + dev: false + /@solana/options@2.0.0-experimental.8618508: resolution: {integrity: sha512-fy/nIRAMC3QHvnKi63KEd86Xr/zFBVxNW4nEpVEU2OT0gCEKwHY4Z55YHf7XujhyuM3PNpiBKg/YYw5QlRU4vg==} dependencies: '@solana/codecs-core': 2.0.0-experimental.8618508 '@solana/codecs-numbers': 2.0.0-experimental.8618508 + /@solana/options@2.0.0-experimental.9741939: + resolution: {integrity: sha512-Fj76WDb+SWEEN3i0gEVQHGPHR2v54ECHILluQ5r18deLHjtZpD48a0dZepf0YcyAG7OHofkRdPxInZ3YYDuZeQ==} + dependencies: + '@solana/codecs-core': 2.0.0-experimental.9741939 + '@solana/codecs-numbers': 2.0.0-experimental.9741939 + dev: false + /@solana/spl-account-compression@0.1.10(@solana/web3.js@1.89.1): resolution: {integrity: sha512-IQAOJrVOUo6LCgeWW9lHuXo6JDbi4g3/RkQtvY0SyalvSWk9BIkHHe4IkAzaQw8q/BxEVBIjz8e9bNYWIAESNw==} engines: {node: '>=16'} @@ -1336,6 +1373,22 @@ packages: - utf-8-validate dev: false + /@solana/spl-token-group@0.0.1(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22): + resolution: {integrity: sha512-dnBpjFhAskL+LzLfZhgeZVpDoO5ialtwear3vcHy3f+WNzJn/HITCmf341kekMHcLEeVaJts+4i/daVsfjqq7A==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.87.6 + dependencies: + '@solana/codecs-data-structures': 2.0.0-experimental.9741939 + '@solana/codecs-numbers': 2.0.0-experimental.9741939 + '@solana/codecs-strings': 2.0.0-experimental.9741939(fastestsmallesttextencoderdecoder@1.0.22) + '@solana/options': 2.0.0-experimental.9741939 + '@solana/spl-type-length-value': 0.1.0 + '@solana/web3.js': 1.89.1 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + dev: false + /@solana/spl-token-metadata@0.1.2(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22): resolution: {integrity: sha512-hJYnAJNkDrtkE2Q41YZhCpeOGU/0JgRFXbtrtOuGGeKc3pkEUHB9DDoxZAxx+XRno13GozUleyBi0qypz4c3bw==} engines: {node: '>=16'} @@ -1398,6 +1451,24 @@ packages: - fastestsmallesttextencoderdecoder - utf-8-validate + /@solana/spl-token@0.4.1(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22): + resolution: {integrity: sha512-DEe15GI0l+XLHwtau/3GUwGQJ9YY/VWNE0k/QuXaaGKo4adMZLEAIQUktRc/S2sRqPjvUdR5anZGxQ9p5khWZw==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.90.0 + dependencies: + '@solana/buffer-layout': 4.0.1 + '@solana/buffer-layout-utils': 0.2.0 + '@solana/spl-token-metadata': 0.1.2(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22) + '@solana/web3.js': 1.89.1 + buffer: 6.0.3 + transitivePeerDependencies: + - bufferutil + - encoding + - fastestsmallesttextencoderdecoder + - utf-8-validate + dev: false + /@solana/spl-type-length-value@0.1.0: resolution: {integrity: sha512-JBMGB0oR4lPttOZ5XiUGyvylwLQjt1CPJa6qQ5oM+MBCndfjz2TKKkw0eATlLLcYmq1jBVsNlJ2cD6ns2GR7lA==} engines: {node: '>=16'} diff --git a/sdk/src/constants.ts b/sdk/src/constants.ts index 6d4fc0b..162c385 100644 --- a/sdk/src/constants.ts +++ b/sdk/src/constants.ts @@ -15,5 +15,6 @@ export enum AllowlistKind { mint = 2, mcc = 3, metadata = 4, + group = 5, any = 255, } diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index 41f8d70..5fec886 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -1654,18 +1654,13 @@ export type Mmm = { "name": "associatedTokenProgram", "isMut": false, "isSigner": false - }, - { - "name": "rent", - "isMut": false, - "isSigner": false } ], "args": [ { "name": "args", "type": { - "defined": "ExtDepositeSellArgs" + "defined": "DepositSellArgs" } } ] @@ -1967,24 +1962,6 @@ export type Mmm = { ] } }, - { - "name": "ExtDepositeSellArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "assetAmount", - "type": "u64" - }, - { - "name": "allowlistAux", - "type": { - "option": "string" - } - } - ] - } - }, { "name": "SolMip1FulfillSellArgs", "type": { @@ -4001,18 +3978,13 @@ export const IDL: Mmm = { "name": "associatedTokenProgram", "isMut": false, "isSigner": false - }, - { - "name": "rent", - "isMut": false, - "isSigner": false } ], "args": [ { "name": "args", "type": { - "defined": "ExtDepositeSellArgs" + "defined": "DepositSellArgs" } } ] @@ -4314,24 +4286,6 @@ export const IDL: Mmm = { ] } }, - { - "name": "ExtDepositeSellArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "assetAmount", - "type": "u64" - }, - { - "name": "allowlistAux", - "type": { - "option": "string" - } - } - ] - } - }, { "name": "SolMip1FulfillSellArgs", "type": { diff --git a/tests/mmm-ext-deposit.spec.ts b/tests/mmm-ext-deposit.spec.ts new file mode 100644 index 0000000..0a5219e --- /dev/null +++ b/tests/mmm-ext-deposit.spec.ts @@ -0,0 +1,112 @@ +import * as anchor from '@project-serum/anchor'; +import { + getAssociatedTokenAddress, + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, +} from '@solana/spl-token'; +import { Keypair, PublicKey, SystemProgram } from '@solana/web3.js'; +import { assert } from 'chai'; +import { + Mmm, + AllowlistKind, + getMMMSellStatePDA, + IDL, + MMMProgramID, +} from '../sdk/src'; +import { + airdrop, + createPool, + createTestMintAndTokenT22Vanilla, + getEmptyAllowLists, + getTokenAccount2022, +} from './utils'; + +describe('mmm-ext-deposit', () => { + const { connection } = anchor.AnchorProvider.env(); + const wallet = new anchor.Wallet(Keypair.generate()); + const provider = new anchor.AnchorProvider(connection, wallet, { + commitment: 'processed', + }); + const program = new anchor.Program( + IDL, + MMMProgramID, + provider, + ) as anchor.Program; + const cosigner = Keypair.generate(); + + beforeEach(async () => { + await airdrop(connection, wallet.publicKey, 50); + }); + + describe('ext_deposit_sell', () => { + it('correctly verifies ANY allowlist when depositing nfts', async () => { + const allowlists = [ + { + kind: AllowlistKind.metadata, + allowlist: PublicKey.default, + }, + { + kind: AllowlistKind.group, + allowlist: PublicKey.default, + }, + ...getEmptyAllowLists(1), + ]; + + const { mint, recipientTokenAccount } = + await createTestMintAndTokenT22Vanilla(connection, wallet.payer); + const poolData = await createPool(program, { + owner: wallet.publicKey, + cosigner, + allowlists, + }); + + const poolAta = await getAssociatedTokenAddress( + mint, + poolData.poolKey, + true, + ); + + const { key: sellState } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + mint, + ); + + assert.equal(await connection.getBalance(poolAta), 0); + assert.equal(await connection.getBalance(sellState), 0); + let poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); + + await program.methods + .extDepositSell({ + assetAmount: new anchor.BN(1), + allowlistAux: null, + }) + .accountsStrict({ + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + pool: poolData.poolKey, + assetMint: mint, + assetTokenAccount: recipientTokenAccount, + sellsideEscrowTokenAccount: poolAta, + sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([cosigner]) + .rpc(); + + let nftEscrow = await getTokenAccount2022( + connection, + poolAta, + TOKEN_2022_PROGRAM_ID, + ); + assert.equal(Number(nftEscrow.amount), 1); + assert.equal(nftEscrow.owner.toBase58(), poolData.poolKey.toBase58()); + poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 1); + assert.equal(await connection.getBalance(recipientTokenAccount), 0); + }); + }); +}); diff --git a/tests/utils/nfts.ts b/tests/utils/nfts.ts index 9a12902..5eb191b 100644 --- a/tests/utils/nfts.ts +++ b/tests/utils/nfts.ts @@ -5,8 +5,26 @@ import { PublicKey, token as getSplTokenAmount, } from '@metaplex-foundation/js'; -import { Connection } from '@solana/web3.js'; -import { getKeypair } from './generic'; +import { + Connection, + Keypair, + SystemProgram, + Transaction, +} from '@solana/web3.js'; +import { getKeypair, sendAndAssertTx } from './generic'; +import { + ExtensionType, + TOKEN_2022_PROGRAM_ID, + createAssociatedTokenAccountInstruction, + createInitializeInstruction, + createInitializeGroupMemberPointerInstruction, + createInitializeMetadataPointerInstruction, + createInitializeMint2Instruction, + createMintToInstruction, + getAssociatedTokenAddressSync, + getMintLen, +} from '@solana/spl-token'; +import { createInitializeMemberInstruction } from '@solana/spl-token-group'; export const getMetaplexInstance = (conn: Connection) => { return Metaplex.make(conn).use(keypairIdentity(getKeypair())); @@ -109,3 +127,112 @@ export const mintCollection = async ( return { collection: collectionNft, members: collectionMembers }; }; + +export async function createTestMintAndTokenT22Vanilla( + connection: Connection, + payer: Keypair, + recipient?: PublicKey, + groupAddress?: PublicKey, +) { + const mintKeypair = Keypair.generate(); + const memberAddress = PublicKey.unique(); + const effectiveGroupAddress = groupAddress ?? PublicKey.unique(); + const tokenProgramId = TOKEN_2022_PROGRAM_ID; + const effectiveRecipient = recipient ?? payer.publicKey; + const targetTokenAccount = getAssociatedTokenAddressSync( + mintKeypair.publicKey, + effectiveRecipient, + true, + tokenProgramId, + ); + + const mintSpace = getMintLen([ExtensionType.MetadataPointer]); + const mintLamports = await connection.getMinimumBalanceForRentExemption( + mintSpace * 2, + ); + + const createMintAccountIx = SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: mintKeypair.publicKey, + space: mintSpace, + lamports: mintLamports, + programId: tokenProgramId, + }); + const createPointerIx = createInitializeMetadataPointerInstruction( + mintKeypair.publicKey, + payer.publicKey, + mintKeypair.publicKey, + tokenProgramId, + ); + const createGroupMemberIx = createInitializeMemberInstruction({ + programId: tokenProgramId, + member: memberAddress, + memberMint: mintKeypair.publicKey, + memberMintAuthority: payer.publicKey, + group: effectiveGroupAddress, + groupUpdateAuthority: payer.publicKey, + }); + const createGroupMemberPointerIx = + createInitializeGroupMemberPointerInstruction( + mintKeypair.publicKey, + payer.publicKey, + memberAddress, + tokenProgramId, + ); + const createInitMintIx = createInitializeMint2Instruction( + mintKeypair.publicKey, + 0, + payer.publicKey, + payer.publicKey, + tokenProgramId, + ); + + const createMetadataIx = createInitializeInstruction({ + metadata: mintKeypair.publicKey, + updateAuthority: payer.publicKey, + mint: mintKeypair.publicKey, + mintAuthority: payer.publicKey, + name: 'xyzname', + symbol: 'xyz', + uri: 'example.com', + programId: tokenProgramId, + }); + + const createAtaIx = createAssociatedTokenAccountInstruction( + payer.publicKey, + targetTokenAccount, + effectiveRecipient, + mintKeypair.publicKey, + tokenProgramId, + ); + + const mintToIx = createMintToInstruction( + mintKeypair.publicKey, + targetTokenAccount, + payer.publicKey, + 1, // amount + [], + tokenProgramId, + ); + + const blockhashData = await connection.getLatestBlockhash(); + const tx = new Transaction().add( + createMintAccountIx, + createPointerIx, + createGroupMemberIx, + createGroupMemberPointerIx, + createInitMintIx, + createMetadataIx, + createAtaIx, + mintToIx, + ); + tx.recentBlockhash = blockhashData.blockhash; + tx.feePayer = payer.publicKey; + tx.partialSign(payer, mintKeypair); + await sendAndAssertTx(connection, tx, blockhashData, false); + + return { + mint: mintKeypair.publicKey, + recipientTokenAccount: targetTokenAccount, + }; +} From e0098669e564805fc30ac6fad4c2ca9feca0d177 Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Thu, 7 Mar 2024 20:53:06 -0800 Subject: [PATCH 09/24] integration test fixes --- Anchor.toml | 3 +++ tests/mmm-ext-deposit.spec.ts | 31 ++++++++++++++----------------- tests/utils/nfts.ts | 35 +++++++++++++++++++---------------- 3 files changed, 36 insertions(+), 33 deletions(-) diff --git a/Anchor.toml b/Anchor.toml index 4e467d4..212ff49 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -30,6 +30,9 @@ address = "ocp4vWUzA2z2XMYJ3QhM9vWdyoyoQwAFJhRdVTbvo9E" # ocp: open_creator_prot [[test.validator.clone]] address = "6Huqrb4xxmmNA4NufYdgpmspoLmjXFd3qEfteCddLgSz" # ocp: policy (allow all) +[[test.validator.clone]] +address = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" # token22 + [programs.localnet] mmm = "mmm3XBJg5gk8XJxEKBvdgptZz6SgK4tXvn36sodowMc" diff --git a/tests/mmm-ext-deposit.spec.ts b/tests/mmm-ext-deposit.spec.ts index 0a5219e..c7716c1 100644 --- a/tests/mmm-ext-deposit.spec.ts +++ b/tests/mmm-ext-deposit.spec.ts @@ -4,7 +4,7 @@ import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, } from '@solana/spl-token'; -import { Keypair, PublicKey, SystemProgram } from '@solana/web3.js'; +import { Keypair, SYSVAR_RENT_PUBKEY, SystemProgram } from '@solana/web3.js'; import { assert } from 'chai'; import { Mmm, @@ -16,7 +16,7 @@ import { import { airdrop, createPool, - createTestMintAndTokenT22Vanilla, + createTestMintAndTokenT22VanillaExt, getEmptyAllowLists, getTokenAccount2022, } from './utils'; @@ -40,30 +40,26 @@ describe('mmm-ext-deposit', () => { describe('ext_deposit_sell', () => { it('correctly verifies ANY allowlist when depositing nfts', async () => { - const allowlists = [ - { - kind: AllowlistKind.metadata, - allowlist: PublicKey.default, - }, - { - kind: AllowlistKind.group, - allowlist: PublicKey.default, - }, - ...getEmptyAllowLists(1), - ]; - const { mint, recipientTokenAccount } = - await createTestMintAndTokenT22Vanilla(connection, wallet.payer); + await createTestMintAndTokenT22VanillaExt(connection, wallet.payer); + const poolData = await createPool(program, { owner: wallet.publicKey, cosigner, - allowlists, + allowlists: [ + { + kind: AllowlistKind.metadata, + value: mint, + }, + ...getEmptyAllowLists(5), + ], }); const poolAta = await getAssociatedTokenAddress( mint, poolData.poolKey, true, + TOKEN_2022_PROGRAM_ID, ); const { key: sellState } = getMMMSellStatePDA( @@ -77,10 +73,11 @@ describe('mmm-ext-deposit', () => { let poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); + console.log('start depositting', mint.toBase58()); await program.methods .extDepositSell({ assetAmount: new anchor.BN(1), - allowlistAux: null, + allowlistAux: '', }) .accountsStrict({ owner: wallet.publicKey, diff --git a/tests/utils/nfts.ts b/tests/utils/nfts.ts index 5eb191b..66fefac 100644 --- a/tests/utils/nfts.ts +++ b/tests/utils/nfts.ts @@ -128,15 +128,14 @@ export const mintCollection = async ( return { collection: collectionNft, members: collectionMembers }; }; -export async function createTestMintAndTokenT22Vanilla( +export async function createTestMintAndTokenT22VanillaExt( connection: Connection, payer: Keypair, recipient?: PublicKey, - groupAddress?: PublicKey, ) { const mintKeypair = Keypair.generate(); - const memberAddress = PublicKey.unique(); - const effectiveGroupAddress = groupAddress ?? PublicKey.unique(); + const memberAddress = Keypair.generate().publicKey; + const effectiveGroupKeyPair = Keypair.generate(); const tokenProgramId = TOKEN_2022_PROGRAM_ID; const effectiveRecipient = recipient ?? payer.publicKey; const targetTokenAccount = getAssociatedTokenAddressSync( @@ -146,7 +145,10 @@ export async function createTestMintAndTokenT22Vanilla( tokenProgramId, ); - const mintSpace = getMintLen([ExtensionType.MetadataPointer]); + const mintSpace = getMintLen([ + ExtensionType.MetadataPointer, + ExtensionType.GroupMemberPointer, + ]); const mintLamports = await connection.getMinimumBalanceForRentExemption( mintSpace * 2, ); @@ -158,20 +160,12 @@ export async function createTestMintAndTokenT22Vanilla( lamports: mintLamports, programId: tokenProgramId, }); - const createPointerIx = createInitializeMetadataPointerInstruction( + const createMetadataPointerIx = createInitializeMetadataPointerInstruction( mintKeypair.publicKey, payer.publicKey, mintKeypair.publicKey, tokenProgramId, ); - const createGroupMemberIx = createInitializeMemberInstruction({ - programId: tokenProgramId, - member: memberAddress, - memberMint: mintKeypair.publicKey, - memberMintAuthority: payer.publicKey, - group: effectiveGroupAddress, - groupUpdateAuthority: payer.publicKey, - }); const createGroupMemberPointerIx = createInitializeGroupMemberPointerInstruction( mintKeypair.publicKey, @@ -198,6 +192,15 @@ export async function createTestMintAndTokenT22Vanilla( programId: tokenProgramId, }); + const createGroupMemberIx = createInitializeMemberInstruction({ + programId: tokenProgramId, + member: memberAddress, + memberMint: mintKeypair.publicKey, + memberMintAuthority: payer.publicKey, + group: effectiveGroupKeyPair.publicKey, + groupUpdateAuthority: payer.publicKey, + }); + const createAtaIx = createAssociatedTokenAccountInstruction( payer.publicKey, targetTokenAccount, @@ -218,11 +221,11 @@ export async function createTestMintAndTokenT22Vanilla( const blockhashData = await connection.getLatestBlockhash(); const tx = new Transaction().add( createMintAccountIx, - createPointerIx, - createGroupMemberIx, + createMetadataPointerIx, createGroupMemberPointerIx, createInitMintIx, createMetadataIx, + // createGroupMemberIx, createAtaIx, mintToIx, ); From 4daf47aa84870ca1ac99637b22919b02f1516178 Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Sat, 9 Mar 2024 15:30:24 -0800 Subject: [PATCH 10/24] add multiple unit tests --- Anchor.toml | 7 +- programs/mmm/src/errors.rs | 6 +- programs/mmm/src/ext_util.rs | 45 +- .../ext_vanilla/ext_deposit_sell.rs | 1 + sdk/src/idl/mmm.ts | 22 +- tests/deps/spl_token_2022.so | Bin 0 -> 637792 bytes tests/mmm-ext-deposit.spec.ts | 508 +++++++++++++++++- tests/utils/nfts.ts | 139 ++++- 8 files changed, 687 insertions(+), 41 deletions(-) create mode 100644 tests/deps/spl_token_2022.so diff --git a/Anchor.toml b/Anchor.toml index 212ff49..ae507ce 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -30,14 +30,15 @@ address = "ocp4vWUzA2z2XMYJ3QhM9vWdyoyoQwAFJhRdVTbvo9E" # ocp: open_creator_prot [[test.validator.clone]] address = "6Huqrb4xxmmNA4NufYdgpmspoLmjXFd3qEfteCddLgSz" # ocp: policy (allow all) -[[test.validator.clone]] -address = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" # token22 +[[test.genesis]] +address = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" +program = "./tests/deps/spl_token_2022.so" [programs.localnet] mmm = "mmm3XBJg5gk8XJxEKBvdgptZz6SgK4tXvn36sodowMc" [scripts] -test = "npx ts-mocha -p ./tsconfig.json -t 1000000 tests/**/mmm-ext-deposit.spec.ts" +test = "npx ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" [toolchain] anchor_version = "0.29.0" diff --git a/programs/mmm/src/errors.rs b/programs/mmm/src/errors.rs index d40c38e..eaa9429 100644 --- a/programs/mmm/src/errors.rs +++ b/programs/mmm/src/errors.rs @@ -62,6 +62,8 @@ pub enum MMMErrorCode { UnexpectedMetadataUri, // 0x178c #[msg("Invalid remaining accounts")] InvalidRemainingAccounts, // 0x178d - #[msg("Invalid token extensions")] - InValidTokenExtension, // 0x178e + #[msg("Invalid token metadata extensions")] + InValidTokenMetadataExtension, // 0x178e + #[msg("Invalid token member extensions")] + InValidTokenMemberExtension, // 0x178f } diff --git a/programs/mmm/src/ext_util.rs b/programs/mmm/src/ext_util.rs index 2c65900..b0cbc2b 100644 --- a/programs/mmm/src/ext_util.rs +++ b/programs/mmm/src/ext_util.rs @@ -29,7 +29,7 @@ pub fn check_allowlists_for_mint_ext( // verify metadata extension if let Ok(metadata_ptr) = mint_deserialized.get_extension::() { if Option::::from(metadata_ptr.metadata_address) != Some(*token_mint.key) { - return Err(MMMErrorCode::InValidTokenExtension.into()); + return Err(MMMErrorCode::InValidTokenMetadataExtension.into()); } } let parsed_metadata = mint_deserialized @@ -57,24 +57,10 @@ pub fn check_allowlists_for_mint_ext( // verify group member extension if let Ok(group_member_ptr) = mint_deserialized.get_extension::() { if Some(*token_mint.key) != Option::::from(group_member_ptr.member_address) { - return Err(MMMErrorCode::InValidTokenExtension.into()); + msg!("group member pointer does not point to itself"); + return Err(MMMErrorCode::InValidTokenMemberExtension.into()); } } - if let Ok(group_member) = mint_deserialized.get_extension::() { - let group_allowlist = allowlists - .iter() - .find(|allowlist| allowlist.kind == ALLOWLIST_KIND_GROUP); - if Some(group_member.group) != group_allowlist.map(|allowlist| allowlist.value) { - return Err(MMMErrorCode::InValidTokenExtension.into()); - } - // counter spoof check - if Some(group_member.mint) != Some(*token_mint.key) { - return Err(MMMErrorCode::InValidTokenExtension.into()); - } - - } else { - return Err(MMMErrorCode::InValidTokenExtension.into()); - } for allowlist_val in allowlists.iter() { match allowlist_val.kind { @@ -94,7 +80,15 @@ pub fn check_allowlists_for_mint_ext( ALLOWLIST_KIND_MCC => { return Err(MMMErrorCode::InvalidAllowLists.into()); } - ALLOWLIST_KIND_METADATA | ALLOWLIST_KIND_GROUP => { + ALLOWLIST_KIND_GROUP => { + let group_address = assert_valid_group(&mint_deserialized, token_mint)?; + if group_address != Some(allowlist_val.value) { + msg!("group address |{}| is not allowed", group_address.unwrap()); + return Err(MMMErrorCode::InvalidAllowLists.into()); + } + return Ok(parsed_metadata); + } + ALLOWLIST_KIND_METADATA => { // Do not validate URI here, as we already did it above. // Group is validated in a separate function. // These checks are separate since allowlist values are unioned together. @@ -109,3 +103,18 @@ pub fn check_allowlists_for_mint_ext( // at the end, we didn't find a match, thus return err Err(MMMErrorCode::InvalidAllowLists.into()) } + +pub fn assert_valid_group( + mint_deserialized: &StateWithExtensions<'_, Token22Mint>, + token_mint: &AccountInfo, +) -> Result> { + if let Ok(group_member) = mint_deserialized.get_extension::() { + // counter spoof check + if Some(group_member.mint) != Some(*token_mint.key) { + msg!("group member mint does not match the token mint"); + return Err(MMMErrorCode::InValidTokenMemberExtension.into()); + } + return Ok(Some(group_member.group)); + } + Err(MMMErrorCode::InValidTokenMemberExtension.into()) +} diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs b/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs index 18c6ee0..d66ebf9 100644 --- a/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs +++ b/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs @@ -35,6 +35,7 @@ pub struct ExtDepositeSell<'info> { )] pub asset_mint: InterfaceAccount<'info, Mint>, #[account( + mut, associated_token::mint = asset_mint, associated_token::authority = owner, associated_token::token_program = token_program, diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index 5fec886..580d7f7 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -1627,7 +1627,7 @@ export type Mmm = { }, { "name": "assetTokenAccount", - "isMut": false, + "isMut": true, "isSigner": false }, { @@ -2316,8 +2316,13 @@ export type Mmm = { }, { "code": 6030, - "name": "InValidTokenExtension", - "msg": "Invalid token extensions" + "name": "InValidTokenMetadataExtension", + "msg": "Invalid token metadata extensions" + }, + { + "code": 6031, + "name": "InValidTokenMemberExtension", + "msg": "Invalid token member extensions" } ] }; @@ -3951,7 +3956,7 @@ export const IDL: Mmm = { }, { "name": "assetTokenAccount", - "isMut": false, + "isMut": true, "isSigner": false }, { @@ -4640,8 +4645,13 @@ export const IDL: Mmm = { }, { "code": 6030, - "name": "InValidTokenExtension", - "msg": "Invalid token extensions" + "name": "InValidTokenMetadataExtension", + "msg": "Invalid token metadata extensions" + }, + { + "code": 6031, + "name": "InValidTokenMemberExtension", + "msg": "Invalid token member extensions" } ] }; diff --git a/tests/deps/spl_token_2022.so b/tests/deps/spl_token_2022.so new file mode 100644 index 0000000000000000000000000000000000000000..5c4c3cc15633bdbff440579a638be39aa3b2eb3b GIT binary patch literal 637792 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 literal 0 HcmV?d00001 diff --git a/tests/mmm-ext-deposit.spec.ts b/tests/mmm-ext-deposit.spec.ts index c7716c1..ddfe33b 100644 --- a/tests/mmm-ext-deposit.spec.ts +++ b/tests/mmm-ext-deposit.spec.ts @@ -4,8 +4,8 @@ import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, } from '@solana/spl-token'; -import { Keypair, SYSVAR_RENT_PUBKEY, SystemProgram } from '@solana/web3.js'; -import { assert } from 'chai'; +import { Keypair, PublicKey, SystemProgram } from '@solana/web3.js'; +import { assert, expect } from 'chai'; import { Mmm, AllowlistKind, @@ -16,6 +16,8 @@ import { import { airdrop, createPool, + createTestGroupMemberMint, + createTestGroupMintExt, createTestMintAndTokenT22VanillaExt, getEmptyAllowLists, getTokenAccount2022, @@ -39,9 +41,18 @@ describe('mmm-ext-deposit', () => { }); describe('ext_deposit_sell', () => { - it('correctly verifies ANY allowlist when depositing nfts', async () => { + it('correctly verifies depositing nfts with group allowlist', async () => { + const { groupKeyPair } = await createTestGroupMintExt( + connection, + wallet.payer, + ); const { mint, recipientTokenAccount } = - await createTestMintAndTokenT22VanillaExt(connection, wallet.payer); + await createTestMintAndTokenT22VanillaExt( + connection, + wallet.payer, + undefined, + groupKeyPair, + ); const poolData = await createPool(program, { owner: wallet.publicKey, @@ -51,6 +62,174 @@ describe('mmm-ext-deposit', () => { kind: AllowlistKind.metadata, value: mint, }, + { + kind: AllowlistKind.group, + value: groupKeyPair.publicKey, + }, + ...getEmptyAllowLists(4), + ], + }); + + const poolAta = await getAssociatedTokenAddress( + mint, + poolData.poolKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + const { key: sellState } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + mint, + ); + + assert.equal(await connection.getBalance(poolAta), 0); + assert.equal(await connection.getBalance(sellState), 0); + let poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); + + await program.methods + .extDepositSell({ + assetAmount: new anchor.BN(1), + allowlistAux: 'example.com', + }) + .accountsStrict({ + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + pool: poolData.poolKey, + assetMint: mint, + assetTokenAccount: recipientTokenAccount, + sellsideEscrowTokenAccount: poolAta, + sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([cosigner]) + .rpc({ skipPreflight: true }); + + let nftEscrow = await getTokenAccount2022( + connection, + poolAta, + TOKEN_2022_PROGRAM_ID, + ); + assert.equal(Number(nftEscrow.amount), 1); + assert.equal(nftEscrow.owner.toBase58(), poolData.poolKey.toBase58()); + poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 1); + assert.equal(await connection.getBalance(recipientTokenAccount), 0); + + const sellStateAccountInfo = await program.account.sellState.fetch( + sellState, + ); + assert.equal( + sellStateAccountInfo.pool.toBase58(), + poolData.poolKey.toBase58(), + ); + assert.equal( + sellStateAccountInfo.poolOwner.toBase58(), + wallet.publicKey.toBase58(), + ); + assert.equal(sellStateAccountInfo.assetMint.toBase58(), mint.toBase58()); + assert.equal(sellStateAccountInfo.assetAmount.toNumber(), 1); + assert.deepEqual( + sellStateAccountInfo.cosignerAnnotation, + new Array(32).fill(0), + ); + + const { mint: mint2, recipientTokenAccount: recipientTokenAccount2 } = + await createTestMintAndTokenT22VanillaExt( + connection, + wallet.payer, + undefined, + groupKeyPair, + ); + const poolAta2 = await getAssociatedTokenAddress( + mint2, + poolData.poolKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + let { key: sellState2 } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + mint2, + ); + await program.methods + .extDepositSell({ + assetAmount: new anchor.BN(1), + allowlistAux: '', + }) + .accountsStrict({ + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + pool: poolData.poolKey, + assetMint: mint2, + assetTokenAccount: recipientTokenAccount2, + sellsideEscrowTokenAccount: poolAta2, + sellState: sellState2, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([cosigner]) + .rpc({ skipPreflight: true }); + + let nftEscrow2 = await getTokenAccount2022( + connection, + poolAta2, + TOKEN_2022_PROGRAM_ID, + ); + assert.equal(Number(nftEscrow2.amount), 1); + assert.equal(nftEscrow2.owner.toBase58(), poolData.poolKey.toBase58()); + poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + // should increment by 1 + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 2); + assert.equal(await connection.getBalance(recipientTokenAccount2), 0); + + const sellStateAccountInfo2 = await program.account.sellState.fetch( + sellState2, + ); + assert.equal( + sellStateAccountInfo2.pool.toBase58(), + poolData.poolKey.toBase58(), + ); + assert.equal( + sellStateAccountInfo2.poolOwner.toBase58(), + wallet.publicKey.toBase58(), + ); + assert.equal( + sellStateAccountInfo2.assetMint.toBase58(), + mint2.toBase58(), + ); + assert.equal(sellStateAccountInfo2.assetAmount.toNumber(), 1); + assert.deepEqual( + sellStateAccountInfo2.cosignerAnnotation, + new Array(32).fill(0), + ); + }); + + it('correctly verifies depositing nfts with ANY allowlist', async () => { + const { groupKeyPair } = await createTestGroupMintExt( + connection, + wallet.payer, + ); + const { mint, recipientTokenAccount } = + await createTestMintAndTokenT22VanillaExt( + connection, + wallet.payer, + undefined, + groupKeyPair, + ); + + const poolData = await createPool(program, { + owner: wallet.publicKey, + cosigner, + allowlists: [ + { + kind: AllowlistKind.any, + value: PublicKey.default, + }, ...getEmptyAllowLists(5), ], }); @@ -73,7 +252,6 @@ describe('mmm-ext-deposit', () => { let poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); - console.log('start depositting', mint.toBase58()); await program.methods .extDepositSell({ assetAmount: new anchor.BN(1), @@ -92,7 +270,7 @@ describe('mmm-ext-deposit', () => { associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, }) .signers([cosigner]) - .rpc(); + .rpc({ skipPreflight: true }); let nftEscrow = await getTokenAccount2022( connection, @@ -104,6 +282,324 @@ describe('mmm-ext-deposit', () => { poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 1); assert.equal(await connection.getBalance(recipientTokenAccount), 0); + + const sellStateAccountInfo = await program.account.sellState.fetch( + sellState, + ); + assert.equal( + sellStateAccountInfo.pool.toBase58(), + poolData.poolKey.toBase58(), + ); + assert.equal( + sellStateAccountInfo.poolOwner.toBase58(), + wallet.publicKey.toBase58(), + ); + assert.equal(sellStateAccountInfo.assetMint.toBase58(), mint.toBase58()); + assert.equal(sellStateAccountInfo.assetAmount.toNumber(), 1); + assert.deepEqual( + sellStateAccountInfo.cosignerAnnotation, + new Array(32).fill(0), + ); + + const { mint: mint2, recipientTokenAccount: recipientTokenAccount2 } = + await createTestMintAndTokenT22VanillaExt( + connection, + wallet.payer, + undefined, + groupKeyPair, + ); + const poolAta2 = await getAssociatedTokenAddress( + mint2, + poolData.poolKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + let { key: sellState2 } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + mint2, + ); + await program.methods + .extDepositSell({ + assetAmount: new anchor.BN(1), + allowlistAux: '', + }) + .accountsStrict({ + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + pool: poolData.poolKey, + assetMint: mint2, + assetTokenAccount: recipientTokenAccount2, + sellsideEscrowTokenAccount: poolAta2, + sellState: sellState2, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([cosigner]) + .rpc({ skipPreflight: true }); + + let nftEscrow2 = await getTokenAccount2022( + connection, + poolAta2, + TOKEN_2022_PROGRAM_ID, + ); + assert.equal(Number(nftEscrow2.amount), 1); + assert.equal(nftEscrow2.owner.toBase58(), poolData.poolKey.toBase58()); + poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + // should increment by 1 + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 2); + assert.equal(await connection.getBalance(recipientTokenAccount2), 0); + + const sellStateAccountInfo2 = await program.account.sellState.fetch( + sellState2, + ); + assert.equal( + sellStateAccountInfo2.pool.toBase58(), + poolData.poolKey.toBase58(), + ); + assert.equal( + sellStateAccountInfo2.poolOwner.toBase58(), + wallet.publicKey.toBase58(), + ); + assert.equal( + sellStateAccountInfo2.assetMint.toBase58(), + mint2.toBase58(), + ); + assert.equal(sellStateAccountInfo2.assetAmount.toNumber(), 1); + assert.deepEqual( + sellStateAccountInfo2.cosignerAnnotation, + new Array(32).fill(0), + ); + }); + + it('failed to verify depositing with wrong allowlist aux', async () => { + const { groupKeyPair } = await createTestGroupMintExt( + connection, + wallet.payer, + ); + const { mint, recipientTokenAccount } = + await createTestMintAndTokenT22VanillaExt( + connection, + wallet.payer, + undefined, + groupKeyPair, + ); + + const poolData = await createPool(program, { + owner: wallet.publicKey, + cosigner, + allowlists: [ + { + kind: AllowlistKind.metadata, + value: mint, + }, + { + kind: AllowlistKind.group, + value: groupKeyPair.publicKey, + }, + ...getEmptyAllowLists(4), + ], + }); + + const poolAta = await getAssociatedTokenAddress( + mint, + poolData.poolKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + const { key: sellState } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + mint, + ); + + assert.equal(await connection.getBalance(poolAta), 0); + assert.equal(await connection.getBalance(sellState), 0); + let poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); + + try { + await program.methods + .extDepositSell({ + assetAmount: new anchor.BN(1), + allowlistAux: 'wrong-aux', + }) + .accountsStrict({ + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + pool: poolData.poolKey, + assetMint: mint, + assetTokenAccount: recipientTokenAccount, + sellsideEscrowTokenAccount: poolAta, + sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([cosigner]) + .rpc({ skipPreflight: true }); + } catch (err) { + assertProgramError(err, 'Unexpected metadata uri'); + } + }); + + it('failed to verify depositing nfts with external group member pointer', async () => { + const { groupKeyPair } = await createTestGroupMintExt( + connection, + wallet.payer, + ); + const { groupMemberKeyPair } = await createTestGroupMemberMint( + connection, + wallet.payer, + groupKeyPair, + ); + const { mint, recipientTokenAccount } = + await createTestMintAndTokenT22VanillaExt( + connection, + wallet.payer, + undefined, + groupKeyPair, + groupMemberKeyPair, + ); + + const poolData = await createPool(program, { + owner: wallet.publicKey, + cosigner, + allowlists: [ + { + kind: AllowlistKind.metadata, + value: mint, + }, + { + kind: AllowlistKind.group, + value: groupKeyPair.publicKey, + }, + ...getEmptyAllowLists(4), + ], + }); + + const poolAta = await getAssociatedTokenAddress( + mint, + poolData.poolKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + const { key: sellState } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + mint, + ); + + assert.equal(await connection.getBalance(poolAta), 0); + assert.equal(await connection.getBalance(sellState), 0); + let poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); + + try { + await program.methods + .extDepositSell({ + assetAmount: new anchor.BN(1), + allowlistAux: '', + }) + .accountsStrict({ + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + pool: poolData.poolKey, + assetMint: mint, + assetTokenAccount: recipientTokenAccount, + sellsideEscrowTokenAccount: poolAta, + sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([cosigner]) + .rpc({ skipPreflight: true }); + } catch (err) { + assertProgramError(err, 'Invalid token member extensions'); + } + }); + + it('failed to verify depositing nfts with disallowed group', async () => { + const { groupKeyPair } = await createTestGroupMintExt( + connection, + wallet.payer, + ); + const { mint, recipientTokenAccount } = + await createTestMintAndTokenT22VanillaExt( + connection, + wallet.payer, + undefined, + groupKeyPair, + ); + + const poolData = await createPool(program, { + owner: wallet.publicKey, + cosigner, + allowlists: [ + { + kind: AllowlistKind.metadata, + value: mint, + }, + { + kind: AllowlistKind.group, + value: mint, // unexpected group address + }, + ...getEmptyAllowLists(4), + ], + }); + + const poolAta = await getAssociatedTokenAddress( + mint, + poolData.poolKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + const { key: sellState } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + mint, + ); + + assert.equal(await connection.getBalance(poolAta), 0); + assert.equal(await connection.getBalance(sellState), 0); + let poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); + + try { + await program.methods + .extDepositSell({ + assetAmount: new anchor.BN(1), + allowlistAux: '', + }) + .accountsStrict({ + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + pool: poolData.poolKey, + assetMint: mint, + assetTokenAccount: recipientTokenAccount, + sellsideEscrowTokenAccount: poolAta, + sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([cosigner]) + .rpc({ skipPreflight: true }); + } catch (err) { + assertProgramError(err, 'invalid allowlists'); + } }); }); }); + +function assertProgramError(_err: unknown, message: string) { + expect(_err).to.be.instanceOf(anchor.ProgramError); + const err = _err as anchor.ProgramError; + + assert.strictEqual(err.msg, message); +} diff --git a/tests/utils/nfts.ts b/tests/utils/nfts.ts index 66fefac..f61ac65 100644 --- a/tests/utils/nfts.ts +++ b/tests/utils/nfts.ts @@ -23,8 +23,12 @@ import { createMintToInstruction, getAssociatedTokenAddressSync, getMintLen, + createInitializeGroupPointerInstruction, } from '@solana/spl-token'; -import { createInitializeMemberInstruction } from '@solana/spl-token-group'; +import { + createInitializeGroupInstruction, + createInitializeMemberInstruction, +} from '@solana/spl-token-group'; export const getMetaplexInstance = (conn: Connection) => { return Metaplex.make(conn).use(keypairIdentity(getKeypair())); @@ -132,10 +136,11 @@ export async function createTestMintAndTokenT22VanillaExt( connection: Connection, payer: Keypair, recipient?: PublicKey, + groupKeyPair?: Keypair, + groupMemberKeyPair?: Keypair, ) { const mintKeypair = Keypair.generate(); - const memberAddress = Keypair.generate().publicKey; - const effectiveGroupKeyPair = Keypair.generate(); + const effectiveGroupKeyPair = groupKeyPair ?? Keypair.generate(); const tokenProgramId = TOKEN_2022_PROGRAM_ID; const effectiveRecipient = recipient ?? payer.publicKey; const targetTokenAccount = getAssociatedTokenAddressSync( @@ -166,6 +171,8 @@ export async function createTestMintAndTokenT22VanillaExt( mintKeypair.publicKey, tokenProgramId, ); + + const memberAddress = groupMemberKeyPair?.publicKey ?? mintKeypair.publicKey; const createGroupMemberPointerIx = createInitializeGroupMemberPointerInstruction( mintKeypair.publicKey, @@ -194,7 +201,7 @@ export async function createTestMintAndTokenT22VanillaExt( const createGroupMemberIx = createInitializeMemberInstruction({ programId: tokenProgramId, - member: memberAddress, + member: mintKeypair.publicKey, memberMint: mintKeypair.publicKey, memberMintAuthority: payer.publicKey, group: effectiveGroupKeyPair.publicKey, @@ -221,11 +228,11 @@ export async function createTestMintAndTokenT22VanillaExt( const blockhashData = await connection.getLatestBlockhash(); const tx = new Transaction().add( createMintAccountIx, - createMetadataPointerIx, createGroupMemberPointerIx, + createMetadataPointerIx, createInitMintIx, createMetadataIx, - // createGroupMemberIx, + createGroupMemberIx, createAtaIx, mintToIx, ); @@ -239,3 +246,123 @@ export async function createTestMintAndTokenT22VanillaExt( recipientTokenAccount: targetTokenAccount, }; } + +export async function createTestGroupMintExt( + connection: Connection, + payer: Keypair, +) { + const tokenProgramId = TOKEN_2022_PROGRAM_ID; + const groupKeyPair = Keypair.generate(); + const mintSpace = getMintLen([ExtensionType.GroupPointer]); + const mintLamports = await connection.getMinimumBalanceForRentExemption( + mintSpace * 2, + ); + + const createMintAccountIx = SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: groupKeyPair.publicKey, + space: mintSpace, + lamports: mintLamports, + programId: tokenProgramId, + }); + const createGroupPointerIx = createInitializeGroupPointerInstruction( + groupKeyPair.publicKey, + payer.publicKey, + groupKeyPair.publicKey, + tokenProgramId, + ); + + const createInitMintIx = createInitializeMint2Instruction( + groupKeyPair.publicKey, + 0, + payer.publicKey, + payer.publicKey, + tokenProgramId, + ); + + const createGroupIx = createInitializeGroupInstruction({ + programId: tokenProgramId, + group: groupKeyPair.publicKey, + mint: groupKeyPair.publicKey, + mintAuthority: payer.publicKey, + updateAuthority: payer.publicKey, + maxSize: 10, + }); + + const blockhashData = await connection.getLatestBlockhash(); + const tx = new Transaction().add( + createMintAccountIx, + createGroupPointerIx, + createInitMintIx, + createGroupIx, + ); + tx.recentBlockhash = blockhashData.blockhash; + tx.feePayer = payer.publicKey; + tx.partialSign(payer, groupKeyPair); + await sendAndAssertTx(connection, tx, blockhashData, false); + + return { + groupKeyPair, + }; +} + +export async function createTestGroupMemberMint( + connection: Connection, + payer: Keypair, + groupAddress: Keypair, +) { + const tokenProgramId = TOKEN_2022_PROGRAM_ID; + const groupMemberKeyPair = Keypair.generate(); + const mintSpace = getMintLen([ExtensionType.GroupPointer]); + const mintLamports = await connection.getMinimumBalanceForRentExemption( + mintSpace * 2, + ); + + const createMintAccountIx = SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: groupMemberKeyPair.publicKey, + space: mintSpace, + lamports: mintLamports, + programId: tokenProgramId, + }); + const createGroupMemberPointerIx = + createInitializeGroupMemberPointerInstruction( + groupMemberKeyPair.publicKey, + payer.publicKey, + groupMemberKeyPair.publicKey, + tokenProgramId, + ); + + const createInitMintIx = createInitializeMint2Instruction( + groupMemberKeyPair.publicKey, + 0, + payer.publicKey, + payer.publicKey, + tokenProgramId, + ); + + const createGroupMemberIx = createInitializeMemberInstruction({ + programId: tokenProgramId, + member: groupMemberKeyPair.publicKey, + memberMint: groupMemberKeyPair.publicKey, + memberMintAuthority: payer.publicKey, + group: groupAddress.publicKey, + groupUpdateAuthority: payer.publicKey, + }); + + const blockhashData = await connection.getLatestBlockhash(); + const tx = new Transaction().add( + createMintAccountIx, + createGroupMemberPointerIx, + createInitMintIx, + createGroupMemberIx, + ); + tx.recentBlockhash = blockhashData.blockhash; + tx.feePayer = payer.publicKey; + tx.partialSign(payer, groupMemberKeyPair); + await sendAndAssertTx(connection, tx, blockhashData, false); + + return { + groupMemberKeyPair, + }; +} From a3bab3215c473237e2a8aaf44bc0a466ca01c4d7 Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Wed, 13 Mar 2024 19:44:42 -0700 Subject: [PATCH 11/24] add fulfill sell + buy and integration tests --- programs/mmm/src/ext_util.rs | 120 ---- .../ext_vanilla/ext_deposit_sell.rs | 3 +- .../ext_vanilla/ext_fulfill_buy.rs | 335 ++++++++++ .../ext_vanilla/ext_fulfill_sell.rs | 237 +++++++ .../mmm/src/instructions/ext_vanilla/mod.rs | 4 + .../instructions/vanilla/sol_fulfill_sell.rs | 39 +- programs/mmm/src/lib.rs | 15 +- programs/mmm/src/util.rs | 167 ++++- sdk/src/idl/mmm.ts | 342 ++++++++++ tests/mmm-ext-deposit.spec.ts | 179 ++---- tests/mmm-ext-fulfill.spec.ts | 606 ++++++++++++++++++ tests/utils/mmm.ts | 111 +++- tests/utils/nfts.ts | 16 +- 13 files changed, 1893 insertions(+), 281 deletions(-) delete mode 100644 programs/mmm/src/ext_util.rs create mode 100644 programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs create mode 100644 programs/mmm/src/instructions/ext_vanilla/ext_fulfill_sell.rs create mode 100644 tests/mmm-ext-fulfill.spec.ts diff --git a/programs/mmm/src/ext_util.rs b/programs/mmm/src/ext_util.rs deleted file mode 100644 index b0cbc2b..0000000 --- a/programs/mmm/src/ext_util.rs +++ /dev/null @@ -1,120 +0,0 @@ -use anchor_lang::prelude::*; -use solana_program::{account_info::AccountInfo, pubkey::Pubkey}; -use spl_token_2022::{ - extension::{ - group_member_pointer::GroupMemberPointer, metadata_pointer::MetadataPointer, - BaseStateWithExtensions, StateWithExtensions, - }, - state::Mint as Token22Mint, -}; -use spl_token_group_interface::state::TokenGroupMember; -use spl_token_metadata_interface::state::TokenMetadata; - -use crate::{errors::MMMErrorCode, state::*}; - -pub fn check_allowlists_for_mint_ext( - allowlists: &[Allowlist], - token_mint: &AccountInfo, - allowlist_aux: Option, -) -> Result { - if token_mint.owner != &spl_token_2022::ID || token_mint.data_is_empty() { - return Err(MMMErrorCode::InvalidTokenMint.into()); - } - let borrowed_data = token_mint.data.borrow(); - let mint_deserialized = StateWithExtensions::::unpack(&borrowed_data)?; - if !mint_deserialized.base.is_initialized { - return Err(MMMErrorCode::InvalidTokenMint.into()); - } - - // verify metadata extension - if let Ok(metadata_ptr) = mint_deserialized.get_extension::() { - if Option::::from(metadata_ptr.metadata_address) != Some(*token_mint.key) { - return Err(MMMErrorCode::InValidTokenMetadataExtension.into()); - } - } - let parsed_metadata = mint_deserialized - .get_variable_len_extension::() - .unwrap(); - - if allowlists - .iter() - .any(|&val| val.kind == ALLOWLIST_KIND_METADATA) - { - // If allowlist_aux is not passed in, do not validate URI. - if let Some(ref aux_key) = allowlist_aux { - // Handle URI padding. - if !parsed_metadata.uri.trim().starts_with(aux_key) { - msg!( - "Failed metadata validation. Expected URI: |{}| but got |{}|", - *aux_key, - parsed_metadata.uri - ); - return Err(MMMErrorCode::UnexpectedMetadataUri.into()); - } - } - } - - // verify group member extension - if let Ok(group_member_ptr) = mint_deserialized.get_extension::() { - if Some(*token_mint.key) != Option::::from(group_member_ptr.member_address) { - msg!("group member pointer does not point to itself"); - return Err(MMMErrorCode::InValidTokenMemberExtension.into()); - } - } - - for allowlist_val in allowlists.iter() { - match allowlist_val.kind { - ALLOWLIST_KIND_EMPTY => {} - ALLOWLIST_KIND_ANY => { - // any is a special case, we don't need to check anything else - return Ok(parsed_metadata); - } - ALLOWLIST_KIND_FVCA => { - return Err(MMMErrorCode::InvalidAllowLists.into()); - } - ALLOWLIST_KIND_MINT => { - if token_mint.key() == allowlist_val.value { - return Ok(parsed_metadata); - } - } - ALLOWLIST_KIND_MCC => { - return Err(MMMErrorCode::InvalidAllowLists.into()); - } - ALLOWLIST_KIND_GROUP => { - let group_address = assert_valid_group(&mint_deserialized, token_mint)?; - if group_address != Some(allowlist_val.value) { - msg!("group address |{}| is not allowed", group_address.unwrap()); - return Err(MMMErrorCode::InvalidAllowLists.into()); - } - return Ok(parsed_metadata); - } - ALLOWLIST_KIND_METADATA => { - // Do not validate URI here, as we already did it above. - // Group is validated in a separate function. - // These checks are separate since allowlist values are unioned together. - continue; - } - _ => { - return Err(MMMErrorCode::InvalidAllowLists.into()); - } - } - } - - // at the end, we didn't find a match, thus return err - Err(MMMErrorCode::InvalidAllowLists.into()) -} - -pub fn assert_valid_group( - mint_deserialized: &StateWithExtensions<'_, Token22Mint>, - token_mint: &AccountInfo, -) -> Result> { - if let Ok(group_member) = mint_deserialized.get_extension::() { - // counter spoof check - if Some(group_member.mint) != Some(*token_mint.key) { - msg!("group member mint does not match the token mint"); - return Err(MMMErrorCode::InValidTokenMemberExtension.into()); - } - return Ok(Some(group_member.group)); - } - Err(MMMErrorCode::InValidTokenMemberExtension.into()) -} diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs b/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs index d66ebf9..7dbcb59 100644 --- a/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs +++ b/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs @@ -9,9 +9,8 @@ use spl_token_2022::onchain::invoke_transfer_checked; use crate::{ constants::*, errors::MMMErrorCode, - ext_util::check_allowlists_for_mint_ext, state::{Pool, SellState}, - util::log_pool, + util::{check_allowlists_for_mint_ext, log_pool}, DepositSellArgs, }; diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs b/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs new file mode 100644 index 0000000..dca0046 --- /dev/null +++ b/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs @@ -0,0 +1,335 @@ +use anchor_lang::{prelude::*, AnchorDeserialize}; +use anchor_spl::{ + associated_token::AssociatedToken, + token_2022::close_account, + token_interface::{Mint, TokenAccount, TokenInterface}, +}; +use solana_program::{program::invoke_signed, system_instruction}; +use spl_token_2022::onchain::invoke_transfer_checked; +use std::convert::TryFrom; + +use crate::{ + ata::init_if_needed_ata, + constants::*, + errors::MMMErrorCode, + index_ra, + instructions::{log_pool, try_close_pool}, + state::{Pool, SellState}, + util::{ + assert_valid_fees_bp, check_allowlists_for_mint_ext, get_buyside_seller_receives, + get_lp_fee_bp, get_sol_fee, get_sol_lp_fee, get_sol_total_price_and_next_price, + try_close_escrow, try_close_sell_state, + }, + SolFulfillBuyArgs, +}; + +// ExtSolFulfillBuy means a seller wants to sell NFT into the pool +// where the pool has some buyside payment liquidity. Therefore, +// the seller expects a min_payment_amount that goes back to the +// seller's wallet for the asset_amount that the seller wants to sell. +// This is mainly used for Token22 extension +#[derive(Accounts)] +#[instruction(args:SolFulfillBuyArgs)] +pub struct ExtSolFulfillBuy<'info> { + #[account(mut)] + pub payer: Signer<'info>, + /// CHECK: we will check the owner field that matches the pool owner + #[account(mut)] + pub owner: UncheckedAccount<'info>, + pub cosigner: Signer<'info>, + #[account(mut)] + /// CHECK: we will check that the referral matches the pool's referral + pub referral: UncheckedAccount<'info>, + #[account( + mut, + seeds = [POOL_PREFIX.as_bytes(), owner.key().as_ref(), pool.uuid.as_ref()], + has_one = owner @ MMMErrorCode::InvalidOwner, + has_one = referral @ MMMErrorCode::InvalidReferral, + has_one = cosigner @ MMMErrorCode::InvalidCosigner, + constraint = pool.payment_mint.eq(&Pubkey::default()) @ MMMErrorCode::InvalidPaymentMint, + constraint = pool.expiry == 0 || pool.expiry > Clock::get().unwrap().unix_timestamp @ MMMErrorCode::Expired, + bump + )] + pub pool: Box>, + /// CHECK: it's a pda, and the private key is owned by the seeds + #[account( + mut, + seeds = [BUYSIDE_SOL_ESCROW_ACCOUNT_PREFIX.as_bytes(), pool.key().as_ref()], + bump, + )] + pub buyside_sol_escrow_account: UncheckedAccount<'info>, + /// CHECK: check_allowlists_for_mint_ext + pub asset_mint: InterfaceAccount<'info, Mint>, + #[account( + mut, + token::mint = asset_mint, + token::authority = payer, + token::token_program = token_program, + )] + pub payer_asset_account: Box>, + /// CHECK: check in init_if_needed_ata + #[account(mut)] + pub sellside_escrow_token_account: UncheckedAccount<'info>, + /// CHECK: check in init_if_needed_ata + #[account(mut)] + pub owner_token_account: UncheckedAccount<'info>, + /// CHECK: will be used for allowlist checks + pub allowlist_aux_account: UncheckedAccount<'info>, + #[account( + init_if_needed, + payer = payer, + seeds = [ + SELL_STATE_PREFIX.as_bytes(), + pool.key().as_ref(), + asset_mint.key().as_ref(), + ], + space = SellState::LEN, + bump + )] + pub sell_state: Account<'info, SellState>, + pub system_program: Program<'info, System>, + pub token_program: Interface<'info, TokenInterface>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub rent: Sysvar<'info, Rent>, + // Remaining accounts + // Branch: using shared escrow accounts + // 0: m2_program + // 1: shared_escrow_account + // 2+: creator accounts + // Branch: not using shared escrow accounts + // 0+: creator accounts +} + +pub fn handler<'info>( + ctx: Context<'_, '_, '_, 'info, ExtSolFulfillBuy<'info>>, + args: SolFulfillBuyArgs, +) -> Result<()> { + let token_program = &ctx.accounts.token_program; + let system_program = &ctx.accounts.system_program; + let associated_token_program = &ctx.accounts.associated_token_program; + let rent = &ctx.accounts.rent; + let pool = &mut ctx.accounts.pool; + let sell_state = &mut ctx.accounts.sell_state; + let owner = &ctx.accounts.owner; + let referral = &ctx.accounts.referral; + let payer = &ctx.accounts.payer; + let payer_asset_account = &ctx.accounts.payer_asset_account; + let asset_mint = &ctx.accounts.asset_mint; + let buyside_sol_escrow_account = &ctx.accounts.buyside_sol_escrow_account; + let pool_key = pool.key(); + let buyside_sol_escrow_account_seeds: &[&[&[u8]]] = &[&[ + BUYSIDE_SOL_ESCROW_ACCOUNT_PREFIX.as_bytes(), + pool_key.as_ref(), + &[ctx.bumps.buyside_sol_escrow_account], + ]]; + let remaining_accounts = ctx.remaining_accounts; + + check_allowlists_for_mint_ext( + &pool.allowlists, + &asset_mint.to_account_info(), + args.allowlist_aux, + )?; + + let (total_price, next_price) = + get_sol_total_price_and_next_price(pool, args.asset_amount, true)?; + let seller_receives = { + let lp_fee_bp = get_lp_fee_bp(pool, buyside_sol_escrow_account.lamports()); + get_buyside_seller_receives( + total_price, + lp_fee_bp, + 0, // metadata_royalty_bp + pool.buyside_creator_royalty_bp, + ) + }?; + let lp_fee = get_sol_lp_fee(pool, buyside_sol_escrow_account.lamports(), seller_receives)?; + + assert_valid_fees_bp(args.maker_fee_bp, args.taker_fee_bp)?; + let maker_fee = get_sol_fee(seller_receives, args.maker_fee_bp)?; + let taker_fee = get_sol_fee(seller_receives, args.taker_fee_bp)?; + let referral_fee = u64::try_from( + maker_fee + .checked_add(taker_fee) + .ok_or(MMMErrorCode::NumericOverflow)?, + ) + .map_err(|_| MMMErrorCode::NumericOverflow)?; + + if pool.reinvest_fulfill_buy { + if pool.using_shared_escrow() { + return Err(MMMErrorCode::InvalidAccountState.into()); + } + let sellside_escrow_token_account = + ctx.accounts.sellside_escrow_token_account.to_account_info(); + init_if_needed_ata( + sellside_escrow_token_account.to_account_info(), + payer.to_account_info(), + pool.to_account_info(), + asset_mint.to_account_info(), + associated_token_program.to_account_info(), + token_program.to_account_info(), + system_program.to_account_info(), + rent.to_account_info(), + )?; + let sellside_escrow_token_account = + ctx.accounts.sellside_escrow_token_account.to_account_info(); + invoke_transfer_checked( + token_program.key, + payer_asset_account.to_account_info(), + asset_mint.to_account_info(), + sellside_escrow_token_account.to_account_info(), + payer.to_account_info(), + remaining_accounts, + args.asset_amount, + 0, // decimals + &[], // seeds + )?; + + pool.sellside_asset_amount = pool + .sellside_asset_amount + .checked_add(args.asset_amount) + .ok_or(MMMErrorCode::NumericOverflow)?; + sell_state.pool = pool.key(); + sell_state.pool_owner = owner.key(); + sell_state.asset_mint = asset_mint.key(); + sell_state.cosigner_annotation = pool.cosigner_annotation; + sell_state.asset_amount = sell_state + .asset_amount + .checked_add(args.asset_amount) + .ok_or(MMMErrorCode::NumericOverflow)?; + } else { + let owner_token_account = ctx.accounts.owner_token_account.to_account_info(); + init_if_needed_ata( + owner_token_account.to_account_info(), + payer.to_account_info(), + owner.to_account_info(), + asset_mint.to_account_info(), + associated_token_program.to_account_info(), + token_program.to_account_info(), + system_program.to_account_info(), + rent.to_account_info(), + )?; + + invoke_transfer_checked( + token_program.key, + payer_asset_account.to_account_info(), + asset_mint.to_account_info(), + owner_token_account.to_account_info(), + payer.to_account_info(), + remaining_accounts, + args.asset_amount, + 0, // decimals + &[], // seeds + )?; + } + + // we can close the payer_asset_account if no amount left + if payer_asset_account.amount == args.asset_amount { + close_account(CpiContext::new( + token_program.to_account_info(), + anchor_spl::token_2022::CloseAccount { + account: payer_asset_account.to_account_info(), + destination: payer.to_account_info(), + authority: payer.to_account_info(), + }, + ))?; + } + + // prevent frontrun by pool config changes + let payment_amount = total_price + .checked_sub(lp_fee) + .ok_or(MMMErrorCode::NumericOverflow)? + .checked_sub(taker_fee as u64) + .ok_or(MMMErrorCode::NumericOverflow)?; + + if payment_amount < args.min_payment_amount { + return Err(MMMErrorCode::InvalidRequestedPrice.into()); + } + + invoke_signed( + &system_instruction::transfer(buyside_sol_escrow_account.key, payer.key, payment_amount), + &[ + buyside_sol_escrow_account.to_account_info(), + payer.to_account_info(), + system_program.to_account_info(), + ], + buyside_sol_escrow_account_seeds, + )?; + + if lp_fee > 0 { + invoke_signed( + &system_instruction::transfer(buyside_sol_escrow_account.key, owner.key, lp_fee), + &[ + buyside_sol_escrow_account.to_account_info(), + owner.to_account_info(), + system_program.to_account_info(), + ], + buyside_sol_escrow_account_seeds, + )?; + } + if referral_fee > 0 { + invoke_signed( + &system_instruction::transfer( + buyside_sol_escrow_account.key, + referral.key, + referral_fee, + ), + &[ + buyside_sol_escrow_account.to_account_info(), + referral.to_account_info(), + system_program.to_account_info(), + ], + buyside_sol_escrow_account_seeds, + )?; + } + + pool.lp_fee_earned = pool + .lp_fee_earned + .checked_add(lp_fee) + .ok_or(MMMErrorCode::NumericOverflow)?; + pool.spot_price = next_price; + + try_close_escrow( + &buyside_sol_escrow_account.to_account_info(), + pool, + system_program, + buyside_sol_escrow_account_seeds, + )?; + try_close_sell_state(sell_state, payer.to_account_info())?; + + // return the remaining per pool escrow balance to the shared escrow account + if pool.using_shared_escrow() { + let min_rent = Rent::get()?.minimum_balance(0); + let shared_escrow_account = index_ra!(remaining_accounts, 1).to_account_info(); + if shared_escrow_account.lamports() + buyside_sol_escrow_account.lamports() > min_rent + && buyside_sol_escrow_account.lamports() > 0 + { + anchor_lang::solana_program::program::invoke_signed( + &anchor_lang::solana_program::system_instruction::transfer( + buyside_sol_escrow_account.key, + shared_escrow_account.key, + buyside_sol_escrow_account.lamports(), + ), + &[ + buyside_sol_escrow_account.to_account_info(), + shared_escrow_account, + system_program.to_account_info(), + ], + buyside_sol_escrow_account_seeds, + )?; + } else { + try_close_escrow( + buyside_sol_escrow_account, + pool, + system_program, + buyside_sol_escrow_account_seeds, + )?; + } + } + pool.buyside_payment_amount = buyside_sol_escrow_account.lamports(); + + log_pool("ext_post_sol_fulfill_buy", pool)?; + try_close_pool(pool, owner.to_account_info())?; + + msg!("{{\"lp_fee\":{},\"total_price\":{}}}", lp_fee, total_price,); + + Ok(()) +} diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_sell.rs b/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_sell.rs new file mode 100644 index 0000000..bb1bc97 --- /dev/null +++ b/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_sell.rs @@ -0,0 +1,237 @@ +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token_2022::close_account, + token_interface::{Mint, TokenAccount, TokenInterface}, +}; +use solana_program::{program::invoke, system_instruction}; +use spl_token_2022::onchain::invoke_transfer_checked; +use std::convert::TryFrom; + +use crate::{ + constants::*, + errors::MMMErrorCode, + instructions::{ + get_pool_price_info, log_pool, try_close_pool, try_close_sell_state, PoolPriceInfo, + }, + state::{Pool, SellState}, + util::check_allowlists_for_mint_ext, + SolFulfillSellArgs, +}; + +// ExtSolFulfillSell means a buyer wants to buy NFTs from the pool +// where the pool has some sellside asset liquidity. Therefore, +// the buyer expects to pay a max_payment_amount for the asset_amount +// that the buyer wants to buy. +// This is mainly used for Token22 extension +#[derive(Accounts)] +#[instruction(args:SolFulfillSellArgs)] +pub struct ExtSolFulfillSell<'info> { + #[account(mut)] + pub payer: Signer<'info>, + /// CHECK: we will check the owner field that matches the pool owner + #[account(mut)] + pub owner: UncheckedAccount<'info>, + pub cosigner: Signer<'info>, + /// CHECK: we will check that the referral matches the pool's referral + #[account(mut)] + pub referral: UncheckedAccount<'info>, + #[account( + mut, + seeds = [POOL_PREFIX.as_bytes(), owner.key().as_ref(), pool.uuid.as_ref()], + has_one = owner @ MMMErrorCode::InvalidOwner, + has_one = referral @ MMMErrorCode::InvalidReferral, + has_one = cosigner @ MMMErrorCode::InvalidCosigner, + constraint = pool.payment_mint.eq(&Pubkey::default()) @ MMMErrorCode::InvalidPaymentMint, + constraint = pool.expiry == 0 || pool.expiry > Clock::get().unwrap().unix_timestamp @ MMMErrorCode::Expired, + constraint = args.buyside_creator_royalty_bp <= 10000 @ MMMErrorCode::InvalidBP, + bump + )] + pub pool: Box>, + /// CHECK: it's a pda, and the private key is owned by the seeds + #[account( + mut, + seeds = [BUYSIDE_SOL_ESCROW_ACCOUNT_PREFIX.as_bytes(), pool.key().as_ref()], + bump, + )] + pub buyside_sol_escrow_account: AccountInfo<'info>, + /// CHECK: check_allowlists_for_mint_ext + pub asset_mint: InterfaceAccount<'info, Mint>, + #[account( + mut, + associated_token::mint = asset_mint, + associated_token::authority = pool, + associated_token::token_program = token_program, + )] + pub sellside_escrow_token_account: Box>, + #[account( + init_if_needed, + payer = payer, + associated_token::mint = asset_mint, + associated_token::authority = payer, + associated_token::token_program = token_program, + )] + pub payer_asset_account: Box>, + #[account( + mut, + seeds = [ + SELL_STATE_PREFIX.as_bytes(), + pool.key().as_ref(), + asset_mint.key().as_ref(), + ], + bump + )] + pub sell_state: Account<'info, SellState>, + pub system_program: Program<'info, System>, + pub token_program: Interface<'info, TokenInterface>, + pub associated_token_program: Program<'info, AssociatedToken>, +} + +pub fn handler<'info>( + ctx: Context<'_, '_, '_, 'info, ExtSolFulfillSell<'info>>, + args: SolFulfillSellArgs, +) -> Result<()> { + let token_program = &ctx.accounts.token_program; + let system_program = &ctx.accounts.system_program; + let owner = &ctx.accounts.owner; + let referral = &ctx.accounts.referral; + let pool = &mut ctx.accounts.pool; + let sell_state = &mut ctx.accounts.sell_state; + + let payer = &ctx.accounts.payer; + let payer_asset_account = &ctx.accounts.payer_asset_account; + let asset_mint = &ctx.accounts.asset_mint; + + let sellside_escrow_token_account = &ctx.accounts.sellside_escrow_token_account; + let buyside_sol_escrow_account = &ctx.accounts.buyside_sol_escrow_account; + let pool_seeds: &[&[&[u8]]] = &[&[ + POOL_PREFIX.as_bytes(), + pool.owner.as_ref(), + pool.uuid.as_ref(), + &[ctx.bumps.pool], + ]]; + + check_allowlists_for_mint_ext( + &pool.allowlists, + &asset_mint.to_account_info(), + args.allowlist_aux, + )?; + + let PoolPriceInfo { + total_price, + next_price, + lp_fee, + maker_fee, + taker_fee, + referral_fee, + transfer_sol_to, + } = get_pool_price_info( + pool, + owner, + buyside_sol_escrow_account, + args.asset_amount, + args.maker_fee_bp, + args.taker_fee_bp, + )?; + + // TODO: make sure that the lp fee is paid with the correct amount + invoke( + &system_instruction::transfer( + payer.key, + transfer_sol_to.key, + u64::try_from( + i64::try_from(total_price) + .map_err(|_| MMMErrorCode::NumericOverflow)? + .checked_sub(maker_fee) + .ok_or(MMMErrorCode::NumericOverflow)?, + ) + .map_err(|_| MMMErrorCode::NumericOverflow)?, + ), + &[ + payer.to_account_info(), + transfer_sol_to, + system_program.to_account_info(), + ], + )?; + + invoke_transfer_checked( + token_program.key, + sellside_escrow_token_account.to_account_info(), + asset_mint.to_account_info(), + payer_asset_account.to_account_info(), + pool.to_account_info(), + &[], + args.asset_amount, + 0, + pool_seeds, + )?; + + // we can close the sellside_escrow_token_account if no amount left + if sellside_escrow_token_account.amount == args.asset_amount { + close_account(CpiContext::new_with_signer( + token_program.to_account_info(), + anchor_spl::token_2022::CloseAccount { + account: sellside_escrow_token_account.to_account_info(), + destination: owner.to_account_info(), + authority: pool.to_account_info(), + }, + pool_seeds, + ))?; + } + + if lp_fee > 0 { + invoke( + &system_instruction::transfer(payer.key, owner.key, lp_fee), + &[ + payer.to_account_info(), + owner.to_account_info(), + system_program.to_account_info(), + ], + )?; + } + + if referral_fee > 0 { + invoke( + &system_instruction::transfer(payer.key, referral.key, referral_fee), + &[ + payer.to_account_info(), + referral.to_account_info(), + system_program.to_account_info(), + ], + )?; + } + + pool.spot_price = next_price; + pool.sellside_asset_amount = pool + .sellside_asset_amount + .checked_sub(args.asset_amount) + .ok_or(MMMErrorCode::NumericOverflow)?; + pool.lp_fee_earned = pool + .lp_fee_earned + .checked_add(lp_fee) + .ok_or(MMMErrorCode::NumericOverflow)?; + + // prevent frontrun by pool config changes + let payment_amount = total_price + .checked_add(lp_fee) + .ok_or(MMMErrorCode::NumericOverflow)? + .checked_add(taker_fee as u64) + .ok_or(MMMErrorCode::NumericOverflow)?; + if payment_amount > args.max_payment_amount { + return Err(MMMErrorCode::InvalidRequestedPrice.into()); + } + + sell_state.asset_amount = sell_state + .asset_amount + .checked_sub(args.asset_amount) + .ok_or(MMMErrorCode::NumericOverflow)?; + try_close_sell_state(sell_state, owner.to_account_info())?; + + pool.buyside_payment_amount = buyside_sol_escrow_account.lamports(); + log_pool("ext_post_sol_fulfill_sell", pool)?; + try_close_pool(pool, owner.to_account_info())?; + + msg!("{{\"lp_fee\":{},\"total_price\":{}}}", lp_fee, total_price); + + Ok(()) +} diff --git a/programs/mmm/src/instructions/ext_vanilla/mod.rs b/programs/mmm/src/instructions/ext_vanilla/mod.rs index d7e1bda..e274456 100644 --- a/programs/mmm/src/instructions/ext_vanilla/mod.rs +++ b/programs/mmm/src/instructions/ext_vanilla/mod.rs @@ -1,3 +1,7 @@ pub mod ext_deposit_sell; +pub mod ext_fulfill_buy; +pub mod ext_fulfill_sell; +pub use ext_fulfill_buy::*; pub use ext_deposit_sell::*; +pub use ext_fulfill_sell::*; diff --git a/programs/mmm/src/instructions/vanilla/sol_fulfill_sell.rs b/programs/mmm/src/instructions/vanilla/sol_fulfill_sell.rs index c1c08e0..3be2d12 100644 --- a/programs/mmm/src/instructions/vanilla/sol_fulfill_sell.rs +++ b/programs/mmm/src/instructions/vanilla/sol_fulfill_sell.rs @@ -8,10 +8,10 @@ use std::convert::TryFrom; use crate::{ constants::*, errors::MMMErrorCode, + instructions::{get_pool_price_info, PoolPriceInfo}, state::{Pool, SellState}, util::{ - assert_valid_fees_bp, check_allowlists_for_mint, get_metadata_royalty_bp, get_sol_fee, - get_sol_lp_fee, get_sol_total_price_and_next_price, log_pool, pay_creator_fees_in_sol, + check_allowlists_for_mint, get_metadata_royalty_bp, log_pool, pay_creator_fees_in_sol, try_close_pool, try_close_sell_state, }, }; @@ -143,25 +143,22 @@ pub fn handler<'info>( args.allowlist_aux, )?; - let (total_price, next_price) = - get_sol_total_price_and_next_price(pool, args.asset_amount, false)?; - let lp_fee = get_sol_lp_fee(pool, buyside_sol_escrow_account.lamports(), total_price)?; - - assert_valid_fees_bp(args.maker_fee_bp, args.taker_fee_bp)?; - let maker_fee = get_sol_fee(total_price, args.maker_fee_bp)?; - let taker_fee = get_sol_fee(total_price, args.taker_fee_bp)?; - let referral_fee = u64::try_from( - maker_fee - .checked_add(taker_fee) - .ok_or(MMMErrorCode::NumericOverflow)?, - ) - .map_err(|_| MMMErrorCode::NumericOverflow)?; - - let transfer_sol_to = if pool.reinvest_fulfill_sell { - buyside_sol_escrow_account.to_account_info() - } else { - owner.to_account_info() - }; + let PoolPriceInfo { + total_price, + next_price, + lp_fee, + maker_fee, + taker_fee, + referral_fee, + transfer_sol_to, + } = get_pool_price_info( + pool, + owner, + buyside_sol_escrow_account, + args.asset_amount, + args.maker_fee_bp, + args.taker_fee_bp, + )?; // TODO: make sure that the lp fee is paid with the correct amount anchor_lang::solana_program::program::invoke( diff --git a/programs/mmm/src/lib.rs b/programs/mmm/src/lib.rs index 8b8e34b..f1443cd 100644 --- a/programs/mmm/src/lib.rs +++ b/programs/mmm/src/lib.rs @@ -8,7 +8,6 @@ declare_id!("mmm3XBJg5gk8XJxEKBvdgptZz6SgK4tXvn36sodowMc"); mod ata; mod constants; mod errors; -mod ext_util; pub mod instructions; pub mod state; pub mod util; @@ -132,4 +131,18 @@ pub mod mmm { ) -> Result<()> { instructions::ext_deposit_sell::handler(ctx, args) } + + pub fn ext_fulfill_sell<'info>( + ctx: Context<'_, '_, '_, 'info, ExtSolFulfillSell<'info>>, + args: SolFulfillSellArgs, + ) -> Result<()> { + instructions::ext_fulfill_sell::handler(ctx, args) + } + + pub fn ext_fulfill_buy<'info>( + ctx: Context<'_, '_, '_, 'info, ExtSolFulfillBuy<'info>>, + args: SolFulfillBuyArgs, + ) -> Result<()> { + instructions::ext_fulfill_buy::handler(ctx, args) + } } diff --git a/programs/mmm/src/util.rs b/programs/mmm/src/util.rs index 6884288..2471e50 100644 --- a/programs/mmm/src/util.rs +++ b/programs/mmm/src/util.rs @@ -1,12 +1,11 @@ use crate::{ constants::{ - BUYSIDE_SOL_ESCROW_ACCOUNT_PREFIX, M2_AUCTION_HOUSE, M2_PREFIX, M2_PROGRAM, + M2_AUCTION_HOUSE, M2_PREFIX, M2_PROGRAM, MAX_METADATA_CREATOR_ROYALTY_BP, MAX_REFERRAL_FEE_BP, MAX_TOTAL_PRICE, MIN_SOL_ESCROW_BALANCE_BP, POOL_PREFIX, }, errors::MMMErrorCode, state::*, - ID, }; use anchor_lang::{prelude::*, solana_program::log::sol_log_data}; use anchor_spl::token_interface::Mint; @@ -19,6 +18,15 @@ use mpl_token_metadata::{ }; use open_creator_protocol::state::Policy; use solana_program::program::invoke_signed; +use spl_token_2022::{ + extension::{ + group_member_pointer::GroupMemberPointer, metadata_pointer::MetadataPointer, + BaseStateWithExtensions, StateWithExtensions, + }, + state::Mint as Token22Mint, +}; +use spl_token_metadata_interface::state::TokenMetadata; +use spl_token_group_interface::state::TokenGroupMember; use std::convert::TryFrom; #[macro_export] @@ -649,3 +657,158 @@ pub fn check_remaining_accounts_for_m2( Ok(()) } + +pub fn check_allowlists_for_mint_ext( + allowlists: &[Allowlist], + token_mint: &AccountInfo, + allowlist_aux: Option, +) -> Result { + if token_mint.owner != &spl_token_2022::ID || token_mint.data_is_empty() { + return Err(MMMErrorCode::InvalidTokenMint.into()); + } + let borrowed_data = token_mint.data.borrow(); + let mint_deserialized = StateWithExtensions::::unpack(&borrowed_data)?; + if !mint_deserialized.base.is_initialized { + return Err(MMMErrorCode::InvalidTokenMint.into()); + } + + // verify metadata extension + if let Ok(metadata_ptr) = mint_deserialized.get_extension::() { + if Option::::from(metadata_ptr.metadata_address) != Some(*token_mint.key) { + return Err(MMMErrorCode::InValidTokenMetadataExtension.into()); + } + } + let parsed_metadata = mint_deserialized + .get_variable_len_extension::() + .unwrap(); + + if allowlists + .iter() + .any(|&val| val.kind == ALLOWLIST_KIND_METADATA) + { + // If allowlist_aux is not passed in, do not validate URI. + if let Some(ref aux_key) = allowlist_aux { + // Handle URI padding. + if !parsed_metadata.uri.trim().starts_with(aux_key) { + msg!( + "Failed metadata validation. Expected URI: |{}| but got |{}|", + *aux_key, + parsed_metadata.uri + ); + return Err(MMMErrorCode::UnexpectedMetadataUri.into()); + } + } + } + + // verify group member extension + if let Ok(group_member_ptr) = mint_deserialized.get_extension::() { + if Some(*token_mint.key) != Option::::from(group_member_ptr.member_address) { + msg!("group member pointer does not point to itself"); + return Err(MMMErrorCode::InValidTokenMemberExtension.into()); + } + } + + for allowlist_val in allowlists.iter() { + match allowlist_val.kind { + ALLOWLIST_KIND_EMPTY => {} + ALLOWLIST_KIND_ANY => { + // any is a special case, we don't need to check anything else + return Ok(parsed_metadata); + } + ALLOWLIST_KIND_FVCA => { + return Err(MMMErrorCode::InvalidAllowLists.into()); + } + ALLOWLIST_KIND_MINT => { + if token_mint.key() == allowlist_val.value { + return Ok(parsed_metadata); + } + } + ALLOWLIST_KIND_MCC => { + return Err(MMMErrorCode::InvalidAllowLists.into()); + } + ALLOWLIST_KIND_GROUP => { + let group_address = assert_valid_group(&mint_deserialized, token_mint)?; + if group_address != Some(allowlist_val.value) { + msg!("group address |{}| is not allowed", group_address.unwrap()); + return Err(MMMErrorCode::InvalidAllowLists.into()); + } + return Ok(parsed_metadata); + } + ALLOWLIST_KIND_METADATA => { + // Do not validate URI here, as we already did it above. + // Group is validated in a separate function. + // These checks are separate since allowlist values are unioned together. + continue; + } + _ => { + return Err(MMMErrorCode::InvalidAllowLists.into()); + } + } + } + + // at the end, we didn't find a match, thus return err + Err(MMMErrorCode::InvalidAllowLists.into()) +} + +pub fn assert_valid_group( + mint_deserialized: &StateWithExtensions<'_, Token22Mint>, + token_mint: &AccountInfo, +) -> Result> { + if let Ok(group_member) = mint_deserialized.get_extension::() { + // counter spoof check + if Some(group_member.mint) != Some(*token_mint.key) { + msg!("group member mint does not match the token mint"); + return Err(MMMErrorCode::InValidTokenMemberExtension.into()); + } + return Ok(Some(group_member.group)); + } + Err(MMMErrorCode::InValidTokenMemberExtension.into()) +} + +pub struct PoolPriceInfo<'info> { + pub total_price: u64, + pub next_price: u64, + pub lp_fee: u64, + pub maker_fee: i64, + pub taker_fee: i64, + pub referral_fee: u64, + pub transfer_sol_to: AccountInfo<'info>, +} + +pub fn get_pool_price_info<'info>( + pool: &Pool, + owner: &UncheckedAccount<'info>, + buyside_sol_escrow_account: &AccountInfo<'info>, + asset_amount: u64, + maker_fee_bp: i16, + taker_fee_bp: i16, +) -> Result> { + let (total_price, next_price) = get_sol_total_price_and_next_price(pool, asset_amount, false)?; + let lp_fee = get_sol_lp_fee(pool, buyside_sol_escrow_account.lamports(), total_price)?; + + assert_valid_fees_bp(maker_fee_bp, taker_fee_bp)?; + let maker_fee = get_sol_fee(total_price, maker_fee_bp)?; + let taker_fee = get_sol_fee(total_price, taker_fee_bp)?; + let referral_fee = u64::try_from( + maker_fee + .checked_add(taker_fee) + .ok_or(MMMErrorCode::NumericOverflow)?, + ) + .map_err(|_| MMMErrorCode::NumericOverflow)?; + + let transfer_sol_to = if pool.reinvest_fulfill_sell { + buyside_sol_escrow_account.to_account_info() + } else { + owner.to_account_info() + }; + + Ok(PoolPriceInfo { + total_price, + next_price, + lp_fee, + maker_fee, + taker_fee, + referral_fee, + transfer_sol_to, + }) +} diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index 580d7f7..bee426e 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -1664,6 +1664,177 @@ export type Mmm = { } } ] + }, + { + "name": "extFulfillSell", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "owner", + "isMut": true, + "isSigner": false + }, + { + "name": "cosigner", + "isMut": false, + "isSigner": true + }, + { + "name": "referral", + "isMut": true, + "isSigner": false + }, + { + "name": "pool", + "isMut": true, + "isSigner": false + }, + { + "name": "buysideSolEscrowAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "assetMint", + "isMut": false, + "isSigner": false + }, + { + "name": "sellsideEscrowTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "payerAssetAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "sellState", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SolFulfillSellArgs" + } + } + ] + }, + { + "name": "extFulfillBuy", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "owner", + "isMut": true, + "isSigner": false + }, + { + "name": "cosigner", + "isMut": false, + "isSigner": true + }, + { + "name": "referral", + "isMut": true, + "isSigner": false + }, + { + "name": "pool", + "isMut": true, + "isSigner": false + }, + { + "name": "buysideSolEscrowAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "assetMint", + "isMut": false, + "isSigner": false + }, + { + "name": "payerAssetAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "sellsideEscrowTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "ownerTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "allowlistAuxAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "sellState", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SolFulfillBuyArgs" + } + } + ] } ], "accounts": [ @@ -3993,6 +4164,177 @@ export const IDL: Mmm = { } } ] + }, + { + "name": "extFulfillSell", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "owner", + "isMut": true, + "isSigner": false + }, + { + "name": "cosigner", + "isMut": false, + "isSigner": true + }, + { + "name": "referral", + "isMut": true, + "isSigner": false + }, + { + "name": "pool", + "isMut": true, + "isSigner": false + }, + { + "name": "buysideSolEscrowAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "assetMint", + "isMut": false, + "isSigner": false + }, + { + "name": "sellsideEscrowTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "payerAssetAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "sellState", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SolFulfillSellArgs" + } + } + ] + }, + { + "name": "extFulfillBuy", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "owner", + "isMut": true, + "isSigner": false + }, + { + "name": "cosigner", + "isMut": false, + "isSigner": true + }, + { + "name": "referral", + "isMut": true, + "isSigner": false + }, + { + "name": "pool", + "isMut": true, + "isSigner": false + }, + { + "name": "buysideSolEscrowAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "assetMint", + "isMut": false, + "isSigner": false + }, + { + "name": "payerAssetAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "sellsideEscrowTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "ownerTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "allowlistAuxAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "sellState", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SolFulfillBuyArgs" + } + } + ] } ], "accounts": [ diff --git a/tests/mmm-ext-deposit.spec.ts b/tests/mmm-ext-deposit.spec.ts index ddfe33b..6f42f69 100644 --- a/tests/mmm-ext-deposit.spec.ts +++ b/tests/mmm-ext-deposit.spec.ts @@ -16,6 +16,7 @@ import { import { airdrop, createPool, + createPoolWithExampleExtDeposits, createTestGroupMemberMint, createTestGroupMintExt, createTestMintAndTokenT22VanillaExt, @@ -42,52 +43,24 @@ describe('mmm-ext-deposit', () => { describe('ext_deposit_sell', () => { it('correctly verifies depositing nfts with group allowlist', async () => { - const { groupKeyPair } = await createTestGroupMintExt( + const { + mint, + recipientTokenAccount, + poolData, + poolAta, + sellState, + groupAddress, + } = await createPoolWithExampleExtDeposits( + program, connection, wallet.payer, - ); - const { mint, recipientTokenAccount } = - await createTestMintAndTokenT22VanillaExt( - connection, - wallet.payer, - undefined, - groupKeyPair, - ); - - const poolData = await createPool(program, { - owner: wallet.publicKey, - cosigner, - allowlists: [ - { - kind: AllowlistKind.metadata, - value: mint, - }, - { - kind: AllowlistKind.group, - value: groupKeyPair.publicKey, - }, - ...getEmptyAllowLists(4), - ], - }); - - const poolAta = await getAssociatedTokenAddress( - mint, - poolData.poolKey, - true, - TOKEN_2022_PROGRAM_ID, - ); - - const { key: sellState } = getMMMSellStatePDA( - program.programId, - poolData.poolKey, - mint, + 'none', + { + owner: wallet.publicKey, + cosigner, + }, ); - assert.equal(await connection.getBalance(poolAta), 0); - assert.equal(await connection.getBalance(sellState), 0); - let poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); - assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); - await program.methods .extDepositSell({ assetAmount: new anchor.BN(1), @@ -115,7 +88,7 @@ describe('mmm-ext-deposit', () => { ); assert.equal(Number(nftEscrow.amount), 1); assert.equal(nftEscrow.owner.toBase58(), poolData.poolKey.toBase58()); - poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + let poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 1); assert.equal(await connection.getBalance(recipientTokenAccount), 0); @@ -142,7 +115,7 @@ describe('mmm-ext-deposit', () => { connection, wallet.payer, undefined, - groupKeyPair, + groupAddress, ); const poolAta2 = await getAssociatedTokenAddress( mint2, @@ -210,41 +183,29 @@ describe('mmm-ext-deposit', () => { }); it('correctly verifies depositing nfts with ANY allowlist', async () => { - const { groupKeyPair } = await createTestGroupMintExt( + const { + mint, + recipientTokenAccount, + poolData, + poolAta, + sellState, + groupAddress, + } = await createPoolWithExampleExtDeposits( + program, connection, wallet.payer, - ); - const { mint, recipientTokenAccount } = - await createTestMintAndTokenT22VanillaExt( - connection, - wallet.payer, - undefined, - groupKeyPair, - ); - - const poolData = await createPool(program, { - owner: wallet.publicKey, - cosigner, - allowlists: [ - { - kind: AllowlistKind.any, - value: PublicKey.default, - }, - ...getEmptyAllowLists(5), - ], - }); - - const poolAta = await getAssociatedTokenAddress( - mint, - poolData.poolKey, - true, - TOKEN_2022_PROGRAM_ID, - ); - - const { key: sellState } = getMMMSellStatePDA( - program.programId, - poolData.poolKey, - mint, + 'none', + { + owner: wallet.publicKey, + cosigner, + allowlists: [ + { + kind: AllowlistKind.any, + value: PublicKey.default, + }, + ...getEmptyAllowLists(5), + ], + }, ); assert.equal(await connection.getBalance(poolAta), 0); @@ -306,7 +267,7 @@ describe('mmm-ext-deposit', () => { connection, wallet.payer, undefined, - groupKeyPair, + groupAddress, ); const poolAta2 = await getAssociatedTokenAddress( mint2, @@ -374,51 +335,17 @@ describe('mmm-ext-deposit', () => { }); it('failed to verify depositing with wrong allowlist aux', async () => { - const { groupKeyPair } = await createTestGroupMintExt( - connection, - wallet.payer, - ); - const { mint, recipientTokenAccount } = - await createTestMintAndTokenT22VanillaExt( + const { mint, recipientTokenAccount, poolData, poolAta, sellState } = + await createPoolWithExampleExtDeposits( + program, connection, wallet.payer, - undefined, - groupKeyPair, - ); - - const poolData = await createPool(program, { - owner: wallet.publicKey, - cosigner, - allowlists: [ - { - kind: AllowlistKind.metadata, - value: mint, - }, + 'none', { - kind: AllowlistKind.group, - value: groupKeyPair.publicKey, + owner: wallet.publicKey, + cosigner, }, - ...getEmptyAllowLists(4), - ], - }); - - const poolAta = await getAssociatedTokenAddress( - mint, - poolData.poolKey, - true, - TOKEN_2022_PROGRAM_ID, - ); - - const { key: sellState } = getMMMSellStatePDA( - program.programId, - poolData.poolKey, - mint, - ); - - assert.equal(await connection.getBalance(poolAta), 0); - assert.equal(await connection.getBalance(sellState), 0); - let poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); - assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); + ); try { await program.methods @@ -446,22 +373,22 @@ describe('mmm-ext-deposit', () => { }); it('failed to verify depositing nfts with external group member pointer', async () => { - const { groupKeyPair } = await createTestGroupMintExt( + const { groupAddress } = await createTestGroupMintExt( connection, wallet.payer, ); const { groupMemberKeyPair } = await createTestGroupMemberMint( connection, wallet.payer, - groupKeyPair, + groupAddress, ); const { mint, recipientTokenAccount } = await createTestMintAndTokenT22VanillaExt( connection, wallet.payer, undefined, - groupKeyPair, - groupMemberKeyPair, + groupAddress, + groupMemberKeyPair.publicKey, // external group member pointer ); const poolData = await createPool(program, { @@ -474,7 +401,7 @@ describe('mmm-ext-deposit', () => { }, { kind: AllowlistKind.group, - value: groupKeyPair.publicKey, + value: groupAddress, }, ...getEmptyAllowLists(4), ], @@ -524,7 +451,7 @@ describe('mmm-ext-deposit', () => { }); it('failed to verify depositing nfts with disallowed group', async () => { - const { groupKeyPair } = await createTestGroupMintExt( + const { groupAddress } = await createTestGroupMintExt( connection, wallet.payer, ); @@ -533,7 +460,7 @@ describe('mmm-ext-deposit', () => { connection, wallet.payer, undefined, - groupKeyPair, + groupAddress, ); const poolData = await createPool(program, { diff --git a/tests/mmm-ext-fulfill.spec.ts b/tests/mmm-ext-fulfill.spec.ts new file mode 100644 index 0000000..51cbb7a --- /dev/null +++ b/tests/mmm-ext-fulfill.spec.ts @@ -0,0 +1,606 @@ +import * as anchor from '@project-serum/anchor'; +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, + getAssociatedTokenAddress, +} from '@solana/spl-token'; +import { + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + SYSVAR_RENT_PUBKEY, + SystemProgram, +} from '@solana/web3.js'; +import { assert } from 'chai'; +import { + Mmm, + getMMMSellStatePDA, + IDL, + MMMProgramID, + CurveKind, + getSolFulfillBuyPrices, +} from '../sdk/src'; +import { + IMMUTABLE_OWNER_EXTENSION_LAMPORTS, + LAMPORT_ERROR_RANGE, + PRICE_ERROR_RANGE, + SIGNATURE_FEE_LAMPORTS, + airdrop, + assertIsBetween, + assertTx, + createPoolWithExampleExtDeposits, + createTestMintAndTokenT22VanillaExt, + getSellStatePDARent, + getTokenAccount2022, + getTokenAccountRent, + sendAndAssertTx, +} from './utils'; + +describe('mmm-ext-fulfill', () => { + const { connection } = anchor.AnchorProvider.env(); + const wallet = new anchor.Wallet(Keypair.generate()); + const provider = new anchor.AnchorProvider(connection, wallet, { + commitment: 'processed', + }); + const program = new anchor.Program( + IDL, + MMMProgramID, + provider, + ) as anchor.Program; + const cosigner = Keypair.generate(); + const buyer = Keypair.generate(); + + async function executeFulfillSell( + maxPaymentAmount: number, + referral: Keypair, + poolKey: PublicKey, + mint: PublicKey, + sellState: PublicKey, + solEscrowKey: PublicKey, + poolAta: PublicKey, + buyerNftAtaAddress: PublicKey, + takerFeeBp: number, + makerFeeBp: number, + ) { + const tx = await program.methods + .extFulfillSell({ + assetAmount: new anchor.BN(1), + maxPaymentAmount: new anchor.BN(maxPaymentAmount), + buysideCreatorRoyaltyBp: 0, + allowlistAux: '', + takerFeeBp, + makerFeeBp, + }) + .accountsStrict({ + payer: buyer.publicKey, + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + referral: referral.publicKey, + pool: poolKey, + buysideSolEscrowAccount: solEscrowKey, + assetMint: mint, + sellsideEscrowTokenAccount: poolAta, + payerAssetAccount: buyerNftAtaAddress, + sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .transaction(); + + const blockhashData = await connection.getLatestBlockhash(); + tx.feePayer = buyer.publicKey; + tx.recentBlockhash = blockhashData.blockhash; + tx.partialSign(cosigner, buyer); + + const txId = await connection.sendRawTransaction(tx.serialize(), { + skipPreflight: true, + }); + const confirmedTx = await connection.confirmTransaction( + { + signature: txId, + blockhash: blockhashData.blockhash, + lastValidBlockHeight: blockhashData.lastValidBlockHeight, + }, + 'processed', + ); + assertTx(txId, confirmedTx); + } + + beforeEach(async () => { + await airdrop(connection, wallet.publicKey, 50); + await airdrop(connection, buyer.publicKey, 50); + }); + + describe('ext_fulfill_sell', () => { + it('Sellside only', async () => { + const { mint, poolData, poolAta, sellState, solEscrowKey } = + await createPoolWithExampleExtDeposits( + program, + connection, + wallet.payer, + 'sell', // side + { + owner: wallet.publicKey, + cosigner: cosigner, + curveType: CurveKind.exp, + curveDelta: new anchor.BN(200), // 200 basis points + expiry: new anchor.BN(new Date().getTime() / 1000 + 1000), + reinvestFulfillBuy: true, + reinvestFulfillSell: true, + }, + ); + + const buyerNftAtaAddress = await getAssociatedTokenAddress( + mint, + buyer.publicKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + let initWalletBalance = await connection.getBalance(wallet.publicKey); + let initReferralBalance = await connection.getBalance( + poolData.referral.publicKey, + ); + let initBuyerBalance = await connection.getBalance(buyer.publicKey); + + let poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + assert.equal(poolAccountInfo.lpFeeEarned.toNumber(), 0); + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 1); + + let expectedTakerFees = 1.02 * LAMPORTS_PER_SOL * 0.01; + await executeFulfillSell( + 1.02 * LAMPORTS_PER_SOL + expectedTakerFees, + poolData.referral, + poolData.poolKey, + mint, + sellState, + solEscrowKey, + poolAta, + buyerNftAtaAddress, + 100, // takerFeeBp + 0, // makerFeeBp + ); + + let tokenAccountRent = + (await getTokenAccountRent(connection)) + + IMMUTABLE_OWNER_EXTENSION_LAMPORTS; + const sellStatePDARent = await getSellStatePDARent(connection); + + const expectedTxFees = + SIGNATURE_FEE_LAMPORTS * 2 + // cosigner + payer + tokenAccountRent; // token account + + { + const expectedTakerFees = 1.02 * LAMPORTS_PER_SOL * 0.01; + const expectedReferralFees = expectedTakerFees; + const [ + buyerBalance, + referralBalance, + poolAtaBalance, + poolEscrowBalance, + afterWalletBalance, + sellStateBalance, + ] = await Promise.all([ + connection.getBalance(buyer.publicKey), + connection.getBalance(poolData.referral.publicKey), + connection.getBalance(poolAta), + connection.getBalance(solEscrowKey), + connection.getBalance(wallet.publicKey), + connection.getBalance(sellState), + ]); + assert.equal( + buyerBalance, + initBuyerBalance - + 1.02 * LAMPORTS_PER_SOL - + expectedTxFees - + expectedTakerFees, + ); + assert.equal( + referralBalance, + initReferralBalance + expectedReferralFees, + ); + assert.equal(poolAtaBalance, 0); + assert.equal(poolEscrowBalance, 1.02 * LAMPORTS_PER_SOL); + assertIsBetween( + afterWalletBalance, + initWalletBalance + tokenAccountRent + sellStatePDARent, + LAMPORT_ERROR_RANGE, + ); + assert.equal(sellStateBalance, 0); + + initReferralBalance = referralBalance; + initBuyerBalance = buyerBalance; + } + + poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + assert.equal( + poolAccountInfo.spotPrice.toNumber(), + 1.02 * LAMPORTS_PER_SOL, + ); + assert.equal(poolAccountInfo.lpFeeEarned.toNumber(), 0); + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); + assert.equal( + poolAccountInfo.buysidePaymentAmount.toNumber(), + 1.02 * LAMPORTS_PER_SOL, + ); + }); + }); + + describe('ext_fulfill_buy', () => { + it('Buyside only', async () => { + const seller = Keypair.generate(); + await airdrop(connection, seller.publicKey, 50); + const { poolData, solEscrowKey, groupAddress } = + await createPoolWithExampleExtDeposits( + program, + connection, + wallet.payer, + 'buy', // side + { + owner: wallet.publicKey, + cosigner, + curveType: CurveKind.exp, + curveDelta: new anchor.BN(125), // 125 bp + expiry: new anchor.BN(0), + spotPrice: new anchor.BN(0.5 * LAMPORTS_PER_SOL), + referralBp: 200, + reinvestFulfillBuy: false, + reinvestFulfillSell: false, + }, + ); + const { + mint: extraMint, + recipientTokenAccount: extraRecipientTokenAccount, + } = await createTestMintAndTokenT22VanillaExt( + connection, + wallet.payer, + seller.publicKey, + groupAddress, + ); + + const ownerExtraNftAtaAddress = await getAssociatedTokenAddress( + extraMint, + wallet.publicKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + const { key: extraNftSellState } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + extraMint, + ); + const extraPoolAta = await getAssociatedTokenAddress( + extraMint, + poolData.poolKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + let [ + initReferralBalance, + initSellerBalance, + initPaymentEscrowBalance, + initWalletBalance, + ] = await Promise.all([ + connection.getBalance(poolData.referral.publicKey), + connection.getBalance(seller.publicKey), + connection.getBalance(solEscrowKey), + connection.getBalance(wallet.publicKey), + ]); + + const expectedTotalPrice = 0.5; + const tx = await program.methods + .extFulfillBuy({ + assetAmount: new anchor.BN(1), + minPaymentAmount: new anchor.BN( + expectedTotalPrice * LAMPORTS_PER_SOL, + ), + allowlistAux: '', + takerFeeBp: 0, + makerFeeBp: 150, + }) + .accountsStrict({ + payer: seller.publicKey, + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + referral: poolData.referral.publicKey, + pool: poolData.poolKey, + buysideSolEscrowAccount: solEscrowKey, + assetMint: extraMint, + payerAssetAccount: extraRecipientTokenAccount, + sellsideEscrowTokenAccount: extraPoolAta, + ownerTokenAccount: ownerExtraNftAtaAddress, + allowlistAuxAccount: SystemProgram.programId, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + rent: SYSVAR_RENT_PUBKEY, + sellState: extraNftSellState, + }) + .transaction(); + + const blockhashData = await connection.getLatestBlockhash(); + tx.feePayer = seller.publicKey; + tx.recentBlockhash = blockhashData.blockhash; + tx.partialSign(cosigner, seller); + + await sendAndAssertTx(connection, tx, blockhashData, false); + + const expectedMakerFees = expectedTotalPrice * LAMPORTS_PER_SOL * 0.015; + const expectedReferralFees = expectedMakerFees; + const [ + sellerBalance, + referralBalance, + afterWalletBalance, + poolEscrowBalance, + ] = await Promise.all([ + connection.getBalance(seller.publicKey), + connection.getBalance(poolData.referral.publicKey), + connection.getBalance(wallet.publicKey), + connection.getBalance(solEscrowKey), + ]); + + assertIsBetween( + sellerBalance, + initSellerBalance + + expectedTotalPrice * LAMPORTS_PER_SOL - + SIGNATURE_FEE_LAMPORTS * 2, + PRICE_ERROR_RANGE, + ); + assert.equal(referralBalance, initReferralBalance + expectedReferralFees); + assert.equal( + poolEscrowBalance, + initPaymentEscrowBalance - + expectedTotalPrice * LAMPORTS_PER_SOL - + expectedMakerFees, + ); + assert.equal(afterWalletBalance, initWalletBalance); + + const poolAccountInfo = await program.account.pool.fetch( + poolData.poolKey, + ); + assertIsBetween( + poolAccountInfo.spotPrice.toNumber(), + (expectedTotalPrice * LAMPORTS_PER_SOL) / 1.0125, + PRICE_ERROR_RANGE, + ); + // do not reinvest so sell side asset amount should be 0 + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); + }); + }); + + describe('ext_fulfillment', () => { + it('Two sides', async () => { + const seller = Keypair.generate(); + await airdrop(connection, seller.publicKey, 50); + const { mint, poolData, poolAta, sellState, solEscrowKey, groupAddress } = + await createPoolWithExampleExtDeposits( + program, + connection, + wallet.payer, + 'both', // side + { + owner: wallet.publicKey, + cosigner: cosigner, + curveType: CurveKind.exp, + curveDelta: new anchor.BN(300), // 300 basis points + expiry: new anchor.BN(new Date().getTime() / 1000 + 1000), + lpFeeBp: 200, + }, + ); + + const { + mint: extraMint, + recipientTokenAccount: extraRecipientTokenAccount, + } = await createTestMintAndTokenT22VanillaExt( + connection, + wallet.payer, + seller.publicKey, + groupAddress, + ); + + const { key: extraNftSellState } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + extraMint, + ); + const extraPoolAta = await getAssociatedTokenAddress( + extraMint, + poolData.poolKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + let [initReferralBalance, initSellerBalance, initBuyerBalance] = + await Promise.all([ + connection.getBalance(poolData.referral.publicKey), + connection.getBalance(seller.publicKey), + connection.getBalance(buyer.publicKey), + ]); + + const expectedLpFees = LAMPORTS_PER_SOL * 0.02; + const expectedTakerFees = LAMPORTS_PER_SOL * 0.01; + const expectedBuyPrices = getSolFulfillBuyPrices({ + totalPriceLamports: LAMPORTS_PER_SOL, + lpFeeBp: 200, + takerFeeBp: 100, + metadataRoyaltyBp: 0, + buysideCreatorRoyaltyBp: 0, + makerFeeBp: 0, + }); + + const tx = await program.methods + .extFulfillBuy({ + assetAmount: new anchor.BN(1), + minPaymentAmount: expectedBuyPrices.sellerReceives, + allowlistAux: null, + takerFeeBp: 100, + makerFeeBp: 0, + }) + .accountsStrict({ + payer: seller.publicKey, + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + referral: poolData.referral.publicKey, + pool: poolData.poolKey, + buysideSolEscrowAccount: solEscrowKey, + assetMint: extraMint, + payerAssetAccount: extraRecipientTokenAccount, + sellsideEscrowTokenAccount: extraPoolAta, + ownerTokenAccount: extraRecipientTokenAccount, + allowlistAuxAccount: SystemProgram.programId, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + rent: SYSVAR_RENT_PUBKEY, + sellState: extraNftSellState, + }) + .transaction(); + + const blockhashData = await connection.getLatestBlockhash(); + tx.feePayer = seller.publicKey; + tx.recentBlockhash = blockhashData.blockhash; + tx.partialSign(cosigner, seller); + + await sendAndAssertTx(connection, tx, blockhashData, false); + + let tokenAccountRent = + (await getTokenAccountRent(connection)) + + IMMUTABLE_OWNER_EXTENSION_LAMPORTS; + const sellStatePDARent = await getSellStatePDARent(connection); + + const expectedTxFees = SIGNATURE_FEE_LAMPORTS * 2; // cosigner + payer + { + const [sellerBalance, referralBalance] = await Promise.all([ + connection.getBalance(seller.publicKey), + connection.getBalance(poolData.referral.publicKey), + ]); + + assert.equal( + sellerBalance, + initSellerBalance + + expectedBuyPrices.sellerReceives.toNumber() - + expectedTxFees - + sellStatePDARent, // no token account rent bc seller ata was closed and pool ata opened + ); + assert.equal( + referralBalance, + initReferralBalance + expectedBuyPrices.takerFeePaid.toNumber(), + ); + initReferralBalance = referralBalance; + } + + let poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + assertIsBetween( + poolAccountInfo.spotPrice.toNumber(), + LAMPORTS_PER_SOL / 1.03, + PRICE_ERROR_RANGE, + ); + assert.equal( + poolAccountInfo.lpFeeEarned.toNumber(), + expectedBuyPrices.lpFeePaid.toNumber(), + ); + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 2); + + const buyerNftAtaAddress = await getAssociatedTokenAddress( + mint, + buyer.publicKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + { + const tx = await program.methods + .extFulfillSell({ + assetAmount: new anchor.BN(1), + maxPaymentAmount: new anchor.BN( + LAMPORTS_PER_SOL + expectedTakerFees + expectedLpFees, + ), + buysideCreatorRoyaltyBp: 0, + allowlistAux: '', + takerFeeBp: 100, + makerFeeBp: 100, + }) + .accountsStrict({ + payer: buyer.publicKey, + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + referral: poolData.referral.publicKey, + pool: poolData.poolKey, + buysideSolEscrowAccount: solEscrowKey, + assetMint: mint, + sellsideEscrowTokenAccount: poolAta, + payerAssetAccount: buyerNftAtaAddress, + sellState: sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .transaction(); + + const blockhashData = await connection.getLatestBlockhash(); + tx.feePayer = buyer.publicKey; + tx.recentBlockhash = blockhashData.blockhash; + tx.partialSign(cosigner, buyer); + + await sendAndAssertTx(connection, tx, blockhashData, false); + } + + { + const expectedMakerFees = LAMPORTS_PER_SOL * 0.01; + const expectedReferralFees = expectedTakerFees + expectedMakerFees; + const [buyerBalance, referralBalance, buyerAta] = await Promise.all([ + connection.getBalance(buyer.publicKey), + connection.getBalance(poolData.referral.publicKey), + getTokenAccount2022( + connection, + buyerNftAtaAddress, + TOKEN_2022_PROGRAM_ID, + ), + ]); + + assertIsBetween( + buyerBalance, + initBuyerBalance - + LAMPORTS_PER_SOL - + expectedLpFees - + expectedTakerFees - + expectedTxFees - + tokenAccountRent, // no token account rent bc seller ata was closed and pool ata opened + PRICE_ERROR_RANGE, + ); + assertIsBetween( + referralBalance, + initReferralBalance + expectedReferralFees, + PRICE_ERROR_RANGE, + ); + assert.equal(Number(buyerAta.amount), 1); + } + + poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + assertIsBetween( + poolAccountInfo.spotPrice.toNumber(), + 1 * LAMPORTS_PER_SOL, + PRICE_ERROR_RANGE, + ); + assertIsBetween( + poolAccountInfo.lpFeeEarned.toNumber(), + expectedBuyPrices.lpFeePaid.toNumber() + 0.02 * LAMPORTS_PER_SOL, + PRICE_ERROR_RANGE, + ); + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 1); + + const [finalSellerBalance, finalBuyerBalance] = await Promise.all([ + connection.getBalance(seller.publicKey), + connection.getBalance(buyer.publicKey), + ]); + + assert.isAtMost( + finalBuyerBalance + finalSellerBalance, + initBuyerBalance + initSellerBalance, + ); + }); + }); +}); diff --git a/tests/utils/mmm.ts b/tests/utils/mmm.ts index 6803d64..1274c1f 100644 --- a/tests/utils/mmm.ts +++ b/tests/utils/mmm.ts @@ -40,6 +40,7 @@ import { getAssociatedTokenAddress, ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, } from '@solana/spl-token'; import { fromWeb3JsPublicKey, @@ -67,9 +68,16 @@ import { OCP_COMPUTE_UNITS, } from './generic'; import { createProgrammableNftMip1, createProgrammableNftUmi } from './mip1'; -import { getMetaplexInstance, mintCollection, mintNfts } from './nfts'; +import { + createTestGroupMintExt, + createTestMintAndTokenT22VanillaExt, + getMetaplexInstance, + mintCollection, + mintNfts, +} from './nfts'; import { umiMintNfts, Nft, umiMintCollection } from './umiNfts'; import { createTestMintAndTokenOCP } from './ocp'; +import { assert } from 'chai'; const TOKEN_METADATA_PROGRAM_ID = new PublicKey( 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s', @@ -158,6 +166,107 @@ export const createPool = async ( return { referral, uuid, poolKey }; }; +export const createPoolWithExampleExtDeposits = async ( + program: Program, + connection: Connection, + payer: Keypair, + side: 'buy' | 'sell' | 'both' | 'none', + poolArgs: Parameters[1], +) => { + const { groupAddress } = await createTestGroupMintExt(connection, payer); + const { mint, recipientTokenAccount } = + await createTestMintAndTokenT22VanillaExt( + connection, + payer, + poolArgs.owner, + groupAddress, + ); + + const poolData = await createPool(program, { + allowlists: [ + { + kind: AllowlistKind.metadata, + value: mint, + }, + { + kind: AllowlistKind.group, + value: groupAddress, + }, + ...getEmptyAllowLists(4), + ], + ...poolArgs, + }); + + const poolAta = await getAssociatedTokenAddress( + mint, + poolData.poolKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + const { key: sellState } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + mint, + ); + + assert.equal(await connection.getBalance(poolAta), 0); + assert.equal(await connection.getBalance(sellState), 0); + let poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); + + if (side === 'both' || side === 'sell') { + await program.methods + .extDepositSell({ + assetAmount: new anchor.BN(1), + allowlistAux: 'example.com', + }) + .accountsStrict({ + owner: poolArgs.owner, + cosigner: poolArgs.cosigner?.publicKey!, + pool: poolData.poolKey, + assetMint: mint, + assetTokenAccount: recipientTokenAccount, + sellsideEscrowTokenAccount: poolAta, + sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([poolArgs.cosigner!]) + .rpc({ skipPreflight: true }); + } + + const { key: solEscrowKey } = getMMMBuysideSolEscrowPDA( + program.programId, + poolData.poolKey, + ); + + if (side === 'both' || side === 'buy') { + await program.methods + .solDepositBuy({ paymentAmount: new anchor.BN(10 * LAMPORTS_PER_SOL) }) + .accountsStrict({ + owner: poolArgs.owner, + cosigner: poolArgs.cosigner?.publicKey ?? poolArgs.owner, + pool: poolData.poolKey, + buysideSolEscrowAccount: solEscrowKey, + systemProgram: SystemProgram.programId, + }) + .signers([...(poolArgs.cosigner ? [poolArgs.cosigner] : [])]) + .rpc({ skipPreflight: true }); + } + + return { + mint, + recipientTokenAccount, + poolData, + poolAta, + sellState, + solEscrowKey, + groupAddress, + }; +}; + export const createPoolWithExampleDeposits = async ( program: Program, connection: Connection, diff --git a/tests/utils/nfts.ts b/tests/utils/nfts.ts index f61ac65..dc2c668 100644 --- a/tests/utils/nfts.ts +++ b/tests/utils/nfts.ts @@ -136,11 +136,11 @@ export async function createTestMintAndTokenT22VanillaExt( connection: Connection, payer: Keypair, recipient?: PublicKey, - groupKeyPair?: Keypair, - groupMemberKeyPair?: Keypair, + groupAddress?: PublicKey, + groupMemberAddress?: PublicKey, ) { const mintKeypair = Keypair.generate(); - const effectiveGroupKeyPair = groupKeyPair ?? Keypair.generate(); + const effectiveGroupAddress = groupAddress ?? Keypair.generate().publicKey; const tokenProgramId = TOKEN_2022_PROGRAM_ID; const effectiveRecipient = recipient ?? payer.publicKey; const targetTokenAccount = getAssociatedTokenAddressSync( @@ -172,7 +172,7 @@ export async function createTestMintAndTokenT22VanillaExt( tokenProgramId, ); - const memberAddress = groupMemberKeyPair?.publicKey ?? mintKeypair.publicKey; + const memberAddress = groupMemberAddress ?? mintKeypair.publicKey; const createGroupMemberPointerIx = createInitializeGroupMemberPointerInstruction( mintKeypair.publicKey, @@ -204,7 +204,7 @@ export async function createTestMintAndTokenT22VanillaExt( member: mintKeypair.publicKey, memberMint: mintKeypair.publicKey, memberMintAuthority: payer.publicKey, - group: effectiveGroupKeyPair.publicKey, + group: effectiveGroupAddress, groupUpdateAuthority: payer.publicKey, }); @@ -302,14 +302,14 @@ export async function createTestGroupMintExt( await sendAndAssertTx(connection, tx, blockhashData, false); return { - groupKeyPair, + groupAddress: groupKeyPair.publicKey, }; } export async function createTestGroupMemberMint( connection: Connection, payer: Keypair, - groupAddress: Keypair, + groupAddress: PublicKey, ) { const tokenProgramId = TOKEN_2022_PROGRAM_ID; const groupMemberKeyPair = Keypair.generate(); @@ -346,7 +346,7 @@ export async function createTestGroupMemberMint( member: groupMemberKeyPair.publicKey, memberMint: groupMemberKeyPair.publicKey, memberMintAuthority: payer.publicKey, - group: groupAddress.publicKey, + group: groupAddress, groupUpdateAuthority: payer.publicKey, }); From 49517751f8ec4672dd1ccda6d6d17ec8637ba368 Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Wed, 13 Mar 2024 20:03:47 -0700 Subject: [PATCH 12/24] fmt --- .../mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs | 2 +- programs/mmm/src/util.rs | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs b/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs index dca0046..14407e2 100644 --- a/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs +++ b/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs @@ -138,7 +138,7 @@ pub fn handler<'info>( total_price, lp_fee_bp, 0, // metadata_royalty_bp - pool.buyside_creator_royalty_bp, + 0, // buyside_creator_royalty_bp, ) }?; let lp_fee = get_sol_lp_fee(pool, buyside_sol_escrow_account.lamports(), seller_receives)?; diff --git a/programs/mmm/src/util.rs b/programs/mmm/src/util.rs index 2471e50..8f39ab0 100644 --- a/programs/mmm/src/util.rs +++ b/programs/mmm/src/util.rs @@ -1,8 +1,7 @@ use crate::{ constants::{ - M2_AUCTION_HOUSE, M2_PREFIX, M2_PROGRAM, - MAX_METADATA_CREATOR_ROYALTY_BP, MAX_REFERRAL_FEE_BP, MAX_TOTAL_PRICE, - MIN_SOL_ESCROW_BALANCE_BP, POOL_PREFIX, + M2_AUCTION_HOUSE, M2_PREFIX, M2_PROGRAM, MAX_METADATA_CREATOR_ROYALTY_BP, + MAX_REFERRAL_FEE_BP, MAX_TOTAL_PRICE, MIN_SOL_ESCROW_BALANCE_BP, POOL_PREFIX, }, errors::MMMErrorCode, state::*, @@ -25,8 +24,8 @@ use spl_token_2022::{ }, state::Mint as Token22Mint, }; -use spl_token_metadata_interface::state::TokenMetadata; use spl_token_group_interface::state::TokenGroupMember; +use spl_token_metadata_interface::state::TokenMetadata; use std::convert::TryFrom; #[macro_export] From 224d2f7cd13597f7821fcfef3f97024916155318 Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Wed, 13 Mar 2024 20:26:50 -0700 Subject: [PATCH 13/24] minor refactor --- programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs | 4 ++-- .../mmm/src/instructions/ext_vanilla/ext_fulfill_sell.rs | 5 +++-- programs/mmm/src/instructions/ext_vanilla/mod.rs | 2 +- programs/mmm/src/instructions/vanilla/sol_fulfill_sell.rs | 4 ++-- programs/mmm/src/util.rs | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs b/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs index 14407e2..42301bd 100644 --- a/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs +++ b/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs @@ -302,8 +302,8 @@ pub fn handler<'info>( if shared_escrow_account.lamports() + buyside_sol_escrow_account.lamports() > min_rent && buyside_sol_escrow_account.lamports() > 0 { - anchor_lang::solana_program::program::invoke_signed( - &anchor_lang::solana_program::system_instruction::transfer( + invoke_signed( + &system_instruction::transfer( buyside_sol_escrow_account.key, shared_escrow_account.key, buyside_sol_escrow_account.lamports(), diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_sell.rs b/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_sell.rs index bb1bc97..7818b22 100644 --- a/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_sell.rs +++ b/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_sell.rs @@ -12,7 +12,8 @@ use crate::{ constants::*, errors::MMMErrorCode, instructions::{ - get_pool_price_info, log_pool, try_close_pool, try_close_sell_state, PoolPriceInfo, + get_sell_fulfill_pool_price_info, log_pool, try_close_pool, try_close_sell_state, + PoolPriceInfo, }, state::{Pool, SellState}, util::check_allowlists_for_mint_ext, @@ -125,7 +126,7 @@ pub fn handler<'info>( taker_fee, referral_fee, transfer_sol_to, - } = get_pool_price_info( + } = get_sell_fulfill_pool_price_info( pool, owner, buyside_sol_escrow_account, diff --git a/programs/mmm/src/instructions/ext_vanilla/mod.rs b/programs/mmm/src/instructions/ext_vanilla/mod.rs index e274456..9017297 100644 --- a/programs/mmm/src/instructions/ext_vanilla/mod.rs +++ b/programs/mmm/src/instructions/ext_vanilla/mod.rs @@ -2,6 +2,6 @@ pub mod ext_deposit_sell; pub mod ext_fulfill_buy; pub mod ext_fulfill_sell; -pub use ext_fulfill_buy::*; pub use ext_deposit_sell::*; +pub use ext_fulfill_buy::*; pub use ext_fulfill_sell::*; diff --git a/programs/mmm/src/instructions/vanilla/sol_fulfill_sell.rs b/programs/mmm/src/instructions/vanilla/sol_fulfill_sell.rs index 3be2d12..c136b4e 100644 --- a/programs/mmm/src/instructions/vanilla/sol_fulfill_sell.rs +++ b/programs/mmm/src/instructions/vanilla/sol_fulfill_sell.rs @@ -8,7 +8,7 @@ use std::convert::TryFrom; use crate::{ constants::*, errors::MMMErrorCode, - instructions::{get_pool_price_info, PoolPriceInfo}, + instructions::{get_sell_fulfill_pool_price_info, PoolPriceInfo}, state::{Pool, SellState}, util::{ check_allowlists_for_mint, get_metadata_royalty_bp, log_pool, pay_creator_fees_in_sol, @@ -151,7 +151,7 @@ pub fn handler<'info>( taker_fee, referral_fee, transfer_sol_to, - } = get_pool_price_info( + } = get_sell_fulfill_pool_price_info( pool, owner, buyside_sol_escrow_account, diff --git a/programs/mmm/src/util.rs b/programs/mmm/src/util.rs index 8f39ab0..1ca03c2 100644 --- a/programs/mmm/src/util.rs +++ b/programs/mmm/src/util.rs @@ -774,7 +774,7 @@ pub struct PoolPriceInfo<'info> { pub transfer_sol_to: AccountInfo<'info>, } -pub fn get_pool_price_info<'info>( +pub fn get_sell_fulfill_pool_price_info<'info>( pool: &Pool, owner: &UncheckedAccount<'info>, buyside_sol_escrow_account: &AccountInfo<'info>, From ea3233e2dcb66792f61dc8aed996057da3a90fc5 Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Thu, 14 Mar 2024 12:42:15 -0700 Subject: [PATCH 14/24] add ext_withdraw_sell --- .../ext_vanilla/ext_withdraw_sell.rs | 136 ++++++++++++++++ .../mmm/src/instructions/ext_vanilla/mod.rs | 2 + programs/mmm/src/lib.rs | 4 + sdk/src/idl/mmm.ts | 146 ++++++++++++++++++ tests/mmm-ext-withdraw.spec.ts | 127 +++++++++++++++ 5 files changed, 415 insertions(+) create mode 100644 programs/mmm/src/instructions/ext_vanilla/ext_withdraw_sell.rs create mode 100644 tests/mmm-ext-withdraw.spec.ts diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_withdraw_sell.rs b/programs/mmm/src/instructions/ext_vanilla/ext_withdraw_sell.rs new file mode 100644 index 0000000..8b4086b --- /dev/null +++ b/programs/mmm/src/instructions/ext_vanilla/ext_withdraw_sell.rs @@ -0,0 +1,136 @@ +use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize}; +use anchor_spl::{ + associated_token::AssociatedToken, + token_2022::{close_account, CloseAccount}, + token_interface::{Mint, TokenAccount, TokenInterface}, +}; +use spl_token_2022::onchain::invoke_transfer_checked; + +use crate::{ + constants::*, + errors::MMMErrorCode, + state::{Pool, SellState}, + util::{log_pool, try_close_pool, try_close_sell_state}, + WithdrawSellArgs, +}; + +#[derive(Accounts)] +#[instruction(args:WithdrawSellArgs)] +pub struct ExtWithdrawSell<'info> { + #[account(mut)] + pub owner: Signer<'info>, + pub cosigner: Signer<'info>, + #[account( + mut, + seeds = [POOL_PREFIX.as_bytes(), owner.key().as_ref(), pool.uuid.as_ref()], + has_one = owner @ MMMErrorCode::InvalidOwner, + has_one = cosigner @ MMMErrorCode::InvalidCosigner, + bump + )] + pub pool: Box>, + pub asset_mint: InterfaceAccount<'info, Mint>, + #[account( + init_if_needed, + payer = owner, + associated_token::mint = asset_mint, + associated_token::authority = owner, + associated_token::token_program = token_program + )] + pub asset_token_account: Box>, + #[account( + mut, + associated_token::mint = asset_mint, + associated_token::authority = pool, + associated_token::token_program = token_program + )] + pub sellside_escrow_token_account: Box>, + /// CHECK: it's a pda, and the private key is owned by the seeds + #[account( + mut, + seeds = [BUYSIDE_SOL_ESCROW_ACCOUNT_PREFIX.as_bytes(), pool.key().as_ref()], + bump, + )] + pub buyside_sol_escrow_account: UncheckedAccount<'info>, + /// CHECK: will be used for allowlist checks + pub allowlist_aux_account: UncheckedAccount<'info>, + #[account( + mut, + seeds = [ + SELL_STATE_PREFIX.as_bytes(), + pool.key().as_ref(), + asset_mint.key().as_ref(), + ], + bump + )] + pub sell_state: Account<'info, SellState>, + pub system_program: Program<'info, System>, + pub token_program: Interface<'info, TokenInterface>, + pub associated_token_program: Program<'info, AssociatedToken>, +} + +pub fn handler(ctx: Context, args: WithdrawSellArgs) -> Result<()> { + let owner = &ctx.accounts.owner; + let asset_token_account = &ctx.accounts.asset_token_account; + let sellside_escrow_token_account = &ctx.accounts.sellside_escrow_token_account; + let token_program = &ctx.accounts.token_program; + let buyside_sol_escrow_account = &ctx.accounts.buyside_sol_escrow_account; + let pool = &mut ctx.accounts.pool; + let sell_state = &mut ctx.accounts.sell_state; + let asset_mint = &ctx.accounts.asset_mint; + + // Note that check_allowlists_for_mint_ext is optional for withdraw_sell + // because sometimes the nft might be moved out of the collection + // and we'd still like to enable the withdraw of those items for the pool owner. + + invoke_transfer_checked( + token_program.key, + sellside_escrow_token_account.to_account_info(), + asset_mint.to_account_info(), + asset_token_account.to_account_info(), + pool.to_account_info(), + &[], // additional_accounts + args.asset_amount, + 0, // decimals + &[&[ + POOL_PREFIX.as_bytes(), + owner.key().as_ref(), + pool.uuid.key().as_ref(), + &[ctx.bumps.pool], + ]], + )?; + + // we can close the sellside_escrow_token_account if no amount left + if sellside_escrow_token_account.amount == args.asset_amount { + close_account(CpiContext::new_with_signer( + token_program.to_account_info(), + CloseAccount { + account: sellside_escrow_token_account.to_account_info(), + destination: owner.to_account_info(), + authority: pool.to_account_info(), + }, + // seeds should be the PDA of 'pool' + &[&[ + POOL_PREFIX.as_bytes(), + owner.key().as_ref(), + pool.uuid.key().as_ref(), + &[ctx.bumps.pool], + ]], + ))?; + } + + pool.sellside_asset_amount = pool + .sellside_asset_amount + .checked_sub(args.asset_amount) + .ok_or(MMMErrorCode::NumericOverflow)?; + sell_state.asset_amount = sell_state + .asset_amount + .checked_sub(args.asset_amount) + .ok_or(MMMErrorCode::NumericOverflow)?; + try_close_sell_state(sell_state, owner.to_account_info())?; + + pool.buyside_payment_amount = buyside_sol_escrow_account.lamports(); + log_pool("post_ext_withdraw_sell", pool)?; + try_close_pool(pool, owner.to_account_info())?; + + Ok(()) +} diff --git a/programs/mmm/src/instructions/ext_vanilla/mod.rs b/programs/mmm/src/instructions/ext_vanilla/mod.rs index 9017297..cfc445e 100644 --- a/programs/mmm/src/instructions/ext_vanilla/mod.rs +++ b/programs/mmm/src/instructions/ext_vanilla/mod.rs @@ -1,7 +1,9 @@ pub mod ext_deposit_sell; pub mod ext_fulfill_buy; pub mod ext_fulfill_sell; +pub mod ext_withdraw_sell; pub use ext_deposit_sell::*; pub use ext_fulfill_buy::*; pub use ext_fulfill_sell::*; +pub use ext_withdraw_sell::*; diff --git a/programs/mmm/src/lib.rs b/programs/mmm/src/lib.rs index f1443cd..68e84e4 100644 --- a/programs/mmm/src/lib.rs +++ b/programs/mmm/src/lib.rs @@ -145,4 +145,8 @@ pub mod mmm { ) -> Result<()> { instructions::ext_fulfill_buy::handler(ctx, args) } + + pub fn ext_withdraw_sell(ctx: Context, args: WithdrawSellArgs) -> Result<()> { + instructions::ext_withdraw_sell::handler(ctx, args) + } } diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index bee426e..d1531e1 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -1835,6 +1835,79 @@ export type Mmm = { } } ] + }, + { + "name": "extWithdrawSell", + "accounts": [ + { + "name": "owner", + "isMut": true, + "isSigner": true + }, + { + "name": "cosigner", + "isMut": false, + "isSigner": true + }, + { + "name": "pool", + "isMut": true, + "isSigner": false + }, + { + "name": "assetMint", + "isMut": false, + "isSigner": false + }, + { + "name": "assetTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "sellsideEscrowTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "buysideSolEscrowAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "allowlistAuxAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "sellState", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "WithdrawSellArgs" + } + } + ] } ], "accounts": [ @@ -4335,6 +4408,79 @@ export const IDL: Mmm = { } } ] + }, + { + "name": "extWithdrawSell", + "accounts": [ + { + "name": "owner", + "isMut": true, + "isSigner": true + }, + { + "name": "cosigner", + "isMut": false, + "isSigner": true + }, + { + "name": "pool", + "isMut": true, + "isSigner": false + }, + { + "name": "assetMint", + "isMut": false, + "isSigner": false + }, + { + "name": "assetTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "sellsideEscrowTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "buysideSolEscrowAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "allowlistAuxAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "sellState", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "WithdrawSellArgs" + } + } + ] } ], "accounts": [ diff --git a/tests/mmm-ext-withdraw.spec.ts b/tests/mmm-ext-withdraw.spec.ts new file mode 100644 index 0000000..a517884 --- /dev/null +++ b/tests/mmm-ext-withdraw.spec.ts @@ -0,0 +1,127 @@ +import * as anchor from '@project-serum/anchor'; +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, +} from '@solana/spl-token'; +import { Keypair, LAMPORTS_PER_SOL, SystemProgram } from '@solana/web3.js'; +import { assert } from 'chai'; +import { Mmm, IDL, MMMProgramID } from '../sdk/src'; +import { + airdrop, + createPoolWithExampleExtDeposits, + getTokenAccount2022, + SIGNATURE_FEE_LAMPORTS, +} from './utils'; + +describe('mmm-ext-withdraw', () => { + const { connection } = anchor.AnchorProvider.env(); + const wallet = new anchor.Wallet(Keypair.generate()); + const provider = new anchor.AnchorProvider(connection, wallet, { + commitment: 'processed', + }); + const program = new anchor.Program( + IDL, + MMMProgramID, + provider, + ) as anchor.Program; + const cosigner = Keypair.generate(); + + beforeEach(async () => { + await airdrop(connection, wallet.publicKey, 50); + }); + + it('Withdraw payment', async () => { + const { + mint, + recipientTokenAccount, + poolData, + poolAta, + sellState, + solEscrowKey, + groupAddress, + } = await createPoolWithExampleExtDeposits( + program, + connection, + wallet.payer, + 'buy', + { + owner: wallet.publicKey, + cosigner, + }, + ); + + const initWalletBalance = await connection.getBalance(wallet.publicKey); + const poolRent = await connection.getBalance(poolData.poolKey); + await program.methods + .solWithdrawBuy({ + paymentAmount: new anchor.BN(100 * LAMPORTS_PER_SOL), + }) + .accountsStrict({ + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + pool: poolData.poolKey, + buysideSolEscrowAccount: solEscrowKey, + systemProgram: SystemProgram.programId, + }) + .signers([cosigner]) + .rpc(); + + assert.equal(await connection.getBalance(poolData.poolKey), 0); + const walletBalance = await connection.getBalance(wallet.publicKey); + assert.equal( + walletBalance, + initWalletBalance + + 10 * LAMPORTS_PER_SOL + // amount initially deposited + poolRent - // pool rent + 2 * SIGNATURE_FEE_LAMPORTS, // signature fees + ); + }); + + it('Withdraw assets', async () => { + const { + mint, + recipientTokenAccount, + poolData, + poolAta, + sellState, + solEscrowKey, + groupAddress, + } = await createPoolWithExampleExtDeposits( + program, + connection, + wallet.payer, + 'sell', + { + owner: wallet.publicKey, + cosigner, + }, + ); + + await program.methods + .extWithdrawSell({ assetAmount: new anchor.BN(1), allowlistAux: null }) + .accountsStrict({ + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + pool: poolData.poolKey, + assetMint: mint, + assetTokenAccount: recipientTokenAccount, + sellsideEscrowTokenAccount: poolAta, + buysideSolEscrowAccount: solEscrowKey, + allowlistAuxAccount: SystemProgram.programId, + sellState: sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([cosigner]) + .rpc({ skipPreflight: true }); + + const ownerNftAta = await getTokenAccount2022( + connection, + recipientTokenAccount, + TOKEN_2022_PROGRAM_ID, + ); + assert.equal(Number(ownerNftAta.amount), 1); + assert.equal(ownerNftAta.owner.toBase58(), wallet.publicKey.toBase58()); + }); +}); From ab5fc874acfa083b7a5606dc7016e876f28004f9 Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Thu, 14 Mar 2024 20:00:49 -0700 Subject: [PATCH 15/24] withdraw from m2 first --- .../ext_vanilla/ext_deposit_sell.rs | 2 +- .../ext_vanilla/ext_fulfill_buy.rs | 25 +++++++++++++++++-- .../ext_vanilla/ext_fulfill_sell.rs | 2 +- .../ext_vanilla/ext_withdraw_sell.rs | 2 +- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs b/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs index 7dbcb59..b414760 100644 --- a/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs +++ b/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs @@ -32,7 +32,7 @@ pub struct ExtDepositeSell<'info> { mint::token_program = token_program, constraint = asset_mint.supply == 1 && asset_mint.decimals == 0 @ MMMErrorCode::InvalidTokenMint, )] - pub asset_mint: InterfaceAccount<'info, Mint>, + pub asset_mint: Box>, #[account( mut, associated_token::mint = asset_mint, diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs b/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs index 42301bd..9afc4db 100644 --- a/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs +++ b/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs @@ -13,7 +13,7 @@ use crate::{ constants::*, errors::MMMErrorCode, index_ra, - instructions::{log_pool, try_close_pool}, + instructions::{check_remaining_accounts_for_m2, log_pool, try_close_pool, withdraw_m2}, state::{Pool, SellState}, util::{ assert_valid_fees_bp, check_allowlists_for_mint_ext, get_buyside_seller_receives, @@ -59,7 +59,7 @@ pub struct ExtSolFulfillBuy<'info> { )] pub buyside_sol_escrow_account: UncheckedAccount<'info>, /// CHECK: check_allowlists_for_mint_ext - pub asset_mint: InterfaceAccount<'info, Mint>, + pub asset_mint: Box>, #[account( mut, token::mint = asset_mint, @@ -153,6 +153,27 @@ pub fn handler<'info>( ) .map_err(|_| MMMErrorCode::NumericOverflow)?; + // withdraw sol from M2 first if shared escrow is enabled + if pool.using_shared_escrow() { + check_remaining_accounts_for_m2(remaining_accounts, &pool.owner.key())?; + + let amount: u64 = (total_price as i64 + maker_fee) as u64; + withdraw_m2( + pool, + ctx.bumps.pool, + buyside_sol_escrow_account, + index_ra!(remaining_accounts, 1), + system_program, + index_ra!(remaining_accounts, 0), + pool.owner, + amount, + )?; + pool.shared_escrow_count = pool + .shared_escrow_count + .checked_sub(args.asset_amount) + .ok_or(MMMErrorCode::NumericOverflow)?; + } + if pool.reinvest_fulfill_buy { if pool.using_shared_escrow() { return Err(MMMErrorCode::InvalidAccountState.into()); diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_sell.rs b/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_sell.rs index 7818b22..8db85e9 100644 --- a/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_sell.rs +++ b/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_sell.rs @@ -57,7 +57,7 @@ pub struct ExtSolFulfillSell<'info> { )] pub buyside_sol_escrow_account: AccountInfo<'info>, /// CHECK: check_allowlists_for_mint_ext - pub asset_mint: InterfaceAccount<'info, Mint>, + pub asset_mint: Box>, #[account( mut, associated_token::mint = asset_mint, diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_withdraw_sell.rs b/programs/mmm/src/instructions/ext_vanilla/ext_withdraw_sell.rs index 8b4086b..e1d7d6d 100644 --- a/programs/mmm/src/instructions/ext_vanilla/ext_withdraw_sell.rs +++ b/programs/mmm/src/instructions/ext_vanilla/ext_withdraw_sell.rs @@ -28,7 +28,7 @@ pub struct ExtWithdrawSell<'info> { bump )] pub pool: Box>, - pub asset_mint: InterfaceAccount<'info, Mint>, + pub asset_mint: Box>, #[account( init_if_needed, payer = owner, From 4e598773449c7272a23c376a9ccb10aa4693259e Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Thu, 14 Mar 2024 22:37:41 -0700 Subject: [PATCH 16/24] address comment --- .../ext_vanilla/ext_fulfill_buy.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs b/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs index 9afc4db..0d57ea3 100644 --- a/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs +++ b/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs @@ -59,6 +59,10 @@ pub struct ExtSolFulfillBuy<'info> { )] pub buyside_sol_escrow_account: UncheckedAccount<'info>, /// CHECK: check_allowlists_for_mint_ext + #[account( + mint::token_program = token_program, + constraint = asset_mint.supply == 1 && asset_mint.decimals == 0 @ MMMErrorCode::InvalidTokenMint, + )] pub asset_mint: Box>, #[account( mut, @@ -95,9 +99,9 @@ pub struct ExtSolFulfillBuy<'info> { // Branch: using shared escrow accounts // 0: m2_program // 1: shared_escrow_account - // 2+: creator accounts + // 2+: transfer hook accounts // Branch: not using shared escrow accounts - // 0+: creator accounts + // 0+: transfer hook accounts } pub fn handler<'info>( @@ -154,7 +158,7 @@ pub fn handler<'info>( .map_err(|_| MMMErrorCode::NumericOverflow)?; // withdraw sol from M2 first if shared escrow is enabled - if pool.using_shared_escrow() { + let remaining_account_without_m2 = if pool.using_shared_escrow() { check_remaining_accounts_for_m2(remaining_accounts, &pool.owner.key())?; let amount: u64 = (total_price as i64 + maker_fee) as u64; @@ -172,7 +176,10 @@ pub fn handler<'info>( .shared_escrow_count .checked_sub(args.asset_amount) .ok_or(MMMErrorCode::NumericOverflow)?; - } + &remaining_accounts[2..] + } else { + remaining_accounts + }; if pool.reinvest_fulfill_buy { if pool.using_shared_escrow() { @@ -198,7 +205,7 @@ pub fn handler<'info>( asset_mint.to_account_info(), sellside_escrow_token_account.to_account_info(), payer.to_account_info(), - remaining_accounts, + remaining_account_without_m2, args.asset_amount, 0, // decimals &[], // seeds @@ -235,7 +242,7 @@ pub fn handler<'info>( asset_mint.to_account_info(), owner_token_account.to_account_info(), payer.to_account_info(), - remaining_accounts, + remaining_account_without_m2, args.asset_amount, 0, // decimals &[], // seeds From 68a31ce7f1c62913e20aebf2e57a5cc942b49925 Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Sat, 16 Mar 2024 16:51:03 -0700 Subject: [PATCH 17/24] add test for shared escrow to trigger pool close/remain_open --- tests/mmm-ext-fulfill.spec.ts | 327 ++++++++++++++++++++++++++++++++- tests/mmm-ext-withdraw.spec.ts | 33 ++-- tests/utils/mmm.ts | 25 ++- 3 files changed, 358 insertions(+), 27 deletions(-) diff --git a/tests/mmm-ext-fulfill.spec.ts b/tests/mmm-ext-fulfill.spec.ts index 51cbb7a..ccdd335 100644 --- a/tests/mmm-ext-fulfill.spec.ts +++ b/tests/mmm-ext-fulfill.spec.ts @@ -19,6 +19,8 @@ import { MMMProgramID, CurveKind, getSolFulfillBuyPrices, + getM2BuyerSharedEscrow, + M2_PROGRAM, } from '../sdk/src'; import { IMMUTABLE_OWNER_EXTENSION_LAMPORTS, @@ -28,7 +30,7 @@ import { airdrop, assertIsBetween, assertTx, - createPoolWithExampleExtDeposits, + createPoolWithExampleT22ExtDeposits, createTestMintAndTokenT22VanillaExt, getSellStatePDARent, getTokenAccount2022, @@ -115,7 +117,7 @@ describe('mmm-ext-fulfill', () => { describe('ext_fulfill_sell', () => { it('Sellside only', async () => { const { mint, poolData, poolAta, sellState, solEscrowKey } = - await createPoolWithExampleExtDeposits( + await createPoolWithExampleT22ExtDeposits( program, connection, wallet.payer, @@ -232,7 +234,7 @@ describe('mmm-ext-fulfill', () => { const seller = Keypair.generate(); await airdrop(connection, seller.publicKey, 50); const { poolData, solEscrowKey, groupAddress } = - await createPoolWithExampleExtDeposits( + await createPoolWithExampleT22ExtDeposits( program, connection, wallet.payer, @@ -369,6 +371,323 @@ describe('mmm-ext-fulfill', () => { // do not reinvest so sell side asset amount should be 0 assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); }); + + it('Buyside only with shared escrow but pool open', async () => { + const seller = Keypair.generate(); + const buyerSharedEscrow = getM2BuyerSharedEscrow(wallet.publicKey).key; + await Promise.all([ + airdrop(connection, seller.publicKey, 50), + airdrop(connection, buyerSharedEscrow, 50), + ]); + const { poolData, solEscrowKey, groupAddress } = + await createPoolWithExampleT22ExtDeposits( + program, + connection, + wallet.payer, + 'buy', // side + { + owner: wallet.publicKey, + cosigner, + curveType: CurveKind.exp, + curveDelta: new anchor.BN(125), // 125 bp + expiry: new anchor.BN(0), + spotPrice: new anchor.BN(0.5 * LAMPORTS_PER_SOL), + referralBp: 200, + reinvestFulfillBuy: false, + reinvestFulfillSell: false, + }, + true, // sharedEscrow + 2, // sharedEscrowCount, one more so pool will not be closed + ); + const { + mint: extraMint, + recipientTokenAccount: extraRecipientTokenAccount, + } = await createTestMintAndTokenT22VanillaExt( + connection, + wallet.payer, + seller.publicKey, + groupAddress, + ); + + const ownerExtraNftAtaAddress = await getAssociatedTokenAddress( + extraMint, + wallet.publicKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + const { key: extraNftSellState } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + extraMint, + ); + const extraPoolAta = await getAssociatedTokenAddress( + extraMint, + poolData.poolKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + let [ + initReferralBalance, + initSellerBalance, + initWalletBalance, + initBuyerSharedEscrowBalance, + ] = await Promise.all([ + connection.getBalance(poolData.referral.publicKey), + connection.getBalance(seller.publicKey), + connection.getBalance(wallet.publicKey), + connection.getBalance(buyerSharedEscrow), + ]); + + const expectedTotalPrice = 0.5; + const tx = await program.methods + .extFulfillBuy({ + assetAmount: new anchor.BN(1), + minPaymentAmount: new anchor.BN( + expectedTotalPrice * LAMPORTS_PER_SOL, + ), + allowlistAux: '', + takerFeeBp: 0, + makerFeeBp: 150, + }) + .accountsStrict({ + payer: seller.publicKey, + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + referral: poolData.referral.publicKey, + pool: poolData.poolKey, + buysideSolEscrowAccount: solEscrowKey, + assetMint: extraMint, + payerAssetAccount: extraRecipientTokenAccount, + sellsideEscrowTokenAccount: extraPoolAta, + ownerTokenAccount: ownerExtraNftAtaAddress, + allowlistAuxAccount: SystemProgram.programId, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + rent: SYSVAR_RENT_PUBKEY, + sellState: extraNftSellState, + }) + .remainingAccounts([ + { + pubkey: M2_PROGRAM, + isWritable: false, + isSigner: false, + }, + { + pubkey: buyerSharedEscrow, + isWritable: true, + isSigner: false, + }, + ]) + .transaction(); + + const blockhashData = await connection.getLatestBlockhash(); + tx.feePayer = seller.publicKey; + tx.recentBlockhash = blockhashData.blockhash; + tx.partialSign(cosigner, seller); + + await sendAndAssertTx(connection, tx, blockhashData, false); + + const expectedMakerFees = expectedTotalPrice * LAMPORTS_PER_SOL * 0.015; + const expectedReferralFees = expectedMakerFees; + const [ + sellerBalance, + referralBalance, + poolAtaBalance, + afterWalletBalance, + poolEscrowBalance, + afterSharedEscrowBalance, + ] = await Promise.all([ + connection.getBalance(seller.publicKey), + connection.getBalance(poolData.referral.publicKey), + connection.getBalance(extraPoolAta), + connection.getBalance(wallet.publicKey), + connection.getBalance(solEscrowKey), + connection.getBalance(buyerSharedEscrow), + ]); + + assert.equal( + sellerBalance, + initSellerBalance + + expectedTotalPrice * LAMPORTS_PER_SOL - + SIGNATURE_FEE_LAMPORTS * 2, + ); + assert.equal(referralBalance, initReferralBalance + expectedReferralFees); + assert.equal(poolAtaBalance, 0); + assert.equal(poolEscrowBalance, 0); // because it's shared escrow, so the pool escrow is empty + assert.equal(afterWalletBalance, initWalletBalance); + assert.notEqual(afterSharedEscrowBalance, 0); + assert.equal( + initBuyerSharedEscrowBalance - afterSharedEscrowBalance, + expectedTotalPrice * LAMPORTS_PER_SOL + expectedMakerFees, + ); + + const poolAccountInfo = await program.account.pool.fetch( + poolData.poolKey, + ); + assertIsBetween( + poolAccountInfo.spotPrice.toNumber(), + (expectedTotalPrice * LAMPORTS_PER_SOL) / 1.0125, + PRICE_ERROR_RANGE, + ); + // do not reinvest so sell side asset amount should be 0 + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); + }); + + it('Buyside only with shared escrow and close the pool', async () => { + const seller = Keypair.generate(); + const buyerSharedEscrow = getM2BuyerSharedEscrow(wallet.publicKey).key; + await Promise.all([ + airdrop(connection, seller.publicKey, 50), + airdrop(connection, buyerSharedEscrow, 50), + ]); + const { poolData, solEscrowKey, groupAddress } = + await createPoolWithExampleT22ExtDeposits( + program, + connection, + wallet.payer, + 'buy', // side + { + owner: wallet.publicKey, + cosigner, + curveType: CurveKind.exp, + curveDelta: new anchor.BN(125), // 125 bp + expiry: new anchor.BN(0), + spotPrice: new anchor.BN(0.5 * LAMPORTS_PER_SOL), + referralBp: 200, + reinvestFulfillBuy: false, + reinvestFulfillSell: false, + }, + true, // sharedEscrow + 1, // sharedEscrowCount, only one so pool will be closed + ); + const { + mint: extraMint, + recipientTokenAccount: extraRecipientTokenAccount, + } = await createTestMintAndTokenT22VanillaExt( + connection, + wallet.payer, + seller.publicKey, + groupAddress, + ); + + const ownerExtraNftAtaAddress = await getAssociatedTokenAddress( + extraMint, + wallet.publicKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + const { key: extraNftSellState } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + extraMint, + ); + const extraPoolAta = await getAssociatedTokenAddress( + extraMint, + poolData.poolKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + let [ + initReferralBalance, + initSellerBalance, + initBuyerSharedEscrowBalance, + ] = await Promise.all([ + connection.getBalance(poolData.referral.publicKey), + connection.getBalance(seller.publicKey), + connection.getBalance(buyerSharedEscrow), + ]); + + const expectedTotalPrice = 0.5; + const tx = await program.methods + .extFulfillBuy({ + assetAmount: new anchor.BN(1), + minPaymentAmount: new anchor.BN( + expectedTotalPrice * LAMPORTS_PER_SOL, + ), + allowlistAux: '', + takerFeeBp: 0, + makerFeeBp: 150, + }) + .accountsStrict({ + payer: seller.publicKey, + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + referral: poolData.referral.publicKey, + pool: poolData.poolKey, + buysideSolEscrowAccount: solEscrowKey, + assetMint: extraMint, + payerAssetAccount: extraRecipientTokenAccount, + sellsideEscrowTokenAccount: extraPoolAta, + ownerTokenAccount: ownerExtraNftAtaAddress, + allowlistAuxAccount: SystemProgram.programId, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + rent: SYSVAR_RENT_PUBKEY, + sellState: extraNftSellState, + }) + .remainingAccounts([ + { + pubkey: M2_PROGRAM, + isWritable: false, + isSigner: false, + }, + { + pubkey: buyerSharedEscrow, + isWritable: true, + isSigner: false, + }, + ]) + .transaction(); + + const blockhashData = await connection.getLatestBlockhash(); + tx.feePayer = seller.publicKey; + tx.recentBlockhash = blockhashData.blockhash; + tx.partialSign(cosigner, seller); + + await sendAndAssertTx(connection, tx, blockhashData, false); + + const expectedMakerFees = expectedTotalPrice * LAMPORTS_PER_SOL * 0.015; + const expectedReferralFees = expectedMakerFees; + const [ + sellerBalance, + referralBalance, + poolAtaBalance, + poolEscrowBalance, + afterSharedEscrowBalance, + ] = await Promise.all([ + connection.getBalance(seller.publicKey), + connection.getBalance(poolData.referral.publicKey), + connection.getBalance(extraPoolAta), + connection.getBalance(solEscrowKey), + connection.getBalance(buyerSharedEscrow), + ]); + + assert.equal( + sellerBalance, + initSellerBalance + + expectedTotalPrice * LAMPORTS_PER_SOL - + SIGNATURE_FEE_LAMPORTS * 2, + ); + assert.equal(referralBalance, initReferralBalance + expectedReferralFees); + assert.equal(poolAtaBalance, 0); + assert.equal(poolEscrowBalance, 0); // because it's shared escrow, so the pool escrow is empty + assert.notEqual(afterSharedEscrowBalance, 0); + assert.equal( + initBuyerSharedEscrowBalance - afterSharedEscrowBalance, + expectedTotalPrice * LAMPORTS_PER_SOL + expectedMakerFees, + ); + + const poolAccountInfo = await program.account.pool.fetchNullable( + poolData.poolKey, + ); + assert.isNull(poolAccountInfo); + }); }); describe('ext_fulfillment', () => { @@ -376,7 +695,7 @@ describe('mmm-ext-fulfill', () => { const seller = Keypair.generate(); await airdrop(connection, seller.publicKey, 50); const { mint, poolData, poolAta, sellState, solEscrowKey, groupAddress } = - await createPoolWithExampleExtDeposits( + await createPoolWithExampleT22ExtDeposits( program, connection, wallet.payer, diff --git a/tests/mmm-ext-withdraw.spec.ts b/tests/mmm-ext-withdraw.spec.ts index a517884..7fa3ea9 100644 --- a/tests/mmm-ext-withdraw.spec.ts +++ b/tests/mmm-ext-withdraw.spec.ts @@ -8,7 +8,7 @@ import { assert } from 'chai'; import { Mmm, IDL, MMMProgramID } from '../sdk/src'; import { airdrop, - createPoolWithExampleExtDeposits, + createPoolWithExampleT22ExtDeposits, getTokenAccount2022, SIGNATURE_FEE_LAMPORTS, } from './utils'; @@ -31,24 +31,17 @@ describe('mmm-ext-withdraw', () => { }); it('Withdraw payment', async () => { - const { - mint, - recipientTokenAccount, - poolData, - poolAta, - sellState, - solEscrowKey, - groupAddress, - } = await createPoolWithExampleExtDeposits( - program, - connection, - wallet.payer, - 'buy', - { - owner: wallet.publicKey, - cosigner, - }, - ); + const { poolData, solEscrowKey } = + await createPoolWithExampleT22ExtDeposits( + program, + connection, + wallet.payer, + 'buy', + { + owner: wallet.publicKey, + cosigner, + }, + ); const initWalletBalance = await connection.getBalance(wallet.publicKey); const poolRent = await connection.getBalance(poolData.poolKey); @@ -86,7 +79,7 @@ describe('mmm-ext-withdraw', () => { sellState, solEscrowKey, groupAddress, - } = await createPoolWithExampleExtDeposits( + } = await createPoolWithExampleT22ExtDeposits( program, connection, wallet.payer, diff --git a/tests/utils/mmm.ts b/tests/utils/mmm.ts index 1274c1f..a425eb1 100644 --- a/tests/utils/mmm.ts +++ b/tests/utils/mmm.ts @@ -166,12 +166,15 @@ export const createPool = async ( return { referral, uuid, poolKey }; }; -export const createPoolWithExampleExtDeposits = async ( +// create pool for T22 extension +export const createPoolWithExampleT22ExtDeposits = async ( program: Program, connection: Connection, payer: Keypair, side: 'buy' | 'sell' | 'both' | 'none', poolArgs: Parameters[1], + sharedEscrow?: boolean, + sharedEscrowCount?: number, ) => { const { groupAddress } = await createTestGroupMintExt(connection, payer); const { mint, recipientTokenAccount } = @@ -215,7 +218,7 @@ export const createPoolWithExampleExtDeposits = async ( let poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); - if (side === 'both' || side === 'sell') { + if (!sharedEscrow && (side === 'both' || side === 'sell')) { await program.methods .extDepositSell({ assetAmount: new anchor.BN(1), @@ -242,7 +245,7 @@ export const createPoolWithExampleExtDeposits = async ( poolData.poolKey, ); - if (side === 'both' || side === 'buy') { + if (!sharedEscrow && (side === 'both' || side === 'buy')) { await program.methods .solDepositBuy({ paymentAmount: new anchor.BN(10 * LAMPORTS_PER_SOL) }) .accountsStrict({ @@ -256,6 +259,22 @@ export const createPoolWithExampleExtDeposits = async ( .rpc({ skipPreflight: true }); } + if (sharedEscrow) { + const sharedEscrowAccount = getM2BuyerSharedEscrow(poolArgs.owner).key; + await program.methods + .setSharedEscrow({ + sharedEscrowCount: new anchor.BN(sharedEscrowCount || 2), + }) + .accountsStrict({ + owner: poolArgs.owner, + cosigner: poolArgs.cosigner?.publicKey ?? poolArgs.owner, + pool: poolData.poolKey, + sharedEscrowAccount, + }) + .signers([...(poolArgs.cosigner ? [poolArgs.cosigner] : [])]) + .rpc(); + } + return { mint, recipientTokenAccount, From 936657dccbdbd386f7088e7c53c32ab6e8e87ee7 Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Sat, 16 Mar 2024 17:00:41 -0700 Subject: [PATCH 18/24] fix test fn import --- tests/mmm-ext-deposit.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/mmm-ext-deposit.spec.ts b/tests/mmm-ext-deposit.spec.ts index 6f42f69..e63443b 100644 --- a/tests/mmm-ext-deposit.spec.ts +++ b/tests/mmm-ext-deposit.spec.ts @@ -16,7 +16,7 @@ import { import { airdrop, createPool, - createPoolWithExampleExtDeposits, + createPoolWithExampleT22ExtDeposits, createTestGroupMemberMint, createTestGroupMintExt, createTestMintAndTokenT22VanillaExt, @@ -50,7 +50,7 @@ describe('mmm-ext-deposit', () => { poolAta, sellState, groupAddress, - } = await createPoolWithExampleExtDeposits( + } = await createPoolWithExampleT22ExtDeposits( program, connection, wallet.payer, @@ -190,7 +190,7 @@ describe('mmm-ext-deposit', () => { poolAta, sellState, groupAddress, - } = await createPoolWithExampleExtDeposits( + } = await createPoolWithExampleT22ExtDeposits( program, connection, wallet.payer, @@ -336,7 +336,7 @@ describe('mmm-ext-deposit', () => { it('failed to verify depositing with wrong allowlist aux', async () => { const { mint, recipientTokenAccount, poolData, poolAta, sellState } = - await createPoolWithExampleExtDeposits( + await createPoolWithExampleT22ExtDeposits( program, connection, wallet.payer, From 837c5949e0fcc96db6486646caccaf39cdf5c0bb Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Mon, 18 Mar 2024 12:54:52 -0700 Subject: [PATCH 19/24] address comments --- programs/mmm/src/errors.rs | 4 +- .../ext_vanilla/ext_withdraw_sell.rs | 29 ++++++++------ .../mmm/src/instructions/ext_vanilla/mod.rs | 8 ++-- ..._fulfill_buy.rs => sol_ext_fulfill_buy.rs} | 12 ++---- ...ulfill_sell.rs => sol_ext_fulfill_sell.rs} | 6 ++- programs/mmm/src/lib.rs | 4 +- programs/mmm/src/util.rs | 10 ++--- sdk/src/idl/mmm.ts | 8 ++-- tests/mmm-ext-deposit.spec.ts | 39 ++++++++++++++++++ tests/mmm-ext-fulfill.spec.ts | 33 ++++++++------- tests/mmm-ext-withdraw.spec.ts | 40 ------------------- tests/mmm-fulfill-exp.spec.ts | 2 +- tests/utils/mmm.ts | 21 +--------- 13 files changed, 101 insertions(+), 115 deletions(-) rename programs/mmm/src/instructions/ext_vanilla/{ext_fulfill_buy.rs => sol_ext_fulfill_buy.rs} (96%) rename programs/mmm/src/instructions/ext_vanilla/{ext_fulfill_sell.rs => sol_ext_fulfill_sell.rs} (97%) diff --git a/programs/mmm/src/errors.rs b/programs/mmm/src/errors.rs index eaa9429..56c1ac0 100644 --- a/programs/mmm/src/errors.rs +++ b/programs/mmm/src/errors.rs @@ -63,7 +63,7 @@ pub enum MMMErrorCode { #[msg("Invalid remaining accounts")] InvalidRemainingAccounts, // 0x178d #[msg("Invalid token metadata extensions")] - InValidTokenMetadataExtension, // 0x178e + InvalidTokenMetadataExtension, // 0x178e #[msg("Invalid token member extensions")] - InValidTokenMemberExtension, // 0x178f + InvalidTokenMemberExtension, // 0x178f } diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_withdraw_sell.rs b/programs/mmm/src/instructions/ext_vanilla/ext_withdraw_sell.rs index e1d7d6d..bbee05a 100644 --- a/programs/mmm/src/instructions/ext_vanilla/ext_withdraw_sell.rs +++ b/programs/mmm/src/instructions/ext_vanilla/ext_withdraw_sell.rs @@ -1,4 +1,4 @@ -use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize}; +use anchor_lang::{prelude::*, AnchorDeserialize}; use anchor_spl::{ associated_token::AssociatedToken, token_2022::{close_account, CloseAccount}, @@ -28,6 +28,10 @@ pub struct ExtWithdrawSell<'info> { bump )] pub pool: Box>, + #[account( + mint::token_program = token_program, + constraint = asset_mint.supply == 1 && asset_mint.decimals == 0 @ MMMErrorCode::InvalidTokenMint, + )] pub asset_mint: Box>, #[account( init_if_needed, @@ -77,6 +81,15 @@ pub fn handler(ctx: Context, args: WithdrawSellArgs) -> Result< let pool = &mut ctx.accounts.pool; let sell_state = &mut ctx.accounts.sell_state; let asset_mint = &ctx.accounts.asset_mint; + + let pool_uuid_key = pool.uuid.key(); + let owner_key = owner.key(); + let pool_seeds: &[&[&[u8]]] = &[&[ + POOL_PREFIX.as_bytes(), + owner_key.as_ref(), + pool_uuid_key.as_ref(), + &[ctx.bumps.pool], + ]]; // Note that check_allowlists_for_mint_ext is optional for withdraw_sell // because sometimes the nft might be moved out of the collection @@ -91,12 +104,7 @@ pub fn handler(ctx: Context, args: WithdrawSellArgs) -> Result< &[], // additional_accounts args.asset_amount, 0, // decimals - &[&[ - POOL_PREFIX.as_bytes(), - owner.key().as_ref(), - pool.uuid.key().as_ref(), - &[ctx.bumps.pool], - ]], + pool_seeds, )?; // we can close the sellside_escrow_token_account if no amount left @@ -109,12 +117,7 @@ pub fn handler(ctx: Context, args: WithdrawSellArgs) -> Result< authority: pool.to_account_info(), }, // seeds should be the PDA of 'pool' - &[&[ - POOL_PREFIX.as_bytes(), - owner.key().as_ref(), - pool.uuid.key().as_ref(), - &[ctx.bumps.pool], - ]], + pool_seeds, ))?; } diff --git a/programs/mmm/src/instructions/ext_vanilla/mod.rs b/programs/mmm/src/instructions/ext_vanilla/mod.rs index cfc445e..132eb09 100644 --- a/programs/mmm/src/instructions/ext_vanilla/mod.rs +++ b/programs/mmm/src/instructions/ext_vanilla/mod.rs @@ -1,9 +1,9 @@ pub mod ext_deposit_sell; -pub mod ext_fulfill_buy; -pub mod ext_fulfill_sell; pub mod ext_withdraw_sell; +pub mod sol_ext_fulfill_buy; +pub mod sol_ext_fulfill_sell; pub use ext_deposit_sell::*; -pub use ext_fulfill_buy::*; -pub use ext_fulfill_sell::*; pub use ext_withdraw_sell::*; +pub use sol_ext_fulfill_buy::*; +pub use sol_ext_fulfill_sell::*; diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs b/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_buy.rs similarity index 96% rename from programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs rename to programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_buy.rs index 0d57ea3..9ee0a18 100644 --- a/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_buy.rs +++ b/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_buy.rs @@ -58,7 +58,6 @@ pub struct ExtSolFulfillBuy<'info> { bump, )] pub buyside_sol_escrow_account: UncheckedAccount<'info>, - /// CHECK: check_allowlists_for_mint_ext #[account( mint::token_program = token_program, constraint = asset_mint.supply == 1 && asset_mint.decimals == 0 @ MMMErrorCode::InvalidTokenMint, @@ -145,7 +144,6 @@ pub fn handler<'info>( 0, // buyside_creator_royalty_bp, ) }?; - let lp_fee = get_sol_lp_fee(pool, buyside_sol_escrow_account.lamports(), seller_receives)?; assert_valid_fees_bp(args.maker_fee_bp, args.taker_fee_bp)?; let maker_fee = get_sol_fee(seller_receives, args.maker_fee_bp)?; @@ -181,6 +179,8 @@ pub fn handler<'info>( remaining_accounts }; + let lp_fee = get_sol_lp_fee(pool, buyside_sol_escrow_account.lamports(), seller_receives)?; + if pool.reinvest_fulfill_buy { if pool.using_shared_escrow() { return Err(MMMErrorCode::InvalidAccountState.into()); @@ -197,8 +197,6 @@ pub fn handler<'info>( system_program.to_account_info(), rent.to_account_info(), )?; - let sellside_escrow_token_account = - ctx.accounts.sellside_escrow_token_account.to_account_info(); invoke_transfer_checked( token_program.key, payer_asset_account.to_account_info(), @@ -277,7 +275,6 @@ pub fn handler<'info>( &[ buyside_sol_escrow_account.to_account_info(), payer.to_account_info(), - system_program.to_account_info(), ], buyside_sol_escrow_account_seeds, )?; @@ -288,7 +285,6 @@ pub fn handler<'info>( &[ buyside_sol_escrow_account.to_account_info(), owner.to_account_info(), - system_program.to_account_info(), ], buyside_sol_escrow_account_seeds, )?; @@ -303,7 +299,6 @@ pub fn handler<'info>( &[ buyside_sol_escrow_account.to_account_info(), referral.to_account_info(), - system_program.to_account_info(), ], buyside_sol_escrow_account_seeds, )?; @@ -339,7 +334,6 @@ pub fn handler<'info>( &[ buyside_sol_escrow_account.to_account_info(), shared_escrow_account, - system_program.to_account_info(), ], buyside_sol_escrow_account_seeds, )?; @@ -354,7 +348,7 @@ pub fn handler<'info>( } pool.buyside_payment_amount = buyside_sol_escrow_account.lamports(); - log_pool("ext_post_sol_fulfill_buy", pool)?; + log_pool("post_ext_sol_fulfill_buy", pool)?; try_close_pool(pool, owner.to_account_info())?; msg!("{{\"lp_fee\":{},\"total_price\":{}}}", lp_fee, total_price,); diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_sell.rs b/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_sell.rs similarity index 97% rename from programs/mmm/src/instructions/ext_vanilla/ext_fulfill_sell.rs rename to programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_sell.rs index 8db85e9..f9b1e23 100644 --- a/programs/mmm/src/instructions/ext_vanilla/ext_fulfill_sell.rs +++ b/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_sell.rs @@ -57,6 +57,10 @@ pub struct ExtSolFulfillSell<'info> { )] pub buyside_sol_escrow_account: AccountInfo<'info>, /// CHECK: check_allowlists_for_mint_ext + #[account( + mint::token_program = token_program, + constraint = asset_mint.supply == 1 && asset_mint.decimals == 0 @ MMMErrorCode::InvalidTokenMint, + )] pub asset_mint: Box>, #[account( mut, @@ -229,7 +233,7 @@ pub fn handler<'info>( try_close_sell_state(sell_state, owner.to_account_info())?; pool.buyside_payment_amount = buyside_sol_escrow_account.lamports(); - log_pool("ext_post_sol_fulfill_sell", pool)?; + log_pool("post_ext_sol_fulfill_sell", pool)?; try_close_pool(pool, owner.to_account_info())?; msg!("{{\"lp_fee\":{},\"total_price\":{}}}", lp_fee, total_price); diff --git a/programs/mmm/src/lib.rs b/programs/mmm/src/lib.rs index 68e84e4..2dd7bff 100644 --- a/programs/mmm/src/lib.rs +++ b/programs/mmm/src/lib.rs @@ -136,14 +136,14 @@ pub mod mmm { ctx: Context<'_, '_, '_, 'info, ExtSolFulfillSell<'info>>, args: SolFulfillSellArgs, ) -> Result<()> { - instructions::ext_fulfill_sell::handler(ctx, args) + instructions::sol_ext_fulfill_sell::handler(ctx, args) } pub fn ext_fulfill_buy<'info>( ctx: Context<'_, '_, '_, 'info, ExtSolFulfillBuy<'info>>, args: SolFulfillBuyArgs, ) -> Result<()> { - instructions::ext_fulfill_buy::handler(ctx, args) + instructions::sol_ext_fulfill_buy::handler(ctx, args) } pub fn ext_withdraw_sell(ctx: Context, args: WithdrawSellArgs) -> Result<()> { diff --git a/programs/mmm/src/util.rs b/programs/mmm/src/util.rs index 1ca03c2..0c26abd 100644 --- a/programs/mmm/src/util.rs +++ b/programs/mmm/src/util.rs @@ -674,7 +674,7 @@ pub fn check_allowlists_for_mint_ext( // verify metadata extension if let Ok(metadata_ptr) = mint_deserialized.get_extension::() { if Option::::from(metadata_ptr.metadata_address) != Some(*token_mint.key) { - return Err(MMMErrorCode::InValidTokenMetadataExtension.into()); + return Err(MMMErrorCode::InvalidTokenMetadataExtension.into()); } } let parsed_metadata = mint_deserialized @@ -703,7 +703,7 @@ pub fn check_allowlists_for_mint_ext( if let Ok(group_member_ptr) = mint_deserialized.get_extension::() { if Some(*token_mint.key) != Option::::from(group_member_ptr.member_address) { msg!("group member pointer does not point to itself"); - return Err(MMMErrorCode::InValidTokenMemberExtension.into()); + return Err(MMMErrorCode::InvalidTokenMemberExtension.into()); } } @@ -755,13 +755,13 @@ pub fn assert_valid_group( ) -> Result> { if let Ok(group_member) = mint_deserialized.get_extension::() { // counter spoof check - if Some(group_member.mint) != Some(*token_mint.key) { + if group_member.mint != *token_mint.key { msg!("group member mint does not match the token mint"); - return Err(MMMErrorCode::InValidTokenMemberExtension.into()); + return Err(MMMErrorCode::InvalidTokenMemberExtension.into()); } return Ok(Some(group_member.group)); } - Err(MMMErrorCode::InValidTokenMemberExtension.into()) + Err(MMMErrorCode::InvalidTokenMemberExtension.into()) } pub struct PoolPriceInfo<'info> { diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index d1531e1..1c26e89 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -2560,12 +2560,12 @@ export type Mmm = { }, { "code": 6030, - "name": "InValidTokenMetadataExtension", + "name": "InvalidTokenMetadataExtension", "msg": "Invalid token metadata extensions" }, { "code": 6031, - "name": "InValidTokenMemberExtension", + "name": "InvalidTokenMemberExtension", "msg": "Invalid token member extensions" } ] @@ -5133,12 +5133,12 @@ export const IDL: Mmm = { }, { "code": 6030, - "name": "InValidTokenMetadataExtension", + "name": "InvalidTokenMetadataExtension", "msg": "Invalid token metadata extensions" }, { "code": 6031, - "name": "InValidTokenMemberExtension", + "name": "InvalidTokenMemberExtension", "msg": "Invalid token member extensions" } ] diff --git a/tests/mmm-ext-deposit.spec.ts b/tests/mmm-ext-deposit.spec.ts index e63443b..9040028 100644 --- a/tests/mmm-ext-deposit.spec.ts +++ b/tests/mmm-ext-deposit.spec.ts @@ -372,6 +372,45 @@ describe('mmm-ext-deposit', () => { } }); + it('failed to verify depositing nfts with empty metadata list', async () => { + const { mint, recipientTokenAccount, poolData, poolAta, sellState } = + await createPoolWithExampleT22ExtDeposits( + program, + connection, + wallet.payer, + 'none', + { + owner: wallet.publicKey, + cosigner, + allowlists: undefined, + }, + ); + + try { + await program.methods + .extDepositSell({ + assetAmount: new anchor.BN(1), + allowlistAux: '', + }) + .accountsStrict({ + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + pool: poolData.poolKey, + assetMint: mint, + assetTokenAccount: recipientTokenAccount, + sellsideEscrowTokenAccount: poolAta, + sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([cosigner]) + .rpc({ skipPreflight: true }); + } catch (err) { + assertProgramError(err, 'invalid allowlists'); + } + }); + it('failed to verify depositing nfts with external group member pointer', async () => { const { groupAddress } = await createTestGroupMintExt( connection, diff --git a/tests/mmm-ext-fulfill.spec.ts b/tests/mmm-ext-fulfill.spec.ts index ccdd335..e474a5a 100644 --- a/tests/mmm-ext-fulfill.spec.ts +++ b/tests/mmm-ext-fulfill.spec.ts @@ -95,18 +95,7 @@ describe('mmm-ext-fulfill', () => { tx.recentBlockhash = blockhashData.blockhash; tx.partialSign(cosigner, buyer); - const txId = await connection.sendRawTransaction(tx.serialize(), { - skipPreflight: true, - }); - const confirmedTx = await connection.confirmTransaction( - { - signature: txId, - blockhash: blockhashData.blockhash, - lastValidBlockHeight: blockhashData.lastValidBlockHeight, - }, - 'processed', - ); - assertTx(txId, confirmedTx); + await sendAndAssertTx(connection, tx, blockhashData, false); } beforeEach(async () => { @@ -337,11 +326,17 @@ describe('mmm-ext-fulfill', () => { referralBalance, afterWalletBalance, poolEscrowBalance, + ownerNftTokenAccount, ] = await Promise.all([ connection.getBalance(seller.publicKey), connection.getBalance(poolData.referral.publicKey), connection.getBalance(wallet.publicKey), connection.getBalance(solEscrowKey), + getTokenAccount2022( + connection, + ownerExtraNftAtaAddress, + TOKEN_2022_PROGRAM_ID, + ), ]); assertIsBetween( @@ -370,6 +365,7 @@ describe('mmm-ext-fulfill', () => { ); // do not reinvest so sell side asset amount should be 0 assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); + assert.equal(Number(ownerNftTokenAccount.amount), 1); }); it('Buyside only with shared escrow but pool open', async () => { @@ -788,13 +784,19 @@ describe('mmm-ext-fulfill', () => { let tokenAccountRent = (await getTokenAccountRent(connection)) + IMMUTABLE_OWNER_EXTENSION_LAMPORTS; - const sellStatePDARent = await getSellStatePDARent(connection); const expectedTxFees = SIGNATURE_FEE_LAMPORTS * 2; // cosigner + payer { - const [sellerBalance, referralBalance] = await Promise.all([ + const [ + sellerBalance, + referralBalance, + sellStatePDARent, + poolTokenAccount, + ] = await Promise.all([ connection.getBalance(seller.publicKey), connection.getBalance(poolData.referral.publicKey), + getSellStatePDARent(connection), + getTokenAccount2022(connection, extraPoolAta, TOKEN_2022_PROGRAM_ID), ]); assert.equal( @@ -808,6 +810,7 @@ describe('mmm-ext-fulfill', () => { referralBalance, initReferralBalance + expectedBuyPrices.takerFeePaid.toNumber(), ); + assert.equal(Number(poolTokenAccount.amount), 1); initReferralBalance = referralBalance; } @@ -887,7 +890,7 @@ describe('mmm-ext-fulfill', () => { expectedLpFees - expectedTakerFees - expectedTxFees - - tokenAccountRent, // no token account rent bc seller ata was closed and pool ata opened + tokenAccountRent, PRICE_ERROR_RANGE, ); assertIsBetween( diff --git a/tests/mmm-ext-withdraw.spec.ts b/tests/mmm-ext-withdraw.spec.ts index 7fa3ea9..e0d8758 100644 --- a/tests/mmm-ext-withdraw.spec.ts +++ b/tests/mmm-ext-withdraw.spec.ts @@ -30,46 +30,6 @@ describe('mmm-ext-withdraw', () => { await airdrop(connection, wallet.publicKey, 50); }); - it('Withdraw payment', async () => { - const { poolData, solEscrowKey } = - await createPoolWithExampleT22ExtDeposits( - program, - connection, - wallet.payer, - 'buy', - { - owner: wallet.publicKey, - cosigner, - }, - ); - - const initWalletBalance = await connection.getBalance(wallet.publicKey); - const poolRent = await connection.getBalance(poolData.poolKey); - await program.methods - .solWithdrawBuy({ - paymentAmount: new anchor.BN(100 * LAMPORTS_PER_SOL), - }) - .accountsStrict({ - owner: wallet.publicKey, - cosigner: cosigner.publicKey, - pool: poolData.poolKey, - buysideSolEscrowAccount: solEscrowKey, - systemProgram: SystemProgram.programId, - }) - .signers([cosigner]) - .rpc(); - - assert.equal(await connection.getBalance(poolData.poolKey), 0); - const walletBalance = await connection.getBalance(wallet.publicKey); - assert.equal( - walletBalance, - initWalletBalance + - 10 * LAMPORTS_PER_SOL + // amount initially deposited - poolRent - // pool rent - 2 * SIGNATURE_FEE_LAMPORTS, // signature fees - ); - }); - it('Withdraw assets', async () => { const { mint, diff --git a/tests/mmm-fulfill-exp.spec.ts b/tests/mmm-fulfill-exp.spec.ts index 028a63d..dcead34 100644 --- a/tests/mmm-fulfill-exp.spec.ts +++ b/tests/mmm-fulfill-exp.spec.ts @@ -1186,7 +1186,7 @@ describe('mmm-fulfill-exp', () => { expectedLpFees - expectedTakerFees - expectedTxFees - - tokenAccountRent, // no token account rent bc seller ata was closed and pool ata opened + tokenAccountRent, PRICE_ERROR_RANGE, ); assertIsBetween( diff --git a/tests/utils/mmm.ts b/tests/utils/mmm.ts index a425eb1..2180d99 100644 --- a/tests/utils/mmm.ts +++ b/tests/utils/mmm.ts @@ -3,11 +3,8 @@ import { PROGRAM_ID as OCP_PROGRAM_ID, } from '@magiceden-oss/open_creator_protocol'; import { - assertAccountExists, - createAmount, generateSigner, OptionOrNullable, - percentAmount, publicKey, some, PublicKey as UmiPublicKey, @@ -16,14 +13,7 @@ import { createSignerFromKeypair, } from '@metaplex-foundation/umi'; import { createUmi } from '@metaplex-foundation/umi-bundle-tests'; -import { - createNft, - createV1, - TokenStandard, - mplTokenMetadata, - fetchDigitalAsset, - verifyCollection, -} from '@metaplex-foundation/mpl-token-metadata'; +import { mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata'; import * as anchor from '@project-serum/anchor'; import { Program } from '@project-serum/anchor'; import { @@ -60,14 +50,13 @@ import { Mmm, } from '../../sdk/src'; import { - airdrop, fillAllowlists, getEmptyAllowLists, getKeypair, MIP1_COMPUTE_UNITS, OCP_COMPUTE_UNITS, } from './generic'; -import { createProgrammableNftMip1, createProgrammableNftUmi } from './mip1'; +import { createProgrammableNftUmi } from './mip1'; import { createTestGroupMintExt, createTestMintAndTokenT22VanillaExt, @@ -77,7 +66,6 @@ import { } from './nfts'; import { umiMintNfts, Nft, umiMintCollection } from './umiNfts'; import { createTestMintAndTokenOCP } from './ocp'; -import { assert } from 'chai'; const TOKEN_METADATA_PROGRAM_ID = new PublicKey( 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s', @@ -213,11 +201,6 @@ export const createPoolWithExampleT22ExtDeposits = async ( mint, ); - assert.equal(await connection.getBalance(poolAta), 0); - assert.equal(await connection.getBalance(sellState), 0); - let poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); - assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); - if (!sharedEscrow && (side === 'both' || side === 'sell')) { await program.methods .extDepositSell({ From 19c4df7d4d005f330337e056d4bcf58bfbfb43a8 Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Mon, 18 Mar 2024 14:08:26 -0700 Subject: [PATCH 20/24] update test --- .../mmm/src/instructions/ext_vanilla/ext_withdraw_sell.rs | 2 +- .../mmm/src/instructions/ext_vanilla/sol_ext_fulfill_buy.rs | 4 ++-- tests/mmm-ext-fulfill.spec.ts | 2 +- tests/mmm-ext-withdraw.spec.ts | 3 +-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_withdraw_sell.rs b/programs/mmm/src/instructions/ext_vanilla/ext_withdraw_sell.rs index bbee05a..e9f12d4 100644 --- a/programs/mmm/src/instructions/ext_vanilla/ext_withdraw_sell.rs +++ b/programs/mmm/src/instructions/ext_vanilla/ext_withdraw_sell.rs @@ -81,7 +81,7 @@ pub fn handler(ctx: Context, args: WithdrawSellArgs) -> Result< let pool = &mut ctx.accounts.pool; let sell_state = &mut ctx.accounts.sell_state; let asset_mint = &ctx.accounts.asset_mint; - + let pool_uuid_key = pool.uuid.key(); let owner_key = owner.key(); let pool_seeds: &[&[&[u8]]] = &[&[ diff --git a/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_buy.rs b/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_buy.rs index 9ee0a18..6330d24 100644 --- a/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_buy.rs +++ b/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_buy.rs @@ -155,6 +155,8 @@ pub fn handler<'info>( ) .map_err(|_| MMMErrorCode::NumericOverflow)?; + let lp_fee = get_sol_lp_fee(pool, buyside_sol_escrow_account.lamports(), seller_receives)?; + // withdraw sol from M2 first if shared escrow is enabled let remaining_account_without_m2 = if pool.using_shared_escrow() { check_remaining_accounts_for_m2(remaining_accounts, &pool.owner.key())?; @@ -179,8 +181,6 @@ pub fn handler<'info>( remaining_accounts }; - let lp_fee = get_sol_lp_fee(pool, buyside_sol_escrow_account.lamports(), seller_receives)?; - if pool.reinvest_fulfill_buy { if pool.using_shared_escrow() { return Err(MMMErrorCode::InvalidAccountState.into()); diff --git a/tests/mmm-ext-fulfill.spec.ts b/tests/mmm-ext-fulfill.spec.ts index e474a5a..280dc9a 100644 --- a/tests/mmm-ext-fulfill.spec.ts +++ b/tests/mmm-ext-fulfill.spec.ts @@ -29,7 +29,6 @@ import { SIGNATURE_FEE_LAMPORTS, airdrop, assertIsBetween, - assertTx, createPoolWithExampleT22ExtDeposits, createTestMintAndTokenT22VanillaExt, getSellStatePDARent, @@ -530,6 +529,7 @@ describe('mmm-ext-fulfill', () => { ); // do not reinvest so sell side asset amount should be 0 assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); + assert.equal(poolAccountInfo.lpFeeEarned.toNumber(), 0); }); it('Buyside only with shared escrow and close the pool', async () => { diff --git a/tests/mmm-ext-withdraw.spec.ts b/tests/mmm-ext-withdraw.spec.ts index e0d8758..3cd6b1b 100644 --- a/tests/mmm-ext-withdraw.spec.ts +++ b/tests/mmm-ext-withdraw.spec.ts @@ -3,14 +3,13 @@ import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, } from '@solana/spl-token'; -import { Keypair, LAMPORTS_PER_SOL, SystemProgram } from '@solana/web3.js'; +import { Keypair, SystemProgram } from '@solana/web3.js'; import { assert } from 'chai'; import { Mmm, IDL, MMMProgramID } from '../sdk/src'; import { airdrop, createPoolWithExampleT22ExtDeposits, getTokenAccount2022, - SIGNATURE_FEE_LAMPORTS, } from './utils'; describe('mmm-ext-withdraw', () => { From 2c6afec8738791c34b63ae75e9ffec5f4b88d8c0 Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Mon, 18 Mar 2024 14:20:06 -0700 Subject: [PATCH 21/24] add todo --- .../mmm/src/instructions/ext_vanilla/sol_ext_fulfill_buy.rs | 1 + programs/mmm/src/instructions/vanilla/sol_fulfill_buy.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_buy.rs b/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_buy.rs index 6330d24..cbf3170 100644 --- a/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_buy.rs +++ b/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_buy.rs @@ -155,6 +155,7 @@ pub fn handler<'info>( ) .map_err(|_| MMMErrorCode::NumericOverflow)?; + // TODO: update lp_fee when shared escrow for both side is enabled let lp_fee = get_sol_lp_fee(pool, buyside_sol_escrow_account.lamports(), seller_receives)?; // withdraw sol from M2 first if shared escrow is enabled diff --git a/programs/mmm/src/instructions/vanilla/sol_fulfill_buy.rs b/programs/mmm/src/instructions/vanilla/sol_fulfill_buy.rs index 38a2c20..d9326f5 100644 --- a/programs/mmm/src/instructions/vanilla/sol_fulfill_buy.rs +++ b/programs/mmm/src/instructions/vanilla/sol_fulfill_buy.rs @@ -164,6 +164,8 @@ pub fn handler<'info>( pool.buyside_creator_royalty_bp, ) }?; + + // TODO: update lp_fee when shared escrow for both side is enabled let lp_fee = get_sol_lp_fee(pool, buyside_sol_escrow_account.lamports(), seller_receives)?; assert_valid_fees_bp(args.maker_fee_bp, args.taker_fee_bp)?; From 7f067273f9de46cbbac971096d20bd2c3a730e4f Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Mon, 18 Mar 2024 15:16:05 -0700 Subject: [PATCH 22/24] change name --- programs/mmm/src/lib.rs | 4 ++-- sdk/src/idl/mmm.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/programs/mmm/src/lib.rs b/programs/mmm/src/lib.rs index 2dd7bff..7a8f890 100644 --- a/programs/mmm/src/lib.rs +++ b/programs/mmm/src/lib.rs @@ -132,14 +132,14 @@ pub mod mmm { instructions::ext_deposit_sell::handler(ctx, args) } - pub fn ext_fulfill_sell<'info>( + pub fn sol_ext_fulfill_sell<'info>( ctx: Context<'_, '_, '_, 'info, ExtSolFulfillSell<'info>>, args: SolFulfillSellArgs, ) -> Result<()> { instructions::sol_ext_fulfill_sell::handler(ctx, args) } - pub fn ext_fulfill_buy<'info>( + pub fn sol_ext_fulfill_buy<'info>( ctx: Context<'_, '_, '_, 'info, ExtSolFulfillBuy<'info>>, args: SolFulfillBuyArgs, ) -> Result<()> { diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index 1c26e89..89e04b2 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -1666,7 +1666,7 @@ export type Mmm = { ] }, { - "name": "extFulfillSell", + "name": "solExtFulfillSell", "accounts": [ { "name": "payer", @@ -1744,7 +1744,7 @@ export type Mmm = { ] }, { - "name": "extFulfillBuy", + "name": "solExtFulfillBuy", "accounts": [ { "name": "payer", @@ -4239,7 +4239,7 @@ export const IDL: Mmm = { ] }, { - "name": "extFulfillSell", + "name": "solExtFulfillSell", "accounts": [ { "name": "payer", @@ -4317,7 +4317,7 @@ export const IDL: Mmm = { ] }, { - "name": "extFulfillBuy", + "name": "solExtFulfillBuy", "accounts": [ { "name": "payer", From 0dec0b98a08a463e74bf04705bbb74327bf9b0a1 Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Mon, 18 Mar 2024 15:27:18 -0700 Subject: [PATCH 23/24] fix test --- tests/mmm-ext-fulfill.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/mmm-ext-fulfill.spec.ts b/tests/mmm-ext-fulfill.spec.ts index 280dc9a..b3fedb0 100644 --- a/tests/mmm-ext-fulfill.spec.ts +++ b/tests/mmm-ext-fulfill.spec.ts @@ -64,7 +64,7 @@ describe('mmm-ext-fulfill', () => { makerFeeBp: number, ) { const tx = await program.methods - .extFulfillSell({ + .solExtFulfillSell({ assetAmount: new anchor.BN(1), maxPaymentAmount: new anchor.BN(maxPaymentAmount), buysideCreatorRoyaltyBp: 0, @@ -282,7 +282,7 @@ describe('mmm-ext-fulfill', () => { const expectedTotalPrice = 0.5; const tx = await program.methods - .extFulfillBuy({ + .solExtFulfillBuy({ assetAmount: new anchor.BN(1), minPaymentAmount: new anchor.BN( expectedTotalPrice * LAMPORTS_PER_SOL, @@ -437,7 +437,7 @@ describe('mmm-ext-fulfill', () => { const expectedTotalPrice = 0.5; const tx = await program.methods - .extFulfillBuy({ + .solExtFulfillBuy({ assetAmount: new anchor.BN(1), minPaymentAmount: new anchor.BN( expectedTotalPrice * LAMPORTS_PER_SOL, @@ -600,7 +600,7 @@ describe('mmm-ext-fulfill', () => { const expectedTotalPrice = 0.5; const tx = await program.methods - .extFulfillBuy({ + .solExtFulfillBuy({ assetAmount: new anchor.BN(1), minPaymentAmount: new anchor.BN( expectedTotalPrice * LAMPORTS_PER_SOL, @@ -747,7 +747,7 @@ describe('mmm-ext-fulfill', () => { }); const tx = await program.methods - .extFulfillBuy({ + .solExtFulfillBuy({ assetAmount: new anchor.BN(1), minPaymentAmount: expectedBuyPrices.sellerReceives, allowlistAux: null, @@ -835,7 +835,7 @@ describe('mmm-ext-fulfill', () => { { const tx = await program.methods - .extFulfillSell({ + .solExtFulfillSell({ assetAmount: new anchor.BN(1), maxPaymentAmount: new anchor.BN( LAMPORTS_PER_SOL + expectedTakerFees + expectedLpFees, From cf7d5555108ea58b9d1c4ac7a3dae0c105437a66 Mon Sep 17 00:00:00 2001 From: Qiyao Qin Date: Mon, 18 Mar 2024 17:39:31 -0700 Subject: [PATCH 24/24] add comment for lp_fee_bp --- programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_buy.rs | 1 + programs/mmm/src/instructions/vanilla/sol_fulfill_buy.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_buy.rs b/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_buy.rs index cbf3170..12a6b03 100644 --- a/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_buy.rs +++ b/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_buy.rs @@ -135,6 +135,7 @@ pub fn handler<'info>( let (total_price, next_price) = get_sol_total_price_and_next_price(pool, args.asset_amount, true)?; + // TODO: update lp_fee_bp when shared escrow for both side is enabled let seller_receives = { let lp_fee_bp = get_lp_fee_bp(pool, buyside_sol_escrow_account.lamports()); get_buyside_seller_receives( diff --git a/programs/mmm/src/instructions/vanilla/sol_fulfill_buy.rs b/programs/mmm/src/instructions/vanilla/sol_fulfill_buy.rs index d9326f5..c9927e4 100644 --- a/programs/mmm/src/instructions/vanilla/sol_fulfill_buy.rs +++ b/programs/mmm/src/instructions/vanilla/sol_fulfill_buy.rs @@ -155,6 +155,7 @@ pub fn handler<'info>( let (total_price, next_price) = get_sol_total_price_and_next_price(pool, args.asset_amount, true)?; let metadata_royalty_bp = get_metadata_royalty_bp(total_price, &parsed_metadata, None); + // TODO: update lp_fee_bp when shared escrow for both side is enabled let seller_receives = { let lp_fee_bp = get_lp_fee_bp(pool, buyside_sol_escrow_account.lamports()); get_buyside_seller_receives(