diff --git a/.gitignore b/.gitignore index 98adea2..2911eac 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,7 @@ coverage .vscode note.txt log.txt -dependencies \ No newline at end of file +dependencies + +/master +/development \ No newline at end of file diff --git a/_tests/match_test/match_orders_cases.rs b/_tests/match_test/match_orders_cases.rs index ec67434..60f887d 100644 --- a/_tests/match_test/match_orders_cases.rs +++ b/_tests/match_test/match_orders_cases.rs @@ -6,6 +6,16 @@ mod success { use super::*; // ✅ buyOrder.orderPrice > sellOrder.orderPrice & buyOrder.baseSize > sellOrder.baseSize + // constants + // balances check + // alice deposit + // deposit check + // create a buy order + // deposited balance check and order check + // same stuff for a sell order + // match orders + // close order in nessesary + // deposited balances check #[tokio::test] async fn greater_buy_price_and_greater_buy_amount() { let buy_price = 46_000_f64; diff --git a/market-contract/src/data_structures/asset_type.sw b/market-contract/src/data_structures/asset_type.sw index 6a3fd96..9e0b0e2 100644 --- a/market-contract/src/data_structures/asset_type.sw +++ b/market-contract/src/data_structures/asset_type.sw @@ -19,3 +19,14 @@ impl Hash for AssetType { } } } + + +// impl core::ops::Eq for AssetType { +// fn eq(self, other: Self) -> bool { +// match (self, other) { +// (Self::Base, Self::Base) => true, +// (Self::Quote, Self::Quote) => true, +// _ => false, +// } +// } +// } \ No newline at end of file diff --git a/market-contract/src/data_structures/order.sw b/market-contract/src/data_structures/order.sw index 3372b9d..b32db52 100644 --- a/market-contract/src/data_structures/order.sw +++ b/market-contract/src/data_structures/order.sw @@ -50,29 +50,4 @@ impl Order { require(price != 0, OrderError::PriceCannotBeZero); self.price = price; } - - // #[storage(read)] - // pub fn calculate_deposit( - // self, - // BASE_ASSET_DECIMALS: u32, - // PRICE_DECIMALS: u32, - // QUOTE_TOKEN_DECIMALS: u32, - // QUOTE_TOKEN: AssetId, - // ) -> Asset { - // match self.order_type { - // OrderType::Sell => Asset::new(self.amount, self.asset), - // OrderType::Buy => { - // Asset::new( - // quote( - // self.amount, - // BASE_ASSET_DECIMALS, - // self.price, - // PRICE_DECIMALS, - // QUOTE_TOKEN_DECIMALS, - // ), - // QUOTE_TOKEN, - // ) - // }, - // } - // } } diff --git a/market-contract/src/data_structures/order_type.sw b/market-contract/src/data_structures/order_type.sw index 266daa7..e92285c 100644 --- a/market-contract/src/data_structures/order_type.sw +++ b/market-contract/src/data_structures/order_type.sw @@ -28,4 +28,4 @@ impl Hash for OrderType { } } } -} +} \ No newline at end of file diff --git a/market-contract/src/interface.sw b/market-contract/src/interface.sw index 505422c..d87451a 100644 --- a/market-contract/src/interface.sw +++ b/market-contract/src/interface.sw @@ -25,7 +25,7 @@ abi Market { // fn fulfill(order_id: b256); #[storage(read, write)] - fn batch_fulfill(order_id: b256, orders: Vec); + fn batch_fulfill(order_sell_id: b256, order_buy_id: b256); #[storage(write)] fn set_fee(amount: u64, user: Option); diff --git a/market-contract/src/main.sw b/market-contract/src/main.sw index f1172fe..a1053a9 100644 --- a/market-contract/src/main.sw +++ b/market-contract/src/main.sw @@ -6,7 +6,6 @@ mod data_structures; mod events; mod interface; mod math; - use ::data_structures::{ account::Account, asset_type::AssetType, @@ -14,7 +13,7 @@ use ::data_structures::{ order::Order, order_type::OrderType, }; -use ::errors::{AccountError, AssetError, AuthError, OrderError}; +use ::errors::{AccountError, AssetError, AuthError, OrderError, TradeError}; use ::events::{ CancelOrderEvent, DepositEvent, @@ -27,7 +26,7 @@ use ::interface::{Info, Market}; use ::math::*; use std::{ - asset::transfer, + asset::*, call_frames::msg_asset_id, constants::{ BASE_ASSET_ID, @@ -70,77 +69,46 @@ impl Market for Contract { #[payable] #[storage(read, write)] fn deposit() { - require( - msg_asset_id() == BASE_ASSET || msg_asset_id() == QUOTE_ASSET, - AssetError::InvalidAsset, - ); + let asset = msg_asset_id(); + let amount = msg_amount(); + require(asset == BASE_ASSET || asset == QUOTE_ASSET, AssetError::InvalidAsset); + let asset_type = if asset == BASE_ASSET { AssetType::Base } else { AssetType::Quote }; + let user = msg_sender().unwrap(); - let (amount, asset_type) = match msg_asset_id() == BASE_ASSET { - true => (msg_amount() * 10.pow(BASE_ASSET_DECIMALS), AssetType::Base), - false => (msg_amount() * 10.pow(QUOTE_ASSET_DECIMALS), AssetType::Quote), - }; - let mut account = match storage.account.get(user).try_read() { - Some(account) => account, - None => Account::new(), - }; + let mut account = storage.account.get(user).try_read().unwrap_or(Account::new()); - account.liquid.credit(msg_amount(), asset_type); + account.liquid.credit(amount, asset_type); storage.account.insert(user, account); - log(DepositEvent { - amount: msg_amount(), - asset: msg_asset_id(), - user, - }); + log(DepositEvent { amount, asset, user }); + } #[storage(read, write)] fn withdraw(amount: u64, asset: AssetId) { - require( - asset == BASE_ASSET || asset == QUOTE_ASSET, - AssetError::InvalidAsset, - ); + require(asset == BASE_ASSET || asset == QUOTE_ASSET, AssetError::InvalidAsset); + let asset_type = if asset == BASE_ASSET { AssetType::Base } else { AssetType::Quote }; let user = msg_sender().unwrap(); let account = storage.account.get(user).try_read(); - require(account.is_some(), AccountError::InvalidUser); - let mut account = account.unwrap(); - // TODO: Is this division correct? - let (internal_amount, asset_type) = match msg_asset_id() == BASE_ASSET { - true => (amount / 10.pow(BASE_ASSET_DECIMALS), AssetType::Base), - false => (amount / 10.pow(QUOTE_ASSET_DECIMALS), AssetType::Quote), - }; - account.liquid.debit(amount, asset_type); storage.account.insert(user, account); - transfer(user, asset, internal_amount); - - log(WithdrawEvent { - amount: internal_amount, - asset, - user, - }); + transfer(user, asset, amount); + + log(WithdrawEvent { amount, asset, user }); } #[storage(read, write)] - // TODO: what types should amount, price be? - fn open_order( - amount: u64, - asset: AssetId, - order_type: OrderType, - price: u64, - ) -> b256 { - require( - asset == BASE_ASSET || asset == QUOTE_ASSET, - AssetError::InvalidAsset, - ); + fn open_order(amount: u64, asset: AssetId, order_type: OrderType, price: u64) -> b256 { + require(asset == BASE_ASSET || asset == QUOTE_ASSET, AssetError::InvalidAsset); + let asset_type = if asset == BASE_ASSET { AssetType::Base } else { AssetType::Quote }; let user = msg_sender().unwrap(); let account = storage.account.get(user).try_read(); @@ -148,59 +116,36 @@ impl Market for Contract { require(account.is_some(), AccountError::InvalidUser); let mut account = account.unwrap(); - let asset_type = if asset == BASE_ASSET { - AssetType::Base - } else { - AssetType::Quote - }; match order_type { OrderType::Sell => { // If the account has enough liquidity of the asset that you already own then lock // it for the new sell order - // TODO: use amount to lock funds - let _internal_amount = if asset == BASE_ASSET { - amount * BASE_ASSET_DECIMALS.as_u64() + let base_amount = if asset == BASE_ASSET { + // example open_order(0.5, btc, SELL, 70k) + amount } else { - amount * QUOTE_ASSET_DECIMALS.as_u64() + // example open_order(35k, usdc, SELL, 70k) + quote_to_base_amount(amount, BASE_ASSET_DECIMALS, price, PRICE_DECIMALS, QUOTE_ASSET_DECIMALS) }; - account.liquid.debit(amount, asset_type); - account.locked.credit(amount, asset_type); + account.liquid.debit(base_amount, AssetType::Base); + account.locked.credit(base_amount, AssetType::Base); } OrderType::Buy => { // Calculate amount to lock of the other asset - // TODO: these "amounts" do not return expected values - let (amount, asset_type) = match asset == BASE_ASSET { - true => { - let amount = base_to_quote_amount( - amount, - BASE_ASSET_DECIMALS, - price, - PRICE_DECIMALS, - QUOTE_ASSET_DECIMALS, - ); - let asset_type = AssetType::Quote; - (amount, asset_type) - }, - false => { - let amount = quote_to_base_amount( - amount, - BASE_ASSET_DECIMALS, - price, - PRICE_DECIMALS, - QUOTE_ASSET_DECIMALS, - ); - let asset_type = AssetType::Base; - (amount, asset_type) - }, + + let quote_amount = if asset == BASE_ASSET { + // example open_order(0.5, btc, BUY, 70k) + base_to_quote_amount(amount, BASE_ASSET_DECIMALS, price, PRICE_DECIMALS, QUOTE_ASSET_DECIMALS) + } else { + // example open_order(35k, usdc, BUY, 70k) + amount }; - // The asset type is the opposite because you're calculating if you have enough of - // the opposite asset to use as payment - account.liquid.debit(amount, asset_type); - account.locked.credit(amount, asset_type); + account.liquid.debit(quote_amount, AssetType::Quote); + account.locked.credit(quote_amount, AssetType::Quote); } } @@ -350,142 +295,48 @@ impl Market for Contract { // fn fulfill(order_id: b256); #[storage(read, write)] - fn batch_fulfill(order_id: b256, orders: Vec) { - // TODO: batching is WIP, almost done but needs more work - - // Order must exist to be fulfilled - let alice = storage.orders.get(order_id).try_read(); - require(alice.is_some(), OrderError::NoOrdersFound); - - let mut alice = alice.unwrap(); - // Cannot open an order without having an account so it's safe to read - let mut alice_account = storage.account.get(alice.owner).read(); - - let mut order_index = 0; - while order_index < orders.len() { - let id = orders.get(order_index).unwrap(); - let bob = storage.orders.get(id).try_read(); - // If bob's order does not exist then proceed to the next order without reverting - if bob.is_none() { - continue; - } - let mut bob = bob.unwrap(); - - // Order types must be different in order to trade (buy against sell) - // Asset types must be the same you trade asset A for asset A instead of B - if alice.order_type == bob.order_type - || alice.asset != bob.asset - { - continue; - } + fn batch_fulfill(order_sell_id: b256, order_buy_id: b256) { - // Upon a trade the id will change so track it before any trades - let bob_id = bob.id(); + let order_buy = storage.orders.get(order_buy_id).try_read(); + let order_sell = storage.orders.get(order_sell_id).try_read(); + require(order_buy.is_some() && order_sell.is_some(), OrderError::NoOrdersFound); - // Attempt to trade orders, figure out amounts that can be traded - let trade = attempt_trade( - alice, - bob, - BASE_ASSET_DECIMALS, - QUOTE_ASSET_DECIMALS, - PRICE_DECIMALS, - ); - - // Failed to trade ex. insufficient price or remaining amount - if trade.is_err() { - continue; - } - - // Retrieve the amount of each asset that can be traded - let ( - alice_order_amount_decrease, - alice_account_delta, - bob_order_amount_decrease, - bob_account_delta, - ) = trade.unwrap(); - - // Update the order quantities with the amounts that can be traded - alice.amount -= alice_order_amount_decrease; - bob.amount -= bob_order_amount_decrease; - - // Update the accounts for bob and alice based on the traded assets - let mut bob_account = storage.account.get(bob.owner).read(); - - alice_account - .locked - .debit(alice_account_delta, alice.asset_type); - alice_account - .liquid - .credit(bob_account_delta, bob.asset_type); - - bob_account.locked.debit(bob_account_delta, bob.asset_type); - bob_account - .liquid - .credit(alice_account_delta, alice.asset_type); - - // Save bob's account because his order is finished - // For optimization save alice at the end of the batch - storage.account.insert(bob.owner, bob_account); - - // If bob's order has been fully filled then we remove it from orders - if bob.amount == 0 { - require(storage.orders.remove(bob_id), OrderError::FailedToRemove); - - // TODO: Emit event - // log(TradeEvent { - // order_id: bob_id, - - // }) - } else { - // We were only partially able to fill bob's order so we replace the old order - // with the updated order - require(storage.orders.remove(bob_id), OrderError::FailedToRemove); - let bob_id = bob.id(); - - // Reject identical orders to prevent accounting issues - require( - storage - .orders - .get(bob_id) - .try_read() - .is_none(), - OrderError::DuplicateOrder, - ); - - storage.orders.insert(bob_id, bob); - - // TODO: event - } + let order_buy = order_buy.unwrap(); + let order_sell = order_sell.unwrap(); - // If the target order has been fulfilled then finish processing batch - if alice.amount == 0 { - require(storage.orders.remove(order_id), OrderError::FailedToRemove); - break; - } + // let mut buyer_account = storage.account.get(order_buy.owner).read(); + // let mut seller_account = storage.account.get(order_sell.owner).read(); - order_index += 1; - } + require(order_buy.order_type == OrderType::Buy, TradeError::CannotTrade); + require(order_sell.order_type == OrderType::Sell, TradeError::CannotTrade); + + require(order_buy.asset == order_sell.asset, TradeError::CannotTrade); + require(order_sell.price <= order_buy.price, TradeError::CannotTrade); + - if alice.amount != 0 { - require(storage.orders.remove(order_id), OrderError::FailedToRemove); - let alice_id = alice.id(); + let mut tmp = order_sell; + // tmp.amount = tmp.amount.flip(); + let trade_size = min(order_sell.amount, order_buy.amount.mul_div(order_buy.price, order_sell.price)); + tmp.amount = trade_size; + + let seller: Identity = order_sell.owner; + let (sellerDealAssetId, sellerDealRefund) = order_return_asset_amount(tmp); + remove_update_order_internal(order_sell, tmp.amount); - // Reject identical orders to prevent accounting issues - require( - storage - .orders - .get(alice_id) - .try_read() - .is_none(), - OrderError::DuplicateOrder, - ); + // tmp.amount = tmp.amount.flip(); - storage.orders.insert(alice_id, alice); - } + let buyer: Identity = order_buy.owner; + let (buyerDealAssetId, buyerDealRefund) = order_return_asset_amount(tmp); + tmp.amount = tmp.amount.mul_div_rounding_up(order_sell.price, order_buy.price); + remove_update_order_internal(order_buy, tmp.amount); - storage.account.insert(alice.owner, alice_account); + require( + sellerDealRefund != 0 && buyerDealRefund != 0, + TradeError::CannotTrade, + ); - // TODO: event + transfer(seller, sellerDealAssetId, sellerDealRefund); + transfer(buyer, buyerDealAssetId, buyerDealRefund); } #[storage(write)] @@ -561,3 +412,50 @@ impl Info for Contract { Order::new(amount, asset, asset_type, order_type, owner, price).id() } } + + +#[storage(read)] +fn order_return_asset_amount(order: Order) -> (AssetId, u64) { + match order.order_type { + OrderType::Sell => { + (order.asset, order.amount) + }, + OrderType::Buy => { + ( + QUOTE_ASSET, + base_size_to_quote_amount( + order.amount, + QUOTE_ASSET_DECIMALS, + order.price, + ), + ) + } + } +} + +fn base_size_to_quote_amount(base_size: u64, base_decimals: u32, base_price: u64) -> u64 { + base_size.mul_div( + base_price, + 10_u64 + .pow(base_decimals + PRICE_DECIMALS - QUOTE_ASSET_DECIMALS), + ) +} + +#[storage(read, write)] +fn remove_update_order_internal(order: Order, base_size: u64) { + if (order.amount == base_size && order.order_type == OrderType::Sell) { + let pos_id = storage.user_order_indexes.get(order.owner).get(order.id()).read() - 1; // pos + 1 indexed + assert(storage.user_order_indexes.get(order.owner).remove(order.id())); + assert(storage.user_orders.get(order.owner).swap_remove(pos_id) == order.id()); + assert(storage.orders.remove(order.id())); + } else { + let mut order = order; + order.amount += base_size; + storage.orders.insert(order.id(), order); + } +} + + +pub fn min(a: u64, b: u64) -> u64 { + if a < b { a } else { b } +} \ No newline at end of file diff --git a/market-contract/tests/functions/core/match_orders.rs b/market-contract/tests/functions/core/match_orders.rs new file mode 100644 index 0000000..aa4b3db --- /dev/null +++ b/market-contract/tests/functions/core/match_orders.rs @@ -0,0 +1,578 @@ +use crate::utils::{ + interface::{ + core::{batch_fulfill, deposit, open_order}, + info::account, + }, + setup::{setup, Defaults, OrderType}, +}; +// constants +// balances check +// alice deposit +// deposit check +// create a buy order +// deposited balance check and order check +// same stuff for a sell order +// match orders +// close order in nessesary +// deposited balances check + +mod success { + + use super::*; + // ✅ buy_price > sell_price & buy_size > sell_size + #[tokio::test] + async fn greater_buy_price_and_greater_buy_amount() { + let buy_price = 46_000_f64; + let buy_size = 2_f64; + let sell_price = 45_000_f64; + let sell_size = 1_f64; + + let alice_liquid_base_expected_balance = 1_f64; + let alice_locked_quote_expected_balance = 47_000_f64; //locked because order will be opened + let bob_liquid_quote_expected_balance = 45_000_f64; + + let defaults = Defaults::default(); + let (contract, alice, bob, assets) = setup( + defaults.base_decimals, + defaults.quote_decimals, + defaults.price_decimals, + ) + .await; + + let bob_instance = contract.with_account(bob.wallet.clone()).unwrap(); + let alice_instance = contract.with_account(alice.wallet.clone()).unwrap(); + + //deposit + let bob_deposit_base_amount = assets.base.parse_units(sell_size) as u64; + let _ = deposit(&bob_instance, bob_deposit_base_amount, assets.base.id).await; + let bob_account = account(&contract, bob.identity()).await.value.unwrap(); + assert_eq!(bob_account.liquid.base, bob_deposit_base_amount); + + //create order bob + let bob_order_id = open_order( + &bob_instance, + assets.base.parse_units(sell_size) as u64, + assets.base.id, + OrderType::Sell, + (sell_price * 1e9) as u64, + ) + .await + .value; + + let bob_account = account(&contract, bob.identity()).await.value.unwrap(); + assert_eq!(bob_account.locked.base, bob_deposit_base_amount); + assert_eq!(bob_account.liquid.base, 0); + + //deposit + let alice_deposit_quote_amount = assets.quote.parse_units(buy_price * buy_size) as u64; + let _ = deposit(&alice_instance, alice_deposit_quote_amount, assets.quote.id).await; + let alice_account = account(&contract, alice.identity()).await.value.unwrap(); + assert_eq!(alice_account.liquid.quote, alice_deposit_quote_amount); + + //create order alice + let alice_order_id = open_order( + &alice_instance, + assets.base.parse_units(buy_size) as u64, + assets.base.id, + OrderType::Buy, + (buy_price * 1e9) as u64, + ) + .await + .value; + + let alice_account = account(&contract, alice.identity()).await.value.unwrap(); + assert_eq!(alice_account.locked.quote, alice_deposit_quote_amount); + assert_eq!(alice_account.liquid.quote, 0); + + batch_fulfill(&contract, bob_order_id, alice_order_id).await; + + let bob_account = account(&contract, bob.identity()).await.value.unwrap(); + assert_eq!(bob_account.locked.base, 0); + assert_eq!(bob_account.locked.quote, 0); + assert_eq!(bob_account.liquid.base, 0); + assert_eq!( + bob_account.liquid.quote, + assets.quote.format_units(bob_liquid_quote_expected_balance) as u64 + ); // 45k usdc + + let alice_account = account(&contract, alice.identity()).await.value.unwrap(); + assert_eq!( + alice_account.locked.quote, + assets + .quote + .format_units(alice_locked_quote_expected_balance) as u64 + ); // 47k usdc + assert_eq!(alice_account.locked.base, 0); + assert_eq!(alice_account.liquid.quote, 0); + assert_eq!( + alice_account.liquid.base, + assets.base.parse_units(alice_liquid_base_expected_balance) as u64 + ); // 1 btc + } + + // ✅ buy_price > sell_price & buy_size < sell_size + #[tokio::test] + async fn greater_buy_price_and_smaller_buy_amount() { + let buy_price = 46_000_f64; + let buy_size = 1_f64; + let sell_price = 45_000_f64; + let sell_size = 2_f64; + + let alice_liquid_base_expected_balance = 1_f64; + let alice_locked_quote_expected_balance = 0_f64; + let bob_liquid_quote_expected_balance = 45_000_f64; + let bob_locked_base_expected_balance = 1_f64; + + let defaults = Defaults::default(); + let (contract, alice, bob, assets) = setup( + defaults.base_decimals, + defaults.quote_decimals, + defaults.price_decimals, + ) + .await; + + let bob_instance = contract.with_account(bob.wallet.clone()).unwrap(); + let alice_instance = contract.with_account(alice.wallet.clone()).unwrap(); + + let bob_deposit_base_amount = assets.base.parse_units(sell_size) as u64; + let _ = deposit(&bob_instance, bob_deposit_base_amount, assets.base.id).await; + let bob_account = account(&contract, bob.identity()).await.value.unwrap(); + assert_eq!(bob_account.liquid.base, bob_deposit_base_amount); + + let bob_order_id = open_order( + &bob_instance, + bob_deposit_base_amount, + assets.base.id, + OrderType::Sell, + (sell_price * 1e9) as u64, + ) + .await + .value; + + let bob_account = account(&contract, bob.identity()).await.value.unwrap(); + assert_eq!(bob_account.locked.base, bob_deposit_base_amount); + assert_eq!(bob_account.liquid.base, 0); + + let alice_deposit_quote_amount = assets.quote.parse_units(buy_price * buy_size) as u64; + let _ = deposit(&alice_instance, alice_deposit_quote_amount, assets.quote.id).await; + let alice_account = account(&contract, alice.identity()).await.value.unwrap(); + assert_eq!(alice_account.liquid.quote, alice_deposit_quote_amount); + + let alice_order_id = open_order( + &alice_instance, + assets.base.parse_units(buy_size) as u64, + assets.base.id, + OrderType::Buy, + (buy_price * 1e9) as u64, + ) + .await + .value; + + let alice_account = account(&contract, alice.identity()).await.value.unwrap(); + assert_eq!(alice_account.locked.quote, alice_deposit_quote_amount); + assert_eq!(alice_account.liquid.quote, 0); + + batch_fulfill(&contract, bob_order_id, alice_order_id).await; + + let bob_account = account(&contract, bob.identity()).await.value.unwrap(); + assert_eq!( + bob_account.locked.base, + assets.base.parse_units(bob_locked_base_expected_balance) as u64 + ); + assert_eq!(bob_account.locked.quote, 0); + assert_eq!(bob_account.liquid.base, 0); + assert_eq!( + bob_account.liquid.quote, + assets.quote.format_units(bob_liquid_quote_expected_balance) as u64 + ); + + let alice_account = account(&contract, alice.identity()).await.value.unwrap(); + assert_eq!(alice_account.locked.quote, 0); + assert_eq!(alice_account.locked.base, 0); + assert_eq!( + alice_account.liquid.quote, + assets + .quote + .format_units(alice_locked_quote_expected_balance) as u64 + ); + assert_eq!( + alice_account.liquid.base, + assets.base.parse_units(alice_liquid_base_expected_balance) as u64 + ); + } + + // ✅ buy_price > sell_price & buy_size = sell_size + #[tokio::test] + async fn greater_buy_price_and_equal_amounts() { + let buy_price = 46_000_f64; + let sell_price = 45_000_f64; + let buy_size = 1_f64; + let sell_size = 1_f64; + + let alice_liquid_base_expected_balance = 1_f64; + let alice_liquid_quote_expected_balance = 1_000_f64; + let bob_liquid_quote_expected_balance = 45_000_f64; + + let defaults = Defaults::default(); + let (contract, alice, bob, assets) = setup( + defaults.base_decimals, + defaults.quote_decimals, + defaults.price_decimals, + ) + .await; + + let bob_instance = contract.with_account(bob.wallet.clone()).unwrap(); + let alice_instance = contract.with_account(alice.wallet.clone()).unwrap(); + + let bob_deposit_base_amount = assets.base.parse_units(sell_size) as u64; + let _ = deposit(&bob_instance, bob_deposit_base_amount, assets.base.id).await; + let bob_account = account(&contract, bob.identity()).await.value.unwrap(); + assert_eq!(bob_account.liquid.base, bob_deposit_base_amount); + + let bob_order_id = open_order( + &bob_instance, + bob_deposit_base_amount, + assets.base.id, + OrderType::Sell, + (sell_price * 1e9) as u64, + ) + .await + .value; + + let bob_account = account(&contract, bob.identity()).await.value.unwrap(); + assert_eq!(bob_account.locked.base, bob_deposit_base_amount); + assert_eq!(bob_account.liquid.base, 0); + + let alice_deposit_quote_amount = assets.quote.parse_units(buy_price * buy_size) as u64; + let _ = deposit(&alice_instance, alice_deposit_quote_amount, assets.quote.id).await; + let alice_account = account(&contract, alice.identity()).await.value.unwrap(); + assert_eq!(alice_account.liquid.quote, alice_deposit_quote_amount); + + let alice_order_id = open_order( + &alice_instance, + assets.base.parse_units(buy_size) as u64, + assets.base.id, + OrderType::Buy, + (buy_price * 1e9) as u64, + ) + .await + .value; + + let alice_account = account(&contract, alice.identity()).await.value.unwrap(); + assert_eq!(alice_account.locked.quote, alice_deposit_quote_amount); + assert_eq!(alice_account.liquid.quote, 0); + + batch_fulfill(&contract, bob_order_id, alice_order_id).await; + + let bob_account = account(&contract, bob.identity()).await.value.unwrap(); + assert_eq!(bob_account.locked.base, 0); + assert_eq!(bob_account.liquid.base, 0); + assert_eq!(bob_account.locked.quote, 0); + assert_eq!( + bob_account.liquid.quote, + assets.quote.format_units(bob_liquid_quote_expected_balance) as u64 + ); + + let alice_account = account(&contract, alice.identity()).await.value.unwrap(); + assert_eq!(alice_account.locked.quote, 0); + assert_eq!( + alice_account.liquid.quote, + assets + .quote + .format_units(alice_liquid_quote_expected_balance) as u64 + ); + assert_eq!(alice_account.locked.base, 0); + assert_eq!( + alice_account.liquid.base, + assets.base.parse_units(alice_liquid_base_expected_balance) as u64 + ); + } + + // ✅ buy_price = sell_price & buy_size > sell_size + #[tokio::test] + async fn equal_prices_and_greater_buy_amount() { + let buy_price = 45_000_f64; + let sell_price = 45_000_f64; + let buy_size = 2_f64; + let sell_size = 1_f64; + + let alice_liquid_base_expected_balance = 1_f64; + let alice_locked_quote_expected_balance = 45_000_f64; + let bob_liquid_quote_expected_balance = 45_000_f64; + + let defaults = Defaults::default(); + let (contract, alice, bob, assets) = setup( + defaults.base_decimals, + defaults.quote_decimals, + defaults.price_decimals, + ) + .await; + + let bob_instance = contract.with_account(bob.wallet.clone()).unwrap(); + let alice_instance = contract.with_account(alice.wallet.clone()).unwrap(); + + let bob_deposit_base_amount = assets.base.parse_units(sell_size) as u64; + let _ = deposit(&bob_instance, bob_deposit_base_amount, assets.base.id).await; + let bob_account = account(&contract, bob.identity()).await.value.unwrap(); + assert_eq!(bob_account.liquid.base, bob_deposit_base_amount); + + let bob_order_id = open_order( + &bob_instance, + bob_deposit_base_amount, + assets.base.id, + OrderType::Sell, + (sell_price * 1e9) as u64, + ) + .await + .value; + + let bob_account = account(&contract, bob.identity()).await.value.unwrap(); + assert_eq!(bob_account.locked.base, bob_deposit_base_amount); + assert_eq!(bob_account.liquid.base, 0); + + let alice_deposit_quote_amount = assets.quote.parse_units(buy_price * buy_size) as u64; + let _ = deposit(&alice_instance, alice_deposit_quote_amount, assets.quote.id).await; + let alice_account = account(&contract, alice.identity()).await.value.unwrap(); + assert_eq!(alice_account.liquid.quote, alice_deposit_quote_amount); + + let alice_order_id = open_order( + &alice_instance, + assets.base.parse_units(buy_size) as u64, + assets.base.id, + OrderType::Buy, + (buy_price * 1e9) as u64, + ) + .await + .value; + + let alice_account = account(&contract, alice.identity()).await.value.unwrap(); + assert_eq!(alice_account.locked.quote, alice_deposit_quote_amount); + assert_eq!(alice_account.liquid.quote, 0); + + batch_fulfill(&contract, bob_order_id, alice_order_id).await; + + let bob_account = account(&contract, bob.identity()).await.value.unwrap(); + assert_eq!(bob_account.locked.base, 0); + assert_eq!(bob_account.liquid.base, 0); + assert_eq!(bob_account.locked.quote, 0); + assert_eq!( + bob_account.liquid.quote, + assets.quote.format_units(bob_liquid_quote_expected_balance) as u64 + ); + + let alice_account = account(&contract, alice.identity()).await.value.unwrap(); + + assert_eq!( + alice_account.locked.quote, + assets + .quote + .format_units(alice_locked_quote_expected_balance) as u64 + ); + assert_eq!(alice_account.liquid.quote, 0); + assert_eq!(alice_account.locked.base, 0); + assert_eq!( + alice_account.liquid.base, + assets.base.parse_units(alice_liquid_base_expected_balance) as u64 + ); + } + + // ✅ buy_price = sell_price & buy_size < sell_size + #[tokio::test] + async fn equal_prices_and_greater_sell_amount() { + let buy_price = 45_000_f64; + let sell_price = 45_000_f64; + let buy_size = 1_f64; + let sell_size = 2_f64; + + let alice_liquid_base_expected_balance = 1_f64; + let bob_liquid_quote_expected_balance = 45_000_f64; + let bob_remaining_locked_base = 1_f64; + + let defaults = Defaults::default(); + let (contract, alice, bob, assets) = setup( + defaults.base_decimals, + defaults.quote_decimals, + defaults.price_decimals, + ) + .await; + + let bob_instance = contract.with_account(bob.wallet.clone()).unwrap(); + let alice_instance = contract.with_account(alice.wallet.clone()).unwrap(); + + let bob_deposit_base_amount = assets.base.parse_units(sell_size) as u64; + let _ = deposit(&bob_instance, bob_deposit_base_amount, assets.base.id).await; + let bob_account = account(&contract, bob.identity()).await.value.unwrap(); + assert_eq!(bob_account.liquid.base, bob_deposit_base_amount); + + let bob_order_id = open_order( + &bob_instance, + bob_deposit_base_amount, + assets.base.id, + OrderType::Sell, + (sell_price * 1e9) as u64, + ) + .await + .value; + + let bob_account = account(&contract, bob.identity()).await.value.unwrap(); + assert_eq!(bob_account.locked.base, bob_deposit_base_amount); + assert_eq!(bob_account.liquid.base, 0); + + let alice_deposit_quote_amount = assets.quote.parse_units(buy_price * buy_size) as u64; + let _ = deposit(&alice_instance, alice_deposit_quote_amount, assets.quote.id).await; + let alice_account = account(&contract, alice.identity()).await.value.unwrap(); + assert_eq!(alice_account.liquid.quote, alice_deposit_quote_amount); + + let alice_order_id = open_order( + &alice_instance, + assets.base.parse_units(buy_size) as u64, + assets.base.id, + OrderType::Buy, + (buy_price * 1e9) as u64, + ) + .await + .value; + + let alice_account = account(&contract, alice.identity()).await.value.unwrap(); + assert_eq!(alice_account.locked.quote, alice_deposit_quote_amount); + assert_eq!(alice_account.liquid.quote, 0); + + batch_fulfill(&contract, bob_order_id, alice_order_id).await; + + let bob_account = account(&contract, bob.identity()).await.value.unwrap(); + assert_eq!( + bob_account.locked.base, + assets.base.parse_units(bob_remaining_locked_base) as u64 + ); + assert_eq!(bob_account.liquid.base, 0); + assert_eq!(bob_account.locked.quote, 0); + assert_eq!( + bob_account.liquid.quote, + assets.quote.format_units(bob_liquid_quote_expected_balance) as u64 + ); + + let alice_account = account(&contract, alice.identity()).await.value.unwrap(); + assert_eq!(alice_account.locked.quote, 0); + assert_eq!(alice_account.liquid.quote, 0); + assert_eq!(alice_account.locked.base, 0); + assert_eq!( + alice_account.liquid.base, + assets.base.parse_units(alice_liquid_base_expected_balance) as u64 + ); + } + + //✅ buy_price = sell_price & buy_size = sell_size + #[tokio::test] + async fn equal_prices_and_equal_amounts() { + let buy_price = 45_000_f64; + let sell_price = 45_000_f64; + let buy_size = 1_f64; + let sell_size = 1_f64; + + let alice_liquid_base_expected_balance = 1_f64; + let bob_liquid_quote_expected_balance = 45_000_f64; + + let defaults = Defaults::default(); + let (contract, alice, bob, assets) = setup( + defaults.base_decimals, + defaults.quote_decimals, + defaults.price_decimals, + ) + .await; + + let bob_instance = contract.with_account(bob.wallet.clone()).unwrap(); + let alice_instance = contract.with_account(alice.wallet.clone()).unwrap(); + + let bob_deposit_base_amount = assets.base.parse_units(sell_size) as u64; + let _ = deposit(&bob_instance, bob_deposit_base_amount, assets.base.id).await; + let bob_account = account(&contract, bob.identity()).await.value.unwrap(); + assert_eq!(bob_account.liquid.base, bob_deposit_base_amount); + + let bob_order_id = open_order( + &bob_instance, + bob_deposit_base_amount, + assets.base.id, + OrderType::Sell, + (sell_price * 1e9) as u64, + ) + .await + .value; + + let bob_account = account(&contract, bob.identity()).await.value.unwrap(); + assert_eq!(bob_account.locked.base, bob_deposit_base_amount); + assert_eq!(bob_account.liquid.base, 0); + + let alice_deposit_quote_amount = assets.quote.parse_units(buy_price * buy_size) as u64; + let _ = deposit(&alice_instance, alice_deposit_quote_amount, assets.quote.id).await; + let alice_account = account(&contract, alice.identity()).await.value.unwrap(); + assert_eq!(alice_account.liquid.quote, alice_deposit_quote_amount); + + let alice_order_id = open_order( + &alice_instance, + assets.base.parse_units(buy_size) as u64, + assets.base.id, + OrderType::Buy, + (buy_price * 1e9) as u64, + ) + .await + .value; + + let alice_account = account(&contract, alice.identity()).await.value.unwrap(); + assert_eq!(alice_account.locked.quote, alice_deposit_quote_amount); + assert_eq!(alice_account.liquid.quote, 0); + + batch_fulfill(&contract, bob_order_id, alice_order_id).await; + + let bob_account = account(&contract, bob.identity()).await.value.unwrap(); + assert_eq!(bob_account.locked.base, 0); + assert_eq!(bob_account.liquid.base, 0); + assert_eq!(bob_account.locked.quote, 0); + assert_eq!( + bob_account.liquid.quote, + assets.quote.format_units(bob_liquid_quote_expected_balance) as u64 + ); + + let alice_account = account(&contract, alice.identity()).await.value.unwrap(); + assert_eq!(alice_account.locked.quote, 0); + assert_eq!(alice_account.liquid.quote, 0); + assert_eq!(alice_account.locked.base, 0); + assert_eq!( + alice_account.liquid.base, + assets.base.parse_units(alice_liquid_base_expected_balance) as u64 + ); + } +} + +// mod revert { +// use super::*; + +// // ❌ buy_price < sell_price & buy_size > sell_size +// #[tokio::test] +// #[should_panic(expected = "CannotTrade")] +// async fn match4() { +// let buy_price = 44_000_f64; +// let buy_size = 2_f64; +// let sell_price = 45_000_f64; +// let sell_size = 1_f64; +// } + +// // // ❌ buy_price < sell_price & buy_size < sell_size +// // #[tokio::test] +// // #[should_panic(expected = "CannotTrade")] +// // async fn match5() { +// // let buy_price = 44_000_f64; +// // let buy_size = 1_f64; +// // let sell_price = 45_000_f64; +// // let sell_size = 2_f64; +// // } + +// // // ❌ buy_price < sell_price & buy_size = sell_size +// // #[tokio::test] +// // #[should_panic(expected = "CannotTrade")] +// // async fn match6() { +// // let buy_price = 44_000_f64; +// // let buy_size = 1_f64; +// // let sell_price = 45_000_f64; +// // let sell_size = 1_f64; +// // } +// } diff --git a/market-contract/tests/functions/core/mod.rs b/market-contract/tests/functions/core/mod.rs index a991a0b..ee69bb8 100644 --- a/market-contract/tests/functions/core/mod.rs +++ b/market-contract/tests/functions/core/mod.rs @@ -4,3 +4,4 @@ mod deposit; mod open_order; mod set_fee; mod withdraw; +mod match_orders; diff --git a/market-contract/tests/utils/interface/core.rs b/market-contract/tests/utils/interface/core.rs index 9b2472f..63dd14b 100644 --- a/market-contract/tests/utils/interface/core.rs +++ b/market-contract/tests/utils/interface/core.rs @@ -11,7 +11,7 @@ pub(crate) async fn deposit( amount: u64, asset: AssetId, ) -> FuelCallResponse<()> { - let tx_params = TxPolicies::new(Some(0), Some(2_000_000), None, None, None); + let tx_params = TxPolicies::new(Some(1), Some(2_000_000), None, None, None); let call_params = CallParameters::new(amount, asset, 1_000_000); contract @@ -66,15 +66,15 @@ pub(crate) async fn cancel_order( .unwrap() } -#[allow(dead_code)] +// #[allow(dead_code)] pub(crate) async fn batch_fulfill( contract: &Market, - order_id: Bits256, - orders: Vec, + order_buy_id: Bits256, + order_sell_id: Bits256, ) -> FuelCallResponse<()> { contract .methods() - .batch_fulfill(order_id, orders) + .batch_fulfill(order_buy_id, order_sell_id) .call() .await .unwrap() diff --git a/market-contract/tests/utils/setup.rs b/market-contract/tests/utils/setup.rs index 698375e..e9cfb67 100644 --- a/market-contract/tests/utils/setup.rs +++ b/market-contract/tests/utils/setup.rs @@ -23,6 +23,15 @@ pub(crate) struct Assets { pub(crate) random: Asset, } +impl Asset { + pub fn parse_units(&self, value: f64) -> f64 { + value * 10_f64.powf(self.decimals as f64) + } + pub fn format_units(&self, value: f64) -> f64 { + value / 10_f64.powf(self.decimals as f64) + } +} + pub(crate) struct Asset { pub(crate) id: AssetId, pub(crate) decimals: u32, @@ -87,7 +96,7 @@ pub(crate) async fn setup( ) -> (Market, User, User, Assets) { let number_of_wallets = 2; let coins_per_wallet = 1; - let amount_per_coin = 100_000_000; + let amount_per_coin = 100_000_000_000_000; let base_asset_id = AssetId::new([0; 32]); let quote_asset_id = AssetId::new([1; 32]);