package keeper_test import ( "testing" "github.com/kava-labs/kava/x/swap/keeper" "github.com/kava-labs/kava/x/swap/testutil" "github.com/kava-labs/kava/x/swap/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/suite" ) type invariantTestSuite struct { testutil.Suite invariants map[string]map[string]sdk.Invariant } func (suite *invariantTestSuite) SetupTest() { suite.Suite.SetupTest() suite.invariants = make(map[string]map[string]sdk.Invariant) keeper.RegisterInvariants(suite, suite.Keeper) } func (suite *invariantTestSuite) SetupValidState() { suite.Keeper.SetPool(suite.Ctx, types.NewPoolRecord( sdk.NewCoins( sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), ), sdk.NewInt(3e6), )) suite.AddCoinsToModule( sdk.NewCoins( sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), ), ) suite.Keeper.SetDepositorShares(suite.Ctx, types.NewShareRecord( sdk.AccAddress("depositor 1---------"), // TODO these addresses are padded to get to the required length of 20 bytes. What is a nicer setup? types.PoolID("ukava", "usdx"), sdk.NewInt(2e6), )) suite.Keeper.SetDepositorShares(suite.Ctx, types.NewShareRecord( sdk.AccAddress("depositor 2---------"), types.PoolID("ukava", "usdx"), sdk.NewInt(1e6), )) suite.Keeper.SetPool(suite.Ctx, types.NewPoolRecord( sdk.NewCoins( sdk.NewCoin("hard", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(2e6)), ), sdk.NewInt(1e6), )) suite.AddCoinsToModule( sdk.NewCoins( sdk.NewCoin("hard", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(2e6)), ), ) suite.Keeper.SetDepositorShares(suite.Ctx, types.NewShareRecord( sdk.AccAddress("depositor 1---------"), types.PoolID("hard", "usdx"), sdk.NewInt(1e6), )) } func (suite *invariantTestSuite) RegisterRoute(moduleName string, route string, invariant sdk.Invariant) { _, exists := suite.invariants[moduleName] if !exists { suite.invariants[moduleName] = make(map[string]sdk.Invariant) } suite.invariants[moduleName][route] = invariant } func (suite *invariantTestSuite) runInvariant(route string, invariant func(k keeper.Keeper) sdk.Invariant) (string, bool) { ctx := suite.Ctx registeredInvariant := suite.invariants[types.ModuleName][route] suite.Require().NotNil(registeredInvariant) // direct call dMessage, dBroken := invariant(suite.Keeper)(ctx) // registered call rMessage, rBroken := registeredInvariant(ctx) // all call aMessage, aBroken := keeper.AllInvariants(suite.Keeper)(ctx) // require matching values for direct call and registered call suite.Require().Equal(dMessage, rMessage, "expected registered invariant message to match") suite.Require().Equal(dBroken, rBroken, "expected registered invariant broken to match") // require matching values for direct call and all invariants call if broken suite.Require().Equal(dBroken, aBroken, "expected all invariant broken to match") if dBroken { suite.Require().Equal(dMessage, aMessage, "expected all invariant message to match") } // return message, broken return dMessage, dBroken } func (suite *invariantTestSuite) TestPoolRecordsInvariant() { // default state is valid message, broken := suite.runInvariant("pool-records", keeper.PoolRecordsInvariant) suite.Equal("swap: validate pool records broken invariant\npool record invalid\n", message) suite.Equal(false, broken) suite.SetupValidState() message, broken = suite.runInvariant("pool-records", keeper.PoolRecordsInvariant) suite.Equal("swap: validate pool records broken invariant\npool record invalid\n", message) suite.Equal(false, broken) // broken with invalid pool record suite.Keeper.SetPool_Raw(suite.Ctx, types.NewPoolRecord( sdk.NewCoins( sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), ), sdk.NewInt(-1e6), )) message, broken = suite.runInvariant("pool-records", keeper.PoolRecordsInvariant) suite.Equal("swap: validate pool records broken invariant\npool record invalid\n", message) suite.Equal(true, broken) } func (suite *invariantTestSuite) TestShareRecordsInvariant() { message, broken := suite.runInvariant("share-records", keeper.ShareRecordsInvariant) suite.Equal("swap: validate share records broken invariant\nshare record invalid\n", message) suite.Equal(false, broken) suite.SetupValidState() message, broken = suite.runInvariant("share-records", keeper.ShareRecordsInvariant) suite.Equal("swap: validate share records broken invariant\nshare record invalid\n", message) suite.Equal(false, broken) // broken with invalid share record suite.Keeper.SetDepositorShares_Raw(suite.Ctx, types.NewShareRecord( sdk.AccAddress("depositor 1---------"), types.PoolID("ukava", "usdx"), sdk.NewInt(-1e6), )) message, broken = suite.runInvariant("share-records", keeper.ShareRecordsInvariant) suite.Equal("swap: validate share records broken invariant\nshare record invalid\n", message) suite.Equal(true, broken) } func (suite *invariantTestSuite) TestPoolReservesInvariant() { message, broken := suite.runInvariant("pool-reserves", keeper.PoolReservesInvariant) suite.Equal("swap: pool reserves broken invariant\npool reserves do not match module account\n", message) suite.Equal(false, broken) suite.SetupValidState() message, broken = suite.runInvariant("pool-reserves", keeper.PoolReservesInvariant) suite.Equal("swap: pool reserves broken invariant\npool reserves do not match module account\n", message) suite.Equal(false, broken) // broken when reserves are greater than module balance suite.Keeper.SetPool(suite.Ctx, types.NewPoolRecord( sdk.NewCoins( sdk.NewCoin("ukava", sdk.NewInt(2e6)), sdk.NewCoin("usdx", sdk.NewInt(10e6)), ), sdk.NewInt(5e6), )) message, broken = suite.runInvariant("pool-reserves", keeper.PoolReservesInvariant) suite.Equal("swap: pool reserves broken invariant\npool reserves do not match module account\n", message) suite.Equal(true, broken) // broken when reserves are less than the module balance suite.Keeper.SetPool(suite.Ctx, types.NewPoolRecord( sdk.NewCoins( sdk.NewCoin("ukava", sdk.NewInt(1e5)), sdk.NewCoin("usdx", sdk.NewInt(5e5)), ), sdk.NewInt(3e5), )) message, broken = suite.runInvariant("pool-reserves", keeper.PoolReservesInvariant) suite.Equal("swap: pool reserves broken invariant\npool reserves do not match module account\n", message) suite.Equal(true, broken) } func (suite *invariantTestSuite) TestPoolSharesInvariant() { message, broken := suite.runInvariant("pool-shares", keeper.PoolSharesInvariant) suite.Equal("swap: pool shares broken invariant\npool shares do not match depositor shares\n", message) suite.Equal(false, broken) suite.SetupValidState() message, broken = suite.runInvariant("pool-shares", keeper.PoolSharesInvariant) suite.Equal("swap: pool shares broken invariant\npool shares do not match depositor shares\n", message) suite.Equal(false, broken) // broken when total shares are greater than depositor shares suite.Keeper.SetPool(suite.Ctx, types.NewPoolRecord( sdk.NewCoins( sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), ), sdk.NewInt(5e6), )) message, broken = suite.runInvariant("pool-shares", keeper.PoolSharesInvariant) suite.Equal("swap: pool shares broken invariant\npool shares do not match depositor shares\n", message) suite.Equal(true, broken) // broken when total shares are less than the depositor shares suite.Keeper.SetPool(suite.Ctx, types.NewPoolRecord( sdk.NewCoins( sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), ), sdk.NewInt(1e5), )) message, broken = suite.runInvariant("pool-shares", keeper.PoolSharesInvariant) suite.Equal("swap: pool shares broken invariant\npool shares do not match depositor shares\n", message) suite.Equal(true, broken) // broken when pool record is missing suite.Keeper.DeletePool(suite.Ctx, types.PoolID("ukava", "usdx")) suite.RemoveCoinsFromModule( sdk.NewCoins( sdk.NewCoin("ukava", sdk.NewInt(1e6)), sdk.NewCoin("usdx", sdk.NewInt(5e6)), ), ) message, broken = suite.runInvariant("pool-shares", keeper.PoolSharesInvariant) suite.Equal("swap: pool shares broken invariant\npool shares do not match depositor shares\n", message) suite.Equal(true, broken) } func TestInvariantTestSuite(t *testing.T) { suite.Run(t, new(invariantTestSuite)) }