0g-chain/x/hvt/keeper/claim.go
Kevin Davis fe38c4aa43
harvest v1 (#658)
* wip: and types and keeper methods

* add keeper tests

* add client

* add spec and events

* respond to review comments

* apply suggestions from review

* feat: add test for validator vesting case

* use int64 for multiplier type

* remove incentive changes
2020-09-21 17:08:43 -04:00

149 lines
5.4 KiB
Go

package keeper
import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/hvt/types"
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
)
// ClaimReward sends the reward amount to the reward owner and deletes the claim from the store
func (k Keeper) ClaimReward(ctx sdk.Context, claimHolder sdk.AccAddress, receiver sdk.AccAddress, depositDenom string, depositType types.DepositType, multiplier types.MultiplierName) error {
claim, found := k.GetClaim(ctx, claimHolder, depositDenom, depositType)
if !found {
return sdkerrors.Wrapf(types.ErrClaimNotFound, "no %s %s claim found for %s", depositDenom, depositType, claimHolder)
}
err := k.validateSenderReceiver(ctx, claimHolder, receiver)
if err != nil {
return err
}
switch depositType {
case types.LP:
err = k.claimLPReward(ctx, claim, receiver, multiplier)
case types.Stake:
err = k.claimDelegatorReward(ctx, claim, receiver, multiplier)
default:
return sdkerrors.Wrap(types.ErrInvalidDepositType, string(depositType))
}
if err != nil {
return err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeClaimHarvestReward,
sdk.NewAttribute(sdk.AttributeKeyAmount, claim.Amount.String()),
sdk.NewAttribute(types.AttributeKeyClaimHolder, claimHolder.String()),
sdk.NewAttribute(types.AttributeKeyDepositDenom, depositDenom),
sdk.NewAttribute(types.AttributeKeyDepositType, string(depositType)),
sdk.NewAttribute(types.AttributeKeyClaimMultiplier, string(multiplier)),
),
)
k.DeleteClaim(ctx, claim)
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) {
switch multiplier.Name {
case types.Small:
return 0, nil
case 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
}
return 0, types.ErrInvalidMultiplier
}
func (k Keeper) claimLPReward(ctx sdk.Context, claim types.Claim, receiver sdk.AccAddress, multiplierName types.MultiplierName) error {
lps, found := k.GetLPSchedule(ctx, claim.DepositDenom)
if !found {
return sdkerrors.Wrapf(types.ErrLPScheduleNotFound, claim.DepositDenom)
}
multiplier, found := lps.GetMultiplier(multiplierName)
if !found {
return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName))
}
if ctx.BlockTime().After(lps.ClaimEnd) {
return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), lps.ClaimEnd)
}
rewardAmount := sdk.NewDecFromInt(claim.Amount.Amount).Mul(multiplier.Factor).RoundInt()
if rewardAmount.IsZero() {
return types.ErrZeroClaim
}
rewardCoin := sdk.NewCoin(claim.Amount.Denom, rewardAmount)
length, err := k.GetPeriodLength(ctx, multiplier)
if err != nil {
return err
}
return k.SendTimeLockedCoinsToAccount(ctx, types.LPAccount, receiver, sdk.NewCoins(rewardCoin), length)
}
func (k Keeper) claimDelegatorReward(ctx sdk.Context, claim types.Claim, receiver sdk.AccAddress, multiplierName types.MultiplierName) error {
dss, found := k.GetDelegatorSchedule(ctx, claim.DepositDenom)
if !found {
return sdkerrors.Wrapf(types.ErrLPScheduleNotFound, claim.DepositDenom)
}
multiplier, found := dss.DistributionSchedule.GetMultiplier(multiplierName)
if !found {
return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName))
}
if ctx.BlockTime().After(dss.DistributionSchedule.ClaimEnd) {
return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), dss.DistributionSchedule.ClaimEnd)
}
rewardAmount := sdk.NewDecFromInt(claim.Amount.Amount).Mul(multiplier.Factor).RoundInt()
if rewardAmount.IsZero() {
return types.ErrZeroClaim
}
rewardCoin := sdk.NewCoin(claim.Amount.Denom, rewardAmount)
length, err := k.GetPeriodLength(ctx, multiplier)
if err != nil {
return err
}
return k.SendTimeLockedCoinsToAccount(ctx, types.DelegatorAccount, receiver, sdk.NewCoins(rewardCoin), length)
}
func (k Keeper) validateSenderReceiver(ctx sdk.Context, sender, receiver sdk.AccAddress) error {
senderAcc := k.accountKeeper.GetAccount(ctx, sender)
if senderAcc == nil {
return sdkerrors.Wrapf(types.ErrAccountNotFound, sender.String())
}
switch senderAcc.(type) {
case *validatorvesting.ValidatorVestingAccount:
if sender.Equals(receiver) {
return sdkerrors.Wrapf(types.ErrInvalidAccountType, "%T", senderAcc)
}
default:
if !sender.Equals(receiver) {
return sdkerrors.Wrapf(types.ErrInvalidReceiver, "%s", sender)
}
}
return nil
}