diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Extensions.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Extensions.cs new file mode 100644 index 0000000..b09a7d5 --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Extensions.cs @@ -0,0 +1,30 @@ +using Neo; +using Neo.SmartContract.Framework; +using Neo.SmartContract.Framework.Attributes; +using System.Numerics; + +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..a2ad05c --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Admin.cs @@ -0,0 +1,113 @@ +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("NdDvLrbtqeCVQkaLstAwh3md8SYYwqWRaE", ContractParameterType.Hash160)] + static readonly UInt160 superAdmin = default; + + [InitialValue("0xc0695bdb8a87a40aff33c73ff6349ccc05fa9f01", ContractParameterType.Hash160)] + static readonly UInt160 Factory = default; + + [InitialValue("0xd6abe115ecb75e1fa0b42f5e85934ce8c1ae2893", ContractParameterType.Hash160)] + static readonly UInt160 bNEO = default; + + static readonly uint ORDER_PER_PAGE = 1 << 8; + + private const string AdminKey = nameof(superAdmin); + private const string GASAdminKey = nameof(GASAdminKey); + private const string FundAddresskey = nameof(FundAddresskey); + + private static readonly byte[] OrderCounterKey = new byte[] { 0x00 }; + private static readonly byte[] PageCounterKey = new byte[] { 0x01 }; + private static readonly byte[] BookMapPrefix = new byte[] { 0x02 }; + private static readonly byte[] PageMapPrefix = new byte[] { 0x03 }; + private static readonly byte[] OrderIndexPrefix = new byte[] { 0x04 }; + private static readonly byte[] MakerIndexPrefix = new byte[] { 0x05 }; + private static readonly byte[] OrderMapPrefix = new byte[] { 0x06 }; + + // 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..3c4f74e --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Event.cs @@ -0,0 +1,24 @@ +using Neo; +using Neo.SmartContract.Framework; +using System.ComponentModel; +using System.Numerics; + +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); + } +} 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..ad775ce --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Helper.cs @@ -0,0 +1,151 @@ +using Neo; +using Neo.SmartContract.Framework; +using Neo.SmartContract.Framework.Attributes; +using Neo.SmartContract.Framework.Services; +using System; +using System.Numerics; + +namespace FlamingoSwapOrderBook +{ + public partial class FlamingoSwapOrderBookContract + { + + [OpCode(OpCode.APPEND)] + private static extern void Append(T[] array, T newItem); + + /// + /// 断言 + /// + /// + /// + private static void Assert(bool condition, string message) + { + if (!condition) + { + throw new Exception(message); + } + } + + /// + /// 断言 + /// + /// + /// + /// + private static void Assert(bool condition, string message, params object[] data) + { + if (!condition) + { + throw new Exception(message); + } + } + + /// + /// 查询token缩写 + /// + /// + /// + /// + public static string GetTokenSymbol(UInt160 token) + { + Assert(token.IsValid, "Invalid Address"); + return (string)Contract.Call(token, "symbol", CallFlags.ReadOnly, new object[] {}); + } + + /// + /// 安全查询交易对,查不到立即中断合约执行 + /// + /// + /// + /// + 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; + } + + /// + /// 查询TokenA,TokenB交易对合约的里的持有量并按A、B顺序返回 + /// + /// + /// + /// + /// + 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 }; + } + + /// + /// 查询交易对合约是否抽取fundfee + /// + /// + /// + public static bool HasFundAddress(UInt160 pairContract) + { + return (byte[])Contract.Call(pairContract, "getFundAddress", CallFlags.ReadOnly, new object[] { }) != null; + } + + /// + /// 向资金池转发兑出请求 + /// + /// + /// + /// + /// + 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 from user + /// + /// + /// + /// + /// + /// + /// + 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); + } + } + + /// + /// Transfer NEP-5 tokens from contract + /// + /// + /// + /// + /// + /// + /// + private static void RequestTransfer(UInt160 token, UInt160 from, UInt160 to, BigInteger amount, byte[] data = null) + { + try + { + var balanceBefore = (BigInteger)Contract.Call(token, "balanceOf", CallFlags.ReadOnly, new object[] { to }); + var result = (bool)Contract.Call(from, "approvedTransfer", CallFlags.All, new object[] { token, to, amount, data }); + var balanceAfter = (BigInteger)Contract.Call(token, "balanceOf", CallFlags.ReadOnly, new object[] { 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); + } + } + } +} diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Storage.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Storage.cs new file mode 100644 index 0000000..dea0963 --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.Storage.cs @@ -0,0 +1,228 @@ +using Neo; +using Neo.SmartContract.Framework; +using Neo.SmartContract.Framework.Attributes; +using Neo.SmartContract.Framework.Native; +using Neo.SmartContract.Framework.Services; +using System.Numerics; + +namespace FlamingoSwapOrderBook +{ + public partial class FlamingoSwapOrderBookContract + { + [Safe] + public static BookInfo GetBookInfo(UInt160 tokenA, UInt160 tokenB) + { + return GetBook(GetPairKey(tokenA, tokenB)); + } + + [Safe] + public static LimitOrder GetLimitOrder(ByteString id) + { + var index = GetOrderIndex(id); + return GetOrder(index, id); + } + + [Safe] + public static LimitOrder[] GetOrdersOnPage(UInt160 tokenA, UInt160 tokenB, BigInteger page) + { + var results = new LimitOrder[0]; + var orderMap = new StorageMap(Storage.CurrentReadOnlyContext, OrderMapPrefix); + var prefix = GetBookInfo(tokenA, tokenB).Symbol + page; + var iterator = orderMap.Find(prefix, FindOptions.ValuesOnly | FindOptions.DeserializeValues); + while (iterator.Next()) Append(results, (LimitOrder)iterator.Value); + return results; + } + + [Safe] + public static LimitOrder[] GetOrdersOf(UInt160 maker) + { + var results = new LimitOrder[0]; + var makerIndex = new StorageMap(Storage.CurrentReadOnlyContext, MakerIndexPrefix); + var orderMap = new StorageMap(Storage.CurrentReadOnlyContext, OrderMapPrefix); + var iterator = makerIndex.Find(maker, FindOptions.ValuesOnly); + while (iterator.Next()) Append(results, (LimitOrder)StdLib.Deserialize(orderMap.Get((ByteString)iterator.Value))); + return results; + } + + private static BigInteger GetFirstAvailablePage(byte[] pairKey) + { + var pageCount = GetPageCounter(pairKey); + for (var page = BigInteger.One; page <= pageCount; page++) + { + if (GetPageOccupancy(pairKey, page) < ORDER_PER_PAGE) return page; + } + return pageCount + 1; + } + + private static ByteString AddLimitOrder(byte[] pairKey, string symbol, LimitOrder order) + { + // Get id and page + var id = GetUnusedID(); + var page = GetFirstAvailablePage(pairKey); + order.ID = id; + order.Page = page; + var index = symbol + order.Page; + SetOrderIndex(id, index); + SetMakerIndex(order.Maker + id, index + id); + SetOrder(index, id, order); + + // Update page status + if (page > GetPageCounter(pairKey)) UpdatePageCounter(pairKey, page); + var pageOccupancy = GetPageOccupancy(pairKey, page) + 1; + Assert(pageOccupancy <= ORDER_PER_PAGE, "Using Full Page"); + UpdatePageOccupancy(pairKey, page, pageOccupancy); + return id; + } + + private static void UpdateLimitOrder(ByteString index, LimitOrder order) + { + SetOrder(index, order.ID, order); + } + + private static void RemoveLimitOrder(byte[] pairKey, ByteString index, LimitOrder order) + { + // Delete order and index + DeleteOrder(index, order.ID); + DeleteMakerIndex(order.Maker + order.ID); + DeleteOrderIndex(order.ID); + + // Update page status + var pageOccupancy = GetPageOccupancy(pairKey, order.Page) - 1; + Assert(pageOccupancy >= 0, "Invalid Page Occupancy"); + UpdatePageOccupancy(pairKey, order.Page, pageOccupancy); + } + + #region BookMap + private static void SetBook(byte[] pairKey, BookInfo book) + { + var bookMap = new StorageMap(Storage.CurrentContext, BookMapPrefix); + bookMap.Put(pairKey, StdLib.Serialize(book)); + } + + private static BookInfo GetBook(byte[] pairKey) + { + var bookMap = new StorageMap(Storage.CurrentReadOnlyContext, BookMapPrefix); + var bookInfo = bookMap.Get(pairKey); + return bookInfo is null ? new BookInfo() : (BookInfo)StdLib.Deserialize(bookInfo); + } + #endregion + + #region MakerIndex + private static void SetMakerIndex(ByteString index, ByteString orderIndex) + { + var makerIndex = new StorageMap(Storage.CurrentContext, MakerIndexPrefix); + makerIndex.Put(index, orderIndex); + } + + private static ByteString GetMakerIndex(ByteString index) + { + var makerIndex = new StorageMap(Storage.CurrentReadOnlyContext, MakerIndexPrefix); + return makerIndex.Get(index); + } + + private static void DeleteMakerIndex(ByteString index) + { + var makerIndex = new StorageMap(Storage.CurrentContext, MakerIndexPrefix); + makerIndex.Delete(index); + } + #endregion + + #region OrderIndex + private static void SetOrderIndex(ByteString id, ByteString index) + { + var orderIndex = new StorageMap(Storage.CurrentContext, OrderIndexPrefix); + orderIndex.Put(id, index); + } + + private static ByteString GetOrderIndex(ByteString id) + { + var orderIndex = new StorageMap(Storage.CurrentReadOnlyContext, OrderIndexPrefix); + return orderIndex.Get(id); + } + + private static void DeleteOrderIndex(ByteString id) + { + var orderIndex = new StorageMap(Storage.CurrentContext, OrderIndexPrefix); + orderIndex.Delete(id); + } + #endregion + + #region OrderMap + private static void SetOrder(ByteString index, ByteString id, LimitOrder order) + { + var orderMap = new StorageMap(Storage.CurrentContext, OrderMapPrefix); + orderMap.Put(index + id, StdLib.Serialize(order)); + } + + private static LimitOrder GetOrder(ByteString index, ByteString id) + { + var orderMap = new StorageMap(Storage.CurrentReadOnlyContext, OrderMapPrefix); + var order = orderMap.Get(index + id); + return order is null ? new LimitOrder() : (LimitOrder)StdLib.Deserialize(order); + } + + private static void DeleteOrder(ByteString index, ByteString id) + { + var orderMap = new StorageMap(Storage.CurrentContext, OrderMapPrefix); + orderMap.Delete(index + id); + } + #endregion + + #region PageMap + private static void UpdatePageOccupancy(byte[] pairKey, BigInteger page, BigInteger amount) + { + var pageMap = new StorageMap(Storage.CurrentContext, PageMapPrefix); + pageMap.Put(pairKey.ToByteString() + page, amount); + } + + private static BigInteger GetPageOccupancy(byte[] pairKey, BigInteger page) + { + var pageMap = new StorageMap(Storage.CurrentReadOnlyContext, PageMapPrefix); + return (BigInteger)pageMap.Get(pairKey.ToByteString() + page); + } + #endregion + + #region PageCounter + private static void UpdatePageCounter(byte[] pairKey, BigInteger count) + { + var counterMap = new StorageMap(Storage.CurrentContext, PageCounterKey); + counterMap.Put(pairKey, count); + } + + private static BigInteger GetPageCounter(byte[] pairKey) + { + var counterMap = new StorageMap(Storage.CurrentReadOnlyContext, PageCounterKey); + return (BigInteger)counterMap.Get(pairKey); + } + #endregion + + #region OrderCounter + /// + /// Find a random number as order ID + /// + /// + private static ByteString GetUnusedID() + { + var context = Storage.CurrentContext; + var counter = Storage.Get(context, OrderCounterKey); + Storage.Put(context, OrderCounterKey, (BigInteger)counter + 1); + var data = (ByteString)Runtime.ExecutingScriptHash; + if (counter is not null) data += counter; + return CryptoLib.Sha256(data); + } + #endregion + + /// + /// 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); + } + } +} \ No newline at end of file diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs new file mode 100644 index 0000000..9f29d98 --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/FlamingoSwapOrderBookContract.cs @@ -0,0 +1,456 @@ +using Neo; +using Neo.SmartContract.Framework; +using Neo.SmartContract.Framework.Attributes; +using Neo.SmartContract.Framework.Native; +using Neo.SmartContract.Framework.Services; +using System.ComponentModel; +using System.Numerics; + +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 + { + public static void CancelOrder(ByteString orderID) + { + // Get order and book info + var index = GetOrderIndex(orderID); + Assert(index is not null, "Order Not Exists"); + var order = GetOrder(index, orderID); + Assert(Runtime.CheckWitness(order.Maker), "No Authorization"); + var me = Runtime.ExecutingScriptHash; + var pairKey = GetPairKey(order.BaseToken, order.QuoteToken); + var bookInfo = GetBook(pairKey); + + // Remove order and index + RemoveLimitOrder(pairKey, index, order); + onOrderStatusChanged(order.BaseToken, order.QuoteToken, orderID, order.IsBuy, order.Maker, order.Price, 0); + + // Do transfer + if (order.IsBuy) SafeTransfer(order.QuoteToken, me, order.Maker, order.LeftAmount * order.Price / bookInfo.QuoteScale); + else SafeTransfer(order.BaseToken, me, order.Maker, order.LeftAmount); + } + + public static BigInteger DealOrder(UInt160 taker, ByteString orderID, BigInteger amount) + { + // Check Parameters + Assert(amount > 0, "Invalid Parameters"); + Assert(Runtime.CheckWitness(taker), "No Authorization"); + + // Get order and book info + var index = GetOrderIndex(orderID); + Assert(index is not null, "Order Not Exists"); + var order = GetOrder(index, orderID); + var pairKey = GetPairKey(order.BaseToken, order.QuoteToken); + var bookInfo = GetBook(pairKey); + + var me = Runtime.ExecutingScriptHash; + var fundAddress = GetFundAddress(); + + var baseAmount = amount > order.LeftAmount ? order.LeftAmount : amount; + var quoteAmount = baseAmount * order.Price / bookInfo.QuoteScale; + var basePayment = baseAmount * 997 / 1000; + var quotePayment = quoteAmount * 997 / 1000; + + if (order.IsBuy) SafeTransfer(order.BaseToken, taker, me, baseAmount); + else SafeTransfer(order.QuoteToken, taker, me, quoteAmount > 0 ? quoteAmount : 1); + + // Update or remove order + if (baseAmount < order.LeftAmount) + { + order.LeftAmount -= baseAmount; + UpdateLimitOrder(index, order); + onOrderStatusChanged(order.BaseToken, order.QuoteToken, orderID, order.IsBuy, order.Maker, order.Price, order.LeftAmount); + } + else + { + RemoveLimitOrder(pairKey, index, order); + onOrderStatusChanged(order.BaseToken, order.QuoteToken, orderID, order.IsBuy, order.Maker, order.Price, 0); + } + + // Transfer + SafeTransfer(order.BaseToken, me, order.IsBuy ? order.Maker : taker, basePayment); + SafeTransfer(order.QuoteToken, me, order.IsBuy ? taker : order.Maker, quotePayment); + + if (fundAddress is not null) + { + SafeTransfer(order.BaseToken, me, fundAddress, baseAmount - basePayment); + SafeTransfer(order.QuoteToken, me, fundAddress, quoteAmount - quotePayment); + } + + return amount - baseAmount; + } + + public static BigInteger[] DealOrders(UInt160 tokenA, UInt160 tokenB, UInt160 taker, bool isBuy, BigInteger amount, ByteString[] orderIDs) + { + // Check Parameters + Assert(amount > 0, "Invalid Parameters"); + Assert(Runtime.CheckWitness(taker), "No Authorization"); + if (orderIDs.Length == 0) return new BigInteger[] { amount, 0 }; + + // Get book info + var pairKey = GetPairKey(tokenA, tokenB); + var bookInfo = GetBook(pairKey); + Assert(bookInfo.BaseToken is not null, "Book Not Exists"); + + var me = Runtime.ExecutingScriptHash; + var fundAddress = GetFundAddress(); + + var takerPayment = BigInteger.Zero; + var takerReceive = BigInteger.Zero; + var baseFee = BigInteger.Zero; + var quoteFee = BigInteger.Zero; + var makerReceive = new Map(); + + foreach (var id in orderIDs) + { + // Get order + var index = GetOrderIndex(id); + if (index is null) continue; + var order = GetOrder(index, id); + Assert(order.BaseToken == bookInfo.BaseToken && order.QuoteToken == bookInfo.QuoteToken && order.IsBuy ^ isBuy, "Invalid Trading"); + + var baseAmount = amount > order.LeftAmount ? order.LeftAmount : amount; + var quoteAmount = baseAmount * order.Price / bookInfo.QuoteScale; + var basePayment = baseAmount * 997 / 1000; + var quotePayment = quoteAmount * 997 / 1000; + baseFee += baseAmount - basePayment; + quoteFee += quoteAmount - quotePayment; + amount -= baseAmount; + + // Record payment + takerPayment += isBuy ? (quoteAmount > 0 ? quoteAmount : 1) : baseAmount; + takerReceive += isBuy ? basePayment : quotePayment; + if (!makerReceive.HasKey(order.Maker)) makerReceive[order.Maker] = 0; + makerReceive[order.Maker] += isBuy ? quotePayment : basePayment; + + // Update or remove order + if (baseAmount < order.LeftAmount) + { + order.LeftAmount -= baseAmount; + UpdateLimitOrder(index, order); + onOrderStatusChanged(order.BaseToken, order.QuoteToken, id, order.IsBuy, order.Maker, order.Price, order.LeftAmount); + } + else + { + RemoveLimitOrder(pairKey, index, order); + onOrderStatusChanged(order.BaseToken, order.QuoteToken, id, order.IsBuy, order.Maker, order.Price, 0); + } + + if (amount <= 0) break; + } + + // Do transfer + SafeTransfer(isBuy ? bookInfo.QuoteToken : bookInfo.BaseToken, taker, me, takerPayment); + SafeTransfer(isBuy ? bookInfo.BaseToken : bookInfo.QuoteToken, me, taker, takerReceive); + foreach (var toAddress in makerReceive.Keys) SafeTransfer(isBuy ? bookInfo.QuoteToken : bookInfo.BaseToken, me, toAddress, makerReceive[toAddress]); + if (fundAddress is not null) + { + SafeTransfer(bookInfo.QuoteToken, me, fundAddress, quoteFee); + SafeTransfer(bookInfo.BaseToken, me, fundAddress, baseFee); + } + + return new BigInteger[] { amount, takerReceive }; + } + + public static ByteString DealLimitOrder(UInt160 tokenA, UInt160 tokenB, UInt160 maker, bool isBuy, BigInteger amount, BigInteger price, ByteString[] orderIDs) + { + // Check Parameters + Assert(price > 0, "Invalid Parameters"); + Assert(ContractManagement.GetContract(maker) == null, "Forbidden"); + + // Orders First + var result = DealOrders(tokenA, tokenB, maker, isBuy, amount, orderIDs); + var leftAmount = result[0]; + + // Then AMM + var pairKey = GetPairKey(tokenA, tokenB); + var bookInfo = GetBook(pairKey); + Assert(bookInfo.BaseToken is not null, "Book Not Exists"); + var pairContract = GetExchangePairWithAssert(tokenA, tokenB); + var hasFundFee = HasFundAddress(pairContract); + var ammReverse = isBuy + ? GetReserves(pairContract, bookInfo.QuoteToken, bookInfo.BaseToken) + : GetReserves(pairContract, bookInfo.BaseToken, bookInfo.QuoteToken); + + // Get amountIn and amountOut + var amountIn = hasFundFee + ? GetAmountInTillPriceWithFundFee(isBuy, price, bookInfo.QuoteScale, ammReverse[0], ammReverse[1]) + : GetAmountInTillPrice(isBuy, price, bookInfo.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]); + } + + // Do swap + if (amountOut > 0) + { + SwapAMM(pairContract, maker, isBuy ? bookInfo.QuoteToken : bookInfo.BaseToken, isBuy ? bookInfo.BaseToken : bookInfo.QuoteToken, amountIn, amountOut); + leftAmount -= isBuy ? amountOut : amountIn; + } + + // Add new limit order + if (leftAmount < bookInfo.MinOrderAmount || leftAmount > bookInfo.MaxOrderAmount) return null; + var me = Runtime.ExecutingScriptHash; + SafeTransfer(isBuy ? bookInfo.QuoteToken : bookInfo.BaseToken, maker, me, isBuy ? leftAmount * price / bookInfo.QuoteScale : leftAmount); + var id = AddLimitOrder(pairKey, bookInfo.Symbol, new LimitOrder(){ + BaseToken = bookInfo.BaseToken, + QuoteToken = bookInfo.QuoteToken, + Time = Runtime.Time, + IsBuy = isBuy, + Maker = maker, + Price = price, + TotalAmount = amount, + LeftAmount = leftAmount + }); + onOrderStatusChanged(bookInfo.BaseToken, bookInfo.QuoteToken, id, isBuy, maker, price, leftAmount); + return id; + } + + public static void DealMarketOrder(UInt160 tokenA, UInt160 tokenB, UInt160 taker, bool isBuy, BigInteger amount, BigInteger amountOutMin, ByteString[] orderIDs) + { + // Check Parameters + Assert(amountOutMin > 0, "Invalid Parameters"); + + // Orders First + var result = DealOrders(tokenA, tokenB, taker, isBuy, amount, orderIDs); + var leftAmount = result[0]; + var receivedPayment = result[1]; + if (leftAmount == 0) return; + + // Then AMM + var bookInfo = GetBookInfo(tokenA, tokenB); + Assert(bookInfo.BaseToken is not null, "Book Not Exists"); + var pairContract = GetExchangePairWithAssert(tokenA, tokenB); + var hasFundFee = HasFundAddress(pairContract); + var ammReverse = GetReserves(pairContract, bookInfo.BaseToken, bookInfo.QuoteToken); + + // Get amountIn and amountOut + var amountIn = isBuy ? GetAmountIn(leftAmount, ammReverse[1], ammReverse[0]) : leftAmount; + var amountOut = isBuy ? leftAmount : GetAmountOut(leftAmount, ammReverse[0], ammReverse[1]); + Assert(amountOut + receivedPayment >= amountOutMin, "Insufficient AmountOut"); + + // Do swap + SwapAMM(pairContract, taker, isBuy ? bookInfo.QuoteToken : bookInfo.BaseToken, isBuy ? bookInfo.BaseToken : bookInfo.QuoteToken, amountIn, amountOut); + } + + /// + /// 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); + var bookInfo = GetBook(pairKey); + Assert(bookInfo.BaseToken is null, "Book Already Exists"); + + SetBook(pairKey, new BookInfo(){ + Symbol = GetTokenSymbol(baseToken) + "/" + GetTokenSymbol(quoteToken), + BaseToken = baseToken, + QuoteToken = quoteToken, + QuoteScale = quoteScale, + MinOrderAmount = minOrderAmount, + MaxOrderAmount = maxOrderAmount, + IsPaused = false + }); + onBookStatusChanged(baseToken, quoteToken, quoteScale, minOrderAmount, maxOrderAmount, false); + } + + /// + /// Set the minimum order amount for addLimitOrder + /// + /// + /// + /// + public static void SetMinOrderAmount(UInt160 tokenA, UInt160 tokenB, BigInteger minOrderAmount) + { + Assert(minOrderAmount > 0, "Invalid Amount Limit"); + Assert(Verify(), "No Authorization"); + + var pairKey = GetPairKey(tokenA, tokenB); + var bookInfo = GetBook(pairKey); + Assert(bookInfo.BaseToken is not null, "Book Not Exists"); + Assert(minOrderAmount <= bookInfo.MaxOrderAmount, "Invalid Amount Limit"); + + bookInfo.MinOrderAmount = minOrderAmount; + SetBook(pairKey, bookInfo); + onBookStatusChanged(bookInfo.BaseToken, bookInfo.QuoteToken, bookInfo.QuoteScale, bookInfo.MinOrderAmount, bookInfo.MaxOrderAmount, bookInfo.IsPaused); + } + + /// + /// Set the maximum trade amount for addLimitOrder + /// + /// + /// + /// + public static void SetMaxOrderAmount(UInt160 tokenA, UInt160 tokenB, BigInteger maxOrderAmount) + { + Assert(maxOrderAmount > 0, "Invalid Amount Limit"); + Assert(Verify(), "No Authorization"); + + var pairKey = GetPairKey(tokenA, tokenB); + var bookInfo = GetBook(pairKey); + Assert(bookInfo.BaseToken is not null, "Book Not Exists"); + Assert(maxOrderAmount >= bookInfo.MinOrderAmount, "Invalid Amount Limit"); + + bookInfo.MaxOrderAmount = maxOrderAmount; + SetBook(pairKey, bookInfo); + onBookStatusChanged(bookInfo.BaseToken, bookInfo.QuoteToken, bookInfo.QuoteScale, bookInfo.MinOrderAmount, bookInfo.MaxOrderAmount, bookInfo.IsPaused); + } + + /// + /// Pause an existing order book + /// + /// + /// + public static void PauseOrderBook(UInt160 tokenA, UInt160 tokenB) + { + Assert(Verify(), "No Authorization"); + + var pairKey = GetPairKey(tokenA, tokenB); + var bookInfo = GetBook(pairKey); + Assert(bookInfo.BaseToken is not null, "Book Not Exists"); + Assert(bookInfo.IsPaused != true, "Already Paused"); + + bookInfo.IsPaused = true; + SetBook(pairKey, bookInfo); + onBookStatusChanged(bookInfo.BaseToken, bookInfo.QuoteToken, bookInfo.QuoteScale, bookInfo.MinOrderAmount, bookInfo.MaxOrderAmount, bookInfo.IsPaused); + } + + /// + /// Resume a paused order book + /// + /// + /// + public static void ResumeOrderBook(UInt160 tokenA, UInt160 tokenB) + { + Assert(Verify(), "No Authorization"); + + var pairKey = GetPairKey(tokenA, tokenB); + var bookInfo = GetBook(pairKey); + Assert(bookInfo.BaseToken is not null, "Book Not Exists"); + Assert(bookInfo.IsPaused == true, "Not Paused"); + + bookInfo.IsPaused = false; + SetBook(pairKey, bookInfo); + onBookStatusChanged(bookInfo.BaseToken, bookInfo.QuoteToken, bookInfo.QuoteScale, bookInfo.MinOrderAmount, bookInfo.MaxOrderAmount, bookInfo.IsPaused); + } + + /// + /// 根据要达到的限价簿价格,计算资金池需要输入的Token量 + /// + /// + /// + /// + /// + /// + public 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; + } + + public 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; + } + + /// + /// 根据输入A获取资金池兑换B的量(扣除千分之三手续费) + /// + /// + /// + /// + /// + public 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; + } + + /// + /// 根据要兑换的输出量B,计算资金池需要输入的A实际量(已计算千分之三手续费) + /// + /// + /// + /// + /// + public 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; + } + + /// + /// 接受nep17 token必备方法 + /// SwapTokenInForTokenOut + /// + /// + /// + /// + public static void OnNEP17Payment(UInt160 from, BigInteger amount, object data) + { + + } + + /// + /// 根据计算好的输入和输出,使用资金池进行兑换 + /// + /// + /// + /// + /// + /// + /// + private static void SwapAMM(UInt160 pairContract, UInt160 sender, UInt160 tokenIn, UInt160 tokenOut, BigInteger amountIn, BigInteger amountOut) + { + //转入tokenIn + SafeTransfer(tokenIn, sender, pairContract, amountIn); + + //判定要转出的是token0还是token1 + BigInteger amount0Out = 0; + BigInteger amount1Out = 0; + if (tokenIn.ToUInteger() < tokenOut.ToUInteger()) amount1Out = amountOut; + else amount0Out = amountOut; + + //转出tokenOut + SwapOut(pairContract, amount0Out, amount1Out, sender); + } + } +} diff --git a/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/BookInfo.cs b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/BookInfo.cs new file mode 100644 index 0000000..e456a04 --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/BookInfo.cs @@ -0,0 +1,16 @@ +using Neo; +using System.Numerics; + +namespace FlamingoSwapOrderBook +{ + public struct BookInfo + { + public string Symbol; + public UInt160 BaseToken; + public UInt160 QuoteToken; + public BigInteger QuoteScale; + public BigInteger MinOrderAmount; + public BigInteger MaxOrderAmount; + public bool IsPaused; + } +} 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..2f80a48 --- /dev/null +++ b/Swap/flamingo-contract-swap/FlamingoSwapOrderBook/Models/LimitOrder.cs @@ -0,0 +1,20 @@ +using Neo; +using Neo.SmartContract.Framework; +using System.Numerics; + +namespace FlamingoSwapOrderBook +{ + public struct LimitOrder + { + 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; + public BigInteger Page; + } +} \ 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..d671745 --- /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..635c94d 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FlamingoSwapOrderBook", "FlamingoSwapOrderBook\FlamingoSwapOrderBook.csproj", "{EEE0CF3F-E6EE-441C-B00B-FCB03FBA4647}" 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 + {EEE0CF3F-E6EE-441C-B00B-FCB03FBA4647}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EEE0CF3F-E6EE-441C-B00B-FCB03FBA4647}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EEE0CF3F-E6EE-441C-B00B-FCB03FBA4647}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EEE0CF3F-E6EE-441C-B00B-FCB03FBA4647}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE