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  }