github.com/gravity-devs/liquidity@v1.5.3/x/liquidity/keeper/invariants.go (about) 1 package keeper 2 3 import ( 4 "fmt" 5 6 sdk "github.com/cosmos/cosmos-sdk/types" 7 8 "github.com/gravity-devs/liquidity/x/liquidity/types" 9 ) 10 11 // RegisterInvariants registers all liquidity invariants. 12 func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) { 13 ir.RegisterRoute(types.ModuleName, "escrow-amount", 14 LiquidityPoolsEscrowAmountInvariant(k)) 15 } 16 17 // AllInvariants runs all invariants of the liquidity module. 18 func AllInvariants(k Keeper) sdk.Invariant { 19 return func(ctx sdk.Context) (string, bool) { 20 res, stop := LiquidityPoolsEscrowAmountInvariant(k)(ctx) 21 return res, stop 22 } 23 } 24 25 // LiquidityPoolsEscrowAmountInvariant checks that outstanding unwithdrawn fees are never negative. 26 func LiquidityPoolsEscrowAmountInvariant(k Keeper) sdk.Invariant { 27 return func(ctx sdk.Context) (string, bool) { 28 remainingCoins := sdk.NewCoins() 29 batches := k.GetAllPoolBatches(ctx) 30 for _, batch := range batches { 31 swapMsgs := k.GetAllPoolBatchSwapMsgStatesNotToBeDeleted(ctx, batch) 32 for _, msg := range swapMsgs { 33 remainingCoins = remainingCoins.Add(msg.RemainingOfferCoin) 34 } 35 depositMsgs := k.GetAllPoolBatchDepositMsgStatesNotToBeDeleted(ctx, batch) 36 for _, msg := range depositMsgs { 37 remainingCoins = remainingCoins.Add(msg.Msg.DepositCoins...) 38 } 39 withdrawMsgs := k.GetAllPoolBatchWithdrawMsgStatesNotToBeDeleted(ctx, batch) 40 for _, msg := range withdrawMsgs { 41 remainingCoins = remainingCoins.Add(msg.Msg.PoolCoin) 42 } 43 } 44 45 batchEscrowAcc := k.accountKeeper.GetModuleAddress(types.ModuleName) 46 escrowAmt := k.bankKeeper.GetAllBalances(ctx, batchEscrowAcc) 47 48 broken := !escrowAmt.IsAllGTE(remainingCoins) 49 50 return sdk.FormatInvariant(types.ModuleName, "batch escrow amount invariant broken", 51 "batch escrow amount LT batch remaining amount"), broken 52 } 53 } 54 55 // These invariants cannot be registered via RegisterInvariants since the module uses per-block batch execution. 56 // We should approach adding these invariant checks inside actual logics of deposit / withdraw / swap. 57 58 var ( 59 BatchLogicInvariantCheckFlag = false // It is only used at the development stage, and is disabled at the product level. 60 // For coin amounts less than coinAmountThreshold, a high errorRate does not mean 61 // that the calculation logic has errors. 62 // For example, if there were two X coins and three Y coins in the pool, and someone deposits 63 // one X coin and one Y coin, it's an acceptable input. 64 // But pool price would change from 2/3 to 3/4 so errorRate will report 1/8(=0.125), 65 // meaning that the price has changed by 12.5%. 66 // This happens with small coin amounts, so there should be a threshold for coin amounts 67 // before we calculate the errorRate. 68 errorRateThreshold = sdk.NewDecWithPrec(5, 2) // 5% 69 coinAmountThreshold = sdk.NewInt(20) // If a decimal error occurs at a value less than 20, the error rate is over 5%. 70 ) 71 72 func errorRate(expected, actual sdk.Dec) sdk.Dec { 73 // To prevent divide-by-zero panics, return 1.0(=100%) as the error rate 74 // when the expected value is 0. 75 if expected.IsZero() { 76 return sdk.OneDec() 77 } 78 return actual.Sub(expected).Quo(expected).Abs() 79 } 80 81 // MintingPoolCoinsInvariant checks the correct ratio of minting amount of pool coins. 82 func MintingPoolCoinsInvariant(poolCoinTotalSupply, mintPoolCoin, depositCoinA, depositCoinB, lastReserveCoinA, lastReserveCoinB, refundedCoinA, refundedCoinB sdk.Int) { 83 if !refundedCoinA.IsZero() { 84 depositCoinA = depositCoinA.Sub(refundedCoinA) 85 } 86 87 if !refundedCoinB.IsZero() { 88 depositCoinB = depositCoinB.Sub(refundedCoinB) 89 } 90 91 poolCoinRatio := mintPoolCoin.ToDec().QuoInt(poolCoinTotalSupply) 92 depositCoinARatio := depositCoinA.ToDec().QuoInt(lastReserveCoinA) 93 depositCoinBRatio := depositCoinB.ToDec().QuoInt(lastReserveCoinB) 94 expectedMintPoolCoinAmtBasedA := depositCoinARatio.MulInt(poolCoinTotalSupply).TruncateInt() 95 expectedMintPoolCoinAmtBasedB := depositCoinBRatio.MulInt(poolCoinTotalSupply).TruncateInt() 96 97 // NewPoolCoinAmount / LastPoolCoinSupply == AfterRefundedDepositCoinA / LastReserveCoinA 98 // NewPoolCoinAmount / LastPoolCoinSupply == AfterRefundedDepositCoinA / LastReserveCoinB 99 if depositCoinA.GTE(coinAmountThreshold) && depositCoinB.GTE(coinAmountThreshold) && 100 lastReserveCoinA.GTE(coinAmountThreshold) && lastReserveCoinB.GTE(coinAmountThreshold) && 101 mintPoolCoin.GTE(coinAmountThreshold) && poolCoinTotalSupply.GTE(coinAmountThreshold) { 102 if errorRate(depositCoinARatio, poolCoinRatio).GT(errorRateThreshold) || 103 errorRate(depositCoinBRatio, poolCoinRatio).GT(errorRateThreshold) { 104 panic("invariant check fails due to incorrect ratio of pool coins") 105 } 106 } 107 108 if mintPoolCoin.GTE(coinAmountThreshold) && 109 (sdk.MaxInt(mintPoolCoin, expectedMintPoolCoinAmtBasedA).Sub(sdk.MinInt(mintPoolCoin, expectedMintPoolCoinAmtBasedA)).ToDec().QuoInt(mintPoolCoin).GT(errorRateThreshold) || 110 sdk.MaxInt(mintPoolCoin, expectedMintPoolCoinAmtBasedB).Sub(sdk.MinInt(mintPoolCoin, expectedMintPoolCoinAmtBasedA)).ToDec().QuoInt(mintPoolCoin).GT(errorRateThreshold)) { 111 panic("invariant check fails due to incorrect amount of pool coins") 112 } 113 } 114 115 // DepositInvariant checks after deposit amounts. 116 func DepositInvariant(lastReserveCoinA, lastReserveCoinB, depositCoinA, depositCoinB, afterReserveCoinA, afterReserveCoinB, refundedCoinA, refundedCoinB sdk.Int) { 117 depositCoinA = depositCoinA.Sub(refundedCoinA) 118 depositCoinB = depositCoinB.Sub(refundedCoinB) 119 120 depositCoinRatio := depositCoinA.ToDec().Quo(depositCoinB.ToDec()) 121 lastReserveRatio := lastReserveCoinA.ToDec().Quo(lastReserveCoinB.ToDec()) 122 afterReserveRatio := afterReserveCoinA.ToDec().Quo(afterReserveCoinB.ToDec()) 123 124 // AfterDepositReserveCoinA = LastReserveCoinA + AfterRefundedDepositCoinA 125 // AfterDepositReserveCoinB = LastReserveCoinB + AfterRefundedDepositCoinA 126 if !afterReserveCoinA.Equal(lastReserveCoinA.Add(depositCoinA)) || 127 !afterReserveCoinB.Equal(lastReserveCoinB.Add(depositCoinB)) { 128 panic("invariant check fails due to incorrect deposit amounts") 129 } 130 131 if depositCoinA.GTE(coinAmountThreshold) && depositCoinB.GTE(coinAmountThreshold) && 132 lastReserveCoinA.GTE(coinAmountThreshold) && lastReserveCoinB.GTE(coinAmountThreshold) { 133 // AfterRefundedDepositCoinA / AfterRefundedDepositCoinA = LastReserveCoinA / LastReserveCoinB 134 if errorRate(lastReserveRatio, depositCoinRatio).GT(errorRateThreshold) { 135 panic("invariant check fails due to incorrect deposit ratio") 136 } 137 // LastReserveCoinA / LastReserveCoinB = AfterDepositReserveCoinA / AfterDepositReserveCoinB 138 if errorRate(lastReserveRatio, afterReserveRatio).GT(errorRateThreshold) { 139 panic("invariant check fails due to incorrect pool price ratio") 140 } 141 } 142 } 143 144 // BurningPoolCoinsInvariant checks the correct burning amount of pool coins. 145 func BurningPoolCoinsInvariant(burnedPoolCoin, withdrawCoinA, withdrawCoinB, reserveCoinA, reserveCoinB, lastPoolCoinSupply sdk.Int, withdrawFeeCoins sdk.Coins) { 146 burningPoolCoinRatio := burnedPoolCoin.ToDec().Quo(lastPoolCoinSupply.ToDec()) 147 if burningPoolCoinRatio.Equal(sdk.OneDec()) { 148 return 149 } 150 151 withdrawCoinARatio := withdrawCoinA.Add(withdrawFeeCoins[0].Amount).ToDec().Quo(reserveCoinA.ToDec()) 152 withdrawCoinBRatio := withdrawCoinB.Add(withdrawFeeCoins[1].Amount).ToDec().Quo(reserveCoinB.ToDec()) 153 154 // BurnedPoolCoinAmount / LastPoolCoinSupply >= (WithdrawCoinA+WithdrawFeeCoinA) / LastReserveCoinA 155 // BurnedPoolCoinAmount / LastPoolCoinSupply >= (WithdrawCoinB+WithdrawFeeCoinB) / LastReserveCoinB 156 if withdrawCoinARatio.GT(burningPoolCoinRatio) || withdrawCoinBRatio.GT(burningPoolCoinRatio) { 157 panic("invariant check fails due to incorrect ratio of burning pool coins") 158 } 159 160 expectedBurningPoolCoinBasedA := lastPoolCoinSupply.ToDec().MulTruncate(withdrawCoinARatio).TruncateInt() 161 expectedBurningPoolCoinBasedB := lastPoolCoinSupply.ToDec().MulTruncate(withdrawCoinBRatio).TruncateInt() 162 163 if burnedPoolCoin.GTE(coinAmountThreshold) && 164 (sdk.MaxInt(burnedPoolCoin, expectedBurningPoolCoinBasedA).Sub(sdk.MinInt(burnedPoolCoin, expectedBurningPoolCoinBasedA)).ToDec().QuoInt(burnedPoolCoin).GT(errorRateThreshold) || 165 sdk.MaxInt(burnedPoolCoin, expectedBurningPoolCoinBasedB).Sub(sdk.MinInt(burnedPoolCoin, expectedBurningPoolCoinBasedB)).ToDec().QuoInt(burnedPoolCoin).GT(errorRateThreshold)) { 166 panic("invariant check fails due to incorrect amount of burning pool coins") 167 } 168 } 169 170 // WithdrawReserveCoinsInvariant checks the after withdraw amounts. 171 func WithdrawReserveCoinsInvariant(withdrawCoinA, withdrawCoinB, reserveCoinA, reserveCoinB, 172 afterReserveCoinA, afterReserveCoinB, afterPoolCoinTotalSupply, lastPoolCoinSupply, burnedPoolCoin sdk.Int) { 173 // AfterWithdrawReserveCoinA = LastReserveCoinA - WithdrawCoinA 174 if !afterReserveCoinA.Equal(reserveCoinA.Sub(withdrawCoinA)) { 175 panic("invariant check fails due to incorrect withdraw coin A amount") 176 } 177 178 // AfterWithdrawReserveCoinB = LastReserveCoinB - WithdrawCoinB 179 if !afterReserveCoinB.Equal(reserveCoinB.Sub(withdrawCoinB)) { 180 panic("invariant check fails due to incorrect withdraw coin B amount") 181 } 182 183 // AfterWithdrawPoolCoinSupply = LastPoolCoinSupply - BurnedPoolCoinAmount 184 if !afterPoolCoinTotalSupply.Equal(lastPoolCoinSupply.Sub(burnedPoolCoin)) { 185 panic("invariant check fails due to incorrect total supply") 186 } 187 } 188 189 // WithdrawAmountInvariant checks the correct ratio of withdraw coin amounts. 190 func WithdrawAmountInvariant(withdrawCoinA, withdrawCoinB, reserveCoinA, reserveCoinB, burnedPoolCoin, poolCoinSupply sdk.Int, withdrawFeeRate sdk.Dec) { 191 ratio := burnedPoolCoin.ToDec().Quo(poolCoinSupply.ToDec()).Mul(sdk.OneDec().Sub(withdrawFeeRate)) 192 idealWithdrawCoinA := reserveCoinA.ToDec().Mul(ratio) 193 idealWithdrawCoinB := reserveCoinB.ToDec().Mul(ratio) 194 diffA := idealWithdrawCoinA.Sub(withdrawCoinA.ToDec()).Abs() 195 diffB := idealWithdrawCoinB.Sub(withdrawCoinB.ToDec()).Abs() 196 if !burnedPoolCoin.Equal(poolCoinSupply) { 197 if diffA.GTE(sdk.OneDec()) { 198 panic(fmt.Sprintf("withdraw coin amount %v differs too much from %v", withdrawCoinA, idealWithdrawCoinA)) 199 } 200 if diffB.GTE(sdk.OneDec()) { 201 panic(fmt.Sprintf("withdraw coin amount %v differs too much from %v", withdrawCoinB, idealWithdrawCoinB)) 202 } 203 } 204 } 205 206 // ImmutablePoolPriceAfterWithdrawInvariant checks the immutable pool price after withdrawing coins. 207 func ImmutablePoolPriceAfterWithdrawInvariant(reserveCoinA, reserveCoinB, withdrawCoinA, withdrawCoinB, afterReserveCoinA, afterReserveCoinB sdk.Int) { 208 // TestReinitializePool tests a scenario where after reserve coins are zero 209 if !afterReserveCoinA.IsZero() && !afterReserveCoinB.IsZero() { 210 reserveCoinA = reserveCoinA.Sub(withdrawCoinA) 211 reserveCoinB = reserveCoinB.Sub(withdrawCoinB) 212 213 reserveCoinRatio := reserveCoinA.ToDec().Quo(reserveCoinB.ToDec()) 214 afterReserveCoinRatio := afterReserveCoinA.ToDec().Quo(afterReserveCoinB.ToDec()) 215 216 // LastReserveCoinA / LastReserveCoinB = AfterWithdrawReserveCoinA / AfterWithdrawReserveCoinB 217 if reserveCoinA.GTE(coinAmountThreshold) && reserveCoinB.GTE(coinAmountThreshold) && 218 withdrawCoinA.GTE(coinAmountThreshold) && withdrawCoinB.GTE(coinAmountThreshold) && 219 errorRate(reserveCoinRatio, afterReserveCoinRatio).GT(errorRateThreshold) { 220 panic("invariant check fails due to incorrect pool price ratio") 221 } 222 } 223 } 224 225 // SwapMatchingInvariants checks swap matching results of both X to Y and Y to X cases. 226 func SwapMatchingInvariants(xToY, yToX []*types.SwapMsgState, matchResultXtoY, matchResultYtoX []types.MatchResult) { 227 beforeMatchingXtoYLen := len(xToY) 228 beforeMatchingYtoXLen := len(yToX) 229 afterMatchingXtoYLen := len(matchResultXtoY) 230 afterMatchingYtoXLen := len(matchResultYtoX) 231 232 notMatchedXtoYLen := beforeMatchingXtoYLen - afterMatchingXtoYLen 233 notMatchedYtoXLen := beforeMatchingYtoXLen - afterMatchingYtoXLen 234 235 if notMatchedXtoYLen != types.CountNotMatchedMsgs(xToY) { 236 panic("invariant check fails due to invalid xToY match length") 237 } 238 239 if notMatchedYtoXLen != types.CountNotMatchedMsgs(yToX) { 240 panic("invariant check fails due to invalid yToX match length") 241 } 242 } 243 244 // SwapPriceInvariants checks swap price invariants. 245 func SwapPriceInvariants(matchResultXtoY, matchResultYtoX []types.MatchResult, poolXDelta, poolYDelta, poolXDelta2, poolYDelta2 sdk.Dec, result types.BatchResult) { 246 invariantCheckX := sdk.ZeroDec() 247 invariantCheckY := sdk.ZeroDec() 248 249 for _, m := range matchResultXtoY { 250 invariantCheckX = invariantCheckX.Sub(m.TransactedCoinAmt) 251 invariantCheckY = invariantCheckY.Add(m.ExchangedDemandCoinAmt) 252 } 253 254 for _, m := range matchResultYtoX { 255 invariantCheckY = invariantCheckY.Sub(m.TransactedCoinAmt) 256 invariantCheckX = invariantCheckX.Add(m.ExchangedDemandCoinAmt) 257 } 258 259 invariantCheckX = invariantCheckX.Add(poolXDelta2) 260 invariantCheckY = invariantCheckY.Add(poolYDelta2) 261 262 if !invariantCheckX.IsZero() && !invariantCheckY.IsZero() { 263 panic(fmt.Errorf("invariant check fails due to invalid swap price: %s", invariantCheckX.String())) 264 } 265 266 validitySwapPrice := types.CheckSwapPrice(matchResultXtoY, matchResultYtoX, result.SwapPrice) 267 if !validitySwapPrice { 268 panic("invariant check fails due to invalid swap price") 269 } 270 } 271 272 // SwapPriceDirectionInvariants checks whether the calculated swap price is increased, decreased, or stayed from the last pool price. 273 func SwapPriceDirectionInvariants(currentPoolPrice sdk.Dec, batchResult types.BatchResult) { 274 switch batchResult.PriceDirection { 275 case types.Increasing: 276 if !batchResult.SwapPrice.GT(currentPoolPrice) { 277 panic("invariant check fails due to incorrect price direction") 278 } 279 case types.Decreasing: 280 if !batchResult.SwapPrice.LT(currentPoolPrice) { 281 panic("invariant check fails due to incorrect price direction") 282 } 283 case types.Staying: 284 if !batchResult.SwapPrice.Equal(currentPoolPrice) { 285 panic("invariant check fails due to incorrect price direction") 286 } 287 } 288 } 289 290 // SwapMsgStatesInvariants checks swap match result states invariants. 291 func SwapMsgStatesInvariants(matchResultXtoY, matchResultYtoX []types.MatchResult, matchResultMap map[uint64]types.MatchResult, 292 swapMsgStates []*types.SwapMsgState, xToY, yToX []*types.SwapMsgState) { 293 if len(matchResultXtoY)+len(matchResultYtoX) != len(matchResultMap) { 294 panic("invalid length of match result") 295 } 296 297 for k, v := range matchResultMap { 298 if k != v.SwapMsgState.MsgIndex { 299 panic("broken map consistency") 300 } 301 } 302 303 for _, sms := range swapMsgStates { 304 for _, smsXtoY := range xToY { 305 if sms.MsgIndex == smsXtoY.MsgIndex { 306 if *(sms) != *(smsXtoY) || sms != smsXtoY { 307 panic("swap message state not matched") 308 } else { 309 break 310 } 311 } 312 } 313 314 for _, smsYtoX := range yToX { 315 if sms.MsgIndex == smsYtoX.MsgIndex { 316 if *(sms) != *(smsYtoX) || sms != smsYtoX { 317 panic("swap message state not matched") 318 } else { 319 break 320 } 321 } 322 } 323 324 if msgAfter, ok := matchResultMap[sms.MsgIndex]; ok { 325 if sms.MsgIndex == msgAfter.SwapMsgState.MsgIndex { 326 if *(sms) != *(msgAfter.SwapMsgState) || sms != msgAfter.SwapMsgState { 327 panic("batch message not matched") 328 } 329 } else { 330 panic("fail msg pointer consistency") 331 } 332 } 333 } 334 } 335 336 // SwapOrdersExecutionStateInvariants checks all executed orders have order price which is not "executable" or not "unexecutable". 337 func SwapOrdersExecutionStateInvariants(matchResultMap map[uint64]types.MatchResult, swapMsgStates []*types.SwapMsgState, 338 batchResult types.BatchResult, denomX string) { 339 for _, sms := range swapMsgStates { 340 if _, ok := matchResultMap[sms.MsgIndex]; ok { 341 if !sms.Executed || !sms.Succeeded { 342 panic("swap msg state consistency error, matched but not succeeded") 343 } 344 345 if sms.Msg.OfferCoin.Denom == denomX { 346 // buy orders having equal or higher order price than found swapPrice 347 if !sms.Msg.OrderPrice.GTE(batchResult.SwapPrice) { 348 panic("execution validity failed, executed but unexecutable") 349 } 350 } else { 351 // sell orders having equal or lower order price than found swapPrice 352 if !sms.Msg.OrderPrice.LTE(batchResult.SwapPrice) { 353 panic("execution validity failed, executed but unexecutable") 354 } 355 } 356 } else { 357 // check whether every unexecuted orders have order price which is not "executable" 358 if sms.Executed && sms.Succeeded { 359 panic("sms consistency error, not matched but succeeded") 360 } 361 362 if sms.Msg.OfferCoin.Denom == denomX { 363 // buy orders having equal or lower order price than found swapPrice 364 if !sms.Msg.OrderPrice.LTE(batchResult.SwapPrice) { 365 panic("execution validity failed, unexecuted but executable") 366 } 367 } else { 368 // sell orders having equal or higher order price than found swapPrice 369 if !sms.Msg.OrderPrice.GTE(batchResult.SwapPrice) { 370 panic("execution validity failed, unexecuted but executable") 371 } 372 } 373 } 374 } 375 }