diff --git a/testing/simapp/test_helpers.go b/testing/simapp/test_helpers.go index 6285c18d..5b190df5 100644 --- a/testing/simapp/test_helpers.go +++ b/testing/simapp/test_helpers.go @@ -170,7 +170,6 @@ func genesisStateWithValSet(app *SimApp, genesisState GenesisState, valSet *tmty // set validators and delegations stakingGenesis := stakingtypes.NewGenesisState(stakingtypes.DefaultParams(), validators, delegations) - genesisState[stakingtypes.ModuleName] = app.AppCodec().MustMarshalJSON(stakingGenesis) multistakingGenesis := multistakingtypes.GenesisState{ MultiStakingLocks: locks, diff --git a/testutil/address.go b/testutil/address.go index 1d009b45..94eb0119 100644 --- a/testutil/address.go +++ b/testutil/address.go @@ -21,3 +21,9 @@ func GenValAddress() sdk.ValAddress { return sdk.ValAddress(priv.PubKey().Address()) } + +func GenValAddressWithPrivKey() (*secp256k1.PrivKey, sdk.ValAddress) { + priv := secp256k1.GenPrivKey() + + return priv, sdk.ValAddress(priv.PubKey().Address()) +} diff --git a/x/multi-staking/keeper/invariants.go b/x/multi-staking/keeper/invariants.go new file mode 100644 index 00000000..864863a5 --- /dev/null +++ b/x/multi-staking/keeper/invariants.go @@ -0,0 +1,108 @@ +package keeper + +import ( + "fmt" + + "github.com/realio-tech/multi-staking-module/x/multi-staking/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +// RegisterInvariants registers all staking invariants +func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) { + ir.RegisterRoute(types.ModuleName, "module-accounts", + ModuleAccountInvariants(k)) + ir.RegisterRoute(types.ModuleName, "validator-lock-denom", + ValidatorLockDenomInvariants(k)) +} + +func ModuleAccountInvariants(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + totalLockCoinAmount := sdk.NewCoins() + + // calculate lock amount + lockCoinAmount := sdk.NewCoins() + k.MultiStakingLockIterator(ctx, func(stakingLock types.MultiStakingLock) bool { + lockCoinAmount = lockCoinAmount.Add(stakingLock.LockedCoin.ToCoin()) + return false + }) + totalLockCoinAmount = totalLockCoinAmount.Add(lockCoinAmount...) + + // calculate unlocking amount + unlockingCoinAmount := sdk.NewCoins() + k.MultiStakingUnlockIterator(ctx, func(unlock types.MultiStakingUnlock) bool { + for _, entry := range unlock.Entries { + unlockingCoinAmount = unlockingCoinAmount.Add(entry.UnlockingCoin.ToCoin()) + } + return false + }) + totalLockCoinAmount = totalLockCoinAmount.Add(unlockingCoinAmount...) + + moduleAccount := authtypes.NewModuleAddress(types.ModuleName) + escrowBalances := k.bankKeeper.GetAllBalances(ctx, moduleAccount) + + broken := !escrowBalances.IsAllGTE(totalLockCoinAmount) + + return sdk.FormatInvariant( + types.ModuleName, + "ModuleAccountInvariants", + fmt.Sprintf( + "\tescrow coins balances: %v\n"+ + "\ttotal lock coin amount: %v\n", + escrowBalances, totalLockCoinAmount), + ), broken + } +} + +func ValidatorLockDenomInvariants(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + var ( + msg string + broken bool + ) + + var multiStakingLocks []types.MultiStakingLock + k.MultiStakingLockIterator(ctx, func(stakingLock types.MultiStakingLock) bool { + multiStakingLocks = append(multiStakingLocks, stakingLock) + return false + }) + + for _, lock := range multiStakingLocks { + valBench32Addr := lock.LockID.ValAddr + valAddr, _ := sdk.ValAddressFromBech32(valBench32Addr) + if valMsDenom := k.GetValidatorMultiStakingCoin(ctx, valAddr); valMsDenom != lock.LockedCoin.Denom { + broken = true + msg += fmt.Sprintf("validator lock denom invariants:\n\t"+ + "\tlock denom: %v allow denom: %v\n"+ + "\tlock: %v\n", + lock.LockedCoin.Denom, valMsDenom, lock) + } + } + + var multiStakingUnlocks []types.MultiStakingUnlock + k.MultiStakingUnlockIterator(ctx, func(stakingUnlock types.MultiStakingUnlock) bool { + multiStakingUnlocks = append(multiStakingUnlocks, stakingUnlock) + return false + }) + + for _, unlock := range multiStakingUnlocks { + valBench32Addr := unlock.UnlockID.ValAddr + valAddr, _ := sdk.ValAddressFromBech32(valBench32Addr) + valMsDenom := k.GetValidatorMultiStakingCoin(ctx, valAddr) + + for _, entry := range unlock.Entries { + if entry.UnlockingCoin.Denom != valMsDenom { + broken = true + msg += fmt.Sprintf("validator unlock denom invariants:\n\t"+ + "\n\tunlock denom: %v allow denom: %v\n"+ + "\n\t entry height %v"+ + "\n\t validator address %s deladdress %s", + entry.UnlockingCoin.Denom, valMsDenom, entry.CreationHeight, unlock.UnlockID.ValAddr, unlock.UnlockID.MultiStakerAddr) + } + } + } + + return sdk.FormatInvariant(types.ModuleName, "validator lock denom", fmt.Sprintf("found invalid validator lock denom\n%s", msg)), broken + } +} diff --git a/x/multi-staking/keeper/invartiants_test.go b/x/multi-staking/keeper/invartiants_test.go new file mode 100644 index 00000000..02f058ca --- /dev/null +++ b/x/multi-staking/keeper/invartiants_test.go @@ -0,0 +1,161 @@ +package keeper_test + +import ( + "time" + + "github.com/realio-tech/multi-staking-module/testutil" + "github.com/realio-tech/multi-staking-module/x/multi-staking/keeper" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +func (suite *KeeperTestSuite) TestModuleAccountInvariants() { + delAddr := testutil.GenAddress() + priv, valAddr := testutil.GenValAddressWithPrivKey() + valPubKey := priv.PubKey() + + testCases := []struct { + name string + malleate func() + expPass bool + }{ + { + name: "Success", + malleate: func() {}, + expPass: true, + }, + { + name: "Success Edit Validator", + malleate: func() { + suite.ctx = suite.ctx.WithBlockHeader(tmproto.Header{Time: time.Now()}) + newRate := sdk.MustNewDecFromStr("0.03") + newMinSelfDelegation := sdk.NewInt(300) + editMsg := stakingtypes.NewMsgEditValidator(valAddr, stakingtypes.Description{ + Moniker: "test 1", + Identity: "test 1", + Website: "test 1", + SecurityContact: "test 1", + Details: "test 1", + }, + &newRate, + &newMinSelfDelegation, + ) + _, err := suite.msgServer.EditValidator(suite.ctx, editMsg) + suite.Require().NoError(err) + }, + expPass: true, + }, + { + name: "Success Delegate", + malleate: func() { + bondAmount := sdk.NewCoin(MultiStakingDenomA, sdk.NewInt(1000)) + delMsg := stakingtypes.NewMsgDelegate(delAddr, valAddr, bondAmount) + _, err := suite.msgServer.Delegate(suite.ctx, delMsg) + suite.Require().NoError(err) + }, + expPass: true, + }, + { + name: "Success Delegate", + malleate: func() { + bondAmount := sdk.NewCoin(MultiStakingDenomA, sdk.NewInt(1000)) + delMsg := stakingtypes.NewMsgDelegate(delAddr, valAddr, bondAmount) + _, err := suite.msgServer.Delegate(suite.ctx, delMsg) + suite.Require().NoError(err) + }, + expPass: true, + }, + { + name: "Success BeginRedelegate", + malleate: func() { + priv, valAddr2 := testutil.GenValAddressWithPrivKey() + valPubKey2 := priv.PubKey() + bondAmount := sdk.NewCoin(MultiStakingDenomA, sdk.NewInt(500)) + createMsg2 := stakingtypes.MsgCreateValidator{ + Description: stakingtypes.Description{ + Moniker: "test", + Identity: "test", + Website: "test", + SecurityContact: "test", + Details: "test", + }, + Commission: stakingtypes.CommissionRates{ + Rate: sdk.MustNewDecFromStr("0.05"), + MaxRate: sdk.MustNewDecFromStr("0.1"), + MaxChangeRate: sdk.MustNewDecFromStr("0.1"), + }, + MinSelfDelegation: sdk.NewInt(200), + DelegatorAddress: delAddr.String(), + ValidatorAddress: valAddr2.String(), + Pubkey: codectypes.UnsafePackAny(valPubKey2), + Value: bondAmount, + } + + _, err := suite.msgServer.CreateValidator(suite.ctx, &createMsg2) + suite.Require().NoError(err) + + multiStakingMsg := stakingtypes.NewMsgBeginRedelegate(delAddr, valAddr, valAddr2, bondAmount) + _, err = suite.msgServer.BeginRedelegate(suite.ctx, multiStakingMsg) + suite.Require().NoError(err) + }, + expPass: true, + }, + { + name: "Success Undelegate", + malleate: func() { + bondAmount := sdk.NewCoin(MultiStakingDenomA, sdk.NewInt(250)) + multiStakingMsg := stakingtypes.NewMsgUndelegate(delAddr, valAddr, bondAmount) + _, err := suite.msgServer.Undelegate(suite.ctx, multiStakingMsg) + suite.Require().NoError(err) + + bondAmount1 := sdk.NewCoin(MultiStakingDenomA, sdk.NewInt(500)) + multiStakingMsg1 := stakingtypes.NewMsgUndelegate(delAddr, valAddr, bondAmount1) + _, err = suite.msgServer.Undelegate(suite.ctx, multiStakingMsg1) + suite.Require().NoError(err) + }, + expPass: true, + }, + } + for _, tc := range testCases { + suite.SetupTest() // reset + + valCoins := sdk.NewCoins(sdk.NewCoin(MultiStakingDenomA, sdk.NewInt(10000)), sdk.NewCoin(MultiStakingDenomB, sdk.NewInt(10000))) + err := suite.FundAccount(delAddr, valCoins) + suite.Require().NoError(err) + + suite.msKeeper.SetBondWeight(suite.ctx, MultiStakingDenomA, sdk.MustNewDecFromStr("0.3")) + bondAmount := sdk.NewCoin(MultiStakingDenomA, sdk.NewInt(3001)) + msg := stakingtypes.MsgCreateValidator{ + Description: stakingtypes.Description{ + Moniker: "test", + Identity: "test", + Website: "test", + SecurityContact: "test", + Details: "test", + }, + Commission: stakingtypes.CommissionRates{ + Rate: sdk.MustNewDecFromStr("0.05"), + MaxRate: sdk.MustNewDecFromStr("0.1"), + MaxChangeRate: sdk.MustNewDecFromStr("0.05"), + }, + MinSelfDelegation: sdk.NewInt(1), + DelegatorAddress: delAddr.String(), + ValidatorAddress: valAddr.String(), + Pubkey: codectypes.UnsafePackAny(valPubKey), + Value: bondAmount, + } + + _, err = suite.msgServer.CreateValidator(suite.ctx, &msg) + suite.Require().NoError(err) + + tc.malleate() + _, broken := keeper.ModuleAccountInvariants(*suite.msKeeper)(suite.ctx) + + if tc.expPass { + suite.Require().False(broken) + } + } +} diff --git a/x/multi-staking/keeper/store_test.go b/x/multi-staking/keeper/store_test.go index ad843b96..b5850480 100644 --- a/x/multi-staking/keeper/store_test.go +++ b/x/multi-staking/keeper/store_test.go @@ -3,15 +3,19 @@ package keeper_test import ( "github.com/realio-tech/multi-staking-module/testutil" multistakingkeeper "github.com/realio-tech/multi-staking-module/x/multi-staking/keeper" + "github.com/realio-tech/multi-staking-module/x/multi-staking/types" sdk "github.com/cosmos/cosmos-sdk/types" ) +var ( + gasDenom = "ario" + govDenom = "arst" +) + func (suite *KeeperTestSuite) TestSetBondWeight() { suite.SetupTest() - gasDenom := "ario" - govDenom := "arst" gasWeight := sdk.OneDec() govWeight := sdk.NewDecWithPrec(2, 4) @@ -28,8 +32,7 @@ func (suite *KeeperTestSuite) TestSetBondWeight() { func (suite *KeeperTestSuite) TestSetValidatorMultiStakingCoin() { valA := testutil.GenValAddress() valB := testutil.GenValAddress() - gasDenom := "ario" - govDenom := "arst" + testCases := []struct { name string malleate func(ctx sdk.Context, msKeeper *multistakingkeeper.Keeper) []string @@ -86,3 +89,43 @@ func (suite *KeeperTestSuite) TestSetValidatorMultiStakingCoin() { }) } } + +func (suite *KeeperTestSuite) TestSetMultiStakingLock() { + suite.SetupTest() + delAddr := testutil.GenAddress() + valAddr := testutil.GenValAddress() + + lock := types.MultiStakingLock{ + LockID: types.LockID{ + MultiStakerAddr: delAddr.String(), + ValAddr: valAddr.String(), + }, + LockedCoin: types.MultiStakingCoin{ + Denom: gasDenom, + Amount: sdk.NewIntFromUint64(1000000), + BondWeight: sdk.NewDec(1), + }, + } + + testCases := []struct { + name string + malleate func() + expError bool + }{ + { + "Success", + func() { + suite.msKeeper.SetMultiStakingLock(suite.ctx, lock) + }, + false, + }, + } + for _, tc := range testCases { + if !tc.expError { + tc.malleate() + msLock, found := suite.msKeeper.GetMultiStakingLock(suite.ctx, lock.LockID) + suite.Require().True(found) + suite.Require().Equal(lock, msLock) + } + } +} diff --git a/x/multi-staking/module.go b/x/multi-staking/module.go index aafe104b..3ddd8f15 100644 --- a/x/multi-staking/module.go +++ b/x/multi-staking/module.go @@ -111,8 +111,9 @@ func (AppModule) Name() string { } // RegisterInvariants registers the staking module invariants. -// TODO: Need to implement invariants func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) { + am.skAppModule.RegisterInvariants(ir) + multistakingkeeper.RegisterInvariants(ir, am.keeper) } // Deprecated: Route returns the message routing key for the staking module. diff --git a/x/multi-staking/types/expected_keepers.go b/x/multi-staking/types/expected_keepers.go index e3127e36..edc7dfb3 100644 --- a/x/multi-staking/types/expected_keepers.go +++ b/x/multi-staking/types/expected_keepers.go @@ -28,6 +28,7 @@ type StakingKeeper interface { type BankKeeper interface { GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin + GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error BurnCoins(ctx sdk.Context, moduleName string, amounts sdk.Coins) error SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error