diff --git a/x/liquidity/keeper/invariants.go b/x/liquidity/keeper/invariants.go new file mode 100644 index 00000000..3b1dd053 --- /dev/null +++ b/x/liquidity/keeper/invariants.go @@ -0,0 +1,135 @@ +package keeper + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmosquad-labs/squad/x/liquidity/types" +) + +// RegisterInvariants registers all liquidity module invariants. +func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) { + ir.RegisterRoute(types.ModuleName, "deposit-coins-escrow", DepositCoinsEscrowInvariant(k)) + ir.RegisterRoute(types.ModuleName, "pool-coin-escrow", PoolCoinEscrowInvariant(k)) + ir.RegisterRoute(types.ModuleName, "remaining-offer-coin-escrow", RemainingOfferCoinEscrowInvariant(k)) + ir.RegisterRoute(types.ModuleName, "pool-status", PoolStatusInvariant(k)) +} + +// AllInvariants returns a combined invariant of the liquidity module. +func AllInvariants(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + for _, inv := range []func(Keeper) sdk.Invariant{ + DepositCoinsEscrowInvariant, + PoolCoinEscrowInvariant, + RemainingOfferCoinEscrowInvariant, + PoolStatusInvariant, + } { + res, stop := inv(k)(ctx) + if stop { + return res, stop + } + } + return "", false + } +} + +// DepositCoinsEscrowInvariant checks that the amount of coins in the global +// escrow address is greater or equal than remaining deposit coins in all +// deposit requests. +func DepositCoinsEscrowInvariant(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + escrowDepositCoins := sdk.Coins{} + _ = k.IterateAllDepositRequests(ctx, func(req types.DepositRequest) (stop bool, err error) { + if req.Status == types.RequestStatusNotExecuted { + escrowDepositCoins = escrowDepositCoins.Add(req.DepositCoins...) + } + return false, nil + }) + balances := k.bankKeeper.GetAllBalances(ctx, types.GlobalEscrowAddress) + broken := !balances.IsAllGTE(escrowDepositCoins) + return sdk.FormatInvariant( + types.ModuleName, "deposit-coins-escrow", + fmt.Sprintf("escrow amount %s is smaller than expected %s", balances, escrowDepositCoins), + ), broken + } +} + +// PoolCoinEscrowInvariant checks that the amount of coins in the global +// escrow address is greater or equal than remaining withdrawing pool +// coins in all withdrawal requests. +func PoolCoinEscrowInvariant(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + escrowPoolCoins := sdk.Coins{} + _ = k.IterateAllWithdrawRequests(ctx, func(req types.WithdrawRequest) (stop bool, err error) { + if req.Status == types.RequestStatusNotExecuted { + escrowPoolCoins = escrowPoolCoins.Add(req.PoolCoin) + } + return false, nil + }) + balances := k.bankKeeper.GetAllBalances(ctx, types.GlobalEscrowAddress) + broken := !balances.IsAllGTE(escrowPoolCoins) + return sdk.FormatInvariant( + types.ModuleName, "pool-coin-escrow", + fmt.Sprintf("escrow amount %s is smaller than expected %s", balances, escrowPoolCoins), + ), broken + } +} + +// RemainingOfferCoinEscrowInvariant checks that the amount of coins in each pair's +// escrow address is greater or equal than remaining offer coins in the pair's +// swap requests. +func RemainingOfferCoinEscrowInvariant(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + var ( + count int + msg string + ) + _ = k.IterateAllPairs(ctx, func(pair types.Pair) (stop bool, err error) { + remainingOfferCoins := sdk.Coins{} + _ = k.IterateSwapRequestsByPair(ctx, pair.Id, func(req types.SwapRequest) (stop bool, err error) { + if !req.Status.ShouldBeDeleted() { + remainingOfferCoins = remainingOfferCoins.Add(req.RemainingOfferCoin) + } + return false, nil + }) + balances := k.bankKeeper.GetAllBalances(ctx, pair.GetEscrowAddress()) + if !balances.IsAllGTE(remainingOfferCoins) { + count++ + msg += fmt.Sprintf("\tpair %d has %s, which is smaller than %s\n", pair.Id, balances, remainingOfferCoins) + } + return false, nil + }) + broken := count != 0 + return sdk.FormatInvariant( + types.ModuleName, "remaining-offer-coin-escrow", + fmt.Sprintf("%d pair(s) with insufficient escrow amount found\n%s", count, msg), + ), broken + } +} + +// PoolStatusInvariant checks that the pools with zero pool coin supply have +// been marked as disabled. +func PoolStatusInvariant(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + var ( + count int + msg string + ) + _ = k.IterateAllPools(ctx, func(pool types.Pool) (stop bool, err error) { + if !pool.Disabled { + ps := k.GetPoolCoinSupply(ctx, pool) + if ps.IsZero() { + count++ + msg += fmt.Sprintf("\tpool %d should be disabled, but not\n", pool.Id) + } + } + return false, nil + }) + broken := count != 0 + return sdk.FormatInvariant( + types.ModuleName, "pool-status", + fmt.Sprintf("%d pool(s) with wrong status found\n%s", count, msg), + ), broken + } +} diff --git a/x/liquidity/keeper/invariants_test.go b/x/liquidity/keeper/invariants_test.go new file mode 100644 index 00000000..87967ccd --- /dev/null +++ b/x/liquidity/keeper/invariants_test.go @@ -0,0 +1,90 @@ +package keeper_test + +import ( + "github.com/cosmosquad-labs/squad/x/liquidity/keeper" +) + +func (s *KeeperTestSuite) TestDepositCoinsEscrowInvariant() { + pair := s.createPair(s.addr(0), "denom1", "denom2", true) + pool := s.createPool(s.addr(0), pair.Id, parseCoins("1000000denom1,1000000denom2"), true) + + req := s.depositBatch(s.addr(1), pool.Id, parseCoins("1000000denom1,1000000denom2"), true) + _, broken := keeper.DepositCoinsEscrowInvariant(s.keeper)(s.ctx) + s.Require().False(broken) + + oldReq := req + req.DepositCoins = parseCoins("2000000denom1,2000000denom2") + s.keeper.SetDepositRequest(s.ctx, req) + _, broken = keeper.DepositCoinsEscrowInvariant(s.keeper)(s.ctx) + s.Require().True(broken) + + req = oldReq + s.keeper.SetDepositRequest(s.ctx, req) + s.nextBlock() + _, broken = keeper.DepositCoinsEscrowInvariant(s.keeper)(s.ctx) + s.Require().False(broken) +} + +func (s *KeeperTestSuite) TestPoolCoinEscrowInvariant() { + pair := s.createPair(s.addr(0), "denom1", "denom2", true) + pool := s.createPool(s.addr(0), pair.Id, parseCoins("1000000denom1,1000000denom2"), true) + + s.depositBatch(s.addr(1), pool.Id, parseCoins("1000000denom1,1000000denom2"), true) + s.nextBlock() + + req := s.withdrawBatch(s.addr(1), pool.Id, parseCoin("1000000pool1")) + _, broken := keeper.PoolCoinEscrowInvariant(s.keeper)(s.ctx) + s.Require().False(broken) + + oldReq := req + req.PoolCoin = parseCoin("2000000pool1") + s.keeper.SetWithdrawRequest(s.ctx, req) + _, broken = keeper.PoolCoinEscrowInvariant(s.keeper)(s.ctx) + s.Require().True(broken) + + req = oldReq + s.keeper.SetWithdrawRequest(s.ctx, req) + s.nextBlock() + _, broken = keeper.PoolCoinEscrowInvariant(s.keeper)(s.ctx) + s.Require().False(broken) +} + +func (s *KeeperTestSuite) TestRemainingOfferCoinEscrowInvariant() { + pair := s.createPair(s.addr(0), "denom1", "denom2", true) + + req := s.buyLimitOrderBatch(s.addr(1), pair.Id, parseDec("1.0"), newInt(1000000), 0, true) + _, broken := keeper.RemainingOfferCoinEscrowInvariant(s.keeper)(s.ctx) + s.Require().False(broken) + + oldReq := req + req.RemainingOfferCoin = parseCoin("2000000denom1") + s.keeper.SetSwapRequest(s.ctx, req) + _, broken = keeper.RemainingOfferCoinEscrowInvariant(s.keeper)(s.ctx) + s.Require().True(broken) + + req = oldReq + s.keeper.SetSwapRequest(s.ctx, req) + s.nextBlock() + _, broken = keeper.RemainingOfferCoinEscrowInvariant(s.keeper)(s.ctx) + s.Require().False(broken) +} + +func (s *KeeperTestSuite) TestPoolStatusInvariant() { + pair := s.createPair(s.addr(0), "denom1", "denom2", true) + pool := s.createPool(s.addr(0), pair.Id, parseCoins("1000000denom1,1000000denom2"), true) + + _, broken := keeper.PoolStatusInvariant(s.keeper)(s.ctx) + s.Require().False(broken) + + s.withdrawBatch(s.addr(0), pool.Id, s.getBalance(s.addr(0), pool.PoolCoinDenom)) + s.nextBlock() + + _, broken = keeper.PoolStatusInvariant(s.keeper)(s.ctx) + s.Require().False(broken) + + pool, _ = s.keeper.GetPool(s.ctx, pool.Id) + pool.Disabled = false + s.keeper.SetPool(s.ctx, pool) + _, broken = keeper.PoolStatusInvariant(s.keeper)(s.ctx) + s.Require().True(broken) +} diff --git a/x/liquidity/types/expected_keepers.go b/x/liquidity/types/expected_keepers.go index b7798ac8..074e8a1f 100644 --- a/x/liquidity/types/expected_keepers.go +++ b/x/liquidity/types/expected_keepers.go @@ -11,12 +11,11 @@ type AccountKeeper interface { type BankKeeper interface { GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin + GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins GetSupply(ctx sdk.Context, denom string) sdk.Coin - SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error SendCoins(ctx sdk.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) error SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error - SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error InputOutputCoins(ctx sdk.Context, inputs []banktypes.Input, outputs []banktypes.Output) error }