2021-05-04 14:47:21 +00:00
package keeper
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
2021-06-21 21:05:17 +00:00
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
2021-05-04 14:47:21 +00:00
hardtypes "github.com/kava-labs/kava/x/hard/types"
"github.com/kava-labs/kava/x/incentive/types"
)
2021-07-22 12:53:18 +00:00
// 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 ) {
2021-05-04 14:47:21 +00:00
previousAccrualTime , found := k . GetPreviousHardBorrowRewardAccrualTime ( ctx , rewardPeriod . CollateralType )
if ! found {
2021-07-22 12:53:18 +00:00
previousAccrualTime = ctx . BlockTime ( )
2021-05-04 14:47:21 +00:00
}
2021-07-22 12:53:18 +00:00
indexes , found := k . GetHardBorrowRewardIndexes ( ctx , rewardPeriod . CollateralType )
if ! found {
indexes = types . RewardIndexes { }
2021-05-04 14:47:21 +00:00
}
2021-07-22 12:53:18 +00:00
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 )
2021-05-04 14:47:21 +00:00
}
2021-07-22 12:53:18 +00:00
}
2021-05-04 14:47:21 +00:00
2021-07-22 12:53:18 +00:00
// getHardBorrowTotalSourceShares fetches the sum of all source shares for a borrow reward.
2021-07-26 19:07:24 +00:00
//
// In the case of hard borrow, this is the total borrowed divided by the borrow interest factor (for a particular denom).
// This gives the "pre interest" or "normalized" value of the total borrowed. This is an amount, that if it was borrowed when
// the interest factor was zero (ie at time 0), the current value of it with interest would be equal to the current total borrowed.
//
// The normalized borrow is also used for each individual borrow's source shares amount. Normalized amounts do not change except through
// user input. This is essential as claims must be synced before any change to a source shares amount. The actual borrowed amounts cannot
// be used as they increase every block due to interest.
2021-07-22 12:53:18 +00:00
func ( k Keeper ) getHardBorrowTotalSourceShares ( ctx sdk . Context , denom string ) sdk . Dec {
totalBorrowedCoins , found := k . hardKeeper . GetBorrowedCoins ( ctx )
2021-05-04 14:47:21 +00:00
if ! found {
2021-07-22 12:53:18 +00:00
// assume no coins have been borrowed
totalBorrowedCoins = sdk . NewCoins ( )
2021-05-04 14:47:21 +00:00
}
2021-07-22 12:53:18 +00:00
totalBorrowed := totalBorrowedCoins . AmountOf ( denom )
interestFactor , found := k . hardKeeper . GetBorrowInterestFactor ( ctx , denom )
2021-05-04 14:47:21 +00:00
if ! found {
2021-07-22 12:53:18 +00:00
// assume nothing has been borrowed so the factor starts at it's default value
interestFactor = sdk . OneDec ( )
2021-05-04 14:47:21 +00:00
}
2021-07-22 12:53:18 +00:00
// return borrowed/factor to get the "pre interest" value of the current total borrowed
return totalBorrowed . ToDec ( ) . Quo ( interestFactor )
2021-05-04 14:47:21 +00:00
}
// 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 {
2021-07-07 16:50:14 +00:00
claim = types . NewHardLiquidityProviderClaim ( borrow . Borrower , sdk . Coins { } , nil , nil )
2021-05-04 14:47:21 +00:00
}
var borrowRewardIndexes types . MultiRewardIndexes
for _ , coin := range borrow . Amount {
2021-06-21 21:05:17 +00:00
globalRewardIndexes , found := k . GetHardBorrowRewardIndexes ( ctx , coin . Denom )
if ! found {
globalRewardIndexes = types . RewardIndexes { }
2021-05-04 14:47:21 +00:00
}
2021-06-21 21:05:17 +00:00
borrowRewardIndexes = borrowRewardIndexes . With ( coin . Denom , globalRewardIndexes )
2021-05-04 14:47:21 +00:00
}
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
}
2021-07-26 19:07:24 +00:00
// Source shares for hard borrows is their normalized borrow amount
normalizedBorrows , err := borrow . NormalizedBorrow ( )
if err != nil {
panic ( fmt . Sprintf ( "during borrow reward sync, could not get normalized borrow for %s: %s" , borrow . Borrower , err . Error ( ) ) )
}
2021-05-04 14:47:21 +00:00
2021-07-26 19:07:24 +00:00
for _ , normedBorrow := range normalizedBorrows {
claim = k . synchronizeSingleHardBorrowReward ( ctx , claim , normedBorrow . Denom , normedBorrow . Amount )
2021-05-04 14:47:21 +00:00
}
k . SetHardLiquidityProviderClaim ( ctx , claim )
}
2021-07-26 19:07:24 +00:00
// synchronizeSingleHardBorrowReward synchronizes a single rewarded borrow denom in a hard claim.
// It returns the claim without setting in the store.
// The public methods for accessing and modifying claims are preferred over this one. Direct modification of claims is easy to get wrong.
func ( k Keeper ) synchronizeSingleHardBorrowReward ( ctx sdk . Context , claim types . HardLiquidityProviderClaim , denom string , sourceShares sdk . Dec ) types . HardLiquidityProviderClaim {
globalRewardIndexes , found := k . GetHardBorrowRewardIndexes ( ctx , 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.
return claim
}
userRewardIndexes , found := claim . BorrowRewardIndexes . Get ( 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 , sourceShares )
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 ( denom , globalRewardIndexes )
return claim
}
// UpdateHardBorrowIndexDenoms adds or removes reward indexes from a claim to match the denoms in the borrow.
2021-05-04 14:47:21 +00:00
func ( k Keeper ) UpdateHardBorrowIndexDenoms ( ctx sdk . Context , borrow hardtypes . Borrow ) {
claim , found := k . GetHardLiquidityProviderClaim ( ctx , borrow . Borrower )
if ! found {
2021-07-07 16:50:14 +00:00
claim = types . NewHardLiquidityProviderClaim ( borrow . Borrower , sdk . Coins { } , nil , nil )
2021-05-04 14:47:21 +00:00
}
borrowDenoms := getDenoms ( borrow . Amount )
borrowRewardIndexDenoms := claim . BorrowRewardIndexes . GetCollateralTypes ( )
borrowRewardIndexes := claim . BorrowRewardIndexes
2021-06-21 21:05:17 +00:00
2021-05-04 14:47:21 +00:00
// Create a new multi-reward index in the claim for every new borrow denom
2021-06-21 21:05:17 +00:00
uniqueBorrowDenoms := setDifference ( borrowDenoms , borrowRewardIndexDenoms )
2021-05-04 14:47:21 +00:00
for _ , denom := range uniqueBorrowDenoms {
2021-06-21 21:05:17 +00:00
globalBorrowRewardIndexes , found := k . GetHardBorrowRewardIndexes ( ctx , denom )
if ! found {
globalBorrowRewardIndexes = types . RewardIndexes { }
2021-05-04 14:47:21 +00:00
}
2021-06-21 21:05:17 +00:00
borrowRewardIndexes = borrowRewardIndexes . With ( denom , globalBorrowRewardIndexes )
2021-05-04 14:47:21 +00:00
}
// Delete multi-reward index from claim if the collateral type is no longer borrowed
2021-06-21 21:05:17 +00:00
uniqueBorrowRewardDenoms := setDifference ( borrowRewardIndexDenoms , borrowDenoms )
2021-05-04 14:47:21 +00:00
for _ , denom := range uniqueBorrowRewardDenoms {
borrowRewardIndexes = borrowRewardIndexes . RemoveRewardIndex ( denom )
}
claim . BorrowRewardIndexes = borrowRewardIndexes
k . SetHardLiquidityProviderClaim ( ctx , claim )
}
2021-06-21 21:05:17 +00:00
2021-07-22 12:53:18 +00:00
// CalculateRewards computes how much rewards should have accrued to a reward source (eg a user's hard borrowed btc amount)
2021-06-21 21:05:17 +00:00
// between two index values.
//
2021-07-22 12:53:18 +00:00
// oldIndex is normally the index stored on a claim, newIndex the current global value, and sourceShares a hard borrowed/supplied amount.
2021-06-21 21:05:17 +00:00
//
2021-07-22 12:53:18 +00:00
// It returns an error if newIndexes does not contain all CollateralTypes from oldIndexes, or if any value of oldIndex.RewardFactor > newIndex.RewardFactor.
2021-06-21 21:05:17 +00:00
// 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.
2021-07-22 12:53:18 +00:00
func ( k Keeper ) CalculateRewards ( oldIndexes , newIndexes types . RewardIndexes , sourceShares sdk . Dec ) ( sdk . Coins , error ) {
2021-06-21 21:05:17 +00:00
// 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 ( )
}
2021-07-22 12:53:18 +00:00
rewardAmount , err := k . CalculateSingleReward ( oldFactor , newIndex . RewardFactor , sourceShares )
2021-06-21 21:05:17 +00:00
if err != nil {
return nil , err
}
reward = reward . Add (
sdk . NewCoin ( newIndex . CollateralType , rewardAmount ) ,
)
}
return reward , nil
}
2021-07-22 12:53:18 +00:00
// CalculateSingleReward computes how much rewards should have accrued to a reward source (eg a user's btcb-a cdp principal)
2021-06-21 21:05:17 +00:00
// between two index values.
//
2021-07-22 12:53:18 +00:00
// oldIndex is normally the index stored on a claim, newIndex the current global value, and sourceShares a cdp principal amount.
2021-06-21 21:05:17 +00:00
//
// 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.
2021-07-22 12:53:18 +00:00
func ( k Keeper ) CalculateSingleReward ( oldIndex , newIndex , sourceShares sdk . Dec ) ( sdk . Int , error ) {
2021-06-21 21:05:17 +00:00
increase := newIndex . Sub ( oldIndex )
if increase . IsNegative ( ) {
return sdk . Int { } , sdkerrors . Wrapf ( types . ErrDecreasingRewardFactor , "old: %v, new: %v" , oldIndex , newIndex )
}
2021-07-22 12:53:18 +00:00
reward := increase . Mul ( sourceShares ) . RoundInt ( )
2021-06-21 21:05:17 +00:00
return reward , nil
}