mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-25 07:45:18 +00:00
Add liquid staking reward redistribution via incentive (#1308)
* wip Add claim * Add distr keeper and claiming * Add claim test * Update claim test with failures * wip Add staking rewards * -S Fix savings to earn incentive methods * Use a single accural time for all earn incentives * Add additional required liquid methods * Update genesis to only include 1 accrual time for earn * Revert "Update genesis to only include 1 accrual time for earn" This reverts commit cc7e35347298681c0c8a4a0b9bf9b9b296c25531. * Revert "Use a single accural time for all earn incentives" This reverts commit aeb49c4622d4e3d99dc6421c8830932b1b546be9. * Update tests with incentive distribution * Add earn to incentive rewards query * add earn cli tx * Update claim example to use ukava large * Use underlying ukava to determine proportional reward amount * Rename liquid methods to reflect derivative value * Add tests for derivative values * Return error to panic in BeginBlocker Co-authored-by: karzak <kjydavis3@gmail.com>
This commit is contained in:
parent
ac96bb9c18
commit
6ef9bab67d
@ -594,6 +594,7 @@ func NewApp(
|
|||||||
app.accountKeeper,
|
app.accountKeeper,
|
||||||
app.bankKeeper,
|
app.bankKeeper,
|
||||||
&app.stakingKeeper,
|
&app.stakingKeeper,
|
||||||
|
&app.distrKeeper,
|
||||||
)
|
)
|
||||||
savingsKeeper := savingskeeper.NewKeeper(
|
savingsKeeper := savingskeeper.NewKeeper(
|
||||||
appCodec,
|
appCodec,
|
||||||
@ -625,8 +626,7 @@ func NewApp(
|
|||||||
app.stakingKeeper,
|
app.stakingKeeper,
|
||||||
&swapKeeper,
|
&swapKeeper,
|
||||||
&savingsKeeper,
|
&savingsKeeper,
|
||||||
// TODO: Liquid keeper
|
&app.liquidKeeper,
|
||||||
nil,
|
|
||||||
&earnKeeper,
|
&earnKeeper,
|
||||||
)
|
)
|
||||||
app.routerKeeper = routerkeeper.NewKeeper(
|
app.routerKeeper = routerkeeper.NewKeeper(
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package incentive
|
package incentive
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
|
||||||
"github.com/kava-labs/kava/x/incentive/keeper"
|
"github.com/kava-labs/kava/x/incentive/keeper"
|
||||||
@ -29,6 +31,8 @@ func BeginBlocker(ctx sdk.Context, k keeper.Keeper) {
|
|||||||
k.AccumulateSavingsRewards(ctx, rp)
|
k.AccumulateSavingsRewards(ctx, rp)
|
||||||
}
|
}
|
||||||
for _, rp := range params.EarnRewardPeriods {
|
for _, rp := range params.EarnRewardPeriods {
|
||||||
k.AccumulateEarnRewards(ctx, rp)
|
if err := k.AccumulateEarnRewards(ctx, rp); err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to accumulate earn rewards: %s", err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -159,6 +159,11 @@ func queryRewardsCmd() *cobra.Command {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
earnClaims, err := executeEarnRewardsQuery(cliCtx, params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if len(hardClaims) > 0 {
|
if len(hardClaims) > 0 {
|
||||||
if err := cliCtx.PrintObjectLegacy(hardClaims); err != nil {
|
if err := cliCtx.PrintObjectLegacy(hardClaims); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -184,6 +189,11 @@ func queryRewardsCmd() *cobra.Command {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(earnClaims) > 0 {
|
||||||
|
if err := cliCtx.PrintObjectLegacy(earnClaims); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
@ -32,6 +32,7 @@ func GetTxCmd() *cobra.Command {
|
|||||||
getCmdClaimDelegator(),
|
getCmdClaimDelegator(),
|
||||||
getCmdClaimSwap(),
|
getCmdClaimSwap(),
|
||||||
getCmdClaimSavings(),
|
getCmdClaimSavings(),
|
||||||
|
getCmdClaimEarn(),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cmd := range cmds {
|
for _, cmd := range cmds {
|
||||||
@ -209,3 +210,35 @@ func getCmdClaimSavings() *cobra.Command {
|
|||||||
}
|
}
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getCmdClaimEarn() *cobra.Command {
|
||||||
|
var denomsToClaim map[string]string
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "claim-earn",
|
||||||
|
Short: "claim sender's earn rewards using given multipliers",
|
||||||
|
Long: `Claim sender's outstanding earn rewards using given multipliers`,
|
||||||
|
Example: fmt.Sprintf(` $ %s tx %s claim-earn --%s ukava=large`, version.AppName, types.ModuleName, multiplierFlag),
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
cliCtx, err := client.GetClientTxContext(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sender := cliCtx.GetFromAddress()
|
||||||
|
selections := types.NewSelectionsFromMap(denomsToClaim)
|
||||||
|
|
||||||
|
msg := types.NewMsgClaimEarnReward(sender.String(), selections)
|
||||||
|
if err := msg.ValidateBasic(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.GenerateOrBroadcastTxCLI(cliCtx, cmd.Flags(), &msg)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringToStringVarP(&denomsToClaim, multiplierFlag, multiplierFlagShort, nil, "specify the denoms to claim, each with a multiplier lockup")
|
||||||
|
if err := cmd.MarkFlagRequired(multiplierFlag); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
@ -1,23 +1,24 @@
|
|||||||
package keeper
|
package keeper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
|
||||||
earntypes "github.com/kava-labs/kava/x/earn/types"
|
earntypes "github.com/kava-labs/kava/x/earn/types"
|
||||||
"github.com/kava-labs/kava/x/incentive/types"
|
"github.com/kava-labs/kava/x/incentive/types"
|
||||||
|
|
||||||
|
distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AccumulateEarnRewards calculates new rewards to distribute this block and updates the global indexes to reflect this.
|
// AccumulateEarnRewards calculates new rewards to distribute this block and updates the global indexes to reflect this.
|
||||||
// The provided rewardPeriod must be valid to avoid panics in calculating time durations.
|
// The provided rewardPeriod must be valid to avoid panics in calculating time durations.
|
||||||
func (k Keeper) AccumulateEarnRewards(ctx sdk.Context, rewardPeriod types.MultiRewardPeriod) {
|
func (k Keeper) AccumulateEarnRewards(ctx sdk.Context, rewardPeriod types.MultiRewardPeriod) error {
|
||||||
if rewardPeriod.CollateralType == "bkava" {
|
if rewardPeriod.CollateralType == "bkava" {
|
||||||
k.accumulateEarnBkavaRewards(ctx, rewardPeriod)
|
return k.accumulateEarnBkavaRewards(ctx, rewardPeriod)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
k.accumulateEarnRewards(
|
k.accumulateEarnRewards(
|
||||||
@ -27,6 +28,8 @@ func (k Keeper) AccumulateEarnRewards(ctx sdk.Context, rewardPeriod types.MultiR
|
|||||||
rewardPeriod.End,
|
rewardPeriod.End,
|
||||||
sdk.NewDecCoinsFromCoins(rewardPeriod.RewardsPerSecond...),
|
sdk.NewDecCoinsFromCoins(rewardPeriod.RewardsPerSecond...),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetProportionalRewardsPerSecond(
|
func GetProportionalRewardsPerSecond(
|
||||||
@ -59,16 +62,13 @@ func GetProportionalRewardsPerSecond(
|
|||||||
|
|
||||||
// accumulateEarnBkavaRewards does the same as AccumulateEarnRewards but for
|
// accumulateEarnBkavaRewards does the same as AccumulateEarnRewards but for
|
||||||
// *all* bkava vaults.
|
// *all* bkava vaults.
|
||||||
func (k Keeper) accumulateEarnBkavaRewards(ctx sdk.Context, rewardPeriod types.MultiRewardPeriod) {
|
func (k Keeper) accumulateEarnBkavaRewards(ctx sdk.Context, rewardPeriod types.MultiRewardPeriod) error {
|
||||||
// TODO: Get staking rewards and distribute
|
|
||||||
|
|
||||||
// All bkava vault denoms
|
// All bkava vault denoms
|
||||||
bkavaVaultsDenoms := make(map[string]bool)
|
bkavaVaultsDenoms := make(map[string]bool)
|
||||||
|
|
||||||
// bkava vault denoms from earn records (non-empty vaults)
|
// bkava vault denoms from earn records (non-empty vaults)
|
||||||
k.earnKeeper.IterateVaultRecords(ctx, func(record earntypes.VaultRecord) (stop bool) {
|
k.earnKeeper.IterateVaultRecords(ctx, func(record earntypes.VaultRecord) (stop bool) {
|
||||||
// TODO: Replace with single bkava denom check method from liquid
|
if k.liquidKeeper.IsDerivativeDenom(ctx, record.TotalShares.Denom) {
|
||||||
if strings.HasPrefix(record.TotalShares.Denom, "bkava-") {
|
|
||||||
bkavaVaultsDenoms[record.TotalShares.Denom] = true
|
bkavaVaultsDenoms[record.TotalShares.Denom] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,14 +78,17 @@ func (k Keeper) accumulateEarnBkavaRewards(ctx sdk.Context, rewardPeriod types.M
|
|||||||
// bkava vault denoms from past incentive indexes, may include vaults
|
// bkava vault denoms from past incentive indexes, may include vaults
|
||||||
// that were fully withdrawn.
|
// that were fully withdrawn.
|
||||||
k.IterateEarnRewardIndexes(ctx, func(vaultDenom string, indexes types.RewardIndexes) (stop bool) {
|
k.IterateEarnRewardIndexes(ctx, func(vaultDenom string, indexes types.RewardIndexes) (stop bool) {
|
||||||
if strings.HasPrefix(vaultDenom, "bkava-") {
|
if k.liquidKeeper.IsDerivativeDenom(ctx, vaultDenom) {
|
||||||
bkavaVaultsDenoms[vaultDenom] = true
|
bkavaVaultsDenoms[vaultDenom] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
totalBkavaSupply := k.liquidKeeper.GetTotalDerivativeSupply(ctx)
|
totalBkavaValue, err := k.liquidKeeper.GetTotalDerivativeValue(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
i := 0
|
i := 0
|
||||||
sortedBkavaVaultsDenoms := make([]string, len(bkavaVaultsDenoms))
|
sortedBkavaVaultsDenoms := make([]string, len(bkavaVaultsDenoms))
|
||||||
@ -99,18 +102,110 @@ func (k Keeper) accumulateEarnBkavaRewards(ctx sdk.Context, rewardPeriod types.M
|
|||||||
|
|
||||||
// Accumulate rewards for each bkava vault.
|
// Accumulate rewards for each bkava vault.
|
||||||
for _, bkavaDenom := range sortedBkavaVaultsDenoms {
|
for _, bkavaDenom := range sortedBkavaVaultsDenoms {
|
||||||
k.accumulateEarnRewards(
|
derivativeValue, err := k.liquidKeeper.GetDerivativeValue(ctx, bkavaDenom)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
k.accumulateBkavaEarnRewards(
|
||||||
ctx,
|
ctx,
|
||||||
bkavaDenom,
|
bkavaDenom,
|
||||||
rewardPeriod.Start,
|
rewardPeriod.Start,
|
||||||
rewardPeriod.End,
|
rewardPeriod.End,
|
||||||
GetProportionalRewardsPerSecond(
|
GetProportionalRewardsPerSecond(
|
||||||
rewardPeriod,
|
rewardPeriod,
|
||||||
totalBkavaSupply,
|
totalBkavaValue.Amount,
|
||||||
k.liquidKeeper.GetDerivativeSupply(ctx, bkavaDenom),
|
derivativeValue.Amount,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k Keeper) accumulateBkavaEarnRewards(
|
||||||
|
ctx sdk.Context,
|
||||||
|
collateralType string,
|
||||||
|
periodStart time.Time,
|
||||||
|
periodEnd time.Time,
|
||||||
|
periodRewardsPerSecond sdk.DecCoins,
|
||||||
|
) {
|
||||||
|
// Collect staking rewards for this validator, does not have any start/end
|
||||||
|
// period time restrictions.
|
||||||
|
stakingRewards := k.collectDerivativeStakingRewards(ctx, collateralType)
|
||||||
|
|
||||||
|
// Collect incentive rewards
|
||||||
|
// **Total rewards** for vault per second, NOT per share
|
||||||
|
perSecondRewards := k.collectPerSecondRewards(
|
||||||
|
ctx,
|
||||||
|
collateralType,
|
||||||
|
periodStart,
|
||||||
|
periodEnd,
|
||||||
|
periodRewardsPerSecond,
|
||||||
|
)
|
||||||
|
|
||||||
|
// **Total rewards** for vault per second, NOT per share
|
||||||
|
rewards := stakingRewards.Add(perSecondRewards...)
|
||||||
|
|
||||||
|
// Distribute rewards by incrementing indexes
|
||||||
|
indexes, found := k.GetEarnRewardIndexes(ctx, collateralType)
|
||||||
|
if !found {
|
||||||
|
indexes = types.RewardIndexes{}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalSourceShares := k.getEarnTotalSourceShares(ctx, collateralType)
|
||||||
|
var increment types.RewardIndexes
|
||||||
|
if totalSourceShares.GT(sdk.ZeroDec()) {
|
||||||
|
// Divide total rewards by total shares to get the reward **per share**
|
||||||
|
// Leave as nil if no source shares
|
||||||
|
increment = types.NewRewardIndexesFromCoins(rewards).Quo(totalSourceShares)
|
||||||
|
}
|
||||||
|
updatedIndexes := indexes.Add(increment)
|
||||||
|
|
||||||
|
if len(updatedIndexes) > 0 {
|
||||||
|
// the store panics when setting empty or nil indexes
|
||||||
|
k.SetEarnRewardIndexes(ctx, collateralType, updatedIndexes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k Keeper) collectDerivativeStakingRewards(ctx sdk.Context, collateralType string) sdk.DecCoins {
|
||||||
|
rewards, err := k.liquidKeeper.CollectStakingRewardsByDenom(ctx, collateralType, types.IncentiveMacc)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, distrtypes.ErrNoValidatorDistInfo) &&
|
||||||
|
!errors.Is(err, distrtypes.ErrEmptyDelegationDistInfo) {
|
||||||
|
panic(fmt.Sprintf("failed to collect staking rewards for %s: %s", collateralType, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise there's no validator or delegation yet
|
||||||
|
rewards = nil
|
||||||
|
}
|
||||||
|
return sdk.NewDecCoinsFromCoins(rewards...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k Keeper) collectPerSecondRewards(
|
||||||
|
ctx sdk.Context,
|
||||||
|
collateralType string,
|
||||||
|
periodStart time.Time,
|
||||||
|
periodEnd time.Time,
|
||||||
|
periodRewardsPerSecond sdk.DecCoins,
|
||||||
|
) sdk.DecCoins {
|
||||||
|
previousAccrualTime, found := k.GetEarnRewardAccrualTime(ctx, collateralType)
|
||||||
|
if !found {
|
||||||
|
previousAccrualTime = ctx.BlockTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
rewards, accumulatedTo := types.CalculatePerSecondRewards(
|
||||||
|
periodStart,
|
||||||
|
periodEnd,
|
||||||
|
periodRewardsPerSecond,
|
||||||
|
previousAccrualTime,
|
||||||
|
ctx.BlockTime(),
|
||||||
|
)
|
||||||
|
|
||||||
|
k.SetEarnRewardAccrualTime(ctx, collateralType, accumulatedTo)
|
||||||
|
|
||||||
|
// Don't need to move funds as they're assumed to be in the IncentiveMacc module account already.
|
||||||
|
return rewards
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k Keeper) accumulateEarnRewards(
|
func (k Keeper) accumulateEarnRewards(
|
||||||
|
@ -94,13 +94,16 @@ func (suite *AccumulateEarnRewardsTests) TestStateUpdatedWhenBlockTimeHasIncreas
|
|||||||
vaultDenom1 := "bkava-meow"
|
vaultDenom1 := "bkava-meow"
|
||||||
vaultDenom2 := "bkava-woof"
|
vaultDenom2 := "bkava-woof"
|
||||||
|
|
||||||
|
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
suite.ctx = suite.ctx.WithBlockTime(previousAccrualTime)
|
||||||
|
|
||||||
earnKeeper := newFakeEarnKeeper().
|
earnKeeper := newFakeEarnKeeper().
|
||||||
addVault(vaultDenom1, earntypes.NewVaultShare(vaultDenom1, d("800000"))).
|
addVault(vaultDenom1, earntypes.NewVaultShare(vaultDenom1, d("800000"))).
|
||||||
addVault(vaultDenom2, earntypes.NewVaultShare(vaultDenom2, d("200000")))
|
addVault(vaultDenom2, earntypes.NewVaultShare(vaultDenom2, d("200000")))
|
||||||
|
|
||||||
liquidKeeper := newFakeLiquidKeeper().
|
liquidKeeper := newFakeLiquidKeeper().
|
||||||
addDerivative(vaultDenom1, i(800000)).
|
addDerivative(suite.ctx, vaultDenom1, i(800000)).
|
||||||
addDerivative(vaultDenom2, i(200000))
|
addDerivative(suite.ctx, vaultDenom2, i(200000))
|
||||||
|
|
||||||
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, nil, nil, liquidKeeper, earnKeeper)
|
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, nil, nil, liquidKeeper, earnKeeper)
|
||||||
|
|
||||||
@ -134,7 +137,6 @@ func (suite *AccumulateEarnRewardsTests) TestStateUpdatedWhenBlockTimeHasIncreas
|
|||||||
}
|
}
|
||||||
|
|
||||||
suite.storeGlobalEarnIndexes(globalIndexes)
|
suite.storeGlobalEarnIndexes(globalIndexes)
|
||||||
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
||||||
suite.keeper.SetEarnRewardAccrualTime(suite.ctx, vaultDenom1, previousAccrualTime)
|
suite.keeper.SetEarnRewardAccrualTime(suite.ctx, vaultDenom1, previousAccrualTime)
|
||||||
suite.keeper.SetEarnRewardAccrualTime(suite.ctx, vaultDenom2, previousAccrualTime)
|
suite.keeper.SetEarnRewardAccrualTime(suite.ctx, vaultDenom2, previousAccrualTime)
|
||||||
|
|
||||||
@ -164,7 +166,8 @@ func (suite *AccumulateEarnRewardsTests) TestStateUpdatedWhenBlockTimeHasIncreas
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
CollateralType: "ukava",
|
CollateralType: "ukava",
|
||||||
RewardFactor: d("3.64"),
|
RewardFactor: d("3.64"). // base incentive
|
||||||
|
Add(d("360")), // staking rewards, 10% of total bkava per second
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,16 +182,20 @@ func (suite *AccumulateEarnRewardsTests) TestStateUpdatedWhenBlockTimeHasIncreas
|
|||||||
vaultDenom1Supply := i(800000)
|
vaultDenom1Supply := i(800000)
|
||||||
vaultDenom2Supply := i(200000)
|
vaultDenom2Supply := i(200000)
|
||||||
|
|
||||||
liquidKeeper := newFakeLiquidKeeper().
|
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
addDerivative(vaultDenom1, vaultDenom1Supply).
|
suite.ctx = suite.ctx.WithBlockTime(previousAccrualTime)
|
||||||
addDerivative(vaultDenom2, vaultDenom2Supply)
|
|
||||||
|
|
||||||
|
liquidKeeper := newFakeLiquidKeeper().
|
||||||
|
addDerivative(suite.ctx, vaultDenom1, vaultDenom1Supply).
|
||||||
|
addDerivative(suite.ctx, vaultDenom2, vaultDenom2Supply)
|
||||||
|
|
||||||
|
vault1Shares := d("700000")
|
||||||
vault2Shares := d("100000")
|
vault2Shares := d("100000")
|
||||||
|
|
||||||
// More bkava minted than deposited into earn
|
// More bkava minted than deposited into earn
|
||||||
// Rewards are higher per-share as a result
|
// Rewards are higher per-share as a result
|
||||||
earnKeeper := newFakeEarnKeeper().
|
earnKeeper := newFakeEarnKeeper().
|
||||||
addVault(vaultDenom1, earntypes.NewVaultShare(vaultDenom1, d("700000"))).
|
addVault(vaultDenom1, earntypes.NewVaultShare(vaultDenom1, vault1Shares)).
|
||||||
addVault(vaultDenom2, earntypes.NewVaultShare(vaultDenom2, vault2Shares))
|
addVault(vaultDenom2, earntypes.NewVaultShare(vaultDenom2, vault2Shares))
|
||||||
|
|
||||||
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, nil, nil, liquidKeeper, earnKeeper)
|
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, nil, nil, liquidKeeper, earnKeeper)
|
||||||
@ -223,7 +230,7 @@ func (suite *AccumulateEarnRewardsTests) TestStateUpdatedWhenBlockTimeHasIncreas
|
|||||||
}
|
}
|
||||||
|
|
||||||
suite.storeGlobalEarnIndexes(globalIndexes)
|
suite.storeGlobalEarnIndexes(globalIndexes)
|
||||||
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
||||||
suite.keeper.SetEarnRewardAccrualTime(suite.ctx, vaultDenom1, previousAccrualTime)
|
suite.keeper.SetEarnRewardAccrualTime(suite.ctx, vaultDenom1, previousAccrualTime)
|
||||||
suite.keeper.SetEarnRewardAccrualTime(suite.ctx, vaultDenom2, previousAccrualTime)
|
suite.keeper.SetEarnRewardAccrualTime(suite.ctx, vaultDenom2, previousAccrualTime)
|
||||||
|
|
||||||
@ -252,7 +259,12 @@ func (suite *AccumulateEarnRewardsTests) TestStateUpdatedWhenBlockTimeHasIncreas
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
CollateralType: "ukava",
|
CollateralType: "ukava",
|
||||||
RewardFactor: d("4.154285714285714286"),
|
RewardFactor: d("4.154285714285714286"). // base incentive
|
||||||
|
Add(vaultDenom1Supply.ToDec(). // staking rewards
|
||||||
|
QuoInt64(10).
|
||||||
|
MulInt64(3600).
|
||||||
|
Quo(vault1Shares),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -278,27 +290,15 @@ func (suite *AccumulateEarnRewardsTests) TestStateUpdatedWhenBlockTimeHasIncreas
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
CollateralType: "ukava",
|
CollateralType: "ukava",
|
||||||
RewardFactor: d("7.24"),
|
RewardFactor: d("7.24").
|
||||||
|
Add(vaultDenom2Supply.ToDec().
|
||||||
|
QuoInt64(10).
|
||||||
|
MulInt64(3600).
|
||||||
|
Quo(vault2Shares),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
suite.storedIndexesEqual(vaultDenom2, vault2expectedIndexes)
|
suite.storedIndexesEqual(vaultDenom2, vault2expectedIndexes)
|
||||||
|
|
||||||
// Verify math described above
|
|
||||||
totalVault2DistributedUkava := i(int64(time.Hour.Seconds())).
|
|
||||||
ToDec().
|
|
||||||
Mul(rewardPeriod.RewardsPerSecond.AmountOf("ukava").ToDec()).
|
|
||||||
// 20% of total rewards
|
|
||||||
// vault 2 supply / (vault 1 supply + vault 2 supply)
|
|
||||||
Mul(
|
|
||||||
vaultDenom2Supply.ToDec().
|
|
||||||
Quo(vaultDenom1Supply.Add(vaultDenom2Supply).ToDec()),
|
|
||||||
)
|
|
||||||
|
|
||||||
totalVault2ClaimableRewards := vault2expectedIndexes[1].
|
|
||||||
RewardFactor.Sub(d("0.04")). // Rewards per share for 1 hr, excluding the starting value
|
|
||||||
Mul(vault2Shares) // * Shares in vault to get total rewards for entire vault
|
|
||||||
|
|
||||||
suite.Equal(totalVault2DistributedUkava, totalVault2ClaimableRewards)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *AccumulateEarnRewardsTests) TestStateUnchangedWhenBlockTimeHasNotIncreased() {
|
func (suite *AccumulateEarnRewardsTests) TestStateUnchangedWhenBlockTimeHasNotIncreased() {
|
||||||
@ -350,13 +350,16 @@ func (suite *AccumulateEarnRewardsTests) TestStateUnchangedWhenBlockTimeHasNotIn
|
|||||||
vaultDenom1 := "bkava-meow"
|
vaultDenom1 := "bkava-meow"
|
||||||
vaultDenom2 := "bkava-woof"
|
vaultDenom2 := "bkava-woof"
|
||||||
|
|
||||||
|
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
suite.ctx = suite.ctx.WithBlockTime(previousAccrualTime)
|
||||||
|
|
||||||
earnKeeper := newFakeEarnKeeper().
|
earnKeeper := newFakeEarnKeeper().
|
||||||
addVault(vaultDenom1, earntypes.NewVaultShare(vaultDenom1, d("1000000"))).
|
addVault(vaultDenom1, earntypes.NewVaultShare(vaultDenom1, d("1000000"))).
|
||||||
addVault(vaultDenom2, earntypes.NewVaultShare(vaultDenom2, d("1000000")))
|
addVault(vaultDenom2, earntypes.NewVaultShare(vaultDenom2, d("1000000")))
|
||||||
|
|
||||||
liquidKeeper := newFakeLiquidKeeper().
|
liquidKeeper := newFakeLiquidKeeper().
|
||||||
addDerivative(vaultDenom1, i(1000000)).
|
addDerivative(suite.ctx, vaultDenom1, i(1000000)).
|
||||||
addDerivative(vaultDenom2, i(1000000))
|
addDerivative(suite.ctx, vaultDenom2, i(1000000))
|
||||||
|
|
||||||
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, nil, nil, liquidKeeper, earnKeeper)
|
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, nil, nil, liquidKeeper, earnKeeper)
|
||||||
|
|
||||||
@ -389,12 +392,10 @@ func (suite *AccumulateEarnRewardsTests) TestStateUnchangedWhenBlockTimeHasNotIn
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
suite.storeGlobalEarnIndexes(previousIndexes)
|
suite.storeGlobalEarnIndexes(previousIndexes)
|
||||||
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
||||||
suite.keeper.SetEarnRewardAccrualTime(suite.ctx, vaultDenom1, previousAccrualTime)
|
suite.keeper.SetEarnRewardAccrualTime(suite.ctx, vaultDenom1, previousAccrualTime)
|
||||||
suite.keeper.SetEarnRewardAccrualTime(suite.ctx, vaultDenom2, previousAccrualTime)
|
suite.keeper.SetEarnRewardAccrualTime(suite.ctx, vaultDenom2, previousAccrualTime)
|
||||||
|
|
||||||
suite.ctx = suite.ctx.WithBlockTime(previousAccrualTime)
|
|
||||||
|
|
||||||
period := types.NewMultiRewardPeriod(
|
period := types.NewMultiRewardPeriod(
|
||||||
true,
|
true,
|
||||||
"bkava",
|
"bkava",
|
||||||
@ -585,13 +586,16 @@ func (suite *AccumulateEarnRewardsTests) TestStateAddedWhenStateDoesNotExist_bka
|
|||||||
vaultDenom1 := "bkava-meow"
|
vaultDenom1 := "bkava-meow"
|
||||||
vaultDenom2 := "bkava-woof"
|
vaultDenom2 := "bkava-woof"
|
||||||
|
|
||||||
|
firstAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime)
|
||||||
|
|
||||||
earnKeeper := newFakeEarnKeeper().
|
earnKeeper := newFakeEarnKeeper().
|
||||||
addVault(vaultDenom1, earntypes.NewVaultShare(vaultDenom1, d("1000000"))).
|
addVault(vaultDenom1, earntypes.NewVaultShare(vaultDenom1, d("1000000"))).
|
||||||
addVault(vaultDenom2, earntypes.NewVaultShare(vaultDenom2, d("1000000")))
|
addVault(vaultDenom2, earntypes.NewVaultShare(vaultDenom2, d("1000000")))
|
||||||
|
|
||||||
liquidKeeper := newFakeLiquidKeeper().
|
liquidKeeper := newFakeLiquidKeeper().
|
||||||
addDerivative(vaultDenom1, i(1000000)).
|
addDerivative(suite.ctx, vaultDenom1, i(1000000)).
|
||||||
addDerivative(vaultDenom2, i(1000000))
|
addDerivative(suite.ctx, vaultDenom2, i(1000000))
|
||||||
|
|
||||||
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, nil, nil, liquidKeeper, earnKeeper)
|
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, nil, nil, liquidKeeper, earnKeeper)
|
||||||
|
|
||||||
@ -603,9 +607,6 @@ func (suite *AccumulateEarnRewardsTests) TestStateAddedWhenStateDoesNotExist_bka
|
|||||||
cs(c("earn", 2000), c("ukava", 1000)),
|
cs(c("earn", 2000), c("ukava", 1000)),
|
||||||
)
|
)
|
||||||
|
|
||||||
firstAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
||||||
suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime)
|
|
||||||
|
|
||||||
suite.keeper.AccumulateEarnRewards(suite.ctx, period)
|
suite.keeper.AccumulateEarnRewards(suite.ctx, period)
|
||||||
|
|
||||||
// After the first accumulation only the current block time should be stored.
|
// After the first accumulation only the current block time should be stored.
|
||||||
@ -632,7 +633,9 @@ func (suite *AccumulateEarnRewardsTests) TestStateAddedWhenStateDoesNotExist_bka
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
CollateralType: "ukava",
|
CollateralType: "ukava",
|
||||||
RewardFactor: d("0.005"),
|
// 10% of total bkava for rewards per second for 10 seconds
|
||||||
|
// 1ukava per share per second + regular 0.005ukava incentive rewards
|
||||||
|
RewardFactor: d("1.005"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
103
x/incentive/keeper/rewards_earn_staking_test.go
Normal file
103
x/incentive/keeper/rewards_earn_staking_test.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
earntypes "github.com/kava-labs/kava/x/earn/types"
|
||||||
|
"github.com/kava-labs/kava/x/incentive/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (suite *AccumulateEarnRewardsTests) TestStakingRewardsDistributed() {
|
||||||
|
vaultDenom1 := "bkava-meow"
|
||||||
|
vaultDenom2 := "bkava-woof"
|
||||||
|
|
||||||
|
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
suite.ctx = suite.ctx.WithBlockTime(previousAccrualTime)
|
||||||
|
|
||||||
|
vaultDenom1Supply := i(800000)
|
||||||
|
vaultDenom2Supply := i(200000)
|
||||||
|
|
||||||
|
liquidKeeper := newFakeLiquidKeeper().
|
||||||
|
addDerivative(suite.ctx, vaultDenom1, vaultDenom1Supply).
|
||||||
|
addDerivative(suite.ctx, vaultDenom2, vaultDenom2Supply)
|
||||||
|
|
||||||
|
vault1Shares := d("700000")
|
||||||
|
vault2Shares := d("100000")
|
||||||
|
|
||||||
|
// More bkava minted than deposited into earn
|
||||||
|
// Rewards are higher per-share as a result
|
||||||
|
earnKeeper := newFakeEarnKeeper().
|
||||||
|
addVault(vaultDenom1, earntypes.NewVaultShare(vaultDenom1, vault1Shares)).
|
||||||
|
addVault(vaultDenom2, earntypes.NewVaultShare(vaultDenom2, vault2Shares))
|
||||||
|
|
||||||
|
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, nil, nil, liquidKeeper, earnKeeper)
|
||||||
|
|
||||||
|
initialVault1RewardFactor := d("0.04")
|
||||||
|
initialVault2RewardFactor := d("0.04")
|
||||||
|
|
||||||
|
globalIndexes := types.MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: vaultDenom1,
|
||||||
|
RewardIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "ukava",
|
||||||
|
RewardFactor: initialVault1RewardFactor,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CollateralType: vaultDenom2,
|
||||||
|
RewardIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "ukava",
|
||||||
|
RewardFactor: initialVault2RewardFactor,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.storeGlobalEarnIndexes(globalIndexes)
|
||||||
|
|
||||||
|
suite.keeper.SetEarnRewardAccrualTime(suite.ctx, vaultDenom1, previousAccrualTime)
|
||||||
|
suite.keeper.SetEarnRewardAccrualTime(suite.ctx, vaultDenom2, previousAccrualTime)
|
||||||
|
|
||||||
|
newAccrualTime := previousAccrualTime.Add(1 * time.Hour)
|
||||||
|
suite.ctx = suite.ctx.WithBlockTime(newAccrualTime)
|
||||||
|
|
||||||
|
rewardPeriod := types.NewMultiRewardPeriod(
|
||||||
|
true,
|
||||||
|
"bkava", // reward period is set for "bkava" to apply to all vaults
|
||||||
|
time.Unix(0, 0), // ensure the test is within start and end times
|
||||||
|
distantFuture,
|
||||||
|
cs(), // no incentives, so only the staking rewards are distributed
|
||||||
|
)
|
||||||
|
suite.keeper.AccumulateEarnRewards(suite.ctx, rewardPeriod)
|
||||||
|
|
||||||
|
// check time and factors
|
||||||
|
|
||||||
|
suite.storedTimeEquals(vaultDenom1, newAccrualTime)
|
||||||
|
suite.storedTimeEquals(vaultDenom2, newAccrualTime)
|
||||||
|
|
||||||
|
// Only contains staking rewards
|
||||||
|
suite.storedIndexesEqual(vaultDenom1, types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "ukava",
|
||||||
|
RewardFactor: initialVault1RewardFactor.
|
||||||
|
Add(vaultDenom1Supply.ToDec().
|
||||||
|
QuoInt64(10).
|
||||||
|
MulInt64(3600).
|
||||||
|
Quo(vault1Shares)),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.storedIndexesEqual(vaultDenom2, types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "ukava",
|
||||||
|
RewardFactor: initialVault2RewardFactor.
|
||||||
|
Add(vaultDenom2Supply.ToDec().
|
||||||
|
QuoInt64(10).
|
||||||
|
MulInt64(3600).
|
||||||
|
Quo(vault2Shares)),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
@ -432,19 +432,26 @@ func (k *fakeEarnKeeper) IterateVaultRecords(
|
|||||||
// fakeLiquidKeeper is a stub liquid keeper.
|
// fakeLiquidKeeper is a stub liquid keeper.
|
||||||
// It can be used to return values to the incentive keeper without having to initialize a full liquid keeper.
|
// It can be used to return values to the incentive keeper without having to initialize a full liquid keeper.
|
||||||
type fakeLiquidKeeper struct {
|
type fakeLiquidKeeper struct {
|
||||||
derivatives map[string]sdk.Int
|
derivatives map[string]sdk.Int
|
||||||
|
lastRewardClaim map[string]time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.LiquidKeeper = newFakeLiquidKeeper()
|
var _ types.LiquidKeeper = newFakeLiquidKeeper()
|
||||||
|
|
||||||
func newFakeLiquidKeeper() *fakeLiquidKeeper {
|
func newFakeLiquidKeeper() *fakeLiquidKeeper {
|
||||||
return &fakeLiquidKeeper{
|
return &fakeLiquidKeeper{
|
||||||
derivatives: map[string]sdk.Int{},
|
derivatives: map[string]sdk.Int{},
|
||||||
|
lastRewardClaim: map[string]time.Time{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *fakeLiquidKeeper) addDerivative(denom string, supply sdk.Int) *fakeLiquidKeeper {
|
func (k *fakeLiquidKeeper) addDerivative(
|
||||||
|
ctx sdk.Context,
|
||||||
|
denom string,
|
||||||
|
supply sdk.Int,
|
||||||
|
) *fakeLiquidKeeper {
|
||||||
k.derivatives[denom] = supply
|
k.derivatives[denom] = supply
|
||||||
|
k.lastRewardClaim[denom] = ctx.BlockTime()
|
||||||
return k
|
return k
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -460,22 +467,56 @@ func (k *fakeLiquidKeeper) GetAllDerivativeDenoms(ctx sdk.Context) (denoms []str
|
|||||||
return denoms
|
return denoms
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *fakeLiquidKeeper) GetTotalDerivativeSupply(ctx sdk.Context) sdk.Int {
|
func (k *fakeLiquidKeeper) GetTotalDerivativeValue(ctx sdk.Context) (sdk.Coin, error) {
|
||||||
totalSupply := sdk.ZeroInt()
|
totalSupply := sdk.ZeroInt()
|
||||||
for _, supply := range k.derivatives {
|
for _, supply := range k.derivatives {
|
||||||
totalSupply = totalSupply.Add(supply)
|
totalSupply = totalSupply.Add(supply)
|
||||||
}
|
}
|
||||||
|
|
||||||
return totalSupply
|
return sdk.NewCoin("ukava", totalSupply), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *fakeLiquidKeeper) GetDerivativeSupply(ctx sdk.Context, denom string) sdk.Int {
|
func (k *fakeLiquidKeeper) GetDerivativeValue(ctx sdk.Context, denom string) (sdk.Coin, error) {
|
||||||
supply, found := k.derivatives[denom]
|
supply, found := k.derivatives[denom]
|
||||||
if !found {
|
if !found {
|
||||||
|
return sdk.NewCoin("ukava", sdk.ZeroInt()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return sdk.NewCoin("ukava", supply), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *fakeLiquidKeeper) CollectStakingRewardsByDenom(
|
||||||
|
ctx sdk.Context,
|
||||||
|
derivativeDenom string,
|
||||||
|
destinationModAccount string,
|
||||||
|
) (sdk.Coins, error) {
|
||||||
|
amt := k.getRewardAmount(ctx, derivativeDenom)
|
||||||
|
|
||||||
|
return sdk.NewCoins(sdk.NewCoin("ukava", amt)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *fakeLiquidKeeper) getRewardAmount(
|
||||||
|
ctx sdk.Context,
|
||||||
|
derivativeDenom string,
|
||||||
|
) sdk.Int {
|
||||||
|
amt, found := k.derivatives[derivativeDenom]
|
||||||
|
if !found {
|
||||||
|
// No error
|
||||||
return sdk.ZeroInt()
|
return sdk.ZeroInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
return supply
|
lastRewardClaim, found := k.lastRewardClaim[derivativeDenom]
|
||||||
|
if !found {
|
||||||
|
panic("last reward claim not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := int64(ctx.BlockTime().Sub(lastRewardClaim).Seconds())
|
||||||
|
if duration <= 0 {
|
||||||
|
return sdk.ZeroInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reward amount just set to 10% of the derivative supply per second
|
||||||
|
return amt.QuoRaw(10).MulRaw(duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assorted Testing Data
|
// Assorted Testing Data
|
||||||
|
@ -88,7 +88,7 @@ func (*Accumulator) calculateNewRewards(rewardsPerSecond sdk.DecCoins, totalSour
|
|||||||
// So return an empty increment instead of one full of zeros.
|
// So return an empty increment instead of one full of zeros.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
increment := newRewardIndexesFromCoins(rewardsPerSecond)
|
increment := NewRewardIndexesFromCoins(rewardsPerSecond)
|
||||||
increment = increment.Mul(sdk.NewDec(durationSeconds)).Quo(totalSourceShares)
|
increment = increment.Mul(sdk.NewDec(durationSeconds)).Quo(totalSourceShares)
|
||||||
return increment
|
return increment
|
||||||
}
|
}
|
||||||
@ -109,11 +109,36 @@ func maxTime(t1, t2 time.Time) time.Time {
|
|||||||
return t1
|
return t1
|
||||||
}
|
}
|
||||||
|
|
||||||
// newRewardIndexesFromCoins is a helper function to initialize a RewardIndexes slice with the values from a Coins slice.
|
// NewRewardIndexesFromCoins is a helper function to initialize a RewardIndexes slice with the values from a Coins slice.
|
||||||
func newRewardIndexesFromCoins(coins sdk.DecCoins) RewardIndexes {
|
func NewRewardIndexesFromCoins(coins sdk.DecCoins) RewardIndexes {
|
||||||
var indexes RewardIndexes
|
var indexes RewardIndexes
|
||||||
for _, coin := range coins {
|
for _, coin := range coins {
|
||||||
indexes = append(indexes, NewRewardIndex(coin.Denom, coin.Amount))
|
indexes = append(indexes, NewRewardIndex(coin.Denom, coin.Amount))
|
||||||
}
|
}
|
||||||
return indexes
|
return indexes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CalculatePerSecondRewards(
|
||||||
|
periodStart time.Time,
|
||||||
|
periodEnd time.Time,
|
||||||
|
periodRewardsPerSecond sdk.DecCoins,
|
||||||
|
previousTime, currentTime time.Time,
|
||||||
|
) (sdk.DecCoins, time.Time) {
|
||||||
|
duration := (&Accumulator{}).getTimeElapsedWithinLimits(
|
||||||
|
previousTime,
|
||||||
|
currentTime,
|
||||||
|
periodStart,
|
||||||
|
periodEnd,
|
||||||
|
)
|
||||||
|
|
||||||
|
upTo := minTime(periodEnd, currentTime)
|
||||||
|
|
||||||
|
durationSeconds := int64(math.RoundToEven(duration.Seconds()))
|
||||||
|
if durationSeconds <= 0 {
|
||||||
|
// If the duration is zero, there will be no increment.
|
||||||
|
// So return an empty increment instead of one full of zeros.
|
||||||
|
return nil, upTo // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
return periodRewardsPerSecond.MulDec(sdk.NewDec(durationSeconds)), upTo
|
||||||
|
}
|
||||||
|
@ -74,8 +74,13 @@ type EarnKeeper interface {
|
|||||||
// LiquidKeeper defines the required methods needed by this modules keeper
|
// LiquidKeeper defines the required methods needed by this modules keeper
|
||||||
type LiquidKeeper interface {
|
type LiquidKeeper interface {
|
||||||
IsDerivativeDenom(ctx sdk.Context, denom string) bool
|
IsDerivativeDenom(ctx sdk.Context, denom string) bool
|
||||||
GetTotalDerivativeSupply(ctx sdk.Context) sdk.Int
|
GetTotalDerivativeValue(ctx sdk.Context) (sdk.Coin, error)
|
||||||
GetDerivativeSupply(ctx sdk.Context, denom string) sdk.Int
|
GetDerivativeValue(ctx sdk.Context, denom string) (sdk.Coin, error)
|
||||||
|
CollectStakingRewardsByDenom(
|
||||||
|
ctx sdk.Context,
|
||||||
|
derivativeDenom string,
|
||||||
|
destinationModAccount string,
|
||||||
|
) (sdk.Coins, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountKeeper expected interface for the account keeper (noalias)
|
// AccountKeeper expected interface for the account keeper (noalias)
|
||||||
|
51
x/liquid/keeper/claim.go
Normal file
51
x/liquid/keeper/claim.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package keeper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/liquid/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (k Keeper) CollectStakingRewards(
|
||||||
|
ctx sdk.Context,
|
||||||
|
validator sdk.ValAddress,
|
||||||
|
destinationModAccount string,
|
||||||
|
) (sdk.Coins, error) {
|
||||||
|
macc := k.accountKeeper.GetModuleAccount(ctx, types.ModuleAccountName)
|
||||||
|
|
||||||
|
// Ensure withdraw address is as expected
|
||||||
|
withdrawAddr := k.distributionKeeper.GetDelegatorWithdrawAddr(ctx, macc.GetAddress())
|
||||||
|
if !withdrawAddr.Equals(macc.GetAddress()) {
|
||||||
|
panic(fmt.Sprintf(
|
||||||
|
"unexpected withdraw address for liquid staking module account, expected %s, got %s",
|
||||||
|
macc.GetAddress(), withdrawAddr,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
rewards, err := k.distributionKeeper.WithdrawDelegationRewards(ctx, macc.GetAddress(), validator)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleAccountName, destinationModAccount, rewards)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rewards, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k Keeper) CollectStakingRewardsByDenom(
|
||||||
|
ctx sdk.Context,
|
||||||
|
derivativeDenom string,
|
||||||
|
destinationModAccount string,
|
||||||
|
) (sdk.Coins, error) {
|
||||||
|
valAddr, err := types.ParseLiquidStakingTokenDenom(derivativeDenom)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return k.CollectStakingRewards(ctx, valAddr, destinationModAccount)
|
||||||
|
}
|
88
x/liquid/keeper/claim_test.go
Normal file
88
x/liquid/keeper/claim_test.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/staking"
|
||||||
|
"github.com/kava-labs/kava/app"
|
||||||
|
"github.com/kava-labs/kava/x/liquid/types"
|
||||||
|
|
||||||
|
distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (suite *KeeperTestSuite) TestCollectStakingRewards() {
|
||||||
|
_, addrs := app.GeneratePrivKeyAddressPairs(5)
|
||||||
|
valAccAddr1, delegator := addrs[0], addrs[1]
|
||||||
|
valAddr1 := sdk.ValAddress(valAccAddr1)
|
||||||
|
|
||||||
|
initialBalance := i(1e9)
|
||||||
|
delegateAmount := i(100e6)
|
||||||
|
|
||||||
|
suite.NoError(suite.App.FundModuleAccount(
|
||||||
|
suite.Ctx,
|
||||||
|
distrtypes.ModuleName,
|
||||||
|
sdk.NewCoins(
|
||||||
|
sdk.NewCoin("ukava", initialBalance),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
|
||||||
|
suite.CreateAccountWithAddress(valAccAddr1, suite.NewBondCoins(initialBalance))
|
||||||
|
suite.CreateAccountWithAddress(delegator, suite.NewBondCoins(initialBalance))
|
||||||
|
|
||||||
|
suite.CreateNewUnbondedValidator(valAddr1, initialBalance)
|
||||||
|
suite.CreateDelegation(valAddr1, delegator, delegateAmount)
|
||||||
|
staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
|
||||||
|
|
||||||
|
// Transfers delegation to module account
|
||||||
|
_, err := suite.Keeper.MintDerivative(suite.Ctx, delegator, valAddr1, suite.NewBondCoin(delegateAmount))
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
validator, found := suite.StakingKeeper.GetValidator(suite.Ctx, valAddr1)
|
||||||
|
suite.Require().True(found)
|
||||||
|
|
||||||
|
suite.Ctx = suite.Ctx.WithBlockHeight(2)
|
||||||
|
|
||||||
|
distrKeeper := suite.App.GetDistrKeeper()
|
||||||
|
stakingKeeper := suite.App.GetStakingKeeper()
|
||||||
|
accKeeper := suite.App.GetAccountKeeper()
|
||||||
|
liquidMacc := accKeeper.GetModuleAccount(suite.Ctx, types.ModuleAccountName)
|
||||||
|
|
||||||
|
// Add rewards
|
||||||
|
rewardCoins := sdk.NewDecCoins(sdk.NewDecCoin("ukava", sdk.NewInt(500e6)))
|
||||||
|
distrKeeper.AllocateTokensToValidator(suite.Ctx, validator, rewardCoins)
|
||||||
|
|
||||||
|
delegation, found := stakingKeeper.GetDelegation(suite.Ctx, liquidMacc.GetAddress(), valAddr1)
|
||||||
|
suite.Require().True(found)
|
||||||
|
|
||||||
|
// Get amount of rewards
|
||||||
|
endingPeriod := distrKeeper.IncrementValidatorPeriod(suite.Ctx, validator)
|
||||||
|
delegationRewards := distrKeeper.CalculateDelegationRewards(suite.Ctx, validator, delegation, endingPeriod)
|
||||||
|
truncatedRewards, _ := delegationRewards.TruncateDecimal()
|
||||||
|
|
||||||
|
suite.Run("collect staking rewards", func() {
|
||||||
|
// Collect rewards
|
||||||
|
derivativeDenom := suite.Keeper.GetLiquidStakingTokenDenom(valAddr1)
|
||||||
|
rewards, err := suite.Keeper.CollectStakingRewardsByDenom(suite.Ctx, derivativeDenom, types.ModuleName)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
suite.Require().Equal(truncatedRewards, rewards)
|
||||||
|
|
||||||
|
suite.True(rewards.AmountOf("ukava").IsPositive())
|
||||||
|
|
||||||
|
// Check balances
|
||||||
|
suite.AccountBalanceEqual(liquidMacc.GetAddress(), rewards)
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.Run("collect staking rewards with non-validator", func() {
|
||||||
|
// acc2 not a validator
|
||||||
|
derivativeDenom := suite.Keeper.GetLiquidStakingTokenDenom(sdk.ValAddress(addrs[2]))
|
||||||
|
_, err := suite.Keeper.CollectStakingRewardsByDenom(suite.Ctx, derivativeDenom, types.ModuleName)
|
||||||
|
suite.Require().Error(err)
|
||||||
|
suite.Require().Equal("no validator distribution info", err.Error())
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.Run("collect staking rewards with invalid denom", func() {
|
||||||
|
derivativeDenom := "bkava"
|
||||||
|
_, err := suite.Keeper.CollectStakingRewardsByDenom(suite.Ctx, derivativeDenom, types.ModuleName)
|
||||||
|
suite.Require().Error(err)
|
||||||
|
suite.Require().Equal("cannot parse denom bkava", err.Error())
|
||||||
|
})
|
||||||
|
}
|
@ -136,6 +136,28 @@ func (k Keeper) GetStakedTokensForDerivatives(ctx sdk.Context, coins sdk.Coins)
|
|||||||
return totalCoin, nil
|
return totalCoin, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTotalDerivativeValue returns the total sum value of all derivative coins
|
||||||
|
// for all validators denominated by the bond token (ukava).
|
||||||
|
func (k Keeper) GetTotalDerivativeValue(ctx sdk.Context) (sdk.Coin, error) {
|
||||||
|
bkavaCoins := sdk.NewCoins()
|
||||||
|
|
||||||
|
k.bankKeeper.IterateTotalSupply(ctx, func(c sdk.Coin) bool {
|
||||||
|
if k.IsDerivativeDenom(ctx, c.Denom) {
|
||||||
|
bkavaCoins = bkavaCoins.Add(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
return k.GetStakedTokensForDerivatives(ctx, bkavaCoins)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDerivativeValue returns the total underlying value of the provided
|
||||||
|
// derivative denominated by the bond token (ukava).
|
||||||
|
func (k Keeper) GetDerivativeValue(ctx sdk.Context, denom string) (sdk.Coin, error) {
|
||||||
|
return k.GetStakedTokensForDerivatives(ctx, sdk.NewCoins(k.bankKeeper.GetSupply(ctx, denom)))
|
||||||
|
}
|
||||||
|
|
||||||
func (k Keeper) mintCoins(ctx sdk.Context, receiver sdk.AccAddress, amount sdk.Coins) error {
|
func (k Keeper) mintCoins(ctx sdk.Context, receiver sdk.AccAddress, amount sdk.Coins) error {
|
||||||
if err := k.bankKeeper.MintCoins(ctx, types.ModuleAccountName, amount); err != nil {
|
if err := k.bankKeeper.MintCoins(ctx, types.ModuleAccountName, amount); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -470,6 +470,61 @@ func (suite *KeeperTestSuite) TestGetStakedTokensForDerivatives() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *KeeperTestSuite) TestGetDerivativeValue() {
|
||||||
|
_, addrs := app.GeneratePrivKeyAddressPairs(5)
|
||||||
|
valAccAddr1, delegator, valAccAddr2 := addrs[0], addrs[1], addrs[2]
|
||||||
|
valAddr1 := sdk.ValAddress(valAccAddr1)
|
||||||
|
|
||||||
|
valAddr2 := sdk.ValAddress(valAccAddr2)
|
||||||
|
|
||||||
|
initialBalance := i(1e9)
|
||||||
|
vestedBalance := i(500e6)
|
||||||
|
delegateAmount := i(100e6)
|
||||||
|
|
||||||
|
suite.CreateAccountWithAddress(valAccAddr1, suite.NewBondCoins(initialBalance))
|
||||||
|
suite.CreateVestingAccountWithAddress(delegator, suite.NewBondCoins(initialBalance), suite.NewBondCoins(vestedBalance))
|
||||||
|
|
||||||
|
suite.CreateNewUnbondedValidator(valAddr1, initialBalance)
|
||||||
|
suite.CreateDelegation(valAddr1, delegator, delegateAmount)
|
||||||
|
|
||||||
|
suite.CreateAccountWithAddress(valAccAddr2, suite.NewBondCoins(initialBalance))
|
||||||
|
|
||||||
|
suite.CreateNewUnbondedValidator(valAddr2, initialBalance)
|
||||||
|
suite.CreateDelegation(valAddr2, delegator, delegateAmount)
|
||||||
|
staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
|
||||||
|
|
||||||
|
_, err := suite.Keeper.MintDerivative(suite.Ctx, delegator, valAddr1, suite.NewBondCoin(delegateAmount))
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
_, err = suite.Keeper.MintDerivative(suite.Ctx, delegator, valAddr2, suite.NewBondCoin(delegateAmount))
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
suite.SlashValidator(valAddr2, d("0.05"))
|
||||||
|
|
||||||
|
suite.Run("total value", func() {
|
||||||
|
totalValue, err := suite.Keeper.GetTotalDerivativeValue(suite.Ctx)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
suite.Require().Equal(
|
||||||
|
// delegateAmount + (delegateAmount * 95%)
|
||||||
|
delegateAmount.Add(delegateAmount.MulRaw(95).QuoRaw(100)),
|
||||||
|
totalValue.Amount,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.Run("1:1 derivative value", func() {
|
||||||
|
derivativeValue, err := suite.Keeper.GetDerivativeValue(suite.Ctx, suite.Keeper.GetLiquidStakingTokenDenom(valAddr1))
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
suite.Require().Equal(suite.NewBondCoin(delegateAmount), derivativeValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.Run("slashed derivative value", func() {
|
||||||
|
derivativeValue, err := suite.Keeper.GetDerivativeValue(suite.Ctx, suite.Keeper.GetLiquidStakingTokenDenom(valAddr2))
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
// delegateAmount * 95%
|
||||||
|
suite.Require().Equal(delegateAmount.MulRaw(95).QuoRaw(100), derivativeValue.Amount)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *KeeperTestSuite) TestDerivativeFromTokens() {
|
func (suite *KeeperTestSuite) TestDerivativeFromTokens() {
|
||||||
_, addrs := app.GeneratePrivKeyAddressPairs(1)
|
_, addrs := app.GeneratePrivKeyAddressPairs(1)
|
||||||
valAccAddr := addrs[0]
|
valAccAddr := addrs[0]
|
||||||
|
@ -14,9 +14,10 @@ import (
|
|||||||
type Keeper struct {
|
type Keeper struct {
|
||||||
cdc codec.Codec
|
cdc codec.Codec
|
||||||
|
|
||||||
accountKeeper types.AccountKeeper
|
accountKeeper types.AccountKeeper
|
||||||
bankKeeper types.BankKeeper
|
bankKeeper types.BankKeeper
|
||||||
stakingKeeper types.StakingKeeper
|
stakingKeeper types.StakingKeeper
|
||||||
|
distributionKeeper types.DistributionKeeper
|
||||||
|
|
||||||
derivativeDenom string
|
derivativeDenom string
|
||||||
}
|
}
|
||||||
@ -24,26 +25,27 @@ type Keeper struct {
|
|||||||
// NewKeeper returns a new keeper for the liquid module.
|
// NewKeeper returns a new keeper for the liquid module.
|
||||||
func NewKeeper(
|
func NewKeeper(
|
||||||
cdc codec.Codec,
|
cdc codec.Codec,
|
||||||
ak types.AccountKeeper, bk types.BankKeeper, sk types.StakingKeeper,
|
ak types.AccountKeeper, bk types.BankKeeper, sk types.StakingKeeper, dk types.DistributionKeeper,
|
||||||
derivativeDenom string,
|
derivativeDenom string,
|
||||||
) Keeper {
|
) Keeper {
|
||||||
|
|
||||||
return Keeper{
|
return Keeper{
|
||||||
cdc: cdc,
|
cdc: cdc,
|
||||||
accountKeeper: ak,
|
accountKeeper: ak,
|
||||||
bankKeeper: bk,
|
bankKeeper: bk,
|
||||||
stakingKeeper: sk,
|
stakingKeeper: sk,
|
||||||
derivativeDenom: derivativeDenom,
|
distributionKeeper: dk,
|
||||||
|
derivativeDenom: derivativeDenom,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDefaultKeeper returns a new keeper for the liquid module with default values.
|
// NewDefaultKeeper returns a new keeper for the liquid module with default values.
|
||||||
func NewDefaultKeeper(
|
func NewDefaultKeeper(
|
||||||
cdc codec.Codec,
|
cdc codec.Codec,
|
||||||
ak types.AccountKeeper, bk types.BankKeeper, sk types.StakingKeeper,
|
ak types.AccountKeeper, bk types.BankKeeper, sk types.StakingKeeper, dk types.DistributionKeeper,
|
||||||
) Keeper {
|
) Keeper {
|
||||||
|
|
||||||
return NewKeeper(cdc, ak, bk, sk, types.DefaultDerivativeDenom)
|
return NewKeeper(cdc, ak, bk, sk, dk, types.DefaultDerivativeDenom)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logger returns a module-specific logger.
|
// Logger returns a module-specific logger.
|
||||||
|
@ -94,7 +94,7 @@ func (suite *KeeperTestSuite) AddCoinsToModule(module string, amount sdk.Coins)
|
|||||||
// AccountBalanceEqual checks if an account has the specified coins.
|
// AccountBalanceEqual checks if an account has the specified coins.
|
||||||
func (suite *KeeperTestSuite) AccountBalanceEqual(addr sdk.AccAddress, coins sdk.Coins) {
|
func (suite *KeeperTestSuite) AccountBalanceEqual(addr sdk.AccAddress, coins sdk.Coins) {
|
||||||
balance := suite.BankKeeper.GetAllBalances(suite.Ctx, addr)
|
balance := suite.BankKeeper.GetAllBalances(suite.Ctx, addr)
|
||||||
suite.Equalf(coins, balance, "expected account balance to equal coins %s, but got %s", coins, balance)
|
suite.Truef(coins.IsEqual(balance), "expected account balance to equal coins %s, but got %s", coins, balance)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *KeeperTestSuite) deliverMsgCreateValidator(ctx sdk.Context, address sdk.ValAddress, selfDelegation sdk.Coin) error {
|
func (suite *KeeperTestSuite) deliverMsgCreateValidator(ctx sdk.Context, address sdk.ValAddress, selfDelegation sdk.Coin) error {
|
||||||
|
@ -17,6 +17,9 @@ type BankKeeper interface {
|
|||||||
MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error
|
MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error
|
||||||
BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error
|
BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error
|
||||||
UndelegateCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
|
UndelegateCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
|
||||||
|
|
||||||
|
IterateTotalSupply(ctx sdk.Context, cb func(sdk.Coin) bool)
|
||||||
|
GetSupply(ctx sdk.Context, denom string) sdk.Coin
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountKeeper defines the expected keeper interface for interacting with account
|
// AccountKeeper defines the expected keeper interface for interacting with account
|
||||||
@ -45,3 +48,8 @@ type StakingKeeper interface {
|
|||||||
ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, shares sdk.Dec,
|
ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, shares sdk.Dec,
|
||||||
) (amount sdk.Int, err error)
|
) (amount sdk.Int, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DistributionKeeper interface {
|
||||||
|
GetDelegatorWithdrawAddr(ctx sdk.Context, delAddr sdk.AccAddress) sdk.AccAddress
|
||||||
|
WithdrawDelegationRewards(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) (sdk.Coins, error)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user