diff --git a/contracts/infinity-pair/src/execute.rs b/contracts/infinity-pair/src/execute.rs index 5877d12..148148c 100644 --- a/contracts/infinity-pair/src/execute.rs +++ b/contracts/infinity-pair/src/execute.rs @@ -221,10 +221,9 @@ pub fn execute_deposit_tokens( _deps: DepsMut, info: MessageInfo, _env: Env, - mut pair: Pair, + pair: Pair, ) -> Result<(Pair, Response), ContractError> { - pair.total_tokens += must_pay(&info, &pair.immutable.denom)?; - + must_pay(&info, &pair.immutable.denom)?; Ok((pair, Response::new())) } @@ -346,7 +345,7 @@ pub fn execute_swap_tokens_for_specific_nft( token_id: String, asset_recipient: Option, ) -> Result<(Pair, Response), ContractError> { - let max_input = must_pay(&info, &pair.immutable.denom)?; + let received_amount = must_pay(&info, &pair.immutable.denom)?; let quote_summary = pair .internal @@ -354,9 +353,10 @@ pub fn execute_swap_tokens_for_specific_nft( .as_ref() .ok_or(ContractError::InvalidPair("pair cannot produce quote".to_string()))?; - ensure!( - max_input >= quote_summary.total(), - ContractError::InvalidPairQuote("payment required is greater than max input".to_string()) + ensure_eq!( + received_amount, + quote_summary.total(), + InfinityError::InvalidInput("received funds does not equal quote".to_string()) ); let mut response = Response::new(); @@ -370,21 +370,17 @@ pub fn execute_swap_tokens_for_specific_nft( response = quote_summary.payout(&pair.immutable.denom, &seller_recipient, response)?; // Payout NFT + ensure!( + NFT_DEPOSITS.has(deps.storage, token_id.clone()), + InfinityError::InvalidInput("pair does not own NFT".to_string()) + ); + NFT_DEPOSITS.remove(deps.storage, token_id.clone()); + let nft_recipient = address_or(asset_recipient.as_ref(), &info.sender); response = transfer_nft(&pair.immutable.collection, &token_id, &nft_recipient, response); - NFT_DEPOSITS.remove(deps.storage, token_id); - - // Refund excess tokens - let refund_amount = max_input - quote_summary.total(); - if !refund_amount.is_zero() { - response = transfer_coin( - coin(refund_amount.u128(), &pair.immutable.denom), - &info.sender, - response, - ); - } // Update pair state + pair.total_tokens -= received_amount; pair.swap_tokens_for_nft(); Ok((pair, response)) diff --git a/contracts/infinity-pair/src/math.rs b/contracts/infinity-pair/src/math.rs index 2f89b6d..edaa6db 100644 --- a/contracts/infinity-pair/src/math.rs +++ b/contracts/infinity-pair/src/math.rs @@ -20,8 +20,8 @@ pub fn calc_exponential_spot_price_user_submits_nft( spot_price: Uint128, delta: Decimal, ) -> Result { - let net_delta = Decimal::one().checked_div(Decimal::one().checked_add(delta)?)?; - Ok(spot_price.mul_floor(net_delta)) + let net_delta = Decimal::one().checked_add(delta)?; + Ok(spot_price.checked_div_floor(net_delta)?) } pub fn calc_exponential_spot_price_user_submits_tokens( @@ -54,7 +54,7 @@ pub fn calc_cp_trade_sell_to_pair_price( ContractError::InvalidPair("pair must have at least 1 NFT".to_string(),) ); let fraction = (Uint128::from(total_nfts + 1u64), Uint128::one()); - Ok(total_tokens.checked_div_ceil(fraction)?) + Ok(total_tokens.checked_div_floor(fraction)?) } pub fn calc_cp_trade_buy_from_pair_price( @@ -66,7 +66,7 @@ pub fn calc_cp_trade_buy_from_pair_price( ContractError::InvalidPair("pair must have greater than 1 NFT".to_string(),) ); let fraction = (Uint128::from(total_nfts - 1u64), Uint128::one()); - Ok(total_tokens.checked_div_floor(fraction)?) + Ok(total_tokens.checked_div_ceil(fraction)?) } #[cfg(test)] diff --git a/contracts/infinity-pair/src/pair.rs b/contracts/infinity-pair/src/pair.rs index ab4bd44..eac0d6f 100644 --- a/contracts/infinity-pair/src/pair.rs +++ b/contracts/infinity-pair/src/pair.rs @@ -245,7 +245,7 @@ impl Pair { } fn update_sell_to_pair_quote_summary(&mut self, payout_context: &PayoutContext) { - if !self.config.is_active { + if !self.config.is_active || self.config.pair_type == PairType::Nft { self.internal.sell_to_pair_quote_summary = None; return; } @@ -273,7 +273,10 @@ impl Pair { } fn update_buy_from_pair_quote_summary(&mut self, payout_context: &PayoutContext) { - if !self.config.is_active || self.internal.total_nfts == 0u64 { + if !self.config.is_active + || self.internal.total_nfts == 0u64 + || self.config.pair_type == PairType::Token + { self.internal.buy_from_pair_quote_summary = None; return; } diff --git a/unit-tests/src/helpers/constants.rs b/unit-tests/src/helpers/constants.rs index 291630e..0f5d587 100644 --- a/unit-tests/src/helpers/constants.rs +++ b/unit-tests/src/helpers/constants.rs @@ -1,5 +1,5 @@ -pub const MIN_PRICE: u128 = 1000000; +pub const _MIN_PRICE: u128 = 1000000; -pub const POOL_CREATION_FEE: u128 = 2000000; +pub const _POOL_CREATION_FEE: u128 = 2000000; -pub const TRADING_FEE_BPS: u64 = 200; +pub const _TRADING_FEE_BPS: u64 = 200; diff --git a/unit-tests/src/helpers/nft_functions.rs b/unit-tests/src/helpers/nft_functions.rs index 71aecc7..ca7efd8 100644 --- a/unit-tests/src/helpers/nft_functions.rs +++ b/unit-tests/src/helpers/nft_functions.rs @@ -6,16 +6,16 @@ use sg721_base::msg::CollectionInfoResponse; use sg_multi_test::StargazeApp; use sg_std::NATIVE_DENOM; -pub const MINT_PRICE: u128 = 100_000_000; +pub const _MINT_PRICE: u128 = 100_000_000; // Mints an NFT for a creator -pub fn mint(router: &mut StargazeApp, creator: &Addr, minter_addr: &Addr) -> String { +pub fn _mint(router: &mut StargazeApp, creator: &Addr, minter_addr: &Addr) -> String { let minter_msg = vending_minter::msg::ExecuteMsg::Mint {}; let res = router.execute_contract( creator.clone(), minter_addr.clone(), &minter_msg, - &coins(MINT_PRICE, NATIVE_DENOM), + &coins(_MINT_PRICE, NATIVE_DENOM), ); assert!(res.is_ok()); @@ -58,7 +58,12 @@ pub fn approve( assert!(res.is_ok()); } -pub fn approve_all(router: &mut StargazeApp, owner: &Addr, collection: &Addr, approve_addr: &Addr) { +pub fn _approve_all( + router: &mut StargazeApp, + owner: &Addr, + collection: &Addr, + approve_addr: &Addr, +) { let approve_msg: Sg721ExecuteMsg = Sg721ExecuteMsg::ApproveAll { operator: approve_addr.to_string(), expires: None, @@ -90,7 +95,7 @@ pub fn _burn(router: &mut StargazeApp, creator: &Addr, collection: &Addr, token_ assert!(res.is_ok()); } -pub fn mint_and_approve_many( +pub fn _mint_and_approve_many( router: &mut StargazeApp, creator: &Addr, owner: &Addr, @@ -104,7 +109,7 @@ pub fn mint_and_approve_many( let token_id = mint_to(router, creator, owner, minter_addr); token_ids.push(token_id); } - approve_all(router, owner, collection, approve_addr); + _approve_all(router, owner, collection, approve_addr); token_ids } diff --git a/unit-tests/src/helpers/pair_functions.rs b/unit-tests/src/helpers/pair_functions.rs index d3faa0c..b54fc78 100644 --- a/unit-tests/src/helpers/pair_functions.rs +++ b/unit-tests/src/helpers/pair_functions.rs @@ -119,13 +119,15 @@ pub fn create_pair_with_deposits( token_ids.push(token_id) } - let response = router.execute_contract( - owner.clone(), - pair_addr.clone(), - &InfinityPairExecuteMsg::DepositTokens {}, - &[coin(num_tokens.u128(), NATIVE_DENOM)], - ); - assert!(response.is_ok()); + if !num_tokens.is_zero() { + let response = router.execute_contract( + owner.clone(), + pair_addr.clone(), + &InfinityPairExecuteMsg::DepositTokens {}, + &[coin(num_tokens.u128(), NATIVE_DENOM)], + ); + assert!(response.is_ok()); + } let pair = router .wrap() diff --git a/unit-tests/src/helpers/utils.rs b/unit-tests/src/helpers/utils.rs index 24fb3a3..0291f8e 100644 --- a/unit-tests/src/helpers/utils.rs +++ b/unit-tests/src/helpers/utils.rs @@ -9,11 +9,11 @@ pub fn assert_error(response: Result, expected: String) { assert_eq!(response.unwrap_err().source().unwrap().to_string(), expected); } -pub fn assert_event(response: Result, ty: &str) { +pub fn _assert_event(response: Result, ty: &str) { assert!(response.unwrap().events.iter().find(|event| event.ty == ty).is_some()); } -pub fn get_native_balances(router: &StargazeApp, addresses: &Vec) -> HashMap { +pub fn _get_native_balances(router: &StargazeApp, addresses: &Vec) -> HashMap { let mut balances: HashMap = HashMap::new(); for address in addresses { let native_balance = router.wrap().query_balance(address, NATIVE_DENOM).unwrap(); @@ -22,6 +22,6 @@ pub fn get_native_balances(router: &StargazeApp, addresses: &Vec) -> HashM balances } -pub fn get_native_balance(router: &StargazeApp, address: Addr) -> Uint128 { - get_native_balances(router, &vec![address.clone()]).get(&address).unwrap().amount +pub fn _get_native_balance(router: &StargazeApp, address: Addr) -> Uint128 { + _get_native_balances(router, &vec![address.clone()]).get(&address).unwrap().amount } diff --git a/unit-tests/src/infinity_pair_tests/deposit_assets_tests.rs b/unit-tests/src/infinity_pair_tests/deposit_assets_tests.rs index 1d66a03..8557c06 100644 --- a/unit-tests/src/infinity_pair_tests/deposit_assets_tests.rs +++ b/unit-tests/src/infinity_pair_tests/deposit_assets_tests.rs @@ -1,7 +1,7 @@ use crate::helpers::nft_functions::{assert_nft_owner, mint_to}; use crate::helpers::pair_functions::create_pair; use crate::helpers::utils::assert_error; -use crate::setup::templates::{setup_infinity_test, InfinityTestSetup}; +use crate::setup::templates::{setup_infinity_test, standard_minter_template, InfinityTestSetup}; use cosmwasm_std::{coin, to_binary, Empty, Uint128}; use cw721::Cw721ExecuteMsg; @@ -14,6 +14,7 @@ use test_suite::common_setup::msg::MinterTemplateResponse; #[test] fn try_deposit_nft() { + let vt = standard_minter_template(1000u32); let InfinityTestSetup { vending_template: MinterTemplateResponse { @@ -24,7 +25,7 @@ fn try_deposit_nft() { infinity_global, infinity_factory, .. - } = setup_infinity_test(1000).unwrap(); + } = setup_infinity_test(vt).unwrap(); let collection_resp = &collection_response_vec[0]; let minter = collection_resp.minter.clone().unwrap(); @@ -63,6 +64,7 @@ fn try_deposit_nft() { #[test] fn try_withdraw_nfts() { + let vt = standard_minter_template(1000u32); let InfinityTestSetup { vending_template: MinterTemplateResponse { @@ -73,7 +75,7 @@ fn try_withdraw_nfts() { infinity_global, infinity_factory, .. - } = setup_infinity_test(1000).unwrap(); + } = setup_infinity_test(vt).unwrap(); let collection_resp = &collection_response_vec[0]; let minter = collection_resp.minter.clone().unwrap(); @@ -183,6 +185,7 @@ fn try_withdraw_nfts() { #[test] fn try_deposit_tokens() { + let vt = standard_minter_template(1000u32); let InfinityTestSetup { vending_template: MinterTemplateResponse { @@ -193,7 +196,7 @@ fn try_deposit_tokens() { infinity_global, infinity_factory, .. - } = setup_infinity_test(1000).unwrap(); + } = setup_infinity_test(vt).unwrap(); let collection_resp = &collection_response_vec[0]; let _minter = collection_resp.minter.clone().unwrap(); @@ -247,6 +250,7 @@ fn try_deposit_tokens() { #[test] fn try_withdraw_tokens() { + let vt = standard_minter_template(1000u32); let InfinityTestSetup { vending_template: MinterTemplateResponse { @@ -257,7 +261,7 @@ fn try_withdraw_tokens() { infinity_global, infinity_factory, .. - } = setup_infinity_test(1000).unwrap(); + } = setup_infinity_test(vt).unwrap(); let collection_resp = &collection_response_vec[0]; let _minter = collection_resp.minter.clone().unwrap(); diff --git a/unit-tests/src/infinity_pair_tests/mod.rs b/unit-tests/src/infinity_pair_tests/mod.rs index 63e4608..72f8d05 100644 --- a/unit-tests/src/infinity_pair_tests/mod.rs +++ b/unit-tests/src/infinity_pair_tests/mod.rs @@ -1,6 +1,10 @@ #[cfg(test)] mod deposit_assets_tests; #[cfg(test)] +mod nft_pair_swap_tests; +#[cfg(test)] mod pair_creation_tests; #[cfg(test)] mod token_pair_swap_tests; +#[cfg(test)] +mod trade_pair_swap_tests; diff --git a/unit-tests/src/infinity_pair_tests/nft_pair_swap_tests.rs b/unit-tests/src/infinity_pair_tests/nft_pair_swap_tests.rs new file mode 100644 index 0000000..32a738c --- /dev/null +++ b/unit-tests/src/infinity_pair_tests/nft_pair_swap_tests.rs @@ -0,0 +1,435 @@ +use crate::helpers::nft_functions::{approve, assert_nft_owner, mint_to}; +use crate::helpers::pair_functions::create_pair_with_deposits; +use crate::helpers::utils::assert_error; +use crate::setup::setup_accounts::{setup_addtl_account, MarketAccounts, INITIAL_BALANCE}; +use crate::setup::setup_infinity_contracts::UOSMO; +use crate::setup::templates::{setup_infinity_test, standard_minter_template, InfinityTestSetup}; + +use cosmwasm_std::{coin, Addr, Decimal, Uint128}; +use cw_multi_test::Executor; +use infinity_global::{GlobalConfig, QueryMsg as InfinityGlobalQueryMsg}; +use infinity_pair::msg::{ExecuteMsg as InfinityPairExecuteMsg, QueryMsg as InfinityPairQueryMsg}; +use infinity_pair::pair::Pair; +use infinity_pair::state::{BondingCurve, PairConfig, PairType, QuoteSummary, TokenPayment}; +use infinity_pair::ContractError; +use infinity_shared::InfinityError; +use sg721_base::msg::{CollectionInfoResponse, QueryMsg as Sg721QueryMsg}; +use sg_std::NATIVE_DENOM; +use test_suite::common_setup::msg::MinterTemplateResponse; + +#[test] +fn try_nft_pair_invalid_swaps() { + let vt = standard_minter_template(1000u32); + let InfinityTestSetup { + vending_template: + MinterTemplateResponse { + collection_response_vec, + mut router, + accts: + MarketAccounts { + creator, + owner, + bidder, + }, + }, + infinity_global, + infinity_factory, + .. + } = setup_infinity_test(vt).unwrap(); + + let collection_resp = &collection_response_vec[0]; + let minter = collection_resp.minter.clone().unwrap(); + let collection = collection_resp.collection.clone().unwrap(); + + let global_config = router + .wrap() + .query_wasm_smart::>( + infinity_global.clone(), + &InfinityGlobalQueryMsg::GlobalConfig {}, + ) + .unwrap(); + + let collection_info = router + .wrap() + .query_wasm_smart::( + collection.clone(), + &Sg721QueryMsg::CollectionInfo {}, + ) + .unwrap(); + + let mut test_pair = create_pair_with_deposits( + &mut router, + &infinity_global, + &infinity_factory, + &minter, + &collection, + &creator, + &owner, + PairConfig { + pair_type: PairType::Nft, + bonding_curve: BondingCurve::Linear { + spot_price: Uint128::from(10_000_000u128), + delta: Uint128::from(1_000_000u128), + }, + is_active: false, + asset_recipient: None, + }, + 10u64, + Uint128::zero(), + ); + + assert_eq!(test_pair.pair.internal.sell_to_pair_quote_summary, None); + assert_eq!(test_pair.pair.internal.buy_from_pair_quote_summary, None); + + let token_id = test_pair.token_ids[0].clone(); + + // Cannot swap with inactive pair + let response = router.execute_contract( + bidder.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::SwapTokensForSpecificNft { + token_id: token_id.clone(), + asset_recipient: None, + }, + &[coin(10_000_000u128, NATIVE_DENOM)], + ); + assert_error(response, ContractError::InvalidPair("pair is inactive".to_string()).to_string()); + + // Set pair to active + let response = router.execute_contract( + owner.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::UpdatePairConfig { + is_active: Some(true), + pair_type: None, + bonding_curve: None, + asset_recipient: None, + }, + &[], + ); + assert!(response.is_ok()); + + test_pair.pair = router + .wrap() + .query_wasm_smart::(test_pair.address.clone(), &InfinityPairQueryMsg::Pair {}) + .unwrap(); + + assert_eq!(test_pair.pair.internal.sell_to_pair_quote_summary, None); + assert_eq!( + test_pair.pair.internal.buy_from_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(100_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(500_000u128), + }), + swap: None, + seller_amount: Uint128::from(9_400_000u128), + }) + ); + + // Cannot do a NFT to token swap with NFT pair + let seller = setup_addtl_account(&mut router, "seller", INITIAL_BALANCE).unwrap(); + let token_id = mint_to(&mut router, &creator.clone(), &seller.clone(), &minter); + approve(&mut router, &seller, &collection, &test_pair.address.clone(), token_id.clone()); + let response = router.execute_contract( + seller.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::SwapNftForTokens { + token_id: token_id.clone(), + min_output: coin(9_400_000u128, NATIVE_DENOM), + asset_recipient: None, + }, + &[], + ); + assert_error( + response, + ContractError::InvalidPair("pair cannot produce quote".to_string()).to_string(), + ); + + // Cannot swap with insufficient funds + let response = router.execute_contract( + bidder.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::SwapTokensForSpecificNft { + token_id: token_id.clone(), + asset_recipient: None, + }, + &[coin(1, NATIVE_DENOM)], + ); + assert_error( + response, + ContractError::InvalidPairQuote("payment required is greater than max input".to_string()) + .to_string(), + ); + + // Cannot swap using alt denom funds + let response = router.execute_contract( + bidder.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::SwapTokensForSpecificNft { + token_id: token_id.clone(), + asset_recipient: None, + }, + &[coin(10_000_000u128, UOSMO)], + ); + assert_error(response, "Must send reserve token 'ustars'".to_string()); + + // Cannot swap for unnowned NFT + let response = router.execute_contract( + bidder.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::SwapTokensForSpecificNft { + token_id: "99999".to_string(), + asset_recipient: None, + }, + &[coin(10_000_000u128, NATIVE_DENOM)], + ); + assert_error( + response, + InfinityError::InvalidInput("pair does not own NFT".to_string()).to_string(), + ); +} + +#[test] +fn try_nft_pair_linear_user_submits_tokens_swap() { + let vt = standard_minter_template(1000u32); + let InfinityTestSetup { + vending_template: + MinterTemplateResponse { + collection_response_vec, + mut router, + accts: + MarketAccounts { + creator, + owner, + bidder, + }, + }, + infinity_global, + infinity_factory, + .. + } = setup_infinity_test(vt).unwrap(); + + let collection_resp = &collection_response_vec[0]; + let minter = collection_resp.minter.clone().unwrap(); + let collection = collection_resp.collection.clone().unwrap(); + + let global_config = router + .wrap() + .query_wasm_smart::>( + infinity_global.clone(), + &InfinityGlobalQueryMsg::GlobalConfig {}, + ) + .unwrap(); + + let collection_info = router + .wrap() + .query_wasm_smart::( + collection.clone(), + &Sg721QueryMsg::CollectionInfo {}, + ) + .unwrap(); + + let mut test_pair = create_pair_with_deposits( + &mut router, + &infinity_global, + &infinity_factory, + &minter, + &collection, + &creator, + &owner, + PairConfig { + pair_type: PairType::Nft, + bonding_curve: BondingCurve::Linear { + spot_price: Uint128::from(10_000_000u128), + delta: Uint128::from(1_000_000u128), + }, + is_active: true, + asset_recipient: None, + }, + 10u64, + Uint128::zero(), + ); + + assert_eq!(test_pair.pair.internal.sell_to_pair_quote_summary, None); + assert_eq!( + test_pair.pair.internal.buy_from_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(100_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(500_000u128), + }), + swap: None, + seller_amount: Uint128::from(9_400_000u128), + }) + ); + + let token_id = test_pair.token_ids[0].clone(); + + // Can swap approved NFT + let response = router.execute_contract( + bidder.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::SwapTokensForSpecificNft { + token_id: token_id.clone(), + asset_recipient: None, + }, + &[coin(10_000_000u128, NATIVE_DENOM)], + ); + assert!(response.is_ok()); + + assert_nft_owner(&router, &collection, token_id, &bidder); + + test_pair.pair = router + .wrap() + .query_wasm_smart::(test_pair.address.clone(), &InfinityPairQueryMsg::Pair {}) + .unwrap(); + + assert_eq!(test_pair.pair.internal.sell_to_pair_quote_summary, None); + assert_eq!( + test_pair.pair.internal.buy_from_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn, + amount: Uint128::from(110_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked(collection_info.royalty_info.unwrap().payment_address), + amount: Uint128::from(550_000u128), + }), + swap: None, + seller_amount: Uint128::from(10_340_000u128), + }) + ); +} + +#[test] +fn try_nft_pair_exponential_user_submits_tokens_swap() { + let vt = standard_minter_template(1000u32); + let InfinityTestSetup { + vending_template: + MinterTemplateResponse { + collection_response_vec, + mut router, + accts: + MarketAccounts { + creator, + owner, + bidder, + }, + }, + infinity_global, + infinity_factory, + .. + } = setup_infinity_test(vt).unwrap(); + + let collection_resp = &collection_response_vec[0]; + let minter = collection_resp.minter.clone().unwrap(); + let collection = collection_resp.collection.clone().unwrap(); + + let global_config = router + .wrap() + .query_wasm_smart::>( + infinity_global.clone(), + &InfinityGlobalQueryMsg::GlobalConfig {}, + ) + .unwrap(); + + let collection_info = router + .wrap() + .query_wasm_smart::( + collection.clone(), + &Sg721QueryMsg::CollectionInfo {}, + ) + .unwrap(); + + let mut test_pair = create_pair_with_deposits( + &mut router, + &infinity_global, + &infinity_factory, + &minter, + &collection, + &creator, + &owner, + PairConfig { + pair_type: PairType::Nft, + bonding_curve: BondingCurve::Exponential { + spot_price: Uint128::from(10_000_000u128), + delta: Decimal::percent(12), + }, + is_active: true, + asset_recipient: None, + }, + 10u64, + Uint128::zero(), + ); + + assert_eq!(test_pair.pair.internal.sell_to_pair_quote_summary, None); + assert_eq!( + test_pair.pair.internal.buy_from_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(100_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(500_000u128), + }), + swap: None, + seller_amount: Uint128::from(9_400_000u128), + }) + ); + + let token_id = test_pair.token_ids[0].clone(); + + // Can swap approved NFT + let response = router.execute_contract( + bidder.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::SwapTokensForSpecificNft { + token_id: token_id.clone(), + asset_recipient: None, + }, + &[coin(10_000_000u128, NATIVE_DENOM)], + ); + assert!(response.is_ok()); + + assert_nft_owner(&router, &collection, token_id, &bidder); + + test_pair.pair = router + .wrap() + .query_wasm_smart::(test_pair.address.clone(), &InfinityPairQueryMsg::Pair {}) + .unwrap(); + + assert_eq!(test_pair.pair.internal.sell_to_pair_quote_summary, None); + assert_eq!( + test_pair.pair.internal.buy_from_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn, + amount: Uint128::from(112_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked(collection_info.royalty_info.unwrap().payment_address), + amount: Uint128::from(560_000u128), + }), + swap: None, + seller_amount: Uint128::from(10_528_000u128), + }) + ); +} diff --git a/unit-tests/src/infinity_pair_tests/pair_creation_tests.rs b/unit-tests/src/infinity_pair_tests/pair_creation_tests.rs index 7aa2c9b..8375691 100644 --- a/unit-tests/src/infinity_pair_tests/pair_creation_tests.rs +++ b/unit-tests/src/infinity_pair_tests/pair_creation_tests.rs @@ -1,6 +1,6 @@ use crate::helpers::pair_functions::create_pair; use crate::helpers::utils::assert_error; -use crate::setup::templates::{setup_infinity_test, InfinityTestSetup}; +use crate::setup::templates::{setup_infinity_test, standard_minter_template, InfinityTestSetup}; use cosmwasm_std::{Addr, Uint128}; use cw_multi_test::Executor; @@ -16,6 +16,7 @@ use test_suite::common_setup::msg::MinterTemplateResponse; #[test] fn try_create_pair() { + let vt = standard_minter_template(1000u32); let InfinityTestSetup { vending_template: MinterTemplateResponse { @@ -26,7 +27,7 @@ fn try_create_pair() { infinity_global, infinity_factory, .. - } = setup_infinity_test(1000).unwrap(); + } = setup_infinity_test(vt).unwrap(); let collection_resp = &collection_response_vec[0]; let _minter = collection_resp.minter.clone().unwrap(); @@ -100,6 +101,7 @@ fn try_create_pair() { #[test] fn try_update_pair_config() { + let vt = standard_minter_template(1000u32); let InfinityTestSetup { vending_template: MinterTemplateResponse { @@ -110,7 +112,7 @@ fn try_update_pair_config() { infinity_global, infinity_factory, .. - } = setup_infinity_test(1000).unwrap(); + } = setup_infinity_test(vt).unwrap(); let collection_resp = &collection_response_vec[0]; let _minter = collection_resp.minter.clone().unwrap(); diff --git a/unit-tests/src/infinity_pair_tests/token_pair_swap_tests.rs b/unit-tests/src/infinity_pair_tests/token_pair_swap_tests.rs index 1fbd4aa..55d0db1 100644 --- a/unit-tests/src/infinity_pair_tests/token_pair_swap_tests.rs +++ b/unit-tests/src/infinity_pair_tests/token_pair_swap_tests.rs @@ -2,7 +2,8 @@ use crate::helpers::nft_functions::{approve, assert_nft_owner, mint_to}; use crate::helpers::pair_functions::create_pair_with_deposits; use crate::helpers::utils::assert_error; use crate::setup::setup_accounts::{setup_addtl_account, MarketAccounts, INITIAL_BALANCE}; -use crate::setup::templates::{setup_infinity_test, InfinityTestSetup}; +use crate::setup::setup_infinity_contracts::UOSMO; +use crate::setup::templates::{setup_infinity_test, standard_minter_template, InfinityTestSetup}; use cosmwasm_std::{coin, Addr, Decimal, Uint128}; use cw_multi_test::Executor; @@ -17,7 +18,8 @@ use sg_std::NATIVE_DENOM; use test_suite::common_setup::msg::MinterTemplateResponse; #[test] -fn try_token_pair_linear_user_submits_nfts_swap() { +fn try_token_pair_invalid_swaps() { + let vt = standard_minter_template(1000u32); let InfinityTestSetup { vending_template: MinterTemplateResponse { @@ -33,7 +35,7 @@ fn try_token_pair_linear_user_submits_nfts_swap() { infinity_global, infinity_factory, .. - } = setup_infinity_test(1000).unwrap(); + } = setup_infinity_test(vt).unwrap(); let collection_resp = &collection_response_vec[0]; let minter = collection_resp.minter.clone().unwrap(); @@ -131,6 +133,7 @@ fn try_token_pair_linear_user_submits_nfts_swap() { seller_amount: Uint128::from(9_400_000u128), }) ); + assert_eq!(test_pair.pair.internal.buy_from_pair_quote_summary, None); // Cannot do a token to NFT swap with token pair let response = router.execute_contract( @@ -163,8 +166,110 @@ fn try_token_pair_linear_user_submits_nfts_swap() { InfinityError::Unauthorized("contract is not approved".to_string()).to_string(), ); - // Can swap approved NFT + // Cannot swap using an alt min output denom approve(&mut router, &seller, &collection, &test_pair.address.clone(), token_id.clone()); + let response = router.execute_contract( + seller.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::SwapNftForTokens { + token_id: token_id.clone(), + min_output: coin(0u128, UOSMO), + asset_recipient: None, + }, + &[], + ); + assert_error( + response, + ContractError::InvalidPairQuote("seller coin is less than min output".to_string()) + .to_string(), + ); +} + +#[test] +fn try_token_pair_linear_user_submits_nfts_swap() { + let vt = standard_minter_template(1000u32); + let InfinityTestSetup { + vending_template: + MinterTemplateResponse { + collection_response_vec, + mut router, + accts: + MarketAccounts { + creator, + owner, + bidder: _, + }, + }, + infinity_global, + infinity_factory, + .. + } = setup_infinity_test(vt).unwrap(); + + let collection_resp = &collection_response_vec[0]; + let minter = collection_resp.minter.clone().unwrap(); + let collection = collection_resp.collection.clone().unwrap(); + + let global_config = router + .wrap() + .query_wasm_smart::>( + infinity_global.clone(), + &InfinityGlobalQueryMsg::GlobalConfig {}, + ) + .unwrap(); + + let collection_info = router + .wrap() + .query_wasm_smart::( + collection.clone(), + &Sg721QueryMsg::CollectionInfo {}, + ) + .unwrap(); + + let mut test_pair = create_pair_with_deposits( + &mut router, + &infinity_global, + &infinity_factory, + &minter, + &collection, + &creator, + &owner, + PairConfig { + pair_type: PairType::Token, + bonding_curve: BondingCurve::Linear { + spot_price: Uint128::from(10_000_000u128), + delta: Uint128::from(1_000_000u128), + }, + is_active: true, + asset_recipient: None, + }, + 0u64, + Uint128::from(100_000_000u128), + ); + + assert_eq!( + test_pair.pair.internal.sell_to_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(100_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(500_000u128), + }), + swap: None, + seller_amount: Uint128::from(9_400_000u128), + }) + ); + assert_eq!(test_pair.pair.internal.buy_from_pair_quote_summary, None); + + let seller = setup_addtl_account(&mut router, "seller", INITIAL_BALANCE).unwrap(); + let token_id = mint_to(&mut router, &creator.clone(), &seller.clone(), &minter); + approve(&mut router, &seller, &collection, &test_pair.address.clone(), token_id.clone()); + + // Can swap approved NFT let response = router.execute_contract( seller.clone(), test_pair.address.clone(), @@ -199,10 +304,12 @@ fn try_token_pair_linear_user_submits_nfts_swap() { seller_amount: Uint128::from(8_460_000u128), }) ); + assert_eq!(test_pair.pair.internal.buy_from_pair_quote_summary, None); } #[test] fn try_token_pair_exponential_user_submits_nfts_swap() { + let vt = standard_minter_template(1000u32); let InfinityTestSetup { vending_template: MinterTemplateResponse { @@ -218,7 +325,7 @@ fn try_token_pair_exponential_user_submits_nfts_swap() { infinity_global, infinity_factory, .. - } = setup_infinity_test(1000).unwrap(); + } = setup_infinity_test(vt).unwrap(); let collection_resp = &collection_response_vec[0]; let minter = collection_resp.minter.clone().unwrap(); @@ -254,51 +361,13 @@ fn try_token_pair_exponential_user_submits_nfts_swap() { spot_price: Uint128::from(10_000_000u128), delta: Decimal::percent(12), }, - is_active: false, + is_active: true, asset_recipient: None, }, 0u64, Uint128::from(100_000_000u128), ); - assert_eq!(test_pair.pair.internal.sell_to_pair_quote_summary, None); - assert_eq!(test_pair.pair.internal.buy_from_pair_quote_summary, None); - - let seller = setup_addtl_account(&mut router, "seller", INITIAL_BALANCE).unwrap(); - let token_id = mint_to(&mut router, &creator.clone(), &seller.clone(), &minter); - - // Cannot swap with inactive pair - let response = router.execute_contract( - seller.clone(), - test_pair.address.clone(), - &InfinityPairExecuteMsg::SwapNftForTokens { - token_id: token_id.clone(), - min_output: coin(9_400_000u128, NATIVE_DENOM), - asset_recipient: None, - }, - &[], - ); - assert_error(response, ContractError::InvalidPair("pair is inactive".to_string()).to_string()); - - // Set pair to active - let response = router.execute_contract( - owner.clone(), - test_pair.address.clone(), - &InfinityPairExecuteMsg::UpdatePairConfig { - is_active: Some(true), - pair_type: None, - bonding_curve: None, - asset_recipient: None, - }, - &[], - ); - assert!(response.is_ok()); - - test_pair.pair = router - .wrap() - .query_wasm_smart::(test_pair.address.clone(), &InfinityPairQueryMsg::Pair {}) - .unwrap(); - assert_eq!( test_pair.pair.internal.sell_to_pair_quote_summary, Some(QuoteSummary { @@ -316,40 +385,13 @@ fn try_token_pair_exponential_user_submits_nfts_swap() { seller_amount: Uint128::from(9_400_000u128), }) ); + assert_eq!(test_pair.pair.internal.buy_from_pair_quote_summary, None); - // Cannot do a token to NFT swap with token pair - let response = router.execute_contract( - seller.clone(), - test_pair.address.clone(), - &InfinityPairExecuteMsg::SwapTokensForSpecificNft { - token_id: token_id.clone(), - asset_recipient: None, - }, - &[coin(10_000_000u128, NATIVE_DENOM)], - ); - assert_error( - response, - ContractError::InvalidPair("pair cannot produce quote".to_string()).to_string(), - ); - - // Cannot swap unappoved NFT - let response = router.execute_contract( - seller.clone(), - test_pair.address.clone(), - &InfinityPairExecuteMsg::SwapNftForTokens { - token_id: token_id.clone(), - min_output: coin(9_400_000u128, NATIVE_DENOM), - asset_recipient: None, - }, - &[], - ); - assert_error( - response, - InfinityError::Unauthorized("contract is not approved".to_string()).to_string(), - ); + let seller = setup_addtl_account(&mut router, "seller", INITIAL_BALANCE).unwrap(); + let token_id = mint_to(&mut router, &creator.clone(), &seller.clone(), &minter); + approve(&mut router, &seller, &collection, &test_pair.address.clone(), token_id.clone()); // Can swap approved NFT - approve(&mut router, &seller, &collection, &test_pair.address.clone(), token_id.clone()); let response = router.execute_contract( seller.clone(), test_pair.address.clone(), @@ -374,14 +416,15 @@ fn try_token_pair_exponential_user_submits_nfts_swap() { Some(QuoteSummary { fair_burn: TokenPayment { recipient: global_config.fair_burn, - amount: Uint128::from(88_000u128), + amount: Uint128::from(89_286u128), }, royalty: Some(TokenPayment { recipient: Addr::unchecked(collection_info.royalty_info.unwrap().payment_address), - amount: Uint128::from(440_000u128), + amount: Uint128::from(446_429u128), }), swap: None, - seller_amount: Uint128::from(8_272_000u128), + seller_amount: Uint128::from(8_392_856u128), }) ); + assert_eq!(test_pair.pair.internal.buy_from_pair_quote_summary, None); } diff --git a/unit-tests/src/infinity_pair_tests/trade_pair_swap_tests.rs b/unit-tests/src/infinity_pair_tests/trade_pair_swap_tests.rs new file mode 100644 index 0000000..ac027f2 --- /dev/null +++ b/unit-tests/src/infinity_pair_tests/trade_pair_swap_tests.rs @@ -0,0 +1,889 @@ +use crate::helpers::nft_functions::{approve, assert_nft_owner, mint_to}; +use crate::helpers::pair_functions::create_pair_with_deposits; +use crate::helpers::utils::assert_error; +use crate::setup::setup_accounts::{setup_addtl_account, MarketAccounts, INITIAL_BALANCE}; +use crate::setup::setup_infinity_contracts::UOSMO; +use crate::setup::templates::{setup_infinity_test, standard_minter_template, InfinityTestSetup}; + +use cosmwasm_std::{coin, to_binary, Addr, Decimal, Empty, Uint128}; +use cw721::Cw721ExecuteMsg; +use cw_multi_test::Executor; +use infinity_global::{GlobalConfig, QueryMsg as InfinityGlobalQueryMsg}; +use infinity_pair::msg::{ExecuteMsg as InfinityPairExecuteMsg, QueryMsg as InfinityPairQueryMsg}; +use infinity_pair::pair::Pair; +use infinity_pair::state::{BondingCurve, PairConfig, PairType, QuoteSummary, TokenPayment}; +use infinity_pair::ContractError; +use infinity_shared::InfinityError; +use sg721_base::msg::{CollectionInfoResponse, QueryMsg as Sg721QueryMsg}; +use sg_std::NATIVE_DENOM; +use test_suite::common_setup::msg::MinterTemplateResponse; + +#[test] +fn try_trade_pair_invalid_swaps() { + let vt = standard_minter_template(1000u32); + let InfinityTestSetup { + vending_template: + MinterTemplateResponse { + collection_response_vec, + mut router, + accts: + MarketAccounts { + creator, + owner, + bidder, + }, + }, + infinity_global, + infinity_factory, + .. + } = setup_infinity_test(vt).unwrap(); + + let collection_resp = &collection_response_vec[0]; + let minter = collection_resp.minter.clone().unwrap(); + let collection = collection_resp.collection.clone().unwrap(); + + let global_config = router + .wrap() + .query_wasm_smart::>( + infinity_global.clone(), + &InfinityGlobalQueryMsg::GlobalConfig {}, + ) + .unwrap(); + + let collection_info = router + .wrap() + .query_wasm_smart::( + collection.clone(), + &Sg721QueryMsg::CollectionInfo {}, + ) + .unwrap(); + + let mut test_pair = create_pair_with_deposits( + &mut router, + &infinity_global, + &infinity_factory, + &minter, + &collection, + &creator, + &owner, + PairConfig { + pair_type: PairType::Trade { + swap_fee_percent: Decimal::percent(0), + reinvest_tokens: false, + reinvest_nfts: false, + }, + bonding_curve: BondingCurve::Linear { + spot_price: Uint128::from(10_000_000u128), + delta: Uint128::from(1_000_000u128), + }, + is_active: false, + asset_recipient: None, + }, + 0u64, + Uint128::zero(), + ); + + assert_eq!(test_pair.pair.internal.sell_to_pair_quote_summary, None); + assert_eq!(test_pair.pair.internal.buy_from_pair_quote_summary, None); + + let seller = setup_addtl_account(&mut router, "seller", INITIAL_BALANCE).unwrap(); + let token_id = mint_to(&mut router, &creator.clone(), &seller.clone(), &minter); + approve(&mut router, &seller, &collection, &test_pair.address.clone(), token_id.clone()); + + // Cannot swap with inactive pair + let response = router.execute_contract( + seller.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::SwapNftForTokens { + token_id: token_id.clone(), + min_output: coin(9_400_000u128, NATIVE_DENOM), + asset_recipient: None, + }, + &[], + ); + assert_error(response, ContractError::InvalidPair("pair is inactive".to_string()).to_string()); + + // Set pair to active + let response = router.execute_contract( + owner.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::UpdatePairConfig { + is_active: Some(true), + pair_type: None, + bonding_curve: None, + asset_recipient: None, + }, + &[], + ); + assert!(response.is_ok()); + + // Cannot swap nfts for tokens with a pair that does not own tokens + let response = router.execute_contract( + seller.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::SwapNftForTokens { + token_id: token_id.clone(), + min_output: coin(9_400_000u128, NATIVE_DENOM), + asset_recipient: None, + }, + &[], + ); + assert_error( + response, + ContractError::InvalidPair("pair cannot produce quote".to_string()).to_string(), + ); + + let response = router.execute_contract( + owner.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::DepositTokens {}, + &[coin(100_000_000u128, NATIVE_DENOM)], + ); + assert!(response.is_ok()); + + // Cannot swap tokens for NFTs with a pair that does not own NFTs + let response = router.execute_contract( + seller.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::SwapTokensForAnyNft { + asset_recipient: None, + }, + &[], + ); + assert_error( + response, + ContractError::InvalidPair("pair does not have any NFTs".to_string()).to_string(), + ); + + let num_nfts = 10u64; + for _ in 0..num_nfts { + let token_id = mint_to(&mut router, &creator.clone(), &owner.clone(), &minter); + + let response = router.execute_contract( + owner.clone(), + collection.clone(), + &Cw721ExecuteMsg::SendNft { + contract: test_pair.address.to_string(), + token_id: token_id.clone(), + msg: to_binary(&Empty {}).unwrap(), + }, + &[], + ); + assert!(response.is_ok()); + + test_pair.token_ids.push(token_id) + } + + test_pair.pair = router + .wrap() + .query_wasm_smart::(test_pair.address.clone(), &InfinityPairQueryMsg::Pair {}) + .unwrap(); + + assert_eq!( + test_pair.pair.internal.sell_to_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(100_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(500_000u128), + }), + swap: None, + seller_amount: Uint128::from(9_400_000u128), + }) + ); + assert_eq!( + test_pair.pair.internal.buy_from_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(110_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(550_000u128), + }), + swap: None, + seller_amount: Uint128::from(10_340_000u128), + }) + ); + + // Cannot swap with insufficient funds + let response = router.execute_contract( + bidder.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::SwapTokensForSpecificNft { + token_id: token_id.clone(), + asset_recipient: None, + }, + &[coin(1, NATIVE_DENOM)], + ); + assert_error( + response, + ContractError::InvalidPairQuote("payment required is greater than max input".to_string()) + .to_string(), + ); + + // Cannot swap using alt denom funds + let response = router.execute_contract( + bidder.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::SwapTokensForSpecificNft { + token_id: token_id.clone(), + asset_recipient: None, + }, + &[coin(10_000_000u128, UOSMO)], + ); + assert_error(response, "Must send reserve token 'ustars'".to_string()); + + // Cannot swap for unnowned NFT + let response = router.execute_contract( + bidder.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::SwapTokensForSpecificNft { + token_id: "99999".to_string(), + asset_recipient: None, + }, + &[coin(11_000_000u128, NATIVE_DENOM)], + ); + assert_error( + response, + InfinityError::InvalidInput("pair does not own NFT".to_string()).to_string(), + ); +} + +#[test] +fn try_trade_pair_linear_swaps() { + let vt = standard_minter_template(1000u32); + let InfinityTestSetup { + vending_template: + MinterTemplateResponse { + collection_response_vec, + mut router, + accts: + MarketAccounts { + creator, + owner, + bidder, + }, + }, + infinity_global, + infinity_factory, + .. + } = setup_infinity_test(vt).unwrap(); + + let collection_resp = &collection_response_vec[0]; + let minter = collection_resp.minter.clone().unwrap(); + let collection = collection_resp.collection.clone().unwrap(); + + let global_config = router + .wrap() + .query_wasm_smart::>( + infinity_global.clone(), + &InfinityGlobalQueryMsg::GlobalConfig {}, + ) + .unwrap(); + + let collection_info = router + .wrap() + .query_wasm_smart::( + collection.clone(), + &Sg721QueryMsg::CollectionInfo {}, + ) + .unwrap(); + + let mut test_pair = create_pair_with_deposits( + &mut router, + &infinity_global, + &infinity_factory, + &minter, + &collection, + &creator, + &owner, + PairConfig { + pair_type: PairType::Trade { + swap_fee_percent: Decimal::zero(), + reinvest_tokens: false, + reinvest_nfts: false, + }, + bonding_curve: BondingCurve::Linear { + spot_price: Uint128::from(10_000_000u128), + delta: Uint128::from(1_000_000u128), + }, + is_active: true, + asset_recipient: None, + }, + 10u64, + Uint128::from(100_000_000u128), + ); + + assert_eq!( + test_pair.pair.internal.sell_to_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(100_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(500_000u128), + }), + swap: None, + seller_amount: Uint128::from(9_400_000u128), + }) + ); + assert_eq!( + test_pair.pair.internal.buy_from_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(110_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(550_000u128), + }), + swap: None, + seller_amount: Uint128::from(10_340_000u128), + }) + ); + + let token_id = test_pair.token_ids[0].clone(); + + // Can swap for NFT + let response = router.execute_contract( + bidder.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::SwapTokensForSpecificNft { + token_id: token_id.clone(), + asset_recipient: None, + }, + &[coin(11_000_000u128, NATIVE_DENOM)], + ); + assert!(response.is_ok()); + assert_nft_owner(&router, &collection, token_id.clone(), &bidder); + + test_pair.pair = router + .wrap() + .query_wasm_smart::(test_pair.address.clone(), &InfinityPairQueryMsg::Pair {}) + .unwrap(); + + assert_eq!( + test_pair.pair.internal.sell_to_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(110_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(550_000u128), + }), + swap: None, + seller_amount: Uint128::from(10_340_000u128), + }) + ); + assert_eq!( + test_pair.pair.internal.buy_from_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(120_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(600_000u128), + }), + swap: None, + seller_amount: Uint128::from(11_280_000u128), + }) + ); + + // Can swap for tokens + approve(&mut router, &bidder, &collection, &test_pair.address.clone(), token_id.clone()); + let response = router.execute_contract( + bidder.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::SwapNftForTokens { + token_id: token_id.clone(), + min_output: coin(10_340_000u128, NATIVE_DENOM), + asset_recipient: None, + }, + &[], + ); + assert!(response.is_ok()); + assert_nft_owner(&router, &collection, token_id, &test_pair.pair.asset_recipient()); + + test_pair.pair = router + .wrap() + .query_wasm_smart::(test_pair.address.clone(), &InfinityPairQueryMsg::Pair {}) + .unwrap(); + + assert_eq!( + test_pair.pair.internal.sell_to_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(100_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(500_000u128), + }), + swap: None, + seller_amount: Uint128::from(9_400_000u128), + }) + ); + assert_eq!( + test_pair.pair.internal.buy_from_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(110_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(550_000u128), + }), + swap: None, + seller_amount: Uint128::from(10_340_000u128), + }) + ); +} + +#[test] +fn try_trade_pair_exponential_swaps() { + let vt = standard_minter_template(1000u32); + let InfinityTestSetup { + vending_template: + MinterTemplateResponse { + collection_response_vec, + mut router, + accts: + MarketAccounts { + creator, + owner, + bidder, + }, + }, + infinity_global, + infinity_factory, + .. + } = setup_infinity_test(vt).unwrap(); + + let collection_resp = &collection_response_vec[0]; + let minter = collection_resp.minter.clone().unwrap(); + let collection = collection_resp.collection.clone().unwrap(); + + let global_config = router + .wrap() + .query_wasm_smart::>( + infinity_global.clone(), + &InfinityGlobalQueryMsg::GlobalConfig {}, + ) + .unwrap(); + + let collection_info = router + .wrap() + .query_wasm_smart::( + collection.clone(), + &Sg721QueryMsg::CollectionInfo {}, + ) + .unwrap(); + + let mut test_pair = create_pair_with_deposits( + &mut router, + &infinity_global, + &infinity_factory, + &minter, + &collection, + &creator, + &owner, + PairConfig { + pair_type: PairType::Trade { + swap_fee_percent: Decimal::zero(), + reinvest_tokens: false, + reinvest_nfts: false, + }, + bonding_curve: BondingCurve::Exponential { + spot_price: Uint128::from(10_000_000u128), + delta: Decimal::percent(6), + }, + is_active: true, + asset_recipient: None, + }, + 10u64, + Uint128::from(100_000_000u128), + ); + + assert_eq!( + test_pair.pair.internal.sell_to_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(100_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(500_000u128), + }), + swap: None, + seller_amount: Uint128::from(9_400_000u128), + }) + ); + assert_eq!( + test_pair.pair.internal.buy_from_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(106_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(530_000u128), + }), + swap: None, + seller_amount: Uint128::from(9_964_000u128), + }) + ); + + let token_id = test_pair.token_ids[0].clone(); + + // Can swap for NFT + let response = router.execute_contract( + bidder.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::SwapTokensForSpecificNft { + token_id: token_id.clone(), + asset_recipient: None, + }, + &[coin(10_600_000u128, NATIVE_DENOM)], + ); + assert!(response.is_ok()); + assert_nft_owner(&router, &collection, token_id.clone(), &bidder); + + test_pair.pair = router + .wrap() + .query_wasm_smart::(test_pair.address.clone(), &InfinityPairQueryMsg::Pair {}) + .unwrap(); + + assert_eq!( + test_pair.pair.internal.sell_to_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(106_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(530_000u128), + }), + swap: None, + seller_amount: Uint128::from(9_964_000u128), + }) + ); + assert_eq!( + test_pair.pair.internal.buy_from_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(112_360u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(561_800u128), + }), + swap: None, + seller_amount: Uint128::from(10_561_840u128), + }) + ); + + // Can swap for tokens + approve(&mut router, &bidder, &collection, &test_pair.address.clone(), token_id.clone()); + let response = router.execute_contract( + bidder.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::SwapNftForTokens { + token_id: token_id.clone(), + min_output: coin(9_964_000u128, NATIVE_DENOM), + asset_recipient: None, + }, + &[], + ); + assert!(response.is_ok()); + assert_nft_owner(&router, &collection, token_id, &test_pair.pair.asset_recipient()); + + test_pair.pair = router + .wrap() + .query_wasm_smart::(test_pair.address.clone(), &InfinityPairQueryMsg::Pair {}) + .unwrap(); + + assert_eq!( + test_pair.pair.internal.sell_to_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(100_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(500_000u128), + }), + swap: None, + seller_amount: Uint128::from(9_400_000u128), + }) + ); + assert_eq!( + test_pair.pair.internal.buy_from_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(106_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(530_000u128), + }), + swap: None, + seller_amount: Uint128::from(9_964_000u128), + }) + ); +} + +#[test] +fn try_trade_pair_constant_product_swaps() { + let vt = standard_minter_template(1000u32); + let InfinityTestSetup { + vending_template: + MinterTemplateResponse { + collection_response_vec, + mut router, + accts: + MarketAccounts { + creator, + owner, + bidder, + }, + }, + infinity_global, + infinity_factory, + .. + } = setup_infinity_test(vt).unwrap(); + + let collection_resp = &collection_response_vec[0]; + let minter = collection_resp.minter.clone().unwrap(); + let collection = collection_resp.collection.clone().unwrap(); + + let global_config = router + .wrap() + .query_wasm_smart::>( + infinity_global.clone(), + &InfinityGlobalQueryMsg::GlobalConfig {}, + ) + .unwrap(); + + let collection_info = router + .wrap() + .query_wasm_smart::( + collection.clone(), + &Sg721QueryMsg::CollectionInfo {}, + ) + .unwrap(); + + let mut test_pair = create_pair_with_deposits( + &mut router, + &infinity_global, + &infinity_factory, + &minter, + &collection, + &creator, + &owner, + PairConfig { + pair_type: PairType::Trade { + swap_fee_percent: Decimal::zero(), + reinvest_tokens: false, + reinvest_nfts: false, + }, + bonding_curve: BondingCurve::ConstantProduct, + is_active: true, + asset_recipient: None, + }, + 10u64, + Uint128::from(100_000_000u128), + ); + + assert_eq!( + test_pair.pair.internal.sell_to_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(90_910u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(454_546u128), + }), + swap: None, + seller_amount: Uint128::from(8_545_453u128), + }) + ); + assert_eq!( + test_pair.pair.internal.buy_from_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(111_112u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(555_556u128), + }), + swap: None, + seller_amount: Uint128::from(10_444_444u128), + }) + ); + + let token_id = test_pair.token_ids[0].clone(); + + // Can swap for NFT + let response = router.execute_contract( + bidder.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::SwapTokensForSpecificNft { + token_id: token_id.clone(), + asset_recipient: None, + }, + &[coin(11_111_112u128, NATIVE_DENOM)], + ); + assert!(response.is_ok()); + assert_nft_owner(&router, &collection, token_id.clone(), &bidder); + + test_pair.pair = router + .wrap() + .query_wasm_smart::(test_pair.address.clone(), &InfinityPairQueryMsg::Pair {}) + .unwrap(); + + assert_eq!( + test_pair.pair.internal.sell_to_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(100_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(500_000u128), + }), + swap: None, + seller_amount: Uint128::from(9_400_000u128), + }) + ); + assert_eq!( + test_pair.pair.internal.buy_from_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(125_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(625_000u128), + }), + swap: None, + seller_amount: Uint128::from(11_750_000u128), + }) + ); + + // Can swap for tokens + approve(&mut router, &bidder, &collection, &test_pair.address.clone(), token_id.clone()); + let response = router.execute_contract( + bidder.clone(), + test_pair.address.clone(), + &InfinityPairExecuteMsg::SwapNftForTokens { + token_id: token_id.clone(), + min_output: coin(9_400_000u128, NATIVE_DENOM), + asset_recipient: None, + }, + &[], + ); + assert!(response.is_ok()); + assert_nft_owner(&router, &collection, token_id, &test_pair.pair.asset_recipient()); + + test_pair.pair = router + .wrap() + .query_wasm_smart::(test_pair.address.clone(), &InfinityPairQueryMsg::Pair {}) + .unwrap(); + + assert_eq!( + test_pair.pair.internal.sell_to_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(90_000u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(450_000u128), + }), + swap: None, + seller_amount: Uint128::from(8_460_000u128), + }) + ); + assert_eq!( + test_pair.pair.internal.buy_from_pair_quote_summary, + Some(QuoteSummary { + fair_burn: TokenPayment { + recipient: global_config.fair_burn.clone(), + amount: Uint128::from(112_500u128), + }, + royalty: Some(TokenPayment { + recipient: Addr::unchecked( + collection_info.royalty_info.as_ref().unwrap().payment_address.clone() + ), + amount: Uint128::from(562_500u128), + }), + swap: None, + seller_amount: Uint128::from(10_575_000u128), + }) + ); +} diff --git a/unit-tests/src/setup/setup_accounts.rs b/unit-tests/src/setup/setup_accounts.rs index c0df1b1..20a5ae5 100644 --- a/unit-tests/src/setup/setup_accounts.rs +++ b/unit-tests/src/setup/setup_accounts.rs @@ -1,9 +1,11 @@ -use cosmwasm_std::{coins, Addr, Coin, StdResult}; +use cosmwasm_std::{coin, coins, Addr, Coin, StdResult}; use cw_multi_test::SudoMsg as CwSudoMsg; use cw_multi_test::{BankSudo, SudoMsg}; use sg_multi_test::StargazeApp; use sg_std::NATIVE_DENOM; +use crate::setup::setup_infinity_contracts::UOSMO; + // all amounts in ustars pub const INITIAL_BALANCE: u128 = 5_000_000_000; pub const _MINT_PRICE: u128 = 100_000_000; @@ -20,7 +22,7 @@ pub fn setup_accounts(router: &mut StargazeApp) -> StdResult<(Addr, Addr, Addr)> let bidder: Addr = Addr::unchecked("bidder"); let creator: Addr = Addr::unchecked("creator"); let creator_funds: Vec = coins(2 * INITIAL_BALANCE, NATIVE_DENOM); - let funds: Vec = coins(INITIAL_BALANCE, NATIVE_DENOM); + let funds: Vec = vec![coin(INITIAL_BALANCE, UOSMO), coin(INITIAL_BALANCE, NATIVE_DENOM)]; router .sudo(SudoMsg::Bank({ BankSudo::Mint { @@ -66,7 +68,7 @@ pub fn setup_addtl_account( initial_balance: u128, ) -> StdResult { let addr: Addr = Addr::unchecked(input); - let funds: Vec = coins(initial_balance, NATIVE_DENOM); + let funds: Vec = vec![coin(initial_balance, UOSMO), coin(initial_balance, NATIVE_DENOM)]; router .sudo(CwSudoMsg::Bank({ BankSudo::Mint { diff --git a/unit-tests/src/setup/templates.rs b/unit-tests/src/setup/templates.rs index ba2757e..11f6f5e 100644 --- a/unit-tests/src/setup/templates.rs +++ b/unit-tests/src/setup/templates.rs @@ -99,9 +99,9 @@ pub struct InfinityTestSetup { pub infinity_pair_code_id: u64, } -pub fn setup_infinity_test(num_tokens: u32) -> Result { - let mut vt = standard_minter_template(num_tokens); - +pub fn setup_infinity_test( + mut vt: MinterTemplateResponse, +) -> Result { setup_block_time(&mut vt.router, GENESIS_MINT_START_TIME, None); let fair_burn = setup_fair_burn(&mut vt.router, &vt.accts.creator);