From 57a3050a92001d2df8adb71cec3e4b562a566e3e Mon Sep 17 00:00:00 2001 From: madergaser Date: Tue, 6 Aug 2024 11:21:52 -0500 Subject: [PATCH] add support for payment proxies (#104) --- Anchor.toml | 8 + .../ext_vanilla/sol_ext_fulfill_buy.rs | 9 +- .../ext_vanilla/sol_ext_fulfill_sell.rs | 9 +- .../instructions/mip1/sol_mip1_fulfill_buy.rs | 9 +- .../mip1/sol_mip1_fulfill_sell.rs | 9 +- .../sol_mpl_core_fulfill_buy.rs | 9 +- .../sol_mpl_core_fulfill_sell.rs | 9 +- .../instructions/ocp/sol_ocp_fulfill_buy.rs | 9 +- .../instructions/ocp/sol_ocp_fulfill_sell.rs | 9 +- .../instructions/vanilla/sol_fulfill_buy.rs | 9 +- .../instructions/vanilla/sol_fulfill_sell.rs | 9 +- programs/mmm/src/lib.rs | 1 + programs/mmm/src/verify_referral.rs | 35 ++++ tests/deps/invalid_proxy.json | 14 ++ tests/deps/proxy.json | 14 ++ tests/mmm-admin.spec.ts | 8 +- tests/mmm-referral.spec.ts | 185 ++++++++++++++++++ 17 files changed, 321 insertions(+), 34 deletions(-) create mode 100644 programs/mmm/src/verify_referral.rs create mode 100644 tests/deps/invalid_proxy.json create mode 100644 tests/deps/proxy.json create mode 100644 tests/mmm-referral.spec.ts diff --git a/Anchor.toml b/Anchor.toml index 9322631..fc8bcce 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -39,6 +39,14 @@ address = "CZ1rQoAHSqWBoAEfqGsiLhgbM59dDrCWk3rnG5FXaoRV" # libreplex royalty enf [[test.validator.clone]] address = "CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d" # metaplex core program +[[test.validator.account]] +address = "9V5HWD1ap6mCDMhBoXU5SVcZZn9ihqJtoMQZsw5MTnoD" # example payment proxy +filename = './tests/deps/proxy.json' + +[[test.validator.account]] +address = "AJtUEMcZv9DDG4EVd8ugG3duAnCmmmVa6xCEUV7FqFFd" # bad payment proxy, owned by invalid program +filename = "./tests/deps/invalid_proxy.json" + [[test.genesis]] address = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" program = "./tests/deps/spl_token_2022.so" 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 d2c2572..23e76e5 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 @@ -23,6 +23,7 @@ use crate::{ 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, }, + verify_referral::verify_referral, SolFulfillBuyArgs, }; @@ -41,14 +42,16 @@ pub struct ExtSolFulfillBuy<'info> { pub owner: UncheckedAccount<'info>, #[account(constraint = owner.key() != cosigner.key() @ MMMErrorCode::InvalidCosigner)] pub cosigner: Signer<'info>, - #[account(mut)] - /// CHECK: we will check that the referral matches the pool's referral + #[account( + mut, + constraint = verify_referral(&pool, &referral) @ MMMErrorCode::InvalidReferral, + )] + /// CHECK: use verify_referral to check the referral account 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, diff --git a/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_sell.rs b/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_sell.rs index 4636dc8..904f1dc 100644 --- a/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_sell.rs +++ b/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_sell.rs @@ -18,6 +18,7 @@ use crate::{ }, state::{Pool, SellState}, util::check_allowlists_for_mint_ext, + verify_referral::verify_referral, SolFulfillSellArgs, }; @@ -36,14 +37,16 @@ pub struct ExtSolFulfillSell<'info> { pub owner: UncheckedAccount<'info>, #[account(constraint = owner.key() != cosigner.key() @ MMMErrorCode::InvalidCosigner)] pub cosigner: Signer<'info>, - /// CHECK: we will check that the referral matches the pool's referral - #[account(mut)] + #[account( + mut, + constraint = verify_referral(&pool, &referral) @ MMMErrorCode::InvalidReferral, + )] + /// CHECK: use verify_referral to check the referral account 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, diff --git a/programs/mmm/src/instructions/mip1/sol_mip1_fulfill_buy.rs b/programs/mmm/src/instructions/mip1/sol_mip1_fulfill_buy.rs index 2b0347a..5294098 100644 --- a/programs/mmm/src/instructions/mip1/sol_mip1_fulfill_buy.rs +++ b/programs/mmm/src/instructions/mip1/sol_mip1_fulfill_buy.rs @@ -24,6 +24,7 @@ use crate::{ get_sol_lp_fee, get_sol_total_price_and_next_price, log_pool, pay_creator_fees_in_sol, try_close_escrow, try_close_pool, try_close_sell_state, }, + verify_referral::verify_referral, }; // FulfillBuy means a seller wants to sell NFT/SFT into the pool @@ -40,14 +41,16 @@ pub struct SolMip1FulfillBuy<'info> { pub owner: UncheckedAccount<'info>, #[account(constraint = owner.key() != cosigner.key() @ MMMErrorCode::InvalidCosigner)] pub cosigner: Signer<'info>, - #[account(mut)] - /// CHECK: we will check that the referral matches the pool's referral + #[account( + mut, + constraint = verify_referral(&pool, &referral) @ MMMErrorCode::InvalidReferral, + )] + /// CHECK: use verify_referral to check the referral account 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, diff --git a/programs/mmm/src/instructions/mip1/sol_mip1_fulfill_sell.rs b/programs/mmm/src/instructions/mip1/sol_mip1_fulfill_sell.rs index 0db15bb..b99594d 100644 --- a/programs/mmm/src/instructions/mip1/sol_mip1_fulfill_sell.rs +++ b/programs/mmm/src/instructions/mip1/sol_mip1_fulfill_sell.rs @@ -18,6 +18,7 @@ use crate::{ 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, try_close_pool, try_close_sell_state, }, + verify_referral::verify_referral, }; #[derive(AnchorSerialize, AnchorDeserialize)] @@ -43,14 +44,16 @@ pub struct SolMip1FulfillSell<'info> { pub owner: UncheckedAccount<'info>, #[account(constraint = owner.key() != cosigner.key() @ MMMErrorCode::InvalidCosigner)] pub cosigner: Signer<'info>, - /// CHECK: we will check that the referral matches the pool's referral - #[account(mut)] + #[account( + mut, + constraint = verify_referral(&pool, &referral) @ MMMErrorCode::InvalidReferral, + )] + /// CHECK: use verify_referral to check the referral account 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, diff --git a/programs/mmm/src/instructions/mpl_core_asset/sol_mpl_core_fulfill_buy.rs b/programs/mmm/src/instructions/mpl_core_asset/sol_mpl_core_fulfill_buy.rs index e38f3f7..d14580c 100644 --- a/programs/mmm/src/instructions/mpl_core_asset/sol_mpl_core_fulfill_buy.rs +++ b/programs/mmm/src/instructions/mpl_core_asset/sol_mpl_core_fulfill_buy.rs @@ -18,6 +18,7 @@ use crate::{ get_sol_fee, get_sol_lp_fee, get_sol_total_price_and_next_price, log_pool, pay_creator_fees_in_sol, try_close_escrow, try_close_pool, try_close_sell_state, }, + verify_referral::verify_referral, AssetInterface, IndexableAsset, }; @@ -40,14 +41,16 @@ pub struct SolMplCoreFulfillBuy<'info> { pub owner: UncheckedAccount<'info>, #[account(constraint = owner.key() != cosigner.key() @ MMMErrorCode::InvalidCosigner)] pub cosigner: Signer<'info>, - #[account(mut)] - /// CHECK: we will check that the referral matches the pool's referral + #[account( + mut, + constraint = verify_referral(&pool, &referral) @ MMMErrorCode::InvalidReferral, + )] + /// CHECK: use verify_referral to check the referral account 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, diff --git a/programs/mmm/src/instructions/mpl_core_asset/sol_mpl_core_fulfill_sell.rs b/programs/mmm/src/instructions/mpl_core_asset/sol_mpl_core_fulfill_sell.rs index c04e6d8..bad3563 100644 --- a/programs/mmm/src/instructions/mpl_core_asset/sol_mpl_core_fulfill_sell.rs +++ b/programs/mmm/src/instructions/mpl_core_asset/sol_mpl_core_fulfill_sell.rs @@ -17,6 +17,7 @@ use crate::{ get_metadata_royalty_bp, log_pool, pay_creator_fees_in_sol, try_close_pool, try_close_sell_state, }, + verify_referral::verify_referral, AssetInterface, IndexableAsset, }; @@ -40,14 +41,16 @@ pub struct SolMplCoreFulfillSell<'info> { pub owner: UncheckedAccount<'info>, #[account(constraint = owner.key() != cosigner.key() @ MMMErrorCode::InvalidCosigner)] pub cosigner: Signer<'info>, - /// CHECK: we will check that the referral matches the pool's referral - #[account(mut)] + #[account( + mut, + constraint = verify_referral(&pool, &referral) @ MMMErrorCode::InvalidReferral, + )] + /// CHECK: use verify_referral to check the referral account 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, diff --git a/programs/mmm/src/instructions/ocp/sol_ocp_fulfill_buy.rs b/programs/mmm/src/instructions/ocp/sol_ocp_fulfill_buy.rs index 9b61a12..fc8f975 100644 --- a/programs/mmm/src/instructions/ocp/sol_ocp_fulfill_buy.rs +++ b/programs/mmm/src/instructions/ocp/sol_ocp_fulfill_buy.rs @@ -22,6 +22,7 @@ use crate::{ get_sol_total_price_and_next_price, log_pool, pay_creator_fees_in_sol, try_close_escrow, try_close_pool, try_close_sell_state, }, + verify_referral::verify_referral, }; // FulfillBuy means a seller wants to sell NFT/SFT into the pool @@ -38,14 +39,16 @@ pub struct SolOcpFulfillBuy<'info> { pub owner: UncheckedAccount<'info>, #[account(constraint = owner.key() != cosigner.key() @ MMMErrorCode::InvalidCosigner)] pub cosigner: Signer<'info>, - #[account(mut)] - /// CHECK: we will check that the referral matches the pool's referral + #[account( + mut, + constraint = verify_referral(&pool, &referral) @ MMMErrorCode::InvalidReferral, + )] + /// CHECK: use verify_referral to check the referral account 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, diff --git a/programs/mmm/src/instructions/ocp/sol_ocp_fulfill_sell.rs b/programs/mmm/src/instructions/ocp/sol_ocp_fulfill_sell.rs index 3465a3c..6c19fc3 100644 --- a/programs/mmm/src/instructions/ocp/sol_ocp_fulfill_sell.rs +++ b/programs/mmm/src/instructions/ocp/sol_ocp_fulfill_sell.rs @@ -17,6 +17,7 @@ use crate::{ get_sol_lp_fee, get_sol_total_price_and_next_price, log_pool, pay_creator_fees_in_sol, try_close_pool, try_close_sell_state, }, + verify_referral::verify_referral, }; #[derive(AnchorSerialize, AnchorDeserialize)] @@ -42,14 +43,16 @@ pub struct SolOcpFulfillSell<'info> { pub owner: UncheckedAccount<'info>, #[account(constraint = owner.key() != cosigner.key() @ MMMErrorCode::InvalidCosigner)] pub cosigner: Signer<'info>, - /// CHECK: we will check that the referral matches the pool's referral - #[account(mut)] + #[account( + mut, + constraint = verify_referral(&pool, &referral) @ MMMErrorCode::InvalidReferral, + )] + /// CHECK: use verify_referral to check the referral account 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, diff --git a/programs/mmm/src/instructions/vanilla/sol_fulfill_buy.rs b/programs/mmm/src/instructions/vanilla/sol_fulfill_buy.rs index 2e0ec96..b4b8d54 100644 --- a/programs/mmm/src/instructions/vanilla/sol_fulfill_buy.rs +++ b/programs/mmm/src/instructions/vanilla/sol_fulfill_buy.rs @@ -18,6 +18,7 @@ use crate::{ get_sol_total_price_and_next_price, log_pool, pay_creator_fees_in_sol, try_close_escrow, try_close_pool, try_close_sell_state, }, + verify_referral::verify_referral, }; #[derive(AnchorSerialize, AnchorDeserialize)] @@ -43,14 +44,16 @@ pub struct SolFulfillBuy<'info> { pub owner: UncheckedAccount<'info>, #[account(constraint = owner.key() != cosigner.key() @ MMMErrorCode::InvalidCosigner)] pub cosigner: Signer<'info>, - #[account(mut)] - /// CHECK: we will check that the referral matches the pool's referral + #[account( + mut, + constraint = verify_referral(&pool, &referral) @ MMMErrorCode::InvalidReferral, + )] + /// CHECK: use verify_referral to check the referral account 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, diff --git a/programs/mmm/src/instructions/vanilla/sol_fulfill_sell.rs b/programs/mmm/src/instructions/vanilla/sol_fulfill_sell.rs index ced157e..05a30a8 100644 --- a/programs/mmm/src/instructions/vanilla/sol_fulfill_sell.rs +++ b/programs/mmm/src/instructions/vanilla/sol_fulfill_sell.rs @@ -14,6 +14,7 @@ use crate::{ check_allowlists_for_mint, get_metadata_royalty_bp, log_pool, pay_creator_fees_in_sol, try_close_pool, try_close_sell_state, }, + verify_referral::verify_referral, }; #[derive(AnchorSerialize, AnchorDeserialize)] @@ -40,14 +41,16 @@ pub struct SolFulfillSell<'info> { pub owner: UncheckedAccount<'info>, #[account(constraint = owner.key() != cosigner.key() @ MMMErrorCode::InvalidCosigner)] pub cosigner: Signer<'info>, - /// CHECK: we will check that the referral matches the pool's referral - #[account(mut)] + #[account( + mut, + constraint = verify_referral(&pool, &referral) @ MMMErrorCode::InvalidReferral, + )] + /// CHECK: use verify_referral to check the referral account 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, diff --git a/programs/mmm/src/lib.rs b/programs/mmm/src/lib.rs index 50e2c76..f750238 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; +pub mod verify_referral; use instructions::*; diff --git a/programs/mmm/src/verify_referral.rs b/programs/mmm/src/verify_referral.rs new file mode 100644 index 0000000..c78f419 --- /dev/null +++ b/programs/mmm/src/verify_referral.rs @@ -0,0 +1,35 @@ +use anchor_lang::prelude::{AccountInfo, Pubkey}; +use solana_program::pubkey; + +use crate::state::Pool; + +const PAYMENT_PROXY_PROGRAM_ID: Pubkey = pubkey!("mpxdRTRiAzvxz8dgW6LQYzDATtKQBx2f1VJ6qsU28hn"); +const PAYMENT_PROXY_DISCRIMINATOR: [u8; 8] = [0xee, 0x4a, 0x13, 0x79, 0x5e, 0x99, 0xac, 0x48]; +const PAYMENT_PROXY_MIN_LEN: usize = 512; + +pub fn verify_referral(pool: &Pool, referral: &AccountInfo<'_>) -> bool { + // Check if the referral account is the one defined in the pool + if referral.key == &pool.referral { + // early return true since the referral is the one expected + return true; + } + + // From now on we assume that the referral account is a payment proxy account with the referral + // as the authority. + + // Check if the account is owned by expected program and that it has expected data length + if referral.owner != &PAYMENT_PROXY_PROGRAM_ID || referral.data_len() < PAYMENT_PROXY_MIN_LEN { + return false; + } + + let data = referral.try_borrow_data().unwrap(); + // Check if proxy account has correct discriminator + if data[0..8] != PAYMENT_PROXY_DISCRIMINATOR { + return false; + } + // Check if proxy account has correct authority + if &data[8..40] != pool.referral.as_ref() { + return false; + } + true +} diff --git a/tests/deps/invalid_proxy.json b/tests/deps/invalid_proxy.json new file mode 100644 index 0000000..1acfa1f --- /dev/null +++ b/tests/deps/invalid_proxy.json @@ -0,0 +1,14 @@ +{ + "pubkey": "AJtUEMcZv9DDG4EVd8ugG3duAnCmmmVa6xCEUV7FqFFd", + "account": { + "lamports": 4454400, + "data": [ + "7koTeV6ZrEiQg0IPEp/Uuttukll5Ybq4Tq0Fyv/p3NtUND3zmVFT1QIAAABpbwAhsbfNghq8fn9TdloaMFqlw+SMn5DN9AMUTEH879PtpgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "base64" + ], + "owner": "CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d", + "executable": false, + "rentEpoch": 18446744073709551615, + "space": 512 + } +} diff --git a/tests/deps/proxy.json b/tests/deps/proxy.json new file mode 100644 index 0000000..9175374 --- /dev/null +++ b/tests/deps/proxy.json @@ -0,0 +1,14 @@ +{ + "pubkey": "9V5HWD1ap6mCDMhBoXU5SVcZZn9ihqJtoMQZsw5MTnoD", + "account": { + "lamports": 4454400, + "data": [ + "7koTeV6ZrEiQg0IPEp/Uuttukll5Ybq4Tq0Fyv/p3NtUND3zmVFT1QIAAABpbwAhsbfNghq8fn9TdloaMFqlw+SMn5DN9AMUTEH879PtpgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "base64" + ], + "owner": "mpxdRTRiAzvxz8dgW6LQYzDATtKQBx2f1VJ6qsU28hn", + "executable": false, + "rentEpoch": 18446744073709551615, + "space": 512 + } +} diff --git a/tests/mmm-admin.spec.ts b/tests/mmm-admin.spec.ts index c45458d..e218889 100644 --- a/tests/mmm-admin.spec.ts +++ b/tests/mmm-admin.spec.ts @@ -324,7 +324,6 @@ describe('mmm-admin', () => { describe('Can update allowlists', () => { it('happy path', async () => { // Ensure cosigner is the only signer of the transaction. - const wallet = new anchor.Wallet(cosigner); const provider = new anchor.AnchorProvider(connection, wallet, { commitment: 'processed', }); @@ -335,8 +334,6 @@ describe('mmm-admin', () => { provider, ) as anchor.Program; - await airdrop(connection, cosigner.publicKey, 50); - const fvca = Keypair.generate(); const newFcva = Keypair.generate(); @@ -386,6 +383,7 @@ describe('mmm-admin', () => { pool: poolKey, systemProgram: SystemProgram.programId, }) + .signers([cosigner]) .rpc(); await program.methods @@ -397,6 +395,7 @@ describe('mmm-admin', () => { owner: wallet.publicKey, pool: poolKey, }) + .signers([cosigner]) .rpc(); const poolAccountInfo = await program.account.pool.fetch(poolKey); @@ -586,7 +585,8 @@ describe('mmm-admin', () => { } }); - it('owner can update when no cosigner', async () => { + // Skipping this because we now enforce owner !== cosigner + it.skip('owner can update when no cosigner', async () => { const fvca = Keypair.generate(); const newFcva = Keypair.generate(); diff --git a/tests/mmm-referral.spec.ts b/tests/mmm-referral.spec.ts new file mode 100644 index 0000000..f53ae80 --- /dev/null +++ b/tests/mmm-referral.spec.ts @@ -0,0 +1,185 @@ +import * as anchor from '@project-serum/anchor'; +import { + getAssociatedTokenAddress, + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token'; +import { + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + SystemProgram, + SYSVAR_RENT_PUBKEY, +} from '@solana/web3.js'; +import { Umi } from '@metaplex-foundation/umi'; +import { + Mmm, + AllowlistKind, + CurveKind, + getMMMSellStatePDA, + IDL, + MMMProgramID, +} from '../sdk/src'; +import { airdrop, createPoolWithExampleDepositsUmi } from './utils'; +import { toWeb3JsPublicKey } from '@metaplex-foundation/umi-web3js-adapters'; +import { mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata'; +import { createUmi } from '@metaplex-foundation/umi-bundle-tests'; + +describe('mmm-referral', () => { + 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(); + let umi: Umi; + + // constants from `proxy.json` account in deps + const mockProxyConfigKey = new PublicKey( + '9V5HWD1ap6mCDMhBoXU5SVcZZn9ihqJtoMQZsw5MTnoD', + ); + const mockProxyAuthorityKey = new PublicKey( + 'Aj7o3CHJAcUv1fa7rBe2CcApxD5A1U8Qjkb2Sua8TTM6', + ); + + beforeAll(async () => { + await airdrop(connection, wallet.publicKey, 50); + umi = ( + await createUmi('http://127.0.0.1:8899', { commitment: 'processed' }) + ).use(mplTokenMetadata()); + }); + + it('Correctly validates referral', async () => { + const buyer = Keypair.generate(); + const [poolData] = await Promise.all([ + createPoolWithExampleDepositsUmi( + program, + [AllowlistKind.fvca], + { + owner: wallet.publicKey, + 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, + referral: mockProxyAuthorityKey, + }, + 'sell', + TOKEN_PROGRAM_ID, + buyer.publicKey, + ), + airdrop(connection, buyer.publicKey, 20), + ]); + + const buyerNftAtaAddress = await getAssociatedTokenAddress( + toWeb3JsPublicKey(poolData.nft.mintAddress), + buyer.publicKey, + true, + TOKEN_PROGRAM_ID, + ); + const { key: sellState } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + toWeb3JsPublicKey(poolData.nft.mintAddress), + ); + + const assertReferral = async (referral: PublicKey, success: boolean) => { + try { + const txId = await program.methods + .solFulfillSell({ + assetAmount: new anchor.BN(1), + maxPaymentAmount: new anchor.BN(100 * LAMPORTS_PER_SOL), + buysideCreatorRoyaltyBp: 0, + allowlistAux: '', + takerFeeBp: 100, + makerFeeBp: 0, + }) + .accountsStrict({ + payer: buyer.publicKey, + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + referral: referral, + pool: poolData.poolKey, + buysideSolEscrowAccount: poolData.poolPaymentEscrow, + assetMetadata: poolData.nft.metadataAddress, + assetMasterEdition: poolData.nft.masterEditionAddress, + assetMint: poolData.nft.mintAddress, + sellsideEscrowTokenAccount: poolData.poolAtaNft, + payerAssetAccount: buyerNftAtaAddress, + allowlistAuxAccount: SystemProgram.programId, + sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + rent: SYSVAR_RENT_PUBKEY, + }) + .signers([buyer, cosigner]) + .rpc(); + if (success) { + expect(txId).not.toBeNull(); + } else { + expect(txId).toBeNull(); + } + } catch (e) { + expect(e).toBeInstanceOf(anchor.AnchorError); + expect((e as anchor.AnchorError).error.errorMessage).toMatch( + /invalid referral/, + ); + } + }; + + // random keypair + await assertReferral(new Keypair().publicKey, false); + // account with correct account data but wrong program owner + await assertReferral( + new PublicKey('AJtUEMcZv9DDG4EVd8ugG3duAnCmmmVa6xCEUV7FqFFd'), + false, + ); + + // correct proxy account + await assertReferral(mockProxyConfigKey, true); + + // check that proxy account also works for fulfill buy + await program.methods + .solFulfillBuy({ + assetAmount: new anchor.BN(1), + minPaymentAmount: new anchor.BN(1), + makerFeeBp: 0, + takerFeeBp: 100, + allowlistAux: null, + }) + .accountsStrict({ + payer: buyer.publicKey, + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + referral: mockProxyConfigKey, + pool: poolData.poolKey, + buysideSolEscrowAccount: poolData.poolPaymentEscrow, + assetMetadata: poolData.nft.metadataAddress, + assetMasterEdition: poolData.nft.masterEditionAddress, + assetMint: poolData.nft.mintAddress, + payerAssetAccount: buyerNftAtaAddress, + sellsideEscrowTokenAccount: poolData.poolAtaNft, + ownerTokenAccount: await getAssociatedTokenAddress( + toWeb3JsPublicKey(poolData.nft.mintAddress), + wallet.publicKey, + false, + TOKEN_PROGRAM_ID, + ), + allowlistAuxAccount: SystemProgram.programId, + sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + rent: SYSVAR_RENT_PUBKEY, + }) + .signers([buyer, cosigner]) + .rpc(); + }); +});