Savings module invariants (#1199)

* add deposits to genesis state

* import/export genesis with deposits

* add helper keeper method + update tests

* invariants + tests

* register invariants on module

* fix genesis test invariant init

* clean up invariants test

* remove comment from test file

* fix invariants test

* run 'make proto-all'
This commit is contained in:
Denali Marsh 2022-03-30 13:51:06 +02:00 committed by GitHub
parent 003b040458
commit 988836dee0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 469 additions and 210 deletions

File diff suppressed because it is too large Load Diff

View File

@ -375,6 +375,8 @@
- [kava/savings/v1beta1/tx.proto](#kava/savings/v1beta1/tx.proto)
- [MsgDeposit](#kava.savings.v1beta1.MsgDeposit)
- [MsgDepositResponse](#kava.savings.v1beta1.MsgDepositResponse)
- [MsgWithdraw](#kava.savings.v1beta1.MsgWithdraw)
- [MsgWithdrawResponse](#kava.savings.v1beta1.MsgWithdrawResponse)
- [Msg](#kava.savings.v1beta1.Msg)
@ -5086,6 +5088,7 @@ GenesisState defines the savings module's genesis state.
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| `params` | [Params](#kava.savings.v1beta1.Params) | | params defines all the parameters of the module. |
| `deposits` | [Deposit](#kava.savings.v1beta1.Deposit) | repeated | |
@ -5222,6 +5225,32 @@ MsgDepositResponse defines the Msg/Deposit response type.
<a name="kava.savings.v1beta1.MsgWithdraw"></a>
### MsgWithdraw
MsgWithdraw defines the Msg/Withdraw request type.
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| `depositor` | [string](#string) | | |
| `amount` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | repeated | |
<a name="kava.savings.v1beta1.MsgWithdrawResponse"></a>
### MsgWithdrawResponse
MsgWithdrawResponse defines the Msg/Withdraw response type.
<!-- end messages -->
<!-- end enums -->
@ -5237,6 +5266,7 @@ Msg defines the savings Msg service.
| Method Name | Request Type | Response Type | Description | HTTP Verb | Endpoint |
| ----------- | ------------ | ------------- | ------------| ------- | -------- |
| `Deposit` | [MsgDeposit](#kava.savings.v1beta1.MsgDeposit) | [MsgDepositResponse](#kava.savings.v1beta1.MsgDepositResponse) | Deposit defines a method for depositing funds to the savings module account | |
| `Withdraw` | [MsgWithdraw](#kava.savings.v1beta1.MsgWithdraw) | [MsgWithdrawResponse](#kava.savings.v1beta1.MsgWithdrawResponse) | Withdraw defines a method for withdrawing funds to the savings module account | |
<!-- end services -->

View File

@ -15,7 +15,6 @@ service Msg {
// Withdraw defines a method for withdrawing funds to the savings module account
rpc Withdraw(MsgWithdraw) returns (MsgWithdrawResponse);
}
// MsgDeposit defines the Msg/Deposit request type.
@ -33,7 +32,7 @@ message MsgWithdraw {
string depositor = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
repeated cosmos.base.v1beta1.Coin amount = 2
[(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins", (gogoproto.nullable) = false];
}
}
// MsgWithdrawResponse defines the Msg/Withdraw response type.
message MsgWithdrawResponse {}
// MsgWithdrawResponse defines the Msg/Withdraw response type.
message MsgWithdrawResponse {}

View File

@ -42,20 +42,26 @@ func (suite *GenesisTestSuite) TestInitExportGenesis() {
[]string{"btc", "ukava", "bnb"},
)
depositAmt := sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e8)))
deposits := types.Deposits{
types.NewDeposit(
suite.addrs[0],
sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e8))), // 100 ukava
depositAmt, // 100 ukava
),
}
savingsGenesis := types.NewGenesisState(params, deposits)
authBuilder := app.NewAuthBankGenesisBuilder().
WithSimpleModuleAccount(types.ModuleAccountName, depositAmt)
cdc := suite.app.AppCodec()
suite.NotPanics(
func() {
suite.app.InitializeFromGenesisStatesWithTime(
suite.genTime,
app.GenesisState{types.ModuleName: suite.app.AppCodec().MustMarshalJSON(&savingsGenesis)},
authBuilder.BuildMarshalled(cdc),
app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(&savingsGenesis)},
)
},
)

View File

@ -0,0 +1,67 @@
package keeper
import (
"github.com/kava-labs/kava/x/savings/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// RegisterInvariants registers the savings module invariants
func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) {
ir.RegisterRoute(types.ModuleName, "deposits", DepositsInvariant(k))
ir.RegisterRoute(types.ModuleName, "solvency", SolvencyInvariant(k))
}
// AllInvariants runs all invariants of the savings module
func AllInvariants(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
if res, stop := DepositsInvariant(k)(ctx); stop {
return res, stop
}
res, stop := SolvencyInvariant(k)(ctx)
return res, stop
}
}
// DepositsInvariant iterates all deposits and asserts that they are valid
func DepositsInvariant(k Keeper) sdk.Invariant {
broken := false
message := sdk.FormatInvariant(types.ModuleName, "validate deposits broken", "deposit invalid")
return func(ctx sdk.Context) (string, bool) {
k.IterateDeposits(ctx, func(deposit types.Deposit) bool {
if err := deposit.Validate(); err != nil {
broken = true
return true
}
if !deposit.Amount.IsAllPositive() {
broken = true
return true
}
return false
})
return message, broken
}
}
// SolvencyInvariant iterates all deposits and ensures the total amount matches the module account coins
func SolvencyInvariant(k Keeper) sdk.Invariant {
message := sdk.FormatInvariant(types.ModuleName, "module solvency broken", "total deposited amount does not match module account")
return func(ctx sdk.Context) (string, bool) {
balance := k.bankKeeper.GetAllBalances(ctx, k.GetSavingsModuleAccount(ctx).GetAddress())
deposited := sdk.Coins{}
k.IterateDeposits(ctx, func(deposit types.Deposit) bool {
for _, coin := range deposit.Amount {
deposited = deposited.Add(coin)
}
return false
})
broken := !deposited.IsEqual(balance)
return message, broken
}
}

View File

@ -0,0 +1,149 @@
package keeper_test
import (
"testing"
"github.com/stretchr/testify/suite"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
tmtime "github.com/tendermint/tendermint/types/time"
"github.com/cosmos/cosmos-sdk/simapp"
sdk "github.com/cosmos/cosmos-sdk/types"
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/savings/keeper"
"github.com/kava-labs/kava/x/savings/types"
)
type invariantTestSuite struct {
suite.Suite
tApp app.TestApp
ctx sdk.Context
keeper keeper.Keeper
bankKeeper bankkeeper.Keeper
addrs []sdk.AccAddress
invariants map[string]map[string]sdk.Invariant
}
func (suite *invariantTestSuite) SetupTest() {
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: tmtime.Now()})
_, addrs := app.GeneratePrivKeyAddressPairs(1)
suite.addrs = addrs
suite.ctx = ctx
suite.keeper = tApp.GetSavingsKeeper()
suite.bankKeeper = tApp.GetBankKeeper()
suite.invariants = make(map[string]map[string]sdk.Invariant)
keeper.RegisterInvariants(suite, suite.keeper)
}
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) SetupValidState() {
depositAmt := sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(2e8)))
suite.keeper.SetDeposit(suite.ctx, types.NewDeposit(
suite.addrs[0],
depositAmt,
))
err := simapp.FundModuleAccount(suite.bankKeeper, suite.ctx, types.ModuleName, depositAmt)
suite.Require().NoError(err)
}
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) TestDepositsInvariant() {
message, broken := suite.runInvariant("deposits", keeper.DepositsInvariant)
suite.Equal("savings: validate deposits broken invariant\ndeposit invalid\n", message)
suite.Equal(false, broken)
suite.SetupValidState()
message, broken = suite.runInvariant("deposits", keeper.DepositsInvariant)
suite.Equal("savings: validate deposits broken invariant\ndeposit invalid\n", message)
suite.Equal(false, broken)
// broken with invalid deposit
suite.keeper.SetDeposit(suite.ctx, types.NewDeposit(
suite.addrs[0],
sdk.Coins{},
))
message, broken = suite.runInvariant("deposits", keeper.DepositsInvariant)
suite.Equal("savings: validate deposits broken invariant\ndeposit invalid\n", message)
suite.Equal(true, broken)
}
func (suite *invariantTestSuite) TestSolvencyInvariant() {
message, broken := suite.runInvariant("solvency", keeper.SolvencyInvariant)
suite.Equal("savings: module solvency broken invariant\ntotal deposited amount does not match module account\n", message)
suite.Equal(false, broken)
suite.SetupValidState()
message, broken = suite.runInvariant("solvency", keeper.SolvencyInvariant)
suite.Equal("savings: module solvency broken invariant\ntotal deposited amount does not match module account\n", message)
suite.Equal(false, broken)
// broken when deposits are greater than module balance
suite.keeper.SetDeposit(suite.ctx, types.NewDeposit(
suite.addrs[0],
sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(3e8))),
))
message, broken = suite.runInvariant("solvency", keeper.SolvencyInvariant)
suite.Equal("savings: module solvency broken invariant\ntotal deposited amount does not match module account\n", message)
suite.Equal(true, broken)
// broken when deposits are less than the module balance
suite.keeper.SetDeposit(suite.ctx, types.NewDeposit(
suite.addrs[0],
sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e8))),
))
message, broken = suite.runInvariant("solvency", keeper.SolvencyInvariant)
suite.Equal("savings: module solvency broken invariant\ntotal deposited amount does not match module account\n", message)
suite.Equal(true, broken)
}
func TestInvariantTestSuite(t *testing.T) {
suite.Run(t, new(invariantTestSuite))
}

View File

@ -6,6 +6,7 @@ import (
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
paramtypes "github.com/cosmos/cosmos-sdk/x/params/types"
"github.com/tendermint/tendermint/libs/log"
@ -39,6 +40,11 @@ func NewKeeper(
}
}
// GetSavingsModuleAccount returns the savings ModuleAccount
func (k Keeper) GetSavingsModuleAccount(ctx sdk.Context) authtypes.ModuleAccountI {
return k.accountKeeper.GetModuleAccount(ctx, types.ModuleAccountName)
}
// GetDeposit returns a deposit from the store for a particular depositor address, deposit denom
func (k Keeper) GetDeposit(ctx sdk.Context, depositor sdk.AccAddress) (types.Deposit, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.DepositsKeyPrefix)

View File

@ -111,7 +111,9 @@ func (am AppModule) Name() string {
}
// RegisterInvariants register module invariants
func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {}
func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {
keeper.RegisterInvariants(ir, am.keeper)
}
// Route module message route name
func (am AppModule) Route() sdk.Route {