diff --git a/contracts/oraiswap_limit_order/src/contract.rs b/contracts/oraiswap_limit_order/src/contract.rs index 6fab689d..d53bc191 100644 --- a/contracts/oraiswap_limit_order/src/contract.rs +++ b/contracts/oraiswap_limit_order/src/contract.rs @@ -8,12 +8,13 @@ use cosmwasm_std::{ use oraiswap::error::ContractError; use crate::order::{ - cancel_order, execute_matching_orders, query_last_order_id, query_order, query_orderbook, - query_orderbook_is_matchable, query_orderbooks, query_orders, remove_pair, submit_order, + cancel_order, execute_matching_orders, get_paid_and_quote_assets, query_last_order_id, + query_order, query_orderbook, query_orderbook_is_matchable, query_orderbooks, query_orders, + remove_pair, submit_order, }; use crate::orderbook::OrderBook; use crate::state::{ - init_last_order_id, read_config, read_orderbook, store_config, store_orderbook, + init_last_order_id, read_config, read_orderbook, store_config, store_orderbook, validate_admin, }; use crate::tick::{query_tick, query_ticks_with_end}; @@ -95,6 +96,24 @@ pub fn execute( spread, min_quote_coin_amount, ), + ExecuteMsg::UpdateOrderbookPair { + asset_infos, + spread, + } => { + validate_admin( + deps.api, + read_config(deps.storage)?.admin, + info.sender.as_str(), + )?; + let pair_key = pair_key(&[ + asset_infos[0].to_raw(deps.api)?, + asset_infos[1].to_raw(deps.api)?, + ]); + let mut orderbook_pair = read_orderbook(deps.storage, &pair_key)?; + orderbook_pair.spread = spread; + store_orderbook(deps.storage, &pair_key, &orderbook_pair)?; + Ok(Response::new().add_attributes(vec![("action", "update_orderbook_data")])) + } ExecuteMsg::SubmitOrder { direction, assets } => { let pair_key = pair_key(&[ assets[0].to_raw(deps.api)?.info, @@ -106,29 +125,15 @@ pub fn execute( // for execute order, it is direct match(user has known it is buy or sell) so no order is needed // Buy: wanting ask asset(orai) => paid offer asset(usdt) // Sell: paid ask asset(orai) => wating offer asset(usdt) - let paid_asset: &Asset; - let quote_asset: &Asset; - - if orderbook_pair.base_coin_info.to_normal(deps.api)? == assets[0].info { - paid_asset = match direction { - OrderDirection::Buy => &assets[1], - OrderDirection::Sell => &assets[0], - }; - quote_asset = &assets[1]; - } else { - paid_asset = match direction { - OrderDirection::Buy => &assets[0], - OrderDirection::Sell => &assets[1], - }; - quote_asset = &assets[0]; - } + let (paid_assets, quote_asset) = + get_paid_and_quote_assets(deps.api, &orderbook_pair, assets, direction)?; // if paid asset is cw20, we check it in Cw20HookMessage - if !paid_asset.is_native_token() { + if !paid_assets[0].is_native_token() { return Err(ContractError::MustProvideNativeToken {}); } - paid_asset.assert_sent_native_token_balance(&info)?; + paid_assets[0].assert_sent_native_token_balance(&info)?; // require minimum amount for quote asset if quote_asset.amount.lt(&orderbook_pair.min_quote_coin_amount) { @@ -139,41 +144,14 @@ pub fn execute( } // then submit order - if orderbook_pair.base_coin_info.to_normal(deps.api)? == assets[0].info { - match direction { - OrderDirection::Buy => submit_order( - deps, - info.sender, - &pair_key, - direction, - [assets[1].clone(), assets[0].clone()], - ), - OrderDirection::Sell => submit_order( - deps, - info.sender, - &pair_key, - direction, - [assets[0].clone(), assets[1].clone()], - ), - } - } else { - match direction { - OrderDirection::Buy => submit_order( - deps, - info.sender, - &pair_key, - direction, - [assets[0].clone(), assets[1].clone()], - ), - OrderDirection::Sell => submit_order( - deps, - info.sender, - &pair_key, - direction, - [assets[1].clone(), assets[0].clone()], - ), - } - } + submit_order( + deps, + &orderbook_pair, + info.sender, + &pair_key, + direction, + paid_assets, + ) } ExecuteMsg::CancelOrder { order_id, @@ -192,12 +170,7 @@ pub fn execute_update_admin( admin: Addr, ) -> Result { let mut contract_info = read_config(deps.storage)?; - let sender_addr = deps.api.addr_canonicalize(info.sender.as_str())?; - - // check authorized - if contract_info.admin.ne(&sender_addr) { - return Err(ContractError::Unauthorized {}); - } + validate_admin(deps.api, contract_info.admin, info.sender.as_str())?; // update new admin contract_info.admin = deps.api.addr_canonicalize(admin.as_str())?; @@ -299,25 +272,10 @@ pub fn receive_cw20( assets[1].to_raw(deps.api)?.info, ]); let orderbook_pair = read_orderbook(deps.storage, &pair_key)?; + let (paid_assets, quote_asset) = + get_paid_and_quote_assets(deps.api, &orderbook_pair, assets, direction)?; - let paid_asset: &Asset; - let quote_asset: &Asset; - - if orderbook_pair.base_coin_info.to_normal(deps.api)? == assets[0].info { - paid_asset = match direction { - OrderDirection::Buy => &assets[1], - OrderDirection::Sell => &assets[0], - }; - quote_asset = &assets[1]; - } else { - paid_asset = match direction { - OrderDirection::Buy => &assets[0], - OrderDirection::Sell => &assets[1], - }; - quote_asset = &assets[0]; - } - - if paid_asset.amount != provided_asset.amount { + if paid_assets[0].amount != provided_asset.amount { return Err(ContractError::AssetMismatch {}); } @@ -329,41 +287,15 @@ pub fn receive_cw20( }); } - if orderbook_pair.base_coin_info.to_normal(deps.api)? == assets[0].info { - match direction { - OrderDirection::Buy => submit_order( - deps, - sender, - &pair_key, - direction, - [assets[1].clone(), assets[0].clone()], - ), - OrderDirection::Sell => submit_order( - deps, - sender, - &pair_key, - direction, - [assets[0].clone(), assets[1].clone()], - ), - } - } else { - match direction { - OrderDirection::Buy => submit_order( - deps, - sender, - &pair_key, - direction, - [assets[0].clone(), assets[1].clone()], - ), - OrderDirection::Sell => submit_order( - deps, - sender, - &pair_key, - direction, - [assets[1].clone(), assets[0].clone()], - ), - } - } + // then submit order + submit_order( + deps, + &orderbook_pair, + sender, + &pair_key, + direction, + paid_assets, + ) } Err(_) => Err(ContractError::InvalidCw20HookMessage {}), } @@ -435,39 +367,25 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::OrderBookMatchable { asset_infos } => { to_binary(&query_orderbook_is_matchable(deps, asset_infos)?) } - // TODO: add test cases QueryMsg::MidPrice { asset_infos } => { let pair_key = pair_key(&[ asset_infos[0].to_raw(deps.api)?, asset_infos[1].to_raw(deps.api)?, ]); - let best_buy = query_ticks_with_end( - deps.storage, - &pair_key, - OrderDirection::Buy, - None, - None, - Some(1), - Some(2), - )?; - let best_sell = query_ticks_with_end( - deps.storage, - &pair_key, - OrderDirection::Sell, - None, - None, - Some(1), - Some(1), - )?; - let best_buy_price = if best_buy.ticks.len() == 0 { - Decimal::zero() + let orderbook_pair = read_orderbook(deps.storage, &pair_key)?; + let (highest_buy_price, buy_found, _) = + orderbook_pair.highest_price(deps.storage, OrderDirection::Buy); + let (lowest_sell_price, sell_found, _) = + orderbook_pair.lowest_price(deps.storage, OrderDirection::Sell); + let best_buy_price = if buy_found { + highest_buy_price } else { - best_buy.ticks[0].price - }; - let best_sell_price = if best_sell.ticks.len() == 0 { Decimal::zero() + }; + let best_sell_price = if sell_found { + lowest_sell_price } else { - best_sell.ticks[0].price + Decimal::zero() }; let mid_price = best_buy_price .checked_add(best_sell_price) diff --git a/contracts/oraiswap_limit_order/src/order.rs b/contracts/oraiswap_limit_order/src/order.rs index 0e1610a8..c32a3d44 100644 --- a/contracts/oraiswap_limit_order/src/order.rs +++ b/contracts/oraiswap_limit_order/src/order.rs @@ -9,8 +9,8 @@ use crate::state::{ PREFIX_ORDER_BY_DIRECTION, PREFIX_ORDER_BY_PRICE, PREFIX_TICK, }; use cosmwasm_std::{ - attr, Addr, Attribute, CanonicalAddr, CosmosMsg, Decimal, Deps, DepsMut, Event, MessageInfo, - Order as OrderBy, Response, StdError, StdResult, Storage, Uint128, + attr, Addr, Api, Attribute, CanonicalAddr, CosmosMsg, Decimal, Deps, DepsMut, Event, + MessageInfo, Order as OrderBy, Response, StdError, StdResult, Storage, Uint128, }; use cosmwasm_storage::ReadonlyBucket; @@ -30,6 +30,7 @@ struct Payment { pub fn submit_order( deps: DepsMut, + orderbook_pair: &OrderBook, sender: Addr, pair_key: &[u8], direction: OrderDirection, @@ -39,6 +40,54 @@ pub fn submit_order( return Err(ContractError::AssetMustNotBeZero {}); } + let offer_amount = assets[0].amount; + let mut ask_amount = assets[1].amount; + + let (highest_buy_price, buy_found, _) = + orderbook_pair.highest_price(deps.storage, OrderDirection::Buy); + let (lowest_sell_price, sell_found, _) = + orderbook_pair.lowest_price(deps.storage, OrderDirection::Sell); + + // check spread for submit order + if let Some(spread) = orderbook_pair.spread { + let buy_spread_factor = Decimal::one() - spread; + let sell_spread_factor = Decimal::one() + spread; + if buy_found && sell_found && spread < Decimal::one() { + match direction { + OrderDirection::Buy => { + let mut price = Decimal::from_ratio(offer_amount, ask_amount); + let spread_price = lowest_sell_price * sell_spread_factor; + if price.ge(&(spread_price)) { + price = spread_price; + ask_amount = Uint128::from(offer_amount * Decimal::one().atomics()) + .checked_div(price.atomics()) + .unwrap_or_default(); + } + } + OrderDirection::Sell => { + let mut price = Decimal::from_ratio(ask_amount, offer_amount); + let spread_price = highest_buy_price * buy_spread_factor; + if spread_price.is_zero() { + return Err(ContractError::PriceMustNotBeZero { + price: spread_price, + }); + } + if spread_price.ge(&price) { + price = spread_price; + ask_amount = Uint128::from(offer_amount * price); + } + } + }; + } + } + + if ask_amount.is_zero() { + return Err(ContractError::TooSmallQuoteAsset { + quote_coin: assets[0].info.to_string(), + min_quote_amount: orderbook_pair.min_quote_coin_amount, + }); + } + let order_id = increase_last_order_id(deps.storage)?; store_order( @@ -48,8 +97,8 @@ pub fn submit_order( order_id, direction, bidder_addr: deps.api.addr_canonicalize(sender.as_str())?, - offer_amount: assets[0].to_raw(deps.api)?.amount, - ask_amount: assets[1].to_raw(deps.api)?.amount, + offer_amount, + ask_amount, filled_offer_amount: Uint128::zero(), filled_ask_amount: Uint128::zero(), status: OrderStatus::Open, @@ -313,91 +362,62 @@ fn execute_bulk_orders( } }; - // list of buy orders and sell orders let buy_bulk_orders = &mut buy_bulk_orders_list[i]; let sell_bulk_orders = &mut sell_bulk_orders_list[j]; - // match price - let match_price = if buy_bulk_orders.average_order_id >= sell_bulk_orders.average_order_id { - buy_price - } else { - sell_price - }; + let match_price = buy_price; + let lef_sell_offer = sell_bulk_orders.volume; + let lef_sell_ask = Uint128::from(lef_sell_offer * match_price); - // remaining_sell_ask_volume = remaining_sell_volume * match_price - let remaining_sell_volume = sell_bulk_orders.remaining_volume; - let remaining_sell_ask_volume = remaining_sell_volume * match_price; + let sell_ask_amount = Uint128::min(buy_bulk_orders.volume, lef_sell_ask); - let remaining_buy_volume = - Uint128::min(buy_bulk_orders.remaining_volume, remaining_sell_ask_volume); // multiply by decimal atomics because we want to get good round values - // remaining_buy_ask_volume = remaining_buy_volume / match_price - let remaining_buy_ask_volume = - Uint128::from(remaining_buy_volume * Decimal::one().atomics()) - .checked_div(match_price.atomics())?; + let sell_offer_amount = Uint128::min( + Uint128::from(sell_ask_amount * Decimal::one().atomics()) + .checked_div(match_price.atomics())?, + lef_sell_offer, + ); - if remaining_buy_ask_volume.is_zero() { - // buy out - i += 1; - } - if remaining_sell_ask_volume.is_zero() { - // sell out - j += 1; + if sell_ask_amount.is_zero() || sell_offer_amount.is_zero() { + continue; } - // fill_base_volume = min(remaining_sell_volume, remaining_buy_ask_volume) - // fill_quote_volume = fill_base_volume * match_price - let fill_base_volume = Uint128::min(remaining_sell_volume, remaining_buy_ask_volume); - let fill_quote_volume = Uint128::from(fill_base_volume * match_price); + sell_bulk_orders.filled_volume += sell_offer_amount; + sell_bulk_orders.filled_ask_volume += sell_ask_amount; - if fill_base_volume.is_zero() || fill_quote_volume.is_zero() { - continue; - } + buy_bulk_orders.filled_volume += sell_ask_amount; + buy_bulk_orders.filled_ask_volume += sell_offer_amount; - // In sell side - // filled_volume = filled_volume + fill_base_volume - // filled_ask_volume = filled_ask_volume + fill_quote_volume - sell_bulk_orders.filled_volume += fill_base_volume; - sell_bulk_orders.filled_ask_volume += fill_quote_volume; - - // In buy side - // filled_volume = filled_volume + fill_quote_volume - // filled_ask_volume = filled_ask_volume + fill_base_volume - buy_bulk_orders.filled_volume += fill_quote_volume; - buy_bulk_orders.filled_ask_volume += fill_base_volume; - - // In buy side - // remaining_volume = remaining_volume - fill_quote_volume - buy_bulk_orders.remaining_volume = buy_bulk_orders - .remaining_volume - .checked_sub(fill_quote_volume)?; - - // In sell side - // remaining_volume = remaining_volume - fill_base_volume - sell_bulk_orders.remaining_volume = sell_bulk_orders - .remaining_volume - .checked_sub(fill_base_volume)?; - // get spread volume in buy side - if buy_bulk_orders.filled_ask_volume > buy_bulk_orders.ask_volume { - buy_bulk_orders.spread_volume += buy_bulk_orders + buy_bulk_orders.volume = buy_bulk_orders.volume.checked_sub(sell_ask_amount)?; + sell_bulk_orders.volume = sell_bulk_orders.volume.checked_sub(sell_offer_amount)?; + + if buy_bulk_orders.filled_ask_volume >= buy_bulk_orders.ask_volume { + buy_bulk_orders.spread_volume = buy_bulk_orders .filled_ask_volume .checked_sub(buy_bulk_orders.ask_volume)?; - buy_bulk_orders.filled_ask_volume = buy_bulk_orders.ask_volume; + buy_bulk_orders.filled_ask_volume = buy_bulk_orders + .filled_ask_volume + .checked_sub(buy_bulk_orders.spread_volume)?; + buy_bulk_orders.ask_volume = Uint128::zero(); } - // get spread volume in sell side - if sell_bulk_orders.filled_ask_volume > sell_bulk_orders.ask_volume { - sell_bulk_orders.spread_volume += sell_bulk_orders + if sell_bulk_orders.filled_ask_volume >= sell_bulk_orders.ask_volume { + sell_bulk_orders.spread_volume = sell_bulk_orders .filled_ask_volume .checked_sub(sell_bulk_orders.ask_volume)?; - sell_bulk_orders.filled_ask_volume = sell_bulk_orders.ask_volume; + sell_bulk_orders.filled_ask_volume = sell_bulk_orders + .filled_ask_volume + .checked_sub(sell_bulk_orders.spread_volume)?; + sell_bulk_orders.ask_volume = Uint128::zero(); } - if buy_bulk_orders.remaining_volume <= min_vol { + if buy_bulk_orders.volume <= min_vol { // buy out + buy_bulk_orders.ask_volume = Uint128::zero(); i += 1; } - if sell_bulk_orders.remaining_volume <= min_vol { + if sell_bulk_orders.volume <= min_vol { // sell out + sell_bulk_orders.ask_volume = Uint128::zero(); j += 1; } } @@ -463,7 +483,6 @@ fn process_orders( let relayer_quote_fee = Uint128::from(RELAY_FEE) * bulk.price; for order in bulk.orders.iter_mut() { - // filled_offer = min(remain_offer, filled_volume) let filled_offer = Uint128::min( order .offer_amount @@ -472,7 +491,6 @@ fn process_orders( bulk.filled_volume, ); - // filled_offer = min(remain_ask, filled_ask_volume) let filled_ask = Uint128::min( order .ask_amount @@ -485,22 +503,17 @@ fn process_orders( continue; } - // filled_volume = filled_volume - filled_offer bulk.filled_volume = bulk .filled_volume .checked_sub(filled_offer) .unwrap_or_default(); - - // filled_ask_volume = filled_ask_volume - filled_ask bulk.filled_ask_volume = bulk .filled_ask_volume .checked_sub(filled_ask) .unwrap_or_default(); - // fill order order.fill_order(filled_ask, filled_offer); - // calculate fee if !filled_ask.is_zero() { trader_ask_asset.amount = filled_ask; let (reward_fee, relayer_fee) = calculate_fee( @@ -825,3 +838,30 @@ pub fn query_orderbook_is_matchable( is_matchable: best_buy_price_list.len() != 0 && best_sell_price_list.len() != 0, }) } + +pub fn get_paid_and_quote_assets( + api: &dyn Api, + orderbook_pair: &OrderBook, + assets: [Asset; 2], + direction: OrderDirection, +) -> StdResult<([Asset; 2], Asset)> { + let mut assets_reverse = assets.clone(); + assets_reverse.reverse(); + let paid_assets: [Asset; 2]; + let quote_asset: Asset; + + if orderbook_pair.base_coin_info.to_normal(api)? == assets[0].info { + paid_assets = match direction { + OrderDirection::Buy => assets_reverse, + OrderDirection::Sell => assets.clone(), + }; + quote_asset = assets[1].clone(); + } else { + paid_assets = match direction { + OrderDirection::Buy => assets.clone(), + OrderDirection::Sell => assets_reverse, + }; + quote_asset = assets[0].clone(); + } + Ok((paid_assets, quote_asset)) +} diff --git a/contracts/oraiswap_limit_order/src/orderbook.rs b/contracts/oraiswap_limit_order/src/orderbook.rs index 55e04371..c2c5083c 100644 --- a/contracts/oraiswap_limit_order/src/orderbook.rs +++ b/contracts/oraiswap_limit_order/src/orderbook.rs @@ -409,7 +409,6 @@ impl OrderBook { ); // both price lists are applicable because buy list is always larger than the first item of sell list let best_sell_price_list = sell_price_list; - if best_buy_price_list.len() == 0 || best_sell_price_list.len() == 0 { return None; } @@ -475,18 +474,11 @@ impl Executor { pub struct BulkOrders { pub orders: Vec, - pub average_order_id: Uint128, pub direction: OrderDirection, pub price: Decimal, - // offer volume pub volume: Uint128, - // remaining volume - pub remaining_volume: Uint128, - // filled volume pub filled_volume: Uint128, - // ask volume pub ask_volume: Uint128, - // filled ask volume pub filled_ask_volume: Uint128, pub spread_volume: Uint128, } @@ -495,33 +487,22 @@ impl BulkOrders { /// Calculate sum of orders base on direction pub fn from_orders(orders: &Vec, price: Decimal, direction: OrderDirection) -> Self { let mut volume = Uint128::zero(); - let mut remaining_volume = Uint128::zero(); let mut ask_volume = Uint128::zero(); - let mut filled_volume = Uint128::zero(); - let mut filled_ask_volume = Uint128::zero(); - let mut sum_order_id = Uint128::zero(); - let mut average_order_id = Uint128::zero(); + let filled_volume = Uint128::zero(); + let filled_ask_volume = Uint128::zero(); let spread_volume = Uint128::zero(); for order in orders { - sum_order_id += Uint128::from(order.order_id); - volume += order.offer_amount; - ask_volume += order.ask_amount; - - remaining_volume += order + volume += order .offer_amount .checked_sub(order.filled_offer_amount) - .unwrap_or_default(); - - filled_volume += order.filled_offer_amount; - filled_ask_volume += order.filled_ask_amount; - } - - if orders.len() > 0 { - average_order_id = sum_order_id - .checked_div((orders.len() as u64).into()) + .unwrap(); + ask_volume += order + .ask_amount + .checked_sub(order.filled_ask_amount) .unwrap(); } + return Self { direction, price, @@ -541,13 +522,11 @@ impl BulkOrders { reward_fee: Uint128::zero(), }) .collect(), - remaining_volume, + volume, filled_volume, + ask_volume, filled_ask_volume, spread_volume, - volume, - ask_volume, - average_order_id, }; } } diff --git a/contracts/oraiswap_limit_order/src/state.rs b/contracts/oraiswap_limit_order/src/state.rs index 85a083ad..926f689f 100644 --- a/contracts/oraiswap_limit_order/src/state.rs +++ b/contracts/oraiswap_limit_order/src/state.rs @@ -1,6 +1,7 @@ -use cosmwasm_std::{CanonicalAddr, Order as OrderBy, StdResult, Storage}; +use cosmwasm_std::{Api, CanonicalAddr, Order as OrderBy, StdError, StdResult, Storage}; use cosmwasm_storage::{singleton, singleton_read, Bucket, ReadonlyBucket}; use oraiswap::{ + error::ContractError, limit_order::{ContractInfo, OrderDirection}, querier::calc_range_start, }; @@ -249,6 +250,17 @@ pub fn read_orders( .collect() } +pub fn validate_admin(api: &dyn Api, admin: CanonicalAddr, sender: &str) -> StdResult<()> { + let sender_addr = api.addr_canonicalize(sender)?; + // check authorized + if admin.ne(&sender_addr) { + return Err(StdError::generic_err( + ContractError::Unauthorized {}.to_string(), + )); + } + Ok(()) +} + static KEY_LAST_ORDER_ID: &[u8] = b"last_order_id"; // should use big int? guess no need static CONTRACT_INFO: &[u8] = b"contract_info"; // contract info static PREFIX_ORDER_BOOK: &[u8] = b"order_book"; // store config for an order book like min ask amount and min sell amount diff --git a/contracts/oraiswap_limit_order/src/testing/contract_test.rs b/contracts/oraiswap_limit_order/src/testing/contract_test.rs index b326649a..9f8f6639 100644 --- a/contracts/oraiswap_limit_order/src/testing/contract_test.rs +++ b/contracts/oraiswap_limit_order/src/testing/contract_test.rs @@ -1,10 +1,12 @@ use std::str::FromStr; +use cosmwasm_std::testing::mock_dependencies; use cosmwasm_std::{to_binary, Addr, Coin, Decimal, StdError, Uint128}; use oraiswap::create_entry_points_testing; +use oraiswap::math::DecimalPlaces; use oraiswap::testing::{AttributeUtil, MockApp, ATOM_DENOM}; -use oraiswap::asset::{Asset, AssetInfo, ORAI_DENOM}; +use oraiswap::asset::{Asset, AssetInfo, AssetInfoRaw, ORAI_DENOM}; use oraiswap::limit_order::{ Cw20HookMsg, ExecuteMsg, InstantiateMsg, LastOrderIdResponse, OrderBookMatchableResponse, OrderBookResponse, OrderBooksResponse, OrderDirection, OrderFilter, OrderResponse, OrderStatus, @@ -12,6 +14,8 @@ use oraiswap::limit_order::{ }; use crate::jsonstr; +use crate::order::get_paid_and_quote_assets; +use crate::orderbook::OrderBook; const USDT_DENOM: &str = "usdt"; fn basic_fixture() -> (MockApp, Addr) { @@ -91,6 +95,135 @@ fn basic_fixture() -> (MockApp, Addr) { (app, limit_order_addr) } +#[test] +fn test_get_paid_and_quote_assets() { + let deps = mock_dependencies(); + let asset_infos_raw = [ + AssetInfoRaw::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + AssetInfoRaw::NativeToken { + denom: USDT_DENOM.to_string(), + }, + ]; + let assets = asset_infos_raw.clone().map(|info| Asset { + info: info.to_normal(deps.as_ref().api).unwrap(), + amount: Uint128::zero(), + }); + let orderbook: OrderBook = OrderBook { + base_coin_info: asset_infos_raw[0].clone(), + quote_coin_info: asset_infos_raw[1].clone(), + spread: None, + min_quote_coin_amount: Uint128::zero(), + }; + // case 1: buy with base coin = asset_infos[0] + let (paid_assets, quote_asset) = get_paid_and_quote_assets( + deps.as_ref().api, + &orderbook, + assets.clone(), + OrderDirection::Buy, + ) + .unwrap(); + assert_eq!(paid_assets[0].info, assets[1].info); + assert_eq!(paid_assets[1].info, assets[0].info); + assert_eq!(quote_asset.info, assets[1].info); + + // case 2: sell with base coin = assets[0] + let (paid_assets, quote_asset) = get_paid_and_quote_assets( + deps.as_ref().api, + &orderbook, + assets.clone(), + OrderDirection::Sell, + ) + .unwrap(); + assert_eq!(paid_assets[0].info, assets[0].info); + assert_eq!(paid_assets[1].info, assets[1].info); + assert_eq!(quote_asset.info, assets[1].info); + + // case 3: buy with base coin = asset_infos[1] + let mut reverse_assets = assets.clone(); + reverse_assets.reverse(); + let (paid_assets, quote_asset) = get_paid_and_quote_assets( + deps.as_ref().api, + &orderbook, + reverse_assets.clone(), + OrderDirection::Buy, + ) + .unwrap(); + assert_eq!(paid_assets[0].info, reverse_assets[0].info); + assert_eq!(paid_assets[1].info, reverse_assets[1].info); + assert_eq!(quote_asset.info, reverse_assets[0].info); + + // case 4: sell with base coin = asset_infos[1] + let mut reverse_assets = assets.clone(); + reverse_assets.reverse(); + let (paid_assets, quote_asset) = get_paid_and_quote_assets( + deps.as_ref().api, + &orderbook, + reverse_assets.clone(), + OrderDirection::Sell, + ) + .unwrap(); + assert_eq!(paid_assets[0].info, reverse_assets[1].info); + assert_eq!(paid_assets[1].info, reverse_assets[0].info); + assert_eq!(quote_asset.info, reverse_assets[0].info); +} + +#[test] +fn test_update_orderbook_data() { + let (mut app, limit_order_addr) = basic_fixture(); + // case 1: try to update orderbook spread with non-admin addr => unauthorized + let asset_infos = [ + AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + AssetInfo::NativeToken { + denom: USDT_DENOM.to_string(), + }, + ]; + let update_msg = ExecuteMsg::UpdateOrderbookPair { + asset_infos: asset_infos.clone(), + spread: Some(Decimal::from_str("0.1").unwrap()), + }; + assert_eq!( + app.execute( + Addr::unchecked("theft"), + limit_order_addr.clone(), + &update_msg, + &[] + ) + .is_err(), + true + ); + + // case 2: good case, admin should update spread from None to something + let spread = Decimal::from_str("0.1").unwrap(); + let update_msg = ExecuteMsg::UpdateOrderbookPair { + asset_infos: asset_infos.clone(), + spread: Some(spread), + }; + app.execute( + Addr::unchecked("addr0000"), + limit_order_addr.clone(), + &update_msg, + &[], + ) + .unwrap(); + let orderbook: OrderBookResponse = app + .query( + limit_order_addr.clone(), + &QueryMsg::OrderBook { + asset_infos: asset_infos.clone(), + }, + ) + .unwrap(); + assert_eq!(orderbook.spread, Some(spread)); + // double check, make sure other fields are still the same + assert_eq!(orderbook.base_coin_info, asset_infos[0]); + assert_eq!(orderbook.quote_coin_info, asset_infos[1]); + assert_eq!(orderbook.min_quote_coin_amount, Uint128::from(10u128)); +} + #[test] fn test_query_mid_price() { let (mut app, limit_order_addr) = basic_fixture(); @@ -681,40 +814,1129 @@ fn submit_order() { ) .unwrap(); - let order_4 = OrderResponse { - order_id: 4u64, - bidder_addr: "addr0000".to_string(), - offer_asset: Asset { - amount: Uint128::from(1212121u128), - info: AssetInfo::Token { - contract_addr: token_addr.clone(), - }, - }, - ask_asset: Asset { - amount: Uint128::from(2121212u128), - info: AssetInfo::NativeToken { - denom: ORAI_DENOM.to_string(), - }, - }, - filled_offer_amount: Uint128::zero(), - filled_ask_amount: Uint128::zero(), - direction: OrderDirection::Buy, - status: OrderStatus::Open, - }; + let order_4 = OrderResponse { + order_id: 4u64, + bidder_addr: "addr0000".to_string(), + offer_asset: Asset { + amount: Uint128::from(1212121u128), + info: AssetInfo::Token { + contract_addr: token_addr.clone(), + }, + }, + ask_asset: Asset { + amount: Uint128::from(2121212u128), + info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + }, + filled_offer_amount: Uint128::zero(), + filled_ask_amount: Uint128::zero(), + direction: OrderDirection::Buy, + status: OrderStatus::Open, + }; + + let order_5 = OrderResponse { + order_id: 5u64, + bidder_addr: "addr0000".to_string(), + offer_asset: Asset { + amount: Uint128::from(1234567u128), + info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + }, + ask_asset: Asset { + amount: Uint128::from(1111111u128), + info: AssetInfo::Token { + contract_addr: token_addr.clone(), + }, + }, + filled_offer_amount: Uint128::zero(), + filled_ask_amount: Uint128::zero(), + direction: OrderDirection::Sell, + status: OrderStatus::Open, + }; + + assert_eq!( + order_4.clone(), + app.query::( + limit_order_addr.clone(), + &QueryMsg::Order { + order_id: 4, + asset_infos: [ + AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + AssetInfo::Token { + contract_addr: token_addr.clone(), + }, + ], + } + ) + .unwrap() + ); + + assert_eq!( + order_5.clone(), + app.query::( + limit_order_addr.clone(), + &QueryMsg::Order { + order_id: 5, + asset_infos: [ + AssetInfo::Token { + contract_addr: token_addr.clone(), + }, + AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + ], + } + ) + .unwrap() + ); + assert_eq!( + app.query::(limit_order_addr.clone(), &QueryMsg::LastOrderId {}) + .unwrap(), + LastOrderIdResponse { last_order_id: 5 } + ); +} + +#[test] +fn submit_order_with_spread_native_token() { + let mut app = MockApp::new(&[ + ( + &"addr0000".to_string(), + &[ + Coin { + denom: ORAI_DENOM.to_string(), + amount: Uint128::from(1000000000u128), + }, + Coin { + denom: USDT_DENOM.to_string(), + amount: Uint128::from(1000000000u128), + }, + ], + ), + ( + &"addr0001".to_string(), + &[ + Coin { + denom: ORAI_DENOM.to_string(), + amount: Uint128::from(1000000000u128), + }, + Coin { + denom: USDT_DENOM.to_string(), + amount: Uint128::from(1000000000u128), + }, + ], + ), + ]); + + app.set_token_contract(Box::new(create_entry_points_testing!(oraiswap_token))); + + app.set_token_balances(&[( + &"asset".to_string(), + &[(&"addr0000".to_string(), &Uint128::from(1000000000u128))], + )]); + + let msg = InstantiateMsg { + name: None, + version: None, + admin: None, + commission_rate: None, + reward_address: None, + }; + let code_id = app.upload(Box::new(create_entry_points_testing!(crate))); + let limit_order_addr = app + .instantiate( + code_id, + Addr::unchecked("addr0000"), + &msg, + &[], + "limit order", + ) + .unwrap(); + + // create order book for pair [orai, usdt] + let msg = ExecuteMsg::CreateOrderBookPair { + base_coin_info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + quote_coin_info: AssetInfo::NativeToken { + denom: USDT_DENOM.to_string(), + }, + spread: Some(Decimal::percent(10)), + min_quote_coin_amount: Uint128::from(10u128), + }; + let _res = app + .execute( + Addr::unchecked("addr0000"), + limit_order_addr.clone(), + &msg, + &[], + ) + .unwrap(); + + let mut assets = [ + Asset { + info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + amount: Uint128::from(100u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: USDT_DENOM.to_string(), + }, + amount: Uint128::from(500u128), + }, + ]; + let asset_infos = assets.clone().map(|asset| asset.info); + // CASE 1: submit first order on buy side => no check spread price, buy_price = 5 + let msg = ExecuteMsg::SubmitOrder { + direction: OrderDirection::Buy, + assets: assets.clone(), + }; + + let _ = app + .execute( + Addr::unchecked("addr0000"), + limit_order_addr.clone(), + &msg, + &[Coin { + denom: assets[1].info.to_string(), + amount: assets[1].amount.clone(), + }], + ) + .unwrap(); + + // query buy ticks - buy side has one tick = 5 + let ticks = app + .query::( + limit_order_addr.clone(), + &QueryMsg::Ticks { + asset_infos: asset_infos.clone(), + direction: OrderDirection::Buy, + start_after: None, + end: None, + limit: None, + order_by: Some(1), + }, + ) + .unwrap(); + + // assert price + assert_eq!( + ticks.ticks[0].price, + Decimal::from_ratio(assets[1].amount, assets[0].amount) + ); + + // CASE 2: submit first order on sell side => no check spread price, sell_price = 6 + assets[1].amount = Uint128::from(600u128); + let msg = ExecuteMsg::SubmitOrder { + direction: OrderDirection::Sell, + assets: assets.clone(), + }; + + let _ = app + .execute( + Addr::unchecked("addr0000"), + limit_order_addr.clone(), + &msg, + &[Coin { + denom: asset_infos[0].to_string(), + amount: assets[0].amount, + }], + ) + .unwrap(); + + // query sell ticks - sell side has one tick = 6 + let ticks = app + .query::( + limit_order_addr.clone(), + &QueryMsg::Ticks { + asset_infos: asset_infos.clone(), + direction: OrderDirection::Sell, + start_after: None, + end: None, + limit: None, + order_by: Some(2), + }, + ) + .unwrap(); + + // assert price + assert_eq!( + ticks.ticks[0].price, + Decimal::from_ratio(assets[1].amount, assets[0].amount) + ); + + // CASE 3: submit buy order out of spread + // buy with price = 6.7 (out of spread = 6.6) => buy with price ~ 6.6 + assets[1].amount = Uint128::from(670u128); + let msg = ExecuteMsg::SubmitOrder { + direction: OrderDirection::Buy, + assets: assets.clone(), + }; + let _res = app + .execute( + Addr::unchecked("addr0000"), + limit_order_addr.clone(), + &msg, + &[Coin { + denom: asset_infos[1].to_string(), + amount: assets[1].amount, + }], + ) + .unwrap(); + + // query buy ticks - buy side has: + // 1. tick = 5 + // 2. tick ~ 6.6 + let ticks = app + .query::( + limit_order_addr.clone(), + &QueryMsg::Ticks { + asset_infos: asset_infos.clone(), + direction: OrderDirection::Buy, + start_after: None, + end: None, + limit: None, + order_by: Some(1), + }, + ) + .unwrap(); + assert_eq!(ticks.ticks.len(), 2); + // Second price ~ 6.6 because submit price = 6.7 is out of spread, price = lowest_sell_price * (1 + spread) = 6.6 + assert_eq!( + ticks.ticks[1].price.limit_decimal_places(Some(1)).unwrap(), + Decimal::from_ratio(66u128, 10u128) + ); + + // CASE 4: submit sell order out of spread + // sell with price = 4.5 (out of spread = 5.97) => submit price ~ 5.97 + assets[1].amount = Uint128::from(450u128); + let msg = ExecuteMsg::SubmitOrder { + direction: OrderDirection::Sell, + assets: assets.clone(), + }; + let _ = app + .execute( + Addr::unchecked("addr0000"), + limit_order_addr.clone(), + &msg, + &[Coin { + denom: asset_infos[0].to_string(), + amount: assets[0].amount, + }], + ) + .unwrap(); + + // query sell ticks - buy side has: + // 1. tick = 6 + // 2. tick ~ 5.97 + let ticks = app + .query::( + limit_order_addr.clone(), + &QueryMsg::Ticks { + asset_infos: asset_infos.clone(), + direction: OrderDirection::Sell, + start_after: None, + end: None, + limit: None, + order_by: Some(2), + }, + ) + .unwrap(); + + assert_eq!(ticks.ticks.len(), 2); + + // Second price ~ 5.94 because submit price = 4.5 is out of spread, price = lowest_sell_price * (1 + spread) = 5.94 + assert_eq!( + ticks.ticks[1].price.limit_decimal_places(Some(2)).unwrap(), + Decimal::from_ratio(597u128, 100u128) + ); + + // CASE 5: submit sell order in spread + // buy with price = 6.5 (in spread = 6.6) + assets[1].amount = Uint128::from(650u128); + let msg = ExecuteMsg::SubmitOrder { + direction: OrderDirection::Buy, + assets: assets.clone(), + }; + let _ = app + .execute( + Addr::unchecked("addr0000"), + limit_order_addr.clone(), + &msg, + &[Coin { + denom: asset_infos[1].to_string(), + amount: assets[1].amount, + }], + ) + .unwrap(); + + // query buy ticks - buy side has: + // 1. tick = 5 + // 2. tick = 6.5 + // 3. tick ~ 6.6 + let ticks = app + .query::( + limit_order_addr.clone(), + &QueryMsg::Ticks { + asset_infos: asset_infos.clone(), + direction: OrderDirection::Buy, + start_after: None, + end: None, + limit: None, + order_by: Some(1), + }, + ) + .unwrap(); + assert_eq!(ticks.ticks.len(), 3); + // Fisrt price = 5 + assert_eq!(ticks.ticks[0].price, Decimal::from_ratio(500u128, 100u128)); + // Second price = 6.5 because of submit price in spread range + assert_eq!(ticks.ticks[1].price, Decimal::from_ratio(650u128, 100u128)); + // Third price ~ 6.6 + assert_eq!( + ticks.ticks[2].price.limit_decimal_places(Some(1)).unwrap(), + Decimal::from_ratio(66u128, 10u128) + ); + + // CASE 6: submit sell order in spread + // sell with price = 6 (in spread = 5.97) + assets[1].amount = Uint128::from(600u128); + let msg = ExecuteMsg::SubmitOrder { + direction: OrderDirection::Sell, + assets: assets.clone(), + }; + + let _ = app + .execute( + Addr::unchecked("addr0000"), + limit_order_addr.clone(), + &msg, + &[Coin { + denom: asset_infos[0].to_string(), + amount: assets[0].amount, + }], + ) + .unwrap(); + + // query sell ticks - buy side has: + // 1. tick = 6 with 2 orders + // 2. tick ~ 5.94 + let ticks = app + .query::( + limit_order_addr.clone(), + &QueryMsg::Ticks { + asset_infos: asset_infos.clone(), + direction: OrderDirection::Sell, + start_after: None, + end: None, + limit: None, + order_by: Some(2), + }, + ) + .unwrap(); + + assert_eq!(ticks.ticks.len(), 2); + // first price + assert_eq!(ticks.ticks[0].price, Decimal::from_ratio(600u128, 100u128)); + // Second price + assert_eq!( + ticks.ticks[1].price.limit_decimal_places(Some(2)).unwrap(), + Decimal::from_ratio(597u128, 100u128) + ); +} + +#[test] +fn submit_order_with_spread_cw20_token() { + let mut app = MockApp::new(&[ + ( + &"addr0000".to_string(), + &[ + Coin { + denom: ORAI_DENOM.to_string(), + amount: Uint128::from(1000000000u128), + }, + Coin { + denom: USDT_DENOM.to_string(), + amount: Uint128::from(1000000000u128), + }, + ], + ), + ( + &"addr0001".to_string(), + &[ + Coin { + denom: ORAI_DENOM.to_string(), + amount: Uint128::from(1000000000u128), + }, + Coin { + denom: USDT_DENOM.to_string(), + amount: Uint128::from(1000000000u128), + }, + ], + ), + ]); + + app.set_token_contract(Box::new(create_entry_points_testing!(oraiswap_token))); + + let usdt_token = app.set_token_balances(&[( + &"asset".to_string(), + &[ + (&"addr0000".to_string(), &Uint128::from(1000000000u128)), + (&"addr0001".to_string(), &Uint128::from(1000000000u128)), + ], + )]); + + let msg = InstantiateMsg { + name: None, + version: None, + admin: None, + commission_rate: None, + reward_address: None, + }; + let code_id = app.upload(Box::new(create_entry_points_testing!(crate))); + let limit_order_addr = app + .instantiate( + code_id, + Addr::unchecked("addr0000"), + &msg, + &[], + "limit order", + ) + .unwrap(); + + // create order book for pair [orai, usdt_token] + let msg = ExecuteMsg::CreateOrderBookPair { + base_coin_info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + quote_coin_info: AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + spread: Some(Decimal::percent(10)), + min_quote_coin_amount: Uint128::zero(), + }; + let _res = app + .execute( + Addr::unchecked("addr0000"), + limit_order_addr.clone(), + &msg, + &[], + ) + .unwrap(); + + // CASE 1: submit first order on buy side => no check spread price, buy_price = 5 + let msg = cw20::Cw20ExecuteMsg::Send { + contract: limit_order_addr.to_string(), + amount: Uint128::new(500u128), + msg: to_binary(&Cw20HookMsg::SubmitOrder { + direction: OrderDirection::Buy, + assets: [ + Asset { + info: AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + amount: Uint128::from(500u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + amount: Uint128::from(100u128), + }, + ], + }) + .unwrap(), + }; + + let _ = app + .execute( + Addr::unchecked("addr0000"), + usdt_token[0].clone(), + &msg, + &[], + ) + .unwrap(); + + // query buy ticks - buy side has one tick = 5 + let ticks = app + .query::( + limit_order_addr.clone(), + &QueryMsg::Ticks { + asset_infos: [ + AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + ], + direction: OrderDirection::Buy, + start_after: None, + end: None, + limit: None, + order_by: Some(1), + }, + ) + .unwrap(); + + // assert price + assert_eq!(ticks.ticks[0].price, Decimal::from_ratio(500u128, 100u128)); + + let orders = app + .query::( + limit_order_addr.clone(), + &QueryMsg::Orders { + asset_infos: [ + AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + ], + direction: None, + filter: OrderFilter::Price(ticks.ticks[0].price), + start_after: None, + limit: None, + order_by: Some(1), + }, + ) + .unwrap(); + + let order_1 = OrderResponse { + order_id: 1u64, + bidder_addr: "addr0000".to_string(), + offer_asset: Asset { + amount: Uint128::from(500u128), + info: AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + }, + ask_asset: Asset { + amount: Uint128::from(100u128), + info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + }, + filled_offer_amount: Uint128::zero(), + filled_ask_amount: Uint128::zero(), + direction: OrderDirection::Buy, + status: OrderStatus::Open, + }; + assert_eq!(order_1.clone(), orders.orders[0]); + + // CASE 2: submit first order on sell side => no check spread price, sell_price = 6 + let msg = ExecuteMsg::SubmitOrder { + direction: OrderDirection::Sell, + assets: [ + Asset { + info: AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + amount: Uint128::from(600u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + amount: Uint128::from(100u128), + }, + ], + }; + let _ = app + .execute( + Addr::unchecked("addr0001"), + limit_order_addr.clone(), + &msg, + &[Coin { + denom: ORAI_DENOM.to_string(), + amount: Uint128::from(100u128), + }], + ) + .unwrap(); + + // query sell ticks - sell side has one tick = 6 + let ticks = app + .query::( + limit_order_addr.clone(), + &QueryMsg::Ticks { + asset_infos: [ + AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + ], + direction: OrderDirection::Sell, + start_after: None, + end: None, + limit: None, + order_by: Some(2), + }, + ) + .unwrap(); + + // assert price + assert_eq!(ticks.ticks[0].price, Decimal::from_ratio(600u128, 100u128)); + + let order_2 = OrderResponse { + order_id: 2u64, + bidder_addr: "addr0001".to_string(), + offer_asset: Asset { + amount: Uint128::from(100u128), + info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + }, + ask_asset: Asset { + amount: Uint128::from(600u128), + info: AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + }, + filled_offer_amount: Uint128::zero(), + filled_ask_amount: Uint128::zero(), + direction: OrderDirection::Sell, + status: OrderStatus::Open, + }; + let orders = app + .query::( + limit_order_addr.clone(), + &QueryMsg::Orders { + asset_infos: [ + AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + ], + direction: None, + filter: OrderFilter::Price(ticks.ticks[0].price), + start_after: None, + limit: None, + order_by: Some(1), + }, + ) + .unwrap(); + + // assert order + assert_eq!(order_2.clone(), orders.orders[0]); + + // CASE 3: submit buy order out of spread + // buy with price = 6.7 (out of spread = 6.6) => buy with price ~ 6.6 + let msg = cw20::Cw20ExecuteMsg::Send { + contract: limit_order_addr.to_string(), + amount: Uint128::new(67000u128), + msg: to_binary(&Cw20HookMsg::SubmitOrder { + direction: OrderDirection::Buy, + assets: [ + Asset { + info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + amount: Uint128::from(10000u128), + }, + Asset { + info: AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + amount: Uint128::from(67000u128), + }, + ], + }) + .unwrap(), + }; + let _ = app + .execute( + Addr::unchecked("addr0000"), + usdt_token[0].clone(), + &msg, + &[], + ) + .unwrap(); + + // query buy ticks - buy side has: + // 1. tick = 5 + // 2. tick ~ 6.6 + let ticks = app + .query::( + limit_order_addr.clone(), + &QueryMsg::Ticks { + asset_infos: [ + AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + ], + direction: OrderDirection::Buy, + start_after: None, + end: None, + limit: None, + order_by: Some(1), + }, + ) + .unwrap(); + + // Fisrt price = 5 + assert_eq!(ticks.ticks[0].price, Decimal::from_ratio(500u128, 100u128)); + + // Second price ~ 6.6 because submit price = 6.7 is out of spread, price = lowest_sell_price * (1 + spread) = 6.6 + assert_eq!( + ticks.ticks[1].price, + Decimal::from_ratio(67000u128, 10151u128) + ); + + let order_3 = OrderResponse { + order_id: 3u64, + bidder_addr: "addr0000".to_string(), + offer_asset: Asset { + amount: Uint128::from(67000u128), + info: AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + }, + ask_asset: Asset { + amount: Uint128::from(10151u128), + info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + }, + filled_offer_amount: Uint128::zero(), + filled_ask_amount: Uint128::zero(), + direction: OrderDirection::Buy, + status: OrderStatus::Open, + }; + + let orders = app + .query::( + limit_order_addr.clone(), + &QueryMsg::Orders { + asset_infos: [ + AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + ], + direction: None, + filter: OrderFilter::Price(ticks.ticks[1].price), + start_after: None, + limit: None, + order_by: Some(1), + }, + ) + .unwrap(); + + // assert order + assert_eq!(order_3.clone(), orders.orders[0]); + + // CASE 4: submit sell order out of spread + // sell with price = 4.5 (out of spread = 5.94) => submit price ~ 5.94 + let msg = ExecuteMsg::SubmitOrder { + direction: OrderDirection::Sell, + assets: [ + Asset { + info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + amount: Uint128::from(10000u128), + }, + Asset { + info: AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + amount: Uint128::from(45000u128), + }, + ], + }; + let _ = app + .execute( + Addr::unchecked("addr0001"), + limit_order_addr.clone(), + &msg, + &[Coin { + denom: ORAI_DENOM.to_string(), + amount: Uint128::from(10000u128), + }], + ) + .unwrap(); + + // query sell ticks - buy side has: + // 1. tick = 6 + // 2. tick ~ 5.94 + let ticks = app + .query::( + limit_order_addr.clone(), + &QueryMsg::Ticks { + asset_infos: [ + AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + ], + direction: OrderDirection::Sell, + start_after: None, + end: None, + limit: None, + order_by: Some(2), + }, + ) + .unwrap(); + + // first price + assert_eq!(ticks.ticks[0].price, Decimal::from_ratio(600u128, 100u128)); + // Second price ~ 5.94 because submit price = 6.7 is out of spread, price = lowest_sell_price * (1 + spread) = 6.6 + assert_eq!( + ticks.ticks[1].price, + Decimal::from_ratio(59403u128, 10000u128) + ); + + let order_4 = OrderResponse { + order_id: 4u64, + bidder_addr: "addr0001".to_string(), + offer_asset: Asset { + amount: Uint128::from(10000u128), + info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + }, + ask_asset: Asset { + amount: Uint128::from(59403u128), + info: AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + }, + filled_offer_amount: Uint128::zero(), + filled_ask_amount: Uint128::zero(), + direction: OrderDirection::Sell, + status: OrderStatus::Open, + }; + let orders = app + .query::( + limit_order_addr.clone(), + &QueryMsg::Orders { + asset_infos: [ + AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + ], + direction: None, + filter: OrderFilter::Price(ticks.ticks[1].price), + start_after: None, + limit: None, + order_by: Some(1), + }, + ) + .unwrap(); + // assert order + assert_eq!(order_4.clone(), orders.orders[0]); + + // CASE 5: submit sell order in spread + // buy with price = 6.5 (in spread = 6.6) + let msg = cw20::Cw20ExecuteMsg::Send { + contract: limit_order_addr.to_string(), + amount: Uint128::new(650u128), + msg: to_binary(&Cw20HookMsg::SubmitOrder { + direction: OrderDirection::Buy, + assets: [ + Asset { + info: AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + amount: Uint128::from(650u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + amount: Uint128::from(100u128), + }, + ], + }) + .unwrap(), + }; + let _ = app + .execute( + Addr::unchecked("addr0000"), + usdt_token[0].clone(), + &msg, + &[], + ) + .unwrap(); + + // query buy ticks - buy side has: + // 1. tick = 5 + // 2. tick = 6.5 + // 3. tick ~ 6.6 + let ticks = app + .query::( + limit_order_addr.clone(), + &QueryMsg::Ticks { + asset_infos: [ + AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + ], + direction: OrderDirection::Buy, + start_after: None, + end: None, + limit: None, + order_by: Some(1), + }, + ) + .unwrap(); + + // Fisrt price = 5 + assert_eq!(ticks.ticks[0].price, Decimal::from_ratio(500u128, 100u128)); + + // Second price = 6.5 because of submit price in spread range + assert_eq!(ticks.ticks[1].price, Decimal::from_ratio(650u128, 100u128)); + + // Third price ~ 6.6 + assert_eq!( + ticks.ticks[2].price, + Decimal::from_ratio(67000u128, 10151u128) + ); + + let order_5 = OrderResponse { + order_id: 5u64, + bidder_addr: "addr0000".to_string(), + offer_asset: Asset { + amount: Uint128::from(650u128), + info: AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + }, + ask_asset: Asset { + amount: Uint128::from(100u128), + info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + }, + filled_offer_amount: Uint128::zero(), + filled_ask_amount: Uint128::zero(), + direction: OrderDirection::Buy, + status: OrderStatus::Open, + }; + + let orders = app + .query::( + limit_order_addr.clone(), + &QueryMsg::Orders { + asset_infos: [ + AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + ], + direction: None, + filter: OrderFilter::Price(ticks.ticks[1].price), + start_after: None, + limit: None, + order_by: Some(1), + }, + ) + .unwrap(); + + // assert order + assert_eq!(order_5.clone(), orders.orders[0]); + + // CASE 6: submit sell order in spread + // sell with price = 6 (in spread = 5.94) + let msg = ExecuteMsg::SubmitOrder { + direction: OrderDirection::Sell, + assets: [ + Asset { + info: AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + amount: Uint128::from(600u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + amount: Uint128::from(100u128), + }, + ], + }; + let _ = app + .execute( + Addr::unchecked("addr0001"), + limit_order_addr.clone(), + &msg, + &[Coin { + denom: ORAI_DENOM.to_string(), + amount: Uint128::from(100u128), + }], + ) + .unwrap(); + + // query sell ticks - buy side has: + // 1. tick = 6 with 2 orders + // 2. tick ~ 5.94 + let ticks = app + .query::( + limit_order_addr.clone(), + &QueryMsg::Ticks { + asset_infos: [ + AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + AssetInfo::Token { + contract_addr: usdt_token[0].clone(), + }, + ], + direction: OrderDirection::Sell, + start_after: None, + end: None, + limit: None, + order_by: Some(2), + }, + ) + .unwrap(); + + // first price + assert_eq!(ticks.ticks[0].price, Decimal::from_ratio(600u128, 100u128)); + // Second price + assert_eq!( + ticks.ticks[1].price, + Decimal::from_ratio(59403u128, 10000u128) + ); - let order_5 = OrderResponse { - order_id: 5u64, - bidder_addr: "addr0000".to_string(), + let order_6 = OrderResponse { + order_id: 6u64, + bidder_addr: "addr0001".to_string(), offer_asset: Asset { - amount: Uint128::from(1234567u128), + amount: Uint128::from(100u128), info: AssetInfo::NativeToken { denom: ORAI_DENOM.to_string(), }, }, ask_asset: Asset { - amount: Uint128::from(1111111u128), + amount: Uint128::from(600u128), info: AssetInfo::Token { - contract_addr: token_addr.clone(), + contract_addr: usdt_token[0].clone(), }, }, filled_offer_amount: Uint128::zero(), @@ -722,49 +1944,28 @@ fn submit_order() { direction: OrderDirection::Sell, status: OrderStatus::Open, }; - - assert_eq!( - order_4.clone(), - app.query::( + let orders = app + .query::( limit_order_addr.clone(), - &QueryMsg::Order { - order_id: 4, + &QueryMsg::Orders { asset_infos: [ AssetInfo::NativeToken { denom: ORAI_DENOM.to_string(), }, AssetInfo::Token { - contract_addr: token_addr.clone(), - }, - ], - } - ) - .unwrap() - ); - - assert_eq!( - order_5.clone(), - app.query::( - limit_order_addr.clone(), - &QueryMsg::Order { - order_id: 5, - asset_infos: [ - AssetInfo::Token { - contract_addr: token_addr.clone(), - }, - AssetInfo::NativeToken { - denom: ORAI_DENOM.to_string(), + contract_addr: usdt_token[0].clone(), }, ], - } + direction: None, + filter: OrderFilter::Price(ticks.ticks[0].price), + start_after: None, + limit: None, + order_by: Some(1), + }, ) - .unwrap() - ); - assert_eq!( - app.query::(limit_order_addr.clone(), &QueryMsg::LastOrderId {}) - .unwrap(), - LastOrderIdResponse { last_order_id: 5 } - ); + .unwrap(); + // assert order + assert_eq!(order_6.clone(), orders.orders[1]); } #[test] @@ -2463,6 +3664,11 @@ fn execute_pair_native_token() { "orai16stq6f4pnrfpz75n9ujv6qg3czcfa4qyjux5en", )) .unwrap(); + let mut spread_balances = app + .query_all_balances(Addr::unchecked( + "orai139tjpfj0h6ld3wff7v2x92ntdewungfss0ml3n", + )) + .unwrap(); println!("round 0 - address0's balances: {:?}", address0_balances); println!("round 0 - address1's balances: {:?}", address1_balances); @@ -2471,6 +3677,10 @@ fn execute_pair_native_token() { "round 0 - reward_balances's balances: {:?}", reward_balances ); + println!( + "round 0 - spread_balances's balances: {:?}\n\n", + spread_balances + ); let mut expected_balances: Vec = [ Coin { @@ -2508,6 +3718,8 @@ fn execute_pair_native_token() { ] .to_vec(); assert_eq!(address2_balances, expected_balances,); + expected_balances = [].to_vec(); + assert_eq!(spread_balances, expected_balances); // assertion; native asset balance let msg = ExecuteMsg::ExecuteOrderBookPair { @@ -2562,6 +3774,11 @@ fn execute_pair_native_token() { "orai16stq6f4pnrfpz75n9ujv6qg3czcfa4qyjux5en", )) .unwrap(); + spread_balances = app + .query_all_balances(Addr::unchecked( + "orai139tjpfj0h6ld3wff7v2x92ntdewungfss0ml3n", + )) + .unwrap(); println!("round 1 - address0's balances: {:?}", address0_balances); println!("round 1 - address1's balances: {:?}", address1_balances); @@ -2570,6 +3787,10 @@ fn execute_pair_native_token() { "round 1 - reward_balances's balances: {:?}", reward_balances ); + println!( + "round 1 - spread_balances's balances: {:?}\n\n", + spread_balances + ); expected_balances = [ Coin { @@ -2578,7 +3799,7 @@ fn execute_pair_native_token() { }, Coin { denom: USDT_DENOM.to_string(), - amount: Uint128::from(984184u128), + amount: Uint128::from(977693u128), }, ] .to_vec(); @@ -2590,7 +3811,7 @@ fn execute_pair_native_token() { }, Coin { denom: USDT_DENOM.to_string(), - amount: Uint128::from(965356u128), + amount: Uint128::from(963224u128), }, ] .to_vec(); @@ -3870,8 +5091,8 @@ fn spread_test() { quote_coin_info: AssetInfo::NativeToken { denom: USDT_DENOM.to_string(), }, - spread: Some(Decimal::percent(10)), - min_quote_coin_amount: Uint128::from(10u128), + spread: None, + min_quote_coin_amount: Uint128::from(50u128), }; let _res = app.execute( @@ -4266,18 +5487,18 @@ fn spread_test() { } #[test] -fn reward_to_executor_test() { - let mut app = MockApp::new(&[ +fn simple_matching_test() { + let mut app: MockApp = MockApp::new(&[ ( &"addr0000".to_string(), &[ Coin { denom: ORAI_DENOM.to_string(), - amount: Uint128::from(1000000000u128), + amount: Uint128::from(10000000000u128), }, Coin { denom: USDT_DENOM.to_string(), - amount: Uint128::from(1000000000u128), + amount: Uint128::from(10000000000u128), }, ], ), @@ -4286,11 +5507,24 @@ fn reward_to_executor_test() { &[ Coin { denom: ORAI_DENOM.to_string(), - amount: Uint128::from(1000000000u128), + amount: Uint128::from(10000000000u128), }, Coin { denom: USDT_DENOM.to_string(), - amount: Uint128::from(1000000000u128), + amount: Uint128::from(10000000000u128), + }, + ], + ), + ( + &"addr0002".to_string(), + &[ + Coin { + denom: ORAI_DENOM.to_string(), + amount: Uint128::from(10000000000u128), + }, + Coin { + denom: USDT_DENOM.to_string(), + amount: Uint128::from(10000000000u128), }, ], ), @@ -4322,8 +5556,8 @@ fn reward_to_executor_test() { quote_coin_info: AssetInfo::NativeToken { denom: USDT_DENOM.to_string(), }, - spread: Some(Decimal::percent(10)), - min_quote_coin_amount: Uint128::from(10000u128), + spread: Some(Decimal::percent(1)), + min_quote_coin_amount: Uint128::from(10u128), }; let _res = app.execute( @@ -4335,50 +5569,19 @@ fn reward_to_executor_test() { /* <----------------------------------- order 1 -----------------------------------> */ let msg = ExecuteMsg::SubmitOrder { - direction: OrderDirection::Buy, - assets: [ - Asset { - info: AssetInfo::NativeToken { - denom: USDT_DENOM.to_string(), - }, - amount: Uint128::from(103000u128), - }, - Asset { - info: AssetInfo::NativeToken { - denom: ORAI_DENOM.to_string(), - }, - amount: Uint128::from(618000u128), - }, - ], - }; - - let _res = app - .execute( - Addr::unchecked("addr0000"), - limit_order_addr.clone(), - &msg, - &[Coin { - denom: USDT_DENOM.to_string(), - amount: Uint128::from(103000u128), - }], - ) - .unwrap(); - - /* <----------------------------------- order 2 -----------------------------------> */ - let msg = ExecuteMsg::SubmitOrder { - direction: OrderDirection::Buy, + direction: OrderDirection::Sell, assets: [ Asset { info: AssetInfo::NativeToken { denom: ORAI_DENOM.to_string(), }, - amount: Uint128::from(610000u128), + amount: Uint128::from(10000000u128), }, Asset { info: AssetInfo::NativeToken { denom: USDT_DENOM.to_string(), }, - amount: Uint128::from(100000u128), + amount: Uint128::from(75123400u128), }, ], }; @@ -4388,138 +5591,158 @@ fn reward_to_executor_test() { Addr::unchecked("addr0000"), limit_order_addr.clone(), &msg, - &[Coin { - denom: USDT_DENOM.to_string(), - amount: Uint128::from(100000u128), - }], - ) - .unwrap(); - - /* <----------------------------------- order 3 -----------------------------------> */ - let msg = ExecuteMsg::SubmitOrder { - direction: OrderDirection::Sell, - assets: [ - Asset { - info: AssetInfo::NativeToken { - denom: USDT_DENOM.to_string(), - }, - amount: Uint128::from(100000u128), - }, - Asset { - info: AssetInfo::NativeToken { - denom: ORAI_DENOM.to_string(), - }, - amount: Uint128::from(600000u128), - }, - ], - }; - - let _res = app - .execute( - Addr::unchecked("addr0001"), - limit_order_addr.clone(), - &msg, &[Coin { denom: ORAI_DENOM.to_string(), - amount: Uint128::from(600000u128), + amount: Uint128::from(10000000u128), }], ) .unwrap(); - /* <----------------------------------- order 4 -----------------------------------> */ + /* <----------------------------------- order 2 -----------------------------------> */ let msg = ExecuteMsg::SubmitOrder { - direction: OrderDirection::Sell, + direction: OrderDirection::Buy, assets: [ Asset { info: AssetInfo::NativeToken { denom: ORAI_DENOM.to_string(), }, - amount: Uint128::from(610000u128), + amount: Uint128::from(100000000u128), }, Asset { info: AssetInfo::NativeToken { denom: USDT_DENOM.to_string(), }, - amount: Uint128::from(100000u128), + amount: Uint128::from(752000000u128), }, ], }; + // offer usdt, ask for orai let _res = app .execute( - Addr::unchecked("addr0001"), + Addr::unchecked("addr0002"), limit_order_addr.clone(), &msg, &[Coin { - denom: ORAI_DENOM.to_string(), - amount: Uint128::from(610000u128), + denom: USDT_DENOM.to_string(), + amount: Uint128::from(752000000u128), }], ) .unwrap(); let mut address0_balances = app.query_all_balances(Addr::unchecked("addr0000")).unwrap(); let mut address1_balances = app.query_all_balances(Addr::unchecked("addr0001")).unwrap(); + let mut address2_balances = app.query_all_balances(Addr::unchecked("addr0002")).unwrap(); println!("round 0 - address0's balances: {:?}", address0_balances); - println!("round 0 - address1's balances: {:?}\n\n", address1_balances); + println!("round 0 - address1's balances: {:?}", address1_balances); + println!("round 0 - address2's balances: {:?}\n\n", address2_balances); let mut expected_balances: Vec = [ Coin { denom: ORAI_DENOM.to_string(), - amount: Uint128::from(1000000000u128), + amount: Uint128::from(9990000000u128), }, Coin { denom: USDT_DENOM.to_string(), - amount: Uint128::from(999797000u128), + amount: Uint128::from(10000000000u128), }, ] .to_vec(); assert_eq!(address0_balances, expected_balances,); + expected_balances = [ Coin { denom: ORAI_DENOM.to_string(), - amount: Uint128::from(998790000u128), + amount: Uint128::from(10000000000u128), }, Coin { denom: USDT_DENOM.to_string(), - amount: Uint128::from(1000000000u128), + amount: Uint128::from(9248000000u128), }, ] .to_vec(); - assert_eq!(address1_balances, expected_balances,); + assert_eq!(address2_balances, expected_balances); + + let msg = ExecuteMsg::ExecuteOrderBookPair { + asset_infos: [ + AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + ], + limit: None, + }; + + // Native token balance mismatch between the argument and the transferred + let res = app.execute( + Addr::unchecked("addr0000"), + limit_order_addr.clone(), + &msg, + &[], + ); + app.assert_fail(res); + + let res = app + .query::( + limit_order_addr.clone(), + &QueryMsg::OrderBookMatchable { + asset_infos: [ + AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + AssetInfo::NativeToken { + denom: USDT_DENOM.to_string(), + }, + ], + }, + ) + .unwrap(); + + let expected_res = OrderBookMatchableResponse { is_matchable: true }; + assert_eq!(res, expected_res); - // assertion; native asset balance - let msg = ExecuteMsg::ExecuteOrderBookPair { + // Excecute all orders + let execute_msg = ExecuteMsg::ExecuteOrderBookPair { asset_infos: [ AssetInfo::NativeToken { denom: ORAI_DENOM.to_string(), }, AssetInfo::NativeToken { - denom: ORAI_DENOM.to_string(), + denom: USDT_DENOM.to_string(), }, ], limit: None, }; - // Native token balance mismatch between the argument and the transferred - let res = app.execute( - Addr::unchecked("addr0000"), - limit_order_addr.clone(), - &msg, - &[], - ); - app.assert_fail(res); + let _res = app + .execute( + Addr::unchecked("addr0000"), + limit_order_addr.clone(), + &execute_msg, + &[], + ) + .unwrap(); + println!("[LOG] attribute - round 1 - {:?}", _res); - // Excecute all orders - let msg = ExecuteMsg::ExecuteOrderBookPair { - asset_infos: [ - AssetInfo::NativeToken { - denom: ORAI_DENOM.to_string(), + /* <----------------------------------- order 3 -----------------------------------> */ + let msg = ExecuteMsg::SubmitOrder { + direction: OrderDirection::Sell, + assets: [ + Asset { + info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + amount: Uint128::from(100000u128), }, - AssetInfo::NativeToken { - denom: USDT_DENOM.to_string(), + Asset { + info: AssetInfo::NativeToken { + denom: USDT_DENOM.to_string(), + }, + amount: Uint128::from(751234u128), }, ], - limit: None, }; let _res = app @@ -4527,55 +5750,90 @@ fn reward_to_executor_test() { Addr::unchecked("addr0000"), limit_order_addr.clone(), &msg, + &[Coin { + denom: ORAI_DENOM.to_string(), + amount: Uint128::from(100000u128), + }], + ) + .unwrap(); + + let _res = app + .execute( + Addr::unchecked("addr0000"), + limit_order_addr.clone(), + &execute_msg, &[], ) .unwrap(); - println!("[LOG] attribute - round 1 - {:?}", _res); + println!("[LOG] attribute - round 2 - {:?}", _res); address0_balances = app.query_all_balances(Addr::unchecked("addr0000")).unwrap(); address1_balances = app.query_all_balances(Addr::unchecked("addr0001")).unwrap(); + address2_balances = app.query_all_balances(Addr::unchecked("addr0002")).unwrap(); println!("round 1 - address0's balances: {:?}", address0_balances); - println!("round 1 - address1's balances: {:?}\n\n", address1_balances); + println!("round 1 - address1's balances: {:?}", address1_balances); + println!("round 1 - address2's balances: {:?}\n\n", address2_balances); expected_balances = [ Coin { denom: ORAI_DENOM.to_string(), - amount: Uint128::from(1000617082u128), + amount: Uint128::from(9989900000u128), }, Coin { denom: USDT_DENOM.to_string(), - amount: Uint128::from(999797000u128), + amount: Uint128::from(10075794254u128), }, ] .to_vec(); - assert_eq!(address0_balances, expected_balances,); + assert_eq!(address0_balances, expected_balances); expected_balances = [ Coin { denom: ORAI_DENOM.to_string(), - amount: Uint128::from(998790000u128), + amount: Uint128::from(10010089300u128), }, Coin { denom: USDT_DENOM.to_string(), - amount: Uint128::from(1000102799u128), + amount: Uint128::from(9248000000u128), }, ] .to_vec(); - assert_eq!(address1_balances, expected_balances,); + assert_eq!(address2_balances, expected_balances); + + let res = app + .query::( + limit_order_addr.clone(), + &QueryMsg::OrderBookMatchable { + asset_infos: [ + AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + AssetInfo::NativeToken { + denom: USDT_DENOM.to_string(), + }, + ], + }, + ) + .unwrap(); + + let expected_res = OrderBookMatchableResponse { + is_matchable: false, + }; + assert_eq!(res, expected_res); } #[test] -fn simple_matching_test() { +fn reward_to_executor_test() { let mut app = MockApp::new(&[ ( &"addr0000".to_string(), &[ Coin { denom: ORAI_DENOM.to_string(), - amount: Uint128::from(10000000000u128), + amount: Uint128::from(1000000000u128), }, Coin { denom: USDT_DENOM.to_string(), - amount: Uint128::from(10000000000u128), + amount: Uint128::from(1000000000u128), }, ], ), @@ -4584,24 +5842,11 @@ fn simple_matching_test() { &[ Coin { denom: ORAI_DENOM.to_string(), - amount: Uint128::from(10000000000u128), - }, - Coin { - denom: USDT_DENOM.to_string(), - amount: Uint128::from(10000000000u128), - }, - ], - ), - ( - &"addr0002".to_string(), - &[ - Coin { - denom: ORAI_DENOM.to_string(), - amount: Uint128::from(10000000000u128), + amount: Uint128::from(1000000000u128), }, Coin { denom: USDT_DENOM.to_string(), - amount: Uint128::from(10000000000u128), + amount: Uint128::from(1000000000u128), }, ], ), @@ -4633,8 +5878,8 @@ fn simple_matching_test() { quote_coin_info: AssetInfo::NativeToken { denom: USDT_DENOM.to_string(), }, - spread: Some(Decimal::percent(1)), - min_quote_coin_amount: Uint128::from(10u128), + spread: Some(Decimal::percent(10)), + min_quote_coin_amount: Uint128::from(10000u128), }; let _res = app.execute( @@ -4644,53 +5889,52 @@ fn simple_matching_test() { &[], ); - /* <----------------------------------- order 0 -----------------------------------> */ + /* <----------------------------------- order 1 -----------------------------------> */ let msg = ExecuteMsg::SubmitOrder { direction: OrderDirection::Buy, assets: [ Asset { info: AssetInfo::NativeToken { - denom: ORAI_DENOM.to_string(), + denom: USDT_DENOM.to_string(), }, - amount: Uint128::from(1000000u128), + amount: Uint128::from(103000u128), }, Asset { info: AssetInfo::NativeToken { - denom: USDT_DENOM.to_string(), + denom: ORAI_DENOM.to_string(), }, - amount: Uint128::from(261500000u128), + amount: Uint128::from(618000u128), }, ], }; - // offer usdt, ask for orai let _res = app .execute( - Addr::unchecked("addr0002"), + Addr::unchecked("addr0000"), limit_order_addr.clone(), &msg, &[Coin { denom: USDT_DENOM.to_string(), - amount: Uint128::from(261500000u128), + amount: Uint128::from(103000u128), }], ) .unwrap(); - /* <----------------------------------- order 1 -----------------------------------> */ + /* <----------------------------------- order 2 -----------------------------------> */ let msg = ExecuteMsg::SubmitOrder { - direction: OrderDirection::Sell, + direction: OrderDirection::Buy, assets: [ Asset { info: AssetInfo::NativeToken { denom: ORAI_DENOM.to_string(), }, - amount: Uint128::from(10000000u128), + amount: Uint128::from(610000u128), }, Asset { info: AssetInfo::NativeToken { denom: USDT_DENOM.to_string(), }, - amount: Uint128::from(75000000u128), + amount: Uint128::from(100000u128), }, ], }; @@ -4700,60 +5944,88 @@ fn simple_matching_test() { Addr::unchecked("addr0000"), limit_order_addr.clone(), &msg, + &[Coin { + denom: USDT_DENOM.to_string(), + amount: Uint128::from(100000u128), + }], + ) + .unwrap(); + + /* <----------------------------------- order 3 -----------------------------------> */ + let msg = ExecuteMsg::SubmitOrder { + direction: OrderDirection::Sell, + assets: [ + Asset { + info: AssetInfo::NativeToken { + denom: USDT_DENOM.to_string(), + }, + amount: Uint128::from(100000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + amount: Uint128::from(600000u128), + }, + ], + }; + + let _res = app + .execute( + Addr::unchecked("addr0001"), + limit_order_addr.clone(), + &msg, &[Coin { denom: ORAI_DENOM.to_string(), - amount: Uint128::from(10000000u128), + amount: Uint128::from(600000u128), }], ) .unwrap(); - /* <----------------------------------- order 2 -----------------------------------> */ + /* <----------------------------------- order 4 -----------------------------------> */ let msg = ExecuteMsg::SubmitOrder { - direction: OrderDirection::Buy, + direction: OrderDirection::Sell, assets: [ Asset { info: AssetInfo::NativeToken { denom: ORAI_DENOM.to_string(), }, - amount: Uint128::from(1000000u128), + amount: Uint128::from(610000u128), }, Asset { info: AssetInfo::NativeToken { denom: USDT_DENOM.to_string(), }, - amount: Uint128::from(261500000u128), + amount: Uint128::from(100000u128), }, ], }; - // offer usdt, ask for orai let _res = app .execute( - Addr::unchecked("addr0002"), + Addr::unchecked("addr0001"), limit_order_addr.clone(), &msg, &[Coin { - denom: USDT_DENOM.to_string(), - amount: Uint128::from(261500000u128), + denom: ORAI_DENOM.to_string(), + amount: Uint128::from(610000u128), }], ) .unwrap(); let mut address0_balances = app.query_all_balances(Addr::unchecked("addr0000")).unwrap(); let mut address1_balances = app.query_all_balances(Addr::unchecked("addr0001")).unwrap(); - let mut address2_balances = app.query_all_balances(Addr::unchecked("addr0002")).unwrap(); println!("round 0 - address0's balances: {:?}", address0_balances); - println!("round 0 - address1's balances: {:?}", address1_balances); - println!("round 0 - address2's balances: {:?}\n\n", address2_balances); + println!("round 0 - address1's balances: {:?}\n\n", address1_balances); let mut expected_balances: Vec = [ Coin { denom: ORAI_DENOM.to_string(), - amount: Uint128::from(9990000000u128), + amount: Uint128::from(1000000000u128), }, Coin { denom: USDT_DENOM.to_string(), - amount: Uint128::from(10000000000u128), + amount: Uint128::from(999797000u128), }, ] .to_vec(); @@ -4761,28 +6033,17 @@ fn simple_matching_test() { expected_balances = [ Coin { denom: ORAI_DENOM.to_string(), - amount: Uint128::from(10000000000u128), + amount: Uint128::from(998790000u128), }, Coin { denom: USDT_DENOM.to_string(), - amount: Uint128::from(10000000000u128), + amount: Uint128::from(1000000000u128), }, ] .to_vec(); assert_eq!(address1_balances, expected_balances,); - expected_balances = [ - Coin { - denom: ORAI_DENOM.to_string(), - amount: Uint128::from(10000000000u128), - }, - Coin { - denom: USDT_DENOM.to_string(), - amount: Uint128::from(9477000000u128), - }, - ] - .to_vec(); - assert_eq!(address2_balances, expected_balances); + // assertion; native asset balance let msg = ExecuteMsg::ExecuteOrderBookPair { asset_infos: [ AssetInfo::NativeToken { @@ -4804,25 +6065,6 @@ fn simple_matching_test() { ); app.assert_fail(res); - let res = app - .query::( - limit_order_addr.clone(), - &QueryMsg::OrderBookMatchable { - asset_infos: [ - AssetInfo::NativeToken { - denom: ORAI_DENOM.to_string(), - }, - AssetInfo::NativeToken { - denom: USDT_DENOM.to_string(), - }, - ], - }, - ) - .unwrap(); - - let expected_res = OrderBookMatchableResponse { is_matchable: true }; - assert_eq!(res, expected_res); - // Excecute all orders let msg = ExecuteMsg::ExecuteOrderBookPair { asset_infos: [ @@ -4848,55 +6090,33 @@ fn simple_matching_test() { address0_balances = app.query_all_balances(Addr::unchecked("addr0000")).unwrap(); address1_balances = app.query_all_balances(Addr::unchecked("addr0001")).unwrap(); - address2_balances = app.query_all_balances(Addr::unchecked("addr0002")).unwrap(); println!("round 1 - address0's balances: {:?}", address0_balances); - println!("round 1 - address1's balances: {:?}", address1_balances); - println!("round 1 - address2's balances: {:?}\n\n", address2_balances); + println!("round 1 - address1's balances: {:?}\n\n", address1_balances); expected_balances = [ Coin { denom: ORAI_DENOM.to_string(), - amount: Uint128::from(9990000000u128), + amount: Uint128::from(1000617082u128), }, Coin { denom: USDT_DENOM.to_string(), - amount: Uint128::from(10074922750u128), + amount: Uint128::from(999797000u128), }, ] .to_vec(); - assert_eq!(address0_balances, expected_balances); + assert_eq!(address0_balances, expected_balances,); expected_balances = [ Coin { denom: ORAI_DENOM.to_string(), - amount: Uint128::from(10001997400u128), + amount: Uint128::from(998790000u128), }, Coin { denom: USDT_DENOM.to_string(), - amount: Uint128::from(9477000000u128), + amount: Uint128::from(1000101135u128), }, ] .to_vec(); - assert_eq!(address2_balances, expected_balances); - let res = app - .query::( - limit_order_addr.clone(), - &QueryMsg::OrderBookMatchable { - asset_infos: [ - AssetInfo::NativeToken { - denom: ORAI_DENOM.to_string(), - }, - AssetInfo::NativeToken { - denom: USDT_DENOM.to_string(), - }, - ], - }, - ) - .unwrap(); - - let expected_res = OrderBookMatchableResponse { - is_matchable: false, - }; - assert_eq!(res, expected_res); + assert_eq!(address1_balances, expected_balances,); } fn mock_basic_query_data() -> (MockApp, Addr) { @@ -5135,7 +6355,7 @@ fn query_matchable() { info: AssetInfo::NativeToken { denom: USDT_DENOM.to_string(), }, - amount: Uint128::from(22000u128), + amount: Uint128::from(21000u128), }, ], }; @@ -5148,7 +6368,7 @@ fn query_matchable() { &msg, &[Coin { denom: USDT_DENOM.to_string(), - amount: Uint128::from(22000u128), + amount: Uint128::from(21000u128), }], ) .unwrap(); @@ -5572,7 +6792,7 @@ fn orders_querier() { quote_coin_info: AssetInfo::NativeToken { denom: ORAI_DENOM.to_string(), }, - spread: Some(Decimal::percent(1)), + spread: Some(Decimal::percent(10)), min_quote_coin_amount: Uint128::from(10u128), }; let _res = app.execute( diff --git a/contracts/oraiswap_limit_order/src/testing/orderbook_test.rs b/contracts/oraiswap_limit_order/src/testing/orderbook_test.rs index 29824249..9d271dff 100644 --- a/contracts/oraiswap_limit_order/src/testing/orderbook_test.rs +++ b/contracts/oraiswap_limit_order/src/testing/orderbook_test.rs @@ -4,6 +4,7 @@ use cosmwasm_std::{testing::mock_dependencies, Api, Decimal}; use oraiswap::{ asset::{AssetInfoRaw, ORAI_DENOM}, limit_order::OrderDirection, + math::DecimalPlaces, testing::ATOM_DENOM, }; @@ -13,6 +14,28 @@ use crate::{ tick::query_ticks_prices, }; +#[test] +fn test_limit_decimal_places() { + let value = Decimal::from_ratio(6655325443433u128, 1000000000000u128); + assert_eq!( + value.limit_decimal_places(Some(2)).unwrap(), + Decimal::from_str("6.65").unwrap() + ); + assert_eq!( + value.limit_decimal_places(Some(10)).unwrap(), + Decimal::from_str("6.655325").unwrap() + ); + assert_eq!( + value.limit_decimal_places(None).unwrap(), + Decimal::from_str("6.655325").unwrap() + ); + + assert_eq!( + value.limit_decimal_places(Some(0)).unwrap(), + Decimal::from_str("6").unwrap() + ) +} + #[test] fn initialize() { let mut deps = mock_dependencies(); diff --git a/packages/oraiswap/src/error.rs b/packages/oraiswap/src/error.rs index 83549688..314157d0 100644 --- a/packages/oraiswap/src/error.rs +++ b/packages/oraiswap/src/error.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{OverflowError, StdError, Uint128}; +use cosmwasm_std::{OverflowError, StdError, Uint128, Decimal}; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -73,6 +73,10 @@ pub enum ContractError { quote_coin: String, min_quote_amount: Uint128, }, + #[error("Price {price} must not be zero")] + PriceMustNotBeZero { + price: Decimal + }, #[error("The contract upgrading process has not completed yet. Please come back after a while, thank you for your patience!")] ContractUpgrade {}, } diff --git a/packages/oraiswap/src/limit_order.rs b/packages/oraiswap/src/limit_order.rs index 41786557..44b0590e 100644 --- a/packages/oraiswap/src/limit_order.rs +++ b/packages/oraiswap/src/limit_order.rs @@ -83,6 +83,11 @@ pub enum ExecuteMsg { min_quote_coin_amount: Uint128, }, + UpdateOrderbookPair { + asset_infos: [AssetInfo; 2], + spread: Option, + }, + /////////////////////// /// User Operations /// /////////////////////// diff --git a/packages/oraiswap/src/math.rs b/packages/oraiswap/src/math.rs index 38f9117c..f66d38cd 100644 --- a/packages/oraiswap/src/math.rs +++ b/packages/oraiswap/src/math.rs @@ -6,6 +6,12 @@ pub trait Converter128 { Self: Sized; } +pub trait DecimalPlaces { + fn limit_decimal_places(&self, maximum_decimal_places: Option) -> StdResult + where + Self: Sized; +} + impl Converter128 for Uint128 { fn checked_div_decimal(&self, denominator: Decimal) -> StdResult { Decimal::one() @@ -14,3 +20,24 @@ impl Converter128 for Uint128 { .map(|coeff| self.clone() * coeff) } } + +pub const DEFAULT_MAX_DECIMAL_PLACES: u32 = 6; +impl DecimalPlaces for Decimal { + fn limit_decimal_places(&self, _maximum_decimal_places: Option) -> StdResult + where + Self: Sized, + { + let mut maximum_decimal_places = + _maximum_decimal_places.unwrap_or(DEFAULT_MAX_DECIMAL_PLACES); + if maximum_decimal_places > DEFAULT_MAX_DECIMAL_PLACES { + maximum_decimal_places = DEFAULT_MAX_DECIMAL_PLACES; + } + let numerator = 10u32.pow(maximum_decimal_places); + let denominator = 1u32; + + (self.checked_mul(Decimal::from_ratio(numerator, denominator)))? + .floor() + .checked_div(Decimal::from_ratio(numerator, denominator)) + .map_err(|err| StdError::generic_err(err.to_string())) + } +}