From 7292b8843a161eac37a6b552db693c41b3b1a1fb Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Mon, 21 Sep 2020 16:20:11 -0400 Subject: [PATCH] [R4R] feat: add variable length lockups for incentive rewards (#655) * fix: update params in spec to match implementation * feat: add variable length lockups for incentive rewards * fix typos * update spec * address review comments * feat: improve claim test --- x/incentive/alias.go | 107 +++++----- x/incentive/client/cli/tx.go | 4 +- x/incentive/client/rest/tx.go | 2 +- x/incentive/handler.go | 4 +- x/incentive/handler_test.go | 4 +- x/incentive/keeper/keeper_test.go | 14 +- x/incentive/keeper/payout.go | 17 +- x/incentive/keeper/payout_test.go | 275 ++++++++++++++++++++----- x/incentive/keeper/rewards.go | 6 +- x/incentive/keeper/rewards_test.go | 16 +- x/incentive/module.go | 2 +- x/incentive/simulation/decoder_test.go | 4 +- x/incentive/simulation/genesis.go | 23 ++- x/incentive/simulation/operations.go | 8 +- x/incentive/spec/01_concepts.md | 2 +- x/incentive/spec/02_state.md | 29 +-- x/incentive/spec/03_messages.md | 6 +- x/incentive/spec/05_params.md | 24 ++- x/incentive/types/errors.go | 15 +- x/incentive/types/genesis_test.go | 6 +- x/incentive/types/msg.go | 6 +- x/incentive/types/msg_test.go | 12 +- x/incentive/types/params.go | 97 ++++++++- x/incentive/types/params_test.go | 20 +- x/incentive/types/querier.go | 1 + x/incentive/types/rewards.go | 82 ++++---- x/incentive/types/rewards_test.go | 51 +++-- 27 files changed, 590 insertions(+), 247 deletions(-) diff --git a/x/incentive/alias.go b/x/incentive/alias.go index affdca9a..b982477e 100644 --- a/x/incentive/alias.go +++ b/x/incentive/alias.go @@ -1,10 +1,6 @@ package incentive -// nolint -// autogenerated code using github.com/rigelrozanski/multitool -// aliases generated for the following subdirectories: -// ALIASGEN: github.com/kava-labs/kava/x/incentive/keeper -// ALIASGEN: github.com/kava-labs/kava/x/incentive/types +// DO NOT EDIT - generated by aliasgen tool (github.com/rhuairahrighairidh/aliasgen) import ( "github.com/kava-labs/kava/x/incentive/keeper" @@ -12,89 +8,104 @@ import ( ) const ( + AttributeKeyClaimAmount = types.AttributeKeyClaimAmount + AttributeKeyClaimPeriod = types.AttributeKeyClaimPeriod + AttributeKeyClaimedBy = types.AttributeKeyClaimedBy + AttributeKeyRewardPeriod = types.AttributeKeyRewardPeriod + AttributeValueCategory = types.AttributeValueCategory + DefaultParamspace = types.DefaultParamspace EventTypeClaim = types.EventTypeClaim - EventTypeRewardPeriod = types.EventTypeRewardPeriod EventTypeClaimPeriod = types.EventTypeClaimPeriod EventTypeClaimPeriodExpiry = types.EventTypeClaimPeriodExpiry - AttributeValueCategory = types.AttributeValueCategory - AttributeKeyClaimedBy = types.AttributeKeyClaimedBy - AttributeKeyClaimAmount = types.AttributeKeyClaimAmount - AttributeKeyRewardPeriod = types.AttributeKeyRewardPeriod - AttributeKeyClaimPeriod = types.AttributeKeyClaimPeriod + EventTypeRewardPeriod = types.EventTypeRewardPeriod + Large = types.Large + Medium = types.Medium ModuleName = types.ModuleName - StoreKey = types.StoreKey - RouterKey = types.RouterKey - DefaultParamspace = types.DefaultParamspace QuerierRoute = types.QuerierRoute + QueryGetClaimPeriods = types.QueryGetClaimPeriods QueryGetClaims = types.QueryGetClaims - RestClaimOwner = types.RestClaimOwner - RestClaimCollateralType = types.RestClaimCollateralType QueryGetParams = types.QueryGetParams + QueryGetRewardPeriods = types.QueryGetRewardPeriods + RestClaimCollateralType = types.RestClaimCollateralType + RestClaimOwner = types.RestClaimOwner + RouterKey = types.RouterKey + Small = types.Small + StoreKey = types.StoreKey ) var ( - // functions aliases + // function aliases NewKeeper = keeper.NewKeeper NewQuerier = keeper.NewQuerier - GetTotalVestingPeriodLength = types.GetTotalVestingPeriodLength - RegisterCodec = types.RegisterCodec - NewGenesisState = types.NewGenesisState - DefaultGenesisState = types.DefaultGenesisState BytesToUint64 = types.BytesToUint64 + DefaultGenesisState = types.DefaultGenesisState + DefaultParams = types.DefaultParams GetClaimPeriodPrefix = types.GetClaimPeriodPrefix GetClaimPrefix = types.GetClaimPrefix + GetTotalVestingPeriodLength = types.GetTotalVestingPeriodLength + NewAugmentedClaim = types.NewAugmentedClaim + NewClaim = types.NewClaim + NewClaimPeriod = types.NewClaimPeriod + NewGenesisState = types.NewGenesisState NewMsgClaimReward = types.NewMsgClaimReward + NewMultiplier = types.NewMultiplier NewParams = types.NewParams - DefaultParams = types.DefaultParams - ParamKeyTable = types.ParamKeyTable - NewReward = types.NewReward NewPeriod = types.NewPeriod NewQueryClaimsParams = types.NewQueryClaimsParams + NewReward = types.NewReward NewRewardPeriod = types.NewRewardPeriod - NewClaimPeriod = types.NewClaimPeriod - NewClaim = types.NewClaim NewRewardPeriodFromReward = types.NewRewardPeriodFromReward + ParamKeyTable = types.ParamKeyTable + RegisterCodec = types.RegisterCodec // variable aliases - ModuleCdc = types.ModuleCdc + ClaimKeyPrefix = types.ClaimKeyPrefix + ClaimPeriodKeyPrefix = types.ClaimPeriodKeyPrefix + DefaultActive = types.DefaultActive + DefaultPreviousBlockTime = types.DefaultPreviousBlockTime + DefaultRewards = types.DefaultRewards + ErrAccountNotFound = types.ErrAccountNotFound ErrClaimNotFound = types.ErrClaimNotFound ErrClaimPeriodNotFound = types.ErrClaimPeriodNotFound - ErrInvalidAccountType = types.ErrInvalidAccountType - ErrNoClaimsFound = types.ErrNoClaimsFound ErrInsufficientModAccountBalance = types.ErrInsufficientModAccountBalance - RewardPeriodKeyPrefix = types.RewardPeriodKeyPrefix - ClaimPeriodKeyPrefix = types.ClaimPeriodKeyPrefix - ClaimKeyPrefix = types.ClaimKeyPrefix - NextClaimPeriodIDPrefix = types.NextClaimPeriodIDPrefix - PreviousBlockTimeKey = types.PreviousBlockTimeKey + ErrInvalidAccountType = types.ErrInvalidAccountType + ErrInvalidMultiplier = types.ErrInvalidMultiplier + ErrNoClaimsFound = types.ErrNoClaimsFound + ErrZeroClaim = types.ErrZeroClaim + GovDenom = types.GovDenom + IncentiveMacc = types.IncentiveMacc KeyActive = types.KeyActive KeyRewards = types.KeyRewards - DefaultActive = types.DefaultActive - DefaultRewards = types.DefaultRewards - DefaultPreviousBlockTime = types.DefaultPreviousBlockTime - GovDenom = types.GovDenom + ModuleCdc = types.ModuleCdc + NextClaimPeriodIDPrefix = types.NextClaimPeriodIDPrefix + PreviousBlockTimeKey = types.PreviousBlockTimeKey PrincipalDenom = types.PrincipalDenom - IncentiveMacc = types.IncentiveMacc + RewardPeriodKeyPrefix = types.RewardPeriodKeyPrefix ) type ( Keeper = keeper.Keeper - SupplyKeeper = types.SupplyKeeper - CdpKeeper = types.CdpKeeper AccountKeeper = types.AccountKeeper + AugmentedClaim = types.AugmentedClaim + AugmentedClaims = types.AugmentedClaims + CdpKeeper = types.CdpKeeper + Claim = types.Claim + ClaimPeriod = types.ClaimPeriod + ClaimPeriods = types.ClaimPeriods + Claims = types.Claims GenesisClaimPeriodID = types.GenesisClaimPeriodID GenesisClaimPeriodIDs = types.GenesisClaimPeriodIDs GenesisState = types.GenesisState MsgClaimReward = types.MsgClaimReward + Multiplier = types.Multiplier + MultiplierName = types.MultiplierName + Multipliers = types.Multipliers Params = types.Params - Reward = types.Reward - Rewards = types.Rewards - QueryClaimsParams = types.QueryClaimsParams PostClaimReq = types.PostClaimReq + QueryClaimsParams = types.QueryClaimsParams + Reward = types.Reward RewardPeriod = types.RewardPeriod RewardPeriods = types.RewardPeriods - ClaimPeriod = types.ClaimPeriod - ClaimPeriods = types.ClaimPeriods - Claim = types.Claim - Claims = types.Claims + Rewards = types.Rewards + SupplyKeeper = types.SupplyKeeper ) diff --git a/x/incentive/client/cli/tx.go b/x/incentive/client/cli/tx.go index 5e77d2f0..4ab6f7fd 100644 --- a/x/incentive/client/cli/tx.go +++ b/x/incentive/client/cli/tx.go @@ -44,7 +44,7 @@ func getCmdClaim(cdc *codec.Codec) *cobra.Command { $ %s tx %s claim kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw bnb `, version.ClientName, types.ModuleName), ), - Args: cobra.ExactArgs(2), + Args: cobra.ExactArgs(3), RunE: func(cmd *cobra.Command, args []string) error { inBuf := bufio.NewReader(cmd.InOrStdin()) cliCtx := context.NewCLIContext().WithCodec(cdc) @@ -54,7 +54,7 @@ func getCmdClaim(cdc *codec.Codec) *cobra.Command { return err } - msg := types.NewMsgClaimReward(owner, args[1]) + msg := types.NewMsgClaimReward(owner, args[1], args[2]) err = msg.ValidateBasic() if err != nil { return err diff --git a/x/incentive/client/rest/tx.go b/x/incentive/client/rest/tx.go index efdb0dec..2e19070c 100644 --- a/x/incentive/client/rest/tx.go +++ b/x/incentive/client/rest/tx.go @@ -42,7 +42,7 @@ func postClaimHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return } - msg := types.NewMsgClaimReward(requestBody.Sender, requestBody.CollateralType) + msg := types.NewMsgClaimReward(requestBody.Sender, requestBody.CollateralType, requestBody.MultiplierName) if err := msg.ValidateBasic(); err != nil { rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return diff --git a/x/incentive/handler.go b/x/incentive/handler.go index d01dda3a..7d6f5fc8 100644 --- a/x/incentive/handler.go +++ b/x/incentive/handler.go @@ -1,6 +1,8 @@ package incentive import ( + "strings" + sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -30,7 +32,7 @@ func handleMsgClaimReward(ctx sdk.Context, k keeper.Keeper, msg types.MsgClaimRe } for _, claim := range claims { - err := k.PayoutClaim(ctx, claim.Owner, claim.CollateralType, claim.ClaimPeriodID) + err := k.PayoutClaim(ctx, claim.Owner, claim.CollateralType, claim.ClaimPeriodID, types.MultiplierName(strings.ToLower(msg.MultiplierName))) if err != nil { return nil, err } diff --git a/x/incentive/handler_test.go b/x/incentive/handler_test.go index 2e9e809a..4aa7b376 100644 --- a/x/incentive/handler_test.go +++ b/x/incentive/handler_test.go @@ -55,7 +55,7 @@ func (suite *HandlerTestSuite) addClaim() { macc := supplyKeeper.GetModuleAccount(suite.ctx, kavadist.ModuleName) err := supplyKeeper.MintCoins(suite.ctx, macc.GetName(), cs(c("ukava", 1000000))) suite.Require().NoError(err) - cp := incentive.NewClaimPeriod("bnb", 1, suite.ctx.BlockTime().Add(time.Hour*168), time.Hour*8766) + cp := incentive.NewClaimPeriod("bnb", 1, suite.ctx.BlockTime().Add(time.Hour*168), incentive.Multipliers{incentive.NewMultiplier(incentive.Small, 1, sdk.MustNewDecFromStr("0.33"))}) suite.NotPanics(func() { suite.keeper.SetClaimPeriod(suite.ctx, cp) }) @@ -67,7 +67,7 @@ func (suite *HandlerTestSuite) addClaim() { func (suite *HandlerTestSuite) TestMsgClaimReward() { suite.addClaim() - msg := incentive.NewMsgClaimReward(suite.addrs[0], "bnb") + msg := incentive.NewMsgClaimReward(suite.addrs[0], "bnb", "small") res, err := suite.handler(suite.ctx, msg) suite.NoError(err) suite.Require().NotNil(res) diff --git a/x/incentive/keeper/keeper_test.go b/x/incentive/keeper/keeper_test.go index 5948d87e..1b1c6b29 100644 --- a/x/incentive/keeper/keeper_test.go +++ b/x/incentive/keeper/keeper_test.go @@ -52,7 +52,7 @@ func (suite *KeeperTestSuite) getModuleAccount(name string) supplyexported.Modul } func (suite *KeeperTestSuite) TestGetSetDeleteRewardPeriod() { - rp := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), time.Hour*8766) + rp := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33"))}) _, found := suite.keeper.GetRewardPeriod(suite.ctx, "bnb") suite.False(found) suite.NotPanics(func() { @@ -69,7 +69,7 @@ func (suite *KeeperTestSuite) TestGetSetDeleteRewardPeriod() { } func (suite *KeeperTestSuite) TestGetSetDeleteClaimPeriod() { - cp := types.NewClaimPeriod("bnb", 1, suite.ctx.BlockTime().Add(time.Hour*168), time.Hour*8766) + cp := types.NewClaimPeriod("bnb", 1, suite.ctx.BlockTime().Add(time.Hour*168), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33"))}) _, found := suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb") suite.False(found) suite.NotPanics(func() { @@ -149,13 +149,13 @@ func (suite *KeeperTestSuite) TestIterateMethods() { } func (suite *KeeperTestSuite) addObjectsToStore() { - rp1 := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), time.Hour*8766) - rp2 := types.NewRewardPeriod("xrp", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), time.Hour*8766) + rp1 := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33"))}) + rp2 := types.NewRewardPeriod("xrp", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33"))}) suite.keeper.SetRewardPeriod(suite.ctx, rp1) suite.keeper.SetRewardPeriod(suite.ctx, rp2) - cp1 := types.NewClaimPeriod("bnb", 1, suite.ctx.BlockTime().Add(time.Hour*168), time.Hour*8766) - cp2 := types.NewClaimPeriod("xrp", 1, suite.ctx.BlockTime().Add(time.Hour*168), time.Hour*8766) + cp1 := types.NewClaimPeriod("bnb", 1, suite.ctx.BlockTime().Add(time.Hour*168), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33"))}) + cp2 := types.NewClaimPeriod("xrp", 1, suite.ctx.BlockTime().Add(time.Hour*168), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33"))}) suite.keeper.SetClaimPeriod(suite.ctx, cp1) suite.keeper.SetClaimPeriod(suite.ctx, cp2) @@ -168,7 +168,7 @@ func (suite *KeeperTestSuite) addObjectsToStore() { suite.keeper.SetClaim(suite.ctx, c2) params := types.NewParams( - true, types.Rewards{types.NewReward(true, "bnb", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24)}, + true, types.Rewards{types.NewReward(true, "bnb", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33"))}, time.Hour*7*24)}, ) suite.keeper.SetParams(suite.ctx, params) diff --git a/x/incentive/keeper/payout.go b/x/incentive/keeper/payout.go index fa9a6f45..6da53557 100644 --- a/x/incentive/keeper/payout.go +++ b/x/incentive/keeper/payout.go @@ -15,7 +15,7 @@ import ( ) // PayoutClaim sends the timelocked claim coins to the input address -func (k Keeper) PayoutClaim(ctx sdk.Context, addr sdk.AccAddress, collateralType string, id uint64) error { +func (k Keeper) PayoutClaim(ctx sdk.Context, addr sdk.AccAddress, collateralType string, id uint64, multiplierName types.MultiplierName) error { claim, found := k.GetClaim(ctx, addr, collateralType, id) if !found { return sdkerrors.Wrapf(types.ErrClaimNotFound, "id: %d, collateral type %s, address: %s", id, collateralType, addr) @@ -24,7 +24,20 @@ func (k Keeper) PayoutClaim(ctx sdk.Context, addr sdk.AccAddress, collateralType if !found { return sdkerrors.Wrapf(types.ErrClaimPeriodNotFound, "id: %d, collateral type: %s", id, collateralType) } - err := k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, addr, sdk.NewCoins(claim.Reward), int64(claimPeriod.TimeLock.Seconds())) + + multiplier, found := claimPeriod.GetMultiplier(multiplierName) + if !found { + return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName)) + } + + rewardAmount := sdk.NewDecFromInt(claim.Reward.Amount).Mul(multiplier.Factor).RoundInt() + if rewardAmount.IsZero() { + return types.ErrZeroClaim + } + rewardCoin := sdk.NewCoin(claim.Reward.Denom, rewardAmount) + length := ctx.BlockTime().AddDate(0, int(multiplier.MonthsLockup), 0).Unix() - ctx.BlockTime().Unix() + + err := k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, addr, sdk.NewCoins(rewardCoin), length) if err != nil { return err } diff --git a/x/incentive/keeper/payout_test.go b/x/incentive/keeper/payout_test.go index 14e039df..10d922f6 100644 --- a/x/incentive/keeper/payout_test.go +++ b/x/incentive/keeper/payout_test.go @@ -16,6 +16,7 @@ import ( "github.com/kava-labs/kava/x/incentive/types" "github.com/kava-labs/kava/x/kavadist" validatorvesting "github.com/kava-labs/kava/x/validator-vesting" + "github.com/tendermint/tendermint/crypto" ) func (suite *KeeperTestSuite) setupChain() { @@ -85,8 +86,8 @@ func (suite *KeeperTestSuite) setupExpiredClaims() { ) // creates two claim periods, one expired, and one that expires in the future - cp1 := types.NewClaimPeriod("bnb", 1, time.Unix(90, 0), time.Hour*8766) - cp2 := types.NewClaimPeriod("xrp", 1, time.Unix(110, 0), time.Hour*8766) + cp1 := types.NewClaimPeriod("bnb", 1, time.Unix(90, 0), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}) + cp2 := types.NewClaimPeriod("xrp", 1, time.Unix(110, 0), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}) suite.keeper = tApp.GetIncentiveKeeper() suite.keeper.SetClaimPeriod(ctx, cp1) suite.keeper.SetClaimPeriod(ctx, cp2) @@ -417,50 +418,6 @@ func (suite *KeeperTestSuite) TestSendCoinsToInvalidAccount() { suite.Require().True(errors.Is(err, types.ErrInvalidAccountType)) } -func (suite *KeeperTestSuite) TestPayoutClaim() { - suite.setupChain() // adds 3 accounts - 1 periodic vesting account, 1 base account, and 1 validator vesting account - - // add 2 claims that correspond to an existing claim period and one claim that has no corresponding claim period - cp1 := types.NewClaimPeriod("bnb", 1, suite.ctx.BlockTime().Add(time.Hour*168), time.Hour*8766) - suite.keeper.SetClaimPeriod(suite.ctx, cp1) - // valid claim for addrs[0] - c1 := types.NewClaim(suite.addrs[0], c("ukava", 100), "bnb", 1) - // invalid claim for addrs[0] - c2 := types.NewClaim(suite.addrs[0], c("ukava", 100), "xrp", 1) - // valid claim for addrs[1] - c3 := types.NewClaim(suite.addrs[1], c("ukava", 100), "bnb", 1) - suite.keeper.SetClaim(suite.ctx, c1) - suite.keeper.SetClaim(suite.ctx, c2) - suite.keeper.SetClaim(suite.ctx, c3) - - // existing claim with corresponding claim period successfully claimed by existing periodic vesting account - err := suite.keeper.PayoutClaim(suite.ctx.WithBlockTime(time.Unix(3700, 0)), suite.addrs[0], "bnb", 1) - suite.Require().NoError(err) - acc := suite.getAccount(suite.addrs[0]) - // account is a periodic vesting account - vacc, ok := acc.(*vesting.PeriodicVestingAccount) - suite.True(ok) - // vesting balance is correct - suite.Equal(cs(c("ukava", 500)), vacc.OriginalVesting) - - // existing claim with corresponding claim period successfully claimed by base account - err = suite.keeper.PayoutClaim(suite.ctx, suite.addrs[1], "bnb", 1) - suite.Require().NoError(err) - acc = suite.getAccount(suite.addrs[1]) - // account has become a periodic vesting account - vacc, ok = acc.(*vesting.PeriodicVestingAccount) - suite.True(ok) - // vesting balance is correct - suite.Equal(cs(c("ukava", 100)), vacc.OriginalVesting) - - // addrs[3] has no claims - err = suite.keeper.PayoutClaim(suite.ctx, suite.addrs[3], "bnb", 1) - suite.Require().True(errors.Is(err, types.ErrClaimNotFound)) - // addrs[0] has an xrp claim, but there is not corresponding claim period - err = suite.keeper.PayoutClaim(suite.ctx, suite.addrs[0], "xrp", 1) - suite.Require().True(errors.Is(err, types.ErrClaimPeriodNotFound)) -} - func (suite *KeeperTestSuite) TestDeleteExpiredClaimPeriods() { suite.setupExpiredClaims() // creates new app state with one non-expired claim period (xrp) and one expired claim period (bnb) as well as a claim that corresponds to each claim period @@ -491,3 +448,229 @@ func (suite *KeeperTestSuite) TestDeleteExpiredClaimPeriods() { suite.True(found) } + +func (suite *KeeperTestSuite) TestPayoutClaim() { + type args struct { + claimOwner sdk.AccAddress + collateralType string + id uint64 + multiplier types.MultiplierName + blockTime time.Time + rewards types.Rewards + rewardperiods types.RewardPeriods + claimPeriods types.ClaimPeriods + claims types.Claims + genIDs types.GenesisClaimPeriodIDs + active bool + validatorVesting bool + expectedAccountBalance sdk.Coins + expectedModAccountBalance sdk.Coins + expectedVestingAccount bool + expectedVestingLength int64 + } + type errArgs struct { + expectPass bool + contains string + } + type claimTest struct { + name string + args args + errArgs errArgs + } + testCases := []claimTest{ + { + "valid small claim", + args{ + claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))), + collateralType: "bnb-a", + id: 1, + blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC), + rewards: types.Rewards{types.NewReward(true, "bnb-a", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, time.Hour*7*24)}, + rewardperiods: types.RewardPeriods{types.NewRewardPeriod("bnb-a", time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24), c("ukava", 1000), time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24*2), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})}, + claimPeriods: types.ClaimPeriods{types.NewClaimPeriod("bnb-a", 1, time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})}, + claims: types.Claims{types.NewClaim(sdk.AccAddress(crypto.AddressHash([]byte("test"))), sdk.NewCoin("ukava", sdk.NewInt(1000)), "bnb-a", 1)}, + genIDs: types.GenesisClaimPeriodIDs{types.GenesisClaimPeriodID{CollateralType: "bnb-a", ID: 2}}, + active: true, + validatorVesting: false, + expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(500)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))), + expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(500))), + expectedVestingAccount: true, + expectedVestingLength: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).AddDate(0, 1, 0).Unix() - time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Unix(), + multiplier: types.Small, + }, + errArgs{ + expectPass: true, + contains: "", + }, + }, + { + "valid large claim", + args{ + claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))), + collateralType: "bnb-a", + id: 1, + blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC), + rewards: types.Rewards{types.NewReward(true, "bnb-a", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, time.Hour*7*24)}, + rewardperiods: types.RewardPeriods{types.NewRewardPeriod("bnb-a", time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24), c("ukava", 1000), time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24*2), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})}, + claimPeriods: types.ClaimPeriods{types.NewClaimPeriod("bnb-a", 1, time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})}, + claims: types.Claims{types.NewClaim(sdk.AccAddress(crypto.AddressHash([]byte("test"))), sdk.NewCoin("ukava", sdk.NewInt(1000)), "bnb-a", 1)}, + genIDs: types.GenesisClaimPeriodIDs{types.GenesisClaimPeriodID{CollateralType: "bnb-a", ID: 2}}, + active: true, + validatorVesting: false, + expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))), + expectedModAccountBalance: sdk.Coins(nil), + expectedVestingAccount: true, + expectedVestingLength: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).AddDate(0, 12, 0).Unix() - time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Unix(), + multiplier: types.Large, + }, + errArgs{ + expectPass: true, + contains: "", + }, + }, + { + "valid liquid claim", + args{ + claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))), + collateralType: "bnb-a", + id: 1, + blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC), + rewards: types.Rewards{types.NewReward(true, "bnb-a", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, time.Hour*7*24)}, + rewardperiods: types.RewardPeriods{types.NewRewardPeriod("bnb-a", time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24), c("ukava", 1000), time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24*2), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})}, + claimPeriods: types.ClaimPeriods{types.NewClaimPeriod("bnb-a", 1, time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})}, + claims: types.Claims{types.NewClaim(sdk.AccAddress(crypto.AddressHash([]byte("test"))), sdk.NewCoin("ukava", sdk.NewInt(1000)), "bnb-a", 1)}, + genIDs: types.GenesisClaimPeriodIDs{types.GenesisClaimPeriodID{CollateralType: "bnb-a", ID: 2}}, + active: true, + validatorVesting: false, + expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(500)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))), + expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(500))), + expectedVestingAccount: false, + expectedVestingLength: 0, + multiplier: types.Small, + }, + errArgs{ + expectPass: true, + contains: "", + }, + }, + { + "no matching claim", + args{ + claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))), + collateralType: "btcb-a", + id: 1, + blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC), + rewards: types.Rewards{types.NewReward(true, "bnb-a", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, time.Hour*7*24)}, + rewardperiods: types.RewardPeriods{types.NewRewardPeriod("bnb-a", time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24), c("ukava", 1000), time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24*2), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})}, + claimPeriods: types.ClaimPeriods{types.NewClaimPeriod("bnb-a", 1, time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})}, + claims: types.Claims{types.NewClaim(sdk.AccAddress(crypto.AddressHash([]byte("test"))), sdk.NewCoin("ukava", sdk.NewInt(1000)), "bnb-a", 1)}, + genIDs: types.GenesisClaimPeriodIDs{types.GenesisClaimPeriodID{CollateralType: "bnb-a", ID: 2}}, + active: true, + validatorVesting: false, + expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(500)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))), + expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(500))), + expectedVestingAccount: false, + expectedVestingLength: 0, + multiplier: types.Small, + }, + errArgs{ + expectPass: false, + contains: "no claim with input id found for owner and collateral type", + }, + }, + { + "validator vesting claim", + args{ + claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))), + collateralType: "bnb-a", + id: 1, + blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC), + rewards: types.Rewards{types.NewReward(true, "bnb-a", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, time.Hour*7*24)}, + rewardperiods: types.RewardPeriods{types.NewRewardPeriod("bnb-a", time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24), c("ukava", 1000), time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24*2), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})}, + claimPeriods: types.ClaimPeriods{types.NewClaimPeriod("bnb-a", 1, time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})}, + claims: types.Claims{types.NewClaim(sdk.AccAddress(crypto.AddressHash([]byte("test"))), sdk.NewCoin("ukava", sdk.NewInt(1000)), "bnb-a", 1)}, + genIDs: types.GenesisClaimPeriodIDs{types.GenesisClaimPeriodID{CollateralType: "bnb-a", ID: 2}}, + active: true, + validatorVesting: true, + expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(500)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))), + expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(500))), + expectedVestingAccount: false, + expectedVestingLength: 0, + multiplier: types.Small, + }, + errArgs{ + expectPass: false, + contains: "account type not supported", + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + // create new app with one funded account + config := sdk.GetConfig() + app.SetBech32AddressPrefixes(config) + // Initialize test app and set context + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tc.args.blockTime}) + authGS := app.NewAuthGenState( + []sdk.AccAddress{tc.args.claimOwner}, + []sdk.Coins{ + sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))), + }) + incentiveGS := types.NewGenesisState(types.NewParams(tc.args.active, tc.args.rewards), types.DefaultPreviousBlockTime, tc.args.rewardperiods, tc.args.claimPeriods, tc.args.claims, tc.args.genIDs) + tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(incentiveGS)}) + if tc.args.validatorVesting { + ak := tApp.GetAccountKeeper() + acc := ak.GetAccount(ctx, tc.args.claimOwner) + bacc := auth.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence()) + bva, err := vesting.NewBaseVestingAccount( + bacc, + sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(20))), time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC).Unix()+100) + suite.Require().NoError(err) + vva := validatorvesting.NewValidatorVestingAccountRaw( + bva, + time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC).Unix(), + vesting.Periods{ + vesting.Period{Length: 25, Amount: cs(c("bnb", 5))}, + vesting.Period{Length: 25, Amount: cs(c("bnb", 5))}, + vesting.Period{Length: 25, Amount: cs(c("bnb", 5))}, + vesting.Period{Length: 25, Amount: cs(c("bnb", 5))}}, + sdk.ConsAddress(crypto.AddressHash([]byte("test"))), + sdk.AccAddress{}, + 95, + ) + err = vva.Validate() + suite.Require().NoError(err) + ak.SetAccount(ctx, vva) + } + supplyKeeper := tApp.GetSupplyKeeper() + supplyKeeper.MintCoins(ctx, types.IncentiveMacc, sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000)))) + keeper := tApp.GetIncentiveKeeper() + suite.app = tApp + suite.ctx = ctx + suite.keeper = keeper + + err := suite.keeper.PayoutClaim(suite.ctx, tc.args.claimOwner, tc.args.collateralType, tc.args.id, tc.args.multiplier) + + if tc.errArgs.expectPass { + suite.Require().NoError(err) + acc := suite.getAccount(tc.args.claimOwner) + suite.Require().Equal(tc.args.expectedAccountBalance, acc.GetCoins()) + mAcc := suite.getModuleAccount(types.IncentiveMacc) + suite.Require().Equal(tc.args.expectedModAccountBalance, mAcc.GetCoins()) + vacc, ok := acc.(*vesting.PeriodicVestingAccount) + if tc.args.expectedVestingAccount { + suite.Require().True(ok) + suite.Require().Equal(tc.args.expectedVestingLength, vacc.VestingPeriods[0].Length) + } else { + suite.Require().False(ok) + } + _, f := suite.keeper.GetClaim(ctx, tc.args.claimOwner, tc.args.collateralType, tc.args.id) + suite.Require().False(f) + } else { + suite.Require().Error(err) + suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains)) + } + }) + } +} diff --git a/x/incentive/keeper/rewards.go b/x/incentive/keeper/rewards.go index 57040683..987b77e1 100644 --- a/x/incentive/keeper/rewards.go +++ b/x/incentive/keeper/rewards.go @@ -12,7 +12,7 @@ import ( // HandleRewardPeriodExpiry deletes expired RewardPeriods from the store and creates a ClaimPeriod in the store for each expired RewardPeriod func (k Keeper) HandleRewardPeriodExpiry(ctx sdk.Context, rp types.RewardPeriod) { - k.CreateUniqueClaimPeriod(ctx, rp.CollateralType, rp.ClaimEnd, rp.ClaimTimeLock) + k.CreateUniqueClaimPeriod(ctx, rp.CollateralType, rp.ClaimEnd, rp.ClaimMultipliers) store := prefix.NewStore(ctx.KVStore(k.key), types.RewardPeriodKeyPrefix) store.Delete([]byte(rp.CollateralType)) return @@ -93,9 +93,9 @@ func (k Keeper) ApplyRewardsToCdps(ctx sdk.Context) { } // CreateUniqueClaimPeriod creates a new claim period in the store and updates the highest claim period id -func (k Keeper) CreateUniqueClaimPeriod(ctx sdk.Context, collateralType string, end time.Time, timeLock time.Duration) { +func (k Keeper) CreateUniqueClaimPeriod(ctx sdk.Context, collateralType string, end time.Time, multipliers types.Multipliers) { id := k.GetNextClaimPeriodID(ctx, collateralType) - claimPeriod := types.NewClaimPeriod(collateralType, id, end, timeLock) + claimPeriod := types.NewClaimPeriod(collateralType, id, end, multipliers) ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeClaimPeriod, diff --git a/x/incentive/keeper/rewards_test.go b/x/incentive/keeper/rewards_test.go index c4edeb8c..49bab5f6 100644 --- a/x/incentive/keeper/rewards_test.go +++ b/x/incentive/keeper/rewards_test.go @@ -15,7 +15,7 @@ import ( ) func (suite *KeeperTestSuite) TestExpireRewardPeriod() { - rp := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), time.Hour*8766) + rp := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}) suite.keeper.SetRewardPeriod(suite.ctx, rp) suite.keeper.SetNextClaimPeriodID(suite.ctx, "bnb", 1) suite.NotPanics(func() { @@ -26,7 +26,7 @@ func (suite *KeeperTestSuite) TestExpireRewardPeriod() { } func (suite *KeeperTestSuite) TestAddToClaim() { - rp := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), time.Hour*8766) + rp := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}) suite.keeper.SetRewardPeriod(suite.ctx, rp) suite.keeper.SetNextClaimPeriodID(suite.ctx, "bnb", 1) suite.keeper.HandleRewardPeriodExpiry(suite.ctx, rp) @@ -44,7 +44,7 @@ func (suite *KeeperTestSuite) TestAddToClaim() { } func (suite *KeeperTestSuite) TestCreateRewardPeriod() { - reward := types.NewReward(true, "bnb", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24) + reward := types.NewReward(true, "bnb", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, time.Hour*7*24) suite.NotPanics(func() { suite.keeper.CreateNewRewardPeriod(suite.ctx, reward) }) @@ -53,9 +53,9 @@ func (suite *KeeperTestSuite) TestCreateRewardPeriod() { } func (suite *KeeperTestSuite) TestCreateAndDeleteRewardsPeriods() { - reward1 := types.NewReward(true, "bnb", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24) - reward2 := types.NewReward(false, "xrp", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24) - reward3 := types.NewReward(false, "btc", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24) + reward1 := types.NewReward(true, "bnb", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, time.Hour*7*24) + reward2 := types.NewReward(false, "xrp", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, time.Hour*7*24) + reward3 := types.NewReward(false, "btc", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, time.Hour*7*24) // add a reward period to the store for a non-active reward suite.NotPanics(func() { suite.keeper.CreateNewRewardPeriod(suite.ctx, reward3) @@ -214,10 +214,10 @@ func (suite *KeeperTestSuite) setupCdpChain() { } incentiveGS := types.NewGenesisState( types.NewParams( - true, types.Rewards{types.NewReward(true, "bnb-a", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24)}, + true, types.Rewards{types.NewReward(true, "bnb-a", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, time.Hour*7*24)}, ), types.DefaultPreviousBlockTime, - types.RewardPeriods{types.NewRewardPeriod("bnb-a", ctx.BlockTime(), ctx.BlockTime().Add(time.Hour*7*24), c("ukava", 1000), ctx.BlockTime().Add(time.Hour*7*24*2), time.Hour*365*24)}, + types.RewardPeriods{types.NewRewardPeriod("bnb-a", ctx.BlockTime(), ctx.BlockTime().Add(time.Hour*7*24), c("ukava", 1000), ctx.BlockTime().Add(time.Hour*7*24*2), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})}, types.ClaimPeriods{}, types.Claims{}, types.GenesisClaimPeriodIDs{}) diff --git a/x/incentive/module.go b/x/incentive/module.go index 921cf0ee..79548a24 100644 --- a/x/incentive/module.go +++ b/x/incentive/module.go @@ -93,7 +93,7 @@ func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedP return nil } -// WeightedOperations returns the all the bep3 module operations with their respective weights. +// WeightedOperations returns the all the incentive module operations with their respective weights. func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation { return simulation.WeightedOperations(simState.AppParams, simState.Cdc, am.accountKeeper, am.supplyKeeper, am.keeper) } diff --git a/x/incentive/simulation/decoder_test.go b/x/incentive/simulation/decoder_test.go index 1759f5b3..d0acb785 100644 --- a/x/incentive/simulation/decoder_test.go +++ b/x/incentive/simulation/decoder_test.go @@ -27,8 +27,8 @@ func TestDecodeDistributionStore(t *testing.T) { // Set up RewardPeriod, ClaimPeriod, Claim, and previous block time rewardPeriod := types.NewRewardPeriod("btc", time.Now().UTC(), time.Now().Add(time.Hour*1).UTC(), - sdk.NewCoin("ukava", sdk.NewInt(10000000000)), time.Now().Add(time.Hour*2).UTC(), time.Duration(time.Hour*2)) - claimPeriod := types.NewClaimPeriod("btc", 1, time.Now().Add(time.Hour*24).UTC(), time.Duration(time.Hour*24)) + sdk.NewCoin("ukava", sdk.NewInt(10000000000)), time.Now().Add(time.Hour*2).UTC(), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33"))}) + claimPeriod := types.NewClaimPeriod("btc", 1, time.Now().Add(time.Hour*24).UTC(), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33"))}) addr, _ := sdk.AccAddressFromBech32("kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw") claim := types.NewClaim(addr, sdk.NewCoin("ukava", sdk.NewInt(1000000)), "bnb", 1) prevBlockTime := time.Now().Add(time.Hour * -1).UTC() diff --git a/x/incentive/simulation/genesis.go b/x/incentive/simulation/genesis.go index 21a50501..37bb2fb8 100644 --- a/x/incentive/simulation/genesis.go +++ b/x/incentive/simulation/genesis.go @@ -67,12 +67,15 @@ func genRewards(r *rand.Rand) types.Rewards { // total reward is in range (half max total reward, max total reward) amount := simulation.RandIntBetween(r, int(MaxTotalAssetReward.Int64()/2), int(MaxTotalAssetReward.Int64())) totalRewards := sdk.NewInt64Coin(RewardDenom, int64(amount)) - // generate a random number of hours between 6-48 to use for reward's times - numbHours := simulation.RandIntBetween(r, 6, 48) - 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 - rewards[i] = types.NewReward(active, denom, totalRewards, duration, timeLock, claimDuration) + // generate a random number of months for lockups + numMonthsSmall := simulation.RandIntBetween(r, 0, 6) + numMonthsLarge := simulation.RandIntBetween(r, 7, 12) + multiplierSmall := types.NewMultiplier(types.Small, int64(numMonthsSmall), sdk.MustNewDecFromStr("0.33")) + multiplierLarge := types.NewMultiplier(types.Large, int64(numMonthsLarge), sdk.MustNewDecFromStr("1.0")) + + duration := time.Duration(time.Hour * time.Duration(simulation.RandIntBetween(r, 1, 48))) + claimDuration := time.Hour * time.Duration(simulation.RandIntBetween(r, 1, 48)) // twice as long as duration + rewards[i] = types.NewReward(active, denom, totalRewards, duration, types.Multipliers{multiplierSmall, multiplierLarge}, claimDuration) } return rewards } @@ -90,9 +93,9 @@ func genRewardPeriods(r *rand.Rand, timestamp time.Time, rewards types.Rewards) // Earlier periods have larger rewards amount := sdk.NewCoin("ukava", baseRewardAmount.Mul(sdk.NewInt(int64(i)))) claimEnd := end.Add(reward.ClaimDuration) - claimTimeLock := reward.TimeLock + claimMultipliers := reward.ClaimMultipliers // Create reward period and append to array - rewardPeriods[i] = types.NewRewardPeriod(reward.CollateralType, start, end, amount, claimEnd, claimTimeLock) + rewardPeriods[i] = types.NewRewardPeriod(reward.CollateralType, start, end, amount, claimEnd, claimMultipliers) // Update start time of next reward period rewardPeriodStart = end } @@ -110,9 +113,9 @@ func genClaimPeriods(rewardPeriods types.RewardPeriods) types.ClaimPeriods { denomRewardPeriodsCount[denom] = numbRewardPeriods // Set end and timelock from the associated reward period end := rewardPeriod.ClaimEnd - claimTimeLock := rewardPeriod.ClaimTimeLock + claimMultipliers := rewardPeriod.ClaimMultipliers // Create the new claim period for this reward period - claimPeriods[i] = types.NewClaimPeriod(denom, numbRewardPeriods, end, claimTimeLock) + claimPeriods[i] = types.NewClaimPeriod(denom, numbRewardPeriods, end, claimMultipliers) } return claimPeriods } diff --git a/x/incentive/simulation/operations.go b/x/incentive/simulation/operations.go index 9cd37ad6..39627f77 100644 --- a/x/incentive/simulation/operations.go +++ b/x/incentive/simulation/operations.go @@ -105,7 +105,13 @@ func SimulateMsgClaimReward(ak auth.AccountKeeper, sk types.SupplyKeeper, k keep return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("couldn't find account %s", claimer.Address) } - msg := types.NewMsgClaimReward(claimer.Address, claim.CollateralType) + multiplierName := types.Small + + if simulation.RandIntBetween(r, 0, 1) == 0 { + multiplierName = types.Large + } + + msg := types.NewMsgClaimReward(claimer.Address, claim.CollateralType, string(multiplierName)) tx := helpers.GenTx( []sdk.Msg{msg}, diff --git a/x/incentive/spec/01_concepts.md b/x/incentive/spec/01_concepts.md index e3e68edf..97a0c015 100644 --- a/x/incentive/spec/01_concepts.md +++ b/x/incentive/spec/01_concepts.md @@ -6,4 +6,4 @@ order: 1 This module presents an implementation of user incentives that are controlled by governance. When users take a certain action, in this case opening a CDP, they become eligible for rewards. Rewards are __opt in__ meaning that users must submit a message before the claim deadline to claim their rewards. The goals and background of this module were subject of a previous Kava governance proposal, which can be found [here](https://ipfs.io/ipfs/QmSYedssC3nyQacDJmNcREtgmTPyaMx2JX7RNkMdAVkdkr/user-growth-fund-proposal.pdf). -When governance adds a collateral type to be eligible for rewards, they set the rate (coins/time) at which rewards are given to users, the length of each reward period, the length of each claim period, and the amount of time reward coins must vest before users who claim them can transfer them. For the duration of a reward period, any user that has minted USDX using an eligible collateral type will ratably accumulate rewards in a `Claim` object. For example, if a user has minted 10% of all USDX for the duration of the reward period, they will earn 10% of all rewards for that period. When the reward period ends, the claim period begins immediately, at which point users can submit a message to claim their rewards. Rewards are time-locked, meaning that when a user claims rewards they will receive them as a vesting balance on their account. Vesting balances can be used to stake coins, but cannot be transferred until the vesting period ends. +When governance adds a collateral type to be eligible for rewards, they set the rate (coins/time) at which rewards are given to users, the length of each reward period, the length of each claim period, and the amount of time reward coins must vest before users who claim them can transfer them. For the duration of a reward period, any user that has minted USDX using an eligible collateral type will ratably accumulate rewards in a `Claim` object. For example, if a user has minted 10% of all USDX for the duration of the reward period, they will earn 10% of all rewards for that period. When the reward period ends, the claim period begins immediately, at which point users can submit a message to claim their rewards. Rewards are time-locked, meaning that when a user claims rewards they will receive them as a vesting balance on their account. Vesting balances can be used to stake coins, but cannot be transferred until the vesting period ends. In addition to vesting, rewards can have multipliers that vary the number of tokens received. For example, a reward with a vesting period of 1 month may have a multiplier of 0.25, meaning that the user will receive 25% of the reward balance if they choose that vesting schedule. diff --git a/x/incentive/spec/02_state.md b/x/incentive/spec/02_state.md index f05ed35c..3974679c 100644 --- a/x/incentive/spec/02_state.md +++ b/x/incentive/spec/02_state.md @@ -11,18 +11,19 @@ order: 2 ```go // Params governance parameters for the incentive module type Params struct { - Active bool `json:"active" yaml:"active"` // top level governance switch to disable all rewards - Rewards Rewards `json:"rewards" yaml:"rewards"` + Active bool `json:"active" yaml:"active"` // top level governance switch to disable all rewards + Rewards Rewards `json:"rewards" yaml:"rewards"` } +// Reward stores the specified state for a single reward period. // Reward stores the specified state for a single reward period. type Reward struct { - Active bool `json:"active" yaml:"active"` // governance switch to disable a period - Denom string `json:"denom" yaml:"denom"` // the collateral denom rewards apply to, must be found in the cdp collaterals - AvailableRewards sdk.Coin `json:"available_rewards" yaml:"available_rewards"` // the total amount of coins distributed per period - Duration time.Duration `json:"duration" yaml:"duration"` // the duration of the period - TimeLock time.Duration `json:"time_lock" yaml:"time_lock"` // how long rewards for this period are timelocked - ClaimDuration time.Duration `json:"claim_duration" yaml:"claim_duration"` // how long users have after the period ends to claim their rewards + Active bool `json:"active" yaml:"active"` // governance switch to disable a period + CollateralType string `json:"collateral_type" yaml:"collateral_type"` // the collateral type rewards apply to, must be found in the cdp collaterals + AvailableRewards sdk.Coin `json:"available_rewards" yaml:"available_rewards"` // the total amount of coins distributed per period + Duration time.Duration `json:"duration" yaml:"duration"` // the duration of the period + ClaimMultipliers Multipliers `json:"claim_multipliers" yaml:"claim_multipliers"` // the reward multiplier and timelock schedule - applied at the time users claim rewards + ClaimDuration time.Duration `json:"claim_duration" yaml:"claim_duration"` // how long users have after the period ends to claim their rewards } ``` @@ -31,12 +32,12 @@ type Reward struct { ```go // GenesisState is the state that must be provided at genesis. type GenesisState struct { - Params Params `json:"params" yaml:"params"` - PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"` - RewardPeriods RewardPeriods `json:"reward_periods" yaml:"reward_periods"` - ClaimPeriods ClaimPeriods `json:"claim_periods" yaml:"claim_periods"` - Claims Claims `json:"claims" yaml:"claims"` - NextClaimPeriodIDs GenesisClaimPeriodIDs `json:"next_claim_period_ids" yaml:"next_claim_period_ids"` + Params Params `json:"params" yaml:"params"` + PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"` + RewardPeriods RewardPeriods `json:"reward_periods" yaml:"reward_periods"` + ClaimPeriods ClaimPeriods `json:"claim_periods" yaml:"claim_periods"` + Claims Claims `json:"claims" yaml:"claims"` + NextClaimPeriodIDs GenesisClaimPeriodIDs `json:"next_claim_period_ids" yaml:"next_claim_period_ids"` } ``` diff --git a/x/incentive/spec/03_messages.md b/x/incentive/spec/03_messages.md index 71c29dbf..deb5ed26 100644 --- a/x/incentive/spec/03_messages.md +++ b/x/incentive/spec/03_messages.md @@ -9,12 +9,14 @@ Users claim rewards using a `MsgClaimReward`. ```go // MsgClaimReward message type used to claim rewards type MsgClaimReward struct { - Sender sdk.AccAddress `json:"sender" yaml:"sender"` - Denom string `json:"denom" yaml:"denom"` + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + CollateralType string `json:"collateral_type" yaml:"collateral_type"` + MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"` } ``` ## State Modifications * Accumulated rewards for active claims are transferred from the `kavadist` module account to the users account as vesting coins +* The number of coins transferred is determined by the multiplier in the message. For example, the multiplier equals 1.0, 100% of the claim's reward value is transferred. If the multiplier equals 0.5, 50% of the claim's reward value is transferred. * The corresponding claim object(s) are deleted from the store diff --git a/x/incentive/spec/05_params.md b/x/incentive/spec/05_params.md index 6742914c..c3f5f897 100644 --- a/x/incentive/spec/05_params.md +++ b/x/incentive/spec/05_params.md @@ -13,11 +13,19 @@ The incentive module contains the following parameters: Each `Reward` has the following parameters -| Key | Type | Example | Description | -|------------------|--------------------|------------------------------------|----------------------------------------------------------------| -| Active | bool | "true | boolean for if rewards for this collateral are active | -| Denom | string | "bnb" | the collateral for which rewards are eligible | -| AvailableRewards | object (coin) | `{"denom":"kava","amount":"1000"}` | the rewards available per reward period | -| Duration | string (time ns) | "172800000000000" | the duration of each reward period | -| TimeLock | string (time ns) | "172800000000000" | the duration for which claimed rewards will be vesting | -| ClaimDuration | string (time ns) | "172800000000000" | how long users have to claim rewards before they expire | +| Key | Type | Example | Description | +|------------------|--------------------|------------------------------------|-------------------------------------------------------------------------------------------------------------------| +| Active | bool | "true | boolean for if rewards for this collateral are active | +| Denom | string | "bnb" | the collateral for which rewards are eligible | +| AvailableRewards | object (coin) | `{"denom":"kava","amount":"1000"}` | the rewards available per reward period | +| Duration | string (time ns) | "172800000000000" | the duration of each reward period | +| ClaimMultipliers | array (Multiplier) | [{see below}] | the number of months for which claimed rewards will be vesting and the multiplier applied when rewards are claimed| +| ClaimDuration | string (time ns) | "172800000000000" | how long users have to claim rewards before they expire | + +Each `Multiplier` has the following parameters: + +| Key | Type | Example | Description | +|-----------------------|--------------------|--------------------------|-----------------------------------------------------------------| +| Name | string | "large" | the unique name of the reward multiplier | +| MonthsLockup | int | "6" | number of months HARD tokens with this multiplier are locked | +| Factor | Dec | "0.5" | the scaling factor for HARD tokens claimed with this multiplier | diff --git a/x/incentive/types/errors.go b/x/incentive/types/errors.go index 04aa282c..2ce76d02 100644 --- a/x/incentive/types/errors.go +++ b/x/incentive/types/errors.go @@ -6,11 +6,14 @@ import ( // DONTCOVER +// Incentive module errors var ( - ErrClaimNotFound = sdkerrors.Register(ModuleName, 1, "no claim with input id found for owner and collateral type") - ErrClaimPeriodNotFound = sdkerrors.Register(ModuleName, 2, "no claim period found for id and collateral type") - ErrInvalidAccountType = sdkerrors.Register(ModuleName, 3, "account type not supported") - ErrNoClaimsFound = sdkerrors.Register(ModuleName, 4, "no claims with collateral type found for address") - ErrInsufficientModAccountBalance = sdkerrors.Register(ModuleName, 5, "module account has insufficient balance to pay claim") - ErrAccountNotFound = sdkerrors.Register(ModuleName, 6, "account not found") + ErrClaimNotFound = sdkerrors.Register(ModuleName, 2, "no claim with input id found for owner and collateral type") + ErrClaimPeriodNotFound = sdkerrors.Register(ModuleName, 3, "no claim period found for id and collateral type") + ErrInvalidAccountType = sdkerrors.Register(ModuleName, 4, "account type not supported") + ErrNoClaimsFound = sdkerrors.Register(ModuleName, 5, "no claims with collateral type found for address") + ErrInsufficientModAccountBalance = sdkerrors.Register(ModuleName, 6, "module account has insufficient balance to pay claim") + ErrAccountNotFound = sdkerrors.Register(ModuleName, 7, "account not found") + ErrInvalidMultiplier = sdkerrors.Register(ModuleName, 8, "invalid rewards multiplier") + ErrZeroClaim = sdkerrors.Register(ModuleName, 9, "cannot claim - claim amount rounds to zero") ) diff --git a/x/incentive/types/genesis_test.go b/x/incentive/types/genesis_test.go index 6bc71336..5f690bb9 100644 --- a/x/incentive/types/genesis_test.go +++ b/x/incentive/types/genesis_test.go @@ -68,11 +68,11 @@ func TestGenesisStateValidate(t *testing.T) { rewards := Rewards{ NewReward( true, "bnb", sdk.NewCoin("ukava", sdk.NewInt(10000000000)), - time.Hour*24*7, time.Hour*8766, time.Hour*24*14, + time.Hour*24*7, Multipliers{NewMultiplier(Small, 1, sdk.MustNewDecFromStr("0.33"))}, 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)} + rewardPeriods := RewardPeriods{NewRewardPeriod("bnb", now, now.Add(time.Hour), sdk.NewCoin("bnb", sdk.OneInt()), now, Multipliers{NewMultiplier(Small, 1, sdk.MustNewDecFromStr("0.33"))})} + claimPeriods := ClaimPeriods{NewClaimPeriod("bnb", 10, now, Multipliers{NewMultiplier(Small, 1, sdk.MustNewDecFromStr("0.33"))})} claims := Claims{NewClaim(owner, sdk.NewCoin("bnb", sdk.OneInt()), "bnb", 10)} gcps := GenesisClaimPeriodIDs{{CollateralType: "bnb", ID: 1}} diff --git a/x/incentive/types/msg.go b/x/incentive/types/msg.go index 21914b8e..02e8c923 100644 --- a/x/incentive/types/msg.go +++ b/x/incentive/types/msg.go @@ -15,13 +15,15 @@ var _ sdk.Msg = &MsgClaimReward{} type MsgClaimReward struct { Sender sdk.AccAddress `json:"sender" yaml:"sender"` CollateralType string `json:"collateral_type" yaml:"collateral_type"` + MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"` } // NewMsgClaimReward returns a new MsgClaimReward. -func NewMsgClaimReward(sender sdk.AccAddress, collateralType string) MsgClaimReward { +func NewMsgClaimReward(sender sdk.AccAddress, collateralType, multiplierName string) MsgClaimReward { return MsgClaimReward{ Sender: sender, CollateralType: collateralType, + MultiplierName: multiplierName, } } @@ -39,7 +41,7 @@ func (msg MsgClaimReward) ValidateBasic() error { if strings.TrimSpace(msg.CollateralType) == "" { return fmt.Errorf("collateral type cannot be blank") } - return nil + return MultiplierName(strings.ToLower(msg.MultiplierName)).IsValid() } // GetSignBytes gets the canonical byte representation of the Msg. diff --git a/x/incentive/types/msg_test.go b/x/incentive/types/msg_test.go index 475c13ea..9f3a44c0 100644 --- a/x/incentive/types/msg_test.go +++ b/x/incentive/types/msg_test.go @@ -15,6 +15,7 @@ import ( type msgTest struct { from sdk.AccAddress collateralType string + multiplierName string expectPass bool } @@ -29,16 +30,25 @@ func (suite *MsgTestSuite) SetupTest() { { from: sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))), collateralType: "bnb", + multiplierName: "large", expectPass: true, }, { from: sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))), collateralType: "", + multiplierName: "small", expectPass: false, }, { from: sdk.AccAddress{}, collateralType: "bnb", + multiplierName: "medium", + expectPass: false, + }, + { + from: sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))), + collateralType: "bnb", + multiplierName: "huge", expectPass: false, }, } @@ -47,7 +57,7 @@ func (suite *MsgTestSuite) SetupTest() { func (suite *MsgTestSuite) TestMsgValidation() { for _, t := range suite.tests { - msg := types.NewMsgClaimReward(t.from, t.collateralType) + msg := types.NewMsgClaimReward(t.from, t.collateralType, t.multiplierName) err := msg.ValidateBasic() if t.expectPass { suite.Require().NoError(err) diff --git a/x/incentive/types/params.go b/x/incentive/types/params.go index 6e14d602..c1e6ee3d 100644 --- a/x/incentive/types/params.go +++ b/x/incentive/types/params.go @@ -14,6 +14,13 @@ import ( kavadistTypes "github.com/kava-labs/kava/x/kavadist/types" ) +// Valid reward multipliers +const ( + Small MultiplierName = "small" + Medium MultiplierName = "medium" + Large MultiplierName = "large" +) + // Parameter keys and default values var ( KeyActive = []byte("Active") @@ -40,7 +47,7 @@ func NewParams(active bool, rewards Rewards) Params { } } -// DefaultParams returns default params for kavadist module +// DefaultParams returns default params for incentive module func DefaultParams() Params { return NewParams(DefaultActive, DefaultRewards) } @@ -97,18 +104,18 @@ type Reward struct { CollateralType string `json:"collateral_type" yaml:"collateral_type"` // the collateral type rewards apply to, must be found in the cdp collaterals AvailableRewards sdk.Coin `json:"available_rewards" yaml:"available_rewards"` // the total amount of coins distributed per period Duration time.Duration `json:"duration" yaml:"duration"` // the duration of the period - TimeLock time.Duration `json:"time_lock" yaml:"time_lock"` // how long rewards for this period are timelocked + ClaimMultipliers Multipliers `json:"claim_multipliers" yaml:"claim_multipliers"` // the reward multiplier and timelock schedule - applied at the time users claim rewards ClaimDuration time.Duration `json:"claim_duration" yaml:"claim_duration"` // how long users have after the period ends to claim their rewards } // NewReward returns a new Reward -func NewReward(active bool, collateralType string, reward sdk.Coin, duration time.Duration, timelock time.Duration, claimDuration time.Duration) Reward { +func NewReward(active bool, collateralType string, reward sdk.Coin, duration time.Duration, multiplier Multipliers, claimDuration time.Duration) Reward { return Reward{ Active: active, CollateralType: collateralType, AvailableRewards: reward, Duration: duration, - TimeLock: timelock, + ClaimMultipliers: multiplier, ClaimDuration: claimDuration, } } @@ -120,9 +127,9 @@ func (r Reward) String() string { CollateralType: %s, Available Rewards: %s, Duration: %s, - Time Lock: %s, + %s, Claim Duration: %s`, - r.Active, r.CollateralType, r.AvailableRewards, r.Duration, r.TimeLock, r.ClaimDuration) + r.Active, r.CollateralType, r.AvailableRewards, r.Duration, r.ClaimMultipliers, r.ClaimDuration) } // Validate performs a basic check of a reward fields. @@ -136,8 +143,8 @@ func (r Reward) Validate() error { if r.Duration <= 0 { return fmt.Errorf("reward duration must be positive, is %s for %s", r.Duration, r.CollateralType) } - if r.TimeLock < 0 { - return fmt.Errorf("reward timelock must be non-negative, is %s for %s", r.TimeLock, r.CollateralType) + if err := r.ClaimMultipliers.Validate(); err != nil { + return err } if r.ClaimDuration <= 0 { return fmt.Errorf("claim duration must be positive, is %s for %s", r.ClaimDuration, r.CollateralType) @@ -177,3 +184,77 @@ func (rs Rewards) String() string { } return out } + +// Multiplier amount the claim rewards get increased by, along with how long the claim rewards are locked +type Multiplier struct { + Name MultiplierName `json:"name" yaml:"name"` + MonthsLockup int64 `json:"months_lockup" yaml:"months_lockup"` + Factor sdk.Dec `json:"factor" yaml:"factor"` +} + +// NewMultiplier returns a new Multiplier +func NewMultiplier(name MultiplierName, lockup int64, factor sdk.Dec) Multiplier { + return Multiplier{ + Name: name, + MonthsLockup: lockup, + Factor: factor, + } +} + +// Validate multiplier param +func (m Multiplier) Validate() error { + if err := m.Name.IsValid(); err != nil { + return err + } + if m.MonthsLockup < 0 { + return fmt.Errorf("expected non-negative lockup, got %d", m.MonthsLockup) + } + if m.Factor.IsNegative() { + return fmt.Errorf("expected non-negative factor, got %s", m.Factor.String()) + } + + return nil +} + +// String implements fmt.Stringer +func (m Multiplier) String() string { + return fmt.Sprintf(`Claim Multiplier: + Name: %s + Months Lockup %d + Factor %s + `, m.Name, m.MonthsLockup, m.Factor) +} + +// Multipliers slice of Multiplier +type Multipliers []Multiplier + +// Validate validates each multiplier +func (ms Multipliers) Validate() error { + for _, m := range ms { + if err := m.Validate(); err != nil { + return err + } + } + return nil +} + +// String implements fmt.Stringer +func (ms Multipliers) String() string { + out := "Claim Multipliers\n" + for _, s := range ms { + out += fmt.Sprintf("%s\n", s) + } + return out +} + +// MultiplierName name for valid multiplier +type MultiplierName string + +// IsValid checks if the input is one of the expected strings +func (mn MultiplierName) IsValid() error { + switch mn { + case Small, Medium, Large: + return nil + } + return fmt.Errorf("invalid multiplier name: %s", mn) +} diff --git a/x/incentive/types/params_test.go b/x/incentive/types/params_test.go index 041a2155..f1ba6251 100644 --- a/x/incentive/types/params_test.go +++ b/x/incentive/types/params_test.go @@ -41,7 +41,7 @@ func (suite *ParamTestSuite) SetupTest() { CollateralType: "bnb-a", AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)), Duration: time.Hour * 24 * 7, - TimeLock: time.Hour * 8766, + ClaimMultipliers: types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, ClaimDuration: time.Hour * 24 * 14, }, }, @@ -61,7 +61,7 @@ func (suite *ParamTestSuite) SetupTest() { CollateralType: "bnb-a", AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)), Duration: time.Hour * 24 * 7, - TimeLock: time.Hour * 8766, + ClaimMultipliers: types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, ClaimDuration: time.Hour * 24 * 14, }, }, @@ -81,7 +81,7 @@ func (suite *ParamTestSuite) SetupTest() { CollateralType: "bnb-a", AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)), Duration: time.Hour * 24 * 7, - TimeLock: time.Hour * 8766, + ClaimMultipliers: types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, ClaimDuration: time.Hour * 24 * 14, }, types.Reward{ @@ -89,7 +89,7 @@ func (suite *ParamTestSuite) SetupTest() { CollateralType: "bnb-a", AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)), Duration: time.Hour * 24 * 7, - TimeLock: time.Hour * 8766, + ClaimMultipliers: types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, ClaimDuration: time.Hour * 24 * 14, }, }, @@ -109,7 +109,7 @@ func (suite *ParamTestSuite) SetupTest() { CollateralType: "bnb-a", AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)), Duration: time.Hour * -24 * 7, - TimeLock: time.Hour * 8766, + ClaimMultipliers: types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, ClaimDuration: time.Hour * 24 * 14, }, }, @@ -129,14 +129,14 @@ func (suite *ParamTestSuite) SetupTest() { CollateralType: "bnb-a", AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)), Duration: time.Hour * 24 * 7, - TimeLock: time.Hour * -8766, + ClaimMultipliers: types.Multipliers{types.NewMultiplier(types.Small, -1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, ClaimDuration: time.Hour * 24 * 14, }, }, }, errResult: errResult{ expectPass: false, - contains: "reward timelock must be non-negative", + contains: "expected non-negative lockup", }, }, { @@ -149,7 +149,7 @@ func (suite *ParamTestSuite) SetupTest() { CollateralType: "bnb-a", AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)), Duration: time.Hour * 24 * 7, - TimeLock: time.Hour * 8766, + ClaimMultipliers: types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, ClaimDuration: time.Hour * 0, }, }, @@ -169,7 +169,7 @@ func (suite *ParamTestSuite) SetupTest() { CollateralType: "bnb-a", AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(0)), Duration: time.Hour * 24 * 7, - TimeLock: time.Hour * 8766, + ClaimMultipliers: types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, ClaimDuration: time.Hour * 24 * 14, }, }, @@ -189,7 +189,7 @@ func (suite *ParamTestSuite) SetupTest() { CollateralType: "", AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(1)), Duration: time.Hour * 24 * 7, - TimeLock: time.Hour * 8766, + ClaimMultipliers: types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, ClaimDuration: time.Hour * 24 * 14, }, }, diff --git a/x/incentive/types/querier.go b/x/incentive/types/querier.go index 5d519d02..e02b4347 100644 --- a/x/incentive/types/querier.go +++ b/x/incentive/types/querier.go @@ -34,4 +34,5 @@ type PostClaimReq struct { BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` Sender sdk.AccAddress `json:"sender" yaml:"sender"` CollateralType string `json:"collateral_type" yaml:"collateral_type"` + MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"` } diff --git a/x/incentive/types/rewards.go b/x/incentive/types/rewards.go index 38eda75c..6da64a0d 100644 --- a/x/incentive/types/rewards.go +++ b/x/incentive/types/rewards.go @@ -11,12 +11,12 @@ import ( // RewardPeriod stores the state of an ongoing reward type RewardPeriod struct { - CollateralType string `json:"collateral_type" yaml:"collateral_type"` - Start time.Time `json:"start" yaml:"start"` - End time.Time `json:"end" yaml:"end"` - Reward sdk.Coin `json:"reward" yaml:"reward"` // per second reward payouts - ClaimEnd time.Time `json:"claim_end" yaml:"claim_end"` - ClaimTimeLock time.Duration `json:"claim_time_lock" yaml:"claim_time_lock"` // the amount of time rewards are timelocked once they are sent to users + CollateralType string `json:"collateral_type" yaml:"collateral_type"` + Start time.Time `json:"start" yaml:"start"` + End time.Time `json:"end" yaml:"end"` + Reward sdk.Coin `json:"reward" yaml:"reward"` // per second reward payouts + ClaimEnd time.Time `json:"claim_end" yaml:"claim_end"` + ClaimMultipliers Multipliers `json:"claim_multipliers" yaml:"claim_multipliers"` // the reward multiplier and timelock schedule - applied at the time users claim rewards } // String implements fmt.Stringer @@ -27,19 +27,19 @@ func (rp RewardPeriod) String() string { End: %s, Reward: %s, Claim End: %s, - Claim Time Lock: %s - `, rp.CollateralType, rp.Start, rp.End, rp.Reward, rp.ClaimEnd, rp.ClaimTimeLock) + %s + `, rp.CollateralType, rp.Start, rp.End, rp.Reward, rp.ClaimEnd, rp.ClaimMultipliers) } // NewRewardPeriod returns a new RewardPeriod -func NewRewardPeriod(collateralType string, start time.Time, end time.Time, reward sdk.Coin, claimEnd time.Time, claimTimeLock time.Duration) RewardPeriod { +func NewRewardPeriod(collateralType string, start time.Time, end time.Time, reward sdk.Coin, claimEnd time.Time, claimMultipliers Multipliers) RewardPeriod { return RewardPeriod{ - CollateralType: collateralType, - Start: start, - End: end, - Reward: reward, - ClaimEnd: claimEnd, - ClaimTimeLock: claimTimeLock, + CollateralType: collateralType, + Start: start, + End: end, + Reward: reward, + ClaimEnd: claimEnd, + ClaimMultipliers: claimMultipliers, } } @@ -60,8 +60,8 @@ func (rp RewardPeriod) Validate() error { 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") + if err := rp.ClaimMultipliers.Validate(); err != nil { + return err } if strings.TrimSpace(rp.CollateralType) == "" { return fmt.Errorf("reward period collateral type cannot be blank: %s", rp) @@ -92,19 +92,19 @@ func (rps RewardPeriods) Validate() error { // ClaimPeriod stores the state of an ongoing claim period type ClaimPeriod struct { - CollateralType string `json:"collateral_type" yaml:"collateral_type"` - ID uint64 `json:"id" yaml:"id"` - End time.Time `json:"end" yaml:"end"` - TimeLock time.Duration `json:"time_lock" yaml:"time_lock"` + CollateralType string `json:"collateral_type" yaml:"collateral_type"` + ID uint64 `json:"id" yaml:"id"` + End time.Time `json:"end" yaml:"end"` + ClaimMultipliers Multipliers `json:"claim_multipliers" yaml:"claim_multipliers"` } // NewClaimPeriod returns a new ClaimPeriod -func NewClaimPeriod(collateralType string, id uint64, end time.Time, timeLock time.Duration) ClaimPeriod { +func NewClaimPeriod(collateralType string, id uint64, end time.Time, multipliers Multipliers) ClaimPeriod { return ClaimPeriod{ - CollateralType: collateralType, - ID: id, - End: end, - TimeLock: timeLock, + CollateralType: collateralType, + ID: id, + End: end, + ClaimMultipliers: multipliers, } } @@ -116,8 +116,8 @@ func (cp ClaimPeriod) Validate() error { 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") + if err := cp.ClaimMultipliers.Validate(); err != nil { + return err } if strings.TrimSpace(cp.CollateralType) == "" { return fmt.Errorf("claim period collateral type cannot be blank: %s", cp) @@ -131,8 +131,18 @@ func (cp ClaimPeriod) String() string { Collateral Type: %s, ID: %d, End: %s, - Claim Time Lock: %s - `, cp.CollateralType, cp.ID, cp.End, cp.TimeLock) + %s + `, cp.CollateralType, cp.ID, cp.End, cp.ClaimMultipliers) +} + +// GetMultiplier returns the named multiplier from the input claim period +func (cp ClaimPeriod) GetMultiplier(name MultiplierName) (Multiplier, bool) { + for _, multiplier := range cp.ClaimMultipliers { + if multiplier.Name == name { + return multiplier, true + } + } + return Multiplier{}, false } // ClaimPeriods array of ClaimPeriod @@ -261,11 +271,11 @@ func NewRewardPeriodFromReward(reward Reward, blockTime time.Time) RewardPeriod rewardsPerSecond := sdk.NewDecFromInt(reward.AvailableRewards.Amount).Quo(sdk.NewDecFromInt(sdk.NewInt(int64(reward.Duration.Seconds())))).TruncateInt() rewardCoinPerSecond := sdk.NewCoin(reward.AvailableRewards.Denom, rewardsPerSecond) return RewardPeriod{ - CollateralType: reward.CollateralType, - Start: blockTime, - End: blockTime.Add(reward.Duration), - Reward: rewardCoinPerSecond, - ClaimEnd: blockTime.Add(reward.Duration).Add(reward.ClaimDuration), - ClaimTimeLock: reward.TimeLock, + CollateralType: reward.CollateralType, + Start: blockTime, + End: blockTime.Add(reward.Duration), + Reward: rewardCoinPerSecond, + ClaimEnd: blockTime.Add(reward.Duration).Add(reward.ClaimDuration), + ClaimMultipliers: reward.ClaimMultipliers, } } diff --git a/x/incentive/types/rewards_test.go b/x/incentive/types/rewards_test.go index 269b4504..2d9531c4 100644 --- a/x/incentive/types/rewards_test.go +++ b/x/incentive/types/rewards_test.go @@ -21,7 +21,7 @@ func TestRewardPeriodsValidate(t *testing.T) { { "valid", 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, Multipliers{NewMultiplier(Small, 1, sdk.MustNewDecFromStr("0.33"))}), }, true, }, @@ -80,14 +80,14 @@ func TestRewardPeriodsValidate(t *testing.T) { false, }, { - "zero claim time lock", + "negative time lock", RewardPeriods{ { - Start: now, - End: now.Add(time.Hour), - Reward: sdk.NewCoin("bnb", sdk.OneInt()), - ClaimEnd: now, - ClaimTimeLock: 0, + Start: now, + End: now.Add(time.Hour), + Reward: sdk.NewCoin("bnb", sdk.OneInt()), + ClaimEnd: now, + ClaimMultipliers: Multipliers{NewMultiplier(Small, -1, sdk.MustNewDecFromStr("0.33"))}, }, }, false, @@ -96,12 +96,12 @@ func TestRewardPeriodsValidate(t *testing.T) { "invalid collateral type", RewardPeriods{ { - Start: now, - End: now.Add(time.Hour), - Reward: sdk.NewCoin("bnb", sdk.OneInt()), - ClaimEnd: now, - ClaimTimeLock: 10, - CollateralType: "", + Start: now, + End: now.Add(time.Hour), + Reward: sdk.NewCoin("bnb", sdk.OneInt()), + ClaimEnd: now, + ClaimMultipliers: Multipliers{NewMultiplier(Small, 1, sdk.MustNewDecFromStr("0.33"))}, + CollateralType: "", }, }, false, @@ -109,8 +109,8 @@ func TestRewardPeriodsValidate(t *testing.T) { { "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), + NewRewardPeriod("bnb", now, now.Add(time.Hour), sdk.NewCoin("bnb", sdk.OneInt()), now, Multipliers{NewMultiplier(Small, 1, sdk.MustNewDecFromStr("0.33"))}), + NewRewardPeriod("bnb", now, now.Add(time.Hour), sdk.NewCoin("bnb", sdk.OneInt()), now, Multipliers{NewMultiplier(Small, 1, sdk.MustNewDecFromStr("0.33"))}), }, false, }, @@ -137,7 +137,7 @@ func TestClaimPeriodsValidate(t *testing.T) { { "valid", ClaimPeriods{ - NewClaimPeriod("bnb", 10, now, 100), + NewClaimPeriod("bnb", 10, now, Multipliers{NewMultiplier(Small, 1, sdk.MustNewDecFromStr("0.33"))}), }, true, }, @@ -156,31 +156,38 @@ func TestClaimPeriodsValidate(t *testing.T) { false, }, { - "zero time lock", + "negative time lock", ClaimPeriods{ - {ID: 10, End: now, TimeLock: 0}, + {ID: 10, End: now, ClaimMultipliers: Multipliers{NewMultiplier(Small, -1, sdk.MustNewDecFromStr("0.33"))}}, + }, + false, + }, + { + "negative multiplier", + ClaimPeriods{ + NewClaimPeriod("bnb", 10, now, Multipliers{NewMultiplier(Small, 1, sdk.MustNewDecFromStr("-0.33"))}), }, false, }, { "start time > end time", ClaimPeriods{ - {ID: 10, End: now, TimeLock: 0}, + {ID: 10, End: now, ClaimMultipliers: Multipliers{NewMultiplier(Small, -1, sdk.MustNewDecFromStr("0.33"))}}, }, false, }, { "invalid collateral type", ClaimPeriods{ - {ID: 10, End: now, TimeLock: 100, CollateralType: ""}, + {ID: 10, End: now, ClaimMultipliers: Multipliers{NewMultiplier(Small, -1, sdk.MustNewDecFromStr("0.33"))}, CollateralType: ""}, }, false, }, { "duplicate reward period", ClaimPeriods{ - NewClaimPeriod("bnb", 10, now, 100), - NewClaimPeriod("bnb", 10, now, 100), + NewClaimPeriod("bnb", 10, now, Multipliers{NewMultiplier(Small, -1, sdk.MustNewDecFromStr("0.33"))}), + NewClaimPeriod("bnb", 10, now, Multipliers{NewMultiplier(Small, -1, sdk.MustNewDecFromStr("0.33"))}), }, false, },