diff --git a/types/utils.go b/types/utils.go index cf542d80..4a8d9f23 100644 --- a/types/utils.go +++ b/types/utils.go @@ -48,3 +48,44 @@ func MustParseRFC3339(s string) time.Time { func DateRangesOverlap(startTimeA, endTimeA, startTimeB, endTimeB time.Time) bool { return startTimeA.Before(endTimeB) && endTimeA.After(startTimeB) } + +// ParseDec is a shortcut for sdk.MustNewDecFromStr. +func ParseDec(s string) sdk.Dec { + return sdk.MustNewDecFromStr(s) +} + +// ParseCoin parses and returns sdk.Coin. +func ParseCoin(s string) sdk.Coin { + coin, err := sdk.ParseCoinNormalized(s) + if err != nil { + panic(err) + } + return coin +} + +// ParseCoins parses and returns sdk.Coins. +func ParseCoins(s string) sdk.Coins { + coins, err := sdk.ParseCoinsNormalized(s) + if err != nil { + panic(err) + } + return coins +} + +// ParseTime parses and returns time.Time in time.RFC3339 format. +func ParseTime(s string) time.Time { + t, err := time.Parse(time.RFC3339, s) + if err != nil { + panic(err) + } + return t +} + +// DecApproxEqual returns true if a and b are approximately equal, +// which means the diff ratio is equal or less than 0.1%. +func DecApproxEqual(a, b sdk.Dec) bool { + if b.GT(a) { + a, b = b, a + } + return a.Sub(b).Quo(a).LTE(sdk.NewDecWithPrec(1, 3)) +} diff --git a/x/liquidity/amm/match.go b/x/liquidity/amm/match.go new file mode 100644 index 00000000..713e86db --- /dev/null +++ b/x/liquidity/amm/match.go @@ -0,0 +1,178 @@ +package amm + +import ( + "sort" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type PriceDirection int + +const ( + PriceStaying PriceDirection = iota + 1 + PriceIncreasing + PriceDecreasing +) + +func InitialMatchPrice(os OrderSource, tickPrec int) (matchPrice sdk.Dec, dir PriceDirection, matchable bool) { + highest, found := os.HighestBuyPrice() + if !found { + return sdk.Dec{}, 0, false + } + lowest, found := os.LowestSellPrice() + if !found { + return sdk.Dec{}, 0, false + } + if highest.LT(lowest) { + return sdk.Dec{}, 0, false + } + + midPrice := highest.Add(lowest).QuoInt64(2) + buyAmt := os.BuyAmountOver(midPrice) + sellAmt := os.SellAmountUnder(midPrice) + switch { + case buyAmt.GT(sellAmt): + dir = PriceIncreasing + case sellAmt.GT(buyAmt): + dir = PriceDecreasing + default: + dir = PriceStaying + } + + switch dir { + case PriceStaying: + matchPrice = RoundPrice(midPrice, tickPrec) + case PriceIncreasing: + matchPrice = DownTick(midPrice, tickPrec) + case PriceDecreasing: + matchPrice = UpTick(midPrice, tickPrec) + } + + return matchPrice, dir, true +} + +func FindMatchPrice(os OrderSource, tickPrec int) (matchPrice sdk.Dec, found bool) { + initialMatchPrice, dir, matchable := InitialMatchPrice(os, tickPrec) + if !matchable { + return sdk.Dec{}, false + } + if dir == PriceStaying { + return initialMatchPrice, true + } + + buyAmtOver := func(i int) sdk.Int { + return os.BuyAmountOver(TickFromIndex(i, tickPrec)) + } + sellAmtUnder := func(i int) sdk.Int { + return os.SellAmountUnder(TickFromIndex(i, tickPrec)) + } + + switch dir { + case PriceIncreasing: + start := TickToIndex(initialMatchPrice, tickPrec) + end := TickToIndex(HighestTick(tickPrec), tickPrec) + i := start + sort.Search(end-start+1, func(i int) bool { + i += start + bg := buyAmtOver(i + 1) + return bg.IsZero() || (bg.LTE(sellAmtUnder(i)) && buyAmtOver(i).GT(sellAmtUnder(i-1))) + }) + if i > end { + i = end + } + return TickFromIndex(i, tickPrec), true + default: // PriceDecreasing + start := TickToIndex(initialMatchPrice, tickPrec) + end := TickToIndex(LowestTick(tickPrec), tickPrec) + i := start - sort.Search(start-end+1, func(i int) bool { + i = start - i + sl := sellAmtUnder(i - 1) + return sl.IsZero() || (buyAmtOver(i+1).LTE(sellAmtUnder(i)) && buyAmtOver(i).GTE(sl)) + }) + if i < end { + i = end + } + return TickFromIndex(i, tickPrec), true + } +} + +func FindLastMatchableOrders(buyOrders, sellOrders []Order, matchPrice sdk.Dec) (lastBuyIdx, lastSellIdx int, lastBuyPartialMatchAmt, lastSellPartialMatchAmt sdk.Int, found bool) { + if len(buyOrders) == 0 || len(sellOrders) == 0 { + return 0, 0, sdk.Int{}, sdk.Int{}, false + } + type Side struct { + orders []Order + totalOpenAmt sdk.Int + i int + partialMatchAmt sdk.Int + } + buySide := &Side{buyOrders, TotalOpenAmount(buyOrders), len(buyOrders) - 1, sdk.Int{}} + sellSide := &Side{sellOrders, TotalOpenAmount(sellOrders), len(sellOrders) - 1, sdk.Int{}} + sides := map[OrderDirection]*Side{ + Buy: buySide, + Sell: sellSide, + } + for { + ok := true + for dir, side := range sides { + i := side.i + order := side.orders[i] + matchAmt := sdk.MinInt(buySide.totalOpenAmt, sellSide.totalOpenAmt) + side.partialMatchAmt = matchAmt.Sub(side.totalOpenAmt.Sub(order.GetOpenAmount())) + if side.totalOpenAmt.Sub(order.GetOpenAmount()).GT(matchAmt) || + (dir == Sell && matchPrice.MulInt(side.partialMatchAmt).TruncateInt().IsZero()) { + if i == 0 { + return + } + side.totalOpenAmt = side.totalOpenAmt.Sub(order.GetOpenAmount()) + side.i-- + ok = false + } + } + if ok { + return buySide.i, sellSide.i, buySide.partialMatchAmt, sellSide.partialMatchAmt, true + } + } +} + +func MatchOrders(buyOrders, sellOrders []Order, matchPrice sdk.Dec) (quoteCoinDust sdk.Int, matched bool) { + bi, si, pmb, pms, found := FindLastMatchableOrders(buyOrders, sellOrders, matchPrice) + if !found { + return sdk.Int{}, false + } + + quoteCoinDust = sdk.ZeroInt() + + for i := 0; i <= bi; i++ { + buyOrder := buyOrders[i] + var receivedBaseCoinAmt sdk.Int + if i < bi { + receivedBaseCoinAmt = buyOrder.GetOpenAmount() + } else { + receivedBaseCoinAmt = pmb + } + paidQuoteCoinAmt := matchPrice.MulInt(receivedBaseCoinAmt).Ceil().TruncateInt() + buyOrder.SetOpenAmount(buyOrder.GetOpenAmount().Sub(receivedBaseCoinAmt)) + buyOrder.DecrRemainingOfferCoin(paidQuoteCoinAmt) + buyOrder.IncrReceivedDemandCoin(receivedBaseCoinAmt) + buyOrder.SetMatched(true) + quoteCoinDust = quoteCoinDust.Add(paidQuoteCoinAmt) + } + + for i := 0; i <= si; i++ { + sellOrder := sellOrders[i] + var paidBaseCoinAmt sdk.Int + if i < si { + paidBaseCoinAmt = sellOrder.GetOpenAmount() + } else { + paidBaseCoinAmt = pms + } + receivedQuoteCoinAmt := matchPrice.MulInt(paidBaseCoinAmt).TruncateInt() + sellOrder.SetOpenAmount(sellOrder.GetOpenAmount().Sub(paidBaseCoinAmt)) + sellOrder.DecrRemainingOfferCoin(paidBaseCoinAmt) + sellOrder.IncrReceivedDemandCoin(receivedQuoteCoinAmt) + sellOrder.SetMatched(true) + quoteCoinDust = quoteCoinDust.Sub(receivedQuoteCoinAmt) + } + + return quoteCoinDust, true +} diff --git a/x/liquidity/amm/match_bench_test.go b/x/liquidity/amm/match_bench_test.go new file mode 100644 index 00000000..6b333727 --- /dev/null +++ b/x/liquidity/amm/match_bench_test.go @@ -0,0 +1,49 @@ +package amm_test + +import ( + "fmt" + "math/big" + "math/rand" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + + squad "github.com/cosmosquad-labs/squad/types" + "github.com/cosmosquad-labs/squad/x/liquidity/amm" +) + +func randInt(r *rand.Rand, min, max sdk.Int) sdk.Int { + return min.Add(sdk.NewIntFromBigInt(new(big.Int).Rand(r, max.Sub(min).BigInt()))) +} + +func randDec(r *rand.Rand, min, max sdk.Dec) sdk.Dec { + return min.Add(sdk.NewDecFromBigIntWithPrec(new(big.Int).Rand(r, max.Sub(min).BigInt()), sdk.Precision)) +} + +func BenchmarkFindMatchPrice(b *testing.B) { + minPrice, maxPrice := squad.ParseDec("0.0000001"), squad.ParseDec("10000000") + minAmt, maxAmt := sdk.NewInt(100), sdk.NewInt(10000000) + minReserveAmt, maxReserveAmt := sdk.NewInt(500), sdk.NewInt(1000000000) + + for seed := int64(0); seed < 5; seed++ { + b.Run(fmt.Sprintf("seed/%d", seed), func(b *testing.B) { + r := rand.New(rand.NewSource(seed)) + ob := amm.NewOrderBook() + for i := 0; i < 10000; i++ { + ob.Add(newOrder(amm.Buy, randDec(r, minPrice, maxPrice), randInt(r, minAmt, maxAmt))) + ob.Add(newOrder(amm.Sell, randDec(r, minPrice, maxPrice), randInt(r, minAmt, maxAmt))) + } + var poolOrderSources []amm.OrderSource + for i := 0; i < 1000; i++ { + rx, ry := randInt(r, minReserveAmt, maxReserveAmt), randInt(r, minReserveAmt, maxReserveAmt) + pool := amm.NewBasicPool(rx, ry, sdk.ZeroInt()) + poolOrderSources = append(poolOrderSources, amm.NewMockPoolOrderSource(pool, "denom1", "denom2")) + } + os := amm.MergeOrderSources(append(poolOrderSources, ob)...) + b.ResetTimer() + for i := 0; i < b.N; i++ { + amm.FindMatchPrice(os, defTickPrec) + } + }) + } +} diff --git a/x/liquidity/amm/order.go b/x/liquidity/amm/order.go new file mode 100644 index 00000000..a32bafc4 --- /dev/null +++ b/x/liquidity/amm/order.go @@ -0,0 +1,118 @@ +package amm + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var _ Order = (*BaseOrder)(nil) + +type OrderDirection int + +const ( + Buy OrderDirection = iota + 1 + Sell +) + +func (dir OrderDirection) String() string { + switch dir { + case Buy: + return "buy" + case Sell: + return "sell" + default: + return fmt.Sprintf("OrderDirection(%d)", dir) + } +} + +type Order interface { + GetDirection() OrderDirection + GetPrice() sdk.Dec + GetAmount() sdk.Int + GetOpenAmount() sdk.Int + SetOpenAmount(amt sdk.Int) Order + GetRemainingOfferCoin() sdk.Coin + DecrRemainingOfferCoin(amt sdk.Int) Order // Decrement remaining offer coin amount + GetReceivedDemandCoin() sdk.Coin + IncrReceivedDemandCoin(amt sdk.Int) Order // Increment received demand coin amount + IsMatched() bool + SetMatched(matched bool) Order +} + +func TotalOpenAmount(orders []Order) sdk.Int { + amt := sdk.ZeroInt() + for _, order := range orders { + amt = amt.Add(order.GetOpenAmount()) + } + return amt +} + +type BaseOrder struct { + Direction OrderDirection + Price sdk.Dec + Amount sdk.Int + OpenAmount sdk.Int + RemainingOfferCoin sdk.Coin + ReceivedDemandCoin sdk.Coin + Matched bool +} + +func NewBaseOrder(dir OrderDirection, price sdk.Dec, amt sdk.Int, offerCoin sdk.Coin, demandCoinDenom string) *BaseOrder { + return &BaseOrder{ + Direction: dir, + Price: price, + Amount: amt, + OpenAmount: amt, + RemainingOfferCoin: offerCoin, + ReceivedDemandCoin: sdk.NewCoin(demandCoinDenom, sdk.ZeroInt()), + } +} + +func (order *BaseOrder) GetDirection() OrderDirection { + return order.Direction +} + +func (order *BaseOrder) GetPrice() sdk.Dec { + return order.Price +} + +func (order *BaseOrder) GetAmount() sdk.Int { + return order.Amount +} + +func (order *BaseOrder) GetOpenAmount() sdk.Int { + return order.OpenAmount +} + +func (order *BaseOrder) SetOpenAmount(amt sdk.Int) Order { + order.OpenAmount = amt + return order +} + +func (order *BaseOrder) GetRemainingOfferCoin() sdk.Coin { + return order.RemainingOfferCoin +} + +func (order *BaseOrder) DecrRemainingOfferCoin(amt sdk.Int) Order { + order.RemainingOfferCoin = order.RemainingOfferCoin.SubAmount(amt) + return order +} + +func (order *BaseOrder) GetReceivedDemandCoin() sdk.Coin { + return order.ReceivedDemandCoin +} + +func (order *BaseOrder) IncrReceivedDemandCoin(amt sdk.Int) Order { + order.ReceivedDemandCoin = order.ReceivedDemandCoin.AddAmount(amt) + return order +} + +func (order *BaseOrder) IsMatched() bool { + return order.Matched +} + +func (order *BaseOrder) SetMatched(matched bool) Order { + order.Matched = matched + return order +} diff --git a/x/liquidity/amm/orderbook.go b/x/liquidity/amm/orderbook.go new file mode 100644 index 00000000..565bf82b --- /dev/null +++ b/x/liquidity/amm/orderbook.go @@ -0,0 +1,172 @@ +package amm + +import ( + "fmt" + "sort" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var _ OrderSource = (*OrderBook)(nil) + +type OrderBook struct { + buys, sells orderBookTicks +} + +func NewOrderBook(orders ...Order) *OrderBook { + ob := &OrderBook{} + for _, order := range orders { + ob.Add(order) + } + return ob +} + +func (ob *OrderBook) Add(orders ...Order) { + for _, order := range orders { + switch order.GetDirection() { + case Buy: + ob.buys.add(order) + case Sell: + ob.sells.add(order) + } + } +} + +func (ob *OrderBook) HighestBuyPrice() (sdk.Dec, bool) { + return ob.buys.highestPrice() +} + +func (ob *OrderBook) LowestSellPrice() (sdk.Dec, bool) { + return ob.sells.lowestPrice() +} + +func (ob *OrderBook) BuyAmountOver(price sdk.Dec) sdk.Int { + return ob.buys.amountOver(price) +} + +func (ob *OrderBook) BuyOrdersOver(price sdk.Dec) []Order { + return ob.buys.ordersOver(price) +} + +func (ob *OrderBook) SellAmountUnder(price sdk.Dec) sdk.Int { + return ob.sells.amountUnder(price) +} + +func (ob *OrderBook) SellOrdersUnder(price sdk.Dec) []Order { + return ob.sells.ordersUnder(price) +} + +type orderBookTicks []*orderBookTick + +func (ticks *orderBookTicks) findPrice(price sdk.Dec) (i int, exact bool) { + i = sort.Search(len(*ticks), func(i int) bool { + return (*ticks)[i].price.LTE(price) + }) + if i < len(*ticks) && (*ticks)[i].price.Equal(price) { + exact = true + } + return +} + +func (ticks *orderBookTicks) add(order Order) { + i, exact := ticks.findPrice(order.GetPrice()) + if exact { + (*ticks)[i].add(order) + } else { + if i < len(*ticks) { + // Insert a new order book tick at index i. + *ticks = append((*ticks)[:i], append([]*orderBookTick{newOrderBookTick(order)}, (*ticks)[i:]...)...) + } else { + // Append a new order book tick at the end. + *ticks = append(*ticks, newOrderBookTick(order)) + } + } +} + +func (ticks *orderBookTicks) highestPrice() (sdk.Dec, bool) { + if len(*ticks) == 0 { + return sdk.Dec{}, false + } + for _, tick := range *ticks { + if TotalOpenAmount(tick.orders).IsPositive() { + return tick.price, true + } + } + return sdk.Dec{}, false +} + +func (ticks *orderBookTicks) lowestPrice() (sdk.Dec, bool) { + if len(*ticks) == 0 { + return sdk.Dec{}, false + } + for i := len(*ticks) - 1; i >= 0; i-- { + if TotalOpenAmount((*ticks)[i].orders).IsPositive() { + return (*ticks)[i].price, true + } + } + return sdk.Dec{}, false +} + +func (ticks *orderBookTicks) amountOver(price sdk.Dec) sdk.Int { + i, exact := ticks.findPrice(price) + if !exact { + i-- + } + amt := sdk.ZeroInt() + for ; i >= 0; i-- { + amt = amt.Add(TotalOpenAmount((*ticks)[i].orders)) + } + return amt +} + +func (ticks *orderBookTicks) amountUnder(price sdk.Dec) sdk.Int { + i, _ := ticks.findPrice(price) + amt := sdk.ZeroInt() + for ; i < len(*ticks); i++ { + amt = amt.Add(TotalOpenAmount((*ticks)[i].orders)) + } + return amt +} + +func (ticks *orderBookTicks) ordersOver(price sdk.Dec) []Order { + i, exact := ticks.findPrice(price) + if !exact { + i-- + } + var orders []Order + for ; i >= 0; i-- { + orders = append(orders, (*ticks)[i].orders...) + } + return orders +} + +func (ticks *orderBookTicks) ordersUnder(price sdk.Dec) []Order { + i, _ := ticks.findPrice(price) + var orders []Order + for ; i < len(*ticks); i++ { + orders = append(orders, (*ticks)[i].orders...) + } + return orders +} + +type orderBookTick struct { + price sdk.Dec + orders []Order +} + +func newOrderBookTick(order Order) *orderBookTick { + return &orderBookTick{ + price: order.GetPrice(), + orders: []Order{order}, + } +} + +func (tick *orderBookTick) add(order Order) { + if !order.GetPrice().Equal(tick.price) { + panic(fmt.Sprintf("order price %q != tick price %q", order.GetPrice(), tick.price)) + } + if first := tick.orders[0]; first.GetDirection() != order.GetDirection() { + panic(fmt.Sprintf("order direction %q != tick direction %q", order.GetDirection(), first.GetDirection())) + } + tick.orders = append(tick.orders, order) +} diff --git a/x/liquidity/amm/orderbook_test.go b/x/liquidity/amm/orderbook_test.go new file mode 100644 index 00000000..9b88de73 --- /dev/null +++ b/x/liquidity/amm/orderbook_test.go @@ -0,0 +1,62 @@ +package amm_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + squad "github.com/cosmosquad-labs/squad/types" + "github.com/cosmosquad-labs/squad/x/liquidity/amm" +) + +func newOrder(dir amm.OrderDirection, price sdk.Dec, amt sdk.Int) *amm.BaseOrder { + return amm.NewBaseOrder(dir, price, amt, sdk.NewCoin("denom1", amm.OfferCoinAmount(dir, price, amt)), "denom2") +} + +func TestOrderBook(t *testing.T) { + ob := amm.NewOrderBook( + newOrder(amm.Buy, squad.ParseDec("10.01"), sdk.NewInt(10000)), + newOrder(amm.Buy, squad.ParseDec("10.00"), sdk.NewInt(10000)), + newOrder(amm.Buy, squad.ParseDec("9.999"), sdk.NewInt(10000)), + newOrder(amm.Sell, squad.ParseDec("9.999"), sdk.NewInt(10000)), + newOrder(amm.Buy, squad.ParseDec("9.998"), sdk.NewInt(10000)), + newOrder(amm.Sell, squad.ParseDec("9.998"), sdk.NewInt(10000)), + newOrder(amm.Sell, squad.ParseDec("9.997"), sdk.NewInt(10000)), + newOrder(amm.Sell, squad.ParseDec("9.996"), sdk.NewInt(10000)), + ) + + highest, found := ob.HighestBuyPrice() + require.True(t, found) + require.True(sdk.DecEq(t, squad.ParseDec("10.01"), highest)) + lowest, found := ob.LowestSellPrice() + require.True(t, found) + require.True(sdk.DecEq(t, squad.ParseDec("9.996"), lowest)) + + for _, tc := range []struct { + price sdk.Dec + expectedBuyAmt int64 + expectedSellAmt int64 + expectedNumBuyOrders int + expectedNumSellOrders int + }{ + {squad.ParseDec("10.02"), 0, 40000, 0, 4}, + {squad.ParseDec("10.01"), 10000, 40000, 1, 4}, + {squad.ParseDec("10.00"), 20000, 40000, 2, 4}, + {squad.ParseDec("9.999"), 30000, 40000, 3, 4}, + {squad.ParseDec("9.998"), 40000, 30000, 4, 3}, + {squad.ParseDec("9.997"), 40000, 20000, 4, 2}, + {squad.ParseDec("9.996"), 40000, 10000, 4, 1}, + {squad.ParseDec("9.995"), 40000, 0, 4, 0}, + } { + t.Run("", func(t *testing.T) { + buyAmt := ob.BuyAmountOver(tc.price) + require.True(sdk.IntEq(t, sdk.NewInt(tc.expectedBuyAmt), buyAmt)) + sellAmt := ob.SellAmountUnder(tc.price) + require.True(sdk.IntEq(t, sdk.NewInt(tc.expectedSellAmt), sellAmt)) + + require.Len(t, ob.BuyOrdersOver(tc.price), tc.expectedNumBuyOrders) + require.Len(t, ob.SellOrdersUnder(tc.price), tc.expectedNumSellOrders) + }) + } +} diff --git a/x/liquidity/amm/ordersource.go b/x/liquidity/amm/ordersource.go new file mode 100644 index 00000000..a728f46b --- /dev/null +++ b/x/liquidity/amm/ordersource.go @@ -0,0 +1,82 @@ +package amm + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var _ OrderSource = (*mergedOrderSource)(nil) + +type OrderView interface { + HighestBuyPrice() (price sdk.Dec, found bool) + LowestSellPrice() (price sdk.Dec, found bool) + BuyAmountOver(price sdk.Dec) sdk.Int // Includes the price + SellAmountUnder(price sdk.Dec) sdk.Int // Includes the price +} + +type OrderSource interface { + OrderView + BuyOrdersOver(price sdk.Dec) []Order // Includes the price + SellOrdersUnder(price sdk.Dec) []Order // Includes the price +} + +type mergedOrderSource struct { + sources []OrderSource +} + +func MergeOrderSources(sources ...OrderSource) OrderSource { + return &mergedOrderSource{sources: sources} +} + +func (os *mergedOrderSource) HighestBuyPrice() (price sdk.Dec, found bool) { + for _, source := range os.sources { + p, f := source.HighestBuyPrice() + if f && (price.IsNil() || p.GT(price)) { + price = p + found = true + } + } + return +} + +func (os *mergedOrderSource) LowestSellPrice() (price sdk.Dec, found bool) { + for _, source := range os.sources { + p, f := source.LowestSellPrice() + if f && (price.IsNil() || p.LT(price)) { + price = p + found = true + } + } + return +} + +func (os *mergedOrderSource) BuyAmountOver(price sdk.Dec) sdk.Int { + amt := sdk.ZeroInt() + for _, source := range os.sources { + amt = amt.Add(source.BuyAmountOver(price)) + } + return amt +} + +func (os *mergedOrderSource) SellAmountUnder(price sdk.Dec) sdk.Int { + amt := sdk.ZeroInt() + for _, source := range os.sources { + amt = amt.Add(source.SellAmountUnder(price)) + } + return amt +} + +func (os *mergedOrderSource) BuyOrdersOver(price sdk.Dec) []Order { + var orders []Order + for _, source := range os.sources { + orders = append(orders, source.BuyOrdersOver(price)...) + } + return orders +} + +func (os *mergedOrderSource) SellOrdersUnder(price sdk.Dec) []Order { + var orders []Order + for _, source := range os.sources { + orders = append(orders, source.SellOrdersUnder(price)...) + } + return orders +} diff --git a/x/liquidity/amm/pool.go b/x/liquidity/amm/pool.go new file mode 100644 index 00000000..cf4aa025 --- /dev/null +++ b/x/liquidity/amm/pool.go @@ -0,0 +1,131 @@ +package amm + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var ( + _ Pool = (*BasicPool)(nil) + _ OrderSource = (*MockPoolOrderSource)(nil) +) + +type Pool interface { + OrderView + Price() sdk.Dec + IsDepleted() bool + Deposit(x, y sdk.Int) (ax, ay, pc sdk.Int) + Withdraw(pc sdk.Int, feeRate sdk.Dec) (x, y sdk.Int) +} + +type BasicPool struct { + rx, ry sdk.Dec + ps sdk.Dec +} + +func NewBasicPool(rx, ry, ps sdk.Int) *BasicPool { + return &BasicPool{ + rx: rx.ToDec(), + ry: ry.ToDec(), + ps: ps.ToDec(), + } +} + +func (pool *BasicPool) Price() sdk.Dec { + if pool.rx.IsZero() || pool.ry.IsZero() { + panic("pool price is not defined for a depleted pool") + } + return pool.rx.Quo(pool.ry) +} + +func (pool *BasicPool) IsDepleted() bool { + return pool.ps.IsZero() || pool.rx.IsZero() || pool.ry.IsZero() +} + +func (pool *BasicPool) Deposit(x, y sdk.Int) (ax, ay, pc sdk.Int) { + // Calculate accepted amount and minting amount. + // Note that we take as many coins as possible(by ceiling numbers) + // from depositor and mint as little coins as possible. + // pc = min(ps * (x / rx), ps * (y / ry)) + pc = sdk.MinDec( + pool.ps.MulTruncate(x.ToDec().QuoTruncate(pool.rx)), + pool.ps.MulTruncate(y.ToDec().QuoTruncate(pool.ry)), + ).TruncateInt() + + mintProportion := pc.ToDec().Quo(pool.ps) // pc / ps + ax = pool.rx.Mul(mintProportion).Ceil().TruncateInt() // rx * mintProportion + ay = pool.ry.Mul(mintProportion).Ceil().TruncateInt() // ry * mintProportion + return +} + +func (pool *BasicPool) Withdraw(pc sdk.Int, feeRate sdk.Dec) (x, y sdk.Int) { + if pc.ToDec().Equal(pool.ps) { + // Redeeming the last pool coin. + x = pool.rx.TruncateInt() + y = pool.ry.TruncateInt() + return + } + + proportion := pc.ToDec().QuoTruncate(pool.ps) // pc / ps + multiplier := sdk.OneDec().Sub(feeRate) // 1 - feeRate + x = pool.rx.MulTruncate(proportion).MulTruncate(multiplier).TruncateInt() // rx * proportion * multiplier + y = pool.ry.MulTruncate(proportion).MulTruncate(multiplier).TruncateInt() // ry * proportion * multiplier + return +} + +func (pool *BasicPool) HighestBuyPrice() (price sdk.Dec, found bool) { + // The highest buy price is actually a bit lower than pool price, + // but it's not important for our matching logic. + return pool.Price(), true +} + +func (pool *BasicPool) LowestSellPrice() (price sdk.Dec, found bool) { + // The lowest sell price is actually a bit higher than the pool price, + // but it's not important for our matching logic. + return pool.Price(), true +} + +func (pool *BasicPool) BuyAmountOver(price sdk.Dec) sdk.Int { + if price.GTE(pool.Price()) { + return sdk.ZeroInt() + } + return pool.rx.QuoTruncate(price).Sub(pool.ry).TruncateInt() +} + +func (pool *BasicPool) SellAmountUnder(price sdk.Dec) sdk.Int { + if price.LTE(pool.Price()) { + return sdk.ZeroInt() + } + return pool.ry.Sub(pool.rx.QuoRoundUp(price)).TruncateInt() +} + +// MockPoolOrderSource demonstrates how to implement a pool OrderSource. +type MockPoolOrderSource struct { + Pool + baseCoinDenom, quoteCoinDenom string +} + +func NewMockPoolOrderSource(pool Pool, baseCoinDenom, quoteCoinDenom string) *MockPoolOrderSource { + return &MockPoolOrderSource{ + Pool: pool, + baseCoinDenom: baseCoinDenom, + quoteCoinDenom: quoteCoinDenom, + } +} + +func (os *MockPoolOrderSource) BuyOrdersOver(price sdk.Dec) []Order { + amt := os.BuyAmountOver(price) + if amt.IsZero() { + return nil + } + quoteCoin := sdk.NewCoin(os.quoteCoinDenom, OfferCoinAmount(Buy, price, amt)) + return []Order{NewBaseOrder(Buy, price, amt, quoteCoin, os.baseCoinDenom)} +} + +func (os *MockPoolOrderSource) SellOrdersUnder(price sdk.Dec) []Order { + amt := os.SellAmountUnder(price) + if amt.IsZero() { + return nil + } + baseCoin := sdk.NewCoin(os.baseCoinDenom, amt) + return []Order{NewBaseOrder(Sell, price, amt, baseCoin, os.quoteCoinDenom)} +} diff --git a/x/liquidity/amm/pool_test.go b/x/liquidity/amm/pool_test.go new file mode 100644 index 00000000..4b751832 --- /dev/null +++ b/x/liquidity/amm/pool_test.go @@ -0,0 +1,326 @@ +package amm_test + +import ( + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + + squad "github.com/cosmosquad-labs/squad/types" + "github.com/cosmosquad-labs/squad/x/liquidity/amm" +) + +func TestBasicPool(t *testing.T) { + r := rand.New(rand.NewSource(0)) + for i := 0; i < 1000; i++ { + rx, ry := sdk.NewInt(1+r.Int63n(100000000)), sdk.NewInt(1+r.Int63n(100000000)) + pool := amm.NewBasicPool(rx, ry, sdk.ZeroInt()) + + highest, found := pool.HighestBuyPrice() + require.True(t, found) + require.True(sdk.DecEq(t, pool.Price(), highest)) + lowest, found := pool.LowestSellPrice() + require.True(t, found) + require.True(sdk.DecEq(t, pool.Price(), lowest)) + + lowest = amm.LowestTick(defTickPrec) + buyAmt := pool.BuyAmountOver(lowest) + expected := rx.ToDec().QuoRoundUp(lowest) + require.True(t, squad.DecApproxEqual(expected, buyAmt.ToDec())) + highest = amm.HighestTick(defTickPrec) + sellAmt := pool.SellAmountUnder(highest) + require.True(t, ry.Sub(sellAmt).LTE(sdk.OneInt())) + } +} + +func TestBasicPool_Price(t *testing.T) { + for _, tc := range []struct { + name string + rx, ry int64 // reserve balance + ps int64 // pool coin supply + p sdk.Dec // expected pool price + }{ + { + name: "normal pool", + ps: 10000, + rx: 20000, + ry: 100, + p: sdk.NewDec(200), + }, + { + name: "decimal rounding", + ps: 10000, + rx: 200, + ry: 300, + p: sdk.MustNewDecFromStr("0.666666666666666667"), + }, + } { + t.Run(tc.name, func(t *testing.T) { + pool := amm.NewBasicPool(sdk.NewInt(tc.rx), sdk.NewInt(tc.ry), sdk.NewInt(tc.ps)) + require.True(sdk.DecEq(t, tc.p, pool.Price())) + }) + } + + // panicking cases + for _, tc := range []struct { + rx, ry int64 + ps int64 + }{ + { + rx: 0, + ry: 1000, + ps: 1000, + }, + { + rx: 1000, + ry: 0, + ps: 1000, + }, + } { + t.Run("panics", func(t *testing.T) { + require.Panics(t, func() { + pool := amm.NewBasicPool(sdk.NewInt(tc.rx), sdk.NewInt(tc.ry), sdk.NewInt(tc.ps)) + pool.Price() + }) + }) + } +} + +func TestBasicPool_IsDepleted(t *testing.T) { + for _, tc := range []struct { + name string + rx, ry int64 // reserve balance + ps int64 // pool coin supply + isDepleted bool + }{ + { + name: "empty pool", + rx: 0, + ry: 0, + ps: 0, + isDepleted: true, + }, + { + name: "depleted, with some coins from outside", + rx: 100, + ry: 0, + ps: 0, + isDepleted: true, + }, + { + name: "depleted, with some coins from outside #2", + rx: 100, + ry: 100, + ps: 0, + isDepleted: true, + }, + { + name: "normal pool", + rx: 10000, + ry: 10000, + ps: 10000, + isDepleted: false, + }, + { + name: "not depleted, but reserve coins are gone", + rx: 0, + ry: 10000, + ps: 10000, + isDepleted: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + pool := amm.NewBasicPool(sdk.NewInt(tc.rx), sdk.NewInt(tc.ry), sdk.NewInt(tc.ps)) + require.Equal(t, tc.isDepleted, pool.IsDepleted()) + }) + } +} + +func TestBasicPool_Deposit(t *testing.T) { + for _, tc := range []struct { + name string + rx, ry int64 // reserve balance + ps int64 // pool coin supply + x, y int64 // depositing coin amount + ax, ay int64 // expected accepted coin amount + pc int64 // expected minted pool coin amount + }{ + // TODO: what if a pool has positive pool coin supply + // but has zero reserve balance? + { + name: "ideal deposit", + rx: 2000, + ry: 100, + ps: 10000, + x: 200, + y: 10, + ax: 200, + ay: 10, + pc: 1000, + }, + { + name: "unbalanced deposit", + rx: 2000, + ry: 100, + ps: 10000, + x: 100, + y: 2000, + ax: 100, + ay: 5, + pc: 500, + }, + { + name: "decimal truncation", + rx: 222, + ry: 333, + ps: 333, + x: 100, + y: 100, + ax: 66, + ay: 99, + pc: 99, + }, + { + name: "decimal truncation #2", + rx: 200, + ry: 300, + ps: 333, + x: 80, + y: 80, + ax: 53, + ay: 80, + pc: 88, + }, + { + name: "zero minting amount", + ps: 100, + rx: 10000, + ry: 10000, + x: 99, + y: 99, + ax: 0, + ay: 0, + pc: 0, + }, + { + name: "tiny minting amount", + rx: 10000, + ry: 10000, + ps: 100, + x: 100, + y: 100, + ax: 100, + ay: 100, + pc: 1, + }, + { + name: "tiny minting amount #2", + rx: 10000, + ry: 10000, + ps: 100, + x: 199, + y: 199, + ax: 100, + ay: 100, + pc: 1, + }, + { + name: "zero minting amount", + rx: 10000, + ry: 10000, + ps: 999, + x: 10, + y: 10, + ax: 0, + ay: 0, + pc: 0, + }, + } { + t.Run(tc.name, func(t *testing.T) { + pool := amm.NewBasicPool(sdk.NewInt(tc.rx), sdk.NewInt(tc.ry), sdk.NewInt(tc.ps)) + ax, ay, pc := pool.Deposit(sdk.NewInt(tc.x), sdk.NewInt(tc.y)) + require.True(sdk.IntEq(t, sdk.NewInt(tc.ax), ax)) + require.True(sdk.IntEq(t, sdk.NewInt(tc.ay), ay)) + require.True(sdk.IntEq(t, sdk.NewInt(tc.pc), pc)) + // Additional assertions + if !pool.IsDepleted() { + require.True(t, (ax.Int64()*tc.ps) >= (pc.Int64()*tc.rx)) // (ax / rx) > (pc / ps) + require.True(t, (ay.Int64()*tc.ps) >= (pc.Int64()*tc.ry)) // (ay / ry) > (pc / ps) + } + }) + } +} + +func TestBasicPool_Withdraw(t *testing.T) { + for _, tc := range []struct { + name string + rx, ry int64 // reserve balance + ps int64 // pool coin supply + pc int64 // redeeming pool coin amount + feeRate sdk.Dec + x, y int64 // withdrawn coin amount + }{ + { + name: "ideal withdraw", + rx: 2000, + ry: 100, + ps: 10000, + pc: 1000, + feeRate: sdk.ZeroDec(), + x: 200, + y: 10, + }, + { + name: "ideal withdraw - with fee", + rx: 2000, + ry: 100, + ps: 10000, + pc: 1000, + feeRate: sdk.MustNewDecFromStr("0.003"), + x: 199, + y: 9, + }, + { + name: "withdraw all", + rx: 123, + ry: 567, + ps: 10, + pc: 10, + feeRate: sdk.MustNewDecFromStr("0.003"), + x: 123, + y: 567, + }, + { + name: "advantageous for pool", + rx: 100, + ry: 100, + ps: 10000, + pc: 99, + feeRate: sdk.ZeroDec(), + x: 0, + y: 0, + }, + { + name: "advantageous for pool", + rx: 10000, + ry: 100, + ps: 10000, + pc: 99, + feeRate: sdk.ZeroDec(), + x: 99, + y: 0, + }, + } { + t.Run(tc.name, func(t *testing.T) { + pool := amm.NewBasicPool(sdk.NewInt(tc.rx), sdk.NewInt(tc.ry), sdk.NewInt(tc.ps)) + x, y := pool.Withdraw(sdk.NewInt(tc.pc), tc.feeRate) + require.True(sdk.IntEq(t, sdk.NewInt(tc.x), x)) + require.True(sdk.IntEq(t, sdk.NewInt(tc.y), y)) + // Additional assertions + require.True(t, (tc.pc*tc.rx) >= (x.Int64()*tc.ps)) + require.True(t, (tc.pc*tc.ry) >= (y.Int64()*tc.ps)) + }) + } +} diff --git a/x/liquidity/types/tick.go b/x/liquidity/amm/tick.go similarity index 77% rename from x/liquidity/types/tick.go rename to x/liquidity/amm/tick.go index e5c5ad43..3ef700ea 100644 --- a/x/liquidity/types/tick.go +++ b/x/liquidity/amm/tick.go @@ -1,4 +1,4 @@ -package types +package amm import ( "math" @@ -44,8 +44,8 @@ func isPow10(x sdk.Dec) bool { return b.Cmp(big.NewInt(1)) == 0 } -// PriceToTick returns the highest price tick under(or equal to) the price. -func PriceToTick(price sdk.Dec, prec int) sdk.Dec { +// PriceToDownTick returns the highest price tick under(or equal to) the price. +func PriceToDownTick(price sdk.Dec, prec int) sdk.Dec { b := price.BigInt() l := char(price) d := int64(l - prec) @@ -57,25 +57,48 @@ func PriceToTick(price sdk.Dec, prec int) sdk.Dec { return sdk.NewDecFromBigIntWithPrec(b, sdk.Precision) } +// PriceToUpTick returns the lowest price tick greater or equal than +// the price. +func PriceToUpTick(price sdk.Dec, prec int) sdk.Dec { + tick := PriceToDownTick(price, prec) + if !tick.Equal(price) { + return UpTick(tick, prec) + } + return tick +} + // UpTick returns the next lowest price tick above the price. -// UpTick guarantees that the price is already fit in ticks. func UpTick(price sdk.Dec, prec int) sdk.Dec { - l := char(price) - return price.Add(pow10(l - prec)) + tick := PriceToDownTick(price, prec) + if tick.Equal(price) { + l := char(price) + return price.Add(pow10(l - prec)) + } + l := char(tick) + return tick.Add(pow10(l - prec)) } // DownTick returns the next highest price tick under the price. -// DownTick guarantees that the price is already fit in ticks. // DownTick doesn't check if the price is the lowest price tick. func DownTick(price sdk.Dec, prec int) sdk.Dec { - l := char(price) - var d sdk.Dec - if isPow10(price) { - d = pow10(l - prec - 1) - } else { - d = pow10(l - prec) + tick := PriceToDownTick(price, prec) + if tick.Equal(price) { + l := char(price) + var d sdk.Dec + if isPow10(price) { + d = pow10(l - prec - 1) + } else { + d = pow10(l - prec) + } + return price.Sub(d) } - return price.Sub(d) + return tick +} + +// HighestTick returns the highest possible price tick. +func HighestTick(prec int) sdk.Dec { + i := new(big.Int).SetBits([]big.Word{0, 0, 0, 0, 0x1000000000000000}) + return PriceToDownTick(sdk.NewDecFromBigIntWithPrec(i, sdk.Precision), prec) } // LowestTick returns the lowest possible price tick. @@ -83,16 +106,6 @@ func LowestTick(prec int) sdk.Dec { return sdk.NewDecWithPrec(1, int64(sdk.Precision-prec)) } -// PriceToUpTick returns the lowest price tick greater or equal than -// the price. -func PriceToUpTick(price sdk.Dec, prec int) sdk.Dec { - tick := PriceToTick(price, prec) - if !tick.Equal(price) { - return UpTick(tick, prec) - } - return tick -} - // TickToIndex returns a tick index for given price. // Tick index 0 means the lowest possible price fit in ticks. func TickToIndex(price sdk.Dec, prec int) int { @@ -130,7 +143,7 @@ func RoundTickIndex(i int) int { // RoundPrice returns rounded price using banker's rounding. func RoundPrice(price sdk.Dec, prec int) sdk.Dec { - tick := PriceToTick(price, prec) + tick := PriceToDownTick(price, prec) if price.Equal(tick) { return price } diff --git a/x/liquidity/types/math_internal_test.go b/x/liquidity/amm/tick_internal_test.go similarity index 98% rename from x/liquidity/types/math_internal_test.go rename to x/liquidity/amm/tick_internal_test.go index 27449b56..3ac5c452 100644 --- a/x/liquidity/types/math_internal_test.go +++ b/x/liquidity/amm/tick_internal_test.go @@ -1,4 +1,4 @@ -package types +package amm import ( "testing" diff --git a/x/liquidity/amm/tick_test.go b/x/liquidity/amm/tick_test.go new file mode 100644 index 00000000..90dd43ff --- /dev/null +++ b/x/liquidity/amm/tick_test.go @@ -0,0 +1,241 @@ +package amm_test + +import ( + "math/big" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + squad "github.com/cosmosquad-labs/squad/types" + "github.com/cosmosquad-labs/squad/x/liquidity/amm" +) + +const defTickPrec = 3 + +func TestPriceToTick(t *testing.T) { + for _, tc := range []struct { + price sdk.Dec + expected sdk.Dec + }{ + {squad.ParseDec("0.000000000000099999"), squad.ParseDec("0.00000000000009999")}, + {squad.ParseDec("1.999999999999999999"), squad.ParseDec("1.999")}, + {squad.ParseDec("99.999999999999999999"), squad.ParseDec("99.99")}, + {squad.ParseDec("100.999999999999999999"), squad.ParseDec("100.9")}, + {squad.ParseDec("9999.999999999999999999"), squad.ParseDec("9999")}, + {squad.ParseDec("10019"), squad.ParseDec("10010")}, + {squad.ParseDec("1000100005"), squad.ParseDec("1000000000")}, + } { + require.True(sdk.DecEq(t, tc.expected, amm.PriceToDownTick(tc.price, defTickPrec))) + } +} + +func TestTick(t *testing.T) { + for _, tc := range []struct { + i int + prec int + expected sdk.Dec + }{ + {0, defTickPrec, sdk.NewDecWithPrec(1, int64(sdk.Precision-defTickPrec))}, + {1, defTickPrec, squad.ParseDec("0.000000000000001001")}, + {8999, defTickPrec, squad.ParseDec("0.000000000000009999")}, + {9000, defTickPrec, squad.ParseDec("0.000000000000010000")}, + {9001, defTickPrec, squad.ParseDec("0.000000000000010010")}, + {17999, defTickPrec, squad.ParseDec("0.000000000000099990")}, + {18000, defTickPrec, squad.ParseDec("0.000000000000100000")}, + {135000, defTickPrec, sdk.NewDec(1)}, + {135001, defTickPrec, squad.ParseDec("1.001")}, + } { + t.Run("", func(t *testing.T) { + res := amm.TickFromIndex(tc.i, tc.prec) + require.True(sdk.DecEq(t, tc.expected, res)) + require.Equal(t, tc.i, amm.TickToIndex(res, tc.prec)) + }) + } +} + +func TestUpTick(t *testing.T) { + for _, tc := range []struct { + price sdk.Dec + prec int + expected sdk.Dec + }{ + {squad.ParseDec("1000000000000000000"), defTickPrec, squad.ParseDec("1001000000000000000")}, + {squad.ParseDec("1000"), defTickPrec, squad.ParseDec("1001")}, + {squad.ParseDec("999.9"), defTickPrec, squad.ParseDec("1000")}, + {squad.ParseDec("999.0"), defTickPrec, squad.ParseDec("999.1")}, + {squad.ParseDec("1.100"), defTickPrec, squad.ParseDec("1.101")}, + {squad.ParseDec("1.000"), defTickPrec, squad.ParseDec("1.001")}, + {squad.ParseDec("0.9999"), defTickPrec, squad.ParseDec("1.000")}, + {squad.ParseDec("0.1000"), defTickPrec, squad.ParseDec("0.1001")}, + {squad.ParseDec("0.09999"), defTickPrec, squad.ParseDec("0.1000")}, + {squad.ParseDec("0.09997"), defTickPrec, squad.ParseDec("0.09998")}, + } { + t.Run("", func(t *testing.T) { + require.True(sdk.DecEq(t, tc.expected, amm.UpTick(tc.price, tc.prec))) + }) + } +} + +func TestDownTick(t *testing.T) { + for _, tc := range []struct { + price sdk.Dec + prec int + expected sdk.Dec + }{ + {squad.ParseDec("1000000000000000000"), defTickPrec, squad.ParseDec("999900000000000000")}, + {squad.ParseDec("10010"), defTickPrec, squad.ParseDec("10000")}, + {squad.ParseDec("100.0"), defTickPrec, squad.ParseDec("99.99")}, + {squad.ParseDec("99.99"), defTickPrec, squad.ParseDec("99.98")}, + {squad.ParseDec("1.000"), defTickPrec, squad.ParseDec("0.9999")}, + {squad.ParseDec("0.9990"), defTickPrec, squad.ParseDec("0.9989")}, + {squad.ParseDec("0.9999"), defTickPrec, squad.ParseDec("0.9998")}, + {squad.ParseDec("0.1"), defTickPrec, squad.ParseDec("0.09999")}, + {squad.ParseDec("0.00000000000001000"), defTickPrec, squad.ParseDec("0.000000000000009999")}, + {squad.ParseDec("0.000000000000001001"), defTickPrec, squad.ParseDec("0.000000000000001000")}, + } { + t.Run("", func(t *testing.T) { + require.True(sdk.DecEq(t, tc.expected, amm.DownTick(tc.price, tc.prec))) + }) + } +} + +func TestHighestTick(t *testing.T) { + for _, tc := range []struct { + prec int + expected string + }{ + {defTickPrec, "133400000000000000000000000000000000000000000000000000000000000000000000000000"}, + {0, "100000000000000000000000000000000000000000000000000000000000000000000000000000"}, + {1, "130000000000000000000000000000000000000000000000000000000000000000000000000000"}, + } { + t.Run("", func(t *testing.T) { + i, ok := new(big.Int).SetString(tc.expected, 10) + require.True(t, ok) + tick := amm.HighestTick(tc.prec) + require.True(sdk.DecEq(t, sdk.NewDecFromBigInt(i), tick)) + require.Panics(t, func() { + amm.UpTick(tick, tc.prec) + }) + }) + } +} + +func TestLowestTick(t *testing.T) { + for _, tc := range []struct { + prec int + expected sdk.Dec + }{ + {0, sdk.NewDecWithPrec(1, 18)}, + {defTickPrec, sdk.NewDecWithPrec(1, 15)}, + } { + t.Run("", func(t *testing.T) { + require.True(sdk.DecEq(t, tc.expected, amm.LowestTick(tc.prec))) + }) + } +} + +func TestPriceToUpTick(t *testing.T) { + for _, tc := range []struct { + price sdk.Dec + prec int + expected sdk.Dec + }{ + {squad.ParseDec("1.0015"), defTickPrec, squad.ParseDec("1.002")}, + {squad.ParseDec("100"), defTickPrec, squad.ParseDec("100")}, + {squad.ParseDec("100.01"), defTickPrec, squad.ParseDec("100.1")}, + {squad.ParseDec("100.099"), defTickPrec, squad.ParseDec("100.1")}, + } { + t.Run("", func(t *testing.T) { + require.True(sdk.DecEq(t, tc.expected, amm.PriceToUpTick(tc.price, tc.prec))) + }) + } +} + +func TestRoundTickIndex(t *testing.T) { + for _, tc := range []struct { + i int + expected int + }{ + {0, 0}, + {1, 2}, + {2, 2}, + {3, 4}, + {4, 4}, + {5, 6}, + {6, 6}, + {7, 8}, + {8, 8}, + {9, 10}, + {10, 10}, + } { + t.Run("", func(t *testing.T) { + require.Equal(t, tc.expected, amm.RoundTickIndex(tc.i)) + }) + } +} + +func TestRoundPrice(t *testing.T) { + for _, tc := range []struct { + price sdk.Dec + prec int + expected sdk.Dec + }{ + {squad.ParseDec("0.000000000000001000"), defTickPrec, squad.ParseDec("0.000000000000001000")}, + {squad.ParseDec("0.000000000000010000"), defTickPrec, squad.ParseDec("0.000000000000010000")}, + {squad.ParseDec("0.000000000000010005"), defTickPrec, squad.ParseDec("0.000000000000010000")}, + {squad.ParseDec("0.000000000000010015"), defTickPrec, squad.ParseDec("0.000000000000010020")}, + {squad.ParseDec("0.000000000000010025"), defTickPrec, squad.ParseDec("0.000000000000010020")}, + {squad.ParseDec("0.000000000000010035"), defTickPrec, squad.ParseDec("0.000000000000010040")}, + {squad.ParseDec("0.000000000000010045"), defTickPrec, squad.ParseDec("0.000000000000010040")}, + {squad.ParseDec("1.0005"), defTickPrec, squad.ParseDec("1.0")}, + {squad.ParseDec("1.0015"), defTickPrec, squad.ParseDec("1.002")}, + {squad.ParseDec("1.0025"), defTickPrec, squad.ParseDec("1.002")}, + {squad.ParseDec("1.0035"), defTickPrec, squad.ParseDec("1.004")}, + } { + t.Run("", func(t *testing.T) { + require.True(sdk.DecEq(t, tc.expected, amm.RoundPrice(tc.price, tc.prec))) + }) + } +} + +func BenchmarkUpTick(b *testing.B) { + b.Run("price fit in ticks", func(b *testing.B) { + price := squad.ParseDec("0.9999") + b.ResetTimer() + for i := 0; i < b.N; i++ { + amm.UpTick(price, defTickPrec) + } + }) + b.Run("price not fit in ticks", func(b *testing.B) { + price := squad.ParseDec("0.99995") + b.ResetTimer() + for i := 0; i < b.N; i++ { + amm.UpTick(price, defTickPrec) + } + }) +} + +func BenchmarkDownTick(b *testing.B) { + b.Run("price fit in ticks", func(b *testing.B) { + price := squad.ParseDec("0.9999") + b.ResetTimer() + for i := 0; i < b.N; i++ { + amm.DownTick(price, defTickPrec) + } + }) + b.Run("price not fit in ticks", func(b *testing.B) { + price := squad.ParseDec("0.99995") + b.ResetTimer() + for i := 0; i < b.N; i++ { + amm.DownTick(price, defTickPrec) + } + }) + b.Run("price at edge", func(b *testing.B) { + price := squad.ParseDec("1") + b.ResetTimer() + for i := 0; i < b.N; i++ { + amm.DownTick(price, defTickPrec) + } + }) +} diff --git a/x/liquidity/amm/util.go b/x/liquidity/amm/util.go new file mode 100644 index 00000000..af4bf0a3 --- /dev/null +++ b/x/liquidity/amm/util.go @@ -0,0 +1,20 @@ +package amm + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// OfferCoinAmount returns the minimum offer coin amount for +// given order direction, price and order amount. +func OfferCoinAmount(dir OrderDirection, price sdk.Dec, amt sdk.Int) sdk.Int { + switch dir { + case Buy: + return price.MulInt(amt).Ceil().TruncateInt() + case Sell: + return amt + default: + panic(fmt.Sprintf("invalid order direction: %s", dir)) + } +} diff --git a/x/liquidity/keeper/batch.go b/x/liquidity/keeper/batch.go index 4aa5318f..49dfb183 100644 --- a/x/liquidity/keeper/batch.go +++ b/x/liquidity/keeper/batch.go @@ -16,7 +16,7 @@ func (k Keeper) ExecuteRequests(ctx sdk.Context) { panic(err) } if err := k.IterateAllSwapRequests(ctx, func(req types.SwapRequest) (stop bool, err error) { - if !req.Status.IsCanceledOrExpired() && !ctx.BlockTime().Before(req.ExpireAt) { // ExpireAt <= BlockTime + if req.Status != types.SwapRequestStatusCompleted && !req.Status.IsCanceledOrExpired() && !ctx.BlockTime().Before(req.ExpireAt) { // ExpireAt <= BlockTime if err := k.FinishSwapRequest(ctx, req, types.SwapRequestStatusExpired); err != nil { return false, err } diff --git a/x/liquidity/keeper/batch_test.go b/x/liquidity/keeper/batch_test.go index 19de20cb..93cce36b 100644 --- a/x/liquidity/keeper/batch_test.go +++ b/x/liquidity/keeper/batch_test.go @@ -2,6 +2,8 @@ package keeper_test import ( _ "github.com/stretchr/testify/suite" + + squad "github.com/cosmosquad-labs/squad/types" ) func (s *KeeperTestSuite) TestDepositWithdraw() { @@ -12,7 +14,7 @@ func (s *KeeperTestSuite) TestDepositWithdraw() { // Create a normal pool creator := s.addr(0) s.createPair(creator, "denom1", "denom2", true) - s.createPool(creator, 1, parseCoins("1000000denom1,1000000denom2"), true) + s.createPool(creator, 1, squad.ParseCoins("1000000denom1,1000000denom2"), true) pool, found := k.GetPool(ctx, 1) s.Require().True(found) @@ -20,7 +22,7 @@ func (s *KeeperTestSuite) TestDepositWithdraw() { // A depositor makes a deposit depositor := s.addr(1) - s.deposit(depositor, pool.Id, parseCoins("500000denom1,500000denom2"), true) + s.deposit(depositor, pool.Id, squad.ParseCoins("500000denom1,500000denom2"), true) s.nextBlock() // The depositor withdraws pool coin diff --git a/x/liquidity/keeper/genesis_test.go b/x/liquidity/keeper/genesis_test.go index 5decd97c..ae2bb249 100644 --- a/x/liquidity/keeper/genesis_test.go +++ b/x/liquidity/keeper/genesis_test.go @@ -1,6 +1,7 @@ package keeper_test import ( + squad "github.com/cosmosquad-labs/squad/types" "github.com/cosmosquad-labs/squad/x/liquidity/types" ) @@ -13,13 +14,13 @@ func (s *KeeperTestSuite) TestDefaultGenesis() { } func (s *KeeperTestSuite) TestImportExportGenesis() { - s.ctx = s.ctx.WithBlockHeight(1).WithBlockTime(parseTime("2022-01-01T00:00:00Z")) + s.ctx = s.ctx.WithBlockHeight(1).WithBlockTime(squad.ParseTime("2022-01-01T00:00:00Z")) k, ctx := s.keeper, s.ctx pair := s.createPair(s.addr(0), "denom1", "denom2", true) - pool := s.createPool(s.addr(0), pair.Id, parseCoins("1000000denom1,1000000denom2"), true) + pool := s.createPool(s.addr(0), pair.Id, squad.ParseCoins("1000000denom1,1000000denom2"), true) - s.deposit(s.addr(1), pool.Id, parseCoins("1000000denom1,1000000denom2"), true) + s.deposit(s.addr(1), pool.Id, squad.ParseCoins("1000000denom1,1000000denom2"), true) s.nextBlock() poolCoin := s.getBalance(s.addr(1), pool.PoolCoinDenom) @@ -27,19 +28,19 @@ func (s *KeeperTestSuite) TestImportExportGenesis() { s.withdraw(s.addr(1), pool.Id, poolCoin) s.nextBlock() - s.buyLimitOrder(s.addr(2), pair.Id, parseDec("1.0"), newInt(10000), 0, true) + s.buyLimitOrder(s.addr(2), pair.Id, squad.ParseDec("1.0"), newInt(10000), 0, true) s.nextBlock() - depositReq := s.deposit(s.addr(3), pool.Id, parseCoins("1000000denom1,1000000denom2"), true) + depositReq := s.deposit(s.addr(3), pool.Id, squad.ParseCoins("1000000denom1,1000000denom2"), true) withdrawReq := s.withdraw(s.addr(1), pool.Id, poolCoin) - swapReq := s.sellLimitOrder(s.addr(3), pair.Id, parseDec("1.0"), newInt(1000), 0, true) + swapReq := s.sellLimitOrder(s.addr(3), pair.Id, squad.ParseDec("1.0"), newInt(1000), 0, true) genState := k.ExportGenesis(ctx) bz := s.app.AppCodec().MustMarshalJSON(genState) s.SetupTest() - s.ctx = s.ctx.WithBlockHeight(1).WithBlockTime(parseTime("2022-01-01T00:00:00Z")) + s.ctx = s.ctx.WithBlockHeight(1).WithBlockTime(squad.ParseTime("2022-01-01T00:00:00Z")) k, ctx = s.keeper, s.ctx var genState2 types.GenesisState diff --git a/x/liquidity/keeper/grpc_query_test.go b/x/liquidity/keeper/grpc_query_test.go index 4da6fe55..fe91a6b7 100644 --- a/x/liquidity/keeper/grpc_query_test.go +++ b/x/liquidity/keeper/grpc_query_test.go @@ -5,6 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" + squad "github.com/cosmosquad-labs/squad/types" "github.com/cosmosquad-labs/squad/x/liquidity" "github.com/cosmosquad-labs/squad/x/liquidity/types" @@ -23,10 +24,10 @@ func (s *KeeperTestSuite) TestGRPCPools() { s.createPair(creator, "denom1", "denom3", true) s.createPair(creator, "denom2", "denom3", true) s.createPair(creator, "denom3", "denom4", true) - s.createPool(creator, 1, parseCoins("1000000denom1,1000000denom2"), true) - s.createPool(creator, 2, parseCoins("5000000denom1,5000000denom3"), true) - s.createPool(creator, 3, parseCoins("3000000denom2,3000000denom3"), true) - pair4 := s.createPool(creator, 4, parseCoins("3000000denom3,3000000denom4"), true) + s.createPool(creator, 1, squad.ParseCoins("1000000denom1,1000000denom2"), true) + s.createPool(creator, 2, squad.ParseCoins("5000000denom1,5000000denom3"), true) + s.createPool(creator, 3, squad.ParseCoins("3000000denom2,3000000denom3"), true) + pair4 := s.createPool(creator, 4, squad.ParseCoins("3000000denom3,3000000denom4"), true) pair4.Disabled = true s.keeper.SetPool(s.ctx, pair4) @@ -107,7 +108,7 @@ func (s *KeeperTestSuite) TestGRPCPools() { func (s *KeeperTestSuite) TestGRPCPool() { creator := s.addr(0) pair := s.createPair(creator, "denom1", "denom2", true) - pool := s.createPool(creator, pair.Id, parseCoins("1000000denom1,1000000denom2"), true) + pool := s.createPool(creator, pair.Id, squad.ParseCoins("1000000denom1,1000000denom2"), true) for _, tc := range []struct { name string @@ -138,7 +139,7 @@ func (s *KeeperTestSuite) TestGRPCPool() { s.Require().Equal(pool.PairId, resp.Pool.PairId) s.Require().Equal(pool.ReserveAddress, resp.Pool.ReserveAddress) s.Require().Equal(pool.PoolCoinDenom, resp.Pool.PoolCoinDenom) - s.Require().Equal(parseCoins("1000000denom1,1000000denom2"), resp.Pool.Balances) + s.Require().Equal(squad.ParseCoins("1000000denom1,1000000denom2"), resp.Pool.Balances) s.Require().Equal(pool.LastDepositRequestId, resp.Pool.LastDepositRequestId) s.Require().Equal(pool.LastWithdrawRequestId, resp.Pool.LastWithdrawRequestId) }, @@ -159,7 +160,7 @@ func (s *KeeperTestSuite) TestGRPCPool() { func (s *KeeperTestSuite) TestGRPCPoolByReserveAddress() { creator := s.addr(0) pair := s.createPair(creator, "denom1", "denom2", true) - pool := s.createPool(creator, pair.Id, parseCoins("2000000denom1,2000000denom2"), true) + pool := s.createPool(creator, pair.Id, squad.ParseCoins("2000000denom1,2000000denom2"), true) for _, tc := range []struct { name string @@ -190,7 +191,7 @@ func (s *KeeperTestSuite) TestGRPCPoolByReserveAddress() { s.Require().Equal(pool.PairId, resp.Pool.PairId) s.Require().Equal(pool.ReserveAddress, resp.Pool.ReserveAddress) s.Require().Equal(pool.PoolCoinDenom, resp.Pool.PoolCoinDenom) - s.Require().Equal(parseCoins("2000000denom1,2000000denom2"), resp.Pool.Balances) + s.Require().Equal(squad.ParseCoins("2000000denom1,2000000denom2"), resp.Pool.Balances) s.Require().Equal(pool.LastDepositRequestId, resp.Pool.LastDepositRequestId) s.Require().Equal(pool.LastWithdrawRequestId, resp.Pool.LastWithdrawRequestId) }, @@ -211,7 +212,7 @@ func (s *KeeperTestSuite) TestGRPCPoolByReserveAddress() { func (s *KeeperTestSuite) TestGRPCPoolByPoolCoinDenom() { creator := s.addr(0) pair := s.createPair(creator, "denom1", "denom2", true) - pool := s.createPool(creator, pair.Id, parseCoins("5000000denom1,5000000denom2"), true) + pool := s.createPool(creator, pair.Id, squad.ParseCoins("5000000denom1,5000000denom2"), true) for _, tc := range []struct { name string @@ -242,7 +243,7 @@ func (s *KeeperTestSuite) TestGRPCPoolByPoolCoinDenom() { s.Require().Equal(pool.PairId, resp.Pool.PairId) s.Require().Equal(pool.ReserveAddress, resp.Pool.ReserveAddress) s.Require().Equal(pool.PoolCoinDenom, resp.Pool.PoolCoinDenom) - s.Require().Equal(parseCoins("5000000denom1,5000000denom2"), resp.Pool.Balances) + s.Require().Equal(squad.ParseCoins("5000000denom1,5000000denom2"), resp.Pool.Balances) s.Require().Equal(pool.LastDepositRequestId, resp.Pool.LastDepositRequestId) s.Require().Equal(pool.LastWithdrawRequestId, resp.Pool.LastWithdrawRequestId) }, @@ -392,13 +393,13 @@ func (s *KeeperTestSuite) TestGRPCPair() { func (s *KeeperTestSuite) TestGRPCDepositRequests() { creator := s.addr(0) pair := s.createPair(creator, "denom1", "denom2", true) - pool := s.createPool(creator, pair.Id, parseCoins("5000000denom1,5000000denom2"), true) + pool := s.createPool(creator, pair.Id, squad.ParseCoins("5000000denom1,5000000denom2"), true) depositor := s.addr(1) - s.deposit(depositor, pool.Id, parseCoins("250000denom1,250000denom2"), true) - s.deposit(depositor, pool.Id, parseCoins("250000denom1,250000denom2"), true) - s.deposit(depositor, pool.Id, parseCoins("250000denom1,250000denom2"), true) - s.deposit(depositor, pool.Id, parseCoins("250000denom1,250000denom2"), true) + s.deposit(depositor, pool.Id, squad.ParseCoins("250000denom1,250000denom2"), true) + s.deposit(depositor, pool.Id, squad.ParseCoins("250000denom1,250000denom2"), true) + s.deposit(depositor, pool.Id, squad.ParseCoins("250000denom1,250000denom2"), true) + s.deposit(depositor, pool.Id, squad.ParseCoins("250000denom1,250000denom2"), true) liquidity.EndBlocker(s.ctx, s.keeper) for _, tc := range []struct { @@ -455,10 +456,10 @@ func (s *KeeperTestSuite) TestGRPCDepositRequests() { func (s *KeeperTestSuite) TestGRPCDepositRequest() { creator := s.addr(0) pair := s.createPair(creator, "denom1", "denom2", true) - pool := s.createPool(creator, pair.Id, parseCoins("5000000denom1,5000000denom2"), true) + pool := s.createPool(creator, pair.Id, squad.ParseCoins("5000000denom1,5000000denom2"), true) depositor := s.addr(1) - req := s.deposit(depositor, pool.Id, parseCoins("250000denom1,250000denom2"), true) + req := s.deposit(depositor, pool.Id, squad.ParseCoins("250000denom1,250000denom2"), true) liquidity.EndBlocker(s.ctx, s.keeper) for _, tc := range []struct { @@ -523,7 +524,7 @@ func (s *KeeperTestSuite) TestGRPCWithdrawRequests() { creator := s.addr(0) pair := s.createPair(creator, "denom1", "denom2", true) - pool := s.createPool(creator, pair.Id, parseCoins("5000000denom1,5000000denom2"), true) + pool := s.createPool(creator, pair.Id, squad.ParseCoins("5000000denom1,5000000denom2"), true) poolCoinBalance := s.app.BankKeeper.GetBalance(s.ctx, creator, pool.PoolCoinDenom) s.Require().Equal(params.InitialPoolCoinSupply, poolCoinBalance.Amount) @@ -586,7 +587,7 @@ func (s *KeeperTestSuite) TestGRPCWithdrawRequests() { func (s *KeeperTestSuite) TestGRPCWithdrawRequest() { creator := s.addr(0) pair := s.createPair(creator, "denom1", "denom2", true) - pool := s.createPool(creator, pair.Id, parseCoins("5000000denom1,5000000denom2"), true) + pool := s.createPool(creator, pair.Id, squad.ParseCoins("5000000denom1,5000000denom2"), true) req := s.withdraw(creator, pool.Id, sdk.NewInt64Coin(pool.PoolCoinDenom, 50000)) liquidity.EndBlocker(s.ctx, s.keeper) @@ -650,11 +651,11 @@ func (s *KeeperTestSuite) TestGRPCSwapRequests() { creator := s.addr(0) pair := s.createPair(creator, "denom1", "denom2", true) - s.buyLimitOrder(s.addr(1), pair.Id, parseDec("1.0"), sdk.NewInt(1000000), 10*time.Second, true) - s.buyLimitOrder(s.addr(1), pair.Id, parseDec("1.0"), sdk.NewInt(5000000), 10*time.Second, true) - s.sellLimitOrder(s.addr(2), pair.Id, parseDec("1.0"), newInt(10000), time.Hour, true) - s.sellLimitOrder(s.addr(2), pair.Id, parseDec("1.0"), newInt(700000), time.Hour, true) - s.buyLimitOrder(s.addr(2), pair.Id, parseDec("1.0"), sdk.NewInt(1000000), 10*time.Second, true) + s.buyLimitOrder(s.addr(1), pair.Id, squad.ParseDec("1.0"), sdk.NewInt(1000000), 10*time.Second, true) + s.buyLimitOrder(s.addr(1), pair.Id, squad.ParseDec("1.0"), sdk.NewInt(5000000), 10*time.Second, true) + s.sellLimitOrder(s.addr(2), pair.Id, squad.ParseDec("1.0"), newInt(10000), time.Hour, true) + s.sellLimitOrder(s.addr(2), pair.Id, squad.ParseDec("1.0"), newInt(700000), time.Hour, true) + s.buyLimitOrder(s.addr(2), pair.Id, squad.ParseDec("1.0"), sdk.NewInt(1000000), 10*time.Second, true) liquidity.EndBlocker(s.ctx, s.keeper) for _, tc := range []struct { @@ -701,7 +702,7 @@ func (s *KeeperTestSuite) TestGRPCSwapRequest() { creator := s.addr(0) pair := s.createPair(creator, "denom1", "denom2", true) - req := s.buyLimitOrder(s.addr(1), pair.Id, parseDec("1.0"), sdk.NewInt(1000000), 10*time.Second, true) + req := s.buyLimitOrder(s.addr(1), pair.Id, squad.ParseDec("1.0"), sdk.NewInt(1000000), 10*time.Second, true) liquidity.EndBlocker(s.ctx, s.keeper) for _, tc := range []struct { diff --git a/x/liquidity/keeper/invariants_test.go b/x/liquidity/keeper/invariants_test.go index f39cb53c..00ccf6ea 100644 --- a/x/liquidity/keeper/invariants_test.go +++ b/x/liquidity/keeper/invariants_test.go @@ -1,19 +1,20 @@ package keeper_test import ( + squad "github.com/cosmosquad-labs/squad/types" "github.com/cosmosquad-labs/squad/x/liquidity/keeper" ) func (s *KeeperTestSuite) TestDepositCoinsEscrowInvariant() { pair := s.createPair(s.addr(0), "denom1", "denom2", true) - pool := s.createPool(s.addr(0), pair.Id, parseCoins("1000000denom1,1000000denom2"), true) + pool := s.createPool(s.addr(0), pair.Id, squad.ParseCoins("1000000denom1,1000000denom2"), true) - req := s.deposit(s.addr(1), pool.Id, parseCoins("1000000denom1,1000000denom2"), true) + req := s.deposit(s.addr(1), pool.Id, squad.ParseCoins("1000000denom1,1000000denom2"), true) _, broken := keeper.DepositCoinsEscrowInvariant(s.keeper)(s.ctx) s.Require().False(broken) oldReq := req - req.DepositCoins = parseCoins("2000000denom1,2000000denom2") + req.DepositCoins = squad.ParseCoins("2000000denom1,2000000denom2") s.keeper.SetDepositRequest(s.ctx, req) _, broken = keeper.DepositCoinsEscrowInvariant(s.keeper)(s.ctx) s.Require().True(broken) @@ -27,17 +28,17 @@ func (s *KeeperTestSuite) TestDepositCoinsEscrowInvariant() { func (s *KeeperTestSuite) TestPoolCoinEscrowInvariant() { pair := s.createPair(s.addr(0), "denom1", "denom2", true) - pool := s.createPool(s.addr(0), pair.Id, parseCoins("1000000denom1,1000000denom2"), true) + pool := s.createPool(s.addr(0), pair.Id, squad.ParseCoins("1000000denom1,1000000denom2"), true) - s.deposit(s.addr(1), pool.Id, parseCoins("1000000denom1,1000000denom2"), true) + s.deposit(s.addr(1), pool.Id, squad.ParseCoins("1000000denom1,1000000denom2"), true) s.nextBlock() - req := s.withdraw(s.addr(1), pool.Id, parseCoin("1000000pool1")) + req := s.withdraw(s.addr(1), pool.Id, squad.ParseCoin("1000000pool1")) _, broken := keeper.PoolCoinEscrowInvariant(s.keeper)(s.ctx) s.Require().False(broken) oldReq := req - req.PoolCoin = parseCoin("2000000pool1") + req.PoolCoin = squad.ParseCoin("2000000pool1") s.keeper.SetWithdrawRequest(s.ctx, req) _, broken = keeper.PoolCoinEscrowInvariant(s.keeper)(s.ctx) s.Require().True(broken) @@ -52,12 +53,12 @@ func (s *KeeperTestSuite) TestPoolCoinEscrowInvariant() { func (s *KeeperTestSuite) TestRemainingOfferCoinEscrowInvariant() { pair := s.createPair(s.addr(0), "denom1", "denom2", true) - req := s.buyLimitOrder(s.addr(1), pair.Id, parseDec("1.0"), newInt(1000000), 0, true) + req := s.buyLimitOrder(s.addr(1), pair.Id, squad.ParseDec("1.0"), newInt(1000000), 0, true) _, broken := keeper.RemainingOfferCoinEscrowInvariant(s.keeper)(s.ctx) s.Require().False(broken) oldReq := req - req.RemainingOfferCoin = parseCoin("2000000denom1") + req.RemainingOfferCoin = squad.ParseCoin("2000000denom1") s.keeper.SetSwapRequest(s.ctx, req) _, broken = keeper.RemainingOfferCoinEscrowInvariant(s.keeper)(s.ctx) s.Require().True(broken) @@ -71,7 +72,7 @@ func (s *KeeperTestSuite) TestRemainingOfferCoinEscrowInvariant() { func (s *KeeperTestSuite) TestPoolStatusInvariant() { pair := s.createPair(s.addr(0), "denom1", "denom2", true) - pool := s.createPool(s.addr(0), pair.Id, parseCoins("1000000denom1,1000000denom2"), true) + pool := s.createPool(s.addr(0), pair.Id, squad.ParseCoins("1000000denom1,1000000denom2"), true) _, broken := keeper.PoolStatusInvariant(s.keeper)(s.ctx) s.Require().False(broken) diff --git a/x/liquidity/keeper/keeper_test.go b/x/liquidity/keeper/keeper_test.go index 891502b1..6f182d16 100644 --- a/x/liquidity/keeper/keeper_test.go +++ b/x/liquidity/keeper/keeper_test.go @@ -158,22 +158,6 @@ func (s *KeeperTestSuite) cancelAllOrders(orderer sdk.AccAddress, pairIds []uint s.Require().NoError(err) } -func parseCoin(s string) sdk.Coin { - coin, err := sdk.ParseCoinNormalized(s) - if err != nil { - panic(err) - } - return coin -} - -func parseCoins(s string) sdk.Coins { - coins, err := sdk.ParseCoinsNormalized(s) - if err != nil { - panic(err) - } - return coins -} - func coinEq(exp, got sdk.Coin) (bool, string, string, string) { return exp.IsEqual(got), "expected:\t%v\ngot:\t\t%v", exp.String(), got.String() } @@ -186,18 +170,6 @@ func decEq(exp, got sdk.Dec) (bool, string, string, string) { return exp.Equal(got), "expected:\t%v\ngot:\t\t%v", exp.String(), got.String() } -func parseDec(s string) sdk.Dec { - return sdk.MustNewDecFromStr(s) -} - func newInt(i int64) sdk.Int { return sdk.NewInt(i) } - -func parseTime(s string) time.Time { - t, err := time.Parse(time.RFC3339, s) - if err != nil { - panic(err) - } - return t -} diff --git a/x/liquidity/keeper/pool.go b/x/liquidity/keeper/pool.go index bd28012d..417399a2 100644 --- a/x/liquidity/keeper/pool.go +++ b/x/liquidity/keeper/pool.go @@ -7,6 +7,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmosquad-labs/squad/x/liquidity/amm" "github.com/cosmosquad-labs/squad/x/liquidity/types" ) @@ -215,20 +216,20 @@ func (k Keeper) ExecuteDepositRequest(ctx sdk.Context, req types.DepositRequest) rx, ry := k.GetPoolBalance(ctx, pool, pair) ps := k.GetPoolCoinSupply(ctx, pool) - poolInfo := types.NewPoolInfo(rx, ry, ps) - if types.IsDepletedPool(poolInfo) { + ammPool := amm.NewBasicPool(rx, ry, ps) + if ammPool.IsDepleted() { k.MarkPoolAsDisabled(ctx, pool) if err := k.FinishDepositRequest(ctx, req, types.RequestStatusFailed); err != nil { - return fmt.Errorf("refund deposit request: %w", err) + return err } return nil } - ax, ay, pc := types.DepositToPool(poolInfo, req.DepositCoins.AmountOf(pair.QuoteCoinDenom), req.DepositCoins.AmountOf(pair.BaseCoinDenom)) + ax, ay, pc := ammPool.Deposit(req.DepositCoins.AmountOf(pair.QuoteCoinDenom), req.DepositCoins.AmountOf(pair.BaseCoinDenom)) if pc.IsZero() { if err := k.FinishDepositRequest(ctx, req, types.RequestStatusFailed); err != nil { - return fmt.Errorf("refund deposit request: %w", err) + return err } return nil } @@ -278,6 +279,7 @@ func (k Keeper) FinishDepositRequest(ctx sdk.Context, req types.DepositRequest, sdk.NewAttribute(types.AttributeKeyPoolId, strconv.FormatUint(req.PoolId, 10)), sdk.NewAttribute(types.AttributeKeyDepositCoins, req.DepositCoins.String()), sdk.NewAttribute(types.AttributeKeyAcceptedCoins, req.AcceptedCoins.String()), + sdk.NewAttribute(types.AttributeKeyRefundedCoins, refundingCoins.String()), sdk.NewAttribute(types.AttributeKeyMintedPoolCoin, req.MintedPoolCoin.String()), sdk.NewAttribute(types.AttributeKeyStatus, req.Status.String()), ), @@ -290,7 +292,9 @@ func (k Keeper) FinishDepositRequest(ctx sdk.Context, req types.DepositRequest, func (k Keeper) ExecuteWithdrawRequest(ctx sdk.Context, req types.WithdrawRequest) error { pool, _ := k.GetPool(ctx, req.PoolId) if pool.Disabled { - k.FinishWithdrawRequest(ctx, req, types.RequestStatusFailed) + if err := k.FinishWithdrawRequest(ctx, req, types.RequestStatusFailed); err != nil { + return err + } return nil } @@ -298,15 +302,23 @@ func (k Keeper) ExecuteWithdrawRequest(ctx sdk.Context, req types.WithdrawReques rx, ry := k.GetPoolBalance(ctx, pool, pair) ps := k.GetPoolCoinSupply(ctx, pool) - poolInfo := types.NewPoolInfo(rx, ry, ps) - if types.IsDepletedPool(poolInfo) { + ammPool := amm.NewBasicPool(rx, ry, ps) + if ammPool.IsDepleted() { k.MarkPoolAsDisabled(ctx, pool) - k.FinishWithdrawRequest(ctx, req, types.RequestStatusFailed) + if err := k.FinishWithdrawRequest(ctx, req, types.RequestStatusFailed); err != nil { + return err + } return nil } params := k.GetParams(ctx) - x, y := types.WithdrawFromPool(poolInfo, req.PoolCoin.Amount, params.WithdrawFeeRate) + x, y := ammPool.Withdraw(req.PoolCoin.Amount, params.WithdrawFeeRate) + if x.IsZero() && y.IsZero() { + if err := k.FinishWithdrawRequest(ctx, req, types.RequestStatusFailed); err != nil { + return err + } + return nil + } withdrawnCoins := sdk.NewCoins(sdk.NewCoin(pair.QuoteCoinDenom, x), sdk.NewCoin(pair.BaseCoinDenom, y)) burningCoins := sdk.NewCoins(req.PoolCoin) @@ -328,11 +340,24 @@ func (k Keeper) ExecuteWithdrawRequest(ctx sdk.Context, req types.WithdrawReques } req.WithdrawnCoins = withdrawnCoins - k.FinishWithdrawRequest(ctx, req, types.RequestStatusSucceeded) + if err := k.FinishWithdrawRequest(ctx, req, types.RequestStatusSucceeded); err != nil { + return err + } return nil } -func (k Keeper) FinishWithdrawRequest(ctx sdk.Context, req types.WithdrawRequest, status types.RequestStatus) { +func (k Keeper) FinishWithdrawRequest(ctx sdk.Context, req types.WithdrawRequest, status types.RequestStatus) error { + if req.Status != types.RequestStatusNotExecuted { // sanity check + return nil + } + + var refundingCoins sdk.Coins + if status == types.RequestStatusFailed { + refundingCoins = sdk.NewCoins(req.PoolCoin) + if err := k.bankKeeper.SendCoins(ctx, types.GlobalEscrowAddress, req.GetWithdrawer(), refundingCoins); err != nil { + return err + } + } req.Status = status k.SetWithdrawRequest(ctx, req) @@ -343,8 +368,11 @@ func (k Keeper) FinishWithdrawRequest(ctx sdk.Context, req types.WithdrawRequest sdk.NewAttribute(types.AttributeKeyWithdrawer, req.Withdrawer), sdk.NewAttribute(types.AttributeKeyPoolId, strconv.FormatUint(req.PoolId, 10)), sdk.NewAttribute(types.AttributeKeyPoolCoin, req.PoolCoin.String()), + sdk.NewAttribute(types.AttributeKeyRefundedCoins, refundingCoins.String()), sdk.NewAttribute(types.AttributeKeyWithdrawnCoins, req.WithdrawnCoins.String()), sdk.NewAttribute(types.AttributeKeyStatus, req.Status.String()), ), }) + + return nil } diff --git a/x/liquidity/keeper/pool_test.go b/x/liquidity/keeper/pool_test.go index 54d9fe15..7ddeb25a 100644 --- a/x/liquidity/keeper/pool_test.go +++ b/x/liquidity/keeper/pool_test.go @@ -4,6 +4,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + squad "github.com/cosmosquad-labs/squad/types" "github.com/cosmosquad-labs/squad/x/liquidity" "github.com/cosmosquad-labs/squad/x/liquidity/types" @@ -18,7 +19,7 @@ func (s *KeeperTestSuite) TestCreatePool() { // Create a normal pool. poolCreator := s.addr(1) - s.createPool(poolCreator, pair.Id, parseCoins("1000000denom1,1000000denom2"), true) + s.createPool(poolCreator, pair.Id, squad.ParseCoins("1000000denom1,1000000denom2"), true) // Check if our pool is set correctly. pool, found := k.GetPool(ctx, 1) @@ -34,7 +35,7 @@ func (s *KeeperTestSuite) TestPoolCreationFee() { pair := s.createPair(s.addr(0), "denom1", "denom2", true) poolCreator := s.addr(1) - depositCoins := parseCoins("1000000denom1,1000000denom2") + depositCoins := squad.ParseCoins("1000000denom1,1000000denom2") s.fundAddr(poolCreator, depositCoins) // The pool creator doesn't have enough balance to pay the pool creation fee. @@ -67,12 +68,12 @@ func (s *KeeperTestSuite) TestCreateSamePool() { pair2 := s.createPair(s.addr(0), "denom2", "denom1", true) // Create a pool with denom1 and denom2. - s.createPool(s.addr(1), pair.Id, parseCoins("1000000denom1,1000000denom2"), true) + s.createPool(s.addr(1), pair.Id, squad.ParseCoins("1000000denom1,1000000denom2"), true) // A user tries to create a pool with same denom pair that already exists, // this will fail. poolCreator := s.addr(2) - depositCoins := parseCoins("1000000denom1,1000000denom2") + depositCoins := squad.ParseCoins("1000000denom1,1000000denom2") params := k.GetParams(ctx) s.fundAddr(poolCreator, depositCoins.Add(params.PoolCreationFee...)) _, err := k.CreatePool(ctx, types.NewMsgCreatePool(poolCreator, pair.Id, depositCoins)) @@ -80,7 +81,7 @@ func (s *KeeperTestSuite) TestCreateSamePool() { // Since the order of denom pair is important, it's ok to create a pool // with reversed denom pair: - s.createPool(poolCreator, pair2.Id, parseCoins("1000000denom2,1000000denom1"), true) + s.createPool(poolCreator, pair2.Id, squad.ParseCoins("1000000denom2,1000000denom1"), true) } func (s *KeeperTestSuite) TestDisabledPool() { @@ -95,7 +96,7 @@ func (s *KeeperTestSuite) TestDisabledPool() { poolCreator := s.addr(1) // Create a pool. - pool := s.createPool(poolCreator, pair.Id, parseCoins("1000000denom1,1000000denom2"), true) + pool := s.createPool(poolCreator, pair.Id, squad.ParseCoins("1000000denom1,1000000denom2"), true) // Send the pool's balances to somewhere else. s.sendCoins(pool.GetReserveAddress(), s.addr(2), s.getBalances(pool.GetReserveAddress())) @@ -107,7 +108,7 @@ func (s *KeeperTestSuite) TestDisabledPool() { s.Require().False(pool.Disabled) // A depositor tries to deposit to the pool. - s.deposit(s.addr(3), pool.Id, parseCoins("1000000denom1,1000000denom2"), true) + s.deposit(s.addr(3), pool.Id, squad.ParseCoins("1000000denom1,1000000denom2"), true) s.nextBlock() // Now, the pool is disabled. @@ -116,7 +117,7 @@ func (s *KeeperTestSuite) TestDisabledPool() { // Here's the second example. // This time, the pool creator withdraws all his coins. - pool = s.createPool(poolCreator, pair2.Id, parseCoins("1000000denom3,1000000denom4"), true) + pool = s.createPool(poolCreator, pair2.Id, squad.ParseCoins("1000000denom3,1000000denom4"), true) s.withdraw(poolCreator, pool.Id, s.getBalance(poolCreator, pool.PoolCoinDenom)) s.nextBlock() @@ -131,14 +132,14 @@ func (s *KeeperTestSuite) TestDepositToDisabledPool() { pair := s.createPair(s.addr(0), "denom1", "denom2", true) // Create a disabled pool by sending the pool's balances to somewhere else. - pool := s.createPool(s.addr(1), pair.Id, parseCoins("1000000denom1,1000000denom2"), true) + pool := s.createPool(s.addr(1), pair.Id, squad.ParseCoins("1000000denom1,1000000denom2"), true) poolReserveAddr := pool.GetReserveAddress() s.sendCoins(poolReserveAddr, s.addr(2), s.getBalances(poolReserveAddr)) // The depositor deposits coins but this will fail because the pool // is treated as disabled. depositor := s.addr(3) - depositCoins := parseCoins("1000000denom1,1000000denom2") + depositCoins := squad.ParseCoins("1000000denom1,1000000denom2") req := s.deposit(depositor, pool.Id, depositCoins, true) err := k.ExecuteDepositRequest(ctx, req) s.Require().NoError(err) @@ -160,7 +161,7 @@ func (s *KeeperTestSuite) TestWithdrawFromDisabledPool() { // Create a disabled pool by sending the pool's balances to somewhere else. poolCreator := s.addr(1) - pool := s.createPool(poolCreator, pair.Id, parseCoins("1000000denom1,1000000denom2"), true) + pool := s.createPool(poolCreator, pair.Id, squad.ParseCoins("1000000denom1,1000000denom2"), true) poolReserveAddr := pool.GetReserveAddress() s.sendCoins(poolReserveAddr, s.addr(1), s.getBalances(poolReserveAddr)) @@ -184,37 +185,37 @@ func (s *KeeperTestSuite) TestCreatePoolAfterDisabled() { // Create a disabled pool. poolCreator := s.addr(1) - pool := s.createPool(poolCreator, pair.Id, parseCoins("1000000denom1,1000000denom2"), true) + pool := s.createPool(poolCreator, pair.Id, squad.ParseCoins("1000000denom1,1000000denom2"), true) s.withdraw(poolCreator, pool.Id, s.getBalance(poolCreator, pool.PoolCoinDenom)) s.nextBlock() // Now a new pool can be created with same denom pair because // all pools with same denom pair are disabled. - s.createPool(s.addr(2), pair.Id, parseCoins("1000000denom1,1000000denom2"), true) + s.createPool(s.addr(2), pair.Id, squad.ParseCoins("1000000denom1,1000000denom2"), true) } func (s *KeeperTestSuite) TestDepositRefund() { pair := s.createPair(s.addr(0), "denom1", "denom2", true) - pool := s.createPool(s.addr(0), pair.Id, parseCoins("1000000denom1,1500000denom2"), true) + pool := s.createPool(s.addr(0), pair.Id, squad.ParseCoins("1000000denom1,1500000denom2"), true) depositor := s.addr(1) - depositCoins := parseCoins("20000denom1,15000denom2") + depositCoins := squad.ParseCoins("20000denom1,15000denom2") s.fundAddr(depositor, depositCoins) req := s.deposit(depositor, pool.Id, depositCoins, false) liquidity.EndBlocker(s.ctx, s.keeper) req, _ = s.keeper.GetDepositRequest(s.ctx, req.PoolId, req.Id) s.Require().Equal(types.RequestStatusSucceeded, req.Status) - s.Require().True(coinEq(parseCoin("10000denom1"), s.getBalance(depositor, "denom1"))) - s.Require().True(coinEq(parseCoin("0denom2"), s.getBalance(depositor, "denom2"))) + s.Require().True(coinEq(squad.ParseCoin("10000denom1"), s.getBalance(depositor, "denom1"))) + s.Require().True(coinEq(squad.ParseCoin("0denom2"), s.getBalance(depositor, "denom2"))) liquidity.BeginBlocker(s.ctx, s.keeper) pair = s.createPair(s.addr(0), "denom2", "denom1", true) - pool = s.createPool(s.addr(0), pair.Id, parseCoins("1000000000denom2,1000000000000000denom1"), true) + pool = s.createPool(s.addr(0), pair.Id, squad.ParseCoins("1000000000denom2,1000000000000000denom1"), true) depositor = s.addr(2) - depositCoins = parseCoins("1denom1,1denom2") + depositCoins = squad.ParseCoins("1denom1,1denom2") s.fundAddr(depositor, depositCoins) req = s.deposit(depositor, pool.Id, depositCoins, false) liquidity.EndBlocker(s.ctx, s.keeper) diff --git a/x/liquidity/keeper/swap.go b/x/liquidity/keeper/swap.go index 7fc1326d..ef682d7d 100644 --- a/x/liquidity/keeper/swap.go +++ b/x/liquidity/keeper/swap.go @@ -8,6 +8,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmosquad-labs/squad/x/liquidity/amm" "github.com/cosmosquad-labs/squad/x/liquidity/types" ) @@ -15,7 +16,7 @@ import ( func (k Keeper) LimitOrder(ctx sdk.Context, msg *types.MsgLimitOrder) (types.SwapRequest, error) { params := k.GetParams(ctx) - if price := types.PriceToTick(msg.Price, int(params.TickPrecision)); !msg.Price.Equal(price) { + if price := amm.PriceToDownTick(msg.Price, int(params.TickPrecision)); !msg.Price.Equal(price) { return types.SwapRequest{}, types.ErrInvalidPriceTick } @@ -96,7 +97,7 @@ func (k Keeper) LimitOrder(ctx sdk.Context, msg *types.MsgLimitOrder) (types.Swa sdk.NewAttribute(types.AttributeKeyRequestId, strconv.FormatUint(req.Id, 10)), sdk.NewAttribute(types.AttributeKeyBatchId, strconv.FormatUint(req.BatchId, 10)), sdk.NewAttribute(types.AttributeKeyExpireAt, req.ExpireAt.Format(time.RFC3339)), - sdk.NewAttribute(types.AttributeKeyRefundedCoin, refundedCoin.String()), + sdk.NewAttribute(types.AttributeKeyRefundedCoins, refundedCoin.String()), ), }) @@ -131,7 +132,7 @@ func (k Keeper) MarketOrder(ctx sdk.Context, msg *types.MsgMarketOrder) (types.S sdkerrors.Wrapf(types.ErrWrongPair, "denom pair (%s, %s) != (%s, %s)", msg.DemandCoinDenom, msg.OfferCoin.Denom, pair.BaseCoinDenom, pair.QuoteCoinDenom) } - price = types.PriceToTick(lastPrice.Mul(sdk.OneDec().Add(params.MaxPriceLimitRatio)), int(params.TickPrecision)) + price = amm.PriceToDownTick(lastPrice.Mul(sdk.OneDec().Add(params.MaxPriceLimitRatio)), int(params.TickPrecision)) offerCoin = sdk.NewCoin(msg.OfferCoin.Denom, price.MulInt(msg.Amount).Ceil().TruncateInt()) case types.SwapDirectionSell: if msg.OfferCoin.Denom != pair.BaseCoinDenom || msg.DemandCoinDenom != pair.QuoteCoinDenom { @@ -139,7 +140,7 @@ func (k Keeper) MarketOrder(ctx sdk.Context, msg *types.MsgMarketOrder) (types.S sdkerrors.Wrapf(types.ErrWrongPair, "denom pair (%s, %s) != (%s, %s)", msg.OfferCoin.Denom, msg.DemandCoinDenom, pair.BaseCoinDenom, pair.QuoteCoinDenom) } - price = types.PriceToUpTick(lastPrice.Mul(sdk.OneDec().Sub(params.MaxPriceLimitRatio)), int(params.TickPrecision)) + price = amm.PriceToUpTick(lastPrice.Mul(sdk.OneDec().Sub(params.MaxPriceLimitRatio)), int(params.TickPrecision)) offerCoin = msg.OfferCoin } if msg.OfferCoin.IsLT(offerCoin) { @@ -168,7 +169,7 @@ func (k Keeper) MarketOrder(ctx sdk.Context, msg *types.MsgMarketOrder) (types.S sdk.NewAttribute(types.AttributeKeyAmount, msg.Amount.String()), sdk.NewAttribute(types.AttributeKeyBatchId, strconv.FormatUint(req.BatchId, 10)), sdk.NewAttribute(types.AttributeKeyExpireAt, req.ExpireAt.Format(time.RFC3339)), - sdk.NewAttribute(types.AttributeKeyRefundedCoin, refundedCoin.String()), + sdk.NewAttribute(types.AttributeKeyRefundedCoins, refundedCoin.String()), ), }) @@ -254,12 +255,8 @@ func (k Keeper) CancelAllOrders(ctx sdk.Context, msg *types.MsgCancelAllOrders) } func (k Keeper) ExecuteMatching(ctx sdk.Context, pair types.Pair) error { - params := k.GetParams(ctx) - tickPrec := int(params.TickPrecision) - + ob := amm.NewOrderBook() skip := true // Whether to skip the matching since there is no orders. - - ob := types.NewOrderBook(tickPrec) if err := k.IterateSwapRequestsByPair(ctx, pair.Id, func(req types.SwapRequest) (stop bool, err error) { switch req.Status { case types.SwapRequestStatusNotExecuted, @@ -271,7 +268,7 @@ func (k Keeper) ExecuteMatching(ctx sdk.Context, pair types.Pair) error { } return false, nil } - ob.AddOrder(types.NewUserOrder(req)) + ob.Add(types.NewUserOrder(req)) if req.Status == types.SwapRequestStatusNotExecuted { req.Status = types.SwapRequestStatusNotMatched k.SetSwapRequest(ctx, req) @@ -290,110 +287,96 @@ func (k Keeper) ExecuteMatching(ctx sdk.Context, pair types.Pair) error { return nil } - var pools []types.PoolI - var poolBuySources, poolSellSources []types.OrderSource + var poolOrderSources []amm.OrderSource _ = k.IteratePoolsByPair(ctx, pair.Id, func(pool types.Pool) (stop bool, err error) { rx, ry := k.GetPoolBalance(ctx, pool, pair) ps := k.GetPoolCoinSupply(ctx, pool) - poolInfo := types.NewPoolInfo(rx, ry, ps) // Pool coin supply is not used when matching - if types.IsDepletedPool(poolInfo) { + ammPool := amm.NewBasicPool(rx, ry, ps) + if ammPool.IsDepleted() { k.MarkPoolAsDisabled(ctx, pool) return false, nil } - pools = append(pools, poolInfo) - - poolReserveAddr := pool.GetReserveAddress() - poolBuySources = append(poolBuySources, types.NewPoolOrderSource(poolInfo, pool.Id, poolReserveAddr, types.SwapDirectionBuy, tickPrec)) - poolSellSources = append(poolSellSources, types.NewPoolOrderSource(poolInfo, pool.Id, poolReserveAddr, types.SwapDirectionSell, tickPrec)) + poolOrderSource := types.NewPoolOrderSource(ammPool, pool.Id, pool.GetReserveAddress(), pair.BaseCoinDenom, pair.QuoteCoinDenom) + poolOrderSources = append(poolOrderSources, poolOrderSource) return false, nil }) - buySource := types.MergeOrderSources(append(poolBuySources, ob.OrderSource(types.SwapDirectionBuy))...) - sellSource := types.MergeOrderSources(append(poolSellSources, ob.OrderSource(types.SwapDirectionSell))...) - - engine := types.NewMatchEngine(buySource, sellSource, tickPrec) - ob, matchPrice, quoteCoinDustAmt, matched := engine.Match() - - if matched { - orders := ob.AllOrders() - bulkOp := types.NewBulkSendCoinsOperation() - for _, order := range orders { - if order, ok := order.(*types.PoolOrder); ok { - var offerCoinDenom string - switch order.Direction { - case types.SwapDirectionBuy: - offerCoinDenom = pair.QuoteCoinDenom - case types.SwapDirectionSell: - offerCoinDenom = pair.BaseCoinDenom - } - paidCoin := sdk.NewCoin(offerCoinDenom, order.OfferCoinAmount.Sub(order.RemainingOfferCoinAmount)) - bulkOp.SendCoins(order.ReserveAddress, pair.GetEscrowAddress(), sdk.NewCoins(paidCoin)) - } - } - if err := bulkOp.Run(ctx, k.bankKeeper); err != nil { - return err - } - bulkOp = types.NewBulkSendCoinsOperation() - for _, order := range orders { - switch order := order.(type) { - case *types.UserOrder: - var offerCoinDenom, demandCoinDenom string - switch order.Direction { - case types.SwapDirectionBuy: - offerCoinDenom = pair.QuoteCoinDenom - demandCoinDenom = pair.BaseCoinDenom - case types.SwapDirectionSell: - offerCoinDenom = pair.BaseCoinDenom - demandCoinDenom = pair.QuoteCoinDenom - } - - // TODO: optimize read/write (can there be only one write?) - req, _ := k.GetSwapRequest(ctx, pair.Id, order.RequestId) - req.OpenAmount = order.OpenAmount - req.RemainingOfferCoin = sdk.NewCoin(offerCoinDenom, order.RemainingOfferCoinAmount) - req.ReceivedCoin.Amount = req.ReceivedCoin.Amount.Add(order.ReceivedAmount) - if order.OpenAmount.IsZero() { - if err := k.FinishSwapRequest(ctx, req, types.SwapRequestStatusCompleted); err != nil { - return err - } - } else { - req.Status = types.SwapRequestStatusPartiallyMatched - k.SetSwapRequest(ctx, req) - // TODO: emit an event? - } + os := amm.MergeOrderSources(append(poolOrderSources, ob)...) - demandCoin := sdk.NewCoin(demandCoinDenom, order.ReceivedAmount) - bulkOp.SendCoins(pair.GetEscrowAddress(), order.Orderer, sdk.NewCoins(demandCoin)) - case *types.PoolOrder: - var demandCoinDenom string - switch order.Direction { - case types.SwapDirectionBuy: - demandCoinDenom = pair.BaseCoinDenom - case types.SwapDirectionSell: - demandCoinDenom = pair.QuoteCoinDenom - } - demandCoin := sdk.NewCoin(demandCoinDenom, order.ReceivedAmount) - bulkOp.SendCoins(pair.GetEscrowAddress(), order.ReserveAddress, sdk.NewCoins(demandCoin)) + params := k.GetParams(ctx) + matchPrice, found := amm.FindMatchPrice(os, int(params.TickPrecision)) + if found { + buyOrders := os.BuyOrdersOver(matchPrice) + sellOrders := os.SellOrdersUnder(matchPrice) + + types.SortOrders(buyOrders, types.DescendingPrice) + types.SortOrders(sellOrders, types.AscendingPrice) + + quoteCoinDust, matched := amm.MatchOrders(buyOrders, sellOrders, matchPrice) + if matched { + if err := k.ApplyMatchResult(ctx, pair, append(buyOrders, sellOrders...), quoteCoinDust); err != nil { + return err } + pair.LastPrice = &matchPrice } - bulkOp.SendCoins(pair.GetEscrowAddress(), types.DustCollectorAddress, sdk.NewCoins(sdk.NewCoin(pair.QuoteCoinDenom, quoteCoinDustAmt))) - if err := bulkOp.Run(ctx, k.bankKeeper); err != nil { - return err - } - - pair.LastPrice = &matchPrice } pair.CurrentBatchId++ k.SetPair(ctx, pair) // TODO: emit an event? - _ = matchPrice + return nil +} + +func (k Keeper) ApplyMatchResult(ctx sdk.Context, pair types.Pair, orders []amm.Order, quoteCoinDust sdk.Int) error { + bulkOp := types.NewBulkSendCoinsOperation() + for _, order := range orders { + if !order.IsMatched() { + continue + } + if order, ok := order.(*types.PoolOrder); ok { + paidCoin := order.OfferCoin.Sub(order.RemainingOfferCoin) + bulkOp.SendCoins(order.ReserveAddress, pair.GetEscrowAddress(), sdk.NewCoins(paidCoin)) + } + } + if err := bulkOp.Run(ctx, k.bankKeeper); err != nil { + return err + } + bulkOp = types.NewBulkSendCoinsOperation() + for _, order := range orders { + if !order.IsMatched() { + continue + } + switch order := order.(type) { + case *types.UserOrder: + // TODO: optimize read/write (can there be only one write?) + req, _ := k.GetSwapRequest(ctx, pair.Id, order.RequestId) + req.OpenAmount = order.OpenAmount + req.RemainingOfferCoin = order.RemainingOfferCoin + req.ReceivedCoin = req.ReceivedCoin.AddAmount(order.ReceivedDemandCoin.Amount) + if order.OpenAmount.IsZero() { + if err := k.FinishSwapRequest(ctx, req, types.SwapRequestStatusCompleted); err != nil { + return err + } + } else { + req.Status = types.SwapRequestStatusPartiallyMatched + k.SetSwapRequest(ctx, req) + // TODO: emit an event? + } + bulkOp.SendCoins(pair.GetEscrowAddress(), order.Orderer, sdk.NewCoins(order.ReceivedDemandCoin)) + case *types.PoolOrder: + bulkOp.SendCoins(pair.GetEscrowAddress(), order.ReserveAddress, sdk.NewCoins(order.ReceivedDemandCoin)) + } + } + bulkOp.SendCoins(pair.GetEscrowAddress(), types.DustCollectorAddress, sdk.NewCoins(sdk.NewCoin(pair.QuoteCoinDenom, quoteCoinDust))) + if err := bulkOp.Run(ctx, k.bankKeeper); err != nil { + return err + } return nil } func (k Keeper) FinishSwapRequest(ctx sdk.Context, req types.SwapRequest, status types.SwapRequestStatus) error { - if req.Status.IsCanceledOrExpired() { // sanity check + if req.Status == types.SwapRequestStatusCompleted || req.Status.IsCanceledOrExpired() { // sanity check return nil } diff --git a/x/liquidity/keeper/swap_test.go b/x/liquidity/keeper/swap_test.go index e55655b0..e4533cfa 100644 --- a/x/liquidity/keeper/swap_test.go +++ b/x/liquidity/keeper/swap_test.go @@ -6,6 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" _ "github.com/stretchr/testify/suite" + squad "github.com/cosmosquad-labs/squad/types" "github.com/cosmosquad-labs/squad/x/liquidity" "github.com/cosmosquad-labs/squad/x/liquidity/types" ) @@ -13,7 +14,7 @@ import ( func (s *KeeperTestSuite) TestLimitOrder() { // Create a denom1/denom2 pair and set last price to 1.0 pair1 := s.createPair(s.addr(0), "denom1", "denom2", true) - lastPrice := parseDec("1.0") + lastPrice := squad.ParseDec("1.0") pair1.LastPrice = &lastPrice s.keeper.SetPair(s.ctx, pair1) @@ -21,7 +22,7 @@ func (s *KeeperTestSuite) TestLimitOrder() { pair2 := s.createPair(s.addr(0), "denom2", "denom1", true) orderer := s.addr(1) - s.fundAddr(orderer, parseCoins("1000000000denom1,1000000000denom2")) + s.fundAddr(orderer, squad.ParseCoins("1000000000denom1,1000000000denom2")) for _, tc := range []struct { name string @@ -31,72 +32,72 @@ func (s *KeeperTestSuite) TestLimitOrder() { { "happy case", types.NewMsgLimitOrder( - orderer, pair1.Id, types.SwapDirectionBuy, parseCoin("1000000denom2"), "denom1", - parseDec("1.0"), newInt(1000000), 0), + orderer, pair1.Id, types.SwapDirectionBuy, squad.ParseCoin("1000000denom2"), "denom1", + squad.ParseDec("1.0"), newInt(1000000), 0), "", }, { "wrong offer coin and demand coin denom", types.NewMsgLimitOrder( - orderer, pair1.Id, types.SwapDirectionBuy, parseCoin("1000000denom1"), "denom2", - parseDec("1.0"), newInt(1000000), 0), + orderer, pair1.Id, types.SwapDirectionBuy, squad.ParseCoin("1000000denom1"), "denom2", + squad.ParseDec("1.0"), newInt(1000000), 0), "denom pair (denom2, denom1) != (denom1, denom2): wrong denom pair", }, { "correct offer coin and demand coin denom", types.NewMsgLimitOrder( - orderer, pair2.Id, types.SwapDirectionBuy, parseCoin("1000000denom1"), "denom2", - parseDec("1.0"), newInt(1000000), 0), + orderer, pair2.Id, types.SwapDirectionBuy, squad.ParseCoin("1000000denom1"), "denom2", + squad.ParseDec("1.0"), newInt(1000000), 0), "", }, { "price not fit in ticks", types.NewMsgLimitOrder( - orderer, pair1.Id, types.SwapDirectionSell, parseCoin("1000000denom1"), "denom2", - parseDec("1.0005"), newInt(1000000), 0), + orderer, pair1.Id, types.SwapDirectionSell, squad.ParseCoin("1000000denom1"), "denom2", + squad.ParseDec("1.0005"), newInt(1000000), 0), "price not fit into ticks", }, { "too long order lifespan", types.NewMsgLimitOrder( - orderer, pair1.Id, types.SwapDirectionSell, parseCoin("1000000denom1"), "denom2", - parseDec("1.0"), newInt(1000000), 48*time.Hour), + orderer, pair1.Id, types.SwapDirectionSell, squad.ParseCoin("1000000denom1"), "denom2", + squad.ParseDec("1.0"), newInt(1000000), 48*time.Hour), "order lifespan is too long", }, { "pair not found", types.NewMsgLimitOrder( - orderer, 3, types.SwapDirectionBuy, parseCoin("1000000denom1"), "denom2", - parseDec("1.0"), newInt(1000000), 0), + orderer, 3, types.SwapDirectionBuy, squad.ParseCoin("1000000denom1"), "denom2", + squad.ParseDec("1.0"), newInt(1000000), 0), "pair not found: not found", }, { "price out of lower limit", types.NewMsgLimitOrder( - orderer, pair1.Id, types.SwapDirectionBuy, parseCoin("1000000denom2"), "denom1", - parseDec("0.8"), newInt(1000000), 0), + orderer, pair1.Id, types.SwapDirectionBuy, squad.ParseCoin("1000000denom2"), "denom1", + squad.ParseDec("0.8"), newInt(1000000), 0), "price out of range limit", }, { "price out of upper limit", types.NewMsgLimitOrder( - orderer, pair1.Id, types.SwapDirectionBuy, parseCoin("2000000denom2"), "denom1", - parseDec("1.2"), newInt(1000000), 0), + orderer, pair1.Id, types.SwapDirectionBuy, squad.ParseCoin("2000000denom2"), "denom1", + squad.ParseDec("1.2"), newInt(1000000), 0), "price out of range limit", }, { "no price limit without last price", types.NewMsgLimitOrder( - orderer, pair2.Id, types.SwapDirectionSell, parseCoin("1000000denom2"), "denom1", - parseDec("100.0"), newInt(1000000), 0), + orderer, pair2.Id, types.SwapDirectionSell, squad.ParseCoin("1000000denom2"), "denom1", + squad.ParseDec("100.0"), newInt(1000000), 0), "", }, { "insufficient offer coin", types.NewMsgLimitOrder( - orderer, pair2.Id, types.SwapDirectionBuy, parseCoin("1000000denom1"), "denom2", - parseDec("10.0"), newInt(1000000), 0), + orderer, pair2.Id, types.SwapDirectionBuy, squad.ParseCoin("1000000denom1"), "denom2", + squad.ParseDec("10.0"), newInt(1000000), 0), "insufficient offer coin", }, } { @@ -116,7 +117,7 @@ func (s *KeeperTestSuite) TestLimitOrder() { func (s *KeeperTestSuite) TestLimitOrderRefund() { pair := s.createPair(s.addr(0), "denom1", "denom2", true) orderer := s.addr(1) - s.fundAddr(orderer, parseCoins("1000000000denom1,1000000000denom2")) + s.fundAddr(orderer, squad.ParseCoins("1000000000denom1,1000000000denom2")) for _, tc := range []struct { msg *types.MsgLimitOrder @@ -124,27 +125,27 @@ func (s *KeeperTestSuite) TestLimitOrderRefund() { }{ { types.NewMsgLimitOrder( - orderer, pair.Id, types.SwapDirectionBuy, parseCoin("1000000denom2"), "denom1", - parseDec("1.0"), newInt(1000000), 0), - parseCoin("0denom2"), + orderer, pair.Id, types.SwapDirectionBuy, squad.ParseCoin("1000000denom2"), "denom1", + squad.ParseDec("1.0"), newInt(1000000), 0), + squad.ParseCoin("0denom2"), }, { types.NewMsgLimitOrder( - orderer, pair.Id, types.SwapDirectionBuy, parseCoin("1000000denom2"), "denom1", - parseDec("1.0"), newInt(10000), 0), - parseCoin("990000denom2"), + orderer, pair.Id, types.SwapDirectionBuy, squad.ParseCoin("1000000denom2"), "denom1", + squad.ParseDec("1.0"), newInt(10000), 0), + squad.ParseCoin("990000denom2"), }, { types.NewMsgLimitOrder( - orderer, pair.Id, types.SwapDirectionBuy, parseCoin("1000denom2"), "denom1", - parseDec("0.9999"), newInt(1000), 0), - parseCoin("0denom2"), + orderer, pair.Id, types.SwapDirectionBuy, squad.ParseCoin("1000denom2"), "denom1", + squad.ParseDec("0.9999"), newInt(1000), 0), + squad.ParseCoin("0denom2"), }, { types.NewMsgLimitOrder( - orderer, pair.Id, types.SwapDirectionBuy, parseCoin("102denom2"), "denom1", - parseDec("1.001"), newInt(100), 0), - parseCoin("1denom2"), + orderer, pair.Id, types.SwapDirectionBuy, squad.ParseCoin("102denom2"), "denom1", + squad.ParseDec("1.001"), newInt(100), 0), + squad.ParseCoin("1denom2"), }, } { s.Run("", func() { @@ -167,7 +168,7 @@ func (s *KeeperTestSuite) TestSingleOrderNoMatch() { pair := s.createPair(s.addr(0), "denom1", "denom2", true) - req := s.buyLimitOrder(s.addr(1), pair.Id, parseDec("1.0"), sdk.NewInt(1000000), 10*time.Second, true) + req := s.buyLimitOrder(s.addr(1), pair.Id, squad.ParseDec("1.0"), sdk.NewInt(1000000), 10*time.Second, true) // Execute matching liquidity.EndBlocker(ctx, k) @@ -183,7 +184,7 @@ func (s *KeeperTestSuite) TestSingleOrderNoMatch() { req, _ = k.GetSwapRequest(ctx, req.PairId, req.Id) s.Require().Equal(types.SwapRequestStatusExpired, req.Status) - s.Require().True(coinsEq(parseCoins("1000000denom2"), s.getBalances(s.addr(1)))) + s.Require().True(coinsEq(squad.ParseCoins("1000000denom2"), s.getBalances(s.addr(1)))) } func (s *KeeperTestSuite) TestTwoOrderExactMatch() { @@ -191,8 +192,8 @@ func (s *KeeperTestSuite) TestTwoOrderExactMatch() { pair := s.createPair(s.addr(0), "denom1", "denom2", true) - req1 := s.buyLimitOrder(s.addr(1), pair.Id, parseDec("1.0"), newInt(10000), time.Hour, true) - req2 := s.sellLimitOrder(s.addr(2), pair.Id, parseDec("1.0"), newInt(10000), time.Hour, true) + req1 := s.buyLimitOrder(s.addr(1), pair.Id, squad.ParseDec("1.0"), newInt(10000), time.Hour, true) + req2 := s.sellLimitOrder(s.addr(2), pair.Id, squad.ParseDec("1.0"), newInt(10000), time.Hour, true) liquidity.EndBlocker(ctx, k) req1, _ = k.GetSwapRequest(ctx, req1.PairId, req1.Id) @@ -200,12 +201,12 @@ func (s *KeeperTestSuite) TestTwoOrderExactMatch() { req2, _ = k.GetSwapRequest(ctx, req2.PairId, req2.Id) s.Require().Equal(types.SwapRequestStatusCompleted, req2.Status) - s.Require().True(coinsEq(parseCoins("10000denom1"), s.getBalances(s.addr(1)))) - s.Require().True(coinsEq(parseCoins("10000denom2"), s.getBalances(s.addr(2)))) + s.Require().True(coinsEq(squad.ParseCoins("10000denom1"), s.getBalances(s.addr(1)))) + s.Require().True(coinsEq(squad.ParseCoins("10000denom2"), s.getBalances(s.addr(2)))) pair, _ = k.GetPair(ctx, pair.Id) s.Require().NotNil(pair.LastPrice) - s.Require().True(decEq(parseDec("1.0"), *pair.LastPrice)) + s.Require().True(decEq(squad.ParseDec("1.0"), *pair.LastPrice)) } func (s *KeeperTestSuite) TestCancelOrder() { @@ -213,7 +214,7 @@ func (s *KeeperTestSuite) TestCancelOrder() { pair := s.createPair(s.addr(0), "denom1", "denom2", true) - req := s.buyLimitOrder(s.addr(1), pair.Id, parseDec("1.0"), newInt(10000), types.DefaultMaxOrderLifespan, true) + req := s.buyLimitOrder(s.addr(1), pair.Id, squad.ParseDec("1.0"), newInt(10000), types.DefaultMaxOrderLifespan, true) // Cannot cancel an order within a same batch err := k.CancelOrder(ctx, types.NewMsgCancelOrder(s.addr(1), req.PairId, req.Id)) @@ -230,7 +231,7 @@ func (s *KeeperTestSuite) TestCancelOrder() { s.Require().Equal(types.SwapRequestStatusCanceled, req.Status) // Coins are refunded - s.Require().True(coinsEq(parseCoins("10000denom2"), s.getBalances(s.addr(1)))) + s.Require().True(coinsEq(squad.ParseCoins("10000denom2"), s.getBalances(s.addr(1)))) s.nextBlock() @@ -242,13 +243,13 @@ func (s *KeeperTestSuite) TestCancelOrder() { func (s *KeeperTestSuite) TestDustCollector() { pair := s.createPair(s.addr(0), "denom1", "denom2", true) - s.buyLimitOrder(s.addr(1), pair.Id, parseDec("0.9005"), newInt(1000), 0, true) - s.sellLimitOrder(s.addr(2), pair.Id, parseDec("0.9005"), newInt(1000), 0, true) + s.buyLimitOrder(s.addr(1), pair.Id, squad.ParseDec("0.9005"), newInt(1000), 0, true) + s.sellLimitOrder(s.addr(2), pair.Id, squad.ParseDec("0.9005"), newInt(1000), 0, true) s.nextBlock() - s.Require().True(coinsEq(parseCoins("1000denom1"), s.getBalances(s.addr(1)))) - s.Require().True(coinsEq(parseCoins("900denom2"), s.getBalances(s.addr(2)))) + s.Require().True(coinsEq(squad.ParseCoins("1000denom1"), s.getBalances(s.addr(1)))) + s.Require().True(coinsEq(squad.ParseCoins("900denom2"), s.getBalances(s.addr(2)))) s.Require().True(s.getBalances(pair.GetEscrowAddress()).IsZero()) - s.Require().True(coinsEq(parseCoins("1denom2"), s.getBalances(types.DustCollectorAddress))) + s.Require().True(coinsEq(squad.ParseCoins("1denom2"), s.getBalances(types.DustCollectorAddress))) } diff --git a/x/liquidity/simulation/operations.go b/x/liquidity/simulation/operations.go index d7dbda09..5ede2a72 100644 --- a/x/liquidity/simulation/operations.go +++ b/x/liquidity/simulation/operations.go @@ -13,6 +13,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/simulation" squadappparams "github.com/cosmosquad-labs/squad/app/params" + "github.com/cosmosquad-labs/squad/x/liquidity/amm" "github.com/cosmosquad-labs/squad/x/liquidity/keeper" "github.com/cosmosquad-labs/squad/x/liquidity/types" ) @@ -372,10 +373,10 @@ func SimulateMsgLimitOrder(ak types.AccountKeeper, bk types.BankKeeper, k keeper minPrice, maxPrice = minMaxPrice(k, ctx, *pair.LastPrice) } else { rx, ry := k.GetPoolBalance(ctx, pool, pair) - poolInfo := types.NewPoolInfo(rx, ry, sdk.Int{}) - minPrice, maxPrice = minMaxPrice(k, ctx, poolInfo.Price()) + ammPool := amm.NewBasicPool(rx, ry, sdk.ZeroInt()) + minPrice, maxPrice = minMaxPrice(k, ctx, ammPool.Price()) } - price := types.PriceToTick(randomDec(r, minPrice, maxPrice), int(params.TickPrecision)) + price := amm.PriceToDownTick(randomDec(r, minPrice, maxPrice), int(params.TickPrecision)) amt := randomInt(r, types.MinCoinAmount, sdk.NewInt(1000000)) @@ -799,7 +800,7 @@ func findPairToMakeMarketOrder(r *rand.Rand, k keeper.Keeper, ctx sdk.Context, s func minMaxPrice(k keeper.Keeper, ctx sdk.Context, lastPrice sdk.Dec) (sdk.Dec, sdk.Dec) { params := k.GetParams(ctx) tickPrec := int(params.TickPrecision) - maxPrice := types.PriceToTick(lastPrice.Mul(sdk.OneDec().Add(params.MaxPriceLimitRatio)), tickPrec) - minPrice := types.PriceToUpTick(lastPrice.Mul(sdk.OneDec().Sub(params.MaxPriceLimitRatio)), tickPrec) + maxPrice := amm.PriceToDownTick(lastPrice.Mul(sdk.OneDec().Add(params.MaxPriceLimitRatio)), tickPrec) + minPrice := amm.PriceToUpTick(lastPrice.Mul(sdk.OneDec().Sub(params.MaxPriceLimitRatio)), tickPrec) return minPrice, maxPrice } diff --git a/x/liquidity/types/common_test.go b/x/liquidity/types/common_test.go index 8637e480..f0e91e4d 100644 --- a/x/liquidity/types/common_test.go +++ b/x/liquidity/types/common_test.go @@ -1,118 +1,12 @@ package types_test import ( - "time" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/tendermint/tendermint/crypto" - - "github.com/cosmosquad-labs/squad/x/liquidity/types" ) var testAddr = sdk.AccAddress(crypto.AddressHash([]byte("test"))) -func newBuyOrder(price sdk.Dec, amt sdk.Int) *types.BaseOrder { - return types.NewBaseOrder(types.SwapDirectionBuy, price, amt, price.MulInt(amt).TruncateInt()) -} - -func newSellOrder(price sdk.Dec, amt sdk.Int) *types.BaseOrder { - return types.NewBaseOrder(types.SwapDirectionSell, price, amt, amt) -} - -func newBuyUserOrder(reqId uint64, price sdk.Dec, amt sdk.Int) *types.UserOrder { - return &types.UserOrder{ - BaseOrder: types.BaseOrder{ - Direction: types.SwapDirectionBuy, - Price: price, - Amount: amt, - OpenAmount: amt, - OfferCoinAmount: price.MulInt(amt).Ceil().TruncateInt(), - RemainingOfferCoinAmount: price.MulInt(amt).Ceil().TruncateInt(), - ReceivedAmount: sdk.ZeroInt(), - }, - RequestId: reqId, - Orderer: testAddr, - } -} - -//nolint -func newSellUserOrder(reqId uint64, price sdk.Dec, amt sdk.Int) *types.UserOrder { - return &types.UserOrder{ - BaseOrder: types.BaseOrder{ - Direction: types.SwapDirectionSell, - Price: price, - Amount: amt, - OpenAmount: amt, - OfferCoinAmount: amt, - RemainingOfferCoinAmount: amt, - ReceivedAmount: sdk.ZeroInt(), - }, - RequestId: reqId, - Orderer: testAddr, - } -} - -func newBuyPoolOrder(poolId uint64, price sdk.Dec, amt sdk.Int) *types.PoolOrder { - return &types.PoolOrder{ - BaseOrder: types.BaseOrder{ - Direction: types.SwapDirectionBuy, - Price: price, - Amount: amt, - OpenAmount: amt, - OfferCoinAmount: price.MulInt(amt).Ceil().TruncateInt(), - RemainingOfferCoinAmount: price.MulInt(amt).Ceil().TruncateInt(), - ReceivedAmount: sdk.ZeroInt(), - }, - PoolId: poolId, - ReserveAddress: testAddr, - } -} - -//nolint -func newSellPoolOrder(poolId uint64, price sdk.Dec, amt sdk.Int) *types.PoolOrder { - return &types.PoolOrder{ - BaseOrder: types.BaseOrder{ - Direction: types.SwapDirectionSell, - Price: price, - Amount: amt, - OpenAmount: amt, - OfferCoinAmount: amt, - RemainingOfferCoinAmount: amt, - ReceivedAmount: sdk.ZeroInt(), - }, - PoolId: poolId, - ReserveAddress: testAddr, - } -} - func newInt(i int64) sdk.Int { return sdk.NewInt(i) } - -func parseDec(s string) sdk.Dec { - return sdk.MustNewDecFromStr(s) -} - -func parseCoin(s string) sdk.Coin { - coin, err := sdk.ParseCoinNormalized(s) - if err != nil { - panic(err) - } - return coin -} - -func parseCoins(s string) sdk.Coins { - coins, err := sdk.ParseCoinsNormalized(s) - if err != nil { - panic(err) - } - return coins -} - -func parseTime(s string) time.Time { - t, err := time.Parse(time.RFC3339, s) - if err != nil { - panic(err) - } - return t -} diff --git a/x/liquidity/types/events.go b/x/liquidity/types/events.go index c9741e7e..9e460f98 100644 --- a/x/liquidity/types/events.go +++ b/x/liquidity/types/events.go @@ -24,7 +24,7 @@ const ( AttributeKeyMintedPoolCoin = "minted_pool_coin" AttributeKeyPoolCoin = "pool_coin" AttributeKeyWithdrawnCoins = "withdrawn_coins" - AttributeKeyRefundedCoin = "refunded_coin" + AttributeKeyRefundedCoins = "refunded_coins" AttributeKeyReserveAddress = "reserve_address" AttributeKeyEscrowAddress = "escrow_address" AttributeKeyRequestId = "request_id" diff --git a/x/liquidity/types/match.go b/x/liquidity/types/match.go deleted file mode 100644 index 6922d63f..00000000 --- a/x/liquidity/types/match.go +++ /dev/null @@ -1,254 +0,0 @@ -package types - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" -) - -type PriceDirection int - -const ( - PriceStaying PriceDirection = iota + 1 - PriceIncreasing - PriceDecreasing -) - -type MatchEngine struct { - BuyOrderSource OrderSource - SellOrderSource OrderSource - TickPrecision int // price tick precision -} - -func NewMatchEngine(buys, sells OrderSource, prec int) *MatchEngine { - return &MatchEngine{ - BuyOrderSource: buys, - SellOrderSource: sells, - TickPrecision: prec, - } -} - -func NewMatchEngineFromOrderBook(ob *OrderBook) *MatchEngine { - return NewMatchEngine(ob.OrderSource(SwapDirectionBuy), ob.OrderSource(SwapDirectionSell), ob.TickPrecision) -} - -func (engine *MatchEngine) Matchable() bool { - highestBuyPrice, found := engine.BuyOrderSource.HighestTick() - if !found { - return false - } - return engine.SellOrderSource.AmountLTE(highestBuyPrice).IsPositive() -} - -func (engine *MatchEngine) EstimatedPriceDirection(midPrice sdk.Dec) PriceDirection { - buyAmount := engine.BuyOrderSource.AmountGTE(midPrice) - sellAmount := engine.SellOrderSource.AmountLTE(midPrice) - switch { - case buyAmount.GT(sellAmount): - return PriceIncreasing - case sellAmount.GT(buyAmount): - return PriceDecreasing - default: - return PriceStaying - } -} - -func (engine *MatchEngine) InitialMatchPrice() (price sdk.Dec, dir PriceDirection) { - highestBuyPrice, found := engine.BuyOrderSource.HighestTick() - if !found { - panic("there is no buy orders") - } - lowestSellPrice, found := engine.SellOrderSource.LowestTick() - if !found { - panic("there is no sell orders") - } - midPrice := highestBuyPrice.Add(lowestSellPrice).QuoInt64(2) - - dir = engine.EstimatedPriceDirection(midPrice) - - switch dir { - case PriceStaying: - price = RoundPrice(midPrice, engine.TickPrecision) - case PriceIncreasing: - price = PriceToTick(midPrice, engine.TickPrecision) // TODO: use lower tick? - case PriceDecreasing: - price = PriceToUpTick(midPrice, engine.TickPrecision) // TODO: use higher tick? - } - return -} - -func (engine *MatchEngine) FindMatchPrice() sdk.Dec { - matchPrice, dir := engine.InitialMatchPrice() - if dir == PriceStaying { // TODO: is this correct? - return matchPrice - } - - tickSource := MergeOrderSources(engine.BuyOrderSource, engine.SellOrderSource) // temporary order source just for ticks - - buysCache := map[int]sdk.Int{} - buyAmountGTE := func(i int) sdk.Int { - ba, ok := buysCache[i] - if !ok { - ba = engine.BuyOrderSource.AmountGTE(TickFromIndex(i, engine.TickPrecision)) - buysCache[i] = ba - } - return ba - } - sellsCache := map[int]sdk.Int{} - sellAmountLTE := func(i int) sdk.Int { - sa, ok := sellsCache[i] - if !ok { - sa = engine.SellOrderSource.AmountLTE(TickFromIndex(i, engine.TickPrecision)) - sellsCache[i] = sa - } - return sa - } - - for { - i := TickToIndex(matchPrice, engine.TickPrecision) - - if buyAmountGTE(i+1).LTE(sellAmountLTE(i)) && buyAmountGTE(i).GTE(sellAmountLTE(i-1)) { - return matchPrice - } - - var nextPrice sdk.Dec - var found bool - switch dir { - case PriceIncreasing: - if buyAmountGTE(i + 1).IsZero() { - return matchPrice - } - nextPrice, found = tickSource.UpTick(matchPrice) - case PriceDecreasing: - if sellAmountLTE(i - 1).IsZero() { - return matchPrice - } - nextPrice, found = tickSource.DownTick(matchPrice) - } - if !found { - return matchPrice - } - matchPrice = nextPrice - } -} - -// TODO: no need to return the order book -func (engine *MatchEngine) Match() (orderBook *OrderBook, matchPrice sdk.Dec, quoteCoinDustAmt sdk.Int, matched bool) { - if !engine.Matchable() { - return - } - - matchPrice = engine.FindMatchPrice() - buyPrice, _ := engine.BuyOrderSource.HighestTick() - sellPrice, _ := engine.SellOrderSource.LowestTick() - - var buyOrders, sellOrders Orders - - orderBook = NewOrderBook(engine.TickPrecision) - - for buyPrice.GTE(matchPrice) { - orders := engine.BuyOrderSource.Orders(buyPrice) - orderBook.AddOrders(orders...) - buyOrders = append(buyOrders, orders...) - var found bool - buyPrice, found = engine.BuyOrderSource.DownTickWithOrders(buyPrice) - if !found { - break - } - } - - for sellPrice.LTE(matchPrice) { - orders := engine.SellOrderSource.Orders(sellPrice) - orderBook.AddOrders(orders...) - sellOrders = append(sellOrders, orders...) - var found bool - sellPrice, found = engine.SellOrderSource.UpTickWithOrders(sellPrice) - if !found { - break - } - } - - quoteCoinDustAmt, matched = MatchOrders(buyOrders, sellOrders, matchPrice) - - return -} - -func FindLastMatchableOrders(buyOrders, sellOrders Orders, matchPrice sdk.Dec) (idx1, idx2 int, partialMatchAmt1, partialMatchAmt2 sdk.Int, found bool) { - type Side struct { - orders Orders - totalAmt sdk.Int - i int - partialMatchAmt sdk.Int - } - buySide := &Side{buyOrders, buyOrders.OpenAmount(), len(buyOrders) - 1, sdk.Int{}} - sellSide := &Side{sellOrders, sellOrders.OpenAmount(), len(sellOrders) - 1, sdk.Int{}} - sides := map[SwapDirection]*Side{ - SwapDirectionBuy: buySide, - SwapDirectionSell: sellSide, - } - for { - ok := true - for dir, side := range sides { - i := side.i - order := side.orders[i] - matchAmt := sdk.MinInt(buySide.totalAmt, sellSide.totalAmt) - side.partialMatchAmt = matchAmt.Sub(side.totalAmt.Sub(order.GetOpenAmount())) - if side.totalAmt.Sub(order.GetOpenAmount()).GT(matchAmt) || - (dir == SwapDirectionSell && matchPrice.MulInt(side.partialMatchAmt).TruncateInt().IsZero()) { - if i == 0 { - return - } - side.totalAmt = side.totalAmt.Sub(order.GetOpenAmount()) - side.i-- - ok = false - } - } - if ok { - return buySide.i, sellSide.i, buySide.partialMatchAmt, sellSide.partialMatchAmt, true - } - } -} - -func MatchOrders(buyOrders, sellOrders Orders, matchPrice sdk.Dec) (quoteCoinDustAmt sdk.Int, matched bool) { - buyOrders.Sort(DescendingPrice) - sellOrders.Sort(AscendingPrice) - - bi, si, pmb, pms, found := FindLastMatchableOrders(buyOrders, sellOrders, matchPrice) - if !found { - return - } - - quoteCoinDustAmt = sdk.ZeroInt() - - for i := 0; i <= bi; i++ { - buyOrder := buyOrders[i] - var receivedBaseCoinAmt sdk.Int - if i < bi { - receivedBaseCoinAmt = buyOrder.GetOpenAmount() - } else { - receivedBaseCoinAmt = pmb - } - paidQuoteCoinAmt := matchPrice.MulInt(receivedBaseCoinAmt).Ceil().TruncateInt() - buyOrder.SetOpenAmount(buyOrder.GetOpenAmount().Sub(receivedBaseCoinAmt)) - buyOrder.SetRemainingOfferCoinAmount(buyOrder.GetRemainingOfferCoinAmount().Sub(paidQuoteCoinAmt)) - buyOrder.SetReceivedAmount(receivedBaseCoinAmt) - quoteCoinDustAmt = quoteCoinDustAmt.Add(paidQuoteCoinAmt) - } - - for i := 0; i <= si; i++ { - sellOrder := sellOrders[i] - var paidBaseCoinAmt sdk.Int - if i < si { - paidBaseCoinAmt = sellOrder.GetOpenAmount() - } else { - paidBaseCoinAmt = pms - } - receivedQuoteCoinAmt := matchPrice.MulInt(paidBaseCoinAmt).TruncateInt() - sellOrder.SetOpenAmount(sellOrder.GetOpenAmount().Sub(paidBaseCoinAmt)) - sellOrder.SetRemainingOfferCoinAmount(sellOrder.GetRemainingOfferCoinAmount().Sub(paidBaseCoinAmt)) - sellOrder.SetReceivedAmount(receivedQuoteCoinAmt) - quoteCoinDustAmt = quoteCoinDustAmt.Sub(receivedQuoteCoinAmt) - } - - matched = true - - return -} diff --git a/x/liquidity/types/match_test.go b/x/liquidity/types/match_test.go deleted file mode 100644 index acbc21a4..00000000 --- a/x/liquidity/types/match_test.go +++ /dev/null @@ -1,151 +0,0 @@ -package types_test - -import ( - "fmt" - "testing" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/require" - - "github.com/cosmosquad-labs/squad/x/liquidity/types" -) - -func TestMatchEngine_Matchable(t *testing.T) { - for _, tc := range []struct { - name string - orders []types.Order - matchable bool - }{ - { - "no orders", - []types.Order{}, - false, - }, - { - "only one order", - []types.Order{ - newBuyOrder(parseDec("1.0"), newInt(100)), - }, - false, - }, - { - "only one order", - []types.Order{ - newSellOrder(parseDec("1.0"), newInt(100)), - }, - false, - }, - { - "two orders with same price", - []types.Order{ - newBuyOrder(parseDec("1.0"), newInt(100)), - newSellOrder(parseDec("1.0"), newInt(100)), - }, - true, - }, - { - "two orders with different prices", - []types.Order{ - newBuyOrder(parseDec("1.5"), newInt(100)), - newSellOrder(parseDec("0.5"), newInt(100)), - }, - true, - }, - { - "two orders with not matchable prices", - []types.Order{ - newBuyOrder(parseDec("0.5"), newInt(100)), - newSellOrder(parseDec("1.5"), newInt(100)), - }, - false, - }, - { - "orders with matchable prices", - []types.Order{ - newBuyOrder(parseDec("1.5"), newInt(100)), - newBuyOrder(parseDec("1.3"), newInt(100)), - newSellOrder(parseDec("1.4"), newInt(100)), - newSellOrder(parseDec("1.6"), newInt(100)), - }, - true, - }, - { - "orders with not matchable prices", - []types.Order{ - newBuyOrder(parseDec("1.4"), newInt(100)), - newBuyOrder(parseDec("1.3"), newInt(100)), - newSellOrder(parseDec("1.5"), newInt(100)), - newSellOrder(parseDec("1.6"), newInt(100)), - }, - false, - }, - } { - t.Run(tc.name, func(t *testing.T) { - ob := types.NewOrderBook(tickPrec) - ob.AddOrders(tc.orders...) - engine := types.NewMatchEngineFromOrderBook(ob) - require.Equal(t, tc.matchable, engine.Matchable()) - }) - } -} - -func TestMatchEngine_EstimatedPriceDirection(t *testing.T) { - for _, tc := range []struct { - name string - orders []types.Order - midPrice sdk.Dec - dir types.PriceDirection - }{ - { - "increasing", - []types.Order{ - newBuyOrder(parseDec("1.5"), newInt(100)), - newSellOrder(parseDec("0.5"), newInt(99)), - }, - parseDec("1.0"), - types.PriceIncreasing, - }, - { - "decreasing", - []types.Order{ - newBuyOrder(parseDec("1.5"), newInt(99)), - newSellOrder(parseDec("0.5"), newInt(100)), - }, - parseDec("1.0"), - types.PriceDecreasing, - }, - { - "staying", - []types.Order{ - newBuyOrder(parseDec("1.5"), newInt(100)), - newSellOrder(parseDec("0.5"), newInt(100)), - }, - parseDec("1.0"), - types.PriceStaying, - }, - } { - t.Run(tc.name, func(t *testing.T) { - ob := types.NewOrderBook(tickPrec) - ob.AddOrders(tc.orders...) - engine := types.NewMatchEngineFromOrderBook(ob) - require.Equal(t, tc.dir, engine.EstimatedPriceDirection(tc.midPrice)) - }) - } -} - -func TestMatchOrders(t *testing.T) { - ob := types.NewOrderBook(tickPrec) - - ob.AddOrders( - newBuyOrder(parseDec("0.9"), newInt(7500)), - newBuyOrder(parseDec("0.8"), newInt(5000)), - newSellOrder(parseDec("0.7"), newInt(10000)), - ) - - types.MatchOrders(ob.BuyTicks.AllOrders(), ob.SellTicks.AllOrders(), parseDec("0.7137")) - - for _, order := range ob.AllOrders() { - fmt.Printf("(%s, %s(%s), paid %s, received %s)\n", - order.GetDirection(), order.GetAmount(), order.GetOpenAmount(), order.GetOfferCoinAmount().Sub(order.GetRemainingOfferCoinAmount()), order.GetReceivedAmount()) - } -} diff --git a/x/liquidity/types/msgs_test.go b/x/liquidity/types/msgs_test.go index eb7e0baa..8193736e 100644 --- a/x/liquidity/types/msgs_test.go +++ b/x/liquidity/types/msgs_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/crypto" + squad "github.com/cosmosquad-labs/squad/types" "github.com/cosmosquad-labs/squad/x/liquidity/types" ) @@ -90,27 +91,27 @@ func TestMsgCreatePool(t *testing.T) { { "invalid deposit coins", func(msg *types.MsgCreatePool) { - msg.DepositCoins = sdk.Coins{parseCoin("0denom1"), parseCoin("1000000denom2")} + msg.DepositCoins = sdk.Coins{squad.ParseCoin("0denom1"), squad.ParseCoin("1000000denom2")} }, "coin 0denom1 amount is not positive", }, { "invalid deposit coins", func(msg *types.MsgCreatePool) { - msg.DepositCoins = sdk.Coins{parseCoin("1000000denom1"), parseCoin("0denom2")} + msg.DepositCoins = sdk.Coins{squad.ParseCoin("1000000denom1"), squad.ParseCoin("0denom2")} }, "coin denom2 amount is not positive", }, { "invalid deposit coins", func(msg *types.MsgCreatePool) { - msg.DepositCoins = parseCoins("1000000denom1,1000000denom2,1000000denom3") + msg.DepositCoins = squad.ParseCoins("1000000denom1,1000000denom2,1000000denom3") }, "wrong number of deposit coins: 3: invalid request", }, } { t.Run(tc.name, func(t *testing.T) { - msg := types.NewMsgCreatePool(testAddr, 1, parseCoins("1000000denom1,1000000denom2")) + msg := types.NewMsgCreatePool(testAddr, 1, squad.ParseCoins("1000000denom1,1000000denom2")) tc.malleate(msg) require.Equal(t, types.TypeMsgCreatePool, msg.Type()) require.Equal(t, types.RouterKey, msg.Route()) @@ -155,21 +156,21 @@ func TestMsgDeposit(t *testing.T) { { "invalid deposit coins", func(msg *types.MsgDeposit) { - msg.DepositCoins = sdk.Coins{parseCoin("0denom1"), parseCoin("1000000denom2")} + msg.DepositCoins = sdk.Coins{squad.ParseCoin("0denom1"), squad.ParseCoin("1000000denom2")} }, "coin 0denom1 amount is not positive", }, { "invalid deposit coins", func(msg *types.MsgDeposit) { - msg.DepositCoins = sdk.Coins{parseCoin("1000000denom1"), parseCoin("0denom2")} + msg.DepositCoins = sdk.Coins{squad.ParseCoin("1000000denom1"), squad.ParseCoin("0denom2")} }, "coin denom2 amount is not positive", }, { "invalid deposit coins", func(msg *types.MsgDeposit) { - msg.DepositCoins = parseCoins("1000000denom1,1000000denom2,1000000denom3") + msg.DepositCoins = squad.ParseCoins("1000000denom1,1000000denom2,1000000denom3") }, "wrong number of deposit coins: 3: invalid request", }, @@ -177,7 +178,7 @@ func TestMsgDeposit(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - msg := types.NewMsgDeposit(testAddr, 1, parseCoins("1000000denom1,1000000denom2")) + msg := types.NewMsgDeposit(testAddr, 1, squad.ParseCoins("1000000denom1,1000000denom2")) tc.malleate(msg) require.Equal(t, types.TypeMsgDeposit, msg.Type()) require.Equal(t, types.RouterKey, msg.Route()) @@ -222,13 +223,13 @@ func TestMsgWithdraw(t *testing.T) { { "invalid pool coin", func(msg *types.MsgWithdraw) { - msg.PoolCoin = parseCoin("0pool1") + msg.PoolCoin = squad.ParseCoin("0pool1") }, "pool coin must be positive: invalid request", }, } { t.Run(tc.name, func(t *testing.T) { - msg := types.NewMsgWithdraw(testAddr, 1, parseCoin("1000000pool1")) + msg := types.NewMsgWithdraw(testAddr, 1, squad.ParseCoin("1000000pool1")) tc.malleate(msg) require.Equal(t, types.TypeMsgWithdraw, msg.Type()) require.Equal(t, types.RouterKey, msg.Route()) @@ -281,14 +282,14 @@ func TestMsgLimitOrder(t *testing.T) { { "invalid offer coin", func(msg *types.MsgLimitOrder) { - msg.OfferCoin = parseCoin("0denom1") + msg.OfferCoin = squad.ParseCoin("0denom1") }, "offer coin must be positive: invalid request", }, { "insufficient offer coin amount", func(msg *types.MsgLimitOrder) { - msg.OfferCoin = parseCoin("10denom1") + msg.OfferCoin = squad.ParseCoin("10denom1") }, "offer coin is less than minimum coin amount: invalid request", }, @@ -302,7 +303,7 @@ func TestMsgLimitOrder(t *testing.T) { { "same offer coin denom and demand coin denom", func(msg *types.MsgLimitOrder) { - msg.OfferCoin = parseCoin("1000000denom1") + msg.OfferCoin = squad.ParseCoin("1000000denom1") msg.DemandCoinDenom = "denom1" }, "offer coin denom and demand coin denom must not be same: invalid request", @@ -310,7 +311,7 @@ func TestMsgLimitOrder(t *testing.T) { { "invalid price", func(msg *types.MsgLimitOrder) { - msg.Price = parseDec("0") + msg.Price = squad.ParseDec("0") }, "price must be positive: invalid request", }, @@ -338,8 +339,8 @@ func TestMsgLimitOrder(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { msg := types.NewMsgLimitOrder( - testAddr, 1, types.SwapDirectionSell, parseCoin("1000000denom2"), - "denom1", parseDec("1.0"), newInt(1000000), orderLifespan) + testAddr, 1, types.SwapDirectionSell, squad.ParseCoin("1000000denom2"), + "denom1", squad.ParseDec("1.0"), newInt(1000000), orderLifespan) tc.malleate(msg) require.Equal(t, types.TypeMsgLimitOrder, msg.Type()) require.Equal(t, types.RouterKey, msg.Route()) @@ -392,14 +393,14 @@ func TestMsgMarketOrder(t *testing.T) { { "invalid offer coin", func(msg *types.MsgMarketOrder) { - msg.OfferCoin = parseCoin("0denom1") + msg.OfferCoin = squad.ParseCoin("0denom1") }, "offer coin must be positive: invalid request", }, { "insufficient offer coin amount", func(msg *types.MsgMarketOrder) { - msg.OfferCoin = parseCoin("10denom1") + msg.OfferCoin = squad.ParseCoin("10denom1") }, "offer coin is less than minimum coin amount: invalid request", }, @@ -413,7 +414,7 @@ func TestMsgMarketOrder(t *testing.T) { { "same offer coin denom and demand coin denom", func(msg *types.MsgMarketOrder) { - msg.OfferCoin = parseCoin("1000000denom1") + msg.OfferCoin = squad.ParseCoin("1000000denom1") msg.DemandCoinDenom = "denom1" }, "offer coin denom and demand coin denom must not be same: invalid request", @@ -442,7 +443,7 @@ func TestMsgMarketOrder(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { msg := types.NewMsgMarketOrder( - testAddr, 1, types.SwapDirectionBuy, parseCoin("1000000denom1"), + testAddr, 1, types.SwapDirectionBuy, squad.ParseCoin("1000000denom1"), "denom2", newInt(1000000), orderLifespan) tc.malleate(msg) require.Equal(t, types.TypeMsgMarketOrder, msg.Type()) diff --git a/x/liquidity/types/order.go b/x/liquidity/types/order.go index d515c054..fa495abb 100644 --- a/x/liquidity/types/order.go +++ b/x/liquidity/types/order.go @@ -1,50 +1,29 @@ package types import ( + "fmt" "sort" sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmosquad-labs/squad/x/liquidity/amm" ) var ( - _ Order = (*BaseOrder)(nil) - _ Order = (*UserOrder)(nil) - _ Order = (*PoolOrder)(nil) + _ amm.Order = (*UserOrder)(nil) + _ amm.Order = (*PoolOrder)(nil) - DescendingPrice PriceComparator = func(a, b Order) bool { + DescendingPrice PriceComparator = func(a, b amm.Order) bool { return a.GetPrice().GT(b.GetPrice()) } - AscendingPrice PriceComparator = func(a, b Order) bool { + AscendingPrice PriceComparator = func(a, b amm.Order) bool { return a.GetPrice().LT(b.GetPrice()) } ) -type Order interface { - GetDirection() SwapDirection - GetPrice() sdk.Dec - GetAmount() sdk.Int - GetOpenAmount() sdk.Int - SetOpenAmount(amount sdk.Int) Order - GetOfferCoinAmount() sdk.Int - GetRemainingOfferCoinAmount() sdk.Int - SetRemainingOfferCoinAmount(amount sdk.Int) Order - GetReceivedAmount() sdk.Int - SetReceivedAmount(amount sdk.Int) Order -} - -type PriceComparator func(a, b Order) bool - -type Orders []Order - -func (orders Orders) OpenAmount() sdk.Int { - amount := sdk.ZeroInt() - for _, order := range orders { - amount = amount.Add(order.GetOpenAmount()) - } - return amount -} +type PriceComparator func(a, b amm.Order) bool -func (orders Orders) Sort(cmp PriceComparator) { +func SortOrders(orders []amm.Order, cmp PriceComparator) { sort.SliceStable(orders, func(i, j int) bool { switch orderA := orders[i].(type) { case *UserOrder: @@ -62,7 +41,7 @@ func (orders Orders) Sort(cmp PriceComparator) { return orderA.PoolId < orderB.PoolId } } - return false // not reachable + panic(fmt.Sprintf("unknown order types: (%T, %T)", orders[i], orders[j])) }) sort.SliceStable(orders, func(i, j int) bool { return orders[i].GetAmount().GT(orders[j].GetAmount()) @@ -72,148 +51,81 @@ func (orders Orders) Sort(cmp PriceComparator) { }) } -type BaseOrder struct { - Direction SwapDirection - Price sdk.Dec - Amount sdk.Int - OpenAmount sdk.Int - OfferCoinAmount sdk.Int - RemainingOfferCoinAmount sdk.Int - ReceivedAmount sdk.Int -} - -func NewBaseOrder(dir SwapDirection, price sdk.Dec, amt, offerCoinAmt sdk.Int) *BaseOrder { - return &BaseOrder{ - Direction: dir, - Price: price, - Amount: amt, - OpenAmount: amt, - OfferCoinAmount: offerCoinAmt, - RemainingOfferCoinAmount: offerCoinAmt, - ReceivedAmount: sdk.ZeroInt(), - } -} - -func (order *BaseOrder) GetDirection() SwapDirection { - return order.Direction -} - -func (order *BaseOrder) GetPrice() sdk.Dec { - return order.Price -} - -func (order *BaseOrder) GetAmount() sdk.Int { - return order.Amount -} - -func (order *BaseOrder) GetOpenAmount() sdk.Int { - return order.OpenAmount -} - -func (order *BaseOrder) SetOpenAmount(amount sdk.Int) Order { - order.OpenAmount = amount - return order -} - -func (order *BaseOrder) GetOfferCoinAmount() sdk.Int { - return order.OfferCoinAmount -} - -func (order *BaseOrder) GetRemainingOfferCoinAmount() sdk.Int { - return order.RemainingOfferCoinAmount -} - -func (order *BaseOrder) SetRemainingOfferCoinAmount(amount sdk.Int) Order { - order.RemainingOfferCoinAmount = amount - return order -} - -func (order *BaseOrder) GetReceivedAmount() sdk.Int { - return order.ReceivedAmount -} - -func (order *BaseOrder) SetReceivedAmount(amount sdk.Int) Order { - order.ReceivedAmount = amount - return order -} - type UserOrder struct { - BaseOrder + *amm.BaseOrder RequestId uint64 Orderer sdk.AccAddress } func NewUserOrder(req SwapRequest) *UserOrder { + var dir amm.OrderDirection + switch req.Direction { + case SwapDirectionBuy: + dir = amm.Buy + case SwapDirectionSell: + dir = amm.Sell + } return &UserOrder{ - BaseOrder: BaseOrder{ - Direction: req.Direction, - Price: req.Price, - Amount: req.OpenAmount, - OpenAmount: req.OpenAmount, - OfferCoinAmount: req.RemainingOfferCoin.Amount, - RemainingOfferCoinAmount: req.RemainingOfferCoin.Amount, - ReceivedAmount: sdk.ZeroInt(), - }, + BaseOrder: amm.NewBaseOrder(dir, req.Price, req.OpenAmount, req.RemainingOfferCoin, req.ReceivedCoin.Denom), RequestId: req.Id, Orderer: req.GetOrderer(), } } -func (order *UserOrder) SetOpenAmount(amount sdk.Int) Order { - order.BaseOrder.SetOpenAmount(amount) +func (order *UserOrder) SetOpenAmount(amt sdk.Int) amm.Order { + order.BaseOrder.SetOpenAmount(amt) + return order +} + +func (order *UserOrder) DecrRemainingOfferCoin(amt sdk.Int) amm.Order { + order.BaseOrder.DecrRemainingOfferCoin(amt) return order } -func (order *UserOrder) SetRemainingOfferCoinAmount(amount sdk.Int) Order { - order.BaseOrder.SetRemainingOfferCoinAmount(amount) +func (order *UserOrder) IncrReceivedDemandCoin(amt sdk.Int) amm.Order { + order.BaseOrder.IncrReceivedDemandCoin(amt) return order } -func (order *UserOrder) SetReceivedAmount(amount sdk.Int) Order { - order.BaseOrder.SetReceivedAmount(amount) +func (order *UserOrder) SetMatched(matched bool) amm.Order { + order.BaseOrder.SetMatched(matched) return order } type PoolOrder struct { - BaseOrder + *amm.BaseOrder PoolId uint64 ReserveAddress sdk.AccAddress + OfferCoin sdk.Coin } -func NewPoolOrder(poolId uint64, reserveAddr sdk.AccAddress, dir SwapDirection, price sdk.Dec, amt sdk.Int) *PoolOrder { - var offerCoinAmt sdk.Int - switch dir { - case SwapDirectionBuy: - offerCoinAmt = price.MulInt(amt).Ceil().TruncateInt() - case SwapDirectionSell: - offerCoinAmt = amt - } +func NewPoolOrder( + poolId uint64, reserveAddr sdk.AccAddress, dir amm.OrderDirection, price sdk.Dec, amt sdk.Int, + offerCoin sdk.Coin, demandCoinDenom string) *PoolOrder { return &PoolOrder{ - BaseOrder: BaseOrder{ - Direction: dir, - Price: price, - Amount: amt, - OpenAmount: amt, - OfferCoinAmount: offerCoinAmt, - RemainingOfferCoinAmount: offerCoinAmt, - ReceivedAmount: sdk.ZeroInt(), - }, + BaseOrder: amm.NewBaseOrder(dir, price, amt, offerCoin, demandCoinDenom), PoolId: poolId, ReserveAddress: reserveAddr, + OfferCoin: offerCoin, } } -func (order *PoolOrder) SetOpenAmount(amount sdk.Int) Order { - order.BaseOrder.SetOpenAmount(amount) +func (order *PoolOrder) SetOpenAmount(amt sdk.Int) amm.Order { + order.BaseOrder.SetOpenAmount(amt) + return order +} + +func (order *PoolOrder) DecrRemainingOfferCoin(amt sdk.Int) amm.Order { + order.BaseOrder.DecrRemainingOfferCoin(amt) return order } -func (order *PoolOrder) SetRemainingOfferCoinAmount(amount sdk.Int) Order { - order.BaseOrder.SetRemainingOfferCoinAmount(amount) +func (order *PoolOrder) IncrReceivedDemandCoin(amt sdk.Int) amm.Order { + order.BaseOrder.IncrReceivedDemandCoin(amt) return order } -func (order *PoolOrder) SetReceivedAmount(amount sdk.Int) Order { - order.BaseOrder.SetReceivedAmount(amount) +func (order *PoolOrder) SetMatched(matched bool) amm.Order { + order.BaseOrder.SetMatched(matched) return order } diff --git a/x/liquidity/types/order_test.go b/x/liquidity/types/order_test.go deleted file mode 100644 index 7cfe9272..00000000 --- a/x/liquidity/types/order_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package types_test - -import ( - "math/rand" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/cosmosquad-labs/squad/x/liquidity/types" -) - -func TestOrders_Sort(t *testing.T) { - for seed := int64(0); seed < 10; seed++ { - r := rand.New(rand.NewSource(seed)) - - const n = 1000 - - reqIds := make([]uint64, n) - for i := uint64(0); i < n; i++ { - reqIds[i] = i + 1 - } - rand.Shuffle(len(reqIds), func(i, j int) { - reqIds[i], reqIds[j] = reqIds[j], reqIds[i] - }) - poolIds := make([]uint64, n) - for i := uint64(0); i < n; i++ { - poolIds[i] = i + 1 - } - rand.Shuffle(len(poolIds), func(i, j int) { - poolIds[i], poolIds[j] = poolIds[j], poolIds[i] - }) - - orders := make(types.Orders, n) - for i := 0; i < n; i++ { - price := types.TickFromIndex(r.Intn(100)+10000, tickPrec) - amt := newInt(r.Int63n(500) + 100) - if r.Intn(2) == 0 { - var reqId uint64 - reqId, reqIds = reqIds[0], reqIds[1:] - orders[i] = newBuyUserOrder(reqId, price, amt) - } else { - var poolId uint64 - poolId, poolIds = poolIds[0], poolIds[1:] - orders[i] = newBuyPoolOrder(poolId, price, amt) - } - } - - const ascendingPrice, descendingPrice = 1, 2 - for _, priceCmp := range []int{1, 2} { - switch priceCmp { - case ascendingPrice: - orders.Sort(types.AscendingPrice) - case descendingPrice: - orders.Sort(types.DescendingPrice) - } - for i := 1; i < n; i++ { - switch priceCmp { - case ascendingPrice: - require.True(t, orders[i].GetPrice().GTE(orders[i-1].GetPrice())) - case descendingPrice: - require.True(t, orders[i].GetPrice().LTE(orders[i-1].GetPrice())) - } - if orders[i].GetPrice().Equal(orders[i-1].GetPrice()) { - require.True(t, orders[i].GetAmount().LTE(orders[i-1].GetAmount())) - if orders[i].GetAmount().Equal(orders[i-1].GetAmount()) { - switch orderA := orders[i].(type) { - case *types.UserOrder: - switch orderB := orders[i-1].(type) { - case *types.UserOrder: - require.Greater(t, orderA.RequestId, orderB.RequestId) - case *types.PoolOrder: - t.Error("not sorted") - t.FailNow() - } - case *types.PoolOrder: - switch orderB := orders[i-1].(type) { - case *types.UserOrder: - // ok - case *types.PoolOrder: - require.Greater(t, orderA.PoolId, orderB.PoolId) - } - } - } - } - } - } - } -} diff --git a/x/liquidity/types/orderbook.go b/x/liquidity/types/orderbook.go deleted file mode 100644 index 23033da6..00000000 --- a/x/liquidity/types/orderbook.go +++ /dev/null @@ -1,79 +0,0 @@ -package types - -import ( - "fmt" - "strings" -) - -type OrderBook struct { - BuyTicks *OrderBookTicks - SellTicks *OrderBookTicks - TickPrecision int -} - -func NewOrderBook(prec int) *OrderBook { - return &OrderBook{ - BuyTicks: NewOrderBookTicks(prec), - SellTicks: NewOrderBookTicks(prec), - TickPrecision: prec, - } -} - -func (ob *OrderBook) AddOrder(order Order) { - switch order.GetDirection() { - case SwapDirectionBuy: - ob.BuyTicks.AddOrder(order) - case SwapDirectionSell: - ob.SellTicks.AddOrder(order) - } -} - -func (ob *OrderBook) AddOrders(orders ...Order) { - for _, order := range orders { - ob.AddOrder(order) - } -} - -func (ob OrderBook) OrderSource(dir SwapDirection) OrderSource { - switch dir { - case SwapDirectionBuy: - return ob.BuyTicks - case SwapDirectionSell: - return ob.SellTicks - default: - panic(fmt.Sprintf("unknown swap direction: %v", dir)) - } -} - -func (ob OrderBook) AllOrders() Orders { - var orders Orders - for _, ticks := range []*OrderBookTicks{ob.BuyTicks, ob.SellTicks} { - orders = append(orders, ticks.AllOrders()...) - } - return orders -} - -func (ob OrderBook) String() string { - os := MergeOrderSources(ob.BuyTicks, ob.SellTicks) - price, found := os.HighestTick() - if !found { - return "" - } - lines := []string{ - "+-----buy------+----------price-----------+-----sell-----+", - } - for { - lines = append(lines, - fmt.Sprintf("| %12s | %24s | %-12s |", - ob.BuyTicks.Orders(price).OpenAmount(), - price.String(), - ob.SellTicks.Orders(price).OpenAmount())) - - price, found = os.DownTickWithOrders(price) - if !found { - break - } - } - lines = append(lines, "+--------------+--------------------------+--------------+") - return strings.Join(lines, "\n") -} diff --git a/x/liquidity/types/ordersource.go b/x/liquidity/types/ordersource.go deleted file mode 100644 index f17dadfb..00000000 --- a/x/liquidity/types/ordersource.go +++ /dev/null @@ -1,531 +0,0 @@ -package types - -import ( - "sort" - - sdk "github.com/cosmos/cosmos-sdk/types" -) - -var ( - _ OrderSource = (*OrderBookTicks)(nil) - _ OrderSource = (*PoolOrderSource)(nil) - _ OrderSource = (*MergedOrderSources)(nil) -) - -// OrderSource defines a source of orders which can be an order book or -// a pool. -type OrderSource interface { - AmountGTE(price sdk.Dec) sdk.Int - AmountLTE(price sdk.Dec) sdk.Int - Orders(price sdk.Dec) Orders - UpTick(price sdk.Dec) (tick sdk.Dec, found bool) - DownTick(price sdk.Dec) (tick sdk.Dec, found bool) - UpTickWithOrders(price sdk.Dec) (tick sdk.Dec, found bool) - DownTickWithOrders(price sdk.Dec) (tick sdk.Dec, found bool) - HighestTick() (tick sdk.Dec, found bool) - LowestTick() (tick sdk.Dec, found bool) -} - -type OrderBookTicks struct { - Ticks []*OrderBookTick - TickPrecision int -} - -func NewOrderBookTicks(prec int) *OrderBookTicks { - return &OrderBookTicks{ - TickPrecision: prec, - } -} - -func (ticks *OrderBookTicks) FindPrice(price sdk.Dec) (i int, exact bool) { - i = sort.Search(len(ticks.Ticks), func(i int) bool { - return ticks.Ticks[i].Price.LTE(price) - }) - if i < len(ticks.Ticks) && ticks.Ticks[i].Price.Equal(price) { - exact = true - } - return -} - -func (ticks *OrderBookTicks) AddOrder(order Order) { - i, exact := ticks.FindPrice(order.GetPrice()) - if exact { - ticks.Ticks[i].Orders = append(ticks.Ticks[i].Orders, order) - } else { - if i < len(ticks.Ticks) { - // Insert a new order book tick at index i. - ticks.Ticks = append(ticks.Ticks[:i], append([]*OrderBookTick{NewOrderBookTick(order)}, ticks.Ticks[i:]...)...) - } else { - // Append a new order group at the end. - ticks.Ticks = append(ticks.Ticks, NewOrderBookTick(order)) - } - } -} - -func (ticks *OrderBookTicks) AddOrders(orders ...Order) { - for _, order := range orders { - ticks.AddOrder(order) - } -} - -func (ticks *OrderBookTicks) AllOrders() []Order { - var orders []Order - for _, tick := range ticks.Ticks { - orders = append(orders, tick.Orders...) - } - return orders -} - -func (ticks *OrderBookTicks) AmountGTE(price sdk.Dec) sdk.Int { - i, exact := ticks.FindPrice(price) - if !exact { - i-- - } - amount := sdk.ZeroInt() - for ; i >= 0; i-- { - amount = amount.Add(ticks.Ticks[i].Orders.OpenAmount()) - } - return amount -} - -func (ticks OrderBookTicks) AmountLTE(price sdk.Dec) sdk.Int { - i, _ := ticks.FindPrice(price) - amount := sdk.ZeroInt() - for ; i < len(ticks.Ticks); i++ { - amount = amount.Add(ticks.Ticks[i].Orders.OpenAmount()) - } - return amount -} - -func (ticks OrderBookTicks) Orders(price sdk.Dec) Orders { - i, exact := ticks.FindPrice(price) - if !exact { - return nil - } - return ticks.Ticks[i].Orders -} - -func (ticks OrderBookTicks) UpTick(price sdk.Dec) (tick sdk.Dec, found bool) { - i, _ := ticks.FindPrice(price) - if i == 0 { - return - } - tick = UpTick(price, ticks.TickPrecision) - found = true - return -} - -func (ticks OrderBookTicks) DownTick(price sdk.Dec) (tick sdk.Dec, found bool) { - i, exact := ticks.FindPrice(price) - if !exact { - i-- - } - if i >= len(ticks.Ticks)-1 { - return - } - tick = DownTick(price, ticks.TickPrecision) - found = true - return -} - -func (ticks OrderBookTicks) UpTickWithOrders(price sdk.Dec) (tick sdk.Dec, found bool) { - i, _ := ticks.FindPrice(price) - if i == 0 { - return - } - for i--; i >= 0; i-- { - if ticks.Ticks[i].Orders.OpenAmount().IsPositive() { - return ticks.Ticks[i].Price, true - } - } - return -} - -func (ticks OrderBookTicks) DownTickWithOrders(price sdk.Dec) (tick sdk.Dec, found bool) { - i, exact := ticks.FindPrice(price) - if !exact { - i-- - } - if i >= len(ticks.Ticks)-1 { - return - } - for i++; i < len(ticks.Ticks); i++ { - if ticks.Ticks[i].Orders.OpenAmount().IsPositive() { - return ticks.Ticks[i].Price, true - } - } - return -} - -func (ticks OrderBookTicks) HighestTick() (tick sdk.Dec, found bool) { - if len(ticks.Ticks) == 0 { - return - } - for i := range ticks.Ticks { - if ticks.Ticks[i].Orders.OpenAmount().IsPositive() { - return ticks.Ticks[i].Price, true - } - } - return -} - -func (ticks OrderBookTicks) LowestTick() (tick sdk.Dec, found bool) { - if len(ticks.Ticks) == 0 { - return - } - for i := len(ticks.Ticks) - 1; i >= 0; i-- { - if ticks.Ticks[i].Orders.OpenAmount().IsPositive() { - return ticks.Ticks[i].Price, true - } - } - return -} - -type OrderBookTick struct { - Price sdk.Dec - Orders Orders -} - -func NewOrderBookTick(order Order) *OrderBookTick { - return &OrderBookTick{ - Price: order.GetPrice(), - Orders: Orders{order}, - } -} - -type PoolOrderSource struct { - PoolId uint64 - ReserveAddress sdk.AccAddress - RX, RY sdk.Int - PoolPrice sdk.Dec - Direction SwapDirection - TickPrecision int - buyAmountCache map[string]sdk.Int // map(price => buyAmountOnTick) - sellAmountCache map[string]sdk.Int // map(price => sellAmountOnTick) -} - -func NewPoolOrderSource(pool PoolI, poolId uint64, reserveAddr sdk.AccAddress, dir SwapDirection, prec int) OrderSource { - rx, ry := pool.Balance() - return &PoolOrderSource{ - PoolId: poolId, - ReserveAddress: reserveAddr, - RX: rx, - RY: ry, - PoolPrice: pool.Price(), - Direction: dir, - TickPrecision: prec, - sellAmountCache: map[string]sdk.Int{}, - buyAmountCache: map[string]sdk.Int{}, - } -} - -func (os PoolOrderSource) BuyAmountOnTick(price sdk.Dec) sdk.Int { - if price.GTE(os.PoolPrice) { - return sdk.ZeroInt() - } - priceStr := price.String() - res, ok := os.buyAmountCache[priceStr] - if !ok { - upPrice := UpTick(price, os.TickPrecision) // P' - res = upPrice.Quo(price).Sub(sdk.OneDec()).MulInt(os.RY).TruncateInt() // (P'/P - 1) * RY - os.buyAmountCache[priceStr] = res - } - return res -} - -func (os PoolOrderSource) SellAmountOnTick(price sdk.Dec) sdk.Int { - if price.LTE(os.PoolPrice) { - return sdk.ZeroInt() - } - priceStr := price.String() - res, ok := os.sellAmountCache[priceStr] - if !ok { - downPrice := DownTick(price, os.TickPrecision) // P' - rx := os.RX.ToDec() - res = rx.QuoTruncate(downPrice).Sub(rx.QuoTruncate(price)).TruncateInt() // RX/P' - RX/P - os.sellAmountCache[priceStr] = res - } - return res -} - -func (os PoolOrderSource) AmountGTE(price sdk.Dec) sdk.Int { - amount := sdk.ZeroInt() - var found bool - switch os.Direction { - case SwapDirectionBuy: - for price.LT(os.PoolPrice) { - ba := os.BuyAmountOnTick(price) - amount = amount.Add(ba) - price, found = os.UpTickWithOrders(price) - if !found { - break - } - } - case SwapDirectionSell: - for { - // If price <= poolPrice, then sell amount at price would be 0, - // so it'll leave the result amount unchanged. - // After that, price would become one tick higher than poolPrice, - // and the calculation will be continued until there's no more - // ticks left. - // We could do an additional optimization that checks - // if price <= poolPrice, but SellAmountOnTick is cached anyway - // and doing such optimization doesn't have much benefit. - // Same applies to the buy side of AmountLTE. - sa := os.SellAmountOnTick(price) - amount = amount.Add(sa) - price, found = os.UpTickWithOrders(price) - if !found { - break - } - } - } - return amount -} - -func (os PoolOrderSource) AmountLTE(price sdk.Dec) sdk.Int { - amount := sdk.ZeroInt() - var found bool - switch os.Direction { - case SwapDirectionBuy: - for { - ba := os.BuyAmountOnTick(price) - amount = amount.Add(ba) - price, found = os.DownTickWithOrders(price) - if !found { - break - } - } - case SwapDirectionSell: - for price.GT(os.PoolPrice) { - sa := os.SellAmountOnTick(price) - amount = amount.Add(sa) - price, found = os.DownTickWithOrders(price) - if !found { - break - } - } - } - return amount -} - -func (os PoolOrderSource) Orders(price sdk.Dec) Orders { - switch os.Direction { - case SwapDirectionBuy: - return Orders{NewPoolOrder(os.PoolId, os.ReserveAddress, SwapDirectionBuy, price, os.BuyAmountOnTick(price))} - case SwapDirectionSell: - return Orders{NewPoolOrder(os.PoolId, os.ReserveAddress, SwapDirectionSell, price, os.SellAmountOnTick(price))} - } - return nil -} - -func (os PoolOrderSource) UpTick(price sdk.Dec) (tick sdk.Dec, found bool) { - switch os.Direction { - case SwapDirectionBuy: - tick = UpTick(price, os.TickPrecision) - found = tick.LT(os.PoolPrice) - case SwapDirectionSell: - tick = UpTick(price, os.TickPrecision) - if tick.GT(os.PoolPrice) { - found = os.SellAmountOnTick(tick).IsPositive() - } else { - found = true - } - } - return -} - -func (os PoolOrderSource) DownTick(price sdk.Dec) (tick sdk.Dec, found bool) { - switch os.Direction { - case SwapDirectionBuy: - tick = DownTick(price, os.TickPrecision) - if tick.LT(os.PoolPrice) { - found = os.BuyAmountOnTick(tick).IsPositive() - } else { - found = true - } - case SwapDirectionSell: - tick = DownTick(price, os.TickPrecision) - found = tick.GT(os.PoolPrice) - } - return -} - -func (os PoolOrderSource) UpTickWithOrders(price sdk.Dec) (tick sdk.Dec, found bool) { - switch os.Direction { - case SwapDirectionBuy: - tick, found = os.UpTick(price) - if !found { - break - } - for tick.LT(os.PoolPrice) { - ba := os.BuyAmountOnTick(tick) - if ba.IsPositive() { - found = true - break - } - tick, found = os.UpTick(tick) - if !found { - break - } - } - case SwapDirectionSell: - if price.LTE(os.PoolPrice) { - return os.UpTick(os.PoolPrice) - } - return os.UpTick(price) - } - return -} - -func (os PoolOrderSource) DownTickWithOrders(price sdk.Dec) (tick sdk.Dec, found bool) { - switch os.Direction { - case SwapDirectionBuy: - if price.GTE(os.PoolPrice) { - return os.DownTick(os.PoolPrice) - } - return os.DownTick(price) - case SwapDirectionSell: - tick, found = os.DownTick(price) - if !found { - break - } - for tick.GT(os.PoolPrice) { - sa := os.SellAmountOnTick(tick) - if sa.IsPositive() { - found = true - break - } - tick, found = os.DownTick(tick) - if !found { - break - } - } - } - return -} - -func (os PoolOrderSource) HighestTick() (tick sdk.Dec, found bool) { - switch os.Direction { - case SwapDirectionBuy: - tick = PriceToTick(os.PoolPrice, os.TickPrecision) - if os.PoolPrice.Equal(tick) { - tick = DownTick(tick, os.TickPrecision) - } - found = true - case SwapDirectionSell: - // TODO: is it possible to calculate? - panic("not implemented") - } - return -} - -func (os PoolOrderSource) LowestTick() (tick sdk.Dec, found bool) { - switch os.Direction { - case SwapDirectionBuy: - // TODO: is it possible to calculate? - panic("not implemented") - case SwapDirectionSell: - tick = UpTick(PriceToTick(os.PoolPrice, os.TickPrecision), os.TickPrecision) - found = true - } - return -} - -type MergedOrderSources struct { - Sources []OrderSource -} - -func MergeOrderSources(sources ...OrderSource) OrderSource { - return &MergedOrderSources{Sources: sources} -} - -func (os *MergedOrderSources) AmountGTE(price sdk.Dec) sdk.Int { - amt := sdk.ZeroInt() - for _, source := range os.Sources { - amt = amt.Add(source.AmountGTE(price)) - } - return amt -} - -func (os *MergedOrderSources) AmountLTE(price sdk.Dec) sdk.Int { - amt := sdk.ZeroInt() - for _, source := range os.Sources { - amt = amt.Add(source.AmountLTE(price)) - } - return amt -} - -func (os *MergedOrderSources) Orders(price sdk.Dec) Orders { - var orders Orders - for _, source := range os.Sources { - orders = append(orders, source.Orders(price)...) - } - return orders -} - -func (os *MergedOrderSources) UpTick(price sdk.Dec) (tick sdk.Dec, found bool) { - for _, source := range os.Sources { - t, f := source.UpTick(price) - if f && (tick.IsNil() || t.LT(tick)) { - tick = t - found = true - } - } - return -} - -func (os *MergedOrderSources) DownTick(price sdk.Dec) (tick sdk.Dec, found bool) { - for _, source := range os.Sources { - t, f := source.DownTick(price) - if f && (tick.IsNil() || t.GT(tick)) { - tick = t - found = true - } - } - return -} - -func (os *MergedOrderSources) UpTickWithOrders(price sdk.Dec) (tick sdk.Dec, found bool) { - for _, source := range os.Sources { - t, f := source.UpTickWithOrders(price) - if f && (tick.IsNil() || t.LT(tick)) { - tick = t - found = true - } - } - return -} - -func (os *MergedOrderSources) DownTickWithOrders(price sdk.Dec) (tick sdk.Dec, found bool) { - for _, source := range os.Sources { - t, f := source.DownTickWithOrders(price) - if f && (tick.IsNil() || t.GT(tick)) { - tick = t - found = true - } - } - return -} - -func (os *MergedOrderSources) HighestTick() (tick sdk.Dec, found bool) { - for _, source := range os.Sources { - t, f := source.HighestTick() - if f && (tick.IsNil() || t.GT(tick)) { - tick = t - found = true - } - } - return -} - -func (os *MergedOrderSources) LowestTick() (tick sdk.Dec, found bool) { - for _, source := range os.Sources { - t, f := source.LowestTick() - if f && (tick.IsNil() || t.LT(tick)) { - tick = t - found = true - } - } - return -} diff --git a/x/liquidity/types/ordersource_test.go b/x/liquidity/types/ordersource_test.go deleted file mode 100644 index d1b26027..00000000 --- a/x/liquidity/types/ordersource_test.go +++ /dev/null @@ -1,290 +0,0 @@ -package types_test - -import ( - "sort" - "testing" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/require" - - "github.com/cosmosquad-labs/squad/x/liquidity/types" -) - -func testOrderBookTicks() *types.OrderBookTicks { - ticks := types.NewOrderBookTicks(tickPrec) - ticks.AddOrders( - newBuyOrder(parseDec("20.0"), newInt(1000)), - newBuyOrder(parseDec("19.0"), newInt(1000)), - newBuyOrder(parseDec("18.0"), newInt(1000)), - newBuyOrder(parseDec("17.0"), newInt(1000)), - newBuyOrder(parseDec("16.0"), newInt(1000)), - newBuyOrder(parseDec("15.0"), newInt(1000)).SetOpenAmount(sdk.ZeroInt()), - newBuyOrder(parseDec("14.0"), newInt(1000)), - newBuyOrder(parseDec("13.0"), newInt(1000)).SetOpenAmount(sdk.ZeroInt()), - newBuyOrder(parseDec("12.0"), newInt(1000)), - newBuyOrder(parseDec("11.0"), newInt(1000)), - newBuyOrder(parseDec("10.0"), newInt(1000)), - ) - return ticks -} - -func TestOrderBookTicks_FindPrice(t *testing.T) { - // An empty order book ticks must return (0, false). - i, exact := types.NewOrderBookTicks(tickPrec).FindPrice(parseDec("20.0")) - require.False(t, exact) - require.Equal(t, 0, i) - - ticks := testOrderBookTicks() - - for _, tc := range []struct { - price sdk.Dec - i int - exact bool - }{ - {parseDec("20.0"), 0, true}, - {parseDec("19.99999999999999999"), 1, false}, - {parseDec("19.00000000000000001"), 1, false}, - {parseDec("19.0"), 1, true}, - {parseDec("18.99999999999999999"), 2, false}, - {parseDec("18.00000000000000001"), 2, false}, - {parseDec("18.0"), 2, true}, - {parseDec("9.999999999999999999"), 11, false}, - } { - t.Run("", func(t *testing.T) { - i, exact := ticks.FindPrice(tc.price) - require.Equal(t, tc.i, i) - require.Equal(t, tc.exact, exact) - }) - } -} - -func TestOrderBookTicks_AddOrder(t *testing.T) { - checkSorted := func(ticks *types.OrderBookTicks) { - require.True(t, sort.SliceIsSorted(ticks.Ticks, func(i, j int) bool { - return ticks.Ticks[i].Price.GTE(ticks.Ticks[j].Price) - }), "ticks must be sorted") - } - - ticks := testOrderBookTicks() - checkSorted(ticks) - require.Len(t, ticks.Ticks, 11) - - // Same price already exists - ticks.AddOrder(newBuyOrder(parseDec("18.0"), newInt(1000))) - checkSorted(ticks) - require.Len(t, ticks.Ticks, 11) - - // New price. We don't care about the tick precision here - ticks.AddOrder(newBuyOrder(parseDec("18.000000000000000001"), newInt(1000))) - checkSorted(ticks) - require.Len(t, ticks.Ticks, 12) - - // Add an order with same price as above again - ticks.AddOrder(newBuyOrder(parseDec("18.000000000000000001"), newInt(1000))) - checkSorted(ticks) - require.Len(t, ticks.Ticks, 12) - - // Add an order with higher price than the highest price in ticks. - ticks.AddOrder(newBuyOrder(parseDec("21.0"), newInt(1000))) - checkSorted(ticks) - require.Len(t, ticks.Ticks, 13) - - // Add an order with lower price than the lowest price in ticks. - ticks.AddOrder(newBuyOrder(parseDec("9.0"), newInt(1000))) - checkSorted(ticks) - require.Len(t, ticks.Ticks, 14) -} - -func TestOrderBookTicks_AmountGTE(t *testing.T) { - // An empty order book ticks - require.True(sdk.IntEq(t, sdk.ZeroInt(), types.NewOrderBookTicks(tickPrec).AmountGTE(parseDec("20.0")))) - - ticks := testOrderBookTicks() - - for _, tc := range []struct { - price sdk.Dec - expected sdk.Int - }{ - {parseDec("20.000000000000000001"), sdk.ZeroInt()}, - {parseDec("20.0"), sdk.NewInt(1000)}, - {parseDec("19.999999999999999999"), sdk.NewInt(1000)}, - {parseDec("19.000000000000000001"), sdk.NewInt(1000)}, - {parseDec("19.0"), sdk.NewInt(2000)}, - {parseDec("9.999999999999999999"), sdk.NewInt(9000)}, - } { - t.Run("", func(t *testing.T) { - require.True(sdk.IntEq(t, tc.expected, ticks.AmountGTE(tc.price))) - }) - } -} - -func TestOrderBookTicks_AmountLTE(t *testing.T) { - // An empty order book ticks - require.True(sdk.IntEq(t, sdk.ZeroInt(), types.OrderBookTicks{}.AmountLTE(parseDec("20.0")))) - - ticks := testOrderBookTicks() - - for _, tc := range []struct { - price sdk.Dec - expected sdk.Int - }{ - {parseDec("20.000000000000000001"), sdk.NewInt(9000)}, - {parseDec("20.0"), sdk.NewInt(9000)}, - {parseDec("19.999999999999999999"), sdk.NewInt(8000)}, - {parseDec("19.000000000000000001"), sdk.NewInt(8000)}, - {parseDec("19.0"), sdk.NewInt(8000)}, - {parseDec("9.999999999999999999"), sdk.ZeroInt()}, - } { - t.Run("", func(t *testing.T) { - require.True(sdk.IntEq(t, tc.expected, ticks.AmountLTE(tc.price))) - }) - } -} - -func TestOrderBookTicks_Orders(t *testing.T) { - ticks := types.OrderBookTicks{} - - orderMap := map[string]types.Orders{ - "20.0": {newBuyOrder(parseDec("20.0"), newInt(1000)), newBuyOrder(parseDec("20.0"), newInt(1000))}, - "19.0": {newBuyOrder(parseDec("19.0"), newInt(500)), newBuyOrder(parseDec("19.0"), newInt(1000))}, - "18.0": {newBuyOrder(parseDec("18.0"), newInt(1000))}, - "17.0": {newBuyOrder(parseDec("17.0"), newInt(1000)), newBuyOrder(parseDec("17.0"), newInt(2000))}, - } - - for _, orders := range orderMap { - ticks.AddOrders(orders...) - } - - // Price not found - require.Len(t, ticks.Orders(parseDec("100.0")), 0) - - for price, orders := range orderMap { - orders2 := ticks.Orders(parseDec(price)) - require.Len(t, orders2, len(orders)) - for i := range orders { - ok := false - for j := range orders2 { - if orders[i] == orders2[j] { - ok = true - break - } - } - require.True(t, ok) - } - } -} - -func TestOrderBookTicks_UpTickWithOrders(t *testing.T) { - // An empty order book ticks - _, found := types.NewOrderBookTicks(tickPrec).UpTick(parseDec("0.1")) - require.False(t, found) - - ticks := testOrderBookTicks() - - for _, tc := range []struct { - price sdk.Dec - tick sdk.Dec - found bool - }{ - {parseDec("20.000000000000000001"), sdk.Dec{}, false}, - {parseDec("20.0"), sdk.Dec{}, false}, - {parseDec("19.999999999999999999"), parseDec("20.0"), true}, - {parseDec("19.000000000000000001"), parseDec("20.0"), true}, - {parseDec("19.0"), parseDec("20.0"), true}, - {parseDec("18.999999999999999999"), parseDec("19.0"), true}, - {parseDec("18.000000000000000001"), parseDec("19.0"), true}, - {parseDec("18.0"), parseDec("19.0"), true}, - {parseDec("14.999999999999999999"), parseDec("16.0"), true}, - {parseDec("10.0"), parseDec("11.0"), true}, - {parseDec("9.999999999999999999"), parseDec("10.0"), true}, - } { - t.Run("", func(t *testing.T) { - tick, found := ticks.UpTickWithOrders(tc.price) - require.Equal(t, tc.found, found) - if found { - require.True(sdk.DecEq(t, tc.tick, tick)) - } - }) - } -} - -func TestOrderBookTicks_DownTickWithOrders(t *testing.T) { - // An empty order book ticks - _, found := types.NewOrderBookTicks(tickPrec).UpTick(parseDec("0.1")) - require.False(t, found) - - ticks := testOrderBookTicks() - - for _, tc := range []struct { - price sdk.Dec - tick sdk.Dec - found bool - }{ - {parseDec("20.000000000000000001"), parseDec("20.0"), true}, - {parseDec("20.0"), parseDec("19.0"), true}, - {parseDec("19.999999999999999999"), parseDec("19.0"), true}, - {parseDec("19.000000000000000001"), parseDec("19.0"), true}, - {parseDec("19.0"), parseDec("18.0"), true}, - {parseDec("18.999999999999999999"), parseDec("18.0"), true}, - {parseDec("18.000000000000000001"), parseDec("18.0"), true}, - {parseDec("18.0"), parseDec("17.0"), true}, - {parseDec("15.000000000000000001"), parseDec("14.0"), true}, - {parseDec("10.000000000000000001"), parseDec("10.0"), true}, - {parseDec("10.0"), sdk.Dec{}, false}, - {parseDec("9.999999999999999999"), sdk.Dec{}, false}, - } { - t.Run("", func(t *testing.T) { - tick, found := ticks.DownTickWithOrders(tc.price) - require.Equal(t, tc.found, found) - if found { - require.True(sdk.DecEq(t, tc.tick, tick)) - } - }) - } -} - -func TestOrderBookTicks_HighestTick(t *testing.T) { - // An empty order book ticks - _, found := types.OrderBookTicks{}.HighestTick() - require.False(t, found) - - ticks := testOrderBookTicks() - tick, found := ticks.HighestTick() - require.True(t, found) - require.True(sdk.DecEq(t, parseDec("20.0"), tick)) - - // Test with orders with zero remaining amount - ticks = types.NewOrderBookTicks(tickPrec) - ticks.AddOrders( - newBuyOrder(parseDec("10.0"), newInt(1000)).SetOpenAmount(sdk.ZeroInt()), - newBuyOrder(parseDec("9.0"), newInt(1000)), - newBuyOrder(parseDec("8.0"), newInt(1000)), - ) - - tick, found = ticks.HighestTick() - require.True(t, found) - require.True(sdk.DecEq(t, parseDec("9.0"), tick)) -} - -func TestOrderBookTicks_LowestTick(t *testing.T) { - // An empty order book ticks - _, found := types.OrderBookTicks{}.LowestTick() - require.False(t, found) - - ticks := testOrderBookTicks() - tick, found := ticks.LowestTick() - require.True(t, found) - require.True(sdk.DecEq(t, parseDec("10.0"), tick)) - - // Test with orders with zero remaining amount - ticks = types.NewOrderBookTicks(tickPrec) - ticks.AddOrders( - newBuyOrder(parseDec("10.0"), newInt(1000)), - newBuyOrder(parseDec("9.0"), newInt(1000)), - newBuyOrder(parseDec("8.0"), newInt(1000)).SetOpenAmount(sdk.ZeroInt()), - ) - - tick, found = ticks.LowestTick() - require.True(t, found) - require.True(sdk.DecEq(t, parseDec("9.0"), tick)) -} diff --git a/x/liquidity/types/pool.go b/x/liquidity/types/pool.go index 25edf125..844ca95e 100644 --- a/x/liquidity/types/pool.go +++ b/x/liquidity/types/pool.go @@ -9,12 +9,19 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" farmingtypes "github.com/cosmosquad-labs/squad/x/farming/types" + "github.com/cosmosquad-labs/squad/x/liquidity/amm" ) -var ( - _ PoolI = (*PoolInfo)(nil) - // TODO: add RangedPoolInfo for v2 -) +var _ amm.OrderSource = (*BasicPoolOrderSource)(nil) + +// PoolReserveAddress returns a unique pool reserve account address for each pool. +func PoolReserveAddress(poolId uint64) sdk.AccAddress { + return farmingtypes.DeriveAddress( + AddressType, + ModuleName, + strings.Join([]string{PoolReserveAddressPrefix, strconv.FormatUint(poolId, 10)}, ModuleAddressNameSplitter), + ) +} // NewPool returns a new pool object. func NewPool(id, pairId uint64) Pool { @@ -29,39 +36,6 @@ func NewPool(id, pairId uint64) Pool { } } -func (pool Pool) GetReserveAddress() sdk.AccAddress { - addr, err := sdk.AccAddressFromBech32(pool.ReserveAddress) - if err != nil { - panic(err) - } - return addr -} - -func (pool Pool) Validate() error { - if pool.Id == 0 { - return fmt.Errorf("pool id must not be 0") - } - if pool.PairId == 0 { - return fmt.Errorf("pair id must not be 0") - } - if _, err := sdk.AccAddressFromBech32(pool.ReserveAddress); err != nil { - return fmt.Errorf("invalid reserve address %s: %w", pool.ReserveAddress, err) - } - if err := sdk.ValidateDenom(pool.PoolCoinDenom); err != nil { - return fmt.Errorf("invalid pool coin denom: %w", err) - } - return nil -} - -// PoolReserveAddress returns a unique pool reserve account address for each pool. -func PoolReserveAddress(poolId uint64) sdk.AccAddress { - return farmingtypes.DeriveAddress( - AddressType, - ModuleName, - strings.Join([]string{PoolReserveAddressPrefix, strconv.FormatUint(poolId, 10)}, ModuleAddressNameSplitter), - ) -} - // PoolCoinDenom returns a unique pool coin denom for a pool. func PoolCoinDenom(poolId uint64) string { return fmt.Sprintf("pool%d", poolId) @@ -81,90 +55,65 @@ func ParsePoolCoinDenom(denom string) uint64 { return poolId } -type PoolI interface { - Balance() (rx, ry sdk.Int) - PoolCoinSupply() sdk.Int - Price() sdk.Dec -} - -type PoolInfo struct { - RX, RY sdk.Int - PS sdk.Int -} - -func NewPoolInfo(rx, ry, ps sdk.Int) PoolInfo { - return PoolInfo{ - RX: rx, - RY: ry, - PS: ps, +func (pool Pool) GetReserveAddress() sdk.AccAddress { + addr, err := sdk.AccAddressFromBech32(pool.ReserveAddress) + if err != nil { + panic(err) } + return addr } -func (info PoolInfo) Balance() (rx, ry sdk.Int) { - return info.RX, info.RY +func (pool Pool) Validate() error { + if pool.Id == 0 { + return fmt.Errorf("pool id must not be 0") + } + if pool.PairId == 0 { + return fmt.Errorf("pair id must not be 0") + } + if _, err := sdk.AccAddressFromBech32(pool.ReserveAddress); err != nil { + return fmt.Errorf("invalid reserve address %s: %w", pool.ReserveAddress, err) + } + if err := sdk.ValidateDenom(pool.PoolCoinDenom); err != nil { + return fmt.Errorf("invalid pool coin denom: %w", err) + } + return nil } -func (info PoolInfo) PoolCoinSupply() sdk.Int { - return info.PS +type BasicPoolOrderSource struct { + amm.Pool + PoolId uint64 + PoolReserveAddress sdk.AccAddress + BaseCoinDenom, QuoteCoinDenom string } -func (info PoolInfo) Price() sdk.Dec { - if info.RX.IsZero() || info.RY.IsZero() { - panic("pool price is not defined for a depleted pool") +func NewPoolOrderSource( + pool amm.Pool, poolId uint64, reserveAddr sdk.AccAddress, baseCoinDenom, quoteCoinDenom string) *BasicPoolOrderSource { + return &BasicPoolOrderSource{ + Pool: pool, + PoolId: poolId, + PoolReserveAddress: reserveAddr, + BaseCoinDenom: baseCoinDenom, + QuoteCoinDenom: quoteCoinDenom, } - return info.RX.ToDec().Quo(info.RY.ToDec()) } -func IsDepletedPool(pool PoolI) bool { - ps := pool.PoolCoinSupply() - if ps.IsZero() { - return true +func (os *BasicPoolOrderSource) BuyOrdersOver(price sdk.Dec) []amm.Order { + // TODO: use providable x amount? + amt := os.BuyAmountOver(price) + if amt.IsZero() { + return nil } - rx, ry := pool.Balance() - if rx.IsZero() || ry.IsZero() { - return true - } - return false + quoteCoin := sdk.NewCoin(os.QuoteCoinDenom, amm.OfferCoinAmount(amm.Buy, price, amt)) + return []amm.Order{NewPoolOrder(os.PoolId, os.PoolReserveAddress, amm.Buy, price, amt, quoteCoin, os.BaseCoinDenom)} } -// DepositToPool returns accepted x amount, accepted y amount and -// minted pool coin amount. -func DepositToPool(pool PoolI, x, y sdk.Int) (ax, ay, pc sdk.Int) { - // Calculate accepted amount and minting amount. - // Note that we take as many coins as possible(by ceiling numbers) - // from depositor and mint as little coins as possible. - rx, ry := pool.Balance() - ps := pool.PoolCoinSupply().ToDec() - // pc = min(ps * (x / rx), ps * (y / ry)) - pc = sdk.MinDec( - ps.MulTruncate(x.ToDec().QuoTruncate(rx.ToDec())), - ps.MulTruncate(y.ToDec().QuoTruncate(ry.ToDec())), - ).TruncateInt() - - mintProportion := pc.ToDec().Quo(ps) // pc / ps - ax = rx.ToDec().Mul(mintProportion).Ceil().TruncateInt() // rx * mintProportion - ay = ry.ToDec().Mul(mintProportion).Ceil().TruncateInt() // ry * mintProportion - - return -} - -func WithdrawFromPool(pool PoolI, pc sdk.Int, feeRate sdk.Dec) (x, y sdk.Int) { - rx, ry := pool.Balance() - ps := pool.PoolCoinSupply() - - // Redeeming the last pool coin - if pc.Equal(ps) { - x = rx - y = ry - return +func (os *BasicPoolOrderSource) SellOrdersUnder(price sdk.Dec) []amm.Order { + amt := os.SellAmountUnder(price) + if amt.IsZero() { + return nil } - - proportion := pc.ToDec().QuoTruncate(ps.ToDec()) // pc / ps - multiplier := sdk.OneDec().Sub(feeRate) // 1 - feeRate - x = rx.ToDec().MulTruncate(proportion).MulTruncate(multiplier).TruncateInt() // rx * proportion * multiplier - y = ry.ToDec().MulTruncate(proportion).MulTruncate(multiplier).TruncateInt() // ry * proportion * multiplier - - return + baseCoin := sdk.NewCoin(os.BaseCoinDenom, amt) + return []amm.Order{NewPoolOrder(os.PoolId, os.PoolReserveAddress, amm.Sell, price, amt, baseCoin, os.QuoteCoinDenom)} } // MustMarshalPool returns the pool bytes. diff --git a/x/liquidity/types/pool_test.go b/x/liquidity/types/pool_test.go index bf73a463..22a1a651 100644 --- a/x/liquidity/types/pool_test.go +++ b/x/liquidity/types/pool_test.go @@ -5,8 +5,6 @@ import ( "github.com/stretchr/testify/require" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmosquad-labs/squad/x/liquidity/types" ) @@ -40,293 +38,3 @@ func TestPoolCoinDenom(t *testing.T) { }) } } - -func TestPoolInfo_Price(t *testing.T) { - for _, tc := range []struct { - name string - rx, ry int64 // reserve balance - ps int64 // pool coin supply - p sdk.Dec // expected pool price - }{ - { - name: "normal pool", - ps: 10000, - rx: 20000, - ry: 100, - p: sdk.NewDec(200), - }, - { - name: "decimal rounding", - ps: 10000, - rx: 200, - ry: 300, - p: sdk.MustNewDecFromStr("0.666666666666666667"), - }, - } { - t.Run(tc.name, func(t *testing.T) { - pool := types.NewPoolInfo(sdk.NewInt(tc.rx), sdk.NewInt(tc.ry), sdk.NewInt(tc.ps)) - require.True(sdk.DecEq(t, tc.p, pool.Price())) - }) - } - - // panicking cases - for _, tc := range []struct { - rx, ry int64 - ps int64 - }{ - { - rx: 0, - ry: 1000, - ps: 1000, - }, - { - rx: 1000, - ry: 0, - ps: 1000, - }, - } { - t.Run("panics", func(t *testing.T) { - require.Panics(t, func() { - pool := types.NewPoolInfo(sdk.NewInt(tc.rx), sdk.NewInt(tc.ry), sdk.NewInt(tc.ps)) - pool.Price() - }) - }) - } -} - -func TestIsDepletedPool(t *testing.T) { - for _, tc := range []struct { - name string - rx, ry int64 // reserve balance - ps int64 // pool coin supply - isDepleted bool - }{ - { - name: "empty pool", - rx: 0, - ry: 0, - ps: 0, - isDepleted: true, - }, - { - name: "depleted, with some coins from outside", - rx: 100, - ry: 0, - ps: 0, - isDepleted: true, - }, - { - name: "depleted, with some coins from outside #2", - rx: 100, - ry: 100, - ps: 0, - isDepleted: true, - }, - { - name: "normal pool", - rx: 10000, - ry: 10000, - ps: 10000, - isDepleted: false, - }, - { - name: "not depleted, but reserve coins are gone", - rx: 0, - ry: 10000, - ps: 10000, - isDepleted: true, - }, - } { - t.Run(tc.name, func(t *testing.T) { - pool := types.NewPoolInfo(sdk.NewInt(tc.rx), sdk.NewInt(tc.ry), sdk.NewInt(tc.ps)) - require.Equal(t, tc.isDepleted, types.IsDepletedPool(pool)) - }) - } -} - -func TestDepositToPool(t *testing.T) { - for _, tc := range []struct { - name string - rx, ry int64 // reserve balance - ps int64 // pool coin supply - x, y int64 // depositing coin amount - ax, ay int64 // expected accepted coin amount - pc int64 // expected minted pool coin amount - }{ - // TODO: what if a pool has positive pool coin supply - // but has zero reserve balance? - { - name: "ideal deposit", - rx: 2000, - ry: 100, - ps: 10000, - x: 200, - y: 10, - ax: 200, - ay: 10, - pc: 1000, - }, - { - name: "unbalanced deposit", - rx: 2000, - ry: 100, - ps: 10000, - x: 100, - y: 2000, - ax: 100, - ay: 5, - pc: 500, - }, - { - name: "decimal truncation", - rx: 222, - ry: 333, - ps: 333, - x: 100, - y: 100, - ax: 66, - ay: 99, - pc: 99, - }, - { - name: "decimal truncation #2", - rx: 200, - ry: 300, - ps: 333, - x: 80, - y: 80, - ax: 53, - ay: 80, - pc: 88, - }, - { - name: "zero minting amount", - ps: 100, - rx: 10000, - ry: 10000, - x: 99, - y: 99, - ax: 0, - ay: 0, - pc: 0, - }, - { - name: "tiny minting amount", - rx: 10000, - ry: 10000, - ps: 100, - x: 100, - y: 100, - ax: 100, - ay: 100, - pc: 1, - }, - { - name: "tiny minting amount #2", - rx: 10000, - ry: 10000, - ps: 100, - x: 199, - y: 199, - ax: 100, - ay: 100, - pc: 1, - }, - { - name: "zero minting amount", - rx: 10000, - ry: 10000, - ps: 999, - x: 10, - y: 10, - ax: 0, - ay: 0, - pc: 0, - }, - } { - t.Run(tc.name, func(t *testing.T) { - pool := types.NewPoolInfo(sdk.NewInt(tc.rx), sdk.NewInt(tc.ry), sdk.NewInt(tc.ps)) - ax, ay, pc := types.DepositToPool(pool, sdk.NewInt(tc.x), sdk.NewInt(tc.y)) - require.True(sdk.IntEq(t, sdk.NewInt(tc.ax), ax)) - require.True(sdk.IntEq(t, sdk.NewInt(tc.ay), ay)) - require.True(sdk.IntEq(t, sdk.NewInt(tc.pc), pc)) - // Additional assertions - if !types.IsDepletedPool(pool) { - require.True(t, (ax.Int64()*tc.ps) >= (pc.Int64()*tc.rx)) // (ax / rx) > (pc / ps) - require.True(t, (ay.Int64()*tc.ps) >= (pc.Int64()*tc.ry)) // (ay / ry) > (pc / ps) - } - }) - } -} - -func TestWithdrawFromPool(t *testing.T) { - for _, tc := range []struct { - name string - rx, ry int64 // reserve balance - ps int64 // pool coin supply - pc int64 // redeeming pool coin amount - feeRate sdk.Dec - x, y int64 // withdrawn coin amount - }{ - { - name: "ideal withdraw", - rx: 2000, - ry: 100, - ps: 10000, - pc: 1000, - feeRate: sdk.ZeroDec(), - x: 200, - y: 10, - }, - { - name: "ideal withdraw - with fee", - rx: 2000, - ry: 100, - ps: 10000, - pc: 1000, - feeRate: sdk.MustNewDecFromStr("0.003"), - x: 199, - y: 9, - }, - { - name: "withdraw all", - rx: 123, - ry: 567, - ps: 10, - pc: 10, - feeRate: sdk.MustNewDecFromStr("0.003"), - x: 123, - y: 567, - }, - { - name: "advantageous for pool", - rx: 100, - ry: 100, - ps: 10000, - pc: 99, - feeRate: sdk.ZeroDec(), - x: 0, - y: 0, - }, - { - name: "advantageous for pool", - rx: 10000, - ry: 100, - ps: 10000, - pc: 99, - feeRate: sdk.ZeroDec(), - x: 99, - y: 0, - }, - } { - t.Run(tc.name, func(t *testing.T) { - pool := types.NewPoolInfo(sdk.NewInt(tc.rx), sdk.NewInt(tc.ry), sdk.NewInt(tc.ps)) - x, y := types.WithdrawFromPool(pool, sdk.NewInt(tc.pc), tc.feeRate) - require.True(sdk.IntEq(t, sdk.NewInt(tc.x), x)) - require.True(sdk.IntEq(t, sdk.NewInt(tc.y), y)) - // Additional assertions - require.True(t, (tc.pc*tc.rx) >= (x.Int64()*tc.ps)) - require.True(t, (tc.pc*tc.ry) >= (y.Int64()*tc.ps)) - }) - } -} diff --git a/x/liquidity/types/request_test.go b/x/liquidity/types/request_test.go index e1a695e7..287c33f5 100644 --- a/x/liquidity/types/request_test.go +++ b/x/liquidity/types/request_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/crypto" + squad "github.com/cosmosquad-labs/squad/types" "github.com/cosmosquad-labs/squad/x/liquidity/types" ) @@ -60,7 +61,7 @@ func TestDepositRequest_Validate(t *testing.T) { { "wrong number of deposit coins", func(req *types.DepositRequest) { - req.DepositCoins = parseCoins("1000000denom1") + req.DepositCoins = squad.ParseCoins("1000000denom1") }, "wrong number of deposit coins: 1", }, @@ -74,7 +75,7 @@ func TestDepositRequest_Validate(t *testing.T) { { "wrong number of accepted coins", func(req *types.DepositRequest) { - req.AcceptedCoins = parseCoins("1000000denom1") + req.AcceptedCoins = squad.ParseCoins("1000000denom1") }, "wrong number of accepted coins: 1", }, @@ -88,7 +89,7 @@ func TestDepositRequest_Validate(t *testing.T) { { "zero minted pool coin", func(req *types.DepositRequest) { - req.MintedPoolCoin = parseCoin("0pool1") + req.MintedPoolCoin = squad.ParseCoin("0pool1") }, "", }, @@ -103,7 +104,7 @@ func TestDepositRequest_Validate(t *testing.T) { t.Run(tc.name, func(t *testing.T) { pool := types.NewPool(1, 1) depositor := sdk.AccAddress(crypto.AddressHash([]byte("depositor"))) - msg := types.NewMsgDeposit(depositor, 1, parseCoins("1000000denom1,1000000denom2")) + msg := types.NewMsgDeposit(depositor, 1, squad.ParseCoins("1000000denom1,1000000denom2")) req := types.NewDepositRequest(msg, pool, 1, 1) tc.malleate(&req) err := req.Validate() @@ -165,7 +166,7 @@ func TestWithdrawRequest_Validate(t *testing.T) { { "zero pool coin", func(req *types.WithdrawRequest) { - req.PoolCoin = parseCoin("0pool1") + req.PoolCoin = squad.ParseCoin("0pool1") }, "pool coin must not be 0", }, @@ -179,14 +180,14 @@ func TestWithdrawRequest_Validate(t *testing.T) { { "valid withdrawn coins", func(req *types.WithdrawRequest) { - req.WithdrawnCoins = parseCoins("1000000denom1") + req.WithdrawnCoins = squad.ParseCoins("1000000denom1") }, "", }, { "wrong number of withdrawn coins", func(req *types.WithdrawRequest) { - req.WithdrawnCoins = parseCoins("100000denom1,1000000denom2,1000000denom3") + req.WithdrawnCoins = squad.ParseCoins("100000denom1,1000000denom2,1000000denom3") }, "wrong number of withdrawn coins: 3", }, @@ -200,7 +201,7 @@ func TestWithdrawRequest_Validate(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { withdrawer := sdk.AccAddress(crypto.AddressHash([]byte("withdrawer"))) - msg := types.NewMsgWithdraw(withdrawer, 1, parseCoin("1000pool1")) + msg := types.NewMsgWithdraw(withdrawer, 1, squad.ParseCoin("1000pool1")) req := types.NewWithdrawRequest(msg, 1, 1) tc.malleate(&req) err := req.Validate() @@ -269,7 +270,7 @@ func TestSwapRequest_Validate(t *testing.T) { { "zero offer coin", func(req *types.SwapRequest) { - req.OfferCoin = parseCoin("0denom1") + req.OfferCoin = squad.ParseCoin("0denom1") }, "offer coin must not be 0", }, @@ -283,7 +284,7 @@ func TestSwapRequest_Validate(t *testing.T) { { "zero remaining offer coin", func(req *types.SwapRequest) { - req.RemainingOfferCoin = parseCoin("0denom1") + req.RemainingOfferCoin = squad.ParseCoin("0denom1") }, "", }, @@ -297,7 +298,7 @@ func TestSwapRequest_Validate(t *testing.T) { { "zero received coin", func(req *types.SwapRequest) { - req.ReceivedCoin = parseCoin("0denom1") + req.ReceivedCoin = squad.ParseCoin("0denom1") }, "", }, @@ -348,10 +349,10 @@ func TestSwapRequest_Validate(t *testing.T) { pair := types.NewPair(1, "denom1", "denom2") orderer := sdk.AccAddress(crypto.AddressHash([]byte("orderer"))) msg := types.NewMsgLimitOrder( - orderer, pair.Id, types.SwapDirectionBuy, parseCoin("1000000denom2"), - "denom1", parseDec("1.0"), newInt(1000000), types.DefaultMaxOrderLifespan) - expireAt := parseTime("2022-01-01T00:00:00Z") - req := types.NewSwapRequestForLimitOrder(msg, 1, pair, parseCoin("1000000denom2"), expireAt, 1) + orderer, pair.Id, types.SwapDirectionBuy, squad.ParseCoin("1000000denom2"), + "denom1", squad.ParseDec("1.0"), newInt(1000000), types.DefaultMaxOrderLifespan) + expireAt := squad.ParseTime("2022-01-01T00:00:00Z") + req := types.NewSwapRequestForLimitOrder(msg, 1, pair, squad.ParseCoin("1000000denom2"), expireAt, 1) tc.malleate(&req) err := req.Validate() if tc.expectedErr == "" { diff --git a/x/liquidity/types/tick_test.go b/x/liquidity/types/tick_test.go deleted file mode 100644 index bc97d916..00000000 --- a/x/liquidity/types/tick_test.go +++ /dev/null @@ -1,177 +0,0 @@ -package types_test - -import ( - "testing" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/require" - - "github.com/cosmosquad-labs/squad/x/liquidity/types" -) - -var tickPrec = int(types.DefaultTickPrecision) - -func TestPriceToTick(t *testing.T) { - for _, tc := range []struct { - price sdk.Dec - expected sdk.Dec - }{ - {parseDec("0.000000000000099999"), parseDec("0.00000000000009999")}, - {parseDec("1.999999999999999999"), parseDec("1.999")}, - {parseDec("99.999999999999999999"), parseDec("99.99")}, - {parseDec("100.999999999999999999"), parseDec("100.9")}, - {parseDec("9999.999999999999999999"), parseDec("9999")}, - {parseDec("10019"), parseDec("10010")}, - {parseDec("1000100005"), parseDec("1000000000")}, - } { - require.True(sdk.DecEq(t, tc.expected, types.PriceToTick(tc.price, tickPrec))) - } -} - -func TestTick(t *testing.T) { - for _, tc := range []struct { - i int - prec int - expected sdk.Dec - }{ - {0, tickPrec, sdk.NewDecWithPrec(1, int64(sdk.Precision-tickPrec))}, - {1, tickPrec, parseDec("0.000000000000001001")}, - {8999, tickPrec, parseDec("0.000000000000009999")}, - {9000, tickPrec, parseDec("0.000000000000010000")}, - {9001, tickPrec, parseDec("0.000000000000010010")}, - {17999, tickPrec, parseDec("0.000000000000099990")}, - {18000, tickPrec, parseDec("0.000000000000100000")}, - {135000, tickPrec, sdk.NewDec(1)}, - {135001, tickPrec, parseDec("1.001")}, - } { - t.Run("", func(t *testing.T) { - res := types.TickFromIndex(tc.i, tc.prec) - require.True(sdk.DecEq(t, tc.expected, res)) - require.Equal(t, tc.i, types.TickToIndex(res, tc.prec)) - }) - } -} - -func TestUpTick(t *testing.T) { - for _, tc := range []struct { - price sdk.Dec - prec int - expected sdk.Dec - }{ - {parseDec("1000000000000000000"), tickPrec, parseDec("1001000000000000000")}, - {parseDec("1000"), tickPrec, parseDec("1001")}, - {parseDec("999.9"), tickPrec, parseDec("1000")}, - {parseDec("999.0"), tickPrec, parseDec("999.1")}, - {parseDec("1.100"), tickPrec, parseDec("1.101")}, - {parseDec("1.000"), tickPrec, parseDec("1.001")}, - {parseDec("0.9999"), tickPrec, parseDec("1.000")}, - {parseDec("0.1000"), tickPrec, parseDec("0.1001")}, - {parseDec("0.09999"), tickPrec, parseDec("0.1000")}, - {parseDec("0.09997"), tickPrec, parseDec("0.09998")}, - } { - t.Run("", func(t *testing.T) { - require.True(sdk.DecEq(t, tc.expected, types.UpTick(tc.price, tc.prec))) - }) - } -} - -func TestDownTick(t *testing.T) { - for _, tc := range []struct { - price sdk.Dec - prec int - expected sdk.Dec - }{ - {parseDec("1000000000000000000"), tickPrec, parseDec("999900000000000000")}, - {parseDec("10010"), tickPrec, parseDec("10000")}, - {parseDec("100.0"), tickPrec, parseDec("99.99")}, - {parseDec("99.99"), tickPrec, parseDec("99.98")}, - {parseDec("1.000"), tickPrec, parseDec("0.9999")}, - {parseDec("0.9990"), tickPrec, parseDec("0.9989")}, - {parseDec("0.9999"), tickPrec, parseDec("0.9998")}, - {parseDec("0.1"), tickPrec, parseDec("0.09999")}, - {parseDec("0.00000000000001000"), tickPrec, parseDec("0.000000000000009999")}, - {parseDec("0.000000000000001001"), tickPrec, parseDec("0.000000000000001000")}, - } { - t.Run("", func(t *testing.T) { - require.True(sdk.DecEq(t, tc.expected, types.DownTick(tc.price, tc.prec))) - }) - } -} - -func TestLowestTick(t *testing.T) { - for _, tc := range []struct { - prec int - expected sdk.Dec - }{ - {0, sdk.NewDecWithPrec(1, 18)}, - {tickPrec, sdk.NewDecWithPrec(1, 15)}, - } { - t.Run("", func(t *testing.T) { - require.True(sdk.DecEq(t, tc.expected, types.LowestTick(tc.prec))) - }) - } -} - -func TestPriceToUpTick(t *testing.T) { - for _, tc := range []struct { - price sdk.Dec - prec int - expected sdk.Dec - }{ - {parseDec("1.0015"), tickPrec, parseDec("1.002")}, - {parseDec("100"), tickPrec, parseDec("100")}, - {parseDec("100.01"), tickPrec, parseDec("100.1")}, - {parseDec("100.099"), tickPrec, parseDec("100.1")}, - } { - t.Run("", func(t *testing.T) { - require.True(sdk.DecEq(t, tc.expected, types.PriceToUpTick(tc.price, tc.prec))) - }) - } -} - -func TestRoundTickIndex(t *testing.T) { - for _, tc := range []struct { - i int - expected int - }{ - {0, 0}, - {1, 2}, - {2, 2}, - {3, 4}, - {4, 4}, - {5, 6}, - {6, 6}, - {7, 8}, - {8, 8}, - {9, 10}, - {10, 10}, - } { - t.Run("", func(t *testing.T) { - require.Equal(t, tc.expected, types.RoundTickIndex(tc.i)) - }) - } -} - -func TestRoundPrice(t *testing.T) { - for _, tc := range []struct { - price sdk.Dec - prec int - expected sdk.Dec - }{ - {parseDec("0.000000000000001000"), tickPrec, parseDec("0.000000000000001000")}, - {parseDec("0.000000000000010000"), tickPrec, parseDec("0.000000000000010000")}, - {parseDec("0.000000000000010005"), tickPrec, parseDec("0.000000000000010000")}, - {parseDec("0.000000000000010015"), tickPrec, parseDec("0.000000000000010020")}, - {parseDec("0.000000000000010025"), tickPrec, parseDec("0.000000000000010020")}, - {parseDec("0.000000000000010035"), tickPrec, parseDec("0.000000000000010040")}, - {parseDec("0.000000000000010045"), tickPrec, parseDec("0.000000000000010040")}, - {parseDec("1.0005"), tickPrec, parseDec("1.0")}, - {parseDec("1.0015"), tickPrec, parseDec("1.002")}, - {parseDec("1.0025"), tickPrec, parseDec("1.002")}, - {parseDec("1.0035"), tickPrec, parseDec("1.004")}, - } { - t.Run("", func(t *testing.T) { - require.True(sdk.DecEq(t, tc.expected, types.RoundPrice(tc.price, tc.prec))) - }) - } -}