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:
Derrick Lee 2022-09-28 13:20:01 -07:00 committed by GitHub
parent ac96bb9c18
commit 6ef9bab67d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 625 additions and 80 deletions

View File

@ -594,6 +594,7 @@ func NewApp(
app.accountKeeper,
app.bankKeeper,
&app.stakingKeeper,
&app.distrKeeper,
)
savingsKeeper := savingskeeper.NewKeeper(
appCodec,
@ -625,8 +626,7 @@ func NewApp(
app.stakingKeeper,
&swapKeeper,
&savingsKeeper,
// TODO: Liquid keeper
nil,
&app.liquidKeeper,
&earnKeeper,
)
app.routerKeeper = routerkeeper.NewKeeper(

View File

@ -1,6 +1,8 @@
package incentive
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/incentive/keeper"
@ -29,6 +31,8 @@ func BeginBlocker(ctx sdk.Context, k keeper.Keeper) {
k.AccumulateSavingsRewards(ctx, rp)
}
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))
}
}
}

View File

@ -159,6 +159,11 @@ func queryRewardsCmd() *cobra.Command {
if err != nil {
return err
}
earnClaims, err := executeEarnRewardsQuery(cliCtx, params)
if err != nil {
return err
}
if len(hardClaims) > 0 {
if err := cliCtx.PrintObjectLegacy(hardClaims); err != nil {
return err
@ -184,6 +189,11 @@ func queryRewardsCmd() *cobra.Command {
return err
}
}
if len(earnClaims) > 0 {
if err := cliCtx.PrintObjectLegacy(earnClaims); err != nil {
return err
}
}
}
return nil
},

View File

@ -32,6 +32,7 @@ func GetTxCmd() *cobra.Command {
getCmdClaimDelegator(),
getCmdClaimSwap(),
getCmdClaimSavings(),
getCmdClaimEarn(),
}
for _, cmd := range cmds {
@ -209,3 +210,35 @@ func getCmdClaimSavings() *cobra.Command {
}
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
}

View File

@ -1,23 +1,24 @@
package keeper
import (
"errors"
"fmt"
"sort"
"strings"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
earntypes "github.com/kava-labs/kava/x/earn/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.
// 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" {
k.accumulateEarnBkavaRewards(ctx, rewardPeriod)
return
return k.accumulateEarnBkavaRewards(ctx, rewardPeriod)
}
k.accumulateEarnRewards(
@ -27,6 +28,8 @@ func (k Keeper) AccumulateEarnRewards(ctx sdk.Context, rewardPeriod types.MultiR
rewardPeriod.End,
sdk.NewDecCoinsFromCoins(rewardPeriod.RewardsPerSecond...),
)
return nil
}
func GetProportionalRewardsPerSecond(
@ -59,16 +62,13 @@ func GetProportionalRewardsPerSecond(
// accumulateEarnBkavaRewards does the same as AccumulateEarnRewards but for
// *all* bkava vaults.
func (k Keeper) accumulateEarnBkavaRewards(ctx sdk.Context, rewardPeriod types.MultiRewardPeriod) {
// TODO: Get staking rewards and distribute
func (k Keeper) accumulateEarnBkavaRewards(ctx sdk.Context, rewardPeriod types.MultiRewardPeriod) error {
// All bkava vault denoms
bkavaVaultsDenoms := make(map[string]bool)
// bkava vault denoms from earn records (non-empty vaults)
k.earnKeeper.IterateVaultRecords(ctx, func(record earntypes.VaultRecord) (stop bool) {
// TODO: Replace with single bkava denom check method from liquid
if strings.HasPrefix(record.TotalShares.Denom, "bkava-") {
if k.liquidKeeper.IsDerivativeDenom(ctx, record.TotalShares.Denom) {
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
// that were fully withdrawn.
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
}
return false
})
totalBkavaSupply := k.liquidKeeper.GetTotalDerivativeSupply(ctx)
totalBkavaValue, err := k.liquidKeeper.GetTotalDerivativeValue(ctx)
if err != nil {
return err
}
i := 0
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.
for _, bkavaDenom := range sortedBkavaVaultsDenoms {
k.accumulateEarnRewards(
derivativeValue, err := k.liquidKeeper.GetDerivativeValue(ctx, bkavaDenom)
if err != nil {
return err
}
k.accumulateBkavaEarnRewards(
ctx,
bkavaDenom,
rewardPeriod.Start,
rewardPeriod.End,
GetProportionalRewardsPerSecond(
rewardPeriod,
totalBkavaSupply,
k.liquidKeeper.GetDerivativeSupply(ctx, bkavaDenom),
totalBkavaValue.Amount,
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(

View File

@ -94,13 +94,16 @@ func (suite *AccumulateEarnRewardsTests) TestStateUpdatedWhenBlockTimeHasIncreas
vaultDenom1 := "bkava-meow"
vaultDenom2 := "bkava-woof"
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.ctx = suite.ctx.WithBlockTime(previousAccrualTime)
earnKeeper := newFakeEarnKeeper().
addVault(vaultDenom1, earntypes.NewVaultShare(vaultDenom1, d("800000"))).
addVault(vaultDenom2, earntypes.NewVaultShare(vaultDenom2, d("200000")))
liquidKeeper := newFakeLiquidKeeper().
addDerivative(vaultDenom1, i(800000)).
addDerivative(vaultDenom2, i(200000))
addDerivative(suite.ctx, vaultDenom1, i(800000)).
addDerivative(suite.ctx, vaultDenom2, i(200000))
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)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetEarnRewardAccrualTime(suite.ctx, vaultDenom1, previousAccrualTime)
suite.keeper.SetEarnRewardAccrualTime(suite.ctx, vaultDenom2, previousAccrualTime)
@ -164,7 +166,8 @@ func (suite *AccumulateEarnRewardsTests) TestStateUpdatedWhenBlockTimeHasIncreas
},
{
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)
vaultDenom2Supply := i(200000)
liquidKeeper := newFakeLiquidKeeper().
addDerivative(vaultDenom1, vaultDenom1Supply).
addDerivative(vaultDenom2, vaultDenom2Supply)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.ctx = suite.ctx.WithBlockTime(previousAccrualTime)
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, d("700000"))).
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)
@ -223,7 +230,7 @@ func (suite *AccumulateEarnRewardsTests) TestStateUpdatedWhenBlockTimeHasIncreas
}
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, vaultDenom2, previousAccrualTime)
@ -252,7 +259,12 @@ func (suite *AccumulateEarnRewardsTests) TestStateUpdatedWhenBlockTimeHasIncreas
},
{
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",
RewardFactor: d("7.24"),
RewardFactor: d("7.24").
Add(vaultDenom2Supply.ToDec().
QuoInt64(10).
MulInt64(3600).
Quo(vault2Shares),
),
},
}
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() {
@ -350,13 +350,16 @@ func (suite *AccumulateEarnRewardsTests) TestStateUnchangedWhenBlockTimeHasNotIn
vaultDenom1 := "bkava-meow"
vaultDenom2 := "bkava-woof"
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.ctx = suite.ctx.WithBlockTime(previousAccrualTime)
earnKeeper := newFakeEarnKeeper().
addVault(vaultDenom1, earntypes.NewVaultShare(vaultDenom1, d("1000000"))).
addVault(vaultDenom2, earntypes.NewVaultShare(vaultDenom2, d("1000000")))
liquidKeeper := newFakeLiquidKeeper().
addDerivative(vaultDenom1, i(1000000)).
addDerivative(vaultDenom2, i(1000000))
addDerivative(suite.ctx, vaultDenom1, i(1000000)).
addDerivative(suite.ctx, vaultDenom2, i(1000000))
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)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetEarnRewardAccrualTime(suite.ctx, vaultDenom1, previousAccrualTime)
suite.keeper.SetEarnRewardAccrualTime(suite.ctx, vaultDenom2, previousAccrualTime)
suite.ctx = suite.ctx.WithBlockTime(previousAccrualTime)
period := types.NewMultiRewardPeriod(
true,
"bkava",
@ -585,13 +586,16 @@ func (suite *AccumulateEarnRewardsTests) TestStateAddedWhenStateDoesNotExist_bka
vaultDenom1 := "bkava-meow"
vaultDenom2 := "bkava-woof"
firstAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime)
earnKeeper := newFakeEarnKeeper().
addVault(vaultDenom1, earntypes.NewVaultShare(vaultDenom1, d("1000000"))).
addVault(vaultDenom2, earntypes.NewVaultShare(vaultDenom2, d("1000000")))
liquidKeeper := newFakeLiquidKeeper().
addDerivative(vaultDenom1, i(1000000)).
addDerivative(vaultDenom2, i(1000000))
addDerivative(suite.ctx, vaultDenom1, i(1000000)).
addDerivative(suite.ctx, vaultDenom2, i(1000000))
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)),
)
firstAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime)
suite.keeper.AccumulateEarnRewards(suite.ctx, period)
// After the first accumulation only the current block time should be stored.
@ -632,7 +633,9 @@ func (suite *AccumulateEarnRewardsTests) TestStateAddedWhenStateDoesNotExist_bka
},
{
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"),
},
}

View 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)),
},
})
}

View File

@ -432,19 +432,26 @@ func (k *fakeEarnKeeper) IterateVaultRecords(
// 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.
type fakeLiquidKeeper struct {
derivatives map[string]sdk.Int
derivatives map[string]sdk.Int
lastRewardClaim map[string]time.Time
}
var _ types.LiquidKeeper = newFakeLiquidKeeper()
func newFakeLiquidKeeper() *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.lastRewardClaim[denom] = ctx.BlockTime()
return k
}
@ -460,22 +467,56 @@ func (k *fakeLiquidKeeper) GetAllDerivativeDenoms(ctx sdk.Context) (denoms []str
return denoms
}
func (k *fakeLiquidKeeper) GetTotalDerivativeSupply(ctx sdk.Context) sdk.Int {
func (k *fakeLiquidKeeper) GetTotalDerivativeValue(ctx sdk.Context) (sdk.Coin, error) {
totalSupply := sdk.ZeroInt()
for _, supply := range k.derivatives {
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]
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 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

View File

@ -88,7 +88,7 @@ func (*Accumulator) calculateNewRewards(rewardsPerSecond sdk.DecCoins, totalSour
// So return an empty increment instead of one full of zeros.
return nil
}
increment := newRewardIndexesFromCoins(rewardsPerSecond)
increment := NewRewardIndexesFromCoins(rewardsPerSecond)
increment = increment.Mul(sdk.NewDec(durationSeconds)).Quo(totalSourceShares)
return increment
}
@ -109,11 +109,36 @@ func maxTime(t1, t2 time.Time) time.Time {
return t1
}
// newRewardIndexesFromCoins is a helper function to initialize a RewardIndexes slice with the values from a Coins slice.
func newRewardIndexesFromCoins(coins sdk.DecCoins) RewardIndexes {
// NewRewardIndexesFromCoins is a helper function to initialize a RewardIndexes slice with the values from a Coins slice.
func NewRewardIndexesFromCoins(coins sdk.DecCoins) RewardIndexes {
var indexes RewardIndexes
for _, coin := range coins {
indexes = append(indexes, NewRewardIndex(coin.Denom, coin.Amount))
}
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
}

View File

@ -74,8 +74,13 @@ type EarnKeeper interface {
// LiquidKeeper defines the required methods needed by this modules keeper
type LiquidKeeper interface {
IsDerivativeDenom(ctx sdk.Context, denom string) bool
GetTotalDerivativeSupply(ctx sdk.Context) sdk.Int
GetDerivativeSupply(ctx sdk.Context, denom string) sdk.Int
GetTotalDerivativeValue(ctx sdk.Context) (sdk.Coin, error)
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)

51
x/liquid/keeper/claim.go Normal file
View 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)
}

View 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())
})
}

View File

@ -136,6 +136,28 @@ func (k Keeper) GetStakedTokensForDerivatives(ctx sdk.Context, coins sdk.Coins)
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 {
if err := k.bankKeeper.MintCoins(ctx, types.ModuleAccountName, amount); err != nil {
return err

View File

@ -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() {
_, addrs := app.GeneratePrivKeyAddressPairs(1)
valAccAddr := addrs[0]

View File

@ -14,9 +14,10 @@ import (
type Keeper struct {
cdc codec.Codec
accountKeeper types.AccountKeeper
bankKeeper types.BankKeeper
stakingKeeper types.StakingKeeper
accountKeeper types.AccountKeeper
bankKeeper types.BankKeeper
stakingKeeper types.StakingKeeper
distributionKeeper types.DistributionKeeper
derivativeDenom string
}
@ -24,26 +25,27 @@ type Keeper struct {
// NewKeeper returns a new keeper for the liquid module.
func NewKeeper(
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,
) Keeper {
return Keeper{
cdc: cdc,
accountKeeper: ak,
bankKeeper: bk,
stakingKeeper: sk,
derivativeDenom: derivativeDenom,
cdc: cdc,
accountKeeper: ak,
bankKeeper: bk,
stakingKeeper: sk,
distributionKeeper: dk,
derivativeDenom: derivativeDenom,
}
}
// NewDefaultKeeper returns a new keeper for the liquid module with default values.
func NewDefaultKeeper(
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 {
return NewKeeper(cdc, ak, bk, sk, types.DefaultDerivativeDenom)
return NewKeeper(cdc, ak, bk, sk, dk, types.DefaultDerivativeDenom)
}
// Logger returns a module-specific logger.

View File

@ -94,7 +94,7 @@ func (suite *KeeperTestSuite) AddCoinsToModule(module string, amount sdk.Coins)
// AccountBalanceEqual checks if an account has the specified coins.
func (suite *KeeperTestSuite) AccountBalanceEqual(addr sdk.AccAddress, coins sdk.Coins) {
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 {

View File

@ -17,6 +17,9 @@ type BankKeeper interface {
MintCoins(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
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
@ -45,3 +48,8 @@ type StakingKeeper interface {
ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, shares sdk.Dec,
) (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)
}