github.com/gravity-devs/liquidity@v1.5.3/x/liquidity/types/swap.go (about)

     1  package types
     2  
     3  import (
     4  	"sort"
     5  
     6  	sdk "github.com/cosmos/cosmos-sdk/types"
     7  )
     8  
     9  // Type of match
    10  type MatchType int
    11  
    12  const (
    13  	ExactMatch MatchType = iota + 1
    14  	NoMatch
    15  	FractionalMatch
    16  )
    17  
    18  // Direction of price
    19  type PriceDirection int
    20  
    21  const (
    22  	Increasing PriceDirection = iota + 1
    23  	Decreasing
    24  	Staying
    25  )
    26  
    27  // Direction of order
    28  type OrderDirection int
    29  
    30  const (
    31  	DirectionXtoY OrderDirection = iota + 1
    32  	DirectionYtoX
    33  )
    34  
    35  // Type of order map to index at price, having the pointer list of the swap batch message.
    36  type Order struct {
    37  	Price         sdk.Dec
    38  	BuyOfferAmt   sdk.Int
    39  	SellOfferAmt  sdk.Int
    40  	SwapMsgStates []*SwapMsgState
    41  }
    42  
    43  // OrderBook is a list of orders
    44  type OrderBook []Order
    45  
    46  // Len implements sort.Interface for OrderBook
    47  func (orderBook OrderBook) Len() int { return len(orderBook) }
    48  
    49  // Less implements sort.Interface for OrderBook
    50  func (orderBook OrderBook) Less(i, j int) bool {
    51  	return orderBook[i].Price.LT(orderBook[j].Price)
    52  }
    53  
    54  // Swap implements sort.Interface for OrderBook
    55  func (orderBook OrderBook) Swap(i, j int) { orderBook[i], orderBook[j] = orderBook[j], orderBook[i] }
    56  
    57  // increasing sort orderbook by order price
    58  func (orderBook OrderBook) Sort() {
    59  	sort.Slice(orderBook, func(i, j int) bool {
    60  		return orderBook[i].Price.LT(orderBook[j].Price)
    61  	})
    62  }
    63  
    64  // decreasing sort orderbook by order price
    65  func (orderBook OrderBook) Reverse() {
    66  	sort.Slice(orderBook, func(i, j int) bool {
    67  		return orderBook[i].Price.GT(orderBook[j].Price)
    68  	})
    69  }
    70  
    71  // Get number of not matched messages on the list.
    72  func CountNotMatchedMsgs(swapMsgStates []*SwapMsgState) int {
    73  	cnt := 0
    74  	for _, m := range swapMsgStates {
    75  		if m.Executed && !m.Succeeded {
    76  			cnt++
    77  		}
    78  	}
    79  	return cnt
    80  }
    81  
    82  // Get number of fractional matched messages on the list.
    83  func CountFractionalMatchedMsgs(swapMsgStates []*SwapMsgState) int {
    84  	cnt := 0
    85  	for _, m := range swapMsgStates {
    86  		if m.Executed && m.Succeeded && !m.ToBeDeleted {
    87  			cnt++
    88  		}
    89  	}
    90  	return cnt
    91  }
    92  
    93  // Order map type indexed by order price at price
    94  type OrderMap map[string]Order
    95  
    96  // Make orderbook by sort orderMap.
    97  func (orderMap OrderMap) SortOrderBook() (orderBook OrderBook) {
    98  	for _, o := range orderMap {
    99  		orderBook = append(orderBook, o)
   100  	}
   101  	orderBook.Sort()
   102  	return orderBook
   103  }
   104  
   105  // struct of swap matching result of the batch
   106  type BatchResult struct {
   107  	MatchType      MatchType
   108  	PriceDirection PriceDirection
   109  	SwapPrice      sdk.Dec
   110  	EX             sdk.Dec
   111  	EY             sdk.Dec
   112  	OriginalEX     sdk.Int
   113  	OriginalEY     sdk.Int
   114  	PoolX          sdk.Dec
   115  	PoolY          sdk.Dec
   116  	TransactAmt    sdk.Dec
   117  }
   118  
   119  // return of zero object, to avoid nil
   120  func NewBatchResult() BatchResult {
   121  	return BatchResult{
   122  		SwapPrice:   sdk.ZeroDec(),
   123  		EX:          sdk.ZeroDec(),
   124  		EY:          sdk.ZeroDec(),
   125  		OriginalEX:  sdk.ZeroInt(),
   126  		OriginalEY:  sdk.ZeroInt(),
   127  		PoolX:       sdk.ZeroDec(),
   128  		PoolY:       sdk.ZeroDec(),
   129  		TransactAmt: sdk.ZeroDec(),
   130  	}
   131  }
   132  
   133  // struct of swap matching result of each Batch swap message
   134  type MatchResult struct {
   135  	OrderDirection         OrderDirection
   136  	OrderMsgIndex          uint64
   137  	OrderPrice             sdk.Dec
   138  	OfferCoinAmt           sdk.Dec
   139  	TransactedCoinAmt      sdk.Dec
   140  	ExchangedDemandCoinAmt sdk.Dec
   141  	OfferCoinFeeAmt        sdk.Dec
   142  	ExchangedCoinFeeAmt    sdk.Dec
   143  	SwapMsgState           *SwapMsgState
   144  }
   145  
   146  // The price and coins of swap messages in orderbook are calculated
   147  // to derive match result with the price direction.
   148  func (orderBook OrderBook) Match(x, y sdk.Dec) (BatchResult, bool) {
   149  	currentPrice := x.Quo(y)
   150  	priceDirection := orderBook.PriceDirection(currentPrice)
   151  	if priceDirection == Staying {
   152  		return orderBook.CalculateMatchStay(currentPrice), true
   153  	}
   154  	return orderBook.CalculateMatch(priceDirection, x, y)
   155  }
   156  
   157  // Check orderbook validity naively
   158  func (orderBook OrderBook) Validate(currentPrice sdk.Dec) bool {
   159  	if !currentPrice.IsPositive() {
   160  		return false
   161  	}
   162  	maxBuyOrderPrice := sdk.ZeroDec()
   163  	minSellOrderPrice := sdk.NewDec(1000000000000)
   164  	for _, order := range orderBook {
   165  		if order.BuyOfferAmt.IsPositive() && order.Price.GT(maxBuyOrderPrice) {
   166  			maxBuyOrderPrice = order.Price
   167  		}
   168  		if order.SellOfferAmt.IsPositive() && (order.Price.LT(minSellOrderPrice)) {
   169  			minSellOrderPrice = order.Price
   170  		}
   171  	}
   172  	if maxBuyOrderPrice.GT(minSellOrderPrice) ||
   173  		maxBuyOrderPrice.Quo(currentPrice).GT(sdk.MustNewDecFromStr("1.10")) ||
   174  		minSellOrderPrice.Quo(currentPrice).LT(sdk.MustNewDecFromStr("0.90")) {
   175  		return false
   176  	}
   177  	return true
   178  }
   179  
   180  // Calculate results for orderbook matching with unchanged price case
   181  func (orderBook OrderBook) CalculateMatchStay(currentPrice sdk.Dec) (r BatchResult) {
   182  	r = NewBatchResult()
   183  	r.SwapPrice = currentPrice
   184  	r.OriginalEX, r.OriginalEY = orderBook.ExecutableAmt(r.SwapPrice)
   185  	r.EX = r.OriginalEX.ToDec()
   186  	r.EY = r.OriginalEY.ToDec()
   187  	r.PriceDirection = Staying
   188  
   189  	s := r.SwapPrice.Mul(r.EY)
   190  	if r.EX.IsZero() || r.EY.IsZero() {
   191  		r.MatchType = NoMatch
   192  	} else if r.EX.Equal(s) { // Normalization to an integrator for easy determination of exactMatch
   193  		r.MatchType = ExactMatch
   194  	} else {
   195  		// Decimal Error, When calculating the Executable value, conservatively Truncated decimal
   196  		r.MatchType = FractionalMatch
   197  		if r.EX.GT(s) {
   198  			r.EX = s
   199  		} else if r.EX.LT(s) {
   200  			r.EY = r.EX.Quo(r.SwapPrice)
   201  		}
   202  	}
   203  	return
   204  }
   205  
   206  // Calculates the batch results with the logic for each direction
   207  func (orderBook OrderBook) CalculateMatch(direction PriceDirection, x, y sdk.Dec) (maxScenario BatchResult, found bool) {
   208  	currentPrice := x.Quo(y)
   209  	lastOrderPrice := currentPrice
   210  	var matchScenarios []BatchResult
   211  	start, end, delta := 0, len(orderBook)-1, 1
   212  	if direction == Decreasing {
   213  		start, end, delta = end, start, -1
   214  	}
   215  	for i := start; i != end+delta; i += delta {
   216  		order := orderBook[i]
   217  		if (direction == Increasing && order.Price.LT(currentPrice)) ||
   218  			(direction == Decreasing && order.Price.GT(currentPrice)) {
   219  			continue
   220  		} else {
   221  			orderPrice := order.Price
   222  			r := orderBook.CalculateSwap(direction, x, y, orderPrice, lastOrderPrice)
   223  			// Check to see if it exceeds a value that can be a decimal error
   224  			if (direction == Increasing && r.PoolY.Sub(r.EX.Quo(r.SwapPrice)).GTE(sdk.OneDec())) ||
   225  				(direction == Decreasing && r.PoolX.Sub(r.EY.Mul(r.SwapPrice)).GTE(sdk.OneDec())) {
   226  				continue
   227  			}
   228  			matchScenarios = append(matchScenarios, r)
   229  			lastOrderPrice = orderPrice
   230  		}
   231  	}
   232  	maxScenario = NewBatchResult()
   233  	for _, s := range matchScenarios {
   234  		MEX, MEY := orderBook.MustExecutableAmt(s.SwapPrice)
   235  		if s.EX.GTE(MEX.ToDec()) && s.EY.GTE(MEY.ToDec()) {
   236  			if s.MatchType == ExactMatch && s.TransactAmt.IsPositive() {
   237  				maxScenario = s
   238  				found = true
   239  				break
   240  			} else if s.TransactAmt.GT(maxScenario.TransactAmt) {
   241  				maxScenario = s
   242  				found = true
   243  			}
   244  		}
   245  	}
   246  	maxScenario.PriceDirection = direction
   247  	return maxScenario, found
   248  }
   249  
   250  // CalculateSwap calculates the batch result.
   251  func (orderBook OrderBook) CalculateSwap(direction PriceDirection, x, y, orderPrice, lastOrderPrice sdk.Dec) BatchResult {
   252  	r := NewBatchResult()
   253  	r.OriginalEX, r.OriginalEY = orderBook.ExecutableAmt(lastOrderPrice.Add(orderPrice).Quo(sdk.NewDec(2)))
   254  	r.EX = r.OriginalEX.ToDec()
   255  	r.EY = r.OriginalEY.ToDec()
   256  
   257  	r.SwapPrice = x.Add(r.EX.MulInt64(2)).Quo(y.Add(r.EY.MulInt64(2))) // P_s = (X + 2EX) / (Y + 2EY)
   258  
   259  	if direction == Increasing {
   260  		r.PoolY = r.SwapPrice.Mul(y).Sub(x).Quo(r.SwapPrice.MulInt64(2)) // (P_s * Y - X / 2P_s)
   261  		if lastOrderPrice.LT(r.SwapPrice) && r.SwapPrice.LT(orderPrice) && !r.PoolY.IsNegative() {
   262  			if r.EX.IsZero() && r.EY.IsZero() {
   263  				r.MatchType = NoMatch
   264  			} else {
   265  				r.MatchType = ExactMatch
   266  			}
   267  		}
   268  	} else if direction == Decreasing {
   269  		r.PoolX = x.Sub(r.SwapPrice.Mul(y)).QuoInt64(2) // (X - P_s * Y) / 2
   270  		if orderPrice.LT(r.SwapPrice) && r.SwapPrice.LT(lastOrderPrice) && !r.PoolX.IsNegative() {
   271  			if r.EX.IsZero() && r.EY.IsZero() {
   272  				r.MatchType = NoMatch
   273  			} else {
   274  				r.MatchType = ExactMatch
   275  			}
   276  		}
   277  	}
   278  
   279  	if r.MatchType == 0 {
   280  		r.OriginalEX, r.OriginalEY = orderBook.ExecutableAmt(orderPrice)
   281  		r.EX = r.OriginalEX.ToDec()
   282  		r.EY = r.OriginalEY.ToDec()
   283  		r.SwapPrice = orderPrice
   284  		// When calculating the Pool value, conservatively Truncated decimal, so Ceil it to reduce the decimal error
   285  		if direction == Increasing {
   286  			r.PoolY = r.SwapPrice.Mul(y).Sub(x).Quo(r.SwapPrice.MulInt64(2)) // (P_s * Y - X) / 2P_s
   287  			r.EX = sdk.MinDec(r.EX, r.EY.Add(r.PoolY).Mul(r.SwapPrice)).Ceil()
   288  			r.EY = sdk.MaxDec(sdk.MinDec(r.EY, r.EX.Quo(r.SwapPrice).Sub(r.PoolY)), sdk.ZeroDec()).Ceil()
   289  		} else if direction == Decreasing {
   290  			r.PoolX = x.Sub(r.SwapPrice.Mul(y)).QuoInt64(2) // (X - P_s * Y) / 2
   291  			r.EY = sdk.MinDec(r.EY, r.EX.Add(r.PoolX).Quo(r.SwapPrice)).Ceil()
   292  			r.EX = sdk.MaxDec(sdk.MinDec(r.EX, r.EY.Mul(r.SwapPrice).Sub(r.PoolX)), sdk.ZeroDec()).Ceil()
   293  		}
   294  		r.MatchType = FractionalMatch
   295  	}
   296  
   297  	if direction == Increasing {
   298  		if r.SwapPrice.LT(x.Quo(y)) || r.PoolY.IsNegative() {
   299  			r.TransactAmt = sdk.ZeroDec()
   300  		} else {
   301  			r.TransactAmt = sdk.MinDec(r.EX, r.EY.Add(r.PoolY).Mul(r.SwapPrice))
   302  		}
   303  	} else if direction == Decreasing {
   304  		if r.SwapPrice.GT(x.Quo(y)) || r.PoolX.IsNegative() {
   305  			r.TransactAmt = sdk.ZeroDec()
   306  		} else {
   307  			r.TransactAmt = sdk.MinDec(r.EY, r.EX.Add(r.PoolX).Quo(r.SwapPrice))
   308  		}
   309  	}
   310  	return r
   311  }
   312  
   313  // Get Price direction of the orderbook with current Price
   314  func (orderBook OrderBook) PriceDirection(currentPrice sdk.Dec) PriceDirection {
   315  	buyAmtOverCurrentPrice := sdk.ZeroDec()
   316  	buyAmtAtCurrentPrice := sdk.ZeroDec()
   317  	sellAmtUnderCurrentPrice := sdk.ZeroDec()
   318  	sellAmtAtCurrentPrice := sdk.ZeroDec()
   319  
   320  	for _, order := range orderBook {
   321  		if order.Price.GT(currentPrice) {
   322  			buyAmtOverCurrentPrice = buyAmtOverCurrentPrice.Add(order.BuyOfferAmt.ToDec())
   323  		} else if order.Price.Equal(currentPrice) {
   324  			buyAmtAtCurrentPrice = buyAmtAtCurrentPrice.Add(order.BuyOfferAmt.ToDec())
   325  			sellAmtAtCurrentPrice = sellAmtAtCurrentPrice.Add(order.SellOfferAmt.ToDec())
   326  		} else if order.Price.LT(currentPrice) {
   327  			sellAmtUnderCurrentPrice = sellAmtUnderCurrentPrice.Add(order.SellOfferAmt.ToDec())
   328  		}
   329  	}
   330  	if buyAmtOverCurrentPrice.GT(currentPrice.Mul(sellAmtUnderCurrentPrice.Add(sellAmtAtCurrentPrice))) {
   331  		return Increasing
   332  	} else if currentPrice.Mul(sellAmtUnderCurrentPrice).GT(buyAmtOverCurrentPrice.Add(buyAmtAtCurrentPrice)) {
   333  		return Decreasing
   334  	}
   335  	return Staying
   336  }
   337  
   338  // calculate the executable amount of the orderbook for each X, Y
   339  func (orderBook OrderBook) ExecutableAmt(swapPrice sdk.Dec) (executableBuyAmtX, executableSellAmtY sdk.Int) {
   340  	executableBuyAmtX = sdk.ZeroInt()
   341  	executableSellAmtY = sdk.ZeroInt()
   342  	for _, order := range orderBook {
   343  		if order.Price.GTE(swapPrice) {
   344  			executableBuyAmtX = executableBuyAmtX.Add(order.BuyOfferAmt)
   345  		}
   346  		if order.Price.LTE(swapPrice) {
   347  			executableSellAmtY = executableSellAmtY.Add(order.SellOfferAmt)
   348  		}
   349  	}
   350  	return
   351  }
   352  
   353  // Check swap executable amount validity of the orderbook
   354  func (orderBook OrderBook) MustExecutableAmt(swapPrice sdk.Dec) (mustExecutableBuyAmtX, mustExecutableSellAmtY sdk.Int) {
   355  	mustExecutableBuyAmtX = sdk.ZeroInt()
   356  	mustExecutableSellAmtY = sdk.ZeroInt()
   357  	for _, order := range orderBook {
   358  		if order.Price.GT(swapPrice) {
   359  			mustExecutableBuyAmtX = mustExecutableBuyAmtX.Add(order.BuyOfferAmt)
   360  		}
   361  		if order.Price.LT(swapPrice) {
   362  			mustExecutableSellAmtY = mustExecutableSellAmtY.Add(order.SellOfferAmt)
   363  		}
   364  	}
   365  	return
   366  }
   367  
   368  // make orderMap key as swap price, value as Buy, Sell Amount from swap msgs, with split as Buy xToY, Sell yToX msg list.
   369  func MakeOrderMap(swapMsgs []*SwapMsgState, denomX, denomY string, onlyNotMatched bool) (OrderMap, []*SwapMsgState, []*SwapMsgState) {
   370  	orderMap := make(OrderMap)
   371  	var xToY []*SwapMsgState // buying Y from X
   372  	var yToX []*SwapMsgState // selling Y for X
   373  	for _, m := range swapMsgs {
   374  		if onlyNotMatched && (m.ToBeDeleted || m.RemainingOfferCoin.IsZero()) {
   375  			continue
   376  		}
   377  		order := Order{
   378  			Price:        m.Msg.OrderPrice,
   379  			BuyOfferAmt:  sdk.ZeroInt(),
   380  			SellOfferAmt: sdk.ZeroInt(),
   381  		}
   382  		orderPriceString := m.Msg.OrderPrice.String()
   383  		switch {
   384  		// buying Y from X
   385  		case m.Msg.OfferCoin.Denom == denomX:
   386  			xToY = append(xToY, m)
   387  			if o, ok := orderMap[orderPriceString]; ok {
   388  				order = o
   389  				order.BuyOfferAmt = o.BuyOfferAmt.Add(m.RemainingOfferCoin.Amount)
   390  			} else {
   391  				order.BuyOfferAmt = m.RemainingOfferCoin.Amount
   392  			}
   393  		// selling Y for X
   394  		case m.Msg.OfferCoin.Denom == denomY:
   395  			yToX = append(yToX, m)
   396  			if o, ok := orderMap[orderPriceString]; ok {
   397  				order = o
   398  				order.SellOfferAmt = o.SellOfferAmt.Add(m.RemainingOfferCoin.Amount)
   399  			} else {
   400  				order.SellOfferAmt = m.RemainingOfferCoin.Amount
   401  			}
   402  		default:
   403  			panic(ErrInvalidDenom)
   404  		}
   405  		order.SwapMsgStates = append(order.SwapMsgStates, m)
   406  		orderMap[orderPriceString] = order
   407  	}
   408  	return orderMap, xToY, yToX
   409  }
   410  
   411  // check validity state of the batch swap messages, and set to delete state to height timeout expired order
   412  func ValidateStateAndExpireOrders(swapMsgStates []*SwapMsgState, currentHeight int64, expireThisHeight bool) {
   413  	for _, order := range swapMsgStates {
   414  		if !order.Executed {
   415  			panic("not executed")
   416  		}
   417  		if order.RemainingOfferCoin.IsZero() {
   418  			if !order.Succeeded || !order.ToBeDeleted {
   419  				panic("broken state consistency for not matched order")
   420  			}
   421  			continue
   422  		}
   423  		// set toDelete, expired msgs
   424  		if currentHeight > order.OrderExpiryHeight {
   425  			if order.Succeeded || !order.ToBeDeleted {
   426  				panic("broken state consistency for fractional matched order")
   427  			}
   428  			continue
   429  		}
   430  		if expireThisHeight && currentHeight == order.OrderExpiryHeight {
   431  			order.ToBeDeleted = true
   432  		}
   433  	}
   434  }
   435  
   436  // Check swap price validity using list of match result.
   437  func CheckSwapPrice(matchResultXtoY, matchResultYtoX []MatchResult, swapPrice sdk.Dec) bool {
   438  	if len(matchResultXtoY) == 0 && len(matchResultYtoX) == 0 {
   439  		return true
   440  	}
   441  	// Check if it is greater than a value that can be a decimal error
   442  	for _, m := range matchResultXtoY {
   443  		if m.TransactedCoinAmt.Quo(swapPrice).Sub(m.ExchangedDemandCoinAmt).Abs().GT(sdk.OneDec()) {
   444  			return false
   445  		}
   446  	}
   447  	for _, m := range matchResultYtoX {
   448  		if m.TransactedCoinAmt.Mul(swapPrice).Sub(m.ExchangedDemandCoinAmt).Abs().GT(sdk.OneDec()) {
   449  			return false
   450  		}
   451  	}
   452  	return !swapPrice.IsZero()
   453  }
   454  
   455  // Find matched orders and set status for msgs
   456  func FindOrderMatch(direction OrderDirection, swapMsgStates []*SwapMsgState, executableAmt, swapPrice sdk.Dec, height int64) (
   457  	matchResults []MatchResult, poolXDelta, poolYDelta sdk.Dec) {
   458  	poolXDelta = sdk.ZeroDec()
   459  	poolYDelta = sdk.ZeroDec()
   460  
   461  	if executableAmt.IsZero() {
   462  		return
   463  	}
   464  
   465  	if direction == DirectionXtoY {
   466  		sort.SliceStable(swapMsgStates, func(i, j int) bool {
   467  			return swapMsgStates[i].Msg.OrderPrice.GT(swapMsgStates[j].Msg.OrderPrice)
   468  		})
   469  	} else if direction == DirectionYtoX {
   470  		sort.SliceStable(swapMsgStates, func(i, j int) bool {
   471  			return swapMsgStates[i].Msg.OrderPrice.LT(swapMsgStates[j].Msg.OrderPrice)
   472  		})
   473  	}
   474  
   475  	matchAmt := sdk.ZeroInt()
   476  	accumMatchAmt := sdk.ZeroInt()
   477  	var matchedSwapMsgStates []*SwapMsgState //nolint:prealloc
   478  
   479  	for i, order := range swapMsgStates {
   480  		// include the matched order in matchAmt, matchedSwapMsgStates
   481  		if (direction == DirectionXtoY && order.Msg.OrderPrice.LT(swapPrice)) ||
   482  			(direction == DirectionYtoX && order.Msg.OrderPrice.GT(swapPrice)) {
   483  			break
   484  		}
   485  
   486  		matchAmt = matchAmt.Add(order.RemainingOfferCoin.Amount)
   487  		matchedSwapMsgStates = append(matchedSwapMsgStates, order)
   488  
   489  		if i == len(swapMsgStates)-1 || !swapMsgStates[i+1].Msg.OrderPrice.Equal(order.Msg.OrderPrice) {
   490  			if matchAmt.IsPositive() {
   491  				var fractionalMatchRatio sdk.Dec
   492  				if accumMatchAmt.Add(matchAmt).ToDec().GTE(executableAmt) {
   493  					fractionalMatchRatio = executableAmt.Sub(accumMatchAmt.ToDec()).Quo(matchAmt.ToDec())
   494  					if fractionalMatchRatio.GT(sdk.NewDec(1)) {
   495  						panic("fractionalMatchRatio should be between 0 and 1")
   496  					}
   497  				} else {
   498  					fractionalMatchRatio = sdk.OneDec()
   499  				}
   500  				if !fractionalMatchRatio.IsPositive() {
   501  					fractionalMatchRatio = sdk.OneDec()
   502  				}
   503  				for _, matchOrder := range matchedSwapMsgStates {
   504  					offerAmt := matchOrder.RemainingOfferCoin.Amount.ToDec()
   505  					matchResult := MatchResult{
   506  						OrderDirection: direction,
   507  						OfferCoinAmt:   offerAmt,
   508  						// TransactedCoinAmt is a value that should not be lost, so Ceil it conservatively considering the decimal error.
   509  						TransactedCoinAmt: offerAmt.Mul(fractionalMatchRatio).Ceil(),
   510  						SwapMsgState:      matchOrder,
   511  					}
   512  					if matchResult.OfferCoinAmt.Sub(matchResult.TransactedCoinAmt).LTE(sdk.OneDec()) {
   513  						// Use ReservedOfferCoinFee to avoid decimal errors when OfferCoinAmt and TransactedCoinAmt are almost equal in value.
   514  						matchResult.OfferCoinFeeAmt = matchResult.SwapMsgState.ReservedOfferCoinFee.Amount.ToDec()
   515  					} else {
   516  						matchResult.OfferCoinFeeAmt = matchResult.SwapMsgState.ReservedOfferCoinFee.Amount.ToDec().Mul(fractionalMatchRatio)
   517  					}
   518  					if direction == DirectionXtoY {
   519  						matchResult.ExchangedDemandCoinAmt = matchResult.TransactedCoinAmt.Quo(swapPrice)
   520  						matchResult.ExchangedCoinFeeAmt = matchResult.OfferCoinFeeAmt.Quo(swapPrice)
   521  					} else if direction == DirectionYtoX {
   522  						matchResult.ExchangedDemandCoinAmt = matchResult.TransactedCoinAmt.Mul(swapPrice)
   523  						matchResult.ExchangedCoinFeeAmt = matchResult.OfferCoinFeeAmt.Mul(swapPrice)
   524  					}
   525  					// Check for differences above maximum decimal error
   526  					if matchResult.TransactedCoinAmt.GT(matchResult.OfferCoinAmt) {
   527  						panic("bad TransactedCoinAmt")
   528  					}
   529  					if matchResult.OfferCoinFeeAmt.GT(matchResult.OfferCoinAmt) && matchResult.OfferCoinFeeAmt.GT(sdk.OneDec()) {
   530  						panic("bad OfferCoinFeeAmt")
   531  					}
   532  					matchResults = append(matchResults, matchResult)
   533  					if direction == DirectionXtoY {
   534  						poolXDelta = poolXDelta.Add(matchResult.TransactedCoinAmt)
   535  						poolYDelta = poolYDelta.Sub(matchResult.ExchangedDemandCoinAmt)
   536  					} else if direction == DirectionYtoX {
   537  						poolXDelta = poolXDelta.Sub(matchResult.ExchangedDemandCoinAmt)
   538  						poolYDelta = poolYDelta.Add(matchResult.TransactedCoinAmt)
   539  					}
   540  				}
   541  				accumMatchAmt = accumMatchAmt.Add(matchAmt)
   542  			}
   543  
   544  			matchAmt = sdk.ZeroInt()
   545  			matchedSwapMsgStates = matchedSwapMsgStates[:0]
   546  		}
   547  	}
   548  	return matchResults, poolXDelta, poolYDelta
   549  }
   550  
   551  // UpdateSwapMsgStates updates SwapMsgStates using the MatchResults.
   552  func UpdateSwapMsgStates(x, y sdk.Dec, xToY, yToX []*SwapMsgState, matchResultXtoY, matchResultYtoX []MatchResult) (
   553  	[]*SwapMsgState, []*SwapMsgState, sdk.Dec, sdk.Dec, sdk.Dec, sdk.Dec) {
   554  	sort.SliceStable(xToY, func(i, j int) bool {
   555  		return xToY[i].Msg.OrderPrice.GT(xToY[j].Msg.OrderPrice)
   556  	})
   557  	sort.SliceStable(yToX, func(i, j int) bool {
   558  		return yToX[i].Msg.OrderPrice.LT(yToX[j].Msg.OrderPrice)
   559  	})
   560  
   561  	poolXDelta := sdk.ZeroDec()
   562  	poolYDelta := sdk.ZeroDec()
   563  
   564  	// Variables to accumulate and offset the values of int 1 caused by decimal error
   565  	decimalErrorX := sdk.ZeroDec()
   566  	decimalErrorY := sdk.ZeroDec()
   567  
   568  	for _, match := range append(matchResultXtoY, matchResultYtoX...) {
   569  		sms := match.SwapMsgState
   570  		if match.OrderDirection == DirectionXtoY {
   571  			poolXDelta = poolXDelta.Add(match.TransactedCoinAmt)
   572  			poolYDelta = poolYDelta.Sub(match.ExchangedDemandCoinAmt)
   573  		} else {
   574  			poolXDelta = poolXDelta.Sub(match.ExchangedDemandCoinAmt)
   575  			poolYDelta = poolYDelta.Add(match.TransactedCoinAmt)
   576  		}
   577  		if sms.RemainingOfferCoin.Amount.ToDec().Sub(match.TransactedCoinAmt).LTE(sdk.OneDec()) {
   578  			// when RemainingOfferCoin and TransactedCoinAmt are almost equal in value, corrects the decimal error and processes as a exact match.
   579  			sms.ExchangedOfferCoin.Amount = sms.ExchangedOfferCoin.Amount.Add(match.TransactedCoinAmt.TruncateInt())
   580  			sms.RemainingOfferCoin.Amount = sms.RemainingOfferCoin.Amount.Sub(match.TransactedCoinAmt.TruncateInt())
   581  			sms.ReservedOfferCoinFee.Amount = sms.ReservedOfferCoinFee.Amount.Sub(match.OfferCoinFeeAmt.TruncateInt())
   582  			if sms.ExchangedOfferCoin.IsNegative() || sms.RemainingOfferCoin.IsNegative() || sms.ReservedOfferCoinFee.IsNegative() {
   583  				panic("negative coin amount after update")
   584  			}
   585  			if sms.RemainingOfferCoin.Amount.Equal(sdk.OneInt()) {
   586  				decimalErrorY = decimalErrorY.Add(sdk.OneDec())
   587  				sms.RemainingOfferCoin.Amount = sdk.ZeroInt()
   588  			}
   589  			if !sms.RemainingOfferCoin.IsZero() || sms.ExchangedOfferCoin.Amount.GT(sms.Msg.OfferCoin.Amount) ||
   590  				sms.ReservedOfferCoinFee.Amount.GT(sdk.OneInt()) {
   591  				panic("invalid state after update")
   592  			} else {
   593  				sms.Succeeded = true
   594  				sms.ToBeDeleted = true
   595  			}
   596  		} else {
   597  			// fractional match
   598  			sms.ExchangedOfferCoin.Amount = sms.ExchangedOfferCoin.Amount.Add(match.TransactedCoinAmt.TruncateInt())
   599  			sms.RemainingOfferCoin.Amount = sms.RemainingOfferCoin.Amount.Sub(match.TransactedCoinAmt.TruncateInt())
   600  			sms.ReservedOfferCoinFee.Amount = sms.ReservedOfferCoinFee.Amount.Sub(match.OfferCoinFeeAmt.TruncateInt())
   601  			if sms.ExchangedOfferCoin.IsNegative() || sms.RemainingOfferCoin.IsNegative() || sms.ReservedOfferCoinFee.IsNegative() {
   602  				panic("negative coin amount after update")
   603  			}
   604  			sms.Succeeded = true
   605  			sms.ToBeDeleted = false
   606  		}
   607  	}
   608  
   609  	// Offset accumulated decimal error values
   610  	poolXDelta = poolXDelta.Add(decimalErrorX)
   611  	poolYDelta = poolYDelta.Add(decimalErrorY)
   612  
   613  	x = x.Add(poolXDelta)
   614  	y = y.Add(poolYDelta)
   615  
   616  	return xToY, yToX, x, y, poolXDelta, poolYDelta
   617  }