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 }