From 9f531b30e3072f8f89b044c3efa545520e0c6216 Mon Sep 17 00:00:00 2001 From: anilcse Date: Mon, 11 Nov 2024 18:47:19 +0530 Subject: [PATCH 1/2] Add more tests --- Cargo.lock | 18 ++ Cargo.toml | 3 +- src/contract.rs | 315 ++++++++++++++++--------- src/error.rs | 44 ++-- src/lib.rs | 3 + src/msg.rs | 4 +- src/state.rs | 8 +- src/tests/bids_tests.rs | 405 +++++++++++++++++++++++++++++++++ {tests => src/tests}/common.rs | 67 +++--- src/tests/deals_tests.rs | 279 +++++++++++++++++++++++ src/tests/mod.rs | 5 + tests/deals.rs | 1 - tests/integration_tests.rs | 224 ++++++++++++++++++ tests/mod.rs | 1 - tests/queries.rs | 1 - 15 files changed, 1223 insertions(+), 155 deletions(-) create mode 100644 src/tests/bids_tests.rs rename {tests => src/tests}/common.rs (87%) create mode 100644 src/tests/deals_tests.rs create mode 100644 src/tests/mod.rs delete mode 100644 tests/deals.rs create mode 100644 tests/integration_tests.rs delete mode 100644 tests/mod.rs delete mode 100644 tests/queries.rs diff --git a/Cargo.lock b/Cargo.lock index 32b1c666..e842c510 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -244,6 +244,7 @@ dependencies = [ "cw-storage-plus", "cw2", "cw20", + "cw20-base", "schemars", "serde", "thiserror", @@ -303,6 +304,23 @@ dependencies = [ "serde", ] +[[package]] +name = "cw20-base" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ad79e86ea3707229bf78df94e08732e8f713207b4a77b2699755596725e7d9" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "cw2", + "cw20", + "schemars", + "semver", + "serde", + "thiserror", +] + [[package]] name = "der" version = "0.7.9" diff --git a/Cargo.toml b/Cargo.toml index 01a29e70..e1692bfb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,4 +51,5 @@ serde = { version = "1.0.188", default-features = false, features = ["derive"] } thiserror = "1.0.49" [dev-dependencies] -cw-multi-test = "0.20.0" \ No newline at end of file +cw-multi-test = "0.20.0" +cw20-base = "1.1.1" diff --git a/src/contract.rs b/src/contract.rs index 6b9fd0d9..efc7107f 100644 --- a/src/contract.rs +++ b/src/contract.rs @@ -1,6 +1,6 @@ use crate::error::ContractError; use crate::helpers::{ - calculate_platform_fee, create_payment_msg, create_token_transfer_msg, get_sorted_bids, + calculate_platform_fee, get_sorted_bids, validate_deal_times, }; use crate::msg::{ExecuteMsg, InstantiateMsg}; @@ -8,10 +8,11 @@ use crate::state::{Bid, Config, Deal, BIDS, CONFIG, DEALS, DEAL_COUNTER}; use cosmwasm_std::Addr; use cosmwasm_std::StdError; use cosmwasm_std::{ - entry_point, to_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Order, Response, - StdResult, Storage, Uint128, + entry_point, to_binary, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, + Order, Response, StdResult, Storage, Uint128, }; use cw2::set_contract_version; +use cw20::Cw20ExecuteMsg; use crate::msg::{BidResponse, DealResponse, DealStatsResponse, DealsResponse, QueryMsg}; use cw_storage_plus::Bound; @@ -60,6 +61,7 @@ pub fn execute( match msg { ExecuteMsg::CreateDeal { sell_token, + bid_token_denom, total_amount, min_price, discount_percentage, @@ -72,6 +74,7 @@ pub fn execute( env, info, sell_token, + bid_token_denom, total_amount, min_price, discount_percentage, @@ -119,6 +122,7 @@ pub fn execute_create_deal( env: Env, info: MessageInfo, sell_token: String, + bid_token_denom: String, total_amount: Uint128, min_price: Uint128, discount_percentage: u64, @@ -142,6 +146,19 @@ pub fn execute_create_deal( }); } + if min_price.is_zero() { + return Err(ContractError::InvalidTimeParameters { + reason: "MinPrice must not be zero".to_string(), + }); + } + + // Validate bid token denomination + if bid_token_denom.is_empty() { + return Err(ContractError::InvalidDenom { + reason: "Bid token denomination cannot be empty".to_string(), + }); + } + // Calculate and validate platform fee let config = CONFIG.load(deps.storage)?; let platform_fee = calculate_platform_fee(total_amount, config.platform_fee_percentage)?; @@ -150,7 +167,7 @@ pub fn execute_create_deal( let provided_fee = info .funds .iter() - .find(|c| c.denom == "uusd") // Replace with your desired denomination + .find(|c| c.denom == bid_token_denom) .map(|c| c.amount) .unwrap_or_default(); @@ -168,6 +185,7 @@ pub fn execute_create_deal( let deal = Deal { seller: info.sender.to_string(), sell_token, + bid_token_denom, total_amount, min_price, discount_percentage, @@ -177,6 +195,8 @@ pub fn execute_create_deal( conclude_time, is_concluded: false, total_bids_amount: Uint128::zero(), + total_tokens_sold: Uint128::zero(), + total_payment_received: Uint128::zero(), }; DEALS.save(deps.storage, deal_id, &deal)?; @@ -215,6 +235,22 @@ pub fn execute_place_bid( }); } + // Validate bid payment + let payment = info + .funds + .iter() + .find(|c| c.denom == deal.bid_token_denom) + .ok_or(ContractError::NoBidPayment {})?; + + if payment.amount != amount { + return Err(ContractError::InvalidBidAmount { + reason: format!( + "Bid amount {} does not match sent payment {}", + amount, payment.amount + ), + }); + } + // Validate discount percentage if discount_percentage > 10000 { return Err(ContractError::InvalidBidAmount { @@ -263,9 +299,34 @@ pub fn execute_update_bid( return Err(ContractError::BiddingEnded {}); } + // Validate new bid payment + let payment = info + .funds + .iter() + .find(|c| c.denom == deal.bid_token_denom) + .ok_or(ContractError::NoBidPayment {})?; + + if payment.amount != new_amount { + return Err(ContractError::InvalidBidAmount { + reason: format!( + "New bid amount {} does not match sent payment {}", + new_amount, payment.amount + ), + }); + } + // Load existing bid let old_bid = BIDS.load(deps.storage, (deal_id, &info.sender))?; + // Create refund message for old bid + let refund_msg = BankMsg::Send { + to_address: info.sender.to_string(), + amount: vec![Coin { + denom: deal.bid_token_denom.clone(), + amount: old_bid.amount, + }], + }; + // Update total bids amount let amount_diff = new_amount.checked_sub(old_bid.amount)?; DEALS.update(deps.storage, deal_id, |deal_opt| -> StdResult<_> { @@ -284,6 +345,7 @@ pub fn execute_update_bid( BIDS.save(deps.storage, (deal_id, &info.sender), &new_bid)?; Ok(Response::new() + .add_message(refund_msg) .add_attribute("method", "update_bid") .add_attribute("deal_id", deal_id.to_string()) .add_attribute("bidder", info.sender)) @@ -307,6 +369,15 @@ pub fn execute_withdraw_bid( let bid = BIDS.load(deps.storage, (deal_id, &info.sender))?; BIDS.remove(deps.storage, (deal_id, &info.sender)); + // Create refund message + let refund_msg = BankMsg::Send { + to_address: info.sender.to_string(), + amount: vec![Coin { + denom: deal.bid_token_denom, + amount: bid.amount, + }], + }; + // Update total bids amount DEALS.update(deps.storage, deal_id, |deal_opt| -> StdResult<_> { let mut deal = deal_opt.unwrap(); @@ -315,6 +386,7 @@ pub fn execute_withdraw_bid( })?; Ok(Response::new() + .add_message(refund_msg) .add_attribute("method", "withdraw_bid") .add_attribute("deal_id", deal_id.to_string()) .add_attribute("bidder", info.sender)) @@ -348,145 +420,174 @@ pub fn execute_conclude_deal( _info: MessageInfo, deal_id: u64, ) -> Result { - // Load deal data let mut deal = DEALS.load(deps.storage, deal_id)?; - // Validation: Check timing and conclusion status - if env.block.time.seconds() < deal.conclude_time { - return Err(ContractError::ConclusionTimeNotReached {}); - } + println!("deal min pice {}", deal.min_price); + + // Validate deal status if deal.is_concluded { return Err(ContractError::DealAlreadyConcluded {}); } - // Case 1: Minimum cap not met - refund all bidders + let current_time = env.block.time.seconds(); + if current_time < deal.conclude_time { + return Err(ContractError::ConclusionTimeNotReached {}); + } + + let mut response = Response::new(); + + // Check if minimum cap is met if deal.total_bids_amount < deal.min_cap { - let refund_messages = process_failed_deal(deps.storage, deal_id)?; + // Process refunds for failed deal + let bids = BIDS.prefix(deal_id); + for result in bids.range(deps.storage, None, None, Order::Ascending) { + let (_, bid) = result?; + let bidder_addr = deps.api.addr_validate(&bid.bidder)?; + + // Create refund message for the bid amount + let refund_msg = BankMsg::Send { + to_address: bidder_addr.to_string(), + amount: vec![Coin { + denom: deal.bid_token_denom.clone(), + amount: bid.amount, + }], + }; + response = response.add_message(refund_msg); + } - // Mark deal as concluded + // Mark deal as concluded and save deal.is_concluded = true; DEALS.save(deps.storage, deal_id, &deal)?; - return Ok(Response::new() - .add_messages(refund_messages) + return Ok(response .add_attribute("method", "conclude_deal_refund") - .add_attribute("deal_id", deal_id.to_string()) .add_attribute("reason", "min_cap_not_met") - .add_attribute("total_refunded", deal.total_bids_amount)); + .add_attribute("total_refunded", deal.total_bids_amount.to_string())); } - // Case 2: Process successful deal - let (messages, stats) = process_successful_deal(deps.storage, &deal, deal_id)?; - - // Mark deal as concluded - deal.is_concluded = true; - DEALS.save(deps.storage, deal_id, &deal)?; - - Ok(Response::new() - .add_messages(messages) - .add_attribute("method", "conclude_deal") - .add_attribute("deal_id", deal_id.to_string()) - .add_attribute("tokens_sold", stats.tokens_sold) - .add_attribute("total_payment", stats.total_payment) - .add_attribute("successful_bids", stats.successful_bids.to_string()) - .add_attribute("refunded_bids", stats.refunded_bids.to_string())) -} - -/// Helper struct to track deal conclusion statistics -struct DealStats { - tokens_sold: Uint128, - total_payment: Uint128, - successful_bids: u32, - refunded_bids: u32, -} - -/// Processes a failed deal by refunding all bidders -fn process_failed_deal( - storage: &dyn Storage, - deal_id: u64, -) -> Result, ContractError> { - let mut messages: Vec = vec![]; - let bids = get_sorted_bids(storage, deal_id)?; - - for (bidder, bid) in bids { - messages.push(create_payment_msg( - bidder, bid.amount, "uusd", // Replace with actual denom - )); - } - - Ok(messages) -} - -/// Processes a successful deal by allocating tokens and handling payments -fn process_successful_deal( - storage: &dyn Storage, - deal: &Deal, - deal_id: u64, -) -> Result<(Vec, DealStats), ContractError> { - let mut messages: Vec = vec![]; - let mut stats = DealStats { - tokens_sold: Uint128::zero(), - total_payment: Uint128::zero(), - successful_bids: 0, - refunded_bids: 0, - }; + // Process successful deal + let mut total_tokens_sold = Uint128::zero(); + let mut total_payment_received = Uint128::zero(); + let seller_addr = deps.api.addr_validate(&deal.seller)?; + // Get sorted bids (by discount percentage, lowest first) + let sorted_bids = get_sorted_bids(deps.storage, deal_id)?; let mut remaining_tokens = deal.total_amount; - let bids = get_sorted_bids(storage, deal_id)?; - // Process bids from lowest to highest discount - for (bidder, bid) in bids { + // Process each bid + for (bidder_addr, bid) in sorted_bids { if remaining_tokens.is_zero() { - // Refund remaining bids - messages.push(create_payment_msg(bidder, bid.amount, "uusd")); - stats.refunded_bids += 1; + // No more tokens available, refund remaining bids + let refund_msg = BankMsg::Send { + to_address: bidder_addr.to_string(), + amount: vec![Coin { + denom: deal.bid_token_denom.clone(), + amount: bid.amount, + }], + }; + response = response.add_message(refund_msg); continue; } - // Calculate token allocation - let tokens_to_receive = std::cmp::min(bid.amount, remaining_tokens); + // Calculate the effective price after discount + let effective_discount = deal.discount_percentage.min(bid.discount_percentage); + + println!("deal effective_discount {}", effective_discount); - // Calculate final price with discount - let base_price = tokens_to_receive.multiply_ratio(deal.min_price, Uint128::new(1u128)); - let discount = base_price.multiply_ratio(bid.discount_percentage, 100u128); - let final_price = base_price.checked_sub(discount)?; + let price_per_token = deal + .min_price + .multiply_ratio(10_000u128 - effective_discount as u128, 10_000u128); - // Check if price meets buyer's max price constraint + // Check if price exceeds bidder's max price if let Some(max_price) = bid.max_price { - if final_price > max_price { - messages.push(create_payment_msg(bidder, bid.amount, "uusd")); - stats.refunded_bids += 1; + if price_per_token > max_price { + // Price too high, refund this bid + let refund_msg = BankMsg::Send { + to_address: bidder_addr.to_string(), + amount: vec![Coin { + denom: deal.bid_token_denom.clone(), + amount: bid.amount, + }], + }; + response = response.add_message(refund_msg); continue; } } - // Process successful bid + println!("deal price_per_token {}", price_per_token); - // 1. Transfer tokens to buyer - messages.push(create_token_transfer_msg( - deal.sell_token.clone(), - bidder.clone(), - tokens_to_receive, - )?); + // Calculate tokens to allocate + let tokens_to_transfer = std::cmp::min( + bid.amount.multiply_ratio(Uint128::new(1), price_per_token), + remaining_tokens, + ); + let payment_amount = tokens_to_transfer.multiply_ratio(price_per_token, Uint128::new(1)); - // 2. Transfer payment to seller - messages.push(create_payment_msg(deal.seller.clone(), final_price, "uusd")); + if tokens_to_transfer.is_zero() { + // Skip if no tokens would be transferred + continue; + } - // Update running totals - remaining_tokens = remaining_tokens.checked_sub(tokens_to_receive)?; - stats.tokens_sold += tokens_to_receive; - stats.total_payment += final_price; - stats.successful_bids += 1; - } + // Calculate refund if partial fill + let refund_amount = bid.amount.checked_sub(payment_amount)?; + if !refund_amount.is_zero() { + let refund_msg = BankMsg::Send { + to_address: bidder_addr.to_string(), + amount: vec![Coin { + denom: deal.bid_token_denom.clone(), + amount: refund_amount, + }], + }; + response = response.add_message(refund_msg); + } - // Validate all tokens are accounted for - if stats.tokens_sold + remaining_tokens != deal.total_amount { - return Err(ContractError::InvalidBidAmount { - reason: "Token allocation mismatch".to_string(), + // Transfer tokens to bidder + let transfer_msg = CosmosMsg::Wasm(cosmwasm_std::WasmMsg::Execute { + contract_addr: deal.sell_token.clone(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: bidder_addr.to_string(), + amount: tokens_to_transfer, + })?, + funds: vec![], }); + response = response.add_message(transfer_msg); + + // Update totals + total_tokens_sold += tokens_to_transfer; + total_payment_received += payment_amount; + remaining_tokens = remaining_tokens.checked_sub(tokens_to_transfer)?; } - Ok((messages, stats)) + // Transfer total payment to seller + let seller_payment_msg = BankMsg::Send { + to_address: seller_addr.to_string(), + amount: vec![Coin { + denom: deal.bid_token_denom.clone(), + amount: total_payment_received, + }], + }; + response = response.add_message(seller_payment_msg); + + // Update deal state + deal.is_concluded = true; + deal.total_tokens_sold = total_tokens_sold; + deal.total_payment_received = total_payment_received; + DEALS.save(deps.storage, deal_id, &deal)?; + + Ok(response + .add_attribute("method", "conclude_deal") + .add_attribute("deal_id", deal_id.to_string()) + .add_attribute("tokens_sold", total_tokens_sold) + .add_attribute("total_payment", total_payment_received) + .add_attribute("remaining_tokens", remaining_tokens)) +} + +/// Helper struct to track deal conclusion statistics +struct DealStats { + tokens_sold: Uint128, + total_payment: Uint128, + successful_bids: u32, + refunded_bids: u32, } const DEFAULT_LIMIT: u32 = 10; diff --git a/src/error.rs b/src/error.rs index 005181df..1e4e6f3a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,33 +9,51 @@ pub enum ContractError { #[error("{0}")] Overflow(#[from] OverflowError), - #[error("Unauthorized")] - Unauthorized {}, - #[error("Deal not found")] DealNotFound {}, #[error("Bid not found")] BidNotFound {}, - #[error("Deal already concluded")] - DealAlreadyConcluded {}, - - #[error("Invalid time parameters: {reason}")] + /// Error for invalid time parameters + #[error("Invalid Time Parameters: {reason}")] InvalidTimeParameters { reason: String }, - #[error("Invalid bid amount: {reason}")] - InvalidBidAmount { reason: String }, - - #[error("Insufficient platform fee. Required: {required}, provided: {provided}")] + /// Error for insufficient platform fee provided by the seller + #[error("Insufficient Platform Fee: required {required}, provided {provided}")] InsufficientPlatformFee { required: u128, provided: u128 }, - #[error("Bidding has not started")] + /// Error when bidding has not started yet + #[error("Bidding has not started yet")] BiddingNotStarted {}, + /// Error when bidding has already ended #[error("Bidding has ended")] BiddingEnded {}, - #[error("Conclusion time not reached")] + #[error("Invalid denomination: {reason}")] + InvalidDenom { reason: String }, + + #[error("No bid payment provided")] + NoBidPayment {}, + + /// Error for invalid bid amount or parameters + #[error("Invalid Bid Amount: {reason}")] + InvalidBidAmount { reason: String }, + + /// Error when conclusion time has not been reached yet + #[error("Conclusion time has not been reached")] ConclusionTimeNotReached {}, + + /// Error when attempting to conclude a deal that is already concluded + #[error("Deal has already been concluded")] + DealAlreadyConcluded {}, + + /// Error when a bid already exists for a bidder on a deal + #[error("Bid already exists for this bidder")] + BidAlreadyExists {}, + + /// Error for unauthorized actions + #[error("Unauthorized")] + Unauthorized {}, } diff --git a/src/lib.rs b/src/lib.rs index 55f52bf1..f1495d81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,3 +18,6 @@ pub mod error; pub mod helpers; pub mod msg; pub mod state; + +#[cfg(test)] +mod tests; diff --git a/src/msg.rs b/src/msg.rs index b97c92e9..4e6f5d43 100644 --- a/src/msg.rs +++ b/src/msg.rs @@ -17,8 +17,10 @@ pub struct InstantiateMsg { pub enum ExecuteMsg { /// Creates a new OTC deal CreateDeal { - /// Address of the token being sold + /// Denom of the token being sold sell_token: String, + /// Denom of the token being accepted for purchase + bid_token_denom: String, /// Total amount of tokens to sell total_amount: Uint128, /// Minimum price per token diff --git a/src/state.rs b/src/state.rs index 1e293569..189aad47 100644 --- a/src/state.rs +++ b/src/state.rs @@ -10,16 +10,19 @@ pub struct Config { #[cw_serde] pub struct Deal { pub seller: String, - pub sell_token: String, + pub sell_token: String, // CW20 token address being sold + pub bid_token_denom: String, // Native token denom for bids (e.g., "uusd") pub total_amount: Uint128, pub min_price: Uint128, - pub discount_percentage: u64, // In basis points (100 = 1%) + pub discount_percentage: u64, pub min_cap: Uint128, pub bid_start_time: u64, pub bid_end_time: u64, pub conclude_time: u64, pub is_concluded: bool, pub total_bids_amount: Uint128, + pub total_tokens_sold: Uint128, + pub total_payment_received: Uint128, } #[cw_serde] @@ -37,6 +40,7 @@ pub const DEAL_COUNTER: Item = Item::new("deal_counter"); // Maps for storing deals and bids pub const DEALS: Map = Map::new("deals"); pub const BIDS: Map<(u64, &Addr), Bid> = Map::new("bids"); +pub const BID_COUNTER: Map<(u64, &Addr), u64> = Map::new("bid_counter"); // Optional indexes for more efficient queries pub const SELLER_DEALS: Map<(&Addr, u64), u64> = Map::new("seller_deals"); diff --git a/src/tests/bids_tests.rs b/src/tests/bids_tests.rs new file mode 100644 index 00000000..bdd6d59a --- /dev/null +++ b/src/tests/bids_tests.rs @@ -0,0 +1,405 @@ +use crate::contract::{execute, instantiate}; +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, InstantiateMsg}; +use crate::state::{BIDS, DEALS}; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; +use cosmwasm_std::{coins, Addr, DepsMut, Env, Order, Timestamp, Uint128}; + +#[cfg(test)] +mod tests { + use super::*; + + const PLATFORM_FEE_PERCENTAGE: u64 = 100; // 1% + const MOCK_SELL_TOKEN: &str = "token"; + const MOCK_PAYMENT_DENOM: &str = "uusdc"; + + fn setup_contract(deps: DepsMut) { + let msg = InstantiateMsg { + platform_fee_percentage: PLATFORM_FEE_PERCENTAGE, + }; + let info = mock_info("creator", &[]); + let res = instantiate(deps, mock_env(), info, msg).unwrap(); + assert_eq!(0, res.messages.len()); + } + + // Helper function to create mock environment with specified time + fn mock_env_at_time(timestamp: u64) -> Env { + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(timestamp); + env + } + + // Helper function to create a standard test deal message with future timestamps + fn create_test_deal_msg(start_time: u64) -> ExecuteMsg { + ExecuteMsg::CreateDeal { + sell_token: MOCK_SELL_TOKEN.to_string(), + bid_token_denom: "uusdc".to_string(), + total_amount: Uint128::new(1000000u128), + min_price: Uint128::new(1u128), + discount_percentage: 1000, // 10% + min_cap: Uint128::new(500000u128), + bid_start_time: start_time + 1000, // Ensure future start + bid_end_time: start_time + 2000, // End time after start + conclude_time: start_time + 3000, // Conclude time after end + } + } + + #[test] + fn test_place_bid() { + let mut deps = mock_dependencies(); + setup_contract(deps.as_mut()); + + let start_time = 1000u64; + let env = mock_env_at_time(start_time); + + // Create deal + let total_amount = Uint128::new(1000000u128); + let platform_fee = total_amount.multiply_ratio(PLATFORM_FEE_PERCENTAGE as u128, 10000u128); + let create_msg = create_test_deal_msg(start_time); + + let info = mock_info("seller", &coins(platform_fee.u128(), MOCK_PAYMENT_DENOM)); + execute(deps.as_mut(), env.clone(), info, create_msg).unwrap(); + + // Place a bid + let bid_env = mock_env_at_time(start_time + 1500); // During bidding period + let bid_amount = Uint128::new(100000u128); + + let bid_msg = ExecuteMsg::PlaceBid { + deal_id: 1, + amount: bid_amount, + discount_percentage: 500, + max_price: None, + }; + + // Include bid payment in mock_info + let info = mock_info("bidder1", &coins(bid_amount.u128(), MOCK_PAYMENT_DENOM)); + execute(deps.as_mut(), bid_env.clone(), info.clone(), bid_msg).unwrap(); + + // Verify the bid + let bid = BIDS + .load(deps.as_ref().storage, (1, &Addr::unchecked("bidder1"))) + .unwrap(); + assert_eq!(bid.amount, bid_amount); + } + + #[test] + fn test_place_bid_before_bidding_starts() { + let mut deps = mock_dependencies(); + setup_contract(deps.as_mut()); + + let start_time = 1000u64; + let env = mock_env_at_time(start_time); + + // Create deal + let total_amount = Uint128::new(1000000u128); + let platform_fee = total_amount.multiply_ratio(PLATFORM_FEE_PERCENTAGE as u128, 10000u128); + let create_msg = create_test_deal_msg(start_time); + + let info = mock_info("seller", &coins(platform_fee.u128(), MOCK_PAYMENT_DENOM)); + execute(deps.as_mut(), env.clone(), info, create_msg).unwrap(); + + // Attempt to place a bid before bidding starts + let bid_env = mock_env_at_time(start_time + 500); // Before bidding period + let bid_amount = Uint128::new(100000u128); + + let bid_msg = ExecuteMsg::PlaceBid { + deal_id: 1, + amount: bid_amount, + discount_percentage: 500, + max_price: Some(Uint128::new(95000u128)), + }; + + let info = mock_info("bidder1", &coins(bid_amount.u128(), MOCK_PAYMENT_DENOM)); + let err = execute(deps.as_mut(), bid_env.clone(), info, bid_msg).unwrap_err(); + assert!(matches!(err, ContractError::BiddingNotStarted {})); + } + + #[test] + fn test_place_bid_after_bidding_ends() { + let mut deps = mock_dependencies(); + setup_contract(deps.as_mut()); + + let start_time = 1000u64; + let env = mock_env_at_time(start_time); + + // Create deal + let total_amount = Uint128::new(1000000u128); + let platform_fee = total_amount.multiply_ratio(PLATFORM_FEE_PERCENTAGE as u128, 10000u128); + let create_msg = create_test_deal_msg(start_time); + + let info = mock_info("seller", &coins(platform_fee.u128(), MOCK_PAYMENT_DENOM)); + execute(deps.as_mut(), env.clone(), info, create_msg).unwrap(); + + // Attempt to place a bid after bidding ends + let bid_env = mock_env_at_time(start_time + 2500); // After bidding period + let bid_amount = Uint128::new(100000u128); + + let bid_msg = ExecuteMsg::PlaceBid { + deal_id: 1, + amount: bid_amount, + discount_percentage: 500, + max_price: Some(Uint128::new(95000u128)), + }; + + let info = mock_info("bidder1", &coins(bid_amount.u128(), MOCK_PAYMENT_DENOM)); + let err = execute(deps.as_mut(), bid_env.clone(), info, bid_msg).unwrap_err(); + assert!(matches!(err, ContractError::BiddingEnded {})); + } + + #[test] + fn test_withdraw_bid() { + let mut deps = mock_dependencies(); + setup_contract(deps.as_mut()); + + let start_time = 1000u64; + let env = mock_env_at_time(start_time); + + // Create deal + let total_amount = Uint128::new(1000000u128); + let platform_fee = total_amount.multiply_ratio(PLATFORM_FEE_PERCENTAGE as u128, 10000u128); + let create_msg = create_test_deal_msg(start_time); + + let info = mock_info("seller", &coins(platform_fee.u128(), MOCK_PAYMENT_DENOM)); + execute(deps.as_mut(), env.clone(), info, create_msg).unwrap(); + + // Place a bid + let bid_env = mock_env_at_time(start_time + 1500); // During bidding period + let bid_amount = Uint128::new(100000u128); + let bid_msg = ExecuteMsg::PlaceBid { + deal_id: 1, + amount: bid_amount, + discount_percentage: 500, + max_price: None, + }; + + let info = mock_info("bidder1", &coins(bid_amount.u128(), MOCK_PAYMENT_DENOM)); + execute(deps.as_mut(), bid_env.clone(), info.clone(), bid_msg).unwrap(); + + // Withdraw the bid + let withdraw_msg = ExecuteMsg::WithdrawBid { deal_id: 1 }; + let res = execute(deps.as_mut(), bid_env.clone(), info.clone(), withdraw_msg).unwrap(); + assert_eq!(res.attributes.len(), 3); + + // Verify the bid is removed + let bid = BIDS + .may_load(deps.as_ref().storage, (1, &Addr::unchecked("bidder1"))) + .unwrap(); + assert!(bid.is_none()); + } + + #[test] + fn test_withdraw_bid_after_bidding_ends() { + let mut deps = mock_dependencies(); + setup_contract(deps.as_mut()); + + let start_time = 1000u64; + let env = mock_env_at_time(start_time); + + // Create deal + let total_amount = Uint128::new(1000000u128); + let platform_fee = total_amount.multiply_ratio(PLATFORM_FEE_PERCENTAGE as u128, 10000u128); + let create_msg = create_test_deal_msg(start_time); + + let info = mock_info("seller", &coins(platform_fee.u128(), MOCK_PAYMENT_DENOM)); + execute(deps.as_mut(), env.clone(), info, create_msg).unwrap(); + + // Place a bid + let bid_env = mock_env_at_time(start_time + 1500); // During bidding period + let bid_amount = Uint128::new(100000u128); + let bid_msg = ExecuteMsg::PlaceBid { + deal_id: 1, + amount: bid_amount, + discount_percentage: 500, + max_price: None, + }; + + let info = mock_info("bidder1", &coins(bid_amount.u128(), MOCK_PAYMENT_DENOM)); + execute(deps.as_mut(), bid_env.clone(), info.clone(), bid_msg).unwrap(); + + // Attempt to withdraw the bid after bidding ends + let withdraw_env = mock_env_at_time(start_time + 2500); // After bidding period + let withdraw_msg = ExecuteMsg::WithdrawBid { deal_id: 1 }; + let err = execute( + deps.as_mut(), + withdraw_env.clone(), + info.clone(), + withdraw_msg, + ) + .unwrap_err(); + assert!(matches!(err, ContractError::BiddingEnded {})); + } + + #[test] + fn test_update_bid_after_bidding_ends() { + let mut deps = mock_dependencies(); + setup_contract(deps.as_mut()); + + let start_time = 1000u64; + let env = mock_env_at_time(start_time); + + // Create deal + let total_amount = Uint128::new(1000000u128); + let platform_fee = total_amount.multiply_ratio(PLATFORM_FEE_PERCENTAGE as u128, 10000u128); + let create_msg = create_test_deal_msg(start_time); + + let info = mock_info("seller", &coins(platform_fee.u128(), MOCK_PAYMENT_DENOM)); + execute(deps.as_mut(), env.clone(), info, create_msg).unwrap(); + + // Place a bid + let bid_env = mock_env_at_time(start_time + 1500); // During bidding period + let bid_amount = Uint128::new(100000u128); + let bid_msg = ExecuteMsg::PlaceBid { + deal_id: 1, + amount: bid_amount, + discount_percentage: 500, + max_price: None, + }; + + let info = mock_info("bidder1", &coins(bid_amount.u128(), MOCK_PAYMENT_DENOM)); + execute(deps.as_mut(), bid_env.clone(), info.clone(), bid_msg).unwrap(); + + // Attempt to update the bid after bidding ends + let update_env = mock_env_at_time(start_time + 2500); // After bidding period + let update_msg = ExecuteMsg::UpdateBid { + deal_id: 1, + new_amount: Uint128::new(150000u128), + new_discount_percentage: 600, + new_max_price: None, + }; + + let err = execute(deps.as_mut(), update_env.clone(), info.clone(), update_msg).unwrap_err(); + assert!(matches!(err, ContractError::BiddingEnded {})); + } + + #[test] + fn test_place_bid_with_invalid_discount() { + let mut deps = mock_dependencies(); + setup_contract(deps.as_mut()); + + let start_time = 1000u64; + let env = mock_env_at_time(start_time); + + // Create deal + let total_amount = Uint128::new(1000000u128); + let platform_fee = total_amount.multiply_ratio(PLATFORM_FEE_PERCENTAGE as u128, 10000u128); + let create_msg = create_test_deal_msg(start_time); + + let info = mock_info("seller", &coins(platform_fee.u128(), MOCK_PAYMENT_DENOM)); + execute(deps.as_mut(), env.clone(), info, create_msg).unwrap(); + + // Place a bid with invalid discount percentage + let bid_env = mock_env_at_time(start_time + 1500); // During bidding period + let bid_amount = Uint128::new(100000u128); + let bid_msg = ExecuteMsg::PlaceBid { + deal_id: 1, + amount: bid_amount, + discount_percentage: 11000, // Invalid: > 100% + max_price: None, + }; + + let info = mock_info("bidder1", &coins(bid_amount.u128(), MOCK_PAYMENT_DENOM)); + let err = execute(deps.as_mut(), bid_env.clone(), info.clone(), bid_msg).unwrap_err(); + assert!(matches!(err, ContractError::InvalidBidAmount { .. })); + } + + #[test] + fn test_place_bid_with_zero_amount() { + let mut deps = mock_dependencies(); + setup_contract(deps.as_mut()); + + let start_time = 1000u64; + let env = mock_env_at_time(start_time); + + // Create deal + let total_amount = Uint128::new(1000000u128); + let platform_fee = total_amount.multiply_ratio(PLATFORM_FEE_PERCENTAGE as u128, 10000u128); + let create_msg = create_test_deal_msg(start_time); + + let info = mock_info("seller", &coins(platform_fee.u128(), MOCK_PAYMENT_DENOM)); + execute(deps.as_mut(), env.clone(), info, create_msg).unwrap(); + + // Place a bid with zero amount + let bid_env = mock_env_at_time(start_time + 1500); // During bidding period + let bid_amount = Uint128::zero(); + + let bid_msg = ExecuteMsg::PlaceBid { + deal_id: 1, + amount: bid_amount, + discount_percentage: 500, + max_price: None, + }; + + let info = mock_info("bidder1", &coins(bid_amount.u128(), MOCK_PAYMENT_DENOM)); + let err = execute(deps.as_mut(), bid_env.clone(), info.clone(), bid_msg).unwrap_err(); + assert!(matches!(err, ContractError::InvalidBidAmount { .. })); + } + + #[test] + fn test_bidder_can_bid_more_than_once() { + let mut deps = mock_dependencies(); + setup_contract(deps.as_mut()); + + let start_time = 1000u64; + let env = mock_env_at_time(start_time); + + // Create deal + let total_amount = Uint128::new(1000000u128); + let platform_fee = total_amount.multiply_ratio(PLATFORM_FEE_PERCENTAGE as u128, 10000u128); + let create_msg = create_test_deal_msg(start_time); + + let info = mock_info("seller", &coins(platform_fee.u128(), MOCK_PAYMENT_DENOM)); + execute(deps.as_mut(), env.clone(), info, create_msg).unwrap(); + + // Place the first bid + let bid_env = mock_env_at_time(start_time + 1500); // During bidding period + let first_amount = Uint128::new(100000u128); + let bid_msg1 = ExecuteMsg::PlaceBid { + deal_id: 1, + amount: first_amount, + discount_percentage: 500, + max_price: None, + }; + + let info = mock_info("bidder1", &coins(first_amount.u128(), MOCK_PAYMENT_DENOM)); + execute(deps.as_mut(), bid_env.clone(), info.clone(), bid_msg1).unwrap(); + + // Attempt to place a second bid without updating + let second_amount = Uint128::new(200000u128); + let bid_msg2 = ExecuteMsg::PlaceBid { + deal_id: 1, + amount: second_amount, + discount_percentage: 600, + max_price: None, + }; + + let info = mock_info("bidder1", &coins(second_amount.u128(), MOCK_PAYMENT_DENOM)); + execute(deps.as_mut(), bid_env.clone(), info.clone(), bid_msg2).unwrap(); + + // Verify all bids + let bids: Vec<(String, Uint128)> = BIDS + .prefix(1) + .range(deps.as_ref().storage, None, None, Order::Ascending) + .map(|item| { + let (_, bid) = item.unwrap(); + (bid.bidder, bid.amount) + }) + .collect(); + + // Verify we have two bids + assert_eq!(bids.len(), 2, "Should have two bids from the same bidder"); + + // Verify both bids are from bidder1 with correct amounts + assert_eq!(bids[0].0, "bidder1"); + assert_eq!(bids[0].1, first_amount); + assert_eq!(bids[1].0, "bidder1"); + assert_eq!(bids[1].1, second_amount); + + // Verify total bids amount in deal + let deal = DEALS.load(deps.as_ref().storage, 1).unwrap(); + assert_eq!( + deal.total_bids_amount, + first_amount + second_amount, + "Total bid amount should be sum of both bids" + ); + } +} diff --git a/tests/common.rs b/src/tests/common.rs similarity index 87% rename from tests/common.rs rename to src/tests/common.rs index 5ebda623..f3cdd687 100644 --- a/tests/common.rs +++ b/src/tests/common.rs @@ -1,17 +1,18 @@ +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; +use cosmwasm_std::{coins, Addr, BankMsg, CosmosMsg, DepsMut, Env, SubMsg, Timestamp, Uint128}; + +use crate::contract::{execute, instantiate}; +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, InstantiateMsg}; +use crate::state::{BIDS, DEALS}; + #[cfg(test)] -mod tests { +pub mod tests { use super::*; - use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cosmwasm_std::{coins, Addr, BankMsg, CosmosMsg, DepsMut, Env, SubMsg, Timestamp, Uint128}; - - use cw_otc_dex::contract::{execute, execute_conclude_deal, instantiate}; - use cw_otc_dex::error::ContractError; - use cw_otc_dex::msg::{ExecuteMsg, InstantiateMsg}; - use cw_otc_dex::state::{Bid, Config, Deal, BIDS, CONFIG, DEALS, DEAL_COUNTER}; const PLATFORM_FEE_PERCENTAGE: u64 = 100; // 1% const MOCK_SELL_TOKEN: &str = "token"; - const MOCK_PAYMENT_DENOM: &str = "uusd"; + const MOCK_PAYMENT_DENOM: &str = "uusdc"; // Helper function to create mock environment with specified time fn mock_env_at_time(timestamp: u64) -> Env { @@ -24,6 +25,7 @@ mod tests { fn create_test_deal_msg(start_time: u64) -> ExecuteMsg { ExecuteMsg::CreateDeal { sell_token: MOCK_SELL_TOKEN.to_string(), + bid_token_denom: "uusdc".to_string(), total_amount: Uint128::new(1000000u128), min_price: Uint128::new(1u128), discount_percentage: 1000, // 10% @@ -101,16 +103,17 @@ mod tests { execute(deps.as_mut(), env.clone(), info, create_msg).unwrap(); // Move to bidding period - let mut bid_env = mock_env_at_time(start_time + 1500); // During bidding period + let bid_env = mock_env_at_time(start_time + 1500); // During bidding period + let bid_amount = Uint128::new(100000u128); let bid_msg = ExecuteMsg::PlaceBid { deal_id: 1, - amount: Uint128::new(100000u128), + amount: bid_amount, discount_percentage: 500, max_price: Some(Uint128::new(95000u128)), }; - let info = mock_info("bidder1", &[]); + let info = mock_info("bidder1", &coins(bid_amount.u128(), MOCK_PAYMENT_DENOM)); let res = execute(deps.as_mut(), bid_env.clone(), info, bid_msg).unwrap(); assert_eq!(res.attributes.len(), 4); @@ -138,19 +141,21 @@ mod tests { execute(deps.as_mut(), env.clone(), info, create_msg).unwrap(); // Move to bidding period - let mut bid_env = mock_env_at_time(start_time + 1500); + let bid_env = mock_env_at_time(start_time + 1500); + let bid_amount = Uint128::new(100000u128); // Place initial bid let bid_msg = ExecuteMsg::PlaceBid { deal_id: 1, - amount: Uint128::new(100000u128), + amount: bid_amount, discount_percentage: 500, max_price: Some(Uint128::new(95000u128)), }; - let info = mock_info("bidder1", &[]); + let info = mock_info("bidder1", &coins(bid_amount.u128(), MOCK_PAYMENT_DENOM)); execute(deps.as_mut(), bid_env.clone(), info.clone(), bid_msg).unwrap(); // Test bid update + let bid_amount = Uint128::new(150000u128); let update_msg = ExecuteMsg::UpdateBid { deal_id: 1, new_amount: Uint128::new(150000u128), @@ -158,6 +163,7 @@ mod tests { new_max_price: Some(Uint128::new(140000u128)), }; + let info = mock_info("bidder1", &coins(bid_amount.u128(), MOCK_PAYMENT_DENOM)); let res = execute(deps.as_mut(), bid_env.clone(), info, update_msg).unwrap(); assert_eq!(res.attributes.len(), 3); @@ -168,6 +174,7 @@ mod tests { assert_eq!(bid.discount_percentage, 600); } + #[test] fn test_conclude_deal() { let mut deps = mock_dependencies(); setup_contract(deps.as_mut()); @@ -183,17 +190,18 @@ mod tests { let info = mock_info("seller", &coins(platform_fee.u128(), MOCK_PAYMENT_DENOM)); execute(deps.as_mut(), env.clone(), info, create_msg).unwrap(); - // Move to bidding period + // Add some bids (but below min_cap) let bid_env = mock_env_at_time(start_time + 1500); + let bid_amount = Uint128::new(400000u128); - // Add some bids (but below min_cap) let bid_msg = ExecuteMsg::PlaceBid { deal_id: 1, - amount: Uint128::new(400000u128), + amount: bid_amount, discount_percentage: 500, max_price: None, }; - let info = mock_info("bidder1", &[]); + + let info = mock_info("bidder1", &coins(bid_amount.u128(), MOCK_PAYMENT_DENOM)); execute(deps.as_mut(), bid_env.clone(), info, bid_msg).unwrap(); // Move to conclusion time @@ -229,17 +237,18 @@ mod tests { // Move to bidding period and place bid meeting min_cap let bid_env = mock_env_at_time(start_time + 1500); + let amount = Uint128::new(600000u128); let bid_msg = ExecuteMsg::PlaceBid { deal_id: 1, - amount: Uint128::new(600000u128), + amount: amount, discount_percentage: 500, max_price: None, }; - let info = mock_info("bidder1", &[]); + let info = mock_info("bidder1", &coins(amount.u128(), MOCK_PAYMENT_DENOM)); execute(deps.as_mut(), bid_env.clone(), info, bid_msg).unwrap(); // Test successful conclusion - let info = mock_info("anyone", &[]); + let info = mock_info("anyone", &coins(amount.u128(), MOCK_PAYMENT_DENOM)); let res = execute(deps.as_mut(), conclude_env.clone(), info, conclude_msg).unwrap(); assert!(res.messages.len() > 0); @@ -251,7 +260,7 @@ mod tests { } #[test] - fn test_conclude_deal_min_cap_not_met() { + pub fn test_conclude_deal_min_cap_not_met() { let mut deps = mock_dependencies(); setup_contract(deps.as_mut()); @@ -269,25 +278,26 @@ mod tests { // Add multiple bids that sum up to less than min_cap let bid_env = mock_env_at_time(start_time + 1500); // During bidding period - + let amount = Uint128::new(200000u128); // First bid let bid_msg1 = ExecuteMsg::PlaceBid { deal_id: 1, - amount: Uint128::new(200000u128), + amount: amount, discount_percentage: 500, max_price: None, }; - let info = mock_info("bidder1", &[]); + let info = mock_info("bidder1", &coins(amount.u128(), MOCK_PAYMENT_DENOM)); execute(deps.as_mut(), bid_env.clone(), info, bid_msg1).unwrap(); + let amount = Uint128::new(150000u128); // Second bid let bid_msg2 = ExecuteMsg::PlaceBid { deal_id: 1, - amount: Uint128::new(150000u128), + amount: amount, discount_percentage: 600, max_price: None, }; - let info = mock_info("bidder2", &[]); + let info = mock_info("bidder2", &coins(amount.u128(), MOCK_PAYMENT_DENOM)); execute(deps.as_mut(), bid_env.clone(), info, bid_msg2).unwrap(); // Verify total bids amount is less than min_cap @@ -377,6 +387,7 @@ mod tests { let msg = ExecuteMsg::CreateDeal { sell_token: MOCK_SELL_TOKEN.to_string(), + bid_token_denom: "uusdc".to_string(), total_amount: Uint128::new(1000000u128), min_price: Uint128::new(1u128), discount_percentage: 11000, // Invalid: > 100% diff --git a/src/tests/deals_tests.rs b/src/tests/deals_tests.rs new file mode 100644 index 00000000..487772bd --- /dev/null +++ b/src/tests/deals_tests.rs @@ -0,0 +1,279 @@ +use crate::contract::{execute, instantiate}; +use crate::error::ContractError; +use crate::msg::{DealsResponse, ExecuteMsg, InstantiateMsg, QueryMsg}; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; +use cosmwasm_std::{coins, from_binary, DepsMut, Env, Timestamp, Uint128}; + +#[cfg(test)] +mod tests { + use super::*; + + const PLATFORM_FEE_PERCENTAGE: u64 = 100; // 1% + const MOCK_SELL_TOKEN: &str = "token"; + const MOCK_PAYMENT_DENOM: &str = "uusdc"; + + fn setup_contract(deps: DepsMut) { + let msg = InstantiateMsg { + platform_fee_percentage: PLATFORM_FEE_PERCENTAGE, + }; + let info = mock_info("creator", &[]); + let res = instantiate(deps, mock_env(), info, msg).unwrap(); + assert_eq!(0, res.messages.len()); + } + + // Helper function to create mock environment with specified time + fn mock_env_at_time(timestamp: u64) -> Env { + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(timestamp); + env + } + + // Helper function to create a standard test deal message with future timestamps + fn create_test_deal_msg(start_time: u64) -> ExecuteMsg { + ExecuteMsg::CreateDeal { + sell_token: MOCK_SELL_TOKEN.to_string(), + bid_token_denom: "uusdc".to_string(), + total_amount: Uint128::new(1000000u128), + min_price: Uint128::new(1u128), + discount_percentage: 1000, // 10% + min_cap: Uint128::new(500000u128), + bid_start_time: start_time + 1000, // Ensure future start + bid_end_time: start_time + 2000, // End time after start + conclude_time: start_time + 3000, // Conclude time after end + } + } + + #[test] + fn test_create_deal_with_invalid_times() { + let mut deps = mock_dependencies(); + setup_contract(deps.as_mut()); + + let current_time = 1000u64; + let env = mock_env_at_time(current_time); + + // Attempt to create a deal with bid_end_time before bid_start_time + let msg = ExecuteMsg::CreateDeal { + sell_token: MOCK_SELL_TOKEN.to_string(), + bid_token_denom: "uusdc".to_string(), + total_amount: Uint128::new(1000000u128), + min_price: Uint128::new(1u128), + discount_percentage: 1000, // 10% + min_cap: Uint128::new(500000u128), + bid_start_time: current_time + 2000, + bid_end_time: current_time + 1000, + conclude_time: current_time + 3000, + }; + + let info = mock_info("seller", &coins(10000, MOCK_PAYMENT_DENOM)); + let err = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + assert!(matches!(err, ContractError::InvalidTimeParameters { .. })); + + // Attempt to create a deal with conclude_time before bid_end_time + let msg = ExecuteMsg::CreateDeal { + sell_token: MOCK_SELL_TOKEN.to_string(), + bid_token_denom: "uusdc".to_string(), + total_amount: Uint128::new(1000000u128), + min_price: Uint128::new(1u128), + discount_percentage: 1000, // 10% + min_cap: Uint128::new(500000u128), + bid_start_time: current_time + 1000, + bid_end_time: current_time + 2000, + conclude_time: current_time + 1500, // Invalid + }; + + let info = mock_info("seller", &coins(10000, MOCK_PAYMENT_DENOM)); + let err = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + assert!(matches!(err, ContractError::InvalidTimeParameters { .. })); + } + + #[test] + fn test_create_deal_with_invalid_platform_fee() { + let mut deps = mock_dependencies(); + + // Set platform fee percentage over 100% + let msg = InstantiateMsg { + platform_fee_percentage: 11000, // Invalid: > 100% + }; + let info = mock_info("creator", &[]); + let err = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert!(matches!(err, ContractError::InvalidTimeParameters { .. })); + } + + #[test] + fn test_list_deals() { + let mut deps = mock_dependencies(); + setup_contract(deps.as_mut()); + + let current_time = 1000u64; + let env = mock_env_at_time(current_time); + + // Create multiple deals + for i in 0..5 { + let msg = ExecuteMsg::CreateDeal { + sell_token: MOCK_SELL_TOKEN.to_string(), + bid_token_denom: "uusdc".to_string(), + total_amount: Uint128::new(1000000u128 + i), + min_price: Uint128::new(1u128), + discount_percentage: 1000, // 10% + min_cap: Uint128::new(500000u128), + bid_start_time: current_time + 1000, + bid_end_time: current_time + 2000, + conclude_time: current_time + 3000, + }; + + let info = mock_info("seller", &coins(10000 + i as u128, MOCK_PAYMENT_DENOM)); + execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + } + + // Query the list of deals + let res = crate::contract::query( + deps.as_ref(), + env.clone(), + QueryMsg::ListDeals { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + let deals_response: DealsResponse = from_binary(&res).unwrap(); + assert_eq!(deals_response.deals.len(), 5); + } + + #[test] + fn test_list_active_deals() { + let mut deps = mock_dependencies(); + setup_contract(deps.as_mut()); + + let current_time = 1000u64; + let env = mock_env_at_time(current_time); + + // Create multiple deals with varying times + for i in 0..5 { + let bid_start = current_time + (i as u64 * 500u64); + let bid_end = bid_start + 1000u64; // Ensure bid_end_time is after bid_start_time + + let msg = ExecuteMsg::CreateDeal { + sell_token: MOCK_SELL_TOKEN.to_string(), + bid_token_denom: "uusdc".to_string(), + total_amount: Uint128::new(1000000u128 + i), + min_price: Uint128::new(1u128), + discount_percentage: 1000, // 10% + min_cap: Uint128::new(500000u128), + bid_start_time: bid_start, + bid_end_time: bid_end, + conclude_time: bid_end + 3000u64, + }; + + let info = mock_info("seller", &coins(10000 + i as u128, MOCK_PAYMENT_DENOM)); + execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + } + + // Query the list of active deals + let active_env = mock_env_at_time(current_time + 1500); + let res = crate::contract::query( + deps.as_ref(), + active_env.clone(), + QueryMsg::ListActiveDeals { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + let deals_response: DealsResponse = from_binary(&res).unwrap(); + assert!(deals_response.deals.len() > 0); + } + + #[test] + fn test_conclude_deal_before_conclusion_time() { + let mut deps = mock_dependencies(); + setup_contract(deps.as_mut()); + + let start_time = 1000u64; + let env = mock_env_at_time(start_time); + + // Create test deal + let total_amount = Uint128::new(1000000u128); + let platform_fee = total_amount.multiply_ratio(PLATFORM_FEE_PERCENTAGE as u128, 10000u128); + let create_msg = create_test_deal_msg(start_time); + + let info = mock_info("seller", &coins(platform_fee.u128(), MOCK_PAYMENT_DENOM)); + execute(deps.as_mut(), env.clone(), info, create_msg).unwrap(); + + // Attempt to conclude the deal before conclusion time + let conclude_msg = ExecuteMsg::ConcludeDeal { deal_id: 1 }; + let info = mock_info("anyone", &[]); + let err = execute( + deps.as_mut(), + mock_env_at_time(start_time + 2500), + info, + conclude_msg, + ) + .unwrap_err(); + + assert!(matches!(err, ContractError::ConclusionTimeNotReached {})); + } + + #[test] + fn test_conclude_deal_twice() { + let mut deps = mock_dependencies(); + setup_contract(deps.as_mut()); + + let start_time = 1000u64; + let env = mock_env_at_time(start_time); + + // Create a deal + let total_amount = Uint128::new(1_000_000u128); + let platform_fee = total_amount.multiply_ratio(PLATFORM_FEE_PERCENTAGE as u128, 10_000u128); + let create_msg = create_test_deal_msg(start_time); + + let info_seller = mock_info("seller", &coins(platform_fee.u128(), MOCK_PAYMENT_DENOM)); + execute(deps.as_mut(), env.clone(), info_seller, create_msg).unwrap(); + + // Place a bid + let bid_env = mock_env_at_time(start_time + 1500); + let bid_amount = Uint128::new(600_000u128); + let bid_msg = ExecuteMsg::PlaceBid { + deal_id: 1, + amount: bid_amount, + discount_percentage: 500, + max_price: None, + }; + let info_bidder = mock_info("bidder1", &coins(bid_amount.u128(), MOCK_PAYMENT_DENOM)); + execute(deps.as_mut(), bid_env.clone(), info_bidder, bid_msg).unwrap(); + + // Conclude the deal the first time + let conclude_env = mock_env_at_time(start_time + 3500); + let conclude_msg = ExecuteMsg::ConcludeDeal { deal_id: 1 }; + let info = mock_info("anyone", &[]); + + // Clone `info` before passing it + execute( + deps.as_mut(), + conclude_env.clone(), + info.clone(), + conclude_msg.clone(), + ) + .unwrap(); + + // Attempt to conclude the same deal again + let err = execute(deps.as_mut(), conclude_env.clone(), info, conclude_msg).unwrap_err(); + + assert!(matches!(err, ContractError::DealAlreadyConcluded {})); + } + + #[test] + fn test_query_nonexistent_deal() { + let mut deps = mock_dependencies(); + setup_contract(deps.as_mut()); + + // Query a deal that doesn't exist + let res = crate::contract::query( + deps.as_ref(), + mock_env(), + QueryMsg::GetDeal { deal_id: 999 }, + ); + assert!(res.is_err()); + } +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 00000000..82a2da41 --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,5 @@ +// src/tests/mod.rs + +pub mod bids_tests; +pub mod common; +pub mod deals_tests; diff --git a/tests/deals.rs b/tests/deals.rs deleted file mode 100644 index 8b137891..00000000 --- a/tests/deals.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs new file mode 100644 index 00000000..530e443c --- /dev/null +++ b/tests/integration_tests.rs @@ -0,0 +1,224 @@ +// // tests/integration_tests.rs + +// use cosmwasm_std::testing::{mock_env, mock_info}; +// use cosmwasm_std::{ +// coins, Addr, BankMsg, CosmosMsg, DepsMut, Empty, StdResult, SubMsg, Uint128, +// }; +// use cw20::{Cw20Coin, Cw20Contract, Cw20ExecuteMsg, Cw20ReceiveMsg}; +// use cw_multi_test::{App, BankKeeper, Contract, ContractWrapper, Executor}; + +// use cw_otc_dex::contract::{execute, instantiate, query}; +// use cw_otc_dex::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +// use cw_otc_dex::state::Deal; + +// const PLATFORM_FEE_PERCENTAGE: u64 = 100; // 1% +// const MOCK_PAYMENT_DENOM: &str = "uusd"; + +// fn contract_cw_otc_dex() -> Box> { +// let contract = ContractWrapper::new(execute, instantiate, query); +// Box::new(contract) +// } + +// fn contract_cw20() -> Box> { +// let contract = ContractWrapper::new( +// cw20_base::contract::execute, +// cw20_base::contract::instantiate, +// cw20_base::contract::query, +// ); +// Box::new(contract) +// } + +// #[test] +// fn integration_test_with_cw20_tokens() { +// // Set up the testing environment +// let mut app = App::default(); + +// // Upload the contracts +// let code_id = app.store_code(contract_cw_otc_dex()); +// let cw20_code_id = app.store_code(contract_cw20()); + +// // Instantiate the CW20 token contract +// let cw20_instantiate_msg = cw20_base::msg::InstantiateMsg { +// name: "TestToken".to_string(), +// symbol: "TTK".to_string(), +// decimals: 6, +// initial_balances: vec![Cw20Coin { +// address: "seller".to_string(), +// amount: Uint128::new(1_000_000u128), +// }], +// mint: None, +// marketing: None, +// }; + +// let token_addr = app +// .instantiate_contract( +// cw20_code_id, +// Addr::unchecked("seller"), +// &cw20_instantiate_msg, +// &[], +// "TestToken", +// None, +// ) +// .unwrap(); + +// // Instantiate the OTC DEX contract +// let msg = InstantiateMsg { +// platform_fee_percentage: PLATFORM_FEE_PERCENTAGE, +// }; +// let contract_addr = app +// .instantiate_contract(code_id, Addr::unchecked("creator"), &msg, &[], "OTC DEX", None) +// .unwrap(); + +// // Seller provides the platform fee and approves token transfer +// let platform_fee = Uint128::new(1_000_000u128 * PLATFORM_FEE_PERCENTAGE as u128 / 10_000); +// app.init_bank_balance( +// &Addr::unchecked("seller"), +// coins(platform_fee.u128(), MOCK_PAYMENT_DENOM), +// ) +// .unwrap(); + +// // Seller approves the contract to transfer tokens +// let approve_msg = Cw20ExecuteMsg::IncreaseAllowance { +// spender: contract_addr.to_string(), +// amount: Uint128::new(1_000_000u128), +// expires: None, +// }; +// app.execute_contract( +// Addr::unchecked("seller"), +// token_addr.clone(), +// &approve_msg, +// &[], +// ) +// .unwrap(); + +// // Create a deal +// let create_deal_msg = ExecuteMsg::CreateDeal { +// sell_token: token_addr.to_string(), +// bid_token_denom: "uusdc".to_string(), +// total_amount: Uint128::new(1_000_000u128), +// min_price: Uint128::new(1u128), +// discount_percentage: 1000, // 10% +// min_cap: Uint128::new(500_000u128), +// bid_start_time: app.block_info().time.plus_seconds(10).seconds(), +// bid_end_time: app.block_info().time.plus_seconds(20).seconds(), +// conclude_time: app.block_info().time.plus_seconds(30).seconds(), +// }; + +// // Execute the deal creation +// let _res = app +// .execute_contract( +// Addr::unchecked("seller"), +// contract_addr.clone(), +// &create_deal_msg, +// &coins(platform_fee.u128(), MOCK_PAYMENT_DENOM), +// ) +// .unwrap(); + +// // Fast forward to bidding period +// app.update_block(|block| { +// block.time = block.time.plus_seconds(15); +// }); + +// // Place bids +// let place_bid_msg1 = ExecuteMsg::PlaceBid { +// deal_id: 1, +// amount: Uint128::new(300_000u128), +// discount_percentage: 500, +// max_price: None, +// }; + +// let place_bid_msg2 = ExecuteMsg::PlaceBid { +// deal_id: 1, +// amount: Uint128::new(400_000u128), +// discount_percentage: 800, +// max_price: None, +// }; + +// // Initialize bidders' balances +// app.init_bank_balance( +// &Addr::unchecked("bidder1"), +// coins(500_000u128, MOCK_PAYMENT_DENOM), +// ) +// .unwrap(); + +// app.init_bank_balance( +// &Addr::unchecked("bidder2"), +// coins(500_000u128, MOCK_PAYMENT_DENOM), +// ) +// .unwrap(); + +// // Execute bid placements +// let _res = app +// .execute_contract( +// Addr::unchecked("bidder1"), +// contract_addr.clone(), +// &place_bid_msg1, +// &[], +// ) +// .unwrap(); + +// let _res = app +// .execute_contract( +// Addr::unchecked("bidder2"), +// contract_addr.clone(), +// &place_bid_msg2, +// &[], +// ) +// .unwrap(); + +// // Fast forward to conclusion time +// app.update_block(|block| { +// block.time = block.time.plus_seconds(20); +// }); + +// // Conclude the deal +// let conclude_deal_msg = ExecuteMsg::ConcludeDeal { deal_id: 1 }; + +// let _res = app +// .execute_contract( +// Addr::unchecked("anyone"), +// contract_addr.clone(), +// &conclude_deal_msg, +// &[], +// ) +// .unwrap(); + +// // Verify that bidders received the tokens +// let cw20_contract = Cw20Contract(token_addr.clone()); + +// let balance1 = cw20_contract +// .balance(&app.wrap(), Addr::unchecked("bidder1")) +// .unwrap(); +// let balance2 = cw20_contract +// .balance(&app.wrap(), Addr::unchecked("bidder2")) +// .unwrap(); + +// assert!(balance1 > Uint128::zero()); +// assert!(balance2 > Uint128::zero()); + +// // Verify seller's remaining token balance +// let seller_balance = cw20_contract +// .balance(&app.wrap(), Addr::unchecked("seller")) +// .unwrap(); + +// assert!(seller_balance < Uint128::new(1_000_000u128)); + +// // Verify seller received payments +// let seller_bank_balance = app +// .wrap() +// .query_balance("seller", MOCK_PAYMENT_DENOM) +// .unwrap(); + +// assert!(seller_bank_balance.amount > Uint128::zero()); + +// // Verify deal is concluded +// let deal: Deal = app +// .wrap() +// .query_wasm_smart( +// contract_addr.clone(), +// &QueryMsg::GetDeal { deal_id: 1 }, +// ) +// .unwrap(); + +// assert!(deal.is_concluded); +// } diff --git a/tests/mod.rs b/tests/mod.rs deleted file mode 100644 index 8b137891..00000000 --- a/tests/mod.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/queries.rs b/tests/queries.rs deleted file mode 100644 index 8b137891..00000000 --- a/tests/queries.rs +++ /dev/null @@ -1 +0,0 @@ - From d375d2cba9fe2872af968bb6b80d5ad876d379a3 Mon Sep 17 00:00:00 2001 From: anilcse Date: Mon, 11 Nov 2024 18:48:35 +0530 Subject: [PATCH 2/2] Fix fmt --- src/contract.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/contract.rs b/src/contract.rs index efc7107f..35768ca5 100644 --- a/src/contract.rs +++ b/src/contract.rs @@ -1,8 +1,5 @@ use crate::error::ContractError; -use crate::helpers::{ - calculate_platform_fee, get_sorted_bids, - validate_deal_times, -}; +use crate::helpers::{calculate_platform_fee, get_sorted_bids, validate_deal_times}; use crate::msg::{ExecuteMsg, InstantiateMsg}; use crate::state::{Bid, Config, Deal, BIDS, CONFIG, DEALS, DEAL_COUNTER}; use cosmwasm_std::Addr;