0g-chain/x/incentive/keeper/rewards_borrow.go
Ruaridh 6f193c7f2a
Refactor incentive accumulators to be the same (#970)
* add test for validate multi reward periods

* tidy up: combine files

* don't accumulate global indexes containing zeros
Previously if the time since last block was 0,
indexes were added containing 0s.
Now leave them out. Missing is assumed to be 0.

* move state independent test to types folder

* clarify reward source concept to "source shares"
- rename variables and update doc comments
- extract method from swap accumulation

* tidy up and expand swap accumulation unit tests

* rename swap test file to match others

* update swap pool id format in tests

* refactor borrow accumulation, use new accumulator

* refactor supply accumulation, use new accumulator

* refactor delegator accumulation, use accumulator

* refactor usdx accumulation, use new accumulator

* fix types const

* remove unsed methods

* more usdx minting param validation.
Protect against the rewards per second denom changing.
It should always be "ukava".

* add safety check in InitGenesis
It prevents huge accumulations on the first block by limiting all
previous accumulation times to be within one year of genesis

* add todo for adding swp token distirbution info
2021-07-22 13:53:18 +01:00

205 lines
8.6 KiB
Go

package keeper
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
hardtypes "github.com/kava-labs/kava/x/hard/types"
"github.com/kava-labs/kava/x/incentive/types"
)
// AccumulateHardBorrowRewards 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) AccumulateHardBorrowRewards(ctx sdk.Context, rewardPeriod types.MultiRewardPeriod) {
previousAccrualTime, found := k.GetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType)
if !found {
previousAccrualTime = ctx.BlockTime()
}
indexes, found := k.GetHardBorrowRewardIndexes(ctx, rewardPeriod.CollateralType)
if !found {
indexes = types.RewardIndexes{}
}
acc := types.NewAccumulator(previousAccrualTime, indexes)
totalSource := k.getHardBorrowTotalSourceShares(ctx, rewardPeriod.CollateralType)
acc.Accumulate(rewardPeriod, totalSource, ctx.BlockTime())
k.SetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType, acc.PreviousAccumulationTime)
if len(acc.Indexes) > 0 {
// the store panics when setting empty or nil indexes
k.SetHardBorrowRewardIndexes(ctx, rewardPeriod.CollateralType, acc.Indexes)
}
}
// getHardBorrowTotalSourceShares fetches the sum of all source shares for a borrow reward.
// In the case of hard borrow, this is the total borrowed divided by the borrow interest factor.
// This give the "pre interest" value of the total borrowed.
func (k Keeper) getHardBorrowTotalSourceShares(ctx sdk.Context, denom string) sdk.Dec {
totalBorrowedCoins, found := k.hardKeeper.GetBorrowedCoins(ctx)
if !found {
// assume no coins have been borrowed
totalBorrowedCoins = sdk.NewCoins()
}
totalBorrowed := totalBorrowedCoins.AmountOf(denom)
interestFactor, found := k.hardKeeper.GetBorrowInterestFactor(ctx, denom)
if !found {
// assume nothing has been borrowed so the factor starts at it's default value
interestFactor = sdk.OneDec()
}
// return borrowed/factor to get the "pre interest" value of the current total borrowed
return totalBorrowed.ToDec().Quo(interestFactor)
}
// InitializeHardBorrowReward initializes the borrow-side of a hard liquidity provider claim
// by creating the claim and setting the borrow reward factor index
func (k Keeper) InitializeHardBorrowReward(ctx sdk.Context, borrow hardtypes.Borrow) {
claim, found := k.GetHardLiquidityProviderClaim(ctx, borrow.Borrower)
if !found {
claim = types.NewHardLiquidityProviderClaim(borrow.Borrower, sdk.Coins{}, nil, nil)
}
var borrowRewardIndexes types.MultiRewardIndexes
for _, coin := range borrow.Amount {
globalRewardIndexes, found := k.GetHardBorrowRewardIndexes(ctx, coin.Denom)
if !found {
globalRewardIndexes = types.RewardIndexes{}
}
borrowRewardIndexes = borrowRewardIndexes.With(coin.Denom, globalRewardIndexes)
}
claim.BorrowRewardIndexes = borrowRewardIndexes
k.SetHardLiquidityProviderClaim(ctx, claim)
}
// SynchronizeHardBorrowReward updates the claim object by adding any accumulated rewards
// and updating the reward index value
func (k Keeper) SynchronizeHardBorrowReward(ctx sdk.Context, borrow hardtypes.Borrow) {
claim, found := k.GetHardLiquidityProviderClaim(ctx, borrow.Borrower)
if !found {
return
}
for _, coin := range borrow.Amount {
globalRewardIndexes, found := k.GetHardBorrowRewardIndexes(ctx, coin.Denom)
if !found {
// The global factor is only not found if
// - the borrowed denom has not started accumulating rewards yet (either there is no reward specified in params, or the reward start time hasn't been hit)
// - OR it was wrongly deleted from state (factors should never be removed while unsynced claims exist)
// If not found we could either skip this sync, or assume the global factor is zero.
// Skipping will avoid storing unnecessary factors in the claim for non rewarded denoms.
// And in the event a global factor is wrongly deleted, it will avoid this function panicking when calculating rewards.
continue
}
userRewardIndexes, found := claim.BorrowRewardIndexes.Get(coin.Denom)
if !found {
// Normally the reward indexes should always be found.
// But if a denom was not rewarded then becomes rewarded (ie a reward period is added to params), then the indexes will be missing from claims for that borrowed denom.
// So given the reward period was just added, assume the starting value for any global reward indexes, which is an empty slice.
userRewardIndexes = types.RewardIndexes{}
}
newRewards, err := k.CalculateRewards(userRewardIndexes, globalRewardIndexes, coin.Amount.ToDec())
if err != nil {
// Global reward factors should never decrease, as it would lead to a negative update to claim.Rewards.
// This panics if a global reward factor decreases or disappears between the old and new indexes.
panic(fmt.Sprintf("corrupted global reward indexes found: %v", err))
}
claim.Reward = claim.Reward.Add(newRewards...)
claim.BorrowRewardIndexes = claim.BorrowRewardIndexes.With(coin.Denom, globalRewardIndexes)
}
k.SetHardLiquidityProviderClaim(ctx, claim)
}
// UpdateHardBorrowIndexDenoms adds any new borrow denoms to the claim's borrow reward index
func (k Keeper) UpdateHardBorrowIndexDenoms(ctx sdk.Context, borrow hardtypes.Borrow) {
claim, found := k.GetHardLiquidityProviderClaim(ctx, borrow.Borrower)
if !found {
claim = types.NewHardLiquidityProviderClaim(borrow.Borrower, sdk.Coins{}, nil, nil)
}
borrowDenoms := getDenoms(borrow.Amount)
borrowRewardIndexDenoms := claim.BorrowRewardIndexes.GetCollateralTypes()
borrowRewardIndexes := claim.BorrowRewardIndexes
// Create a new multi-reward index in the claim for every new borrow denom
uniqueBorrowDenoms := setDifference(borrowDenoms, borrowRewardIndexDenoms)
for _, denom := range uniqueBorrowDenoms {
globalBorrowRewardIndexes, found := k.GetHardBorrowRewardIndexes(ctx, denom)
if !found {
globalBorrowRewardIndexes = types.RewardIndexes{}
}
borrowRewardIndexes = borrowRewardIndexes.With(denom, globalBorrowRewardIndexes)
}
// Delete multi-reward index from claim if the collateral type is no longer borrowed
uniqueBorrowRewardDenoms := setDifference(borrowRewardIndexDenoms, borrowDenoms)
for _, denom := range uniqueBorrowRewardDenoms {
borrowRewardIndexes = borrowRewardIndexes.RemoveRewardIndex(denom)
}
claim.BorrowRewardIndexes = borrowRewardIndexes
k.SetHardLiquidityProviderClaim(ctx, claim)
}
// CalculateRewards computes how much rewards should have accrued to a reward source (eg a user's hard borrowed btc amount)
// between two index values.
//
// oldIndex is normally the index stored on a claim, newIndex the current global value, and sourceShares a hard borrowed/supplied amount.
//
// It returns an error if newIndexes does not contain all CollateralTypes from oldIndexes, or if any value of oldIndex.RewardFactor > newIndex.RewardFactor.
// This should never happen, as it would mean that a global reward index has decreased in value, or that a global reward index has been deleted from state.
func (k Keeper) CalculateRewards(oldIndexes, newIndexes types.RewardIndexes, sourceShares sdk.Dec) (sdk.Coins, error) {
// check for missing CollateralType's
for _, oldIndex := range oldIndexes {
if newIndex, found := newIndexes.Get(oldIndex.CollateralType); !found {
return nil, sdkerrors.Wrapf(types.ErrDecreasingRewardFactor, "old: %v, new: %v", oldIndex, newIndex)
}
}
var reward sdk.Coins
for _, newIndex := range newIndexes {
oldFactor, found := oldIndexes.Get(newIndex.CollateralType)
if !found {
oldFactor = sdk.ZeroDec()
}
rewardAmount, err := k.CalculateSingleReward(oldFactor, newIndex.RewardFactor, sourceShares)
if err != nil {
return nil, err
}
reward = reward.Add(
sdk.NewCoin(newIndex.CollateralType, rewardAmount),
)
}
return reward, nil
}
// CalculateSingleReward computes how much rewards should have accrued to a reward source (eg a user's btcb-a cdp principal)
// between two index values.
//
// oldIndex is normally the index stored on a claim, newIndex the current global value, and sourceShares a cdp principal amount.
//
// Returns an error if oldIndex > newIndex. This should never happen, as it would mean that a global reward index has decreased in value,
// or that a global reward index has been deleted from state.
func (k Keeper) CalculateSingleReward(oldIndex, newIndex, sourceShares sdk.Dec) (sdk.Int, error) {
increase := newIndex.Sub(oldIndex)
if increase.IsNegative() {
return sdk.Int{}, sdkerrors.Wrapf(types.ErrDecreasingRewardFactor, "old: %v, new: %v", oldIndex, newIndex)
}
reward := increase.Mul(sourceShares).RoundInt()
return reward, nil
}