diff --git a/market-contract/Forc.lock b/market-contract/Forc.lock index 04fbb97..fab76fc 100644 --- a/market-contract/Forc.lock +++ b/market-contract/Forc.lock @@ -2,10 +2,18 @@ name = "core" source = "path+from-root-566CA1D5F8BEAFBF" +[[package]] +name = "fixed_point" +source = "git+https://github.com/FuelLabs/sway-libs?tag=v0.18.0#8d196e9379463d4596ac582a20a84ed52ff58c69" +dependencies = ["std"] + [[package]] name = "market-contract" source = "member" -dependencies = ["std"] +dependencies = [ + "fixed_point", + "std", +] [[package]] name = "std" diff --git a/market-contract/Forc.toml b/market-contract/Forc.toml index b48f2e7..afe2fb2 100644 --- a/market-contract/Forc.toml +++ b/market-contract/Forc.toml @@ -5,3 +5,4 @@ license = "Apache-2.0" name = "market-contract" [dependencies] +fixed_point = { git = "https://github.com/FuelLabs/sway-libs", tag = "v0.18.0" } diff --git a/market-contract/src/main.sw b/market-contract/src/main.sw index 74e64c9..fc9d31c 100644 --- a/market-contract/src/main.sw +++ b/market-contract/src/main.sw @@ -340,6 +340,7 @@ impl Market for Contract { alice_account_delta, bob_order_amount_decrease, bob_account_delta, + bob_unlock_amount, ) = trade.unwrap(); // Update the order quantities with the amounts that can be traded @@ -364,7 +365,7 @@ impl Market for Contract { alice_account.locked.debit(alice_account_delta, asset_1); alice_account.liquid.credit(bob_account_delta, asset_2); - bob_account.locked.debit(bob_account_delta, asset_2); + bob_account.locked.debit(bob_account_delta, asset_2); //todo bob_account.liquid.credit(alice_account_delta, asset_1); // Save bob's account because his order is finished @@ -376,6 +377,15 @@ impl Market for Contract { require(storage.orders.remove(bob_id), OrderError::FailedToRemove); remove_user_order(bob.owner, bob_id); + // Edge case to rescue funds when bob's price was greater and they reduced alice + // amount to 0 + // Ex. Alice sell 1 BTC @ 70k, Bob buy 1 BTC @ 71k. Rescue 1k worth of locked funds + if bob_unlock_amount != 0 { + bob_account.locked.debit(bob_unlock_amount, asset_2); //todo + bob_account.liquid.credit(bob_unlock_amount, asset_2); + storage.account.insert(bob.owner, bob_account); + } + // TODO: Emit event // log(TradeEvent { // order_id: bob_id, diff --git a/market-contract/src/math.sw b/market-contract/src/math.sw index ba772ea..a90ac46 100644 --- a/market-contract/src/math.sw +++ b/market-contract/src/math.sw @@ -4,6 +4,7 @@ use ::data_structures::{asset_type::AssetType, order::Order, order_type::OrderTy use ::errors::TradeError; use std::u128::U128; +use fixed_point::ufp128::UFP128; impl u64 { pub fn mul_div(self, mul_to: u64, div_to: u64) -> u64 { @@ -26,9 +27,9 @@ impl u64 { } fn calc_amount(buy_amount: u64, buy_price: u64, sell_price: u64) -> u64 { - let price_ratio = U128::from((0, buy_price)) / U128::from((0, sell_price)); - let amount = price_ratio * U128::from((0, buy_amount)); - amount.as_u64().unwrap() + let price_ratio = UFP128::from((0, buy_price)) / UFP128::from((0, sell_price)); + let amount = price_ratio * UFP128::from((0, buy_amount)); + U128::from(amount.into()).as_u64().unwrap() } pub fn attempt_trade( @@ -37,7 +38,7 @@ pub fn attempt_trade( base_asset_decimals: u32, quote_asset_decimals: u32, price_decimals: u32, -) -> Result<(u64, u64, u64, u64), TradeError> { +) -> Result<(u64, u64, u64, u64, u64), TradeError> { // In this function: // Decrease the order size for alice and bob until they are 0 == their orders are fulfilled // Track the amount that each account has to transfer for their trade @@ -50,9 +51,10 @@ pub fn attempt_trade( mut seller_account_delta, mut buyer_order_amount_decrease, mut buyer_account_delta, + mut bob_unlock_amount, ) = match alice.order_type { - OrderType::Sell => (alice, bob, 0, 0, 0, 0), - OrderType::Buy => (bob, alice, 0, 0, 0, 0), + OrderType::Sell => (alice, bob, 0, 0, 0, 0, 0), + OrderType::Buy => (bob, alice, 0, 0, 0, 0, 0), }; if buyer.price < seller.price { @@ -73,11 +75,12 @@ pub fn attempt_trade( price_decimals, quote_asset_decimals, ); + bob_unlock_amount = buyer.price - seller.price; } else if buyer_buy_amount < seller.amount { seller_order_amount_decrease = buyer_buy_amount; - buyer_order_amount_decrease = buyer_buy_amount; + buyer_order_amount_decrease = buyer.amount; buyer_account_delta = base_to_quote_amount( - buyer_order_amount_decrease, + seller_order_amount_decrease, base_asset_decimals, buyer.price, price_decimals, @@ -110,6 +113,7 @@ pub fn attempt_trade( price_decimals, quote_asset_decimals, ); + bob_unlock_amount = buyer.price - seller.price; } else if buyer_buy_amount < seller.amount { seller_order_amount_decrease = buyer_buy_amount; buyer_order_amount_decrease = buyer_buy_amount; @@ -143,12 +147,14 @@ pub fn attempt_trade( seller_account_delta, buyer_order_amount_decrease, buyer_account_delta, + bob_unlock_amount, )), OrderType::Buy => Result::Ok(( buyer_order_amount_decrease, buyer_account_delta, seller_order_amount_decrease, seller_account_delta, + bob_unlock_amount, )), } } diff --git a/market-contract/tests/functions/core/batch_fulfill.rs b/market-contract/tests/functions/core/batch_fulfill.rs index ee4f216..f97edcb 100644 --- a/market-contract/tests/functions/core/batch_fulfill.rs +++ b/market-contract/tests/functions/core/batch_fulfill.rs @@ -167,10 +167,83 @@ mod success { Ok(()) } - #[ignore] #[tokio::test] async fn bob_buy_greater_price_same_amount() -> anyhow::Result<()> { // Bob should only get the exact amount that alice is selling + let defaults = Defaults::default(); + let (alice_contract, owner, user, assets) = setup( + defaults.base_decimals, + defaults.quote_decimals, + defaults.price_decimals, + ) + .await?; + let bob_contract = alice_contract.with_account(&user.wallet).await?; + + let alice_order_id = open_order( + &alice_contract, + &assets.base, + 1.0, + OrderType::Sell, + 1.0, + &assets.base, + 70000.0, + &owner, + true, + 1, + &defaults, + ) + .await?; + + let bob_order_id = open_order( + &bob_contract, + &assets.quote, + 70000.0 * 2.0, + OrderType::Buy, + 1.0, + &assets.base, + 71000.0, + &user, + false, + 1, + &defaults, + ) + .await?; + + // TODO: assert log events + let _response = alice_contract + .batch_fulfill(alice_order_id, vec![bob_order_id]) + .await?; + + let alice_orders = alice_contract + .user_orders(owner.identity()) + .await? + .value + .len() as u64; + let alice_account = alice_contract + .account(owner.identity()) + .await? + .value + .unwrap(); + let alice_expected_account = + create_account(0, assets.quote.to_contract_units(70000.0), 0, 0); + assert_eq!(alice_account, alice_expected_account); + assert_eq!(alice_orders, 0); + + let bob_orders = alice_contract + .user_orders(user.identity()) + .await? + .value + .len() as u64; + + let bob_account = bob_contract.account(user.identity()).await?.value.unwrap(); + let bob_expected_account = create_account( + assets.base.to_contract_units(1.0), + assets.quote.to_contract_units(70000.0), + 0, + 0, + ); + assert_eq!(bob_account, bob_expected_account); + assert_eq!(bob_orders, 0); Ok(()) } @@ -205,7 +278,7 @@ mod success { let bob_order_id = open_order( &bob_contract, &assets.quote, - 70000.0 * 1.5, + 70000.0 * 2.0, OrderType::Buy, 0.5, &assets.base, @@ -231,6 +304,107 @@ mod success { let alice_remaining_locked_base = assets.base.to_contract_units(1.0) - bought_amount_in_base; + // TODO: assert log events + // TODO: bob may not have enough funds so it errors? + let _response = alice_contract + .batch_fulfill(alice_order_id, vec![bob_order_id]) + .await?; + + // let alice_orders = alice_contract + // .user_orders(owner.identity()) + // .await? + // .value + // .len() as u64; + // let alice_account = alice_contract + // .account(owner.identity()) + // .await? + // .value + // .unwrap(); + // let alice_expected_account = + // create_account(0, bought_amount_in_quote, alice_remaining_locked_base, 0); + // assert_eq!(alice_account, alice_expected_account); + // assert_eq!(alice_orders, 0); + + // let bob_orders = alice_contract + // .user_orders(user.identity()) + // .await? + // .value + // .len() as u64; + + // let deposit = assets.quote.to_contract_units(70000.0 * 1.5); + // let order_amount_in_quote = base_to_quote_amount( + // assets.base.to_contract_units(0.5), + // defaults.base_decimals, + // assets.base.to_contract_units(71000.0), + // defaults.price_decimals, + // defaults.quote_decimals, + // ); + // let remaining_deposit = deposit - order_amount_in_quote; + // let alice_amount_in_quote = base_to_quote_amount( + // assets.base.to_contract_units(1.0), + // defaults.base_decimals, + // assets.base.to_contract_units(70000.0), + // defaults.price_decimals, + // defaults.quote_decimals, + // ); + // let remaining_amount = order_amount_in_quote - alice_amount_in_quote; + + // // deposit is liquid quote + // // lock some in quote for order (deposit - order) + // // fulfil order which is less than locked amount (deposit - order) - alice in quote + // let bob_account = bob_contract.account(user.identity()).await?.value.unwrap(); + // let bob_expected_account = create_account( + // assets.base.to_contract_units(1.0), + // remaining_deposit, + // 0, + // remaining_amount, + // ); + // assert_eq!(bob_account, bob_expected_account); + // assert_eq!(bob_orders, 1); + Ok(()) + } + + #[tokio::test] + async fn bob_buy_same_price_same_amount() -> anyhow::Result<()> { + let defaults = Defaults::default(); + let (alice_contract, owner, user, assets) = setup( + defaults.base_decimals, + defaults.quote_decimals, + defaults.price_decimals, + ) + .await?; + let bob_contract = alice_contract.with_account(&user.wallet).await?; + + let alice_order_id = open_order( + &alice_contract, + &assets.base, + 1.0, + OrderType::Sell, + 1.0, + &assets.base, + 70000.0, + &owner, + true, + 1, + &defaults, + ) + .await?; + + let bob_order_id = open_order( + &bob_contract, + &assets.quote, + 70000.0, + OrderType::Buy, + 1.0, + &assets.base, + 70000.0, + &user, + false, + 1, + &defaults, + ) + .await?; + // TODO: assert log events let _response = alice_contract .batch_fulfill(alice_order_id, vec![bob_order_id]) @@ -247,7 +421,7 @@ mod success { .value .unwrap(); let alice_expected_account = - create_account(0, bought_amount_in_quote, alice_remaining_locked_base, 0); + create_account(0, assets.quote.to_contract_units(70000.0), 0, 0); assert_eq!(alice_account, alice_expected_account); assert_eq!(alice_orders, 0); @@ -257,41 +431,17 @@ mod success { .value .len() as u64; - let deposit = assets.quote.to_contract_units(70000.0 * 1.5); - let order_amount_in_quote = base_to_quote_amount( - assets.base.to_contract_units(0.5), - defaults.base_decimals, - assets.base.to_contract_units(71000.0), - defaults.price_decimals, - defaults.quote_decimals, - ); - let remaining_deposit = deposit - order_amount_in_quote; - let alice_amount_in_quote = base_to_quote_amount( - assets.base.to_contract_units(1.0), - defaults.base_decimals, - assets.base.to_contract_units(70000.0), - defaults.price_decimals, - defaults.quote_decimals, - ); - let remaining_amount = order_amount_in_quote - alice_amount_in_quote; - - // deposit is liquid quote - // lock some in quote for order (deposit - order) - // fulfil order which is less than locked amount (deposit - order) - alice in quote let bob_account = bob_contract.account(user.identity()).await?.value.unwrap(); - let bob_expected_account = create_account( - assets.base.to_contract_units(1.0), - remaining_deposit, - 0, - remaining_amount, - ); + let bob_expected_account = + create_account(assets.base.to_contract_units(1.0), 0, 0, 0); assert_eq!(bob_account, bob_expected_account); - assert_eq!(bob_orders, 1); + assert_eq!(bob_orders, 0); Ok(()) } #[tokio::test] - async fn bob_buy_same_price_same_amount() -> anyhow::Result<()> { + async fn bob_buy_same_price_greater_amount() -> anyhow::Result<()> { + // Bob should only get the exact amount that alice is selling let defaults = Defaults::default(); let (alice_contract, owner, user, assets) = setup( defaults.base_decimals, @@ -319,9 +469,9 @@ mod success { let bob_order_id = open_order( &bob_contract, &assets.quote, - 70000.0, + 70000.0 * 1.5, OrderType::Buy, - 1.0, + 1.5, &assets.base, 70000.0, &user, @@ -358,17 +508,15 @@ mod success { .len() as u64; let bob_account = bob_contract.account(user.identity()).await?.value.unwrap(); - let bob_expected_account = - create_account(assets.base.to_contract_units(1.0), 0, 0, 0); + let bob_expected_account = create_account( + assets.base.to_contract_units(1.0), + 0, + 0, + assets.quote.to_contract_units(35000.0), + ); assert_eq!(bob_account, bob_expected_account); - assert_eq!(bob_orders, 0); - Ok(()) - } + assert_eq!(bob_orders, 1); - #[ignore] - #[tokio::test] - async fn bob_buy_same_price_greater_amount() -> anyhow::Result<()> { - // Bob should only get the exact amount that alice is selling Ok(()) } @@ -394,6 +542,81 @@ mod success { #[ignore] #[tokio::test] async fn bob_sell_smaller_price_same_amount() -> anyhow::Result<()> { + let defaults = Defaults::default(); + let (alice_contract, owner, user, assets) = setup( + defaults.base_decimals, + defaults.quote_decimals, + defaults.price_decimals, + ) + .await?; + let bob_contract = alice_contract.with_account(&user.wallet).await?; + + let alice_order_id = open_order( + &alice_contract, + &assets.quote, + 71000.0, + OrderType::Buy, + 1.0, + &assets.base, + 71000.0, + &owner, + false, + 1, + &defaults, + ) + .await?; + + let bob_order_id = open_order( + &bob_contract, + &assets.base, + 1.0, + OrderType::Sell, + 1.0, + &assets.base, + 70000.0, + &user, + true, + 1, + &defaults, + ) + .await?; + + // TODO: assert log events + let _response = alice_contract + // .batch_fulfill(bob_order_id, vec![alice_order_id]) // not crash? + .batch_fulfill(alice_order_id, vec![bob_order_id]) // test case crashes?? + .await?; + + // let alice_orders = alice_contract + // .user_orders(owner.identity()) + // .await? + // .value + // .len() as u64; + // let alice_account = alice_contract + // .account(owner.identity()) + // .await? + // .value + // .unwrap(); + // let alice_expected_account = create_account( + // assets.base.to_contract_units(1.0), + // assets.quote.to_contract_units(1000.0), + // 0, + // 0, + // ); + // assert_eq!(alice_account, alice_expected_account); + // assert_eq!(alice_orders, 0); + + // let bob_orders = alice_contract + // .user_orders(user.identity()) + // .await? + // .value + // .len() as u64; + + // let bob_account = bob_contract.account(user.identity()).await?.value.unwrap(); + // let bob_expected_account = + // create_account(0, assets.quote.to_contract_units(70000.0), 0, 0); + // assert_eq!(bob_account, bob_expected_account); + // assert_eq!(bob_orders, 0); Ok(()) } @@ -575,9 +798,84 @@ mod success { #[tokio::test] async fn bob_sell_same_price_greater_amount() -> anyhow::Result<()> { + let defaults = Defaults::default(); + let (alice_contract, owner, user, assets) = setup( + defaults.base_decimals, + defaults.quote_decimals, + defaults.price_decimals, + ) + .await?; + let bob_contract = alice_contract.with_account(&user.wallet).await?; + + let alice_order_id = open_order( + &alice_contract, + &assets.quote, + 70000.0, + OrderType::Buy, + 1.0, + &assets.base, + 70000.0, + &owner, + false, + 1, + &defaults, + ) + .await?; + + let bob_order_id = open_order( + &bob_contract, + &assets.base, + 1.5, + OrderType::Sell, + 1.5, + &assets.base, + 70000.0, + &user, + true, + 1, + &defaults, + ) + .await?; + + // TODO: assert log events + let _response = alice_contract + .batch_fulfill(alice_order_id, vec![bob_order_id]) + .await?; + + let alice_orders = alice_contract + .user_orders(owner.identity()) + .await? + .value + .len() as u64; + let alice_account = alice_contract + .account(owner.identity()) + .await? + .value + .unwrap(); + let alice_expected_account = + create_account(assets.base.to_contract_units(1.0), 0, 0, 0); + assert_eq!(alice_account, alice_expected_account); + assert_eq!(alice_orders, 0); + + let bob_orders = alice_contract + .user_orders(user.identity()) + .await? + .value + .len() as u64; + + let bob_account = bob_contract.account(user.identity()).await?.value.unwrap(); + let bob_expected_account = create_account( + 0, + assets.quote.to_contract_units(70000.0), + assets.base.to_contract_units(0.5), + 0, + ); + assert_eq!(bob_account, bob_expected_account); + assert_eq!(bob_orders, 1); Ok(()) } + #[ignore] #[tokio::test] async fn bob_sell_same_price_smaller_amount() -> anyhow::Result<()> { Ok(())