diff --git a/contracts/stargaze-marketplace-v2/src/execute.rs b/contracts/stargaze-marketplace-v2/src/execute.rs index 5d804bea..7759114a 100644 --- a/contracts/stargaze-marketplace-v2/src/execute.rs +++ b/contracts/stargaze-marketplace-v2/src/execute.rs @@ -248,7 +248,7 @@ pub fn execute_set_ask( only_tradable(&deps.querier, &env.block, &collection)?; let config = CONFIG.load(deps.storage)?; - only_valid_price(deps.storage, &config, &collection, &details.price)?; + only_valid_price(deps.storage, &config, &collection, &details.price, None)?; // Check and collect listing fee let listing_payment = one_coin(&info)?; @@ -349,7 +349,7 @@ pub fn execute_update_ask( ) ); - only_valid_price(deps.storage, &config, &ask.collection, &details.price)?; + only_valid_price(deps.storage, &config, &ask.collection, &details.price, None)?; ask.details = details; @@ -501,7 +501,6 @@ pub fn execute_set_bid( only_tradable(&deps.querier, &env.block, &collection)?; let config = CONFIG.load(deps.storage)?; - only_valid_price(deps.storage, &config, &collection, &details.price)?; let mut funds = NativeBalance(info.funds.clone()); funds.normalize(); @@ -511,9 +510,9 @@ pub fn execute_set_bid( let bid = Bid::new( info.sender.clone(), - collection, + collection.clone(), token_id, - details, + details.clone(), env.block.height, nonce, ); @@ -524,6 +523,13 @@ pub fn execute_set_bid( // bid could have a match with an existing ask without the need of buy_now if let Some(ask) = matching_ask { + only_valid_price( + deps.storage, + &config, + &collection, + &details.price, + Some(&ask.details.price.denom), + )?; // If a matching ask is found perform the sale funds = funds .sub(ask.details.price.clone()) @@ -552,7 +558,7 @@ pub fn execute_set_bid( // If no match is found. Bid creation should: // * store the bid // * emit event - + only_valid_price(deps.storage, &config, &collection, &details.price, None)?; funds = funds .sub(bid.details.price.clone()) .map_err(|_| ContractError::InsufficientFunds)?; @@ -609,7 +615,7 @@ pub fn execute_update_bid( ) ); - only_valid_price(deps.storage, &config, &bid.collection, &details.price)?; + only_valid_price(deps.storage, &config, &bid.collection, &details.price, None)?; let mut funds = NativeBalance(info.funds.clone()); funds.normalize(); @@ -775,7 +781,7 @@ pub fn execute_set_collection_bid( only_tradable(&deps.querier, &env.block, &collection)?; let config = CONFIG.load(deps.storage)?; - only_valid_price(deps.storage, &config, &collection, &details.price)?; + only_valid_price(deps.storage, &config, &collection, &details.price, None)?; let mut funds = NativeBalance(info.funds.clone()); funds.normalize(); @@ -883,6 +889,7 @@ pub fn execute_update_collection_bid( &config, &collection_bid.collection, &details.price, + None, )?; let mut funds = NativeBalance(info.funds.clone()); diff --git a/contracts/stargaze-marketplace-v2/src/helpers.rs b/contracts/stargaze-marketplace-v2/src/helpers.rs index bddd31c1..ff484eb4 100644 --- a/contracts/stargaze-marketplace-v2/src/helpers.rs +++ b/contracts/stargaze-marketplace-v2/src/helpers.rs @@ -52,24 +52,34 @@ pub fn only_contract_admin( Ok(()) } +// only_valid_price checks non zero amounts and enforces being in the collection denom or optional denom pub fn only_valid_price( storage: &dyn Storage, config: &Config, collection: &Addr, price: &Coin, + denom: Option<&str>, ) -> Result<(), ContractError> { ensure!( price.amount > Uint128::zero(), ContractError::InvalidInput("order price must be greater than 0".to_string()) ); - let query_result = COLLECTION_DENOMS.may_load(storage, collection.clone())?; - let collection_denom = query_result.unwrap_or(config.default_denom.clone()); - ensure_eq!( - collection_denom, - price.denom, - ContractError::InvalidInput("invalid denom".to_string()) - ); + if let Some(denom) = denom { + ensure_eq!( + denom, + price.denom, + ContractError::InvalidInput("invalid denom".to_string()) + ); + } else { + let query_result = COLLECTION_DENOMS.may_load(storage, collection.clone())?; + let collection_denom = query_result.unwrap_or(config.default_denom.clone()); + ensure_eq!( + collection_denom, + price.denom, + ContractError::InvalidInput("invalid denom".to_string()) + ); + } Ok(()) } diff --git a/contracts/stargaze-marketplace-v2/src/tests/unit_tests/sales.rs b/contracts/stargaze-marketplace-v2/src/tests/unit_tests/sales.rs index 2367a697..837be65c 100644 --- a/contracts/stargaze-marketplace-v2/src/tests/unit_tests/sales.rs +++ b/contracts/stargaze-marketplace-v2/src/tests/unit_tests/sales.rs @@ -845,3 +845,235 @@ fn try_accept_ask_invalid_inputs() { ContractError::InvalidInput("ask price is greater than max input".to_string()).to_string(), ); } + +#[test] +fn try_set_ask_with_old_denom() { + let TestContext { + mut app, + contracts: + TestContracts { + marketplace, + collection, + .. + }, + accounts: + TestAccounts { + creator, + owner, + bidder, + .. + }, + } = test_context(); + + let token_id = "1"; + mint(&mut app, &creator, &owner, &collection, token_id); + approve(&mut app, &owner, &collection, &marketplace, token_id); + + // Set ask with NATIVE_DENOM + let ask_price = coin(5_000_000, NATIVE_DENOM); + let set_ask = ExecuteMsg::SetAsk { + collection: collection.to_string(), + token_id: token_id.to_string(), + details: OrderDetails { + price: ask_price.clone(), + recipient: None, + finder: None, + }, + }; + let response = app.execute_contract( + owner.clone(), + marketplace.clone(), + &set_ask, + &[coin(LISTING_FEE, NATIVE_DENOM)], + ); + assert!(response.is_ok()); + + // Update collection denom to ATOM_DENOM + let update_collection_denom = ExecuteMsg::UpdateCollectionDenom { + collection: collection.to_string(), + denom: ATOM_DENOM.to_string(), + }; + let response = app.execute_contract( + creator.clone(), + marketplace.clone(), + &update_collection_denom, + &[], + ); + assert!(response.is_ok()); + let atom_bid = coin(5_000_000, ATOM_DENOM); + + // buying now should fail with ATOM_DENOM because listing is in NATIVE_DENOM + let buy_now = ExecuteMsg::BuySpecificNft { + collection: collection.to_string(), + token_id: token_id.to_string(), + details: OrderDetails { + price: atom_bid.clone(), + recipient: None, + finder: None, + }, + }; + let response = app.execute_contract( + bidder.clone(), + marketplace.clone(), + &buy_now, + &[atom_bid.clone()], + ); + assert!(response.is_err()); + + // set bid with ATOM_DENOM should succeed + let set_bid = ExecuteMsg::SetBid { + collection: collection.to_string(), + token_id: token_id.to_string(), + details: OrderDetails { + price: atom_bid.clone(), + recipient: None, + finder: None, + }, + }; + let response = app.execute_contract(bidder.clone(), marketplace.clone(), &set_bid, &[atom_bid]); + assert!(response.is_ok()); + + // set bid with NATIVE_DENOM should fail + let native_bid = coin(4_20, NATIVE_DENOM); + let set_bid_native = ExecuteMsg::SetBid { + collection: collection.to_string(), + token_id: token_id.to_string(), + details: OrderDetails { + price: native_bid.clone(), + recipient: None, + finder: None, + }, + }; + let response = app.execute_contract( + bidder.clone(), + marketplace.clone(), + &set_bid_native, + &[native_bid], + ); + assert!(response.is_err()); + + let buy_price = coin(5_000_000, NATIVE_DENOM); + let buy_now_asking_price = ExecuteMsg::BuySpecificNft { + collection: collection.to_string(), + token_id: token_id.to_string(), + details: OrderDetails { + price: buy_price.clone(), + recipient: None, + finder: None, + }, + }; + let response = app.execute_contract( + bidder, + marketplace.clone(), + &buy_now_asking_price, + &[buy_price], + ); + assert!(response.is_ok()); +} + +#[test] +fn try_match_bid_with_old_denom() { + let TestContext { + mut app, + contracts: + TestContracts { + marketplace, + collection, + .. + }, + accounts: + TestAccounts { + creator, + owner, + bidder, + .. + }, + } = test_context(); + + let token_id = "1"; + mint(&mut app, &creator, &owner, &collection, token_id); + approve(&mut app, &owner, &collection, &marketplace, token_id); + + // Set ask with NATIVE_DENOM + let ask_price = coin(5_000_000, NATIVE_DENOM); + let set_ask = ExecuteMsg::SetAsk { + collection: collection.to_string(), + token_id: token_id.to_string(), + details: OrderDetails { + price: ask_price.clone(), + recipient: None, + finder: None, + }, + }; + let response = app.execute_contract( + owner.clone(), + marketplace.clone(), + &set_ask, + &[coin(LISTING_FEE, NATIVE_DENOM)], + ); + assert!(response.is_ok()); + + // Update collection denom to ATOM_DENOM + let update_collection_denom = ExecuteMsg::UpdateCollectionDenom { + collection: collection.to_string(), + denom: ATOM_DENOM.to_string(), + }; + let response = app.execute_contract( + creator.clone(), + marketplace.clone(), + &update_collection_denom, + &[], + ); + assert!(response.is_ok()); + let atom_bid = coin(5_000_000, ATOM_DENOM); + + // buying now should fail with ATOM_DENOM because listing is in NATIVE_DENOM + let buy_now = ExecuteMsg::BuySpecificNft { + collection: collection.to_string(), + token_id: token_id.to_string(), + details: OrderDetails { + price: atom_bid.clone(), + recipient: None, + finder: None, + }, + }; + let response = app.execute_contract( + bidder.clone(), + marketplace.clone(), + &buy_now, + &[atom_bid.clone()], + ); + assert!(response.is_err()); + + // set bid with ATOM_DENOM should succeed + let set_bid = ExecuteMsg::SetBid { + collection: collection.to_string(), + token_id: token_id.to_string(), + details: OrderDetails { + price: atom_bid.clone(), + recipient: None, + finder: None, + }, + }; + let response = app.execute_contract(bidder.clone(), marketplace.clone(), &set_bid, &[atom_bid]); + assert!(response.is_ok()); + + // set bid with NATIVE_DENOM suceed if it matches the ask price + let native_bid = coin(5_000_000, NATIVE_DENOM); + let set_bid_native = ExecuteMsg::SetBid { + collection: collection.to_string(), + token_id: token_id.to_string(), + details: OrderDetails { + price: native_bid.clone(), + recipient: None, + finder: None, + }, + }; + let response = app.execute_contract( + bidder.clone(), + marketplace.clone(), + &set_bid_native, + &[native_bid], + ); + assert!(response.is_ok()); +}