0g-chain/x/harvest/keeper/claim.go
Kevin Davis ed7ce81e30
Harvest Migration (#672)
* add harvest to kava-4 migration

* add mainnet harvest incentive schedules

* fix: check for 0 lockup instead of assuming small in always 0
2020-09-30 19:07:30 -04:00

150 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/harvest/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) {
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
}
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
}