Add Init/ExportGenesis for Earn module (#1280)

* Add vault and vault shares to genesis

* Fix error and denoms

* Update init and export genesis with correct data

* Fix total supply genesis check, remove legacy test for enum errors

* Add vault slice validate tests

* Update denom validation, add additional test
This commit is contained in:
Derrick Lee 2022-07-28 10:01:30 -07:00 committed by GitHub
parent 82e2f26e14
commit ed9991d44a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 622 additions and 7 deletions

View File

@ -10,16 +10,54 @@ import (
)
// InitGenesis initializes genesis state
func InitGenesis(ctx sdk.Context, k keeper.Keeper, ak types.AccountKeeper, gs types.GenesisState) {
func InitGenesis(
ctx sdk.Context,
k keeper.Keeper,
ak types.AccountKeeper,
gs types.GenesisState,
) {
if err := gs.Validate(); err != nil {
panic(fmt.Sprintf("failed to validate %s genesis state: %s", types.ModuleName, err))
}
// Total of all vault share records, vault record total supply should equal this
vaultTotalSupplies := sdk.NewCoins()
for _, vaultShareRecord := range gs.VaultShareRecords {
if err := vaultShareRecord.Validate(); err != nil {
panic(fmt.Sprintf("invalid vault share: %s", err))
}
vaultTotalSupplies = vaultTotalSupplies.Add(vaultShareRecord.AmountSupplied...)
k.SetVaultShareRecord(ctx, vaultShareRecord)
}
for _, vaultRecord := range gs.VaultRecords {
if err := vaultRecord.Validate(); err != nil {
panic(fmt.Sprintf("invalid vault record: %s", err))
}
if !vaultRecord.TotalSupply.Amount.Equal(vaultTotalSupplies.AmountOf(vaultRecord.Denom)) {
panic(fmt.Sprintf(
"invalid vault record total supply for %s, got %s but sum of vault shares is %s",
vaultRecord.Denom,
vaultRecord.TotalSupply.Amount,
vaultTotalSupplies.AmountOf(vaultRecord.Denom),
))
}
k.SetVaultRecord(ctx, vaultRecord)
}
k.SetParams(ctx, gs.Params)
}
// ExportGenesis returns a GenesisState for a given context and keeper
func ExportGenesis(ctx sdk.Context, k keeper.Keeper) types.GenesisState {
params := k.GetParams(ctx)
return types.NewGenesisState(params)
vaultRecords := k.GetAllVaultRecords(ctx)
vaultShareRecords := k.GetAllVaultShareRecords(ctx)
return types.NewGenesisState(params, vaultRecords, vaultShareRecords)
}

145
x/earn/genesis_test.go Normal file
View File

@ -0,0 +1,145 @@
package earn_test
import (
"testing"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/earn"
"github.com/kava-labs/kava/x/earn/testutil"
"github.com/kava-labs/kava/x/earn/types"
"github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types"
)
type genesisTestSuite struct {
testutil.Suite
}
func (suite *genesisTestSuite) Test_InitGenesis_ValidationPanic() {
invalidState := types.NewGenesisState(
types.Params{
AllowedVaults: types.AllowedVaults{
types.NewAllowedVault("usdx", types.STRATEGY_TYPE_HARD),
},
},
types.VaultRecords{
{
Denom: "",
TotalSupply: sdk.NewInt64Coin("usdx", 0),
},
},
types.VaultShareRecords{},
)
suite.Panics(func() {
earn.InitGenesis(suite.Ctx, suite.Keeper, suite.AccountKeeper, invalidState)
}, "expected init genesis to panic with invalid state")
}
func (suite *genesisTestSuite) Test_InitAndExportGenesis() {
depositor_1, err := sdk.AccAddressFromBech32("kava1esagqd83rhqdtpy5sxhklaxgn58k2m3s3mnpea")
suite.Require().NoError(err)
depositor_2, err := sdk.AccAddressFromBech32("kava1mq9qxlhze029lm0frzw2xr6hem8c3k9ts54w0w")
suite.Require().NoError(err)
// slices are sorted by key as stored in the data store, so init and export can be compared with equal
state := types.NewGenesisState(
types.Params{
AllowedVaults: types.AllowedVaults{
types.NewAllowedVault("usdx", types.STRATEGY_TYPE_HARD),
types.NewAllowedVault("ukava", types.STRATEGY_TYPE_SAVINGS),
},
},
types.VaultRecords{
types.VaultRecord{
Denom: "ukava",
TotalSupply: sdk.NewInt64Coin("ukava", 2000000),
},
types.VaultRecord{
Denom: "usdx",
TotalSupply: sdk.NewInt64Coin("usdx", 1000000),
},
},
types.VaultShareRecords{
types.VaultShareRecord{
Depositor: depositor_1,
AmountSupplied: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000), sdk.NewInt64Coin("ukava", 1900000)),
},
types.VaultShareRecord{
Depositor: depositor_2,
AmountSupplied: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000), sdk.NewInt64Coin("ukava", 100000)),
},
},
)
earn.InitGenesis(suite.Ctx, suite.Keeper, suite.AccountKeeper, state)
suite.Equal(state.Params, suite.Keeper.GetParams(suite.Ctx))
vaultRecord1, _ := suite.Keeper.GetVaultRecord(suite.Ctx, "ukava")
vaultRecord2, _ := suite.Keeper.GetVaultRecord(suite.Ctx, "usdx")
suite.Equal(state.VaultRecords[0], vaultRecord1)
suite.Equal(state.VaultRecords[1], vaultRecord2)
shareRecord1, _ := suite.Keeper.GetVaultShareRecord(suite.Ctx, depositor_1)
shareRecord2, _ := suite.Keeper.GetVaultShareRecord(suite.Ctx, depositor_2)
suite.Equal(state.VaultShareRecords[0], shareRecord1)
suite.Equal(state.VaultShareRecords[1], shareRecord2)
exportedState := earn.ExportGenesis(suite.Ctx, suite.Keeper)
suite.Equal(state, exportedState)
}
func (suite *genesisTestSuite) Test_Marshall() {
depositor_1, err := sdk.AccAddressFromBech32("kava1esagqd83rhqdtpy5sxhklaxgn58k2m3s3mnpea")
suite.Require().NoError(err)
depositor_2, err := sdk.AccAddressFromBech32("kava1mq9qxlhze029lm0frzw2xr6hem8c3k9ts54w0w")
suite.Require().NoError(err)
// slices are sorted by key as stored in the data store, so init and export can be compared with equal
state := types.NewGenesisState(
types.Params{
AllowedVaults: types.AllowedVaults{
types.NewAllowedVault("usdx", types.STRATEGY_TYPE_HARD),
types.NewAllowedVault("ukava", types.STRATEGY_TYPE_SAVINGS),
},
},
types.VaultRecords{
types.VaultRecord{
Denom: "ukava",
TotalSupply: sdk.NewInt64Coin("ukava", 2000000),
},
types.VaultRecord{
Denom: "usdx",
TotalSupply: sdk.NewInt64Coin("usdx", 1000000),
},
},
types.VaultShareRecords{
types.VaultShareRecord{
Depositor: depositor_1,
AmountSupplied: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000), sdk.NewInt64Coin("ukava", 1900000)),
},
types.VaultShareRecord{
Depositor: depositor_2,
AmountSupplied: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000), sdk.NewInt64Coin("ukava", 100000)),
},
},
)
encodingCfg := app.MakeEncodingConfig()
cdc := encodingCfg.Marshaler
bz, err := cdc.Marshal(&state)
suite.Require().NoError(err, "expected genesis state to marshal without error")
var decodedState types.GenesisState
err = cdc.Unmarshal(bz, &decodedState)
suite.Require().NoError(err, "expected genesis state to unmarshal without error")
suite.Equal(state, decodedState)
}
func TestGenesisTestSuite(t *testing.T) {
suite.Run(t, new(genesisTestSuite))
}

View File

@ -222,3 +222,34 @@ func (k *Keeper) SetVaultShareRecord(
bz := k.cdc.MustMarshal(&record)
store.Set(types.DepositorVaultSharesKey(record.Depositor), bz)
}
// IterateVaultShareRecords iterates over all vault share objects in the store
// and performs a callback function.
func (k Keeper) IterateVaultShareRecords(
ctx sdk.Context,
cb func(record types.VaultShareRecord) (stop bool),
) {
store := prefix.NewStore(ctx.KVStore(k.key), types.VaultShareRecordKeyPrefix)
iterator := sdk.KVStorePrefixIterator(store, []byte{})
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var record types.VaultShareRecord
k.cdc.MustUnmarshal(iterator.Value(), &record)
if cb(record) {
break
}
}
}
// GetAllVaultShareRecords returns all vault share records from the store.
func (k Keeper) GetAllVaultShareRecords(ctx sdk.Context) types.VaultShareRecords {
var records types.VaultShareRecords
k.IterateVaultShareRecords(ctx, func(record types.VaultShareRecord) bool {
records = append(records, record)
return false
})
return records
}

View File

@ -1,9 +1,15 @@
package types
// NewGenesisState creates a new genesis state.
func NewGenesisState(params Params) GenesisState {
func NewGenesisState(
params Params,
vaultRecords VaultRecords,
vaultShareRecords VaultShareRecords,
) GenesisState {
return GenesisState{
Params: params,
Params: params,
VaultRecords: vaultRecords,
VaultShareRecords: vaultShareRecords,
}
}
@ -13,6 +19,14 @@ func (gs GenesisState) Validate() error {
return err
}
if err := gs.VaultRecords.Validate(); err != nil {
return err
}
if err := gs.VaultShareRecords.Validate(); err != nil {
return err
}
return nil
}
@ -20,5 +34,7 @@ func (gs GenesisState) Validate() error {
func DefaultGenesisState() GenesisState {
return NewGenesisState(
DefaultParams(),
VaultRecords{},
VaultShareRecords{},
)
}

View File

@ -4,6 +4,7 @@ import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// NewVaultRecord returns a new VaultRecord with 0 supply.
@ -14,9 +15,48 @@ func NewVaultRecord(vaultDenom string) VaultRecord {
}
}
// Validate returns an error if a VaultRecord is invalid.
func (vr *VaultRecord) Validate() error {
if err := sdk.ValidateDenom(vr.Denom); err != nil {
return sdkerrors.Wrap(ErrInvalidVaultDenom, err.Error())
}
if vr.TotalSupply.Denom != vr.Denom {
return fmt.Errorf(
"total supply denom %v does not match vault record denom %v",
vr.TotalSupply.Denom,
vr.Denom,
)
}
if err := vr.TotalSupply.Validate(); err != nil {
return fmt.Errorf("vault total supply is invalid: %w", err)
}
return nil
}
// VaultRecords is a slice of VaultRecord.
type VaultRecords []VaultRecord
type VaultShareRecords []VaultShareRecord
// Validate returns an error if a slice of VaultRecords is invalid.
func (vrs VaultRecords) Validate() error {
denoms := make(map[string]bool)
for _, vr := range vrs {
if err := vr.Validate(); err != nil {
return err
}
if denoms[vr.Denom] {
return fmt.Errorf("duplicate vault denom %s", vr.Denom)
}
denoms[vr.Denom] = true
}
return nil
}
// NewVaultShareRecord returns a new VaultShareRecord with the provided supplied
// coins.
@ -27,6 +67,41 @@ func NewVaultShareRecord(depositor sdk.AccAddress, supplied ...sdk.Coin) VaultSh
}
}
// Validate returns an error if an VaultShareRecord is invalid.
func (vsr VaultShareRecord) Validate() error {
if vsr.Depositor.Empty() {
return fmt.Errorf("depositor is empty")
}
if err := vsr.AmountSupplied.Validate(); err != nil {
return fmt.Errorf("invalid vault share record amount supplied: %w", err)
}
return nil
}
// VaultShareRecords is a slice of VaultShareRecord.
type VaultShareRecords []VaultShareRecord
// Validate returns an error if a slice of VaultRecords is invalid.
func (vsrs VaultShareRecords) Validate() error {
addrs := make(map[string]bool)
for _, vr := range vsrs {
if err := vr.Validate(); err != nil {
return err
}
if _, found := addrs[vr.Depositor.String()]; found {
return fmt.Errorf("duplicate address %s", vr.Depositor.String())
}
addrs[vr.Depositor.String()] = true
}
return nil
}
// NewAllowedVaults returns a new AllowedVaults with the given denom and strategy type.
func NewAllowedVault(denom string, strategyType StrategyType) AllowedVault {
return AllowedVault{
@ -55,8 +130,8 @@ func (a AllowedVaults) Validate() error {
}
func (a *AllowedVault) Validate() error {
if a.Denom == "" {
return ErrInvalidVaultDenom
if err := sdk.ValidateDenom(a.Denom); err != nil {
return sdkerrors.Wrap(ErrInvalidVaultDenom, err.Error())
}
if a.VaultStrategy == STRATEGY_TYPE_UNSPECIFIED {

310
x/earn/types/vault_test.go Normal file
View File

@ -0,0 +1,310 @@
package types_test
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/earn/types"
)
func TestVaultRecordValidate(t *testing.T) {
type errArgs struct {
expectPass bool
contains string
}
tests := []struct {
name string
vaultRecords types.VaultRecords
errArgs errArgs
}{
{
name: "valid vault records",
vaultRecords: types.VaultRecords{
{
Denom: "usdx",
TotalSupply: sdk.NewInt64Coin("usdx", 0),
},
{
Denom: "ukava",
TotalSupply: sdk.NewInt64Coin("ukava", 5),
},
},
errArgs: errArgs{
expectPass: true,
},
},
{
name: "invalid - duplicate denom",
vaultRecords: types.VaultRecords{
{
Denom: "usdx",
TotalSupply: sdk.NewInt64Coin("usdx", 0),
},
{
Denom: "usdx",
TotalSupply: sdk.NewInt64Coin("usdx", 5),
},
},
errArgs: errArgs{
expectPass: false,
contains: "duplicate vault denom usdx",
},
},
{
name: "invalid - invalid denom",
vaultRecords: types.VaultRecords{
{
Denom: "",
TotalSupply: sdk.NewInt64Coin("ukava", 0),
},
},
errArgs: errArgs{
expectPass: false,
contains: "invalid denom",
},
},
{
name: "invalid - mismatch denom",
vaultRecords: types.VaultRecords{
{
Denom: "usdx",
TotalSupply: sdk.NewInt64Coin("ukava", 0),
},
},
errArgs: errArgs{
expectPass: false,
contains: "total supply denom ukava does not match vault record denom usdx",
},
},
{
name: "invalid - negative",
vaultRecords: types.VaultRecords{
{
Denom: "usdx",
TotalSupply: sdk.Coin{Denom: "usdx", Amount: sdk.NewInt(-1)},
},
},
errArgs: errArgs{
expectPass: false,
contains: "negative coin amount",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.vaultRecords.Validate()
if test.errArgs.expectPass {
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), test.errArgs.contains)
}
})
}
}
func TestVaultShareRecordsValidate(t *testing.T) {
_, addrs := app.GeneratePrivKeyAddressPairs(2)
type errArgs struct {
expectPass bool
contains string
}
tests := []struct {
name string
vaultRecords types.VaultShareRecords
errArgs errArgs
}{
{
name: "valid vault share records",
vaultRecords: types.VaultShareRecords{
{
Depositor: addrs[0],
AmountSupplied: sdk.NewCoins(
sdk.NewInt64Coin("usdx", 0),
),
},
{
Depositor: addrs[1],
AmountSupplied: sdk.NewCoins(
sdk.NewInt64Coin("usdx", 0),
sdk.NewInt64Coin("ukava", 5),
),
},
},
errArgs: errArgs{
expectPass: true,
},
},
{
name: "invalid - duplicate address",
vaultRecords: types.VaultShareRecords{
{
Depositor: addrs[0],
AmountSupplied: sdk.NewCoins(
sdk.NewInt64Coin("usdx", 0),
),
},
{
Depositor: addrs[0],
AmountSupplied: sdk.NewCoins(
sdk.NewInt64Coin("usdx", 0),
sdk.NewInt64Coin("ukava", 5),
),
},
},
errArgs: errArgs{
expectPass: false,
contains: "duplicate address",
},
},
{
name: "invalid - invalid address",
vaultRecords: types.VaultShareRecords{
{
Depositor: sdk.AccAddress{},
AmountSupplied: sdk.NewCoins(
sdk.NewInt64Coin("usdx", 0),
),
},
},
errArgs: errArgs{
expectPass: false,
contains: "depositor is empty",
},
},
{
name: "invalid - negative",
vaultRecords: types.VaultShareRecords{
{
Depositor: addrs[0],
AmountSupplied: sdk.Coins{
sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(-1)},
},
},
},
errArgs: errArgs{
expectPass: false,
contains: "amount is not positive",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.vaultRecords.Validate()
if test.errArgs.expectPass {
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), test.errArgs.contains)
}
})
}
}
func TestAllowedVaultsValidate(t *testing.T) {
type errArgs struct {
expectPass bool
contains string
}
tests := []struct {
name string
vaultRecords types.AllowedVaults
errArgs errArgs
}{
{
name: "valid vault share records",
vaultRecords: types.AllowedVaults{
{
Denom: "usdx",
VaultStrategy: types.STRATEGY_TYPE_HARD,
},
{
Denom: "busd",
VaultStrategy: types.STRATEGY_TYPE_HARD,
},
},
errArgs: errArgs{
expectPass: true,
},
},
{
name: "invalid - duplicate denom",
vaultRecords: types.AllowedVaults{
{
Denom: "usdx",
VaultStrategy: types.STRATEGY_TYPE_HARD,
},
{
Denom: "usdx",
VaultStrategy: types.STRATEGY_TYPE_HARD,
},
},
errArgs: errArgs{
expectPass: false,
contains: "duplicate vault denom usdx",
},
},
{
name: "invalid - invalid denom",
vaultRecords: types.AllowedVaults{
{
Denom: "",
VaultStrategy: types.STRATEGY_TYPE_HARD,
},
},
errArgs: errArgs{
expectPass: false,
contains: "invalid denom",
},
},
{
name: "invalid - invalid strategy",
vaultRecords: types.AllowedVaults{
{
Denom: "usdx",
VaultStrategy: types.STRATEGY_TYPE_UNSPECIFIED,
},
},
errArgs: errArgs{
expectPass: false,
contains: "invalid vault strategy",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.vaultRecords.Validate()
if test.errArgs.expectPass {
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), test.errArgs.contains)
}
})
}
}
func TestNewVaultShareRecord(t *testing.T) {
_, addrs := app.GeneratePrivKeyAddressPairs(1)
coins := sdk.NewCoins(
sdk.NewInt64Coin("usdx", 10),
sdk.NewInt64Coin("ukava", 5),
)
shareRecord := types.NewVaultShareRecord(addrs[0], coins...)
require.Equal(t, coins, shareRecord.AmountSupplied)
}