diff --git a/x/incentive/keeper/payout.go b/x/incentive/keeper/payout.go index d83abad2..b9204221 100644 --- a/x/incentive/keeper/payout.go +++ b/x/incentive/keeper/payout.go @@ -1,6 +1,8 @@ package keeper import ( + "time" + sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/cosmos-sdk/x/auth" @@ -12,6 +14,15 @@ import ( validatorvesting "github.com/kava-labs/kava/x/validator-vesting" ) +const ( + // BeginningOfMonth harvest rewards that are claimed after the 15th at 14:00UTC of the month always vest on the first of the month + BeginningOfMonth = 1 + // MidMonth harvest rewards that are claimed before the 15th at 14:00UTC of the month always vest on the 15 of the month + MidMonth = 15 + // PaymentHour harvest rewards always vest at 14:00UTC + PaymentHour = 14 +) + // ClaimUSDXMintingReward sends the reward amount to the input address and zero's out the claim in the store func (k Keeper) ClaimUSDXMintingReward(ctx sdk.Context, addr sdk.AccAddress, multiplierName types.MultiplierName) error { claim, found := k.GetUSDXMintingClaim(ctx, addr) @@ -40,7 +51,10 @@ func (k Keeper) ClaimUSDXMintingReward(ctx sdk.Context, addr sdk.AccAddress, mul return types.ErrZeroClaim } rewardCoin := sdk.NewCoin(claim.Reward.Denom, rewardAmount) - length := ctx.BlockTime().AddDate(0, int(multiplier.MonthsLockup), 0).Unix() - ctx.BlockTime().Unix() + length, err := k.GetPeriodLength(ctx, multiplier) + if err != nil { + return err + } err = k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, addr, sdk.NewCoins(rewardCoin), length) if err != nil { @@ -93,9 +107,12 @@ func (k Keeper) ClaimHardReward(ctx sdk.Context, addr sdk.AccAddress, multiplier } rewardCoins = append(rewardCoins, sdk.NewCoin(coin.Denom, rewardAmount)) } - length := ctx.BlockTime().AddDate(0, int(multiplier.MonthsLockup), 0).Unix() - ctx.BlockTime().Unix() + length, err := k.GetPeriodLength(ctx, multiplier) + if err != nil { + return err + } - err := k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, addr, rewardCoins, length) + err = k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, addr, rewardCoins, length) if err != nil { return err } @@ -170,6 +187,29 @@ func (k Keeper) SendTimeLockedCoinsToBaseAccount(ctx sdk.Context, senderModule s return nil } +// GetPeriodLength returns the length of the period based on the input blocktime and multiplier +// note that pay dates are always the 1st or 15th of the month at 14:00UTC. +func (k Keeper) GetPeriodLength(ctx sdk.Context, multiplier types.Multiplier) (int64, error) { + + if multiplier.MonthsLockup == 0 { + return 0, nil + } + switch multiplier.Name { + case types.Small, types.Medium, types.Large: + currentDay := ctx.BlockTime().Day() + payDay := BeginningOfMonth + monthOffset := int64(1) + if currentDay < MidMonth || (currentDay == MidMonth && ctx.BlockTime().Hour() < PaymentHour) { + payDay = MidMonth + monthOffset = int64(0) + } + periodEndDate := time.Date(ctx.BlockTime().Year(), ctx.BlockTime().Month(), payDay, PaymentHour, 0, 0, 0, time.UTC).AddDate(0, int(multiplier.MonthsLockup+monthOffset), 0) + return periodEndDate.Unix() - ctx.BlockTime().Unix(), nil + default: + return 0, types.ErrInvalidMultiplier + } +} + // addCoinsToVestingSchedule adds coins to the input account's vesting schedule where length is the amount of time (from the current block time), in seconds, that the coins will be vesting for // the input address must be a periodic vesting account func (k Keeper) addCoinsToVestingSchedule(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins, length int64) { diff --git a/x/incentive/keeper/payout_test.go b/x/incentive/keeper/payout_test.go index aeb85b92..4196d28f 100644 --- a/x/incentive/keeper/payout_test.go +++ b/x/incentive/keeper/payout_test.go @@ -2,7 +2,6 @@ package keeper_test import ( "errors" - "fmt" "strings" "time" @@ -55,7 +54,7 @@ func (suite *KeeperTestSuite) TestPayoutUSDXMintingClaim() { multiplier: types.MultiplierName("large"), timeElapsed: 86400, expectedBalance: cs(c("usdx", 10000000000), c("ukava", 10576385600)), - expectedPeriods: vesting.Periods{vesting.Period{Length: 31536000, Amount: cs(c("ukava", 10571385600))}}, + expectedPeriods: vesting.Periods{vesting.Period{Length: 32918400, Amount: cs(c("ukava", 10571385600))}}, isPeriodicVestingAccount: true, }, errArgs{ @@ -144,7 +143,6 @@ func (suite *KeeperTestSuite) TestPayoutUSDXMintingClaim() { } claim, found := suite.keeper.GetUSDXMintingClaim(suite.ctx, suite.addrs[0]) - fmt.Println(claim) suite.Require().True(found) suite.Require().Equal(c("ukava", 0), claim.Reward) } else { @@ -678,3 +676,142 @@ func (suite *KeeperTestSuite) SetupWithAccountState() { suite.ctx = ctx suite.addrs = addrs } + +func (suite *KeeperTestSuite) TestGetPeriodLength() { + type args struct { + blockTime time.Time + multiplier types.Multiplier + expectedLength int64 + } + type errArgs struct { + expectPass bool + contains string + } + type periodTest struct { + name string + args args + errArgs errArgs + } + testCases := []periodTest{ + { + name: "first half of month", + args: args{ + blockTime: time.Date(2020, 11, 2, 15, 0, 0, 0, time.UTC), + multiplier: types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.333333")), + expectedLength: time.Date(2021, 5, 15, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 11, 2, 15, 0, 0, 0, time.UTC).Unix(), + }, + errArgs: errArgs{ + expectPass: true, + contains: "", + }, + }, + { + name: "first half of month long lockup", + args: args{ + blockTime: time.Date(2020, 11, 2, 15, 0, 0, 0, time.UTC), + multiplier: types.NewMultiplier(types.Medium, 24, sdk.MustNewDecFromStr("0.333333")), + expectedLength: time.Date(2022, 11, 15, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 11, 2, 15, 0, 0, 0, time.UTC).Unix(), + }, + errArgs: errArgs{ + expectPass: true, + contains: "", + }, + }, + { + name: "second half of month", + args: args{ + blockTime: time.Date(2020, 12, 31, 15, 0, 0, 0, time.UTC), + multiplier: types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.333333")), + expectedLength: time.Date(2021, 7, 1, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 12, 31, 15, 0, 0, 0, time.UTC).Unix(), + }, + errArgs: errArgs{ + expectPass: true, + contains: "", + }, + }, + { + name: "second half of month long lockup", + args: args{ + blockTime: time.Date(2020, 12, 31, 15, 0, 0, 0, time.UTC), + multiplier: types.NewMultiplier(types.Large, 24, sdk.MustNewDecFromStr("0.333333")), + expectedLength: time.Date(2023, 1, 1, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 12, 31, 15, 0, 0, 0, time.UTC).Unix(), + }, + errArgs: errArgs{ + expectPass: true, + contains: "", + }, + }, + { + name: "end of feb", + args: args{ + blockTime: time.Date(2021, 2, 28, 15, 0, 0, 0, time.UTC), + multiplier: types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.333333")), + expectedLength: time.Date(2021, 9, 1, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2021, 2, 28, 15, 0, 0, 0, time.UTC).Unix(), + }, + errArgs: errArgs{ + expectPass: true, + contains: "", + }, + }, + { + name: "leap year", + args: args{ + blockTime: time.Date(2020, 2, 29, 15, 0, 0, 0, time.UTC), + multiplier: types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.333333")), + expectedLength: time.Date(2020, 9, 1, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 2, 29, 15, 0, 0, 0, time.UTC).Unix(), + }, + errArgs: errArgs{ + expectPass: true, + contains: "", + }, + }, + { + name: "leap year long lockup", + args: args{ + blockTime: time.Date(2020, 2, 29, 15, 0, 0, 0, time.UTC), + multiplier: types.NewMultiplier(types.Large, 24, sdk.MustNewDecFromStr("1")), + expectedLength: time.Date(2022, 3, 1, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 2, 29, 15, 0, 0, 0, time.UTC).Unix(), + }, + errArgs: errArgs{ + expectPass: true, + contains: "", + }, + }, + { + name: "exactly half of month", + args: args{ + blockTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + multiplier: types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.333333")), + expectedLength: time.Date(2021, 7, 1, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC).Unix(), + }, + errArgs: errArgs{ + expectPass: true, + contains: "", + }, + }, + { + name: "just before half of month", + args: args{ + blockTime: time.Date(2020, 12, 15, 13, 59, 59, 0, time.UTC), + multiplier: types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.333333")), + expectedLength: time.Date(2021, 6, 15, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 12, 15, 13, 59, 59, 0, time.UTC).Unix(), + }, + errArgs: errArgs{ + expectPass: true, + contains: "", + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + ctx := suite.ctx.WithBlockTime(tc.args.blockTime) + length, err := suite.keeper.GetPeriodLength(ctx, tc.args.multiplier) + if tc.errArgs.expectPass { + suite.Require().NoError(err) + suite.Require().Equal(tc.args.expectedLength, length) + } else { + suite.Require().Error(err) + } + }) + } +}