package keeper
import (
"fmt"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cybercongress/go-cyber/v7/x/liquidity/types"
)
func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) {
ir.RegisterRoute(types.ModuleName, "escrow-amount",
LiquidityPoolsEscrowAmountInvariant(k))
}
func AllInvariants(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
res, stop := LiquidityPoolsEscrowAmountInvariant(k)(ctx)
return res, stop
}
}
func LiquidityPoolsEscrowAmountInvariant(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
remainingCoins := sdk.NewCoins()
batches := k.GetAllPoolBatches(ctx)
for _, batch := range batches {
swapMsgs := k.GetAllPoolBatchSwapMsgStatesNotToBeDeleted(ctx, batch)
for _, msg := range swapMsgs {
remainingCoins = remainingCoins.Add(msg.RemainingOfferCoin)
}
depositMsgs := k.GetAllPoolBatchDepositMsgStatesNotToBeDeleted(ctx, batch)
for _, msg := range depositMsgs {
remainingCoins = remainingCoins.Add(msg.Msg.DepositCoins...)
}
withdrawMsgs := k.GetAllPoolBatchWithdrawMsgStatesNotToBeDeleted(ctx, batch)
for _, msg := range withdrawMsgs {
remainingCoins = remainingCoins.Add(msg.Msg.PoolCoin)
}
}
batchEscrowAcc := k.accountKeeper.GetModuleAddress(types.ModuleName)
escrowAmt := k.bankKeeper.GetAllBalances(ctx, batchEscrowAcc)
broken := !escrowAmt.IsAllGTE(remainingCoins)
return sdk.FormatInvariant(types.ModuleName, "batch escrow amount invariant broken",
"batch escrow amount LT batch remaining amount"), broken
}
}
var (
BatchLogicInvariantCheckFlag = false errorRateThreshold = sdk.NewDecWithPrec(5, 2) coinAmountThreshold = sdk.NewInt(20) )
func errorRate(expected, actual sdk.Dec) sdk.Dec {
if expected.IsZero() {
return sdk.OneDec()
}
return actual.Sub(expected).Quo(expected).Abs()
}
func MintingPoolCoinsInvariant(poolCoinTotalSupply, mintPoolCoin, depositCoinA, depositCoinB, lastReserveCoinA, lastReserveCoinB, refundedCoinA, refundedCoinB math.Int) {
if !refundedCoinA.IsZero() {
depositCoinA = depositCoinA.Sub(refundedCoinA)
}
if !refundedCoinB.IsZero() {
depositCoinB = depositCoinB.Sub(refundedCoinB)
}
poolCoinRatio := sdk.NewDecFromInt(mintPoolCoin).QuoInt(poolCoinTotalSupply)
depositCoinARatio := sdk.NewDecFromInt(depositCoinA).QuoInt(lastReserveCoinA)
depositCoinBRatio := sdk.NewDecFromInt(depositCoinB).QuoInt(lastReserveCoinB)
expectedMintPoolCoinAmtBasedA := depositCoinARatio.MulInt(poolCoinTotalSupply).TruncateInt()
expectedMintPoolCoinAmtBasedB := depositCoinBRatio.MulInt(poolCoinTotalSupply).TruncateInt()
if depositCoinA.GTE(coinAmountThreshold) && depositCoinB.GTE(coinAmountThreshold) &&
lastReserveCoinA.GTE(coinAmountThreshold) && lastReserveCoinB.GTE(coinAmountThreshold) &&
mintPoolCoin.GTE(coinAmountThreshold) && poolCoinTotalSupply.GTE(coinAmountThreshold) {
if errorRate(depositCoinARatio, poolCoinRatio).GT(errorRateThreshold) ||
errorRate(depositCoinBRatio, poolCoinRatio).GT(errorRateThreshold) {
panic("invariant check fails due to incorrect ratio of pool coins")
}
}
if mintPoolCoin.GTE(coinAmountThreshold) &&
(sdk.NewDecFromInt(sdk.MaxInt(mintPoolCoin, expectedMintPoolCoinAmtBasedA).Sub(sdk.MinInt(mintPoolCoin, expectedMintPoolCoinAmtBasedA))).QuoInt(mintPoolCoin).GT(errorRateThreshold) ||
sdk.NewDecFromInt(sdk.MaxInt(mintPoolCoin, expectedMintPoolCoinAmtBasedB).Sub(sdk.MinInt(mintPoolCoin, expectedMintPoolCoinAmtBasedA))).QuoInt(mintPoolCoin).GT(errorRateThreshold)) {
panic("invariant check fails due to incorrect amount of pool coins")
}
}
func DepositInvariant(lastReserveCoinA, lastReserveCoinB, depositCoinA, depositCoinB, afterReserveCoinA, afterReserveCoinB, refundedCoinA, refundedCoinB math.Int) {
depositCoinA = depositCoinA.Sub(refundedCoinA)
depositCoinB = depositCoinB.Sub(refundedCoinB)
depositCoinRatio := sdk.NewDecFromInt(depositCoinA).Quo(sdk.NewDecFromInt(depositCoinB))
lastReserveRatio := sdk.NewDecFromInt(lastReserveCoinA).Quo(sdk.NewDecFromInt(lastReserveCoinB))
afterReserveRatio := sdk.NewDecFromInt(afterReserveCoinA).Quo(sdk.NewDecFromInt(afterReserveCoinB))
if !afterReserveCoinA.Equal(lastReserveCoinA.Add(depositCoinA)) ||
!afterReserveCoinB.Equal(lastReserveCoinB.Add(depositCoinB)) {
panic("invariant check fails due to incorrect deposit amounts")
}
if depositCoinA.GTE(coinAmountThreshold) && depositCoinB.GTE(coinAmountThreshold) &&
lastReserveCoinA.GTE(coinAmountThreshold) && lastReserveCoinB.GTE(coinAmountThreshold) {
if errorRate(lastReserveRatio, depositCoinRatio).GT(errorRateThreshold) {
panic("invariant check fails due to incorrect deposit ratio")
}
if errorRate(lastReserveRatio, afterReserveRatio).GT(errorRateThreshold) {
panic("invariant check fails due to incorrect pool price ratio")
}
}
}
func BurningPoolCoinsInvariant(burnedPoolCoin, withdrawCoinA, withdrawCoinB, reserveCoinA, reserveCoinB, lastPoolCoinSupply math.Int, withdrawFeeCoins sdk.Coins) {
burningPoolCoinRatio := sdk.NewDecFromInt(burnedPoolCoin).Quo(sdk.NewDecFromInt(lastPoolCoinSupply))
if burningPoolCoinRatio.Equal(sdk.OneDec()) {
return
}
withdrawCoinARatio := sdk.NewDecFromInt(withdrawCoinA.Add(withdrawFeeCoins[0].Amount)).Quo(sdk.NewDecFromInt(reserveCoinA))
withdrawCoinBRatio := sdk.NewDecFromInt(withdrawCoinB.Add(withdrawFeeCoins[1].Amount)).Quo(sdk.NewDecFromInt(reserveCoinB))
if withdrawCoinARatio.GT(burningPoolCoinRatio) || withdrawCoinBRatio.GT(burningPoolCoinRatio) {
panic("invariant check fails due to incorrect ratio of burning pool coins")
}
expectedBurningPoolCoinBasedA := sdk.NewDecFromInt(lastPoolCoinSupply).MulTruncate(withdrawCoinARatio).TruncateInt()
expectedBurningPoolCoinBasedB := sdk.NewDecFromInt(lastPoolCoinSupply).MulTruncate(withdrawCoinBRatio).TruncateInt()
if burnedPoolCoin.GTE(coinAmountThreshold) &&
(sdk.NewDecFromInt(sdk.MaxInt(burnedPoolCoin, expectedBurningPoolCoinBasedA).Sub(sdk.MinInt(burnedPoolCoin, expectedBurningPoolCoinBasedA))).QuoInt(burnedPoolCoin).GT(errorRateThreshold) ||
sdk.NewDecFromInt(sdk.MaxInt(burnedPoolCoin, expectedBurningPoolCoinBasedB).Sub(sdk.MinInt(burnedPoolCoin, expectedBurningPoolCoinBasedB))).QuoInt(burnedPoolCoin).GT(errorRateThreshold)) {
panic("invariant check fails due to incorrect amount of burning pool coins")
}
}
func WithdrawReserveCoinsInvariant(withdrawCoinA, withdrawCoinB, reserveCoinA, reserveCoinB,
afterReserveCoinA, afterReserveCoinB, afterPoolCoinTotalSupply, lastPoolCoinSupply, burnedPoolCoin math.Int,
) {
if !afterReserveCoinA.Equal(reserveCoinA.Sub(withdrawCoinA)) {
panic("invariant check fails due to incorrect withdraw coin A amount")
}
if !afterReserveCoinB.Equal(reserveCoinB.Sub(withdrawCoinB)) {
panic("invariant check fails due to incorrect withdraw coin B amount")
}
if !afterPoolCoinTotalSupply.Equal(lastPoolCoinSupply.Sub(burnedPoolCoin)) {
panic("invariant check fails due to incorrect total supply")
}
}
func WithdrawAmountInvariant(withdrawCoinA, withdrawCoinB, reserveCoinA, reserveCoinB, burnedPoolCoin, poolCoinSupply math.Int, withdrawFeeRate sdk.Dec) {
ratio := sdk.NewDecFromInt(burnedPoolCoin).Quo(sdk.NewDecFromInt(poolCoinSupply)).Mul(sdk.OneDec().Sub(withdrawFeeRate))
idealWithdrawCoinA := sdk.NewDecFromInt(reserveCoinA).Mul(ratio)
idealWithdrawCoinB := sdk.NewDecFromInt(reserveCoinB).Mul(ratio)
diffA := idealWithdrawCoinA.Sub(sdk.NewDecFromInt(withdrawCoinA)).Abs()
diffB := idealWithdrawCoinB.Sub(sdk.NewDecFromInt(withdrawCoinB)).Abs()
if !burnedPoolCoin.Equal(poolCoinSupply) {
if diffA.GTE(sdk.OneDec()) {
panic(fmt.Sprintf("withdraw coin amount %v differs too much from %v", withdrawCoinA, idealWithdrawCoinA))
}
if diffB.GTE(sdk.OneDec()) {
panic(fmt.Sprintf("withdraw coin amount %v differs too much from %v", withdrawCoinB, idealWithdrawCoinB))
}
}
}
func ImmutablePoolPriceAfterWithdrawInvariant(reserveCoinA, reserveCoinB, withdrawCoinA, withdrawCoinB, afterReserveCoinA, afterReserveCoinB math.Int) {
if !afterReserveCoinA.IsZero() && !afterReserveCoinB.IsZero() {
reserveCoinA = reserveCoinA.Sub(withdrawCoinA)
reserveCoinB = reserveCoinB.Sub(withdrawCoinB)
reserveCoinRatio := sdk.NewDecFromInt(reserveCoinA).Quo(sdk.NewDecFromInt(reserveCoinB))
afterReserveCoinRatio := sdk.NewDecFromInt(afterReserveCoinA).Quo(sdk.NewDecFromInt(afterReserveCoinB))
if reserveCoinA.GTE(coinAmountThreshold) && reserveCoinB.GTE(coinAmountThreshold) &&
withdrawCoinA.GTE(coinAmountThreshold) && withdrawCoinB.GTE(coinAmountThreshold) &&
errorRate(reserveCoinRatio, afterReserveCoinRatio).GT(errorRateThreshold) {
panic("invariant check fails due to incorrect pool price ratio")
}
}
}
func SwapMatchingInvariants(xToY, yToX []*types.SwapMsgState, matchResultXtoY, matchResultYtoX []types.MatchResult) {
beforeMatchingXtoYLen := len(xToY)
beforeMatchingYtoXLen := len(yToX)
afterMatchingXtoYLen := len(matchResultXtoY)
afterMatchingYtoXLen := len(matchResultYtoX)
notMatchedXtoYLen := beforeMatchingXtoYLen - afterMatchingXtoYLen
notMatchedYtoXLen := beforeMatchingYtoXLen - afterMatchingYtoXLen
if notMatchedXtoYLen != types.CountNotMatchedMsgs(xToY) {
panic("invariant check fails due to invalid xToY match length")
}
if notMatchedYtoXLen != types.CountNotMatchedMsgs(yToX) {
panic("invariant check fails due to invalid yToX match length")
}
}
func SwapPriceInvariants(matchResultXtoY, matchResultYtoX []types.MatchResult, poolXDelta, poolYDelta, poolXDelta2, poolYDelta2 sdk.Dec, result types.BatchResult) {
invariantCheckX := sdk.ZeroDec()
invariantCheckY := sdk.ZeroDec()
for _, m := range matchResultXtoY {
invariantCheckX = invariantCheckX.Sub(m.TransactedCoinAmt)
invariantCheckY = invariantCheckY.Add(m.ExchangedDemandCoinAmt)
}
for _, m := range matchResultYtoX {
invariantCheckY = invariantCheckY.Sub(m.TransactedCoinAmt)
invariantCheckX = invariantCheckX.Add(m.ExchangedDemandCoinAmt)
}
invariantCheckX = invariantCheckX.Add(poolXDelta2)
invariantCheckY = invariantCheckY.Add(poolYDelta2)
if !invariantCheckX.IsZero() && !invariantCheckY.IsZero() {
panic(fmt.Errorf("invariant check fails due to invalid swap price: %s", invariantCheckX.String()))
}
validitySwapPrice := types.CheckSwapPrice(matchResultXtoY, matchResultYtoX, result.SwapPrice)
if !validitySwapPrice {
panic("invariant check fails due to invalid swap price")
}
}
func SwapPriceDirectionInvariants(currentPoolPrice sdk.Dec, batchResult types.BatchResult) {
switch batchResult.PriceDirection {
case types.Increasing:
if !batchResult.SwapPrice.GT(currentPoolPrice) {
panic("invariant check fails due to incorrect price direction")
}
case types.Decreasing:
if !batchResult.SwapPrice.LT(currentPoolPrice) {
panic("invariant check fails due to incorrect price direction")
}
case types.Staying:
if !batchResult.SwapPrice.Equal(currentPoolPrice) {
panic("invariant check fails due to incorrect price direction")
}
}
}
func SwapMsgStatesInvariants(matchResultXtoY, matchResultYtoX []types.MatchResult, matchResultMap map[uint64]types.MatchResult,
swapMsgStates []*types.SwapMsgState, xToY, yToX []*types.SwapMsgState,
) {
if len(matchResultXtoY)+len(matchResultYtoX) != len(matchResultMap) {
panic("invalid length of match result")
}
for k, v := range matchResultMap {
if k != v.SwapMsgState.MsgIndex {
panic("broken map consistency")
}
}
for _, sms := range swapMsgStates {
for _, smsXtoY := range xToY {
if sms.MsgIndex == smsXtoY.MsgIndex {
if *(sms) != *(smsXtoY) || sms != smsXtoY {
panic("swap message state not matched")
} else {
break
}
}
}
for _, smsYtoX := range yToX {
if sms.MsgIndex == smsYtoX.MsgIndex {
if *(sms) != *(smsYtoX) || sms != smsYtoX {
panic("swap message state not matched")
} else {
break
}
}
}
if msgAfter, ok := matchResultMap[sms.MsgIndex]; ok {
if sms.MsgIndex == msgAfter.SwapMsgState.MsgIndex {
if *(sms) != *(msgAfter.SwapMsgState) || sms != msgAfter.SwapMsgState {
panic("batch message not matched")
}
} else {
panic("fail msg pointer consistency")
}
}
}
}
func SwapOrdersExecutionStateInvariants(matchResultMap map[uint64]types.MatchResult, swapMsgStates []*types.SwapMsgState,
batchResult types.BatchResult, denomX string,
) {
for _, sms := range swapMsgStates {
if _, ok := matchResultMap[sms.MsgIndex]; ok {
if !sms.Executed || !sms.Succeeded {
panic("swap msg state consistency error, matched but not succeeded")
}
if sms.Msg.OfferCoin.Denom == denomX {
if !sms.Msg.OrderPrice.GTE(batchResult.SwapPrice) {
panic("execution validity failed, executed but unexecutable")
}
} else {
if !sms.Msg.OrderPrice.LTE(batchResult.SwapPrice) {
panic("execution validity failed, executed but unexecutable")
}
}
} else {
if sms.Executed && sms.Succeeded {
panic("sms consistency error, not matched but succeeded")
}
if sms.Msg.OfferCoin.Denom == denomX {
if !sms.Msg.OrderPrice.LTE(batchResult.SwapPrice) {
panic("execution validity failed, unexecuted but executable")
}
} else {
if !sms.Msg.OrderPrice.GTE(batchResult.SwapPrice) {
panic("execution validity failed, unexecuted but executable")
}
}
}
}
}