From d130734c2edc2a143fc31582b098f6a940e3d49f Mon Sep 17 00:00:00 2001 From: Federico Kunze <31522760+fedekunze@users.noreply.github.com> Date: Thu, 28 May 2020 10:26:08 -0400 Subject: [PATCH] x/incentive: genesis validation (#519) * x/incentive: genesis validation * validation funcs for arrays * tests * genesis tests * rewards tests * address @alexanderbez comments * fix genesis incentive simulation --- x/incentive/simulation/genesis.go | 64 ++++--- x/incentive/types/genesis.go | 45 ++++- x/incentive/types/genesis_test.go | 163 ++++++++++++++++++ x/incentive/types/params.go | 72 ++++---- x/incentive/types/params_test.go | 4 +- x/incentive/types/rewards.go | 130 ++++++++++++-- x/incentive/types/rewards_test.go | 272 ++++++++++++++++++++++++++++++ 7 files changed, 668 insertions(+), 82 deletions(-) create mode 100644 x/incentive/types/genesis_test.go create mode 100644 x/incentive/types/rewards_test.go diff --git a/x/incentive/simulation/genesis.go b/x/incentive/simulation/genesis.go index cd6513d1..dd13c191 100644 --- a/x/incentive/simulation/genesis.go +++ b/x/incentive/simulation/genesis.go @@ -48,8 +48,8 @@ func genParams(r *rand.Rand) types.Params { // genRewards generates rewards for each specified collateral type func genRewards(r *rand.Rand) types.Rewards { - var rewards types.Rewards - for _, denom := range CollateralDenoms { + rewards := make(types.Rewards, len(CollateralDenoms)) + for i, denom := range CollateralDenoms { active := true // total reward is in range (half max total reward, max total reward) amount := simulation.RandIntBetween(r, int(MaxTotalAssetReward.Int64()/2), int(MaxTotalAssetReward.Int64())) @@ -59,32 +59,29 @@ func genRewards(r *rand.Rand) types.Rewards { duration := time.Duration(time.Hour * time.Duration(numbHours)) timeLock := time.Duration(time.Hour * time.Duration(numbHours/2)) // half as long as duration claimDuration := time.Hour * time.Duration(numbHours*2) // twice as long as duration - reward := types.NewReward(active, denom, totalRewards, duration, timeLock, claimDuration) - rewards = append(rewards, reward) + rewards[i] = types.NewReward(active, denom, totalRewards, duration, timeLock, claimDuration) } return rewards } // genRewardPeriods generates chronological reward periods for each given reward type func genRewardPeriods(r *rand.Rand, timestamp time.Time, rewards types.Rewards) types.RewardPeriods { - var rewardPeriods types.RewardPeriods - for _, reward := range rewards { - rewardPeriodStart := timestamp - for i := 10; i >= simulation.RandIntBetween(r, 2, 9); i-- { - // Set up reward period parameters - start := rewardPeriodStart - end := start.Add(reward.Duration).UTC() - baseRewardAmount := reward.AvailableRewards.Amount.Quo(sdk.NewInt(100)) // base period reward is 1/100 total reward - // Earlier periods have larger rewards - amount := sdk.NewCoin(reward.Denom, baseRewardAmount.Mul(sdk.NewInt(int64(i)))) - claimEnd := end.Add(reward.ClaimDuration) - claimTimeLock := reward.TimeLock - // Create reward period and append to array - rewardPeriod := types.NewRewardPeriod(reward.Denom, start, end, amount, claimEnd, claimTimeLock) - rewardPeriods = append(rewardPeriods, rewardPeriod) - // Update start time of next reward period - rewardPeriodStart = end - } + rewardPeriods := make(types.RewardPeriods, len(rewards)) + rewardPeriodStart := timestamp + + for i, reward := range rewards { + // Set up reward period parameters + start := rewardPeriodStart + end := start.Add(reward.Duration).UTC() + baseRewardAmount := reward.AvailableRewards.Amount.Quo(sdk.NewInt(100)) // base period reward is 1/100 total reward + // Earlier periods have larger rewards + amount := sdk.NewCoin(reward.Denom, baseRewardAmount.Mul(sdk.NewInt(int64(i)))) + claimEnd := end.Add(reward.ClaimDuration) + claimTimeLock := reward.TimeLock + // Create reward period and append to array + rewardPeriods[i] = types.NewRewardPeriod(reward.Denom, start, end, amount, claimEnd, claimTimeLock) + // Update start time of next reward period + rewardPeriodStart = end } return rewardPeriods } @@ -92,8 +89,8 @@ func genRewardPeriods(r *rand.Rand, timestamp time.Time, rewards types.Rewards) // genClaimPeriods loads valid claim periods for an array of reward periods func genClaimPeriods(rewardPeriods types.RewardPeriods) types.ClaimPeriods { denomRewardPeriodsCount := make(map[string]uint64) - var claimPeriods types.ClaimPeriods - for _, rewardPeriod := range rewardPeriods { + claimPeriods := make(types.ClaimPeriods, len(rewardPeriods)) + for i, rewardPeriod := range rewardPeriods { // Increment reward period count for this denom (this is our claim period's ID) denom := rewardPeriod.Denom numbRewardPeriods := denomRewardPeriodsCount[denom] + 1 @@ -102,8 +99,7 @@ func genClaimPeriods(rewardPeriods types.RewardPeriods) types.ClaimPeriods { end := rewardPeriod.ClaimEnd claimTimeLock := rewardPeriod.ClaimTimeLock // Create the new claim period for this reward period - claimPeriod := types.NewClaimPeriod(denom, numbRewardPeriods, end, claimTimeLock) - claimPeriods = append(claimPeriods, claimPeriod) + claimPeriods[i] = types.NewClaimPeriod(denom, numbRewardPeriods, end, claimTimeLock) } return claimPeriods } @@ -112,16 +108,14 @@ func genClaimPeriods(rewardPeriods types.RewardPeriods) types.ClaimPeriods { func genNextClaimPeriodIds(cps types.ClaimPeriods) types.GenesisClaimPeriodIDs { // Build a map of the most recent claim periods by denom mostRecentClaimPeriodByDenom := make(map[string]uint64) - for _, cp := range cps { - if cp.ID > mostRecentClaimPeriodByDenom[cp.Denom] { - mostRecentClaimPeriodByDenom[cp.Denom] = cp.ID - } - } - // Write map contents to an array of GenesisClaimPeriodIDs var claimPeriodIDs types.GenesisClaimPeriodIDs - for key, value := range mostRecentClaimPeriodByDenom { - claimPeriodID := types.GenesisClaimPeriodID{Denom: key, ID: value} - claimPeriodIDs = append(claimPeriodIDs, claimPeriodID) + for _, cp := range cps { + if cp.ID <= mostRecentClaimPeriodByDenom[cp.Denom] { + continue + } + claimPeriodIDs = append(claimPeriodIDs, types.GenesisClaimPeriodID{Denom: cp.Denom, ID: cp.ID}) + mostRecentClaimPeriodByDenom[cp.Denom] = cp.ID + } return claimPeriodIDs } diff --git a/x/incentive/types/genesis.go b/x/incentive/types/genesis.go index f9514727..ecd21186 100644 --- a/x/incentive/types/genesis.go +++ b/x/incentive/types/genesis.go @@ -2,8 +2,11 @@ package types import ( "bytes" + "errors" "fmt" "time" + + sdk "github.com/cosmos/cosmos-sdk/types" ) // GenesisClaimPeriodID stores the next claim id and its corresponding denom @@ -12,9 +15,37 @@ type GenesisClaimPeriodID struct { ID uint64 `json:"id" yaml:"id"` } +// Validate performs a basic check of a GenesisClaimPeriodID fields. +func (gcp GenesisClaimPeriodID) Validate() error { + if gcp.ID == 0 { + return errors.New("genesis claim period id cannot be 0") + } + return sdk.ValidateDenom(gcp.Denom) +} + // GenesisClaimPeriodIDs array of GenesisClaimPeriodID type GenesisClaimPeriodIDs []GenesisClaimPeriodID +// Validate checks if all the GenesisClaimPeriodIDs are valid and there are no duplicated +// entries. +func (gcps GenesisClaimPeriodIDs) Validate() error { + seenIDS := make(map[string]bool) + var key string + for _, gcp := range gcps { + key = gcp.Denom + string(gcp.ID) + if seenIDS[key] { + return fmt.Errorf("duplicated genesis claim period with id %d and denom %s", gcp.ID, gcp.Denom) + } + + if err := gcp.Validate(); err != nil { + return err + } + seenIDS[key] = true + } + + return nil +} + // GenesisState is the state that must be provided at genesis. type GenesisState struct { Params Params `json:"params" yaml:"params"` @@ -52,14 +83,22 @@ func DefaultGenesisState() GenesisState { // Validate performs basic validation of genesis data returning an // error for any failed validation criteria. func (gs GenesisState) Validate() error { - if err := gs.Params.Validate(); err != nil { return err } if gs.PreviousBlockTime.IsZero() { - return fmt.Errorf("previous block time not set or zero") + return errors.New("previous block time cannot be 0") } - return nil + if err := gs.RewardPeriods.Validate(); err != nil { + return err + } + if err := gs.ClaimPeriods.Validate(); err != nil { + return err + } + if err := gs.Claims.Validate(); err != nil { + return err + } + return gs.NextClaimPeriodIDs.Validate() } // Equal checks whether two gov GenesisState structs are equivalent diff --git a/x/incentive/types/genesis_test.go b/x/incentive/types/genesis_test.go new file mode 100644 index 00000000..0b147c25 --- /dev/null +++ b/x/incentive/types/genesis_test.go @@ -0,0 +1,163 @@ +package types + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + tmtypes "github.com/tendermint/tendermint/types" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestGenesisClaimPeriodIDsValidate(t *testing.T) { + testCases := []struct { + msg string + genesisClaimPeriodIDs GenesisClaimPeriodIDs + expPass bool + }{ + { + "valid", + GenesisClaimPeriodIDs{ + {Denom: "bnb", ID: 1}, + }, + true, + }, + { + "invalid denom", + GenesisClaimPeriodIDs{ + {Denom: "", ID: 1}, + }, + false, + }, + { + "invalid ID", + GenesisClaimPeriodIDs{ + {Denom: "bnb", ID: 0}, + }, + false, + }, + { + "duplicate", + GenesisClaimPeriodIDs{ + {Denom: "bnb", ID: 1}, + {Denom: "bnb", ID: 1}, + }, + false, + }, + } + + for _, tc := range testCases { + err := tc.genesisClaimPeriodIDs.Validate() + if tc.expPass { + require.NoError(t, err, tc.msg) + } else { + require.Error(t, err, tc.msg) + } + } +} + +func TestGenesisStateValidate(t *testing.T) { + now := time.Now() + owner := sdk.AccAddress(tmtypes.NewMockPV().GetPubKey().Address()) + + rewards := Rewards{ + NewReward( + true, "bnb", sdk.NewCoin("ukava", sdk.NewInt(10000000000)), + time.Hour*24*7, time.Hour*8766, time.Hour*24*14, + ), + } + rewardPeriods := RewardPeriods{NewRewardPeriod("bnb", now, now.Add(time.Hour), sdk.NewCoin("bnb", sdk.OneInt()), now, 10)} + claimPeriods := ClaimPeriods{NewClaimPeriod("bnb", 10, now, 100)} + claims := Claims{NewClaim(owner, sdk.NewCoin("bnb", sdk.OneInt()), "bnb", 10)} + gcps := GenesisClaimPeriodIDs{{Denom: "bnb", ID: 1}} + + testCases := []struct { + msg string + genesisState GenesisState + expPass bool + }{ + { + msg: "default", + genesisState: DefaultGenesisState(), + expPass: true, + }, + { + msg: "valid genesis", + genesisState: NewGenesisState( + NewParams(true, rewards), + now, rewardPeriods, claimPeriods, claims, gcps, + ), + expPass: true, + }, + { + msg: "invalid Params", + genesisState: GenesisState{ + Params: Params{ + Active: true, + Rewards: Rewards{ + Reward{}, + }, + }, + }, + expPass: false, + }, + { + msg: "zero PreviousBlockTime", + genesisState: GenesisState{ + PreviousBlockTime: time.Time{}, + }, + expPass: false, + }, + { + msg: "invalid RewardsPeriod", + genesisState: GenesisState{ + PreviousBlockTime: now, + RewardPeriods: RewardPeriods{ + {Start: time.Time{}}, + }, + }, + expPass: false, + }, + { + msg: "invalid ClaimPeriods", + genesisState: GenesisState{ + PreviousBlockTime: now, + ClaimPeriods: ClaimPeriods{ + {ID: 0}, + }, + }, + expPass: false, + }, + { + msg: "invalid Claims", + genesisState: GenesisState{ + PreviousBlockTime: now, + Claims: Claims{ + {ClaimPeriodID: 0}, + }, + }, + expPass: false, + }, + { + msg: "invalid NextClaimPeriodIds", + genesisState: GenesisState{ + PreviousBlockTime: now, + NextClaimPeriodIDs: GenesisClaimPeriodIDs{ + {ID: 0}, + }, + }, + expPass: false, + }, + } + + for _, tc := range testCases { + err := tc.genesisState.Validate() + if tc.expPass { + require.NoError(t, err, tc.msg) + } else { + require.Error(t, err, tc.msg) + } + } +} diff --git a/x/incentive/types/params.go b/x/incentive/types/params.go index 17ca6a20..ade6b091 100644 --- a/x/incentive/types/params.go +++ b/x/incentive/types/params.go @@ -2,7 +2,6 @@ package types import ( "fmt" - "strings" "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -71,11 +70,7 @@ func (p Params) Validate() error { return err } - if err := validateRewardsParam(p.Rewards); err != nil { - return err - } - - return nil + return validateRewardsParam(p.Rewards) } func validateActiveParam(i interface{}) error { @@ -91,33 +86,8 @@ func validateRewardsParam(i interface{}) error { if !ok { return fmt.Errorf("invalid parameter type: %T", i) } - rewardDenoms := make(map[string]bool) - for _, reward := range rewards { - if strings.TrimSpace(reward.Denom) == "" { - return fmt.Errorf("cannot have empty reward denom: %s", reward) - } - if rewardDenoms[reward.Denom] { - return fmt.Errorf("cannot have duplicate reward denoms: %s", reward.Denom) - } - rewardDenoms[reward.Denom] = true - if !reward.AvailableRewards.IsValid() { - return fmt.Errorf("invalid reward coins %s for %s", reward.AvailableRewards, reward.Denom) - } - if !reward.AvailableRewards.IsPositive() { - return fmt.Errorf("reward amount must be positive, is %s for %s", reward.AvailableRewards, reward.Denom) - } - if int(reward.Duration.Seconds()) <= 0 { - return fmt.Errorf("reward duration must be positive, is %s for %s", reward.Duration.String(), reward.Denom) - } - if int(reward.TimeLock.Seconds()) < 0 { - return fmt.Errorf("reward timelock must be non-negative, is %s for %s", reward.TimeLock.String(), reward.Denom) - } - if int(reward.ClaimDuration.Seconds()) <= 0 { - return fmt.Errorf("claim duration must be positive, is %s for %s", reward.ClaimDuration.String(), reward.Denom) - } - } - return nil + return rewards.Validate() } // Reward stores the specified state for a single reward period. @@ -154,9 +124,47 @@ func (r Reward) String() string { r.Active, r.Denom, r.AvailableRewards, r.Duration, r.TimeLock, r.ClaimDuration) } +// Validate performs a basic check of a reward fields. +func (r Reward) Validate() error { + if !r.AvailableRewards.IsValid() { + return fmt.Errorf("invalid reward coins %s for %s", r.AvailableRewards, r.Denom) + } + if !r.AvailableRewards.IsPositive() { + return fmt.Errorf("reward amount must be positive, is %s for %s", r.AvailableRewards, r.Denom) + } + if r.Duration <= 0 { + return fmt.Errorf("reward duration must be positive, is %s for %s", r.Duration, r.Denom) + } + if r.TimeLock < 0 { + return fmt.Errorf("reward timelock must be non-negative, is %s for %s", r.TimeLock, r.Denom) + } + if r.ClaimDuration <= 0 { + return fmt.Errorf("claim duration must be positive, is %s for %s", r.ClaimDuration, r.Denom) + } + return sdk.ValidateDenom(r.Denom) +} + // Rewards array of Reward type Rewards []Reward +// Validate checks if all the rewards are valid and there are no duplicated +// entries. +func (rs Rewards) Validate() error { + rewardDenoms := make(map[string]bool) + for _, r := range rs { + if rewardDenoms[r.Denom] { + return fmt.Errorf("cannot have duplicate reward denoms: %s", r.Denom) + } + + if err := r.Validate(); err != nil { + return err + } + + rewardDenoms[r.Denom] = true + } + return nil +} + // String implements fmt.Stringer func (rs Rewards) String() string { out := "Rewards\n" diff --git a/x/incentive/types/params_test.go b/x/incentive/types/params_test.go index 97f3fbc7..2105d1bf 100644 --- a/x/incentive/types/params_test.go +++ b/x/incentive/types/params_test.go @@ -187,7 +187,7 @@ func (suite *ParamTestSuite) SetupTest() { types.Reward{ Active: true, Denom: "", - AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(0)), + AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(1)), Duration: time.Hour * 24 * 7, TimeLock: time.Hour * 8766, ClaimDuration: time.Hour * 24 * 14, @@ -196,7 +196,7 @@ func (suite *ParamTestSuite) SetupTest() { }, errResult: errResult{ expectPass: false, - contains: "cannot have empty reward denom", + contains: "invalid denom", }, }, } diff --git a/x/incentive/types/rewards.go b/x/incentive/types/rewards.go index 76539c86..4a717224 100644 --- a/x/incentive/types/rewards.go +++ b/x/incentive/types/rewards.go @@ -1,6 +1,7 @@ package types import ( + "errors" "fmt" "time" @@ -41,9 +42,50 @@ func NewRewardPeriod(denom string, start time.Time, end time.Time, reward sdk.Co } } +// Validate performs a basic check of a RewardPeriod fields. +func (rp RewardPeriod) Validate() error { + if rp.Start.IsZero() { + return errors.New("reward period start time cannot be 0") + } + if rp.End.IsZero() { + return errors.New("reward period end time cannot be 0") + } + if rp.Start.After(rp.End) { + return fmt.Errorf("end period time %s cannot be before start time %s", rp.End, rp.Start) + } + if !rp.Reward.IsValid() { + return fmt.Errorf("invalid reward amount: %s", rp.Reward) + } + if rp.ClaimEnd.IsZero() { + return errors.New("reward period claim end time cannot be 0") + } + if rp.ClaimTimeLock == 0 { + return errors.New("reward claim time lock cannot be 0") + } + return sdk.ValidateDenom(rp.Denom) +} + // RewardPeriods array of RewardPeriod type RewardPeriods []RewardPeriod +// Validate checks if all the RewardPeriods are valid and there are no duplicated +// entries. +func (rps RewardPeriods) Validate() error { + seenPeriods := make(map[string]bool) + for _, rp := range rps { + if seenPeriods[rp.Denom] { + return fmt.Errorf("duplicated reward period with denom %s", rp.Denom) + } + + if err := rp.Validate(); err != nil { + return err + } + seenPeriods[rp.Denom] = true + } + + return nil +} + // ClaimPeriod stores the state of an ongoing claim period type ClaimPeriod struct { Denom string `json:"denom" yaml:"denom"` @@ -52,16 +94,6 @@ type ClaimPeriod struct { TimeLock time.Duration `json:"time_lock" yaml:"time_lock"` } -// String implements fmt.Stringer -func (cp ClaimPeriod) String() string { - return fmt.Sprintf(`Claim Period: - Denom: %s, - ID: %d, - End: %s, - Claim Time Lock: %s - `, cp.Denom, cp.ID, cp.End, cp.TimeLock) -} - // NewClaimPeriod returns a new ClaimPeriod func NewClaimPeriod(denom string, id uint64, end time.Time, timeLock time.Duration) ClaimPeriod { return ClaimPeriod{ @@ -72,9 +104,53 @@ func NewClaimPeriod(denom string, id uint64, end time.Time, timeLock time.Durati } } +// Validate performs a basic check of a ClaimPeriod fields. +func (cp ClaimPeriod) Validate() error { + if cp.ID == 0 { + return errors.New("claim period id cannot be 0") + } + if cp.End.IsZero() { + return errors.New("claim period end time cannot be 0") + } + if cp.TimeLock == 0 { + return errors.New("claim period time lock cannot be 0") + } + return sdk.ValidateDenom(cp.Denom) +} + +// String implements fmt.Stringer +func (cp ClaimPeriod) String() string { + return fmt.Sprintf(`Claim Period: + Denom: %s, + ID: %d, + End: %s, + Claim Time Lock: %s + `, cp.Denom, cp.ID, cp.End, cp.TimeLock) +} + // ClaimPeriods array of ClaimPeriod type ClaimPeriods []ClaimPeriod +// Validate checks if all the ClaimPeriods are valid and there are no duplicated +// entries. +func (cps ClaimPeriods) Validate() error { + seenPeriods := make(map[string]bool) + var key string + for _, cp := range cps { + key = cp.Denom + string(cp.ID) + if seenPeriods[key] { + return fmt.Errorf("duplicated claim period with id %d and denom %s", cp.ID, cp.Denom) + } + + if err := cp.Validate(); err != nil { + return err + } + seenPeriods[key] = true + } + + return nil +} + // Claim stores the rewards that can be claimed by owner type Claim struct { Owner sdk.AccAddress `json:"owner" yaml:"owner"` @@ -93,6 +169,20 @@ func NewClaim(owner sdk.AccAddress, reward sdk.Coin, denom string, claimPeriodID } } +// Validate performs a basic check of a Claim fields. +func (c Claim) Validate() error { + if c.Owner.Empty() { + return errors.New("claim owner cannot be empty") + } + if !c.Reward.IsValid() { + return fmt.Errorf("invalid reward amount: %s", c.Reward) + } + if c.ClaimPeriodID == 0 { + return errors.New("claim period id cannot be 0") + } + return sdk.ValidateDenom(c.Denom) +} + // String implements fmt.Stringer func (c Claim) String() string { return fmt.Sprintf(`Claim: @@ -106,6 +196,26 @@ func (c Claim) String() string { // Claims array of Claim type Claims []Claim +// Validate checks if all the claims are valid and there are no duplicated +// entries. +func (cs Claims) Validate() error { + seemClaims := make(map[string]bool) + var key string + for _, c := range cs { + key = c.Denom + string(c.ClaimPeriodID) + c.Owner.String() + if c.Owner != nil && seemClaims[key] { + return fmt.Errorf("duplicated claim from owner %s and denom %s", c.Owner, c.Denom) + } + + if err := c.Validate(); err != nil { + return err + } + seemClaims[key] = true + } + + return nil +} + // NewRewardPeriodFromReward returns a new reward period from the input reward and block time func NewRewardPeriodFromReward(reward Reward, blockTime time.Time) RewardPeriod { // note: reward periods store the amount of rewards paid PER SECOND diff --git a/x/incentive/types/rewards_test.go b/x/incentive/types/rewards_test.go new file mode 100644 index 00000000..4a24bf04 --- /dev/null +++ b/x/incentive/types/rewards_test.go @@ -0,0 +1,272 @@ +package types + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + tmtypes "github.com/tendermint/tendermint/types" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestRewardPeriodsValidate(t *testing.T) { + now := time.Now() + + testCases := []struct { + msg string + rewardPeriods RewardPeriods + expPass bool + }{ + { + "valid", + RewardPeriods{ + NewRewardPeriod("bnb", now, now.Add(time.Hour), sdk.NewCoin("bnb", sdk.OneInt()), now, 10), + }, + true, + }, + { + "zero start time", + RewardPeriods{ + {Start: time.Time{}}, + }, + false, + }, + { + "zero end time", + RewardPeriods{ + {Start: now, End: time.Time{}}, + }, + false, + }, + { + "zero end time", + RewardPeriods{ + {Start: now, End: time.Time{}}, + }, + false, + }, + { + "start time > end time", + RewardPeriods{ + { + Start: now.Add(time.Hour), + End: now, + }, + }, + false, + }, + { + "invalid reward", + RewardPeriods{ + { + Start: now, + End: now.Add(time.Hour), + Reward: sdk.Coin{Denom: "", Amount: sdk.ZeroInt()}, + }, + }, + false, + }, + { + "zero claim end time", + RewardPeriods{ + { + Start: now, + End: now.Add(time.Hour), + Reward: sdk.NewCoin("bnb", sdk.OneInt()), + ClaimEnd: time.Time{}, + }, + }, + false, + }, + { + "zero claim time lock", + RewardPeriods{ + { + Start: now, + End: now.Add(time.Hour), + Reward: sdk.NewCoin("bnb", sdk.OneInt()), + ClaimEnd: now, + ClaimTimeLock: 0, + }, + }, + false, + }, + { + "invalid denom", + RewardPeriods{ + { + Start: now, + End: now.Add(time.Hour), + Reward: sdk.NewCoin("bnb", sdk.OneInt()), + ClaimEnd: now, + ClaimTimeLock: 10, + Denom: "", + }, + }, + false, + }, + { + "duplicate reward period", + RewardPeriods{ + NewRewardPeriod("bnb", now, now.Add(time.Hour), sdk.NewCoin("bnb", sdk.OneInt()), now, 10), + NewRewardPeriod("bnb", now, now.Add(time.Hour), sdk.NewCoin("bnb", sdk.OneInt()), now, 10), + }, + false, + }, + } + + for _, tc := range testCases { + err := tc.rewardPeriods.Validate() + if tc.expPass { + require.NoError(t, err, tc.msg) + } else { + require.Error(t, err, tc.msg) + } + } +} + +func TestClaimPeriodsValidate(t *testing.T) { + now := time.Now() + + testCases := []struct { + msg string + claimPeriods ClaimPeriods + expPass bool + }{ + { + "valid", + ClaimPeriods{ + NewClaimPeriod("bnb", 10, now, 100), + }, + true, + }, + { + "invalid ID", + ClaimPeriods{ + {ID: 0}, + }, + false, + }, + { + "zero end time", + ClaimPeriods{ + {ID: 10, End: time.Time{}}, + }, + false, + }, + { + "zero time lock", + ClaimPeriods{ + {ID: 10, End: now, TimeLock: 0}, + }, + false, + }, + { + "start time > end time", + ClaimPeriods{ + {ID: 10, End: now, TimeLock: 0}, + }, + false, + }, + { + "invalid denom", + ClaimPeriods{ + {ID: 10, End: now, TimeLock: 100, Denom: ""}, + }, + false, + }, + { + "duplicate reward period", + ClaimPeriods{ + NewClaimPeriod("bnb", 10, now, 100), + NewClaimPeriod("bnb", 10, now, 100), + }, + false, + }, + } + + for _, tc := range testCases { + err := tc.claimPeriods.Validate() + if tc.expPass { + require.NoError(t, err, tc.msg) + } else { + require.Error(t, err, tc.msg) + } + } +} + +func TestClaimsValidate(t *testing.T) { + owner := sdk.AccAddress(tmtypes.NewMockPV().GetPubKey().Address()) + + testCases := []struct { + msg string + claims Claims + expPass bool + }{ + { + "valid", + Claims{ + NewClaim(owner, sdk.NewCoin("bnb", sdk.OneInt()), "bnb", 10), + }, + true, + }, + { + "invalid owner", + Claims{ + {Owner: nil}, + }, + false, + }, + { + "invalid reward", + Claims{ + { + Owner: owner, + Reward: sdk.Coin{Denom: "", Amount: sdk.ZeroInt()}, + }, + }, + false, + }, + { + "zero claim period id", + Claims{ + { + Owner: owner, + Reward: sdk.NewCoin("bnb", sdk.OneInt()), + ClaimPeriodID: 0, + }, + }, + false, + }, + { + "invalid denom", + Claims{ + { + Owner: owner, + Reward: sdk.NewCoin("bnb", sdk.OneInt()), + ClaimPeriodID: 10, + Denom: "", + }, + }, + false, + }, + { + "duplicate reward period", + Claims{ + NewClaim(owner, sdk.NewCoin("bnb", sdk.OneInt()), "bnb", 10), + NewClaim(owner, sdk.NewCoin("bnb", sdk.OneInt()), "bnb", 10), + }, + false, + }, + } + + for _, tc := range testCases { + err := tc.claims.Validate() + if tc.expPass { + require.NoError(t, err, tc.msg) + } else { + require.Error(t, err, tc.msg) + } + } +}