From 92f7e9f065fa0f36310787b0e64ceb348d2ba350 Mon Sep 17 00:00:00 2001 From: txhsl <799498265@qq.com> Date: Fri, 26 Aug 2022 17:59:21 +0800 Subject: [PATCH 01/16] remove aggregator from v1 --- .../FlamingoSwapOrderBook/Extensions.cs | 30 + .../FlamingoSwapOrderBook.csproj | 12 + .../FlamingoSwapOrderBookContract.Admin.cs | 109 ++ .../FlamingoSwapOrderBookContract.Event.cs | 24 + .../FlamingoSwapOrderBookContract.Helper.cs | 499 +++++++++ .../FlamingoSwapOrderBookContract.cs | 990 ++++++++++++++++++ .../Models/LimitOrder.cs | 14 + .../FlamingoSwapOrderBook/Models/OrderBook.cs | 18 + .../Models/OrderReceipt.cs | 19 + .../Models/ReservesData.cs | 10 + .../flamingo-contract-swap.sln | 8 +- 11 files changed, 1732 insertions(+), 1 deletion(-) create mode 100644 Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Extensions.cs create mode 100644 Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBook.csproj create mode 100644 Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Admin.cs create mode 100644 Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Event.cs create mode 100644 Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Helper.cs create mode 100644 Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs create mode 100644 Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/LimitOrder.cs create mode 100644 Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/OrderBook.cs create mode 100644 Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/OrderReceipt.cs create mode 100644 Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/ReservesData.cs diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Extensions.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Extensions.cs new file mode 100644 index 0000000..e0f8941 --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Extensions.cs @@ -0,0 +1,30 @@ +using System.Numerics; +using Neo; +using Neo.SmartContract.Framework; +using Neo.SmartContract.Framework.Attributes; + +namespace FlamingoSwapOrderBook +{ + public static class Extensions + { + /// + /// uint160 转为正整数,用于合约地址排序,其它场景勿用 + /// + /// 合约地址 + /// + [OpCode(OpCode.PUSHDATA1, "0100")] + [OpCode(OpCode.CAT)] + [OpCode(OpCode.CONVERT, "21")] + public static extern BigInteger ToUInteger(this UInt160 val); + + /// + /// Is Valid and not Zero address + /// + /// + /// + public static bool IsAddress(this UInt160 address) + { + return address.IsValid && !address.IsZero; + } + } +} diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBook.csproj b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBook.csproj new file mode 100644 index 0000000..ec0abe1 --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBook.csproj @@ -0,0 +1,12 @@ + + + net6.0 + + + + + + + + + \ No newline at end of file diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Admin.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Admin.cs new file mode 100644 index 0000000..54be322 --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Admin.cs @@ -0,0 +1,109 @@ +using Neo; +using Neo.SmartContract; +using Neo.SmartContract.Framework; +using Neo.SmartContract.Framework.Attributes; +using Neo.SmartContract.Framework.Native; +using Neo.SmartContract.Framework.Services; + +namespace FlamingoSwapOrderBook +{ + public partial class FlamingoSwapOrderBookContract + { + #region Admin + +#warning Update the admin address if necessary + [InitialValue("NdrUjmLFCmr6RjM52njho5sFUeeTdKPxG9", ContractParameterType.Hash160)] + static readonly UInt160 superAdmin = default; + + [InitialValue("0xc0695bdb8a87a40aff33c73ff6349ccc05fa9f01", ContractParameterType.Hash160)] + static readonly UInt160 Factory = default; + + [InitialValue("0x48c40d4666f93408be1bef038b6722404d9a4c2a", ContractParameterType.Hash160)] + static readonly UInt160 bNEO = default; + + private const string AdminKey = nameof(superAdmin); + private const string GASAdminKey = nameof(GASAdminKey); + private const string FundAddresskey = nameof(FundAddresskey); + + private static readonly byte[] OrderIDKey = new byte[] { 0x00 }; + private static readonly byte[] BookMapPrefix = new byte[] { 0x01 }; + private static readonly byte[] OrderMapPrefix = new byte[] { 0x02 }; + private static readonly byte[] ReceiptMapPrefix = new byte[] { 0x03 }; + private static readonly byte[] PauseMapPrefix = new byte[] { 0x04 }; + + // When this contract address is included in the transaction signature, + // this method will be triggered as a VerificationTrigger to verify that the signature is correct. + // For example, this method needs to be called when withdrawing token from the contract. + [Safe] + public static bool Verify() => Runtime.CheckWitness(GetAdmin()); + + [Safe] + public static UInt160 GetAdmin() + { + var admin = Storage.Get(Storage.CurrentReadOnlyContext, AdminKey); + return admin?.Length == 20 ? (UInt160)admin : superAdmin; + } + + public static bool SetAdmin(UInt160 admin) + { + Assert(Verify(), "No Authorization"); + Assert(admin.IsAddress(), "Invalid Address"); + Storage.Put(Storage.CurrentContext, AdminKey, admin); + return true; + } + + public static void ClaimGASFrombNEO(UInt160 receiveAddress) + { + Assert(Runtime.CheckWitness(GetGASAdmin()), "Forbidden"); + var me = Runtime.ExecutingScriptHash; + var beforeBalance = GAS.BalanceOf(me); + Assert((bool)Contract.Call(bNEO, "transfer", CallFlags.All, Runtime.ExecutingScriptHash, bNEO, 0, null), "claim fail"); + var afterBalance = GAS.BalanceOf(me); + + GAS.Transfer(me, receiveAddress, afterBalance - beforeBalance); + } + + [Safe] + public static UInt160 GetGASAdmin() + { + var address = Storage.Get(Storage.CurrentReadOnlyContext, GASAdminKey); + return address?.Length == 20 ? (UInt160)address : null; + } + + public static bool SetGASAdmin(UInt160 GASAdmin) + { + Assert(GASAdmin.IsAddress(), "Invalid Address"); + Assert(Verify(), "No Authorization"); + Storage.Put(Storage.CurrentContext, GASAdminKey, GASAdmin); + return true; + } + #endregion + + #region FundFee + + [Safe] + public static UInt160 GetFundAddress() + { + var address = Storage.Get(Storage.CurrentReadOnlyContext, FundAddresskey); + return address?.Length == 20 ? (UInt160)address : null; + } + + public static bool SetFundAddress(UInt160 address) + { + Assert(address.IsAddress(), "Invalid Address"); + Assert(Verify(), "No Authorization"); + Storage.Put(Storage.CurrentContext, FundAddresskey, address); + return true; + } + #endregion + + #region Upgrade + + public static void Update(ByteString nefFile, string manifest) + { + Assert(Verify(), "No Authorization"); + ContractManagement.Update(nefFile, manifest, null); + } + #endregion + } +} diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Event.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Event.cs new file mode 100644 index 0000000..dd3a4da --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Event.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using System.Numerics; +using Neo; +using Neo.SmartContract.Framework; + +namespace FlamingoSwapOrderBook +{ + public partial class FlamingoSwapOrderBookContract + { + /// + /// When orderbook status changed + /// + [DisplayName("BookStatusChanged")] + public static event BookStatusChangedEvent onBookStatusChanged; + public delegate void BookStatusChangedEvent(UInt160 baseToken, UInt160 quoteToken, BigInteger quoteScale, BigInteger minOrderAmount, BigInteger maxOrderAmount, bool isPaused); + + /// + /// When order status changed + /// + [DisplayName("OrderStatusChanged")] + public static event OrderStatusChangedEvent onOrderStatusChanged; + public delegate void OrderStatusChangedEvent(UInt160 baseToken, UInt160 quoteToken, ByteString id, bool isBuy, UInt160 maker, BigInteger price, BigInteger leftAmount); + } +} \ No newline at end of file diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Helper.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Helper.cs new file mode 100644 index 0000000..c9de1a8 --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Helper.cs @@ -0,0 +1,499 @@ +using Neo; +using Neo.SmartContract.Framework; +using Neo.SmartContract.Framework.Attributes; +using Neo.SmartContract.Framework.Native; +using Neo.SmartContract.Framework.Services; +using System; +using System.Numerics; + +namespace FlamingoSwapOrderBook +{ + public partial class FlamingoSwapOrderBookContract + { + /// + /// Insert a not-fully-deal limit order into orderbook + /// + /// + /// + /// + /// + /// + /// + private static bool InsertOrder(byte[] pairKey, OrderBook book, ByteString id, LimitOrder order, bool isBuy) + { + var firstID = isBuy ? book.firstBuyID : book.firstSellID; + + // Check if there is no order + var canBeFirst = firstID is null; + if (!canBeFirst) + { + // Check if this order is the worthiest + var firstOrder = GetOrder(firstID); + canBeFirst = (isBuy && (firstOrder.price < order.price)) || (!isBuy && (firstOrder.price > order.price)); + } + if (canBeFirst) + { + // Insert to the first + if (isBuy) book.firstBuyID = id; + else book.firstSellID = id; + SetOrderBook(pairKey, book); + order.nextID = firstID; + SetOrder(id, order); + return true; + } + else + { + // Find the position + return InsertNotFirst(firstID, id, order, isBuy); + } + } + + private static bool InsertNotFirst(ByteString firstID, ByteString id, LimitOrder order, bool isBuy) + { + var currentID = firstID; + while (currentID is not null) + { + // Check before + var currentOrder = GetOrder(currentID); + var canFollow = (isBuy && (order.price <= currentOrder.price)) || (!isBuy && (order.price >= currentOrder.price)); + if (!canFollow) break; + + if (currentOrder.nextID is not null) + { + // Check after + var nextOrder = GetOrder(currentOrder.nextID); + var canPrecede = (isBuy && (nextOrder.price < order.price)) || (!isBuy && (nextOrder.price > order.price)); + canFollow = canFollow && canPrecede; + } + if (canFollow) + { + // Do insert + order.nextID = currentOrder.nextID; + SetOrder(id, order); + currentOrder.nextID = id; + SetOrder(currentID, currentOrder); + return true; + } + currentID = currentOrder.nextID; + } + return false; + } + + private static bool InsertOrderAt(ByteString parentID, ByteString id, LimitOrder order, bool isBuy) + { + var parentOrder = GetOrder(parentID); + var canFollow = (isBuy && (order.price <= parentOrder.price)) || (!isBuy && (order.price >= parentOrder.price)); + if (!canFollow) return false; + + if (parentOrder.nextID is not null) + { + // Check after + var nextOrder = GetOrder(parentOrder.nextID); + var canPrecede = (isBuy && (nextOrder.price < order.price)) || (!isBuy && (nextOrder.price > order.price)); + canFollow = canFollow && canPrecede; + } + if (canFollow) + { + // Do insert + order.nextID = parentOrder.nextID; + SetOrder(id, order); + parentOrder.nextID = id; + SetOrder(parentID, parentOrder); + return true; + } + return false; + } + + /// + /// Get the parent order id + /// + /// + /// + /// + /// + private static ByteString GetParentID(byte[] pairKey, bool isBuy, ByteString childID) + { + var book = GetOrderBook(pairKey); + var firstID = isBuy ? book.firstBuyID : book.firstSellID; + if (firstID == childID) return null; + + var currentID = firstID; + while (currentID is not null) + { + var currentOrder = GetOrder(currentID); + if (currentOrder.nextID == childID) return currentID; + currentID = currentOrder.nextID; + } + return null; + } + + /// + /// Remove a canceled limit order from orderbook + /// + /// + /// + /// + /// + /// + private static bool RemoveOrder(byte[] pairKey, OrderBook book, ByteString id, bool isBuy) + { + // Remove from BookMap + var firstID = isBuy ? book.firstBuyID : book.firstSellID; + if (firstID is null) return false; + if (firstID == id) + { + // Delete the first + if (isBuy) book.firstBuyID = GetOrder(firstID).nextID; + else book.firstSellID = GetOrder(firstID).nextID; + SetOrderBook(pairKey, book); + DeleteOrder(firstID); + return true; + } + else + { + // Find the position + return RemoveNotFirst(firstID, id); + } + } + + private static bool RemoveNotFirst(ByteString firstID, ByteString id) + { + var currentID = firstID; + var currentOrder = GetOrder(currentID); + while (currentOrder.nextID is not null) + { + // Check next + if (currentOrder.nextID == id) + { + // Do remove + var newNextID = GetOrder(currentOrder.nextID).nextID; + DeleteOrder(currentOrder.nextID); + currentOrder.nextID = newNextID; + SetOrder(currentID, currentOrder); + return true; + } + currentID = currentOrder.nextID; + currentOrder = GetOrder(currentID); + } + return false; + } + + private static bool RemoveOrderAt(ByteString parentID, ByteString id) + { + var parentOrder = GetOrder(parentID); + if (parentOrder.nextID != id) return false; + + // Do remove + var newNextID = GetOrder(id).nextID; + DeleteOrder(id); + parentOrder.nextID = newNextID; + SetOrder(parentID, parentOrder); + return true; + } + + /// + /// Check if a limit order exists + /// + /// + /// + private static bool OrderExists(ByteString id) + { + StorageMap orderMap = new(Storage.CurrentReadOnlyContext, OrderMapPrefix); + return orderMap.Get(id) is not null; + } + + /// + /// Check if an order book exists + /// + /// + /// + private static bool BookExists(byte[] pairKey) + { + StorageMap bookMap = new(Storage.CurrentReadOnlyContext, BookMapPrefix); + return bookMap.Get(pairKey) is not null; + } + + /// + /// Set an order book as paused + /// + /// + /// + private static void SetPaused(byte[] pairKey) + { + StorageMap pauseMap = new(Storage.CurrentContext, PauseMapPrefix); + pauseMap.Put(pairKey, 1); + } + + /// + /// Remove an order book from paused + /// + /// + /// + private static void RemovePaused(byte[] pairKey) + { + StorageMap pauseMap = new(Storage.CurrentContext, PauseMapPrefix); + pauseMap.Delete(pairKey); + } + + /// + /// Check if an order book is paused + /// + /// + /// + private static bool BookPaused(byte[] pairKey) + { + StorageMap pauseMap = new(Storage.CurrentReadOnlyContext, PauseMapPrefix); + return pauseMap.Get(pairKey) is not null; + } + + /// + /// Get the detail of a limit order + /// + /// + /// + private static LimitOrder GetOrder(ByteString id) + { + StorageMap orderMap = new(Storage.CurrentReadOnlyContext, OrderMapPrefix); + return (LimitOrder)StdLib.Deserialize(orderMap.Get(id)); + } + + /// + /// Update a limit order + /// + /// + /// + private static void SetOrder(ByteString id, LimitOrder order) + { + StorageMap orderMap = new(Storage.CurrentContext, OrderMapPrefix); + orderMap.Put(id, StdLib.Serialize(order)); + } + + /// + /// Delete a limit order + /// + /// + private static void DeleteOrder(ByteString id) + { + StorageMap orderMap = new(Storage.CurrentContext, OrderMapPrefix); + orderMap.Delete(id); + } + + /// + /// Get the detail of an order receipt + /// + /// + /// + /// + private static OrderReceipt GetReceipt(UInt160 maker, ByteString id) + { + StorageMap receiptMap = new(Storage.CurrentReadOnlyContext, ReceiptMapPrefix); + return (OrderReceipt)StdLib.Deserialize(receiptMap.Get(maker + id)); + } + + /// + /// Get all receipts of the maker + /// + /// + /// + private static Iterator ReceiptsOf(UInt160 maker) + { + StorageMap receiptMap = new(Storage.CurrentReadOnlyContext, ReceiptMapPrefix); + return receiptMap.Find(maker, FindOptions.ValuesOnly | FindOptions.DeserializeValues); + } + + [OpCode(OpCode.APPEND)] + private static extern void Append(T[] array, T newItem); + + /// + /// Update an order receipt + /// + /// + /// + /// + private static void SetReceipt(UInt160 maker, ByteString id, OrderReceipt receipt) + { + StorageMap receiptMap = new(Storage.CurrentContext, ReceiptMapPrefix); + receiptMap.Put(maker + id, StdLib.Serialize(receipt)); + } + + /// + /// Delete an order receipt + /// + /// + /// + private static void DeleteReceipt(UInt160 maker, ByteString id) + { + StorageMap receiptMap = new(Storage.CurrentContext, ReceiptMapPrefix); + receiptMap.Delete(maker + id); + } + + /// + /// Get the detail of a book + /// + /// + /// + private static OrderBook GetOrderBook(byte[] pairKey) + { + StorageMap bookMap = new(Storage.CurrentReadOnlyContext, BookMapPrefix); + return (OrderBook)StdLib.Deserialize(bookMap.Get(pairKey)); + } + + /// + /// Update a book + /// + /// + /// + private static void SetOrderBook(byte[] pairKey, OrderBook book) + { + StorageMap bookMap = new(Storage.CurrentContext, BookMapPrefix); + bookMap.Put(pairKey, StdLib.Serialize(book)); + } + + /// + /// Find a random number as order ID + /// + /// + private static ByteString GetUnusedID() + { + StorageContext context = Storage.CurrentContext; + ByteString counter = Storage.Get(context, OrderIDKey); + Storage.Put(context, OrderIDKey, (BigInteger)counter + 1); + ByteString data = Runtime.ExecutingScriptHash; + if (counter is not null) data += counter; + return CryptoLib.Sha256(data); + } + + /// + /// Get the pair contract + /// + /// + /// + /// + public static UInt160 GetExchangePairWithAssert(UInt160 tokenA, UInt160 tokenB) + { + Assert(tokenA.IsValid && tokenB.IsValid, "Invalid A or B Address"); + var pairContract = (byte[])Contract.Call(Factory, "getExchangePair", CallFlags.ReadOnly, new object[] { tokenA, tokenB }); + Assert(pairContract != null && pairContract.Length == 20, "PairContract Not Found", tokenA, tokenB); + return (UInt160)pairContract; + } + + /// + /// Get TokenA and TokenB reserves from paicontract + /// + /// + /// + /// + /// + public static BigInteger[] GetReserves(UInt160 pairContract, UInt160 tokenA, UInt160 tokenB) + { + var reserveData = (ReservesData)Contract.Call(pairContract, "getReserves", CallFlags.ReadOnly, new object[] { }); + return tokenA.ToUInteger() < tokenB.ToUInteger() ? new BigInteger[] { reserveData.Reserve0, reserveData.Reserve1 } : new BigInteger[] { reserveData.Reserve1, reserveData.Reserve0 }; + } + + /// + /// Check if pair contract charge a fundfee + /// + /// + /// + public static bool HasFundAddress(UInt160 pairContract) + { + return (byte[])Contract.Call(pairContract, "getFundAddress", CallFlags.ReadOnly, new object[] { }) != null; + } + + /// + /// Get amountOut from pair + /// + /// + /// + /// + /// + private static void SwapOut(UInt160 pairContract, BigInteger amount0Out, BigInteger amount1Out, UInt160 toAddress) + { + Contract.Call(pairContract, "swap", CallFlags.All, new object[] { amount0Out, amount1Out, toAddress, null }); + } + + /// + /// Transfer NEP-5 tokens + /// + /// + /// + /// + /// + /// + /// + private static void SafeTransfer(UInt160 token, UInt160 from, UInt160 to, BigInteger amount, byte[] data = null) + { + try + { + var result = (bool)Contract.Call(token, "transfer", CallFlags.All, new object[] { from, to, amount, data }); + Assert(result, "Transfer Fail in OrderBook", token); + } + catch (Exception) + { + Assert(false, "Transfer Error in OrderBook", token); + } + } + + private static void RequestTransfer(UInt160 token, UInt160 from, UInt160 to, BigInteger amount, byte[] data = null) + { + try + { + var balanceBefore = GetBalanceOf(token, to); + var result = (bool)Contract.Call(from, "approvedTransfer", CallFlags.All, new object[] { token, to, amount, data }); + var balanceAfter = GetBalanceOf(token, to); + Assert(result, "Transfer Not Approved in OrderBook", token); + Assert(balanceAfter == balanceBefore + amount, "Unexpected Transfer in OrderBook", token); + } + catch (Exception) + { + Assert(false, "Transfer Error in OrderBook", token); + } + } + + private static BigInteger GetBalanceOf(UInt160 token, UInt160 address) + { + return (BigInteger)Contract.Call(token, "balanceOf", CallFlags.ReadOnly, new object[] { address }); + } + + /// + /// Get unique pair key + /// + /// + /// + /// + private static byte[] GetPairKey(UInt160 tokenA, UInt160 tokenB) + { + return (BigInteger)tokenA < (BigInteger)tokenB + ? BookMapPrefix.Concat(tokenA).Concat(tokenB) + : BookMapPrefix.Concat(tokenB).Concat(tokenA); + } + + /// + /// Check if + /// + /// + /// + /// + private static void Assert(bool condition, string message, object data = null) + { + if (!condition) + { + throw new Exception(message); + } + } + + /// + /// Check if + /// + /// + /// + /// + private static void Assert(bool condition, string message, params object[] data) + { + if (!condition) + { + throw new Exception(message); + } + } + } +} diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs new file mode 100644 index 0000000..c21234c --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs @@ -0,0 +1,990 @@ +using Neo; +using Neo.SmartContract.Framework; +using Neo.SmartContract.Framework.Attributes; +using Neo.SmartContract.Framework.Native; +using Neo.SmartContract.Framework.Services; +using System.Numerics; +using System.ComponentModel; + +namespace FlamingoSwapOrderBook +{ + [DisplayName("FlamingoSwapOrderBook")] + [ManifestExtra("Author", "Flamingo Finance")] + [ManifestExtra("Email", "developer@flamingo.finance")] + [ManifestExtra("Description", "This is a Flamingo Contract")] + [ContractPermission("*")] + public partial class FlamingoSwapOrderBookContract : SmartContract + { + /// + /// Deal and add limit order base on input strategy + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 sender, bool isBuy, BigInteger amount, BigInteger price, BigInteger expectBookAmount) + { + // Market order + var leftAmount = expectBookAmount > 0 ? amount - expectBookAmount + DealMarketOrder(tokenA, tokenB, sender, isBuy, price, expectBookAmount) : amount; + + // Swap AMM + var pairKey = GetPairKey(tokenA, tokenB); + var book = GetOrderBook(pairKey); + var pairContract = GetExchangePairWithAssert(tokenA, tokenB); + var hasFundFee = HasFundAddress(pairContract); + + var ammReverse = isBuy + ? GetReserves(pairContract, book.quoteToken, book.baseToken) + : GetReserves(pairContract, book.baseToken, book.quoteToken); + var amountIn = hasFundFee + ? GetAmountInTillPriceWithFundFee(isBuy, price, book.quoteScale, ammReverse[0], ammReverse[1]) + : GetAmountInTillPrice(isBuy, price, book.quoteScale, ammReverse[0], ammReverse[1]); + if (amountIn < 0) amountIn = 0; + var amountOut = GetAmountOut(amountIn, ammReverse[0], ammReverse[1]); + + if (isBuy && leftAmount < amountOut) + { + amountOut = leftAmount; + amountIn = GetAmountIn(amountOut, ammReverse[0], ammReverse[1]); + } + if (!isBuy && leftAmount < amountIn) + { + amountIn = leftAmount; + amountOut = GetAmountOut(amountIn, ammReverse[0], ammReverse[1]); + } + + if (amountOut > 0) + { + SwapAMM(pairContract, sender, isBuy ? book.quoteToken : book.baseToken, isBuy ? book.baseToken : book.quoteToken, amountIn, amountOut); + leftAmount -= isBuy ? amountOut : amountIn; + } + + // Add limit order + if (leftAmount < book.minOrderAmount || leftAmount > book.maxOrderAmount) return null; + var me = Runtime.ExecutingScriptHash; + if (isBuy) SafeTransfer(book.quoteToken, sender, me, leftAmount * price / book.quoteScale); + else SafeTransfer(book.baseToken, sender, me, leftAmount); + + var id = GetUnusedID(); + Assert(InsertOrder(pairKey, book, id, new LimitOrder(){ + maker = sender, + price = price, + amount = leftAmount + }, isBuy), "Add Order Fail"); + + // Add receipt + SetReceipt(sender, id, new OrderReceipt(){ + baseToken = book.baseToken, + quoteToken = book.quoteToken, + id = id, + time = Runtime.Time, + isBuy = isBuy, + totalAmount = leftAmount + }); + onOrderStatusChanged(book.baseToken, book.quoteToken, id, !!isBuy, sender, price, leftAmount); + return id; + } + + /// + /// Deal market order based on input strategy + /// + /// + /// + /// + /// + /// + /// + /// + public static void RouteMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 sender, bool isBuy, BigInteger amount, BigInteger slippage, BigInteger expectBookAmount) + { + // Market order + var pairKey = GetPairKey(tokenA, tokenB); + var book = GetOrderBook(pairKey); + var price = slippage * book.quoteScale / amount; + + var balanceBefore = GetBalanceOf(isBuy ? book.quoteToken : book.quoteToken, sender); + var leftAmount = expectBookAmount > 0 ? amount - expectBookAmount + DealMarketOrder(tokenA, tokenB, sender, isBuy, price, expectBookAmount) : amount; + var balanceAfter = GetBalanceOf(isBuy ? book.quoteToken : book.quoteToken, sender); + + // Swap AMM + var pairContract = GetExchangePairWithAssert(tokenA, tokenB); + var ammReverse = GetReserves(pairContract, book.baseToken, book.quoteToken); + + var amountIn = isBuy ? GetAmountIn(leftAmount, ammReverse[1], ammReverse[0]) : leftAmount; + var amountOut = isBuy ? leftAmount : GetAmountOut(leftAmount, ammReverse[0], ammReverse[1]); + + if (amountOut > 0) SwapAMM(pairContract, sender, isBuy ? book.quoteToken : book.baseToken, isBuy ? book.baseToken : book.quoteToken, amountIn, amountOut); + Assert(isBuy ? amountIn + balanceBefore - balanceAfter <= slippage : amountOut + balanceAfter - balanceBefore >= slippage, "Insufficient Slippage"); + } + + #region DEX like API + /// + /// Register a new book + /// + /// + /// + /// + /// + /// + public static void RegisterOrderBook(UInt160 baseToken, UInt160 quoteToken, uint quoteDecimals, BigInteger minOrderAmount, BigInteger maxOrderAmount) + { + Assert(baseToken.IsAddress() && quoteToken.IsAddress(), "Invalid Address"); + Assert(baseToken != quoteToken, "Invalid Trade Pair"); + Assert(minOrderAmount > 0 && maxOrderAmount > 0 && minOrderAmount <= maxOrderAmount, "Invalid Amount Limit"); + Assert(Verify(), "No Authorization"); + + var pairKey = GetPairKey(baseToken, quoteToken); + var quoteScale = BigInteger.Pow(10, (int)quoteDecimals); + Assert(!BookExists(pairKey), "Book Already Registered"); + SetOrderBook(pairKey, new OrderBook(){ + baseToken = baseToken, + quoteToken = quoteToken, + quoteScale = quoteScale, + minOrderAmount = minOrderAmount, + maxOrderAmount = maxOrderAmount + }); + onBookStatusChanged(baseToken, quoteToken, quoteScale, minOrderAmount, maxOrderAmount, BookPaused(pairKey)); + } + + /// + /// Set the minimum order amount for addLimitOrder + /// + /// + /// + /// + public static void SetMinOrderAmount(UInt160 baseToken, UInt160 quoteToken, BigInteger minOrderAmount) + { + Assert(baseToken.IsAddress() && quoteToken.IsAddress(), "Invalid Address"); + Assert(minOrderAmount > 0, "Invalid Amount Limit"); + Assert(Verify(), "No Authorization"); + + var pairKey = GetPairKey(baseToken, quoteToken); + var book = GetOrderBook(pairKey); + Assert(book.baseToken == baseToken && book.quoteToken == quoteToken, "Invalid Trade Pair"); + Assert(minOrderAmount <= book.maxOrderAmount, "Invalid Amount Limit"); + + book.minOrderAmount = minOrderAmount; + SetOrderBook(pairKey, book); + onBookStatusChanged(book.baseToken, book.quoteToken, book.quoteScale, book.minOrderAmount, book.maxOrderAmount, BookPaused(pairKey)); + } + + /// + /// Set the maximum trade amount for addLimitOrder + /// + /// + /// + /// + public static void SetMaxOrderAmount(UInt160 baseToken, UInt160 quoteToken, BigInteger maxOrderAmount) + { + Assert(baseToken.IsAddress() && quoteToken.IsAddress(), "Invalid Address"); + Assert(maxOrderAmount > 0, "Invalid Amount Limit"); + Assert(Verify(), "No Authorization"); + + var pairKey = GetPairKey(baseToken, quoteToken); + var book = GetOrderBook(pairKey); + Assert(book.baseToken == baseToken && book.quoteToken == quoteToken, "Invalid Trade Pair"); + Assert(maxOrderAmount >= book.minOrderAmount, "Invalid Amount Limit"); + + book.maxOrderAmount = maxOrderAmount; + SetOrderBook(pairKey, book); + onBookStatusChanged(book.baseToken, book.quoteToken, book.quoteScale, book.minOrderAmount, book.maxOrderAmount, BookPaused(pairKey)); + } + + /// + /// Pause an existing order book + /// + /// + /// + public static void PauseOrderBook(UInt160 baseToken, UInt160 quoteToken) + { + Assert(baseToken.IsAddress() && quoteToken.IsAddress(), "Invalid Address"); + Assert(Verify(), "No Authorization"); + + var pairKey = GetPairKey(baseToken, quoteToken); + var book = GetOrderBook(pairKey); + Assert(book.baseToken == baseToken && book.quoteToken == quoteToken, "Invalid Trade Pair"); + Assert(!BookPaused(pairKey), "Book Already Paused"); + + SetPaused(pairKey); + onBookStatusChanged(book.baseToken, book.quoteToken, book.quoteScale, book.minOrderAmount, book.maxOrderAmount, BookPaused(pairKey)); + } + + /// + /// Resume a paused order book + /// + /// + /// + public static void ResumeOrderBook(UInt160 baseToken, UInt160 quoteToken) + { + Assert(baseToken.IsAddress() && quoteToken.IsAddress(), "Invalid Address"); + Assert(Verify(), "No Authorization"); + + var pairKey = GetPairKey(baseToken, quoteToken); + var book = GetOrderBook(pairKey); + Assert(book.baseToken == baseToken && book.quoteToken == quoteToken, "Invalid Trade Pair"); + Assert(BookPaused(pairKey), "Book Not Paused"); + + RemovePaused(pairKey); + onBookStatusChanged(book.baseToken, book.quoteToken, book.quoteScale, book.minOrderAmount, book.maxOrderAmount, BookPaused(pairKey)); + } + + /// + /// Add a new order into orderbook but try deal it first + /// + /// + /// + /// + /// + /// + /// + /// Null or a new order id + public static ByteString AddLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 maker, bool isBuy, BigInteger price, BigInteger amount) + { + // Check parameters + Assert(price > 0 && amount > 0, "Invalid Parameters"); + var pairKey = GetPairKey(tokenA, tokenB); + Assert(!BookPaused(pairKey), "Book is Paused"); + Assert(Runtime.CheckWitness(maker), "No Authorization"); + Assert(ContractManagement.GetContract(maker) == null, "Forbidden"); + + // Deposit token + var book = GetOrderBook(pairKey); + if (amount < book.minOrderAmount || amount > book.maxOrderAmount) return null; + var me = Runtime.ExecutingScriptHash; + if (isBuy) SafeTransfer(book.quoteToken, maker, me, amount * price / book.quoteScale); + else SafeTransfer(book.baseToken, maker, me, amount); + + // Do add + var id = GetUnusedID(); + Assert(InsertOrder(pairKey, book, id, new LimitOrder(){ + maker = maker, + price = price, + amount = amount + }, isBuy), "Add Order Fail"); + + // Add receipt + SetReceipt(maker, id, new OrderReceipt(){ + baseToken = book.baseToken, + quoteToken = book.quoteToken, + id = id, + time = Runtime.Time, + isBuy = isBuy, + totalAmount = amount + }); + onOrderStatusChanged(book.baseToken, book.quoteToken, id, !!isBuy, maker, price, amount); + return id; + } + + /// + /// Add a new order into orderbook with an expected parent order id + /// + /// + /// + /// + /// + /// Null or a new order id + public static ByteString AddLimitOrderAt(ByteString parentID, UInt160 maker, BigInteger price, BigInteger amount) + { + // Check parameters + Assert(price > 0 && amount > 0, "Invalid Parameters"); + var receipt = GetReceipt(GetOrder(parentID).maker, parentID); + var pairKey = GetPairKey(receipt.baseToken, receipt.quoteToken); + Assert(!BookPaused(pairKey), "Book is Paused"); + Assert(Runtime.CheckWitness(maker), "No Authorization"); + Assert(ContractManagement.GetContract(maker) == null, "Forbidden"); + + // Check amount + var book = GetOrderBook(pairKey); + Assert(amount >= book.minOrderAmount && amount <= book.maxOrderAmount, "Invalid Limit Order Amount"); + + // Deposit token + var me = Runtime.ExecutingScriptHash; + if (receipt.isBuy) SafeTransfer(receipt.quoteToken, maker, me, amount * price / book.quoteScale); + else SafeTransfer(receipt.baseToken, maker, me, amount); + + // Insert new order + var id = GetUnusedID(); + Assert(InsertOrderAt(parentID, id, new LimitOrder(){ + maker = maker, + price = price, + amount = amount + }, receipt.isBuy), "Add Order Fail"); + + // Add receipt + SetReceipt(maker, id, new OrderReceipt(){ + baseToken = receipt.baseToken, + quoteToken = receipt.quoteToken, + id = id, + time = Runtime.Time, + isBuy = receipt.isBuy, + totalAmount = amount + }); + onOrderStatusChanged(receipt.baseToken, receipt.quoteToken, id, !!receipt.isBuy, maker, price, amount); + return id; + } + + /// + /// Cancel a limit order with its id + /// + /// + /// + /// + /// + public static void CancelOrder(UInt160 tokenA, UInt160 tokenB, bool isBuy, ByteString id) + { + // Check if exist + var pairKey = GetPairKey(tokenA, tokenB); + Assert(OrderExists(id), "Order Not Exists"); + var order = GetOrder(id); + Assert(Runtime.CheckWitness(order.maker), "No Authorization"); + + // Do remove + var book = GetOrderBook(pairKey); + Assert(RemoveOrder(pairKey, book, id, isBuy), "Remove Order Fail"); + + // Remove receipt + DeleteReceipt(order.maker, id); + onOrderStatusChanged(book.baseToken, book.quoteToken, id, !!isBuy, order.maker, order.price, 0); + + // Withdraw token + var me = Runtime.ExecutingScriptHash; + if (isBuy) SafeTransfer(book.quoteToken, me, order.maker, order.amount * order.price / book.quoteScale); + else SafeTransfer(book.baseToken, me, order.maker, order.amount); + } + + /// + /// Cancel a limit order with its id and parent order id + /// + /// + /// + /// + public static bool CancelOrderAt(ByteString parentID, ByteString id) + { + if (!OrderExists(id)) return false; + Assert(OrderExists(parentID), "Parent Order Not Exists"); + var order = GetOrder(id); + Assert(Runtime.CheckWitness(order.maker), "No Authorization"); + + // Do remove + var receipt = GetReceipt(order.maker, id); + var pairKey = GetPairKey(receipt.baseToken, receipt.quoteToken); + var book = GetOrderBook(pairKey); + + Assert(RemoveOrderAt(parentID, id), "Remove Order Fail"); + + // Remove receipt + DeleteReceipt(order.maker, id); + onOrderStatusChanged(receipt.baseToken, receipt.quoteToken, id, !!receipt.isBuy, order.maker, order.price, 0); + + // Withdraw token + var me = Runtime.ExecutingScriptHash; + if (receipt.isBuy) SafeTransfer(receipt.quoteToken, me, order.maker, order.amount * order.price / book.quoteScale); + else SafeTransfer(receipt.baseToken, me, order.maker, order.amount); + return true; + } + + /// + /// Try get the parent order id of an existing order + /// + /// + /// + /// + /// + /// + [Safe] + public static ByteString GetParentOrderID(UInt160 tokenA, UInt160 tokenB, bool isBuy, ByteString id) + { + var pairKey = GetPairKey(tokenA, tokenB); + return GetParentID(pairKey, isBuy, id); + } + + /// + /// Get first N limit orders and their details + /// + /// + /// + /// + /// + /// + /// + [Safe] + public static OrderReceipt[] GetNOrders(UInt160 tokenA, UInt160 tokenB, bool isBuy, uint pos, uint n) + { + var results = new OrderReceipt[n]; + + var pairKey = GetPairKey(tokenA, tokenB); + var book = GetOrderBook(pairKey); + var firstID = isBuy ? book.firstBuyID : book.firstSellID; + if (firstID is null) return results; + + var currentOrderID = firstID; + var currentOrder = GetOrder(currentOrderID); + for (int i = 0; i < pos + n; i++) + { + if (i >= pos) + { + var receipt = GetReceipt(currentOrder.maker, currentOrderID); + receipt.maker = currentOrder.maker; + receipt.price = currentOrder.price; + receipt.leftAmount = currentOrder.amount; + results[i - pos] = receipt; + + if (currentOrder.nextID is null) break; + } + currentOrderID = currentOrder.nextID; + currentOrder = GetOrder(currentOrder.nextID); + } + + return results; + } + + /// + /// Get all orders and their details of maker + /// + /// + /// + /// + /// + [Safe] + public static OrderReceipt[] GetOrdersOf(UInt160 maker, uint pos, uint n) + { + var results = new OrderReceipt[n]; + var iterator = ReceiptsOf(maker); + // Makeup details + for (int i = 0; i < pos + n; i++) + { + if (iterator.Next() && i >= pos) + { + results[i - pos] = (OrderReceipt)iterator.Value; + var order = GetOrder(results[i - pos].id); + results[i - pos].maker = order.maker; + results[i - pos].price = order.price; + results[i - pos].leftAmount = order.amount; + } + } + return results; + } + + /// + /// Get the total reverse of tradable orders with an expected price + /// + /// + /// + /// + /// + /// + [Safe] + public static BigInteger GetTotalTradable(UInt160 tokenA, UInt160 tokenB, bool isBuy, BigInteger price) + { + var pairKey = GetPairKey(tokenA, tokenB); + if (BookPaused(pairKey)) return 0; + return GetTotalTradableInternal(pairKey, isBuy, price); + } + + private static BigInteger GetTotalTradableInternal(byte[] pairKey, bool isBuy, BigInteger price) + { + var totalTradable = BigInteger.Zero; + var book = GetOrderBook(pairKey); + var currentID = isBuy ? book.firstSellID : book.firstBuyID; + if (currentID is null) return totalTradable; + while (currentID is not null) + { + var currentOrder = GetOrder(currentID); + if ((isBuy && currentOrder.price > price) || (!isBuy && currentOrder.price < price)) break; + totalTradable += currentOrder.amount; + currentID = currentOrder.nextID; + } + return totalTradable; + } + + /// + /// Try to match without real payment + /// + /// + /// + /// + /// + /// + /// + [Safe] + public static BigInteger[] MatchOrder(UInt160 tokenA, UInt160 tokenB, bool isBuy, BigInteger price, BigInteger amount) + { + // Check if exist + var pairKey = GetPairKey(tokenA, tokenB); + if (BookPaused(pairKey)) return new BigInteger[] { amount, 0 }; + return MatchOrderInternal(pairKey, isBuy, price, amount); + } + + private static BigInteger[] MatchOrderInternal(byte[] pairKey, bool isBuy, BigInteger price, BigInteger amount) + { + var totalPayment = BigInteger.Zero; + var book = GetOrderBook(pairKey); + var currentID = isBuy ? book.firstSellID : book.firstBuyID; + if (currentID is null) return new BigInteger[] { amount, 0 }; + var currentOrder = GetOrder(currentID); + + while (amount > 0) + { + // Check price + if ((isBuy && currentOrder.price > price) || (!isBuy && currentOrder.price < price)) break; + + if (currentOrder.amount <= amount) + { + totalPayment += currentOrder.amount * currentOrder.price / book.quoteScale; + amount -= currentOrder.amount; + } + else + { + totalPayment += amount * currentOrder.price / book.quoteScale; + amount = 0; + } + + if (currentOrder.nextID is null) break; + currentOrder = GetOrder(currentOrder.nextID); + } + + // A least payment for buyer + if (isBuy && totalPayment == 0) totalPayment += 1; + return new BigInteger[] { amount, totalPayment }; + } + + /// + /// Try to match without real payment + /// + /// + /// + /// + /// + /// + /// Left amount and tradable base + [Safe] + public static BigInteger[] MatchQuote(UInt160 tokenA, UInt160 tokenB, bool isBuy, BigInteger price, BigInteger quoteAmount) + { + var pairKey = GetPairKey(tokenA, tokenB); + if (BookPaused(pairKey)) return new BigInteger[] { quoteAmount, 0 }; + return MatchQuoteInternal(pairKey, isBuy, price, quoteAmount); + } + + private static BigInteger[] MatchQuoteInternal(byte[] pairKey, bool isBuy, BigInteger price, BigInteger quoteAmount) + { + var totalTradable = BigInteger.Zero; + var book = GetOrderBook(pairKey); + var currentID = isBuy ? book.firstSellID : book.firstBuyID; + if (currentID is null) return new BigInteger[] { quoteAmount, 0 }; + var currentOrder = GetOrder(currentID); + + while (quoteAmount > 0) + { + // Check price + if ((isBuy && currentOrder.price > price) || (!isBuy && currentOrder.price < price)) break; + var payment = currentOrder.amount * currentOrder.price / book.quoteScale; + if (payment <= quoteAmount) + { + totalTradable += currentOrder.amount; + quoteAmount -= payment; + } + else + { + // For buyer, real payment <= expected + if (isBuy) totalTradable += quoteAmount * book.quoteScale / currentOrder.price; + // For seller, real payment >= expected + else totalTradable += (quoteAmount * book.quoteScale + currentOrder.price - 1) / currentOrder.price; + quoteAmount = 0; + } + + if (currentOrder.nextID is null) break; + currentOrder = GetOrder(currentOrder.nextID); + } + return new BigInteger[] { quoteAmount, totalTradable }; + } + + /// + /// Try to make a market deal with orderbook + /// + /// + /// + /// + /// + /// + /// + /// Left amount + public static BigInteger DealMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 taker, bool isBuy, BigInteger price, BigInteger amount) + { + // Check parameters + Assert(price > 0 && amount > 0, "Invalid Parameters"); + var pairKey = GetPairKey(tokenA, tokenB); + Assert(!BookPaused(pairKey), "Book is Paused"); + Assert(Runtime.CheckWitness(taker), "No Authorization"); + Assert(ContractManagement.GetContract(taker) == null, "Forbidden"); + + return DealMarketOrderInternal(pairKey, taker, isBuy, price, amount, false); + } + + public static BigInteger DealMarketOrder(UInt160 tokenA, UInt160 tokenB, bool isBuy, BigInteger price, BigInteger amount) + { + // Check parameters + Assert(price > 0 && amount > 0, "Invalid Parameters"); + var pairKey = GetPairKey(tokenA, tokenB); + Assert(!BookPaused(pairKey), "Book is Paused"); + var caller = Runtime.CallingScriptHash; + Assert(ContractManagement.GetContract(caller) != null, "Forbidden"); + + return DealMarketOrderInternal(pairKey, caller, isBuy, price, amount, true); + } + + private static BigInteger DealMarketOrderInternal(byte[] pairKey, UInt160 taker, bool isBuy, BigInteger price, BigInteger leftAmount, bool shouldRequest) + { + // Check if can deal + var book = GetOrderBook(pairKey); + var firstID = isBuy ? book.firstSellID : book.firstBuyID; + if (firstID is null) return leftAmount; + var firstOrder = GetOrder(firstID); + var canDeal = (isBuy && firstOrder.price <= price) || (!isBuy && firstOrder.price >= price); + if (!canDeal) return leftAmount; + + var me = Runtime.ExecutingScriptHash; + var fundAddress = GetFundAddress(); + + var quoteFee = BigInteger.Zero; + var baseFee = BigInteger.Zero; + + var takerReceive = BigInteger.Zero; + var takerPayment = BigInteger.Zero; + var makerReceive = new Map(); + + var currentID = firstID; + while (currentID is not null) + { + var currentOrder = GetOrder(currentID); + if ((isBuy && currentOrder.price > price) || (!isBuy && currentOrder.price < price)) break; + + BigInteger quoteAmount; + BigInteger baseAmount; + BigInteger quotePayment; + BigInteger basePayment; + + if (currentOrder.amount <= leftAmount) + { + // Full-fill + quoteAmount = currentOrder.amount * currentOrder.price / book.quoteScale; + baseAmount = currentOrder.amount; + + // Remove full-fill order + DeleteOrder(currentID); + DeleteReceipt(currentOrder.maker, currentID); + firstID = currentOrder.nextID; + + onOrderStatusChanged(book.baseToken, book.quoteToken, currentID, !isBuy, currentOrder.maker, currentOrder.price, 0); + leftAmount -= currentOrder.amount; + } + else + { + // Part-fill + quoteAmount = leftAmount * currentOrder.price / book.quoteScale; + baseAmount = leftAmount; + + // Update order + currentOrder.amount -= leftAmount; + SetOrder(currentID, currentOrder); + + onOrderStatusChanged(book.baseToken, book.quoteToken, currentID, !isBuy, currentOrder.maker, currentOrder.price, currentOrder.amount); + leftAmount = 0; + } + + // Record payment + quotePayment = quoteAmount * 997 / 1000; + basePayment = baseAmount * 997 / 1000; + quoteFee += quoteAmount - quotePayment; + baseFee += baseAmount - basePayment; + + if (isBuy) + { + takerPayment += quoteAmount; + takerReceive += basePayment; + if (makerReceive.HasKey(currentOrder.maker)) makerReceive[currentOrder.maker] += quotePayment; + else makerReceive[currentOrder.maker] = quotePayment; + } + else + { + takerPayment += baseAmount; + takerReceive += quotePayment; + if (makerReceive.HasKey(taker)) makerReceive[taker] += basePayment; + else makerReceive[taker] = basePayment; + } + + // Check if still tradable + if (leftAmount == 0) break; + currentID = currentOrder.nextID; + } + + // Update book if necessary + if (isBuy && book.firstSellID != firstID) + { + book.firstSellID = firstID; + SetOrderBook(pairKey, book); + } + if (!isBuy && book.firstBuyID != firstID) + { + book.firstBuyID = firstID; + SetOrderBook(pairKey, book); + } + + // Do transfer + if (takerPayment == 0) takerPayment += 1; + if (isBuy) + { + if (shouldRequest) RequestTransfer(book.quoteToken, taker, me, takerPayment); + else SafeTransfer(book.quoteToken, taker, me, takerPayment); + SafeTransfer(book.baseToken, me, taker, takerReceive); + foreach (var toAddress in makerReceive.Keys) SafeTransfer(book.quoteToken, me, toAddress, makerReceive[toAddress]); + } + else + { + if (shouldRequest) RequestTransfer(book.baseToken, taker, me, takerPayment); + else SafeTransfer(book.baseToken, taker, me, takerPayment); + SafeTransfer(book.quoteToken, me, taker, takerReceive); + foreach (var toAddress in makerReceive.Keys) SafeTransfer(book.baseToken, me, toAddress, makerReceive[toAddress]); + } + + + if (fundAddress is not null) + { + SafeTransfer(book.quoteToken, me, fundAddress, quoteFee); + SafeTransfer(book.baseToken, me, fundAddress, baseFee); + } + return leftAmount; + } + + /// + /// Deal a whole limit order with it id and parent id + /// + /// + /// + /// + public static void DealMarketOrderAt(UInt160 taker, ByteString parentID, ByteString id) + { + Assert(Runtime.CheckWitness(taker), "No Authorization"); + + // Do deal + var order = GetOrder(id); + var me = Runtime.ExecutingScriptHash; + var receipt = GetReceipt(order.maker, id); + var pairKey = GetPairKey(receipt.baseToken, receipt.quoteToken); + Assert(!BookPaused(pairKey), "Book is Paused"); + var quoteScale = GetOrderBook(pairKey).quoteScale; + var fundAddress = GetFundAddress(); + + var baseAmount = order.amount; + var quoteAmount = order.amount * order.price / quoteScale; + var basePayment = baseAmount * 997 / 1000; + var quotePayment = quoteAmount * 997 / 1000; + var baseFee = baseAmount - basePayment; + var quoteFee = quoteAmount - quotePayment; + + if (receipt.isBuy) SafeTransfer(receipt.baseToken, taker, me, baseAmount); + else SafeTransfer(receipt.quoteToken, taker, me, quoteAmount); + + // Remove order and receipt + Assert(RemoveOrderAt(parentID, id), "Remove Order Fail"); + DeleteReceipt(order.maker, id); + onOrderStatusChanged(receipt.baseToken, receipt.quoteToken, id, !!receipt.isBuy, order.maker, order.price, 0); + + // Transfer + if (receipt.isBuy) + { + SafeTransfer(receipt.baseToken, me, order.maker, basePayment); + SafeTransfer(receipt.quoteToken, me, taker, quotePayment); + } + else + { + SafeTransfer(receipt.baseToken, me, taker, basePayment); + SafeTransfer(receipt.quoteToken, me, order.maker, quotePayment); + } + + if (fundAddress is not null) + { + SafeTransfer(receipt.baseToken, me, fundAddress, baseFee); + SafeTransfer(receipt.quoteToken, me, fundAddress, quoteFee); + } + } + + /// + /// Price reporter + /// + /// + /// + /// + /// + [Safe] + public static BigInteger GetMarketPrice(UInt160 tokenA, UInt160 tokenB, bool isBuy) + { + var pairKey = GetPairKey(tokenA, tokenB); + var book = GetOrderBook(pairKey); + var firstID = isBuy ? book.firstSellID : book.firstBuyID; + if (firstID is null) return 0; + return GetOrder(firstID).price; + } + + [Safe] + public static OrderBook GetBookInfo(UInt160 tokenA, UInt160 tokenB) + { + var pairKey = GetPairKey(tokenA, tokenB); + return GetOrderBook(pairKey); + } + #endregion + + #region AMM like API + /// + /// Calculate amountOut with amountIn + /// + /// + /// + /// + /// + /// Unsatisfied amountIn and amountOut + [Safe] + public static BigInteger[] GetAmountOut(UInt160 tokenFrom, UInt160 tokenTo, BigInteger price, BigInteger amountIn) + { + // Check if exist + var pairKey = GetPairKey(tokenFrom, tokenTo); + if (BookPaused(pairKey)) return new BigInteger[] { amountIn, 0 }; + var book = GetOrderBook(pairKey); + var isBuy = tokenFrom == book.quoteToken; + if (isBuy) + { + var result = MatchQuoteInternal(pairKey, isBuy, price, amountIn); + return new BigInteger[]{ result[0], result[1] * 997 / 1000 }; // 0.3% fee + } + else + { + var result = MatchOrderInternal(pairKey, isBuy, price, amountIn); + return new BigInteger[]{ result[0], result[1] * 997 / 1000 }; // 0.3% fee + } + } + + /// + /// Calculate amountOut with amountIn + /// + /// + /// + /// + /// + /// Unsatisfied amountOut and amountIn + [Safe] + public static BigInteger[] GetAmountIn(UInt160 tokenFrom, UInt160 tokenTo, BigInteger price, BigInteger amountOut) + { + // Check if exist + var pairKey = GetPairKey(tokenFrom, tokenTo); + if (BookPaused(pairKey)) return new BigInteger[] { amountOut, 0 }; + var book = GetOrderBook(pairKey); + var isBuy = tokenFrom == book.quoteToken; + if (isBuy) + { + var result = MatchOrderInternal(pairKey, isBuy, price, (amountOut * 1000 + 996) / 997); // 0.3% fee + return new BigInteger[]{ result[0] * 997 / 1000, result[1] }; + } + else + { + var result = MatchQuoteInternal(pairKey, isBuy, price, (amountOut * 1000 + 996) / 997); // 0.3% fee + return new BigInteger[]{ (result[0] * 997 + 999) / 1000, result[1] }; + } + } + #endregion + + /// + /// Accept NEP-17 token + /// SwapTokenInForTokenOut + /// + /// + /// + /// + public static void OnNEP17Payment(UInt160 from, BigInteger amount, object data) + { + + } + + /// + /// Calculate amountIn to reach AMM price + /// + /// + /// + /// + /// + /// + private static BigInteger GetAmountInTillPrice(bool isBuy, BigInteger price, BigInteger quoteScale, BigInteger reserveIn, BigInteger reserveOut) + { + Assert(price > 0 && quoteScale > 0 && reserveIn > 0 && reserveOut > 0, "Parameter Invalid"); + var amountIn = BigInteger.Pow(reserveIn, 2) * 9000000; + if (isBuy) amountIn += reserveIn * reserveOut * price * 3988000000000 / quoteScale; + else amountIn += reserveIn * reserveOut * quoteScale * 3988000000000 / price; + return (amountIn.Sqrt() - reserveIn * 1997000) / 1994000; + } + + private static BigInteger GetAmountInTillPriceWithFundFee(bool isBuy, BigInteger price, BigInteger quoteScale, BigInteger reserveIn, BigInteger reserveOut) + { + Assert(price > 0 && quoteScale > 0 && reserveIn > 0 && reserveOut > 0, "Parameter Invalid"); + var amountIn = BigInteger.Pow(reserveIn, 2) * 6250000; + if (isBuy) amountIn += reserveIn * reserveOut * price * 3986006000000 / quoteScale; + else amountIn += reserveIn * reserveOut * quoteScale * 3986006000000 / price; + return (amountIn.Sqrt() - reserveIn * 1996500) / 1993003; + } + + /// + /// AMM GetAmountOut + /// + /// + /// + /// + /// + private static BigInteger GetAmountOut(BigInteger amountIn, BigInteger reserveIn, BigInteger reserveOut) + { + Assert(amountIn >= 0 && reserveIn > 0 && reserveOut > 0, "AmountIn Must >= 0"); + var amountInWithFee = amountIn * 997; + var numerator = amountInWithFee * reserveOut; + var denominator = reserveIn * 1000 + amountInWithFee; + var amountOut = numerator / denominator; + return amountOut; + } + + /// + /// AMM GetAmountIn + /// + /// + /// + /// + /// + private static BigInteger GetAmountIn(BigInteger amountOut, BigInteger reserveIn, BigInteger reserveOut) + { + Assert(amountOut >= 0 && reserveIn > 0 && reserveOut > 0, "AmountOut Must >= 0"); + var numerator = reserveIn * amountOut * 1000; + var denominator = (reserveOut - amountOut) * 997; + var amountIn = (numerator / denominator) + 1; + return amountIn; + } + + /// + /// Swap as a router + /// + /// + /// + /// + /// + /// + /// + private static void SwapAMM(UInt160 pairContract, UInt160 sender, UInt160 tokenIn, UInt160 tokenOut, BigInteger amountIn, BigInteger amountOut) + { + SafeTransfer(tokenIn, sender, pairContract, amountIn); + + BigInteger amount0Out = 0; + BigInteger amount1Out = 0; + if (tokenIn.ToUInteger() < tokenOut.ToUInteger()) amount1Out = amountOut; + else amount0Out = amountOut; + + SwapOut(pairContract, amount0Out, amount1Out, sender); + } + } +} diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/LimitOrder.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/LimitOrder.cs new file mode 100644 index 0000000..3c2aa46 --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/LimitOrder.cs @@ -0,0 +1,14 @@ +using Neo; +using Neo.SmartContract.Framework; +using System.Numerics; + +namespace FlamingoSwapOrderBook +{ + public struct LimitOrder + { + public UInt160 maker; + public BigInteger price; + public BigInteger amount; + public ByteString nextID; + } +} \ No newline at end of file diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/OrderBook.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/OrderBook.cs new file mode 100644 index 0000000..5c8f02c --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/OrderBook.cs @@ -0,0 +1,18 @@ +using Neo; +using Neo.SmartContract.Framework; +using System.Numerics; + +namespace FlamingoSwapOrderBook +{ + public struct OrderBook + { + public UInt160 baseToken; + public UInt160 quoteToken; + public BigInteger quoteScale; + public BigInteger minOrderAmount; + public BigInteger maxOrderAmount; + + public ByteString firstBuyID; + public ByteString firstSellID; + } +} \ No newline at end of file diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/OrderReceipt.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/OrderReceipt.cs new file mode 100644 index 0000000..90770a5 --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/OrderReceipt.cs @@ -0,0 +1,19 @@ +using Neo; +using Neo.SmartContract.Framework; +using System.Numerics; + +namespace FlamingoSwapOrderBook +{ + public struct OrderReceipt + { + public UInt160 baseToken; + public UInt160 quoteToken; + public ByteString id; + public ulong time; + public bool isBuy; + public UInt160 maker; + public BigInteger price; + public BigInteger totalAmount; + public BigInteger leftAmount; + } +} \ No newline at end of file diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/ReservesData.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/ReservesData.cs new file mode 100644 index 0000000..4a89fbf --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/ReservesData.cs @@ -0,0 +1,10 @@ +using System.Numerics; + +namespace FlamingoSwapOrderBook +{ + public struct ReservesData + { + public BigInteger Reserve0; + public BigInteger Reserve1; + } +} diff --git a/Swap/flamingo-contract-swap/flamingo-contract-swap.sln b/Swap/flamingo-contract-swap/flamingo-contract-swap.sln index 16c7070..cc927ca 100644 --- a/Swap/flamingo-contract-swap/flamingo-contract-swap.sln +++ b/Swap/flamingo-contract-swap/flamingo-contract-swap.sln @@ -15,7 +15,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlashLoanTemple", "FlashLoa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlamingoSwapFactory", "FlamingoSwapFactory\FlamingoSwapFactory.csproj", "{7F4FDB74-D11E-45A7-B6A0-C85D4528748A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProxyTemplate", "ProxyTemplate\ProxyTemplate.csproj", "{71582763-E444-4646-BCCD-828BE7859047}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProxyTemplate", "ProxyTemplate\ProxyTemplate.csproj", "{71582763-E444-4646-BCCD-828BE7859047}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlamingoSwapOrderBook", "FlamingoSwapOrderBook\FlamingoSwapOrderBook.csproj", "{99811C5F-FF6B-465F-A341-E60B13BBCD1B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -51,6 +53,10 @@ Global {71582763-E444-4646-BCCD-828BE7859047}.Debug|Any CPU.Build.0 = Debug|Any CPU {71582763-E444-4646-BCCD-828BE7859047}.Release|Any CPU.ActiveCfg = Release|Any CPU {71582763-E444-4646-BCCD-828BE7859047}.Release|Any CPU.Build.0 = Release|Any CPU + {99811C5F-FF6B-465F-A341-E60B13BBCD1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99811C5F-FF6B-465F-A341-E60B13BBCD1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99811C5F-FF6B-465F-A341-E60B13BBCD1B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99811C5F-FF6B-465F-A341-E60B13BBCD1B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 9eb3589416113d3798c4ffae961855206252e7d9 Mon Sep 17 00:00:00 2001 From: txhsl <799498265@qq.com> Date: Mon, 29 Aug 2022 15:50:01 +0800 Subject: [PATCH 02/16] bugfix and assertion --- .../FlamingoSwapOrderBookContract.Helper.cs | 9 +- .../FlamingoSwapOrderBookContract.cs | 87 +++++++++++++------ 2 files changed, 67 insertions(+), 29 deletions(-) diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Helper.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Helper.cs index c9de1a8..8cec70d 100644 --- a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Helper.cs +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Helper.cs @@ -254,7 +254,8 @@ private static bool BookPaused(byte[] pairKey) private static LimitOrder GetOrder(ByteString id) { StorageMap orderMap = new(Storage.CurrentReadOnlyContext, OrderMapPrefix); - return (LimitOrder)StdLib.Deserialize(orderMap.Get(id)); + var order = orderMap.Get(id); + return order is null ? new LimitOrder() : (LimitOrder)StdLib.Deserialize(order); } /// @@ -287,7 +288,8 @@ private static void DeleteOrder(ByteString id) private static OrderReceipt GetReceipt(UInt160 maker, ByteString id) { StorageMap receiptMap = new(Storage.CurrentReadOnlyContext, ReceiptMapPrefix); - return (OrderReceipt)StdLib.Deserialize(receiptMap.Get(maker + id)); + var receipt = receiptMap.Get(maker + id); + return receipt is null ? new OrderReceipt() : (OrderReceipt)StdLib.Deserialize(receipt); } /// @@ -335,7 +337,8 @@ private static void DeleteReceipt(UInt160 maker, ByteString id) private static OrderBook GetOrderBook(byte[] pairKey) { StorageMap bookMap = new(Storage.CurrentReadOnlyContext, BookMapPrefix); - return (OrderBook)StdLib.Deserialize(bookMap.Get(pairKey)); + var book = bookMap.Get(pairKey); + return book is null ? new OrderBook() : (OrderBook)StdLib.Deserialize(book); } /// diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs index c21234c..4405317 100644 --- a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs @@ -28,12 +28,19 @@ public partial class FlamingoSwapOrderBookContract : SmartContract /// public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 sender, bool isBuy, BigInteger amount, BigInteger price, BigInteger expectBookAmount) { + Assert(amount > 0 && price > 0 && expectBookAmount >= 0 && amount >= expectBookAmount, "Invalid Parameters"); + var pairKey = GetPairKey(tokenA, tokenB); + Assert(!BookPaused(pairKey), "Book is Paused"); + Assert(Runtime.CheckWitness(sender), "No Authorization"); + Assert(ContractManagement.GetContract(sender) == null, "Forbidden"); + // Market order - var leftAmount = expectBookAmount > 0 ? amount - expectBookAmount + DealMarketOrder(tokenA, tokenB, sender, isBuy, price, expectBookAmount) : amount; + var leftAmount = expectBookAmount > 0 ? amount - expectBookAmount + DealMarketOrderInternal(pairKey, sender, isBuy, price, expectBookAmount, false) : amount; + if (leftAmount == 0) return null; // Swap AMM - var pairKey = GetPairKey(tokenA, tokenB); var book = GetOrderBook(pairKey); + Assert(book.baseToken.IsAddress() && book.quoteToken.IsAddress(), "Invalid Trade Pair"); var pairContract = GetExchangePairWithAssert(tokenA, tokenB); var hasFundFee = HasFundAddress(pairContract); @@ -83,7 +90,7 @@ public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 id = id, time = Runtime.Time, isBuy = isBuy, - totalAmount = leftAmount + totalAmount = amount }); onOrderStatusChanged(book.baseToken, book.quoteToken, id, !!isBuy, sender, price, leftAmount); return id; @@ -99,16 +106,28 @@ public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 /// /// /// - public static void RouteMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 sender, bool isBuy, BigInteger amount, BigInteger slippage, BigInteger expectBookAmount) + /// + public static bool RouteMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 sender, bool isBuy, BigInteger amount, BigInteger slippage, BigInteger expectBookAmount) { - // Market order + Assert(amount > 0 && slippage > 0 && expectBookAmount >= 0 && amount >= expectBookAmount, "Invalid Parameters"); var pairKey = GetPairKey(tokenA, tokenB); + Assert(!BookPaused(pairKey), "Book is Paused"); + Assert(Runtime.CheckWitness(sender), "No Authorization"); + Assert(ContractManagement.GetContract(sender) == null, "Forbidden"); + + // Market order var book = GetOrderBook(pairKey); + Assert(book.baseToken.IsAddress() && book.quoteToken.IsAddress(), "Invalid Trade Pair"); var price = slippage * book.quoteScale / amount; - var balanceBefore = GetBalanceOf(isBuy ? book.quoteToken : book.quoteToken, sender); - var leftAmount = expectBookAmount > 0 ? amount - expectBookAmount + DealMarketOrder(tokenA, tokenB, sender, isBuy, price, expectBookAmount) : amount; - var balanceAfter = GetBalanceOf(isBuy ? book.quoteToken : book.quoteToken, sender); + var balanceBefore = GetBalanceOf(book.quoteToken, sender); + var leftAmount = expectBookAmount > 0 ? amount - expectBookAmount + DealMarketOrderInternal(pairKey, sender, isBuy, price, expectBookAmount, false) : amount; + var balanceAfter = GetBalanceOf(book.quoteToken, sender); + if (leftAmount == 0) + { + Assert(isBuy ? balanceBefore - balanceAfter <= slippage : balanceAfter - balanceBefore >= slippage, "Insufficient Slippage"); + return true; + } // Swap AMM var pairContract = GetExchangePairWithAssert(tokenA, tokenB); @@ -119,6 +138,7 @@ public static void RouteMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 send if (amountOut > 0) SwapAMM(pairContract, sender, isBuy ? book.quoteToken : book.baseToken, isBuy ? book.baseToken : book.quoteToken, amountIn, amountOut); Assert(isBuy ? amountIn + balanceBefore - balanceAfter <= slippage : amountOut + balanceAfter - balanceBefore >= slippage, "Insufficient Slippage"); + return true; } #region DEX like API @@ -130,7 +150,8 @@ public static void RouteMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 send /// /// /// - public static void RegisterOrderBook(UInt160 baseToken, UInt160 quoteToken, uint quoteDecimals, BigInteger minOrderAmount, BigInteger maxOrderAmount) + /// + public static bool RegisterOrderBook(UInt160 baseToken, UInt160 quoteToken, uint quoteDecimals, BigInteger minOrderAmount, BigInteger maxOrderAmount) { Assert(baseToken.IsAddress() && quoteToken.IsAddress(), "Invalid Address"); Assert(baseToken != quoteToken, "Invalid Trade Pair"); @@ -148,6 +169,7 @@ public static void RegisterOrderBook(UInt160 baseToken, UInt160 quoteToken, uint maxOrderAmount = maxOrderAmount }); onBookStatusChanged(baseToken, quoteToken, quoteScale, minOrderAmount, maxOrderAmount, BookPaused(pairKey)); + return true; } /// @@ -156,9 +178,9 @@ public static void RegisterOrderBook(UInt160 baseToken, UInt160 quoteToken, uint /// /// /// - public static void SetMinOrderAmount(UInt160 baseToken, UInt160 quoteToken, BigInteger minOrderAmount) + /// + public static bool SetMinOrderAmount(UInt160 baseToken, UInt160 quoteToken, BigInteger minOrderAmount) { - Assert(baseToken.IsAddress() && quoteToken.IsAddress(), "Invalid Address"); Assert(minOrderAmount > 0, "Invalid Amount Limit"); Assert(Verify(), "No Authorization"); @@ -170,6 +192,7 @@ public static void SetMinOrderAmount(UInt160 baseToken, UInt160 quoteToken, BigI book.minOrderAmount = minOrderAmount; SetOrderBook(pairKey, book); onBookStatusChanged(book.baseToken, book.quoteToken, book.quoteScale, book.minOrderAmount, book.maxOrderAmount, BookPaused(pairKey)); + return true; } /// @@ -178,9 +201,9 @@ public static void SetMinOrderAmount(UInt160 baseToken, UInt160 quoteToken, BigI /// /// /// - public static void SetMaxOrderAmount(UInt160 baseToken, UInt160 quoteToken, BigInteger maxOrderAmount) + /// + public static bool SetMaxOrderAmount(UInt160 baseToken, UInt160 quoteToken, BigInteger maxOrderAmount) { - Assert(baseToken.IsAddress() && quoteToken.IsAddress(), "Invalid Address"); Assert(maxOrderAmount > 0, "Invalid Amount Limit"); Assert(Verify(), "No Authorization"); @@ -192,6 +215,7 @@ public static void SetMaxOrderAmount(UInt160 baseToken, UInt160 quoteToken, BigI book.maxOrderAmount = maxOrderAmount; SetOrderBook(pairKey, book); onBookStatusChanged(book.baseToken, book.quoteToken, book.quoteScale, book.minOrderAmount, book.maxOrderAmount, BookPaused(pairKey)); + return true; } /// @@ -199,9 +223,9 @@ public static void SetMaxOrderAmount(UInt160 baseToken, UInt160 quoteToken, BigI /// /// /// - public static void PauseOrderBook(UInt160 baseToken, UInt160 quoteToken) + /// + public static bool PauseOrderBook(UInt160 baseToken, UInt160 quoteToken) { - Assert(baseToken.IsAddress() && quoteToken.IsAddress(), "Invalid Address"); Assert(Verify(), "No Authorization"); var pairKey = GetPairKey(baseToken, quoteToken); @@ -211,6 +235,7 @@ public static void PauseOrderBook(UInt160 baseToken, UInt160 quoteToken) SetPaused(pairKey); onBookStatusChanged(book.baseToken, book.quoteToken, book.quoteScale, book.minOrderAmount, book.maxOrderAmount, BookPaused(pairKey)); + return true; } /// @@ -218,9 +243,9 @@ public static void PauseOrderBook(UInt160 baseToken, UInt160 quoteToken) /// /// /// - public static void ResumeOrderBook(UInt160 baseToken, UInt160 quoteToken) + /// + public static bool ResumeOrderBook(UInt160 baseToken, UInt160 quoteToken) { - Assert(baseToken.IsAddress() && quoteToken.IsAddress(), "Invalid Address"); Assert(Verify(), "No Authorization"); var pairKey = GetPairKey(baseToken, quoteToken); @@ -230,6 +255,7 @@ public static void ResumeOrderBook(UInt160 baseToken, UInt160 quoteToken) RemovePaused(pairKey); onBookStatusChanged(book.baseToken, book.quoteToken, book.quoteScale, book.minOrderAmount, book.maxOrderAmount, BookPaused(pairKey)); + return true; } /// @@ -253,6 +279,7 @@ public static ByteString AddLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 m // Deposit token var book = GetOrderBook(pairKey); + Assert(book.baseToken.IsAddress() && book.quoteToken.IsAddress(), "Invalid Trade Pair"); if (amount < book.minOrderAmount || amount > book.maxOrderAmount) return null; var me = Runtime.ExecutingScriptHash; if (isBuy) SafeTransfer(book.quoteToken, maker, me, amount * price / book.quoteScale); @@ -291,7 +318,9 @@ public static ByteString AddLimitOrderAt(ByteString parentID, UInt160 maker, Big { // Check parameters Assert(price > 0 && amount > 0, "Invalid Parameters"); - var receipt = GetReceipt(GetOrder(parentID).maker, parentID); + var parent = GetOrder(parentID); + Assert(parent.maker.IsAddress(), "Parent Not Exists"); + var receipt = GetReceipt(parent.maker, parentID); var pairKey = GetPairKey(receipt.baseToken, receipt.quoteToken); Assert(!BookPaused(pairKey), "Book is Paused"); Assert(Runtime.CheckWitness(maker), "No Authorization"); @@ -334,12 +363,13 @@ public static ByteString AddLimitOrderAt(ByteString parentID, UInt160 maker, Big /// /// /// - public static void CancelOrder(UInt160 tokenA, UInt160 tokenB, bool isBuy, ByteString id) + /// + public static bool CancelOrder(UInt160 tokenA, UInt160 tokenB, bool isBuy, ByteString id) { // Check if exist var pairKey = GetPairKey(tokenA, tokenB); - Assert(OrderExists(id), "Order Not Exists"); var order = GetOrder(id); + Assert(order.maker.IsAddress(), "Order Not Exists"); Assert(Runtime.CheckWitness(order.maker), "No Authorization"); // Do remove @@ -354,6 +384,7 @@ public static void CancelOrder(UInt160 tokenA, UInt160 tokenB, bool isBuy, ByteS var me = Runtime.ExecutingScriptHash; if (isBuy) SafeTransfer(book.quoteToken, me, order.maker, order.amount * order.price / book.quoteScale); else SafeTransfer(book.baseToken, me, order.maker, order.amount); + return true; } /// @@ -364,9 +395,8 @@ public static void CancelOrder(UInt160 tokenA, UInt160 tokenB, bool isBuy, ByteS /// public static bool CancelOrderAt(ByteString parentID, ByteString id) { - if (!OrderExists(id)) return false; - Assert(OrderExists(parentID), "Parent Order Not Exists"); var order = GetOrder(id); + Assert(order.maker.IsAddress(), "Order Not Exists"); Assert(Runtime.CheckWitness(order.maker), "No Authorization"); // Do remove @@ -640,6 +670,7 @@ private static BigInteger DealMarketOrderInternal(byte[] pairKey, UInt160 taker, { // Check if can deal var book = GetOrderBook(pairKey); + Assert(book.baseToken.IsAddress() && book.quoteToken.IsAddress(), "Invalid Trade Pair"); var firstID = isBuy ? book.firstSellID : book.firstBuyID; if (firstID is null) return leftAmount; var firstOrder = GetOrder(firstID); @@ -765,16 +796,19 @@ private static BigInteger DealMarketOrderInternal(byte[] pairKey, UInt160 taker, /// /// /// - public static void DealMarketOrderAt(UInt160 taker, ByteString parentID, ByteString id) + /// + public static bool DealMarketOrderAt(UInt160 taker, ByteString parentID, ByteString id) { + // Check parameters Assert(Runtime.CheckWitness(taker), "No Authorization"); - - // Do deal var order = GetOrder(id); - var me = Runtime.ExecutingScriptHash; + Assert(order.maker.IsAddress(), "Order Not Exists"); var receipt = GetReceipt(order.maker, id); var pairKey = GetPairKey(receipt.baseToken, receipt.quoteToken); Assert(!BookPaused(pairKey), "Book is Paused"); + + // Do deal + var me = Runtime.ExecutingScriptHash; var quoteScale = GetOrderBook(pairKey).quoteScale; var fundAddress = GetFundAddress(); @@ -810,6 +844,7 @@ public static void DealMarketOrderAt(UInt160 taker, ByteString parentID, ByteStr SafeTransfer(receipt.baseToken, me, fundAddress, baseFee); SafeTransfer(receipt.quoteToken, me, fundAddress, quoteFee); } + return true; } /// From 27cd5307d5b4f3bd2221c3571660250a786ca842 Mon Sep 17 00:00:00 2001 From: txhsl <799498265@qq.com> Date: Mon, 29 Aug 2022 17:38:57 +0800 Subject: [PATCH 03/16] fix amount --- .../FlamingoSwapOrderBookContract.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs index 4405317..bdcfb4e 100644 --- a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs @@ -35,7 +35,9 @@ public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 Assert(ContractManagement.GetContract(sender) == null, "Forbidden"); // Market order - var leftAmount = expectBookAmount > 0 ? amount - expectBookAmount + DealMarketOrderInternal(pairKey, sender, isBuy, price, expectBookAmount, false) : amount; + var leftAmount = amount; + if (isBuy) leftAmount -= expectBookAmount > 0 ? expectBookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, price, (expectBookAmount * 1000 + 996) / 997, false) * 997 / 1000 : 0; + else leftAmount -= expectBookAmount > 0 ? expectBookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, price, expectBookAmount, false) : 0; if (leftAmount == 0) return null; // Swap AMM @@ -120,8 +122,10 @@ public static bool RouteMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 send Assert(book.baseToken.IsAddress() && book.quoteToken.IsAddress(), "Invalid Trade Pair"); var price = slippage * book.quoteScale / amount; + var leftAmount = amount; var balanceBefore = GetBalanceOf(book.quoteToken, sender); - var leftAmount = expectBookAmount > 0 ? amount - expectBookAmount + DealMarketOrderInternal(pairKey, sender, isBuy, price, expectBookAmount, false) : amount; + if (isBuy) leftAmount -= expectBookAmount > 0 ? expectBookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, price, (expectBookAmount * 1000 + 996) / 997, false) * 997 / 1000 : 0; + else leftAmount -= expectBookAmount > 0 ? expectBookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, price, expectBookAmount, false) : 0; var balanceAfter = GetBalanceOf(book.quoteToken, sender); if (leftAmount == 0) { From 6d65e40cb8501ffcd9cc68324124cca1af732256 Mon Sep 17 00:00:00 2001 From: txhsl <799498265@qq.com> Date: Mon, 29 Aug 2022 18:08:11 +0800 Subject: [PATCH 04/16] method name --- .../FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs index bdcfb4e..a7629b1 100644 --- a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs @@ -446,7 +446,7 @@ public static ByteString GetParentOrderID(UInt160 tokenA, UInt160 tokenB, bool i /// /// [Safe] - public static OrderReceipt[] GetNOrders(UInt160 tokenA, UInt160 tokenB, bool isBuy, uint pos, uint n) + public static OrderReceipt[] GetFirstNOrders(UInt160 tokenA, UInt160 tokenB, bool isBuy, uint pos, uint n) { var results = new OrderReceipt[n]; From f8055538c9f91cae14e72cd50d5a05979172ff50 Mon Sep 17 00:00:00 2001 From: txhsl <799498265@qq.com> Date: Thu, 1 Sep 2022 17:25:38 +0800 Subject: [PATCH 05/16] add claim fee payment, add getters, and optimize --- .../FlamingoSwapOrderBookContract.Admin.cs | 1 + .../FlamingoSwapOrderBookContract.Helper.cs | 33 ++++++ .../FlamingoSwapOrderBookContract.cs | 108 +++++++++++++++--- 3 files changed, 125 insertions(+), 17 deletions(-) diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Admin.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Admin.cs index 54be322..bb39c66 100644 --- a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Admin.cs +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Admin.cs @@ -30,6 +30,7 @@ public partial class FlamingoSwapOrderBookContract private static readonly byte[] OrderMapPrefix = new byte[] { 0x02 }; private static readonly byte[] ReceiptMapPrefix = new byte[] { 0x03 }; private static readonly byte[] PauseMapPrefix = new byte[] { 0x04 }; + private static readonly byte[] FeeMapPrefix = new byte[] { 0x05 }; // When this contract address is included in the transaction signature, // this method will be triggered as a VerificationTrigger to verify that the signature is correct. diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Helper.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Helper.cs index 8cec70d..30c6aec 100644 --- a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Helper.cs +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Helper.cs @@ -352,6 +352,39 @@ private static void SetOrderBook(byte[] pairKey, OrderBook book) bookMap.Put(pairKey, StdLib.Serialize(book)); } + /// + /// Stage the fundfee payment for later claim + /// + /// + /// + private static void StageFundFee(UInt160 token, BigInteger amount) + { + Assert(amount >= 0, "Invalid Fee Amount"); + StorageMap feeMap = new(Storage.CurrentContext, FeeMapPrefix); + feeMap.Put(token, (BigInteger)feeMap.Get(token) + amount); + } + + /// + /// Get staged fundfee amount + /// + /// + /// + private static BigInteger GetStagedFundFee(UInt160 token) + { + StorageMap feeMap = new(Storage.CurrentReadOnlyContext, FeeMapPrefix); + return (BigInteger)feeMap.Get(token); + } + + /// + /// Reset the staged fundfee amount to 0 + /// + /// + private static void CleanStagedFundFee(UInt160 token) + { + StorageMap feeMap = new(Storage.CurrentContext, FeeMapPrefix); + feeMap.Delete(token); + } + /// /// Find a random number as order ID /// diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs index a7629b1..dc7abdf 100644 --- a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs @@ -122,26 +122,30 @@ public static bool RouteMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 send Assert(book.baseToken.IsAddress() && book.quoteToken.IsAddress(), "Invalid Trade Pair"); var price = slippage * book.quoteScale / amount; - var leftAmount = amount; - var balanceBefore = GetBalanceOf(book.quoteToken, sender); - if (isBuy) leftAmount -= expectBookAmount > 0 ? expectBookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, price, (expectBookAmount * 1000 + 996) / 997, false) * 997 / 1000 : 0; - else leftAmount -= expectBookAmount > 0 ? expectBookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, price, expectBookAmount, false) : 0; - var balanceAfter = GetBalanceOf(book.quoteToken, sender); - if (leftAmount == 0) + var quoteAmount = BigInteger.Zero; + if (expectBookAmount > 0) { - Assert(isBuy ? balanceBefore - balanceAfter <= slippage : balanceAfter - balanceBefore >= slippage, "Insufficient Slippage"); - return true; + var balanceBefore = GetBalanceOf(book.quoteToken, sender); + if (isBuy) amount -= expectBookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, price, (expectBookAmount * 1000 + 996) / 997, false) * 997 / 1000; + else amount -= expectBookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, price, expectBookAmount, false); + var balanceAfter = GetBalanceOf(book.quoteToken, sender); + quoteAmount = isBuy ? balanceBefore - balanceAfter : balanceAfter - balanceBefore; + if (amount == 0) + { + Assert(isBuy ? quoteAmount <= slippage : quoteAmount >= slippage, "Insufficient Slippage"); + return true; + } } // Swap AMM var pairContract = GetExchangePairWithAssert(tokenA, tokenB); var ammReverse = GetReserves(pairContract, book.baseToken, book.quoteToken); - var amountIn = isBuy ? GetAmountIn(leftAmount, ammReverse[1], ammReverse[0]) : leftAmount; - var amountOut = isBuy ? leftAmount : GetAmountOut(leftAmount, ammReverse[0], ammReverse[1]); + var amountIn = isBuy ? GetAmountIn(amount, ammReverse[1], ammReverse[0]) : amount; + var amountOut = isBuy ? amount : GetAmountOut(amount, ammReverse[0], ammReverse[1]); if (amountOut > 0) SwapAMM(pairContract, sender, isBuy ? book.quoteToken : book.baseToken, isBuy ? book.baseToken : book.quoteToken, amountIn, amountOut); - Assert(isBuy ? amountIn + balanceBefore - balanceAfter <= slippage : amountOut + balanceAfter - balanceBefore >= slippage, "Insufficient Slippage"); + Assert(isBuy ? amountIn + quoteAmount <= slippage : amountOut + quoteAmount >= slippage, "Insufficient Slippage"); return true; } @@ -476,6 +480,30 @@ public static OrderReceipt[] GetFirstNOrders(UInt160 tokenA, UInt160 tokenB, boo return results; } + [Safe] + public static OrderReceipt[] GetFirstNOrders(ByteString orderID, uint n) + { + var results = new OrderReceipt[n]; + + var currentOrderID = orderID; + var currentOrder = GetOrder(currentOrderID); + if (!currentOrder.maker.IsAddress()) return results; + for (int i = 0; i < n; i++) + { + var receipt = GetReceipt(currentOrder.maker, currentOrderID); + receipt.maker = currentOrder.maker; + receipt.price = currentOrder.price; + receipt.leftAmount = currentOrder.amount; + results[i] = receipt; + + if (currentOrder.nextID is null) break; + + currentOrderID = currentOrder.nextID; + currentOrder = GetOrder(currentOrder.nextID); + } + return results; + } + /// /// Get all orders and their details of maker /// @@ -488,7 +516,7 @@ public static OrderReceipt[] GetOrdersOf(UInt160 maker, uint pos, uint n) { var results = new OrderReceipt[n]; var iterator = ReceiptsOf(maker); - // Makeup details + // Make up details for (int i = 0; i < pos + n; i++) { if (iterator.Next() && i >= pos) @@ -503,6 +531,36 @@ public static OrderReceipt[] GetOrdersOf(UInt160 maker, uint pos, uint n) return results; } + [Safe] + public static OrderReceipt[] GetOrdersOf(ByteString orderID, uint n) + { + var results = new OrderReceipt[n]; + var order = GetOrder(orderID); + if (!order.maker.IsAddress()) return results; + var iterator = ReceiptsOf(order.maker); + // Make up details + while (iterator.Next()) + { + var receipt = (OrderReceipt)iterator.Value; + if (receipt.id.Equals(orderID)) + { + for (int i = 0; i < n; i++) + { + results[i] = receipt; + order = GetOrder(results[i].id); + results[i].maker = order.maker; + results[i].price = order.price; + results[i].leftAmount = order.amount; + + if (!iterator.Next()) break; + receipt = (OrderReceipt)iterator.Value; + } + return results; + } + } + return results; + } + /// /// Get the total reverse of tradable orders with an expected price /// @@ -785,12 +843,9 @@ private static BigInteger DealMarketOrderInternal(byte[] pairKey, UInt160 taker, foreach (var toAddress in makerReceive.Keys) SafeTransfer(book.baseToken, me, toAddress, makerReceive[toAddress]); } + StageFundFee(book.baseToken, baseFee); + StageFundFee(book.quoteToken, quoteFee); - if (fundAddress is not null) - { - SafeTransfer(book.quoteToken, me, fundAddress, quoteFee); - SafeTransfer(book.baseToken, me, fundAddress, baseFee); - } return leftAmount; } @@ -851,6 +906,25 @@ public static bool DealMarketOrderAt(UInt160 taker, ByteString parentID, ByteStr return true; } + /// + /// Claim the staged fundfee payment to fund address + /// + /// + /// + public static BigInteger ClaimFundFee(UInt160 token) + { + var fundAddress = GetFundAddress(); + if (fundAddress is null) return 0; + var amount = GetStagedFundFee(token); + var me = Runtime.ExecutingScriptHash; + if (amount > 0) + { + CleanStagedFundFee(token); + SafeTransfer(token, me, fundAddress, amount); + } + return amount; + } + /// /// Price reporter /// From 0a6970167d0b02ecdd23d41e89950cf62a01f2b4 Mon Sep 17 00:00:00 2001 From: txhsl <799498265@qq.com> Date: Thu, 1 Sep 2022 17:44:46 +0800 Subject: [PATCH 06/16] summarize claim fund --- .../FlamingoSwapOrderBookContract.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs index dc7abdf..734bd93 100644 --- a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs @@ -909,20 +909,23 @@ public static bool DealMarketOrderAt(UInt160 taker, ByteString parentID, ByteStr /// /// Claim the staged fundfee payment to fund address /// - /// + /// /// - public static BigInteger ClaimFundFee(UInt160 token) + public static bool ClaimFundFee(UInt160[] tokens) { var fundAddress = GetFundAddress(); - if (fundAddress is null) return 0; - var amount = GetStagedFundFee(token); + if (fundAddress is null) return false; var me = Runtime.ExecutingScriptHash; - if (amount > 0) + foreach (var token in tokens) { - CleanStagedFundFee(token); - SafeTransfer(token, me, fundAddress, amount); + var amount = GetStagedFundFee(token); + if (amount > 0) + { + CleanStagedFundFee(token); + SafeTransfer(token, me, fundAddress, amount); + } } - return amount; + return true; } /// From b25056ff8f771616a43ea389dc754c21af918ac4 Mon Sep 17 00:00:00 2001 From: txhsl <799498265@qq.com> Date: Fri, 2 Sep 2022 10:55:15 +0800 Subject: [PATCH 07/16] remove duplicated assert, add deadline, some fix --- .../FlamingoSwapOrderBookContract.cs | 16 +++++++-------- .../FlamingoSwapRouterContract.cs | 20 ++++++++----------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs index 734bd93..e4009a2 100644 --- a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs @@ -25,14 +25,16 @@ public partial class FlamingoSwapOrderBookContract : SmartContract /// /// /// + /// /// - public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 sender, bool isBuy, BigInteger amount, BigInteger price, BigInteger expectBookAmount) + public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 sender, bool isBuy, BigInteger amount, BigInteger price, BigInteger expectBookAmount, BigInteger deadLine) { Assert(amount > 0 && price > 0 && expectBookAmount >= 0 && amount >= expectBookAmount, "Invalid Parameters"); var pairKey = GetPairKey(tokenA, tokenB); Assert(!BookPaused(pairKey), "Book is Paused"); Assert(Runtime.CheckWitness(sender), "No Authorization"); Assert(ContractManagement.GetContract(sender) == null, "Forbidden"); + Assert((BigInteger)Runtime.Time <= deadLine, "Exceeded the deadline"); // Market order var leftAmount = amount; @@ -108,14 +110,15 @@ public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 /// /// /// + /// /// - public static bool RouteMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 sender, bool isBuy, BigInteger amount, BigInteger slippage, BigInteger expectBookAmount) + public static bool RouteMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 sender, bool isBuy, BigInteger amount, BigInteger slippage, BigInteger expectBookAmount, BigInteger deadLine) { Assert(amount > 0 && slippage > 0 && expectBookAmount >= 0 && amount >= expectBookAmount, "Invalid Parameters"); var pairKey = GetPairKey(tokenA, tokenB); Assert(!BookPaused(pairKey), "Book is Paused"); Assert(Runtime.CheckWitness(sender), "No Authorization"); - Assert(ContractManagement.GetContract(sender) == null, "Forbidden"); + Assert((BigInteger)Runtime.Time <= deadLine, "Exceeded the deadline"); // Market order var book = GetOrderBook(pairKey); @@ -898,11 +901,8 @@ public static bool DealMarketOrderAt(UInt160 taker, ByteString parentID, ByteStr SafeTransfer(receipt.quoteToken, me, order.maker, quotePayment); } - if (fundAddress is not null) - { - SafeTransfer(receipt.baseToken, me, fundAddress, baseFee); - SafeTransfer(receipt.quoteToken, me, fundAddress, quoteFee); - } + StageFundFee(receipt.baseToken, baseFee); + StageFundFee(receipt.quoteToken, quoteFee); return true; } diff --git a/Swap/flamingo-contract-swap/FlamingoSwapRouter/FlamingoSwapRouterContract.cs b/Swap/flamingo-contract-swap/FlamingoSwapRouter/FlamingoSwapRouterContract.cs index 5bd733d..37b108f 100644 --- a/Swap/flamingo-contract-swap/FlamingoSwapRouter/FlamingoSwapRouterContract.cs +++ b/Swap/flamingo-contract-swap/FlamingoSwapRouter/FlamingoSwapRouterContract.cs @@ -31,7 +31,7 @@ public partial class FlamingoSwapRouterContract : SmartContract public static BigInteger[] AddLiquidity(UInt160 sender, UInt160 tokenA, UInt160 tokenB, BigInteger amountADesired, BigInteger amountBDesired, BigInteger amountAMin, BigInteger amountBMin, BigInteger deadLine) { //验证参数 - Assert(sender.IsValid && tokenA.IsValid && tokenB.IsValid && amountADesired >= 0 && amountBDesired >= 0 && amountAMin >= 0 && amountBMin >= 0 && deadLine > 0, "Invalid Parameters"); + Assert(amountADesired >= 0 && amountBDesired >= 0 && amountAMin >= 0 && amountBMin >= 0, "Invalid Parameters"); //验证权限 Assert(Runtime.CheckWitness(sender), "Forbidden"); //看看有没有超过最后期限 @@ -80,7 +80,7 @@ public static BigInteger[] AddLiquidity(UInt160 sender, UInt160 tokenA, UInt160 public static BigInteger[] AddLiquidity(UInt160 tokenA, UInt160 tokenB, BigInteger amountADesired, BigInteger amountBDesired, BigInteger amountAMin, BigInteger amountBMin, BigInteger deadLine) { //验证参数 - Assert(tokenA.IsValid && tokenB.IsValid && amountADesired >= 0 && amountBDesired >= 0 && amountAMin >= 0 && amountBMin >= 0 && deadLine > 0, "Invalid Parameters"); + Assert(amountADesired >= 0 && amountBDesired >= 0 && amountAMin >= 0 && amountBMin >= 0, "Invalid Parameters"); //验证权限 var caller = Runtime.CallingScriptHash; Assert(ContractManagement.GetContract(caller) != null, "Forbidden"); @@ -142,7 +142,7 @@ public static BigInteger[] AddLiquidity(UInt160 tokenA, UInt160 tokenB, BigInteg public static BigInteger[] RemoveLiquidity(UInt160 sender, UInt160 tokenA, UInt160 tokenB, BigInteger liquidity, BigInteger amountAMin, BigInteger amountBMin, BigInteger deadLine) { //验证参数 - Assert(sender.IsValid && tokenA.IsValid && tokenB.IsValid && liquidity >= 0 && amountAMin >= 0 && amountBMin >= 0 && deadLine > 0, "Invalid Parameters"); + Assert(liquidity > 0 && amountAMin >= 0 && amountBMin >= 0, "Invalid Parameters"); //验证权限 Assert(Runtime.CheckWitness(sender), "Forbidden"); //看看有没有超过最后期限 @@ -166,7 +166,7 @@ public static BigInteger[] RemoveLiquidity(UInt160 sender, UInt160 tokenA, UInt1 public static BigInteger[] RemoveLiquidity(UInt160 tokenA, UInt160 tokenB, BigInteger liquidity, BigInteger amountAMin, BigInteger amountBMin, BigInteger deadLine) { //验证参数 - Assert(tokenA.IsValid && tokenB.IsValid && liquidity > 0 && amountAMin >= 0 && amountBMin >= 0 && deadLine > 0, "Invalid Parameters"); + Assert(liquidity > 0 && amountAMin >= 0 && amountBMin >= 0, "Invalid Parameters"); //验证权限 var caller = Runtime.CallingScriptHash; Assert(ContractManagement.GetContract(caller) != null, "Forbidden"); @@ -214,8 +214,6 @@ public static BigInteger Quote(BigInteger amountA, BigInteger reserveA, BigInteg /// public static BigInteger GetAmountOut(BigInteger amountIn, BigInteger reserveIn, BigInteger reserveOut) { - // Assert(amountIn > 0, "amountIn should be positive number"); - // Assert(reserveIn > 0 && reserveOut > 0, "reserve should be positive number"); Assert(amountIn > 0 && reserveIn > 0 && reserveOut > 0, "AmountIn Must > 0"); var amountInWithFee = amountIn * 997; @@ -234,8 +232,6 @@ public static BigInteger GetAmountOut(BigInteger amountIn, BigInteger reserveIn, /// public static BigInteger GetAmountIn(BigInteger amountOut, BigInteger reserveIn, BigInteger reserveOut) { - //Assert(amountOut > 0, "amountOut should be positive number"); - //Assert(reserveIn > 0 && reserveOut > 0, "reserve should be positive number"); Assert(amountOut > 0 && reserveIn > 0 && reserveOut > 0, "AmountOut Must > 0"); var numerator = reserveIn * amountOut * 1000; var denominator = (reserveOut - amountOut) * 997; @@ -320,7 +316,7 @@ public static void OnNEP17Payment(UInt160 sender, BigInteger amountIn, object da public static bool SwapTokenInForTokenOut(UInt160 sender, BigInteger amountIn, BigInteger amountOutMin, UInt160[] paths, BigInteger deadLine) { //验证参数 - Assert(sender.IsValid && amountIn > 0 && amountOutMin >= 0 && paths.Length >= 2 && deadLine > 0, "Invalid Parameters"); + Assert(amountOutMin >= 0, "Invalid Parameters"); //验证权限 Assert(Runtime.CheckWitness(sender), "Forbidden"); //看看有没有超过最后期限 @@ -339,7 +335,7 @@ public static bool SwapTokenInForTokenOut(UInt160 sender, BigInteger amountIn, B public static bool SwapTokenInForTokenOut(BigInteger amountIn, BigInteger amountOutMin, UInt160[] paths, BigInteger deadLine) { //验证参数 - Assert(amountIn > 0 && amountOutMin >= 0 && paths.Length >= 2 && deadLine > 0, "Invalid Parameters"); + Assert(amountOutMin >= 0, "Invalid Parameters"); //验证权限 var caller = Runtime.CallingScriptHash; Assert(ContractManagement.GetContract(caller) != null, "Forbidden"); @@ -369,7 +365,7 @@ public static bool SwapTokenInForTokenOut(BigInteger amountIn, BigInteger amount public static bool SwapTokenOutForTokenIn(UInt160 sender, BigInteger amountOut, BigInteger amountInMax, UInt160[] paths, BigInteger deadLine) { //验证参数 - Assert(sender.IsValid && amountOut > 0 && amountInMax >= 0 && paths.Length >= 2 && deadLine > 0, "Invalid Parameters"); + Assert(amountInMax >= 0, "Invalid Parameters"); //验证权限 Assert(Runtime.CheckWitness(sender), "Forbidden"); //看看有没有超过最后期限 @@ -388,7 +384,7 @@ public static bool SwapTokenOutForTokenIn(UInt160 sender, BigInteger amountOut, public static bool SwapTokenOutForTokenIn(BigInteger amountOut, BigInteger amountInMax, UInt160[] paths, BigInteger deadLine) { //验证参数 - Assert(amountOut > 0 && amountInMax >= 0 && paths.Length >= 2 && deadLine > 0, "Invalid Parameters"); + Assert(amountInMax >= 0, "Invalid Parameters"); //验证权限 var caller = Runtime.CallingScriptHash; Assert(ContractManagement.GetContract(caller) != null, "Forbidden"); From 39fa2490ef25826ff06443562fe5d607ade7a44b Mon Sep 17 00:00:00 2001 From: txhsl <799498265@qq.com> Date: Fri, 2 Sep 2022 17:16:03 +0800 Subject: [PATCH 08/16] [Safe] BookTradable --- .../FlamingoSwapOrderBookContract.cs | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs index e4009a2..9bebd2c 100644 --- a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs @@ -444,7 +444,7 @@ public static ByteString GetParentOrderID(UInt160 tokenA, UInt160 tokenB, bool i } /// - /// Get first N limit orders and their details + /// Get first N limit orders and their details, start from pos /// /// /// @@ -483,6 +483,12 @@ public static OrderReceipt[] GetFirstNOrders(UInt160 tokenA, UInt160 tokenB, boo return results; } + /// + /// Get first N limit orders and their details, start from orderID + /// + /// + /// + /// [Safe] public static OrderReceipt[] GetFirstNOrders(ByteString orderID, uint n) { @@ -508,7 +514,7 @@ public static OrderReceipt[] GetFirstNOrders(ByteString orderID, uint n) } /// - /// Get all orders and their details of maker + /// Get N orders of maker and their details, start from pos /// /// /// @@ -534,6 +540,12 @@ public static OrderReceipt[] GetOrdersOf(UInt160 maker, uint pos, uint n) return results; } + /// + /// Get N orders of maker and their details, start from orderID + /// + /// + /// + /// [Safe] public static OrderReceipt[] GetOrdersOf(ByteString orderID, uint n) { @@ -698,7 +710,7 @@ private static BigInteger[] MatchQuoteInternal(byte[] pairKey, bool isBuy, BigIn } /// - /// Try to make a market deal with orderbook + /// Try to make a market deal with orderbook, taker is not a contract /// /// /// @@ -719,6 +731,15 @@ public static BigInteger DealMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 return DealMarketOrderInternal(pairKey, taker, isBuy, price, amount, false); } + /// + /// Try to make a market deal with orderbook, taker is a contract + /// + /// + /// + /// + /// + /// + /// Left amount public static BigInteger DealMarketOrder(UInt160 tokenA, UInt160 tokenB, bool isBuy, BigInteger price, BigInteger amount) { // Check parameters @@ -945,12 +966,31 @@ public static BigInteger GetMarketPrice(UInt160 tokenA, UInt160 tokenB, bool isB return GetOrder(firstID).price; } + /// + /// Get trade pair details + /// + /// + /// + /// [Safe] public static OrderBook GetBookInfo(UInt160 tokenA, UInt160 tokenB) { var pairKey = GetPairKey(tokenA, tokenB); return GetOrderBook(pairKey); } + + /// + /// Check if a pair of token is tradable + /// + /// + /// + /// + [Safe] + public static bool BookTradable(UInt160 tokenA, UInt160 tokenB) + { + var pairKey = GetPairKey(tokenA, tokenB); + return BookExists(pairKey) && !BookPaused(pairKey); + } #endregion #region AMM like API From 498f21d011297dfe64f9aef9c1b494ade8f585c2 Mon Sep 17 00:00:00 2001 From: txhsl <799498265@qq.com> Date: Thu, 22 Sep 2022 18:06:38 +0800 Subject: [PATCH 09/16] add price limit for book part --- .../FlamingoSwapOrderBookContract.cs | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs index 9bebd2c..f21f7c0 100644 --- a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs @@ -22,24 +22,26 @@ public partial class FlamingoSwapOrderBookContract : SmartContract /// /// /// - /// - /// - /// + /// Total buy(real get)/sell amount of the limit order + /// Price limit of the order + /// Expected amount to buy(real get)/sell from/to book before amm + /// Price limit of bookAmount part /// /// - public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 sender, bool isBuy, BigInteger amount, BigInteger price, BigInteger expectBookAmount, BigInteger deadLine) + public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 sender, bool isBuy, BigInteger amount, BigInteger price, BigInteger bookAmount, BigInteger bookPrice, BigInteger deadLine) { - Assert(amount > 0 && price > 0 && expectBookAmount >= 0 && amount >= expectBookAmount, "Invalid Parameters"); + Assert(amount > 0 && price > 0 && bookAmount >= 0 && amount >= bookAmount, "Invalid Parameters"); var pairKey = GetPairKey(tokenA, tokenB); - Assert(!BookPaused(pairKey), "Book is Paused"); + Assert(!BookPaused(pairKey), "Book Is Paused"); Assert(Runtime.CheckWitness(sender), "No Authorization"); Assert(ContractManagement.GetContract(sender) == null, "Forbidden"); - Assert((BigInteger)Runtime.Time <= deadLine, "Exceeded the deadline"); + if (bookAmount > 0) Assert((isBuy && price >= bookPrice) || (!isBuy && price <= bookPrice), "BookPrice Beyond Limit"); + Assert((BigInteger)Runtime.Time <= deadLine, "Exceeded The Deadline"); // Market order var leftAmount = amount; - if (isBuy) leftAmount -= expectBookAmount > 0 ? expectBookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, price, (expectBookAmount * 1000 + 996) / 997, false) * 997 / 1000 : 0; - else leftAmount -= expectBookAmount > 0 ? expectBookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, price, expectBookAmount, false) : 0; + if (isBuy) leftAmount -= bookAmount > 0 ? bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, (bookAmount * 1000 + 996) / 997, false) * 997 / 1000 : 0; + else leftAmount -= bookAmount > 0 ? bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, false) : 0; if (leftAmount == 0) return null; // Swap AMM @@ -107,30 +109,32 @@ public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 /// /// /// - /// - /// - /// + /// Total buy(real get)/sell amount of the limit order + /// The amount limit of final payment/receive(real get) + /// Expected amount to buy(real get)/sell from/to book before amm + /// Price limit of bookAmount part /// /// - public static bool RouteMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 sender, bool isBuy, BigInteger amount, BigInteger slippage, BigInteger expectBookAmount, BigInteger deadLine) + public static bool RouteMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 sender, bool isBuy, BigInteger amount, BigInteger slippage, BigInteger bookAmount, BigInteger bookPrice, BigInteger deadLine) { - Assert(amount > 0 && slippage > 0 && expectBookAmount >= 0 && amount >= expectBookAmount, "Invalid Parameters"); + Assert(amount > 0 && slippage > 0 && bookAmount >= 0 && amount >= bookAmount, "Invalid Parameters"); var pairKey = GetPairKey(tokenA, tokenB); Assert(!BookPaused(pairKey), "Book is Paused"); Assert(Runtime.CheckWitness(sender), "No Authorization"); - Assert((BigInteger)Runtime.Time <= deadLine, "Exceeded the deadline"); + Assert((BigInteger)Runtime.Time <= deadLine, "Exceeded the Deadline"); // Market order var book = GetOrderBook(pairKey); Assert(book.baseToken.IsAddress() && book.quoteToken.IsAddress(), "Invalid Trade Pair"); var price = slippage * book.quoteScale / amount; + if (bookAmount > 0) Assert((isBuy && price >= bookPrice) || (!isBuy && price <= bookPrice), "BookPrice Beyond Limit"); var quoteAmount = BigInteger.Zero; - if (expectBookAmount > 0) + if (bookAmount > 0) { var balanceBefore = GetBalanceOf(book.quoteToken, sender); - if (isBuy) amount -= expectBookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, price, (expectBookAmount * 1000 + 996) / 997, false) * 997 / 1000; - else amount -= expectBookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, price, expectBookAmount, false); + if (isBuy) amount -= bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, (bookAmount * 1000 + 996) / 997, false) * 997 / 1000; + else amount -= bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, false); var balanceAfter = GetBalanceOf(book.quoteToken, sender); quoteAmount = isBuy ? balanceBefore - balanceAfter : balanceAfter - balanceBefore; if (amount == 0) @@ -284,7 +288,7 @@ public static ByteString AddLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 m // Check parameters Assert(price > 0 && amount > 0, "Invalid Parameters"); var pairKey = GetPairKey(tokenA, tokenB); - Assert(!BookPaused(pairKey), "Book is Paused"); + Assert(!BookPaused(pairKey), "Book Is Paused"); Assert(Runtime.CheckWitness(maker), "No Authorization"); Assert(ContractManagement.GetContract(maker) == null, "Forbidden"); @@ -333,7 +337,7 @@ public static ByteString AddLimitOrderAt(ByteString parentID, UInt160 maker, Big Assert(parent.maker.IsAddress(), "Parent Not Exists"); var receipt = GetReceipt(parent.maker, parentID); var pairKey = GetPairKey(receipt.baseToken, receipt.quoteToken); - Assert(!BookPaused(pairKey), "Book is Paused"); + Assert(!BookPaused(pairKey), "Book Is Paused"); Assert(Runtime.CheckWitness(maker), "No Authorization"); Assert(ContractManagement.GetContract(maker) == null, "Forbidden"); @@ -724,7 +728,7 @@ public static BigInteger DealMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 // Check parameters Assert(price > 0 && amount > 0, "Invalid Parameters"); var pairKey = GetPairKey(tokenA, tokenB); - Assert(!BookPaused(pairKey), "Book is Paused"); + Assert(!BookPaused(pairKey), "Book Is Paused"); Assert(Runtime.CheckWitness(taker), "No Authorization"); Assert(ContractManagement.GetContract(taker) == null, "Forbidden"); @@ -745,7 +749,7 @@ public static BigInteger DealMarketOrder(UInt160 tokenA, UInt160 tokenB, bool is // Check parameters Assert(price > 0 && amount > 0, "Invalid Parameters"); var pairKey = GetPairKey(tokenA, tokenB); - Assert(!BookPaused(pairKey), "Book is Paused"); + Assert(!BookPaused(pairKey), "Book Is Paused"); var caller = Runtime.CallingScriptHash; Assert(ContractManagement.GetContract(caller) != null, "Forbidden"); @@ -888,7 +892,7 @@ public static bool DealMarketOrderAt(UInt160 taker, ByteString parentID, ByteStr Assert(order.maker.IsAddress(), "Order Not Exists"); var receipt = GetReceipt(order.maker, id); var pairKey = GetPairKey(receipt.baseToken, receipt.quoteToken); - Assert(!BookPaused(pairKey), "Book is Paused"); + Assert(!BookPaused(pairKey), "Book Is Paused"); // Do deal var me = Runtime.ExecutingScriptHash; From 9fb405930a744d446888985b2a622412b684a115 Mon Sep 17 00:00:00 2001 From: txhsl <799498265@qq.com> Date: Fri, 23 Sep 2022 11:50:21 +0800 Subject: [PATCH 10/16] change bookAmount to in-book amount --- .../FlamingoSwapOrderBookContract.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs index f21f7c0..6dd7193 100644 --- a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs @@ -22,9 +22,9 @@ public partial class FlamingoSwapOrderBookContract : SmartContract /// /// /// - /// Total buy(real get)/sell amount of the limit order + /// Total buy/sell(real get/pay) amount of the limit order /// Price limit of the order - /// Expected amount to buy(real get)/sell from/to book before amm + /// Expected amount to buy/sell(in-book amount) in book before amm /// Price limit of bookAmount part /// /// @@ -40,7 +40,7 @@ public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 // Market order var leftAmount = amount; - if (isBuy) leftAmount -= bookAmount > 0 ? bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, (bookAmount * 1000 + 996) / 997, false) * 997 / 1000 : 0; + if (isBuy) leftAmount -= bookAmount > 0 ? (bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, false)) * 997 / 1000 : 0; else leftAmount -= bookAmount > 0 ? bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, false) : 0; if (leftAmount == 0) return null; @@ -109,9 +109,9 @@ public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 /// /// /// - /// Total buy(real get)/sell amount of the limit order - /// The amount limit of final payment/receive(real get) - /// Expected amount to buy(real get)/sell from/to book before amm + /// Total buy/sell(real get/pay) amount of the limit order + /// The amount limit of final receive/payment(real get/pay) + /// Expected amount to buy/sell(in-book amount) in book before amm /// Price limit of bookAmount part /// /// @@ -133,7 +133,7 @@ public static bool RouteMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 send if (bookAmount > 0) { var balanceBefore = GetBalanceOf(book.quoteToken, sender); - if (isBuy) amount -= bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, (bookAmount * 1000 + 996) / 997, false) * 997 / 1000; + if (isBuy) amount -= (bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, false)) * 997 / 1000; else amount -= bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, false); var balanceAfter = GetBalanceOf(book.quoteToken, sender); quoteAmount = isBuy ? balanceBefore - balanceAfter : balanceAfter - balanceBefore; From 4a01d27666b7c1c50a5e8accdb260f32ecf8ab13 Mon Sep 17 00:00:00 2001 From: txhsl <799498265@qq.com> Date: Fri, 23 Sep 2022 14:37:49 +0800 Subject: [PATCH 11/16] Revert "change bookAmount to in-book amount" This reverts commit 9fb405930a744d446888985b2a622412b684a115. --- .../FlamingoSwapOrderBookContract.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs index 6dd7193..f21f7c0 100644 --- a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs @@ -22,9 +22,9 @@ public partial class FlamingoSwapOrderBookContract : SmartContract /// /// /// - /// Total buy/sell(real get/pay) amount of the limit order + /// Total buy(real get)/sell amount of the limit order /// Price limit of the order - /// Expected amount to buy/sell(in-book amount) in book before amm + /// Expected amount to buy(real get)/sell from/to book before amm /// Price limit of bookAmount part /// /// @@ -40,7 +40,7 @@ public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 // Market order var leftAmount = amount; - if (isBuy) leftAmount -= bookAmount > 0 ? (bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, false)) * 997 / 1000 : 0; + if (isBuy) leftAmount -= bookAmount > 0 ? bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, (bookAmount * 1000 + 996) / 997, false) * 997 / 1000 : 0; else leftAmount -= bookAmount > 0 ? bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, false) : 0; if (leftAmount == 0) return null; @@ -109,9 +109,9 @@ public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 /// /// /// - /// Total buy/sell(real get/pay) amount of the limit order - /// The amount limit of final receive/payment(real get/pay) - /// Expected amount to buy/sell(in-book amount) in book before amm + /// Total buy(real get)/sell amount of the limit order + /// The amount limit of final payment/receive(real get) + /// Expected amount to buy(real get)/sell from/to book before amm /// Price limit of bookAmount part /// /// @@ -133,7 +133,7 @@ public static bool RouteMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 send if (bookAmount > 0) { var balanceBefore = GetBalanceOf(book.quoteToken, sender); - if (isBuy) amount -= (bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, false)) * 997 / 1000; + if (isBuy) amount -= bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, (bookAmount * 1000 + 996) / 997, false) * 997 / 1000; else amount -= bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, false); var balanceAfter = GetBalanceOf(book.quoteToken, sender); quoteAmount = isBuy ? balanceBefore - balanceAfter : balanceAfter - balanceBefore; From 7baa058e8b3af6490e9d3afa80295c2f2141e059 Mon Sep 17 00:00:00 2001 From: txhsl <799498265@qq.com> Date: Tue, 8 Nov 2022 17:39:21 +0800 Subject: [PATCH 12/16] route market order as swap --- .../FlamingoSwapOrderBookContract.cs | 168 +++++++++++++----- 1 file changed, 120 insertions(+), 48 deletions(-) diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs index f21f7c0..8d7b4b3 100644 --- a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs @@ -16,7 +16,7 @@ namespace FlamingoSwapOrderBook public partial class FlamingoSwapOrderBookContract : SmartContract { /// - /// Deal and add limit order base on input strategy + /// Deal and add limit order base on input strategy, settled by base amount /// /// /// @@ -30,7 +30,7 @@ public partial class FlamingoSwapOrderBookContract : SmartContract /// public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 sender, bool isBuy, BigInteger amount, BigInteger price, BigInteger bookAmount, BigInteger bookPrice, BigInteger deadLine) { - Assert(amount > 0 && price > 0 && bookAmount >= 0 && amount >= bookAmount, "Invalid Parameters"); + Assert(amount > 0 && price > 0 && bookAmount >= 0 && bookPrice > 0 && amount >= bookAmount, "Invalid Parameters"); var pairKey = GetPairKey(tokenA, tokenB); Assert(!BookPaused(pairKey), "Book Is Paused"); Assert(Runtime.CheckWitness(sender), "No Authorization"); @@ -40,8 +40,8 @@ public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 // Market order var leftAmount = amount; - if (isBuy) leftAmount -= bookAmount > 0 ? bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, (bookAmount * 1000 + 996) / 997, false) * 997 / 1000 : 0; - else leftAmount -= bookAmount > 0 ? bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, false) : 0; + if (isBuy) leftAmount -= bookAmount > 0 ? bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, (bookAmount * 1000 + 996) / 997, false)[0] * 997 / 1000 : 0; + else leftAmount -= bookAmount > 0 ? bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, false)[0] : 0; if (leftAmount == 0) return null; // Swap AMM @@ -50,24 +50,24 @@ public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 var pairContract = GetExchangePairWithAssert(tokenA, tokenB); var hasFundFee = HasFundAddress(pairContract); - var ammReverse = isBuy + var reverse = isBuy ? GetReserves(pairContract, book.quoteToken, book.baseToken) : GetReserves(pairContract, book.baseToken, book.quoteToken); var amountIn = hasFundFee - ? GetAmountInTillPriceWithFundFee(isBuy, price, book.quoteScale, ammReverse[0], ammReverse[1]) - : GetAmountInTillPrice(isBuy, price, book.quoteScale, ammReverse[0], ammReverse[1]); + ? GetAmountInTillPriceWithFundFee(isBuy, price, book.quoteScale, reverse[0], reverse[1]) + : GetAmountInTillPrice(isBuy, price, book.quoteScale, reverse[0], reverse[1]); if (amountIn < 0) amountIn = 0; - var amountOut = GetAmountOut(amountIn, ammReverse[0], ammReverse[1]); + var amountOut = GetAmountOut(amountIn, reverse[0], reverse[1]); if (isBuy && leftAmount < amountOut) { amountOut = leftAmount; - amountIn = GetAmountIn(amountOut, ammReverse[0], ammReverse[1]); + amountIn = GetAmountIn(amountOut, reverse[0], reverse[1]); } if (!isBuy && leftAmount < amountIn) { amountIn = leftAmount; - amountOut = GetAmountOut(amountIn, ammReverse[0], ammReverse[1]); + amountOut = GetAmountOut(amountIn, reverse[0], reverse[1]); } if (amountOut > 0) @@ -103,56 +103,128 @@ public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 } /// - /// Deal market order based on input strategy + /// Deal market order based on input strategy, half swap like (amountIn & amountOut) but half book like (bookAmount) /// - /// - /// + /// + /// /// - /// - /// Total buy(real get)/sell amount of the limit order - /// The amount limit of final payment/receive(real get) + /// + /// /// Expected amount to buy(real get)/sell from/to book before amm /// Price limit of bookAmount part /// /// - public static bool RouteMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 sender, bool isBuy, BigInteger amount, BigInteger slippage, BigInteger bookAmount, BigInteger bookPrice, BigInteger deadLine) + public static bool RouteMarketOrderInForOut(UInt160 tokenFrom, UInt160 tokenTo, UInt160 sender, BigInteger amountIn, BigInteger amountOutMin, BigInteger bookAmount, BigInteger bookPrice, BigInteger deadLine) { - Assert(amount > 0 && slippage > 0 && bookAmount >= 0 && amount >= bookAmount, "Invalid Parameters"); - var pairKey = GetPairKey(tokenA, tokenB); - Assert(!BookPaused(pairKey), "Book is Paused"); + Assert(amountIn > 0 && amountOutMin > 0 && bookAmount >= 0 && bookPrice > 0, "Invalid Parameters"); Assert(Runtime.CheckWitness(sender), "No Authorization"); Assert((BigInteger)Runtime.Time <= deadLine, "Exceeded the Deadline"); - // Market order - var book = GetOrderBook(pairKey); - Assert(book.baseToken.IsAddress() && book.quoteToken.IsAddress(), "Invalid Trade Pair"); - var price = slippage * book.quoteScale / amount; - if (bookAmount > 0) Assert((isBuy && price >= bookPrice) || (!isBuy && price <= bookPrice), "BookPrice Beyond Limit"); - - var quoteAmount = BigInteger.Zero; + // Deal in order book if (bookAmount > 0) { - var balanceBefore = GetBalanceOf(book.quoteToken, sender); - if (isBuy) amount -= bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, (bookAmount * 1000 + 996) / 997, false) * 997 / 1000; - else amount -= bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, false); - var balanceAfter = GetBalanceOf(book.quoteToken, sender); - quoteAmount = isBuy ? balanceBefore - balanceAfter : balanceAfter - balanceBefore; - if (amount == 0) + // Check pair status + var pairKey = GetPairKey(tokenFrom, tokenTo); + Assert(!BookPaused(pairKey), "Book is Paused"); + var book = GetOrderBook(pairKey); + Assert(book.baseToken.IsAddress() && book.quoteToken.IsAddress(), "Invalid Trade Pair"); + + // Check price limit + var isBuy = tokenTo == book.baseToken; + var price = isBuy ? amountIn * book.quoteScale / amountOutMin : amountOutMin * book.quoteScale / amountIn; + Assert((isBuy && price >= bookPrice) || (!isBuy && price <= bookPrice), "BookPrice Beyond Limit"); + + // Deal and record consumed amountIn and satisfied amountOut + if (isBuy) + { + var result = DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, (bookAmount * 1000 + 996) / 997, false); + amountOutMin -= bookAmount - result[0] * 997 / 1000; + amountIn -= result[1]; + } + else { - Assert(isBuy ? quoteAmount <= slippage : quoteAmount >= slippage, "Insufficient Slippage"); + var result = DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, false); + amountIn -= bookAmount - result[0]; + amountOutMin -= result[1]; + } + Assert(amountIn >= 0, "Exceeded AmountIn"); // Should not spend more than amountIn + if (amountIn == 0) // No longer need further swap + { + Assert(amountOutMin <= 0, "Insufficient AmountOut"); // Should get more than amountOutMin return true; } } // Swap AMM - var pairContract = GetExchangePairWithAssert(tokenA, tokenB); - var ammReverse = GetReserves(pairContract, book.baseToken, book.quoteToken); + var pairContract = GetExchangePairWithAssert(tokenFrom, tokenTo); + var reverse = GetReserves(pairContract, tokenFrom, tokenTo); + var amountOut = GetAmountOut(amountIn, reverse[0], reverse[1]); + Assert(amountOut >= amountOutMin, "Insufficient AmountOut"); + + if (amountOut > 0) SwapAMM(pairContract, sender, tokenFrom, tokenTo, amountIn, amountOut); + return true; + } + + /// + /// Deal market order based on input strategy, half swap like (amountIn & amountOut) but half book like (bookAmount) + /// + /// + /// + /// + /// + /// + /// Expected amount to buy(real get)/sell from/to book before amm + /// Price limit of bookAmount part + /// + /// + public static bool RouteMarketOrderOutForIn(UInt160 tokenFrom, UInt160 tokenTo, UInt160 sender, BigInteger amountOut, BigInteger amountInMax, BigInteger bookAmount, BigInteger bookPrice, BigInteger deadLine) + { + Assert(amountOut > 0 && amountInMax > 0 && bookAmount >= 0 && bookPrice > 0, "Invalid Parameters"); + Assert(Runtime.CheckWitness(sender), "No Authorization"); + Assert((BigInteger)Runtime.Time <= deadLine, "Exceeded the Deadline"); + + // Market order + if (bookAmount > 0) + { + // Check pair status + var pairKey = GetPairKey(tokenFrom, tokenTo); + Assert(!BookPaused(pairKey), "Book is Paused"); + var book = GetOrderBook(pairKey); + Assert(book.baseToken.IsAddress() && book.quoteToken.IsAddress(), "Invalid Trade Pair"); + + // Check price limit + var isBuy = tokenTo == book.baseToken; + var price = isBuy ? amountInMax * book.quoteScale / amountOut : amountOut * book.quoteScale / amountInMax; + if (bookAmount > 0) Assert((isBuy && price >= bookPrice) || (!isBuy && price <= bookPrice), "BookPrice Beyond Limit"); + + // Deal and record consumed amountIn and satisfied amountOut + if (isBuy) + { + var result = DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, (bookAmount * 1000 + 996) / 997, false); + amountOut -= bookAmount - result[0] * 997 / 1000; + amountInMax -= result[1]; + } + else + { + var result = DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, false); + amountInMax -= bookAmount - result[0]; + amountOut -= result[1]; + } + Assert(amountOut >= 0, "Exceeded AmountOut"); // Should not get more than amountOut + if (amountOut == 0) // No longer need further swap + { + Assert(amountInMax >= 0, "Insufficient AmountIn"); // Should spend no more than amountInMax + return true; + } + } - var amountIn = isBuy ? GetAmountIn(amount, ammReverse[1], ammReverse[0]) : amount; - var amountOut = isBuy ? amount : GetAmountOut(amount, ammReverse[0], ammReverse[1]); + // Swap AMM + var pairContract = GetExchangePairWithAssert(tokenFrom, tokenTo); + var reverse = GetReserves(pairContract, tokenFrom, tokenTo); + var amountIn = GetAmountIn(amountOut, reverse[0], reverse[1]); + Assert(amountIn <= amountInMax, "Insufficient AmountIn"); - if (amountOut > 0) SwapAMM(pairContract, sender, isBuy ? book.quoteToken : book.baseToken, isBuy ? book.baseToken : book.quoteToken, amountIn, amountOut); - Assert(isBuy ? amountIn + quoteAmount <= slippage : amountOut + quoteAmount >= slippage, "Insufficient Slippage"); + if (amountOut > 0) SwapAMM(pairContract, sender, tokenFrom, tokenTo, amountIn, amountOut); return true; } @@ -722,8 +794,8 @@ private static BigInteger[] MatchQuoteInternal(byte[] pairKey, bool isBuy, BigIn /// /// /// - /// Left amount - public static BigInteger DealMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 taker, bool isBuy, BigInteger price, BigInteger amount) + /// Left amount and real payment + public static BigInteger[] DealMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 taker, bool isBuy, BigInteger price, BigInteger amount) { // Check parameters Assert(price > 0 && amount > 0, "Invalid Parameters"); @@ -743,8 +815,8 @@ public static BigInteger DealMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 /// /// /// - /// Left amount - public static BigInteger DealMarketOrder(UInt160 tokenA, UInt160 tokenB, bool isBuy, BigInteger price, BigInteger amount) + /// Left amount and real payment + public static BigInteger[] DealMarketOrder(UInt160 tokenA, UInt160 tokenB, bool isBuy, BigInteger price, BigInteger amount) { // Check parameters Assert(price > 0 && amount > 0, "Invalid Parameters"); @@ -756,16 +828,16 @@ public static BigInteger DealMarketOrder(UInt160 tokenA, UInt160 tokenB, bool is return DealMarketOrderInternal(pairKey, caller, isBuy, price, amount, true); } - private static BigInteger DealMarketOrderInternal(byte[] pairKey, UInt160 taker, bool isBuy, BigInteger price, BigInteger leftAmount, bool shouldRequest) + private static BigInteger[] DealMarketOrderInternal(byte[] pairKey, UInt160 taker, bool isBuy, BigInteger price, BigInteger leftAmount, bool shouldRequest) { // Check if can deal var book = GetOrderBook(pairKey); Assert(book.baseToken.IsAddress() && book.quoteToken.IsAddress(), "Invalid Trade Pair"); var firstID = isBuy ? book.firstSellID : book.firstBuyID; - if (firstID is null) return leftAmount; + if (firstID is null) return new BigInteger[] { leftAmount, 0 }; var firstOrder = GetOrder(firstID); var canDeal = (isBuy && firstOrder.price <= price) || (!isBuy && firstOrder.price >= price); - if (!canDeal) return leftAmount; + if (!canDeal) return new BigInteger[] { leftAmount, 0 }; var me = Runtime.ExecutingScriptHash; var fundAddress = GetFundAddress(); @@ -874,7 +946,7 @@ private static BigInteger DealMarketOrderInternal(byte[] pairKey, UInt160 taker, StageFundFee(book.baseToken, baseFee); StageFundFee(book.quoteToken, quoteFee); - return leftAmount; + return new BigInteger[] { leftAmount, isBuy ? takerPayment : takerReceive };; } /// From 17c43c59c403079c4abc97daf4183cd547793c9b Mon Sep 17 00:00:00 2001 From: txhsl <799498265@qq.com> Date: Thu, 10 Nov 2022 17:54:41 +0800 Subject: [PATCH 13/16] allow DealMarketOrder to accept quote amount --- .../FlamingoSwapOrderBookContract.cs | 232 ++++++------------ 1 file changed, 80 insertions(+), 152 deletions(-) diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs index 8d7b4b3..1a6e1e7 100644 --- a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs @@ -22,9 +22,9 @@ public partial class FlamingoSwapOrderBookContract : SmartContract /// /// /// - /// Total buy(real get)/sell amount of the limit order + /// Total buy(real get)/sell base token amount of the limit order /// Price limit of the order - /// Expected amount to buy(real get)/sell from/to book before amm + /// Expected base token amount to buy(real get)/sell from/to book before amm /// Price limit of bookAmount part /// /// @@ -40,8 +40,8 @@ public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 // Market order var leftAmount = amount; - if (isBuy) leftAmount -= bookAmount > 0 ? bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, (bookAmount * 1000 + 996) / 997, false)[0] * 997 / 1000 : 0; - else leftAmount -= bookAmount > 0 ? bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, false)[0] : 0; + if (isBuy) leftAmount -= bookAmount > 0 ? bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, (bookAmount * 1000 + 996) / 997, true, false)[0] * 997 / 1000 : 0; + else leftAmount -= bookAmount > 0 ? bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, true, false)[0] : 0; if (leftAmount == 0) return null; // Swap AMM @@ -103,25 +103,25 @@ public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 } /// - /// Deal market order based on input strategy, half swap like (amountIn & amountOut) but half book like (bookAmount) + /// Deal market order based on input strategy, settled by base amount or quote amount /// /// /// /// /// /// - /// Expected amount to buy(real get)/sell from/to book before amm + /// Expected amount to buy(real get)/sell from/to book before amm /// Price limit of bookAmount part /// /// - public static bool RouteMarketOrderInForOut(UInt160 tokenFrom, UInt160 tokenTo, UInt160 sender, BigInteger amountIn, BigInteger amountOutMin, BigInteger bookAmount, BigInteger bookPrice, BigInteger deadLine) + public static bool RouteMarketOrderInForOut(UInt160 tokenFrom, UInt160 tokenTo, UInt160 sender, BigInteger amountIn, BigInteger amountOutMin, BigInteger amountToBook, BigInteger bookPrice, BigInteger deadLine) { - Assert(amountIn > 0 && amountOutMin > 0 && bookAmount >= 0 && bookPrice > 0, "Invalid Parameters"); + Assert(amountIn > 0 && amountOutMin > 0 && amountToBook >= 0 && amountIn >= amountToBook && bookPrice > 0, "Invalid Parameters"); Assert(Runtime.CheckWitness(sender), "No Authorization"); Assert((BigInteger)Runtime.Time <= deadLine, "Exceeded the Deadline"); // Deal in order book - if (bookAmount > 0) + if (amountToBook > 0) { // Check pair status var pairKey = GetPairKey(tokenFrom, tokenTo); @@ -135,18 +135,10 @@ public static bool RouteMarketOrderInForOut(UInt160 tokenFrom, UInt160 tokenTo, Assert((isBuy && price >= bookPrice) || (!isBuy && price <= bookPrice), "BookPrice Beyond Limit"); // Deal and record consumed amountIn and satisfied amountOut - if (isBuy) - { - var result = DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, (bookAmount * 1000 + 996) / 997, false); - amountOutMin -= bookAmount - result[0] * 997 / 1000; - amountIn -= result[1]; - } - else - { - var result = DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, false); - amountIn -= bookAmount - result[0]; - amountOutMin -= result[1]; - } + var result = DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, amountToBook, !isBuy, false); + amountOutMin -= result[1]; + amountIn -= result[0]; + Assert(amountIn >= 0, "Exceeded AmountIn"); // Should not spend more than amountIn if (amountIn == 0) // No longer need further swap { @@ -173,18 +165,18 @@ public static bool RouteMarketOrderInForOut(UInt160 tokenFrom, UInt160 tokenTo, /// /// /// - /// Expected amount to buy(real get)/sell from/to book before amm + /// Expected amount to buy(real get)/sell from/to book before amm /// Price limit of bookAmount part /// /// - public static bool RouteMarketOrderOutForIn(UInt160 tokenFrom, UInt160 tokenTo, UInt160 sender, BigInteger amountOut, BigInteger amountInMax, BigInteger bookAmount, BigInteger bookPrice, BigInteger deadLine) + public static bool RouteMarketOrderOutForIn(UInt160 tokenFrom, UInt160 tokenTo, UInt160 sender, BigInteger amountOut, BigInteger amountInMax, BigInteger amountFromBook, BigInteger bookPrice, BigInteger deadLine) { - Assert(amountOut > 0 && amountInMax > 0 && bookAmount >= 0 && bookPrice > 0, "Invalid Parameters"); + Assert(amountOut > 0 && amountInMax > 0 && amountFromBook >= 0 && amountOut >= amountFromBook && bookPrice > 0, "Invalid Parameters"); Assert(Runtime.CheckWitness(sender), "No Authorization"); Assert((BigInteger)Runtime.Time <= deadLine, "Exceeded the Deadline"); // Market order - if (bookAmount > 0) + if (amountFromBook > 0) { // Check pair status var pairKey = GetPairKey(tokenFrom, tokenTo); @@ -195,27 +187,16 @@ public static bool RouteMarketOrderOutForIn(UInt160 tokenFrom, UInt160 tokenTo, // Check price limit var isBuy = tokenTo == book.baseToken; var price = isBuy ? amountInMax * book.quoteScale / amountOut : amountOut * book.quoteScale / amountInMax; - if (bookAmount > 0) Assert((isBuy && price >= bookPrice) || (!isBuy && price <= bookPrice), "BookPrice Beyond Limit"); + Assert((isBuy && price >= bookPrice) || (!isBuy && price <= bookPrice), "BookPrice Beyond Limit"); // Deal and record consumed amountIn and satisfied amountOut - if (isBuy) - { - var result = DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, (bookAmount * 1000 + 996) / 997, false); - amountOut -= bookAmount - result[0] * 997 / 1000; - amountInMax -= result[1]; - } - else - { - var result = DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, false); - amountInMax -= bookAmount - result[0]; - amountOut -= result[1]; - } - Assert(amountOut >= 0, "Exceeded AmountOut"); // Should not get more than amountOut - if (amountOut == 0) // No longer need further swap - { - Assert(amountInMax >= 0, "Insufficient AmountIn"); // Should spend no more than amountInMax - return true; - } + var result = DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, (amountFromBook * 1000 + 996) / 997, isBuy, false); + amountOut -= result[1]; + amountInMax -= result[0]; + + Assert(amountOut >= 0, "Exceeded AmountIn"); // Should not get more than amountOut + Assert(amountInMax >= 0, "Insufficient AmountIn"); // Should spend no more than amountInMax + if (amountOut <= 0) return true; // No longer need further swap } // Swap AMM @@ -692,19 +673,20 @@ private static BigInteger GetTotalTradableInternal(byte[] pairKey, bool isBuy, B /// /// /// - /// + /// + /// Unsatisfied input amount and the amount of another token [Safe] - public static BigInteger[] MatchOrder(UInt160 tokenA, UInt160 tokenB, bool isBuy, BigInteger price, BigInteger amount) + public static BigInteger[] MatchOrder(UInt160 tokenA, UInt160 tokenB, bool isBuy, BigInteger price, BigInteger amount, bool isAmountInBase) { // Check if exist var pairKey = GetPairKey(tokenA, tokenB); if (BookPaused(pairKey)) return new BigInteger[] { amount, 0 }; - return MatchOrderInternal(pairKey, isBuy, price, amount); + return MatchOrderInternal(pairKey, isBuy, price, amount, isAmountInBase); } - private static BigInteger[] MatchOrderInternal(byte[] pairKey, bool isBuy, BigInteger price, BigInteger amount) + private static BigInteger[] MatchOrderInternal(byte[] pairKey, bool isBuy, BigInteger price, BigInteger amount, bool isAmountInBase) { - var totalPayment = BigInteger.Zero; + var result = BigInteger.Zero; var book = GetOrderBook(pairKey); var currentID = isBuy ? book.firstSellID : book.firstBuyID; if (currentID is null) return new BigInteger[] { amount, 0 }; @@ -715,14 +697,18 @@ private static BigInteger[] MatchOrderInternal(byte[] pairKey, bool isBuy, BigIn // Check price if ((isBuy && currentOrder.price > price) || (!isBuy && currentOrder.price < price)) break; - if (currentOrder.amount <= amount) + var quoteAmount = currentOrder.amount * currentOrder.price / book.quoteScale; + var baseAmount = currentOrder.amount; + + if ((isAmountInBase && amount >= baseAmount) || (!isAmountInBase && amount >= quoteAmount)) { - totalPayment += currentOrder.amount * currentOrder.price / book.quoteScale; - amount -= currentOrder.amount; + result += isAmountInBase ? quoteAmount : baseAmount; + amount -= isAmountInBase ? baseAmount : quoteAmount; } else { - totalPayment += amount * currentOrder.price / book.quoteScale; + result += isAmountInBase ? amount * currentOrder.price / book.quoteScale : + isBuy ? amount * book.quoteScale / currentOrder.price : (amount * book.quoteScale + currentOrder.price - 1) / currentOrder.price; amount = 0; } @@ -730,59 +716,7 @@ private static BigInteger[] MatchOrderInternal(byte[] pairKey, bool isBuy, BigIn currentOrder = GetOrder(currentOrder.nextID); } - // A least payment for buyer - if (isBuy && totalPayment == 0) totalPayment += 1; - return new BigInteger[] { amount, totalPayment }; - } - - /// - /// Try to match without real payment - /// - /// - /// - /// - /// - /// - /// Left amount and tradable base - [Safe] - public static BigInteger[] MatchQuote(UInt160 tokenA, UInt160 tokenB, bool isBuy, BigInteger price, BigInteger quoteAmount) - { - var pairKey = GetPairKey(tokenA, tokenB); - if (BookPaused(pairKey)) return new BigInteger[] { quoteAmount, 0 }; - return MatchQuoteInternal(pairKey, isBuy, price, quoteAmount); - } - - private static BigInteger[] MatchQuoteInternal(byte[] pairKey, bool isBuy, BigInteger price, BigInteger quoteAmount) - { - var totalTradable = BigInteger.Zero; - var book = GetOrderBook(pairKey); - var currentID = isBuy ? book.firstSellID : book.firstBuyID; - if (currentID is null) return new BigInteger[] { quoteAmount, 0 }; - var currentOrder = GetOrder(currentID); - - while (quoteAmount > 0) - { - // Check price - if ((isBuy && currentOrder.price > price) || (!isBuy && currentOrder.price < price)) break; - var payment = currentOrder.amount * currentOrder.price / book.quoteScale; - if (payment <= quoteAmount) - { - totalTradable += currentOrder.amount; - quoteAmount -= payment; - } - else - { - // For buyer, real payment <= expected - if (isBuy) totalTradable += quoteAmount * book.quoteScale / currentOrder.price; - // For seller, real payment >= expected - else totalTradable += (quoteAmount * book.quoteScale + currentOrder.price - 1) / currentOrder.price; - quoteAmount = 0; - } - - if (currentOrder.nextID is null) break; - currentOrder = GetOrder(currentOrder.nextID); - } - return new BigInteger[] { quoteAmount, totalTradable }; + return new BigInteger[] { amount, result }; } /// @@ -794,8 +728,9 @@ private static BigInteger[] MatchQuoteInternal(byte[] pairKey, bool isBuy, BigIn /// /// /// - /// Left amount and real payment - public static BigInteger[] DealMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 taker, bool isBuy, BigInteger price, BigInteger amount) + /// + /// Payment and receive + public static BigInteger[] DealMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 taker, bool isBuy, BigInteger price, BigInteger amount, bool isAmountInBase) { // Check parameters Assert(price > 0 && amount > 0, "Invalid Parameters"); @@ -804,7 +739,7 @@ public static BigInteger[] DealMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt1 Assert(Runtime.CheckWitness(taker), "No Authorization"); Assert(ContractManagement.GetContract(taker) == null, "Forbidden"); - return DealMarketOrderInternal(pairKey, taker, isBuy, price, amount, false); + return DealMarketOrderInternal(pairKey, taker, isBuy, price, amount, isAmountInBase, false); } /// @@ -815,8 +750,9 @@ public static BigInteger[] DealMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt1 /// /// /// - /// Left amount and real payment - public static BigInteger[] DealMarketOrder(UInt160 tokenA, UInt160 tokenB, bool isBuy, BigInteger price, BigInteger amount) + /// + /// Payment and receive + public static BigInteger[] DealMarketOrder(UInt160 tokenA, UInt160 tokenB, bool isBuy, BigInteger price, BigInteger amount, bool isAmountInBase) { // Check parameters Assert(price > 0 && amount > 0, "Invalid Parameters"); @@ -825,19 +761,30 @@ public static BigInteger[] DealMarketOrder(UInt160 tokenA, UInt160 tokenB, bool var caller = Runtime.CallingScriptHash; Assert(ContractManagement.GetContract(caller) != null, "Forbidden"); - return DealMarketOrderInternal(pairKey, caller, isBuy, price, amount, true); + return DealMarketOrderInternal(pairKey, caller, isBuy, price, amount, isAmountInBase, true); } - private static BigInteger[] DealMarketOrderInternal(byte[] pairKey, UInt160 taker, bool isBuy, BigInteger price, BigInteger leftAmount, bool shouldRequest) + /// + /// Order execution + /// + /// + /// + /// + /// + /// + /// + /// + /// + private static BigInteger[] DealMarketOrderInternal(byte[] pairKey, UInt160 taker, bool isBuy, BigInteger price, BigInteger amount, bool isAmountInBase, bool shouldRequest) { // Check if can deal var book = GetOrderBook(pairKey); Assert(book.baseToken.IsAddress() && book.quoteToken.IsAddress(), "Invalid Trade Pair"); var firstID = isBuy ? book.firstSellID : book.firstBuyID; - if (firstID is null) return new BigInteger[] { leftAmount, 0 }; + if (firstID is null) return new BigInteger[] { amount, 0 }; var firstOrder = GetOrder(firstID); var canDeal = (isBuy && firstOrder.price <= price) || (!isBuy && firstOrder.price >= price); - if (!canDeal) return new BigInteger[] { leftAmount, 0 }; + if (!canDeal) return new BigInteger[] { amount, 0 }; var me = Runtime.ExecutingScriptHash; var fundAddress = GetFundAddress(); @@ -855,42 +802,37 @@ private static BigInteger[] DealMarketOrderInternal(byte[] pairKey, UInt160 take var currentOrder = GetOrder(currentID); if ((isBuy && currentOrder.price > price) || (!isBuy && currentOrder.price < price)) break; - BigInteger quoteAmount; - BigInteger baseAmount; - BigInteger quotePayment; - BigInteger basePayment; + var quoteAmount = currentOrder.amount * currentOrder.price / book.quoteScale; + var baseAmount = currentOrder.amount; - if (currentOrder.amount <= leftAmount) + if ((isAmountInBase && amount >= baseAmount) || (!isAmountInBase && amount >= quoteAmount)) { // Full-fill - quoteAmount = currentOrder.amount * currentOrder.price / book.quoteScale; - baseAmount = currentOrder.amount; - // Remove full-fill order DeleteOrder(currentID); DeleteReceipt(currentOrder.maker, currentID); firstID = currentOrder.nextID; onOrderStatusChanged(book.baseToken, book.quoteToken, currentID, !isBuy, currentOrder.maker, currentOrder.price, 0); - leftAmount -= currentOrder.amount; + amount -= isAmountInBase ? baseAmount : quoteAmount; } else { // Part-fill - quoteAmount = leftAmount * currentOrder.price / book.quoteScale; - baseAmount = leftAmount; + quoteAmount = isAmountInBase ? amount * currentOrder.price / book.quoteScale : amount; + baseAmount = isAmountInBase ? amount : isBuy ? amount * book.quoteScale / currentOrder.price : (amount * book.quoteScale + currentOrder.price - 1) / currentOrder.price; // Update order - currentOrder.amount -= leftAmount; + currentOrder.amount -= baseAmount; SetOrder(currentID, currentOrder); onOrderStatusChanged(book.baseToken, book.quoteToken, currentID, !isBuy, currentOrder.maker, currentOrder.price, currentOrder.amount); - leftAmount = 0; + amount = 0; } // Record payment - quotePayment = quoteAmount * 997 / 1000; - basePayment = baseAmount * 997 / 1000; + var quotePayment = quoteAmount * 997 / 1000; + var basePayment = baseAmount * 997 / 1000; quoteFee += quoteAmount - quotePayment; baseFee += baseAmount - basePayment; @@ -910,7 +852,7 @@ private static BigInteger[] DealMarketOrderInternal(byte[] pairKey, UInt160 take } // Check if still tradable - if (leftAmount == 0) break; + if (amount == 0) break; currentID = currentOrder.nextID; } @@ -946,7 +888,7 @@ private static BigInteger[] DealMarketOrderInternal(byte[] pairKey, UInt160 take StageFundFee(book.baseToken, baseFee); StageFundFee(book.quoteToken, quoteFee); - return new BigInteger[] { leftAmount, isBuy ? takerPayment : takerReceive };; + return new BigInteger[] { takerPayment, takerReceive }; } /// @@ -1086,16 +1028,9 @@ public static BigInteger[] GetAmountOut(UInt160 tokenFrom, UInt160 tokenTo, BigI if (BookPaused(pairKey)) return new BigInteger[] { amountIn, 0 }; var book = GetOrderBook(pairKey); var isBuy = tokenFrom == book.quoteToken; - if (isBuy) - { - var result = MatchQuoteInternal(pairKey, isBuy, price, amountIn); - return new BigInteger[]{ result[0], result[1] * 997 / 1000 }; // 0.3% fee - } - else - { - var result = MatchOrderInternal(pairKey, isBuy, price, amountIn); - return new BigInteger[]{ result[0], result[1] * 997 / 1000 }; // 0.3% fee - } + + var result = MatchOrderInternal(pairKey, isBuy, price, amountIn, !isBuy); + return new BigInteger[]{ result[0], result[1] * 997 / 1000 }; // 0.3% fee } /// @@ -1114,16 +1049,9 @@ public static BigInteger[] GetAmountIn(UInt160 tokenFrom, UInt160 tokenTo, BigIn if (BookPaused(pairKey)) return new BigInteger[] { amountOut, 0 }; var book = GetOrderBook(pairKey); var isBuy = tokenFrom == book.quoteToken; - if (isBuy) - { - var result = MatchOrderInternal(pairKey, isBuy, price, (amountOut * 1000 + 996) / 997); // 0.3% fee - return new BigInteger[]{ result[0] * 997 / 1000, result[1] }; - } - else - { - var result = MatchQuoteInternal(pairKey, isBuy, price, (amountOut * 1000 + 996) / 997); // 0.3% fee - return new BigInteger[]{ (result[0] * 997 + 999) / 1000, result[1] }; - } + + var result = MatchOrderInternal(pairKey, isBuy, price, (amountOut * 1000 + 996) / 997, isBuy); // 0.3% fee + return new BigInteger[]{ isBuy ? result[0] * 997 / 1000 : (result[0] * 997 + 999) / 1000, result[1] }; } #endregion From 218de7797e673f38cad541bf8145492ee1dbf6d1 Mon Sep 17 00:00:00 2001 From: txhsl <799498265@qq.com> Date: Fri, 11 Nov 2022 15:13:22 +0800 Subject: [PATCH 14/16] bugfix --- .../FlamingoSwapOrderBookContract.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs index 1a6e1e7..a3bb0a0 100644 --- a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs @@ -40,8 +40,8 @@ public static ByteString RouteLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 // Market order var leftAmount = amount; - if (isBuy) leftAmount -= bookAmount > 0 ? bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, (bookAmount * 1000 + 996) / 997, true, false)[0] * 997 / 1000 : 0; - else leftAmount -= bookAmount > 0 ? bookAmount - DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, true, false)[0] : 0; + if (isBuy) leftAmount -= bookAmount > 0 ? DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, (bookAmount * 1000 + 996) / 997, true, false)[1] : 0; + else leftAmount -= bookAmount > 0 ? DealMarketOrderInternal(pairKey, sender, isBuy, bookPrice, bookAmount, true, false)[0] : 0; if (leftAmount == 0) return null; // Swap AMM @@ -781,10 +781,10 @@ private static BigInteger[] DealMarketOrderInternal(byte[] pairKey, UInt160 take var book = GetOrderBook(pairKey); Assert(book.baseToken.IsAddress() && book.quoteToken.IsAddress(), "Invalid Trade Pair"); var firstID = isBuy ? book.firstSellID : book.firstBuyID; - if (firstID is null) return new BigInteger[] { amount, 0 }; + if (firstID is null) return new BigInteger[] { 0, 0 }; var firstOrder = GetOrder(firstID); var canDeal = (isBuy && firstOrder.price <= price) || (!isBuy && firstOrder.price >= price); - if (!canDeal) return new BigInteger[] { amount, 0 }; + if (!canDeal) return new BigInteger[] { 0, 0 }; var me = Runtime.ExecutingScriptHash; var fundAddress = GetFundAddress(); From 2f13598651f1fb6a588751a9a2acd782ec351131 Mon Sep 17 00:00:00 2001 From: txhsl <799498265@qq.com> Date: Fri, 11 Nov 2022 18:20:22 +0800 Subject: [PATCH 15/16] update GetAmountIn --- .../FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs index a3bb0a0..770c6b9 100644 --- a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs @@ -1051,7 +1051,7 @@ public static BigInteger[] GetAmountIn(UInt160 tokenFrom, UInt160 tokenTo, BigIn var isBuy = tokenFrom == book.quoteToken; var result = MatchOrderInternal(pairKey, isBuy, price, (amountOut * 1000 + 996) / 997, isBuy); // 0.3% fee - return new BigInteger[]{ isBuy ? result[0] * 997 / 1000 : (result[0] * 997 + 999) / 1000, result[1] }; + return new BigInteger[]{ (result[0] * 997 + 999) / 1000, result[1] + 1 }; } #endregion From 3af405a4be30d2df0d6f8364069579b0afca4785 Mon Sep 17 00:00:00 2001 From: Hu Shili <799498265@qq.com> Date: Fri, 11 Nov 2022 19:49:22 +0800 Subject: [PATCH 16/16] update GetAmountIn --- .../FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs index 770c6b9..5c8185b 100644 --- a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs @@ -1030,7 +1030,7 @@ public static BigInteger[] GetAmountOut(UInt160 tokenFrom, UInt160 tokenTo, BigI var isBuy = tokenFrom == book.quoteToken; var result = MatchOrderInternal(pairKey, isBuy, price, amountIn, !isBuy); - return new BigInteger[]{ result[0], result[1] * 997 / 1000 }; // 0.3% fee + return new BigInteger[] { result[0], result[1] * 997 / 1000 }; // 0.3% fee } /// @@ -1051,7 +1051,7 @@ public static BigInteger[] GetAmountIn(UInt160 tokenFrom, UInt160 tokenTo, BigIn var isBuy = tokenFrom == book.quoteToken; var result = MatchOrderInternal(pairKey, isBuy, price, (amountOut * 1000 + 996) / 997, isBuy); // 0.3% fee - return new BigInteger[]{ (result[0] * 997 + 999) / 1000, result[1] + 1 }; + return isBuy ? new BigInteger[] { result[0] * 997 / 1000, result[1] + 1 } : new BigInteger[]{ (result[0] * 997 + 999) / 1000, result[1] }; } #endregion