0g-chain/x/incentive/keeper/payout.go
Kevin Davis 4d6f6aab3c
[R4R] re-enable claiming rewards validator vesting accounts (#920)
* feat: add new msg types for claim rewards from validator vesting accounts

* fix: validate owner is validator vesting account

* feat: add validator vesting tests for incent claims

* address review comments

* fix: client command name and example
2021-06-07 09:04:32 -05:00

442 lines
16 KiB
Go

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"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
supplyExported "github.com/cosmos/cosmos-sdk/x/supply/exported"
"github.com/kava-labs/kava/x/incentive/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
)
// 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)
if !found {
return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", addr)
}
multiplier, found := k.GetMultiplier(ctx, multiplierName)
if !found {
return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName))
}
claimEnd := k.GetClaimEnd(ctx)
if ctx.BlockTime().After(claimEnd) {
return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), claimEnd)
}
claim, err := k.SynchronizeUSDXMintingClaim(ctx, claim)
if err != nil {
return err
}
rewardAmount := claim.Reward.Amount.ToDec().Mul(multiplier.Factor).RoundInt()
if rewardAmount.IsZero() {
return types.ErrZeroClaim
}
rewardCoin := sdk.NewCoin(claim.Reward.Denom, rewardAmount)
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 {
return err
}
k.ZeroUSDXMintingClaim(ctx, claim)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeClaim,
sdk.NewAttribute(types.AttributeKeyClaimedBy, claim.GetOwner().String()),
sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.GetReward().String()),
sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.GetType()),
),
)
return nil
}
// ClaimUSDXMintingReward sends the reward amount to the input receiver address and zero's out the claim in the store
func (k Keeper) ClaimUSDXMintingRewardVVesting(ctx sdk.Context, owner, receiver sdk.AccAddress, multiplierName types.MultiplierName) error {
claim, found := k.GetUSDXMintingClaim(ctx, owner)
if !found {
return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner)
}
acc := k.accountKeeper.GetAccount(ctx, owner)
if acc == nil {
return sdkerrors.Wrapf(types.ErrAccountNotFound, "address not found: %s", owner)
}
_, ok := acc.(*validatorvesting.ValidatorVestingAccount)
if !ok {
return sdkerrors.Wrapf(types.ErrInvalidAccountType, "owner account must be validator vesting account %s", owner)
}
multiplier, found := k.GetMultiplier(ctx, multiplierName)
if !found {
return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName))
}
claimEnd := k.GetClaimEnd(ctx)
if ctx.BlockTime().After(claimEnd) {
return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), claimEnd)
}
claim, err := k.SynchronizeUSDXMintingClaim(ctx, claim)
if err != nil {
return err
}
rewardAmount := claim.Reward.Amount.ToDec().Mul(multiplier.Factor).RoundInt()
if rewardAmount.IsZero() {
return types.ErrZeroClaim
}
rewardCoin := sdk.NewCoin(claim.Reward.Denom, rewardAmount)
length, err := k.GetPeriodLength(ctx, multiplier)
if err != nil {
return err
}
err = k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, receiver, sdk.NewCoins(rewardCoin), length)
if err != nil {
return err
}
k.ZeroUSDXMintingClaim(ctx, claim)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeClaim,
sdk.NewAttribute(types.AttributeKeyClaimedBy, claim.GetOwner().String()),
sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.GetReward().String()),
sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.GetType()),
),
)
return nil
}
// ClaimHardReward sends the reward amount to the input address and zero's out the claim in the store
func (k Keeper) ClaimHardReward(ctx sdk.Context, addr sdk.AccAddress, multiplierName types.MultiplierName) error {
_, found := k.GetHardLiquidityProviderClaim(ctx, addr)
if !found {
return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", addr)
}
multiplier, found := k.GetMultiplier(ctx, multiplierName)
if !found {
return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName))
}
claimEnd := k.GetClaimEnd(ctx)
if ctx.BlockTime().After(claimEnd) {
return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), claimEnd)
}
k.SynchronizeHardLiquidityProviderClaim(ctx, addr)
claim, found := k.GetHardLiquidityProviderClaim(ctx, addr)
if !found {
return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", addr)
}
var rewardCoins sdk.Coins
for _, coin := range claim.Reward {
rewardAmount := coin.Amount.ToDec().Mul(multiplier.Factor).RoundInt()
if rewardAmount.IsZero() {
continue
}
rewardCoins = append(rewardCoins, sdk.NewCoin(coin.Denom, rewardAmount))
}
if rewardCoins.IsZero() {
return types.ErrZeroClaim
}
length, err := k.GetPeriodLength(ctx, multiplier)
if err != nil {
return err
}
err = k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, addr, rewardCoins, length)
if err != nil {
return err
}
k.ZeroHardLiquidityProviderClaim(ctx, claim)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeClaim,
sdk.NewAttribute(types.AttributeKeyClaimedBy, claim.GetOwner().String()),
sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.GetReward().String()),
sdk.NewAttribute(types.AttributeKeyClaimType, claim.GetType()),
),
)
return nil
}
// ClaimHardReward sends the reward amount to the input address and zero's out the claim in the store
func (k Keeper) ClaimHardRewardVVesting(ctx sdk.Context, owner, receiver sdk.AccAddress, multiplierName types.MultiplierName) error {
_, found := k.GetHardLiquidityProviderClaim(ctx, owner)
if !found {
return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner)
}
acc := k.accountKeeper.GetAccount(ctx, owner)
if acc == nil {
return sdkerrors.Wrapf(types.ErrAccountNotFound, "address not found: %s", owner)
}
_, ok := acc.(*validatorvesting.ValidatorVestingAccount)
if !ok {
return sdkerrors.Wrapf(types.ErrInvalidAccountType, "owner account must be validator vesting account %s", owner)
}
multiplier, found := k.GetMultiplier(ctx, multiplierName)
if !found {
return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName))
}
claimEnd := k.GetClaimEnd(ctx)
if ctx.BlockTime().After(claimEnd) {
return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), claimEnd)
}
k.SynchronizeHardLiquidityProviderClaim(ctx, owner)
claim, found := k.GetHardLiquidityProviderClaim(ctx, owner)
if !found {
return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner)
}
var rewardCoins sdk.Coins
for _, coin := range claim.Reward {
rewardAmount := coin.Amount.ToDec().Mul(multiplier.Factor).RoundInt()
if rewardAmount.IsZero() {
continue
}
rewardCoins = append(rewardCoins, sdk.NewCoin(coin.Denom, rewardAmount))
}
if rewardCoins.IsZero() {
return types.ErrZeroClaim
}
length, err := k.GetPeriodLength(ctx, multiplier)
if err != nil {
return err
}
err = k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, receiver, rewardCoins, length)
if err != nil {
return err
}
k.ZeroHardLiquidityProviderClaim(ctx, claim)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeClaim,
sdk.NewAttribute(types.AttributeKeyClaimedBy, claim.GetOwner().String()),
sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.GetReward().String()),
sdk.NewAttribute(types.AttributeKeyClaimType, claim.GetType()),
),
)
return nil
}
// SendTimeLockedCoinsToAccount sends time-locked coins from the input module account to the recipient. If the recipients account is not a vesting account and the input length is greater than zero, the recipient account is converted to a periodic vesting account and the coins are added to the vesting balance as a vesting period with the input length.
func (k Keeper) SendTimeLockedCoinsToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins, length int64) error {
macc := k.supplyKeeper.GetModuleAccount(ctx, senderModule)
if !macc.GetCoins().IsAllGTE(amt) {
return sdkerrors.Wrapf(types.ErrInsufficientModAccountBalance, "%s", senderModule)
}
// 0. Get the account from the account keeper and do a type switch, error if it's a validator vesting account or module account (can make this work for validator vesting later if necessary)
acc := k.accountKeeper.GetAccount(ctx, recipientAddr)
if acc == nil {
return sdkerrors.Wrapf(types.ErrAccountNotFound, recipientAddr.String())
}
if length == 0 {
return k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, amt)
}
switch acc.(type) {
case *validatorvesting.ValidatorVestingAccount, supplyExported.ModuleAccountI:
return sdkerrors.Wrapf(types.ErrInvalidAccountType, "%T", acc)
case *vesting.PeriodicVestingAccount:
return k.SendTimeLockedCoinsToPeriodicVestingAccount(ctx, senderModule, recipientAddr, amt, length)
case *auth.BaseAccount:
return k.SendTimeLockedCoinsToBaseAccount(ctx, senderModule, recipientAddr, amt, length)
default:
return sdkerrors.Wrapf(types.ErrInvalidAccountType, "%T", acc)
}
}
// SendTimeLockedCoinsToPeriodicVestingAccount sends time-locked coins from the input module account to the recipient
func (k Keeper) SendTimeLockedCoinsToPeriodicVestingAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins, length int64) error {
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, amt)
if err != nil {
return err
}
k.addCoinsToVestingSchedule(ctx, recipientAddr, amt, length)
return nil
}
// SendTimeLockedCoinsToBaseAccount sends time-locked coins from the input module account to the recipient, converting the recipient account to a vesting account
func (k Keeper) SendTimeLockedCoinsToBaseAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins, length int64) error {
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, amt)
if err != nil {
return err
}
acc := k.accountKeeper.GetAccount(ctx, recipientAddr)
// transition the account to a periodic vesting account:
bacc := authtypes.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence())
newPeriods := vesting.Periods{types.NewPeriod(amt, length)}
bva, err := vesting.NewBaseVestingAccount(bacc, amt, ctx.BlockTime().Unix()+length)
if err != nil {
return err
}
pva := vesting.NewPeriodicVestingAccountRaw(bva, ctx.BlockTime().Unix(), newPeriods)
k.accountKeeper.SetAccount(ctx, pva)
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) {
acc := k.accountKeeper.GetAccount(ctx, addr)
vacc := acc.(*vesting.PeriodicVestingAccount)
// Add the new vesting coins to OriginalVesting
vacc.OriginalVesting = vacc.OriginalVesting.Add(amt...)
// update vesting periods
// EndTime = 100
// BlockTime = 110
// length == 6
if vacc.EndTime < ctx.BlockTime().Unix() {
// edge case one - the vesting account's end time is in the past (ie, all previous vesting periods have completed)
// append a new period to the vesting account, update the end time, update the account in the store and return
newPeriodLength := (ctx.BlockTime().Unix() - vacc.EndTime) + length // 110 - 100 + 6 = 16
newPeriod := types.NewPeriod(amt, newPeriodLength)
vacc.VestingPeriods = append(vacc.VestingPeriods, newPeriod)
vacc.EndTime = ctx.BlockTime().Unix() + length
k.accountKeeper.SetAccount(ctx, vacc)
return
}
// StartTime = 110
// BlockTime = 100
// length = 6
if vacc.StartTime > ctx.BlockTime().Unix() {
// edge case two - the vesting account's start time is in the future (all periods have not started)
// update the start time to now and adjust the period lengths in place - a new period will be inserted in the next code block
updatedPeriods := vesting.Periods{}
for i, period := range vacc.VestingPeriods {
updatedPeriod := period
if i == 0 {
updatedPeriod = types.NewPeriod(period.Amount, (vacc.StartTime-ctx.BlockTime().Unix())+period.Length) // 110 - 100 + 6 = 16
}
updatedPeriods = append(updatedPeriods, updatedPeriod)
}
vacc.VestingPeriods = updatedPeriods
vacc.StartTime = ctx.BlockTime().Unix()
}
// logic for inserting a new vesting period into the existing vesting schedule
remainingLength := vacc.EndTime - ctx.BlockTime().Unix()
elapsedTime := ctx.BlockTime().Unix() - vacc.StartTime
proposedEndTime := ctx.BlockTime().Unix() + length
if remainingLength < length {
// in the case that the proposed length is longer than the remaining length of all vesting periods, create a new period with length equal to the difference between the proposed length and the previous total length
newPeriodLength := length - remainingLength
newPeriod := types.NewPeriod(amt, newPeriodLength)
vacc.VestingPeriods = append(vacc.VestingPeriods, newPeriod)
// update the end time so that the sum of all period lengths equals endTime - startTime
vacc.EndTime = proposedEndTime
} else {
// In the case that the proposed length is less than or equal to the sum of all previous period lengths, insert the period and update other periods as necessary.
// EXAMPLE (l is length, a is amount)
// Original Periods: {[l: 1 a: 1], [l: 2, a: 1], [l:8, a:3], [l: 5, a: 3]}
// Period we want to insert [l: 5, a: x]
// Expected result:
// {[l: 1, a: 1], [l:2, a: 1], [l:2, a:x], [l:6, a:3], [l:5, a:3]}
// StartTime = 100
// Periods = [5,5,5,5]
// EndTime = 120
// BlockTime = 101
// length = 2
// for period in Periods:
// iteration 1:
// lengthCounter = 5
// if 5 < 101 - 100 + 2 - no
// if 5 = 3 - no
// else
// newperiod = 2 - 0
newPeriods := vesting.Periods{}
lengthCounter := int64(0)
appendRemaining := false
for _, period := range vacc.VestingPeriods {
if appendRemaining {
newPeriods = append(newPeriods, period)
continue
}
lengthCounter += period.Length
if lengthCounter < elapsedTime+length { // 1
newPeriods = append(newPeriods, period)
} else if lengthCounter == elapsedTime+length {
newPeriod := types.NewPeriod(period.Amount.Add(amt...), period.Length)
newPeriods = append(newPeriods, newPeriod)
appendRemaining = true
} else {
newPeriod := types.NewPeriod(amt, elapsedTime+length-types.GetTotalVestingPeriodLength(newPeriods))
previousPeriod := types.NewPeriod(period.Amount, period.Length-newPeriod.Length)
newPeriods = append(newPeriods, newPeriod, previousPeriod)
appendRemaining = true
}
}
vacc.VestingPeriods = newPeriods
}
k.accountKeeper.SetAccount(ctx, vacc)
return
}