diff --git a/x/incentive/keeper/rewards.go b/x/incentive/keeper/rewards.go deleted file mode 100644 index 48931a0b..00000000 --- a/x/incentive/keeper/rewards.go +++ /dev/null @@ -1,914 +0,0 @@ -package keeper - -import ( - "fmt" - "math" - "time" - - sdk "github.com/cosmos/cosmos-sdk/types" - - cdptypes "github.com/kava-labs/kava/x/cdp/types" - hardtypes "github.com/kava-labs/kava/x/hard/types" - "github.com/kava-labs/kava/x/incentive/types" -) - -// AccumulateUSDXMintingRewards updates the rewards accumulated for the input reward period -func (k Keeper) AccumulateUSDXMintingRewards(ctx sdk.Context, rewardPeriod types.RewardPeriod) error { - previousAccrualTime, found := k.GetPreviousUSDXMintingAccrualTime(ctx, rewardPeriod.CollateralType) - if !found { - k.SetPreviousUSDXMintingAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) - return nil - } - timeElapsed := CalculateTimeElapsed(rewardPeriod.Start, rewardPeriod.End, ctx.BlockTime(), previousAccrualTime) - if timeElapsed.IsZero() { - return nil - } - if rewardPeriod.RewardsPerSecond.Amount.IsZero() { - k.SetPreviousUSDXMintingAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) - return nil - } - totalPrincipal := k.cdpKeeper.GetTotalPrincipal(ctx, rewardPeriod.CollateralType, types.PrincipalDenom).ToDec() - if totalPrincipal.IsZero() { - k.SetPreviousUSDXMintingAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) - return nil - } - newRewards := timeElapsed.Mul(rewardPeriod.RewardsPerSecond.Amount) - cdpFactor, found := k.cdpKeeper.GetInterestFactor(ctx, rewardPeriod.CollateralType) - if !found { - k.SetPreviousUSDXMintingAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) - return nil - } - rewardFactor := newRewards.ToDec().Mul(cdpFactor).Quo(totalPrincipal) - - previousRewardFactor, found := k.GetUSDXMintingRewardFactor(ctx, rewardPeriod.CollateralType) - if !found { - previousRewardFactor = sdk.ZeroDec() - } - newRewardFactor := previousRewardFactor.Add(rewardFactor) - k.SetUSDXMintingRewardFactor(ctx, rewardPeriod.CollateralType, newRewardFactor) - k.SetPreviousUSDXMintingAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) - return nil -} - -// AccumulateHardBorrowRewards updates the rewards accumulated for the input reward period -func (k Keeper) AccumulateHardBorrowRewards(ctx sdk.Context, rewardPeriod types.MultiRewardPeriod) error { - previousAccrualTime, found := k.GetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType) - if !found { - k.SetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) - return nil - } - timeElapsed := CalculateTimeElapsed(rewardPeriod.Start, rewardPeriod.End, ctx.BlockTime(), previousAccrualTime) - if timeElapsed.IsZero() { - return nil - } - if rewardPeriod.RewardsPerSecond.IsZero() { - k.SetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) - return nil - } - - totalBorrowedCoins, foundTotalBorrowedCoins := k.hardKeeper.GetBorrowedCoins(ctx) - if !foundTotalBorrowedCoins { - k.SetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) - return nil - } - - totalBorrowed := totalBorrowedCoins.AmountOf(rewardPeriod.CollateralType).ToDec() - if totalBorrowed.IsZero() { - k.SetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) - return nil - } - - previousRewardIndexes, found := k.GetHardBorrowRewardIndexes(ctx, rewardPeriod.CollateralType) - if !found { - for _, rewardCoin := range rewardPeriod.RewardsPerSecond { - rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) - previousRewardIndexes = append(previousRewardIndexes, rewardIndex) - } - k.SetHardBorrowRewardIndexes(ctx, rewardPeriod.CollateralType, previousRewardIndexes) - } - hardFactor, found := k.hardKeeper.GetBorrowInterestFactor(ctx, rewardPeriod.CollateralType) - if !found { - k.SetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) - return nil - } - - newRewardIndexes := previousRewardIndexes - for _, rewardCoin := range rewardPeriod.RewardsPerSecond { - newRewards := rewardCoin.Amount.ToDec().Mul(timeElapsed.ToDec()) - previousRewardIndex, found := previousRewardIndexes.GetRewardIndex(rewardCoin.Denom) - if !found { - previousRewardIndex = types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) - } - - // Calculate new reward factor and update reward index - rewardFactor := newRewards.Mul(hardFactor).Quo(totalBorrowed) - newRewardFactorValue := previousRewardIndex.RewardFactor.Add(rewardFactor) - newRewardIndex := types.NewRewardIndex(rewardCoin.Denom, newRewardFactorValue) - i, found := newRewardIndexes.GetFactorIndex(rewardCoin.Denom) - if found { - newRewardIndexes[i] = newRewardIndex - } else { - newRewardIndexes = append(newRewardIndexes, newRewardIndex) - } - } - k.SetHardBorrowRewardIndexes(ctx, rewardPeriod.CollateralType, newRewardIndexes) - k.SetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) - return nil -} - -// AccumulateHardSupplyRewards updates the rewards accumulated for the input reward period -func (k Keeper) AccumulateHardSupplyRewards(ctx sdk.Context, rewardPeriod types.MultiRewardPeriod) error { - previousAccrualTime, found := k.GetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType) - if !found { - k.SetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) - return nil - } - timeElapsed := CalculateTimeElapsed(rewardPeriod.Start, rewardPeriod.End, ctx.BlockTime(), previousAccrualTime) - if timeElapsed.IsZero() { - return nil - } - if rewardPeriod.RewardsPerSecond.IsZero() { - k.SetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) - return nil - } - - totalSuppliedCoins, foundTotalSuppliedCoins := k.hardKeeper.GetSuppliedCoins(ctx) - if !foundTotalSuppliedCoins { - k.SetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) - return nil - } - - totalSupplied := totalSuppliedCoins.AmountOf(rewardPeriod.CollateralType).ToDec() - if totalSupplied.IsZero() { - k.SetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) - return nil - } - - previousRewardIndexes, found := k.GetHardSupplyRewardIndexes(ctx, rewardPeriod.CollateralType) - if !found { - for _, rewardCoin := range rewardPeriod.RewardsPerSecond { - rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) - previousRewardIndexes = append(previousRewardIndexes, rewardIndex) - } - k.SetHardSupplyRewardIndexes(ctx, rewardPeriod.CollateralType, previousRewardIndexes) - } - hardFactor, found := k.hardKeeper.GetSupplyInterestFactor(ctx, rewardPeriod.CollateralType) - if !found { - k.SetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) - return nil - } - - newRewardIndexes := previousRewardIndexes - for _, rewardCoin := range rewardPeriod.RewardsPerSecond { - newRewards := rewardCoin.Amount.ToDec().Mul(timeElapsed.ToDec()) - previousRewardIndex, found := previousRewardIndexes.GetRewardIndex(rewardCoin.Denom) - if !found { - previousRewardIndex = types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) - } - - // Calculate new reward factor and update reward index - rewardFactor := newRewards.Mul(hardFactor).Quo(totalSupplied) - newRewardFactorValue := previousRewardIndex.RewardFactor.Add(rewardFactor) - newRewardIndex := types.NewRewardIndex(rewardCoin.Denom, newRewardFactorValue) - i, found := newRewardIndexes.GetFactorIndex(rewardCoin.Denom) - if found { - newRewardIndexes[i] = newRewardIndex - } else { - newRewardIndexes = append(newRewardIndexes, newRewardIndex) - } - } - k.SetHardSupplyRewardIndexes(ctx, rewardPeriod.CollateralType, newRewardIndexes) - k.SetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) - return nil -} - -// InitializeUSDXMintingClaim creates or updates a claim such that no new rewards are accrued, but any existing rewards are not lost. -// this function should be called after a cdp is created. If a user previously had a cdp, then closed it, they shouldn't -// accrue rewards during the period the cdp was closed. By setting the reward factor to the current global reward factor, -// any unclaimed rewards are preserved, but no new rewards are added. -func (k Keeper) InitializeUSDXMintingClaim(ctx sdk.Context, cdp cdptypes.CDP) { - _, found := k.GetUSDXMintingRewardPeriod(ctx, cdp.Type) - if !found { - // this collateral type is not incentivized, do nothing - return - } - rewardFactor, found := k.GetUSDXMintingRewardFactor(ctx, cdp.Type) - if !found { - rewardFactor = sdk.ZeroDec() - } - claim, found := k.GetUSDXMintingClaim(ctx, cdp.Owner) - if !found { // this is the owner's first usdx minting reward claim - claim = types.NewUSDXMintingClaim(cdp.Owner, sdk.NewCoin(types.USDXMintingRewardDenom, sdk.ZeroInt()), types.RewardIndexes{types.NewRewardIndex(cdp.Type, rewardFactor)}) - k.SetUSDXMintingClaim(ctx, claim) - return - } - // the owner has an existing usdx minting reward claim - index, hasRewardIndex := claim.HasRewardIndex(cdp.Type) - if !hasRewardIndex { // this is the owner's first usdx minting reward for this collateral type - claim.RewardIndexes = append(claim.RewardIndexes, types.NewRewardIndex(cdp.Type, rewardFactor)) - } else { // the owner has a previous usdx minting reward for this collateral type - claim.RewardIndexes[index] = types.NewRewardIndex(cdp.Type, rewardFactor) - } - k.SetUSDXMintingClaim(ctx, claim) -} - -// SynchronizeUSDXMintingReward updates the claim object by adding any accumulated rewards and updating the reward index value. -// this should be called before a cdp is modified, immediately after the 'SynchronizeInterest' method is called in the cdp module -func (k Keeper) SynchronizeUSDXMintingReward(ctx sdk.Context, cdp cdptypes.CDP) { - _, found := k.GetUSDXMintingRewardPeriod(ctx, cdp.Type) - if !found { - // this collateral type is not incentivized, do nothing - return - } - - globalRewardFactor, found := k.GetUSDXMintingRewardFactor(ctx, cdp.Type) - if !found { - globalRewardFactor = sdk.ZeroDec() - } - claim, found := k.GetUSDXMintingClaim(ctx, cdp.Owner) - if !found { - claim = types.NewUSDXMintingClaim(cdp.Owner, sdk.NewCoin(types.USDXMintingRewardDenom, sdk.ZeroInt()), types.RewardIndexes{types.NewRewardIndex(cdp.Type, globalRewardFactor)}) - k.SetUSDXMintingClaim(ctx, claim) - return - } - - // the owner has an existing usdx minting reward claim - index, hasRewardIndex := claim.HasRewardIndex(cdp.Type) - if !hasRewardIndex { // this is the owner's first usdx minting reward for this collateral type - claim.RewardIndexes = append(claim.RewardIndexes, types.NewRewardIndex(cdp.Type, globalRewardFactor)) - k.SetUSDXMintingClaim(ctx, claim) - return - } - userRewardFactor := claim.RewardIndexes[index].RewardFactor - rewardsAccumulatedFactor := globalRewardFactor.Sub(userRewardFactor) - if rewardsAccumulatedFactor.IsZero() { - return - } - claim.RewardIndexes[index].RewardFactor = globalRewardFactor - newRewardsAmount := rewardsAccumulatedFactor.Mul(cdp.GetTotalPrincipal().Amount.ToDec()).RoundInt() - if newRewardsAmount.IsZero() { - k.SetUSDXMintingClaim(ctx, claim) - return - } - newRewardsCoin := sdk.NewCoin(types.USDXMintingRewardDenom, newRewardsAmount) - claim.Reward = claim.Reward.Add(newRewardsCoin) - k.SetUSDXMintingClaim(ctx, claim) -} - -// InitializeHardSupplyReward initializes the supply-side of a hard liquidity provider claim -// by creating the claim and setting the supply reward factor index -func (k Keeper) InitializeHardSupplyReward(ctx sdk.Context, deposit hardtypes.Deposit) { - var supplyRewardIndexes types.MultiRewardIndexes - for _, coin := range deposit.Amount { - globalRewardIndexes, foundGlobalRewardIndexes := k.GetHardSupplyRewardIndexes(ctx, coin.Denom) - var multiRewardIndex types.MultiRewardIndex - if foundGlobalRewardIndexes { - multiRewardIndex = types.NewMultiRewardIndex(coin.Denom, globalRewardIndexes) - } else { - multiRewardIndex = types.NewMultiRewardIndex(coin.Denom, types.RewardIndexes{}) - } - supplyRewardIndexes = append(supplyRewardIndexes, multiRewardIndex) - } - - claim, found := k.GetHardLiquidityProviderClaim(ctx, deposit.Depositor) - if !found { - // Instantiate claim object - claim = types.NewHardLiquidityProviderClaim(deposit.Depositor, sdk.Coins{}, nil, nil, nil) - } - - claim.SupplyRewardIndexes = supplyRewardIndexes - k.SetHardLiquidityProviderClaim(ctx, claim) -} - -// SynchronizeHardSupplyReward updates the claim object by adding any accumulated rewards -// and updating the reward index value -func (k Keeper) SynchronizeHardSupplyReward(ctx sdk.Context, deposit hardtypes.Deposit) { - claim, found := k.GetHardLiquidityProviderClaim(ctx, deposit.Depositor) - if !found { - return - } - - for _, coin := range deposit.Amount { - globalRewardIndexes, foundGlobalRewardIndexes := k.GetHardSupplyRewardIndexes(ctx, coin.Denom) - if !foundGlobalRewardIndexes { - continue - } - - userMultiRewardIndex, foundUserMultiRewardIndex := claim.SupplyRewardIndexes.GetRewardIndex(coin.Denom) - if !foundUserMultiRewardIndex { - continue - } - - userRewardIndexIndex, foundUserRewardIndexIndex := claim.SupplyRewardIndexes.GetRewardIndexIndex(coin.Denom) - if !foundUserRewardIndexIndex { - continue - } - - for _, globalRewardIndex := range globalRewardIndexes { - userRewardIndex, foundUserRewardIndex := userMultiRewardIndex.RewardIndexes.GetRewardIndex(globalRewardIndex.CollateralType) - if !foundUserRewardIndex { - // User deposited this coin type before it had rewards. When new rewards are added, legacy depositors - // should immediately begin earning rewards. Enable users to do so by updating their claim with the global - // reward index denom and start their reward factor at 0.0 - userRewardIndex = types.NewRewardIndex(globalRewardIndex.CollateralType, sdk.ZeroDec()) - userMultiRewardIndex.RewardIndexes = append(userMultiRewardIndex.RewardIndexes, userRewardIndex) - claim.SupplyRewardIndexes[userRewardIndexIndex] = userMultiRewardIndex - } - - globalRewardFactor := globalRewardIndex.RewardFactor - userRewardFactor := userRewardIndex.RewardFactor - rewardsAccumulatedFactor := globalRewardFactor.Sub(userRewardFactor) - if rewardsAccumulatedFactor.IsNegative() { - panic(fmt.Sprintf("reward accumulation factor cannot be negative: %s", rewardsAccumulatedFactor)) - } - - newRewardsAmount := rewardsAccumulatedFactor.Mul(deposit.Amount.AmountOf(coin.Denom).ToDec()).RoundInt() - - factorIndex, foundFactorIndex := userMultiRewardIndex.RewardIndexes.GetFactorIndex(globalRewardIndex.CollateralType) - if !foundFactorIndex { // should never trigger, as we basically do this check at the start of this loop - continue - } - claim.SupplyRewardIndexes[userRewardIndexIndex].RewardIndexes[factorIndex].RewardFactor = globalRewardIndex.RewardFactor - - newRewardsCoin := sdk.NewCoin(userRewardIndex.CollateralType, newRewardsAmount) - claim.Reward = claim.Reward.Add(newRewardsCoin) - } - } - k.SetHardLiquidityProviderClaim(ctx, claim) -} - -// 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, nil) - } - - var borrowRewardIndexes types.MultiRewardIndexes - for _, coin := range borrow.Amount { - globalRewardIndexes, foundGlobalRewardIndexes := k.GetHardBorrowRewardIndexes(ctx, coin.Denom) - var multiRewardIndex types.MultiRewardIndex - if foundGlobalRewardIndexes { - multiRewardIndex = types.NewMultiRewardIndex(coin.Denom, globalRewardIndexes) - } else { - multiRewardIndex = types.NewMultiRewardIndex(coin.Denom, types.RewardIndexes{}) - } - borrowRewardIndexes = append(borrowRewardIndexes, multiRewardIndex) - } - - 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, foundGlobalRewardIndexes := k.GetHardBorrowRewardIndexes(ctx, coin.Denom) - if !foundGlobalRewardIndexes { - continue - } - - userMultiRewardIndex, foundUserMultiRewardIndex := claim.BorrowRewardIndexes.GetRewardIndex(coin.Denom) - if !foundUserMultiRewardIndex { - continue - } - - userRewardIndexIndex, foundUserRewardIndexIndex := claim.BorrowRewardIndexes.GetRewardIndexIndex(coin.Denom) - if !foundUserRewardIndexIndex { - continue - } - - for _, globalRewardIndex := range globalRewardIndexes { - userRewardIndex, foundUserRewardIndex := userMultiRewardIndex.RewardIndexes.GetRewardIndex(globalRewardIndex.CollateralType) - if !foundUserRewardIndex { - // User borrowed this coin type before it had rewards. When new rewards are added, legacy borrowers - // should immediately begin earning rewards. Enable users to do so by updating their claim with the global - // reward index denom and start their reward factor at 0.0 - userRewardIndex = types.NewRewardIndex(globalRewardIndex.CollateralType, sdk.ZeroDec()) - userMultiRewardIndex.RewardIndexes = append(userMultiRewardIndex.RewardIndexes, userRewardIndex) - claim.BorrowRewardIndexes[userRewardIndexIndex] = userMultiRewardIndex - } - - globalRewardFactor := globalRewardIndex.RewardFactor - userRewardFactor := userRewardIndex.RewardFactor - rewardsAccumulatedFactor := globalRewardFactor.Sub(userRewardFactor) - if rewardsAccumulatedFactor.IsNegative() { - panic(fmt.Sprintf("reward accumulation factor cannot be negative: %s", rewardsAccumulatedFactor)) - } - - newRewardsAmount := rewardsAccumulatedFactor.Mul(borrow.Amount.AmountOf(coin.Denom).ToDec()).RoundInt() - - factorIndex, foundFactorIndex := userMultiRewardIndex.RewardIndexes.GetFactorIndex(globalRewardIndex.CollateralType) - if !foundFactorIndex { // should never trigger - continue - } - claim.BorrowRewardIndexes[userRewardIndexIndex].RewardIndexes[factorIndex].RewardFactor = globalRewardIndex.RewardFactor - newRewardsCoin := sdk.NewCoin(userRewardIndex.CollateralType, newRewardsAmount) - claim.Reward = claim.Reward.Add(newRewardsCoin) - } - } - k.SetHardLiquidityProviderClaim(ctx, claim) -} - -// UpdateHardSupplyIndexDenoms adds any new deposit denoms to the claim's supply reward index -func (k Keeper) UpdateHardSupplyIndexDenoms(ctx sdk.Context, deposit hardtypes.Deposit) { - claim, found := k.GetHardLiquidityProviderClaim(ctx, deposit.Depositor) - if !found { - claim = types.NewHardLiquidityProviderClaim(deposit.Depositor, sdk.Coins{}, nil, nil, nil) - } - - depositDenoms := getDenoms(deposit.Amount) - supplyRewardIndexDenoms := claim.SupplyRewardIndexes.GetCollateralTypes() - - uniqueDepositDenoms := setDifference(depositDenoms, supplyRewardIndexDenoms) - uniqueSupplyRewardDenoms := setDifference(supplyRewardIndexDenoms, depositDenoms) - - supplyRewardIndexes := claim.SupplyRewardIndexes - // Create a new multi-reward index in the claim for every new deposit denom - for _, denom := range uniqueDepositDenoms { - _, foundUserRewardIndexes := claim.SupplyRewardIndexes.GetRewardIndex(denom) - if !foundUserRewardIndexes { - globalSupplyRewardIndexes, foundGlobalSupplyRewardIndexes := k.GetHardSupplyRewardIndexes(ctx, denom) - var multiRewardIndex types.MultiRewardIndex - if foundGlobalSupplyRewardIndexes { - multiRewardIndex = types.NewMultiRewardIndex(denom, globalSupplyRewardIndexes) - } else { - multiRewardIndex = types.NewMultiRewardIndex(denom, types.RewardIndexes{}) - } - supplyRewardIndexes = append(supplyRewardIndexes, multiRewardIndex) - } - } - - // Delete multi-reward index from claim if the collateral type is no longer deposited - for _, denom := range uniqueSupplyRewardDenoms { - supplyRewardIndexes = supplyRewardIndexes.RemoveRewardIndex(denom) - } - - claim.SupplyRewardIndexes = supplyRewardIndexes - 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, nil) - } - - borrowDenoms := getDenoms(borrow.Amount) - borrowRewardIndexDenoms := claim.BorrowRewardIndexes.GetCollateralTypes() - - uniqueBorrowDenoms := setDifference(borrowDenoms, borrowRewardIndexDenoms) - uniqueBorrowRewardDenoms := setDifference(borrowRewardIndexDenoms, borrowDenoms) - - borrowRewardIndexes := claim.BorrowRewardIndexes - // Create a new multi-reward index in the claim for every new borrow denom - for _, denom := range uniqueBorrowDenoms { - _, foundUserRewardIndexes := claim.BorrowRewardIndexes.GetRewardIndex(denom) - if !foundUserRewardIndexes { - globalBorrowRewardIndexes, foundGlobalBorrowRewardIndexes := k.GetHardBorrowRewardIndexes(ctx, denom) - var multiRewardIndex types.MultiRewardIndex - if foundGlobalBorrowRewardIndexes { - multiRewardIndex = types.NewMultiRewardIndex(denom, globalBorrowRewardIndexes) - } else { - multiRewardIndex = types.NewMultiRewardIndex(denom, types.RewardIndexes{}) - } - borrowRewardIndexes = append(borrowRewardIndexes, multiRewardIndex) - } - } - - // Delete multi-reward index from claim if the collateral type is no longer borrowed - for _, denom := range uniqueBorrowRewardDenoms { - borrowRewardIndexes = borrowRewardIndexes.RemoveRewardIndex(denom) - } - - claim.BorrowRewardIndexes = borrowRewardIndexes - k.SetHardLiquidityProviderClaim(ctx, claim) -} - -// SynchronizeHardDelegatorRewards updates the claim object by adding any accumulated rewards, and setting the reward indexes to the global values. -// valAddr and shouldIncludeValidator are used to ignore or include delegations to a particular validator when summing up the total delegation. -// Normally only delegations to Bonded validators are included in the total. This is needed as staking hooks are sometimes called on the wrong side of a validator's state update (from this module's perspective). -func (k Keeper) SynchronizeHardDelegatorRewards(ctx sdk.Context, delegator sdk.AccAddress, valAddr sdk.ValAddress, shouldIncludeValidator bool) { - claim, found := k.GetHardLiquidityProviderClaim(ctx, delegator) - if !found { - return - } - - delagatorFactor, found := k.GetHardDelegatorRewardFactor(ctx, types.BondDenom) - if !found { - return - } - - delegatorIndex, hasDelegatorRewardIndex := claim.HasDelegatorRewardIndex(types.BondDenom) - if !hasDelegatorRewardIndex { - return - } - - userRewardFactor := claim.DelegatorRewardIndexes[delegatorIndex].RewardFactor - rewardsAccumulatedFactor := delagatorFactor.Sub(userRewardFactor) - if rewardsAccumulatedFactor.IsNegative() { - panic(fmt.Sprintf("reward accumulation factor cannot be negative: %s", rewardsAccumulatedFactor)) - } - claim.DelegatorRewardIndexes[delegatorIndex].RewardFactor = delagatorFactor - - totalDelegated := sdk.ZeroDec() - - delegations := k.stakingKeeper.GetDelegatorDelegations(ctx, delegator, 200) - for _, delegation := range delegations { - validator, found := k.stakingKeeper.GetValidator(ctx, delegation.GetValidatorAddr()) - if !found { - continue - } - - if valAddr == nil { - // Delegators don't accumulate rewards if their validator is unbonded - if validator.GetStatus() != sdk.Bonded { - continue - } - } else { - if !shouldIncludeValidator && validator.OperatorAddress.Equals(valAddr) { - // ignore tokens delegated to the validator - continue - } - } - - if validator.GetTokens().IsZero() { - continue - } - - delegatedTokens := validator.TokensFromShares(delegation.GetShares()) - if delegatedTokens.IsNegative() { - continue - } - totalDelegated = totalDelegated.Add(delegatedTokens) - } - rewardsEarned := rewardsAccumulatedFactor.Mul(totalDelegated).RoundInt() - - // Add rewards to delegator's hard claim - newRewardsCoin := sdk.NewCoin(types.HardLiquidityRewardDenom, rewardsEarned) - claim.Reward = claim.Reward.Add(newRewardsCoin) - k.SetHardLiquidityProviderClaim(ctx, claim) -} - -// AccumulateHardDelegatorRewards updates the rewards accumulated for the input reward period -func (k Keeper) AccumulateHardDelegatorRewards(ctx sdk.Context, rewardPeriod types.RewardPeriod) error { - previousAccrualTime, found := k.GetPreviousHardDelegatorRewardAccrualTime(ctx, rewardPeriod.CollateralType) - if !found { - k.SetPreviousHardDelegatorRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) - return nil - } - timeElapsed := CalculateTimeElapsed(rewardPeriod.Start, rewardPeriod.End, ctx.BlockTime(), previousAccrualTime) - if timeElapsed.IsZero() { - return nil - } - if rewardPeriod.RewardsPerSecond.Amount.IsZero() { - k.SetPreviousHardDelegatorRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) - return nil - } - - totalBonded := k.stakingKeeper.TotalBondedTokens(ctx).ToDec() - if totalBonded.IsZero() { - k.SetPreviousHardDelegatorRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) - return nil - } - - newRewards := timeElapsed.Mul(rewardPeriod.RewardsPerSecond.Amount) - rewardFactor := newRewards.ToDec().Quo(totalBonded) - - previousRewardFactor, found := k.GetHardDelegatorRewardFactor(ctx, rewardPeriod.CollateralType) - if !found { - previousRewardFactor = sdk.ZeroDec() - } - newRewardFactor := previousRewardFactor.Add(rewardFactor) - k.SetHardDelegatorRewardFactor(ctx, rewardPeriod.CollateralType, newRewardFactor) - k.SetPreviousHardDelegatorRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) - return nil -} - -// InitializeHardDelegatorReward initializes the delegator reward index of a hard claim -func (k Keeper) InitializeHardDelegatorReward(ctx sdk.Context, delegator sdk.AccAddress) { - delegatorFactor, foundDelegatorFactor := k.GetHardDelegatorRewardFactor(ctx, types.BondDenom) - if !foundDelegatorFactor { // Should always be found... - delegatorFactor = sdk.ZeroDec() - } - - delegatorRewardIndexes := types.NewRewardIndex(types.BondDenom, delegatorFactor) - - claim, found := k.GetHardLiquidityProviderClaim(ctx, delegator) - if !found { - // Instantiate claim object - claim = types.NewHardLiquidityProviderClaim(delegator, sdk.Coins{}, nil, nil, nil) - } else { - k.SynchronizeHardDelegatorRewards(ctx, delegator, nil, false) - claim, _ = k.GetHardLiquidityProviderClaim(ctx, delegator) - } - - claim.DelegatorRewardIndexes = types.RewardIndexes{delegatorRewardIndexes} - k.SetHardLiquidityProviderClaim(ctx, claim) -} - -// ZeroUSDXMintingClaim zeroes out the claim object's rewards and returns the updated claim object -func (k Keeper) ZeroUSDXMintingClaim(ctx sdk.Context, claim types.USDXMintingClaim) types.USDXMintingClaim { - claim.Reward = sdk.NewCoin(claim.Reward.Denom, sdk.ZeroInt()) - k.SetUSDXMintingClaim(ctx, claim) - return claim -} - -// SynchronizeUSDXMintingClaim updates the claim object by adding any rewards that have accumulated. -// Returns the updated claim object -func (k Keeper) SynchronizeUSDXMintingClaim(ctx sdk.Context, claim types.USDXMintingClaim) (types.USDXMintingClaim, error) { - for _, ri := range claim.RewardIndexes { - cdp, found := k.cdpKeeper.GetCdpByOwnerAndCollateralType(ctx, claim.Owner, ri.CollateralType) - if !found { - // if the cdp for this collateral type has been closed, no updates are needed - continue - } - claim = k.synchronizeRewardAndReturnClaim(ctx, cdp) - } - return claim, nil -} - -// this function assumes a claim already exists, so don't call it if that's not the case -func (k Keeper) synchronizeRewardAndReturnClaim(ctx sdk.Context, cdp cdptypes.CDP) types.USDXMintingClaim { - k.SynchronizeUSDXMintingReward(ctx, cdp) - claim, _ := k.GetUSDXMintingClaim(ctx, cdp.Owner) - return claim -} - -// SynchronizeHardLiquidityProviderClaim adds any accumulated rewards -func (k Keeper) SynchronizeHardLiquidityProviderClaim(ctx sdk.Context, owner sdk.AccAddress) { - // Synchronize any hard liquidity supply-side rewards - deposit, foundDeposit := k.hardKeeper.GetDeposit(ctx, owner) - if foundDeposit { - k.SynchronizeHardSupplyReward(ctx, deposit) - } - - // Synchronize any hard liquidity borrow-side rewards - borrow, foundBorrow := k.hardKeeper.GetBorrow(ctx, owner) - if foundBorrow { - k.SynchronizeHardBorrowReward(ctx, borrow) - } - - // Synchronize any hard delegator rewards - k.SynchronizeHardDelegatorRewards(ctx, owner, nil, false) -} - -// ZeroHardLiquidityProviderClaim zeroes out the claim object's rewards and returns the updated claim object -func (k Keeper) ZeroHardLiquidityProviderClaim(ctx sdk.Context, claim types.HardLiquidityProviderClaim) types.HardLiquidityProviderClaim { - claim.Reward = sdk.NewCoins() - k.SetHardLiquidityProviderClaim(ctx, claim) - return claim -} - -// CalculateTimeElapsed calculates the number of reward-eligible seconds that have passed since the previous -// time rewards were accrued, taking into account the end time of the reward period -func CalculateTimeElapsed(start, end, blockTime time.Time, previousAccrualTime time.Time) sdk.Int { - if (end.Before(blockTime) && - (end.Before(previousAccrualTime) || end.Equal(previousAccrualTime))) || - (start.After(blockTime)) || - (start.Equal(blockTime)) { - return sdk.ZeroInt() - } - if start.After(previousAccrualTime) && start.Before(blockTime) { - previousAccrualTime = start - } - - if end.Before(blockTime) { - return sdk.MaxInt(sdk.ZeroInt(), sdk.NewInt(int64(math.RoundToEven( - end.Sub(previousAccrualTime).Seconds(), - )))) - } - return sdk.MaxInt(sdk.ZeroInt(), sdk.NewInt(int64(math.RoundToEven( - blockTime.Sub(previousAccrualTime).Seconds(), - )))) -} - -// SimulateHardSynchronization calculates a user's outstanding hard rewards by simulating reward synchronization -func (k Keeper) SimulateHardSynchronization(ctx sdk.Context, claim types.HardLiquidityProviderClaim) types.HardLiquidityProviderClaim { - // 1. Simulate Hard supply-side rewards - for _, ri := range claim.SupplyRewardIndexes { - globalRewardIndexes, foundGlobalRewardIndexes := k.GetHardSupplyRewardIndexes(ctx, ri.CollateralType) - if !foundGlobalRewardIndexes { - continue - } - - userRewardIndexes, foundUserRewardIndexes := claim.SupplyRewardIndexes.GetRewardIndex(ri.CollateralType) - if !foundUserRewardIndexes { - continue - } - - userRewardIndexIndex, foundUserRewardIndexIndex := claim.SupplyRewardIndexes.GetRewardIndexIndex(ri.CollateralType) - if !foundUserRewardIndexIndex { - continue - } - - for _, globalRewardIndex := range globalRewardIndexes { - userRewardIndex, foundUserRewardIndex := userRewardIndexes.RewardIndexes.GetRewardIndex(globalRewardIndex.CollateralType) - if !foundUserRewardIndex { - userRewardIndex = types.NewRewardIndex(globalRewardIndex.CollateralType, sdk.ZeroDec()) - userRewardIndexes.RewardIndexes = append(userRewardIndexes.RewardIndexes, userRewardIndex) - claim.SupplyRewardIndexes[userRewardIndexIndex].RewardIndexes = append(claim.SupplyRewardIndexes[userRewardIndexIndex].RewardIndexes, userRewardIndex) - } - - globalRewardFactor := globalRewardIndex.RewardFactor - userRewardFactor := userRewardIndex.RewardFactor - rewardsAccumulatedFactor := globalRewardFactor.Sub(userRewardFactor) - if rewardsAccumulatedFactor.IsZero() { - continue - } - deposit, found := k.hardKeeper.GetDeposit(ctx, claim.GetOwner()) - if !found { - continue - } - newRewardsAmount := rewardsAccumulatedFactor.Mul(deposit.Amount.AmountOf(ri.CollateralType).ToDec()).RoundInt() - if newRewardsAmount.IsZero() || newRewardsAmount.IsNegative() { - continue - } - - factorIndex, foundFactorIndex := userRewardIndexes.RewardIndexes.GetFactorIndex(globalRewardIndex.CollateralType) - if !foundFactorIndex { - continue - } - claim.SupplyRewardIndexes[userRewardIndexIndex].RewardIndexes[factorIndex].RewardFactor = globalRewardIndex.RewardFactor - newRewardsCoin := sdk.NewCoin(userRewardIndex.CollateralType, newRewardsAmount) - claim.Reward = claim.Reward.Add(newRewardsCoin) - } - } - - // 2. Simulate Hard borrow-side rewards - for _, ri := range claim.BorrowRewardIndexes { - globalRewardIndexes, foundGlobalRewardIndexes := k.GetHardBorrowRewardIndexes(ctx, ri.CollateralType) - if !foundGlobalRewardIndexes { - continue - } - - userRewardIndexes, foundUserRewardIndexes := claim.BorrowRewardIndexes.GetRewardIndex(ri.CollateralType) - if !foundUserRewardIndexes { - continue - } - - userRewardIndexIndex, foundUserRewardIndexIndex := claim.BorrowRewardIndexes.GetRewardIndexIndex(ri.CollateralType) - if !foundUserRewardIndexIndex { - continue - } - - for _, globalRewardIndex := range globalRewardIndexes { - userRewardIndex, foundUserRewardIndex := userRewardIndexes.RewardIndexes.GetRewardIndex(globalRewardIndex.CollateralType) - if !foundUserRewardIndex { - userRewardIndex = types.NewRewardIndex(globalRewardIndex.CollateralType, sdk.ZeroDec()) - userRewardIndexes.RewardIndexes = append(userRewardIndexes.RewardIndexes, userRewardIndex) - claim.BorrowRewardIndexes[userRewardIndexIndex].RewardIndexes = append(claim.BorrowRewardIndexes[userRewardIndexIndex].RewardIndexes, userRewardIndex) - } - - globalRewardFactor := globalRewardIndex.RewardFactor - userRewardFactor := userRewardIndex.RewardFactor - rewardsAccumulatedFactor := globalRewardFactor.Sub(userRewardFactor) - if rewardsAccumulatedFactor.IsZero() { - continue - } - borrow, found := k.hardKeeper.GetBorrow(ctx, claim.GetOwner()) - if !found { - continue - } - newRewardsAmount := rewardsAccumulatedFactor.Mul(borrow.Amount.AmountOf(ri.CollateralType).ToDec()).RoundInt() - if newRewardsAmount.IsZero() || newRewardsAmount.IsNegative() { - continue - } - - factorIndex, foundFactorIndex := userRewardIndexes.RewardIndexes.GetFactorIndex(globalRewardIndex.CollateralType) - if !foundFactorIndex { - continue - } - claim.BorrowRewardIndexes[userRewardIndexIndex].RewardIndexes[factorIndex].RewardFactor = globalRewardIndex.RewardFactor - newRewardsCoin := sdk.NewCoin(userRewardIndex.CollateralType, newRewardsAmount) - claim.Reward = claim.Reward.Add(newRewardsCoin) - } - } - - // 3. Simulate Hard delegator rewards - delagatorFactor, found := k.GetHardDelegatorRewardFactor(ctx, types.BondDenom) - if !found { - return claim - } - - delegatorIndex, hasDelegatorRewardIndex := claim.HasDelegatorRewardIndex(types.BondDenom) - if !hasDelegatorRewardIndex { - return claim - } - - userRewardFactor := claim.DelegatorRewardIndexes[delegatorIndex].RewardFactor - rewardsAccumulatedFactor := delagatorFactor.Sub(userRewardFactor) - if rewardsAccumulatedFactor.IsZero() { - return claim - } - claim.DelegatorRewardIndexes[delegatorIndex].RewardFactor = delagatorFactor - - totalDelegated := sdk.ZeroDec() - - delegations := k.stakingKeeper.GetDelegatorDelegations(ctx, claim.GetOwner(), 200) - for _, delegation := range delegations { - validator, found := k.stakingKeeper.GetValidator(ctx, delegation.GetValidatorAddr()) - if !found { - continue - } - - // Delegators don't accumulate rewards if their validator is unbonded/slashed - if validator.GetStatus() != sdk.Bonded { - continue - } - - if validator.GetTokens().IsZero() { - continue - } - - delegatedTokens := validator.TokensFromShares(delegation.GetShares()) - if delegatedTokens.IsZero() || delegatedTokens.IsNegative() { - continue - } - totalDelegated = totalDelegated.Add(delegatedTokens) - } - - rewardsEarned := rewardsAccumulatedFactor.Mul(totalDelegated).RoundInt() - if rewardsEarned.IsZero() || rewardsEarned.IsNegative() { - return claim - } - - // Add rewards to delegator's hard claim - newRewardsCoin := sdk.NewCoin(types.HardLiquidityRewardDenom, rewardsEarned) - claim.Reward = claim.Reward.Add(newRewardsCoin) - - return claim -} - -// SimulateUSDXMintingSynchronization calculates a user's outstanding USDX minting rewards by simulating reward synchronization -func (k Keeper) SimulateUSDXMintingSynchronization(ctx sdk.Context, claim types.USDXMintingClaim) types.USDXMintingClaim { - for _, ri := range claim.RewardIndexes { - _, found := k.GetUSDXMintingRewardPeriod(ctx, ri.CollateralType) - if !found { - continue - } - - globalRewardFactor, found := k.GetUSDXMintingRewardFactor(ctx, ri.CollateralType) - if !found { - globalRewardFactor = sdk.ZeroDec() - } - - // the owner has an existing usdx minting reward claim - index, hasRewardIndex := claim.HasRewardIndex(ri.CollateralType) - if !hasRewardIndex { // this is the owner's first usdx minting reward for this collateral type - claim.RewardIndexes = append(claim.RewardIndexes, types.NewRewardIndex(ri.CollateralType, globalRewardFactor)) - } - userRewardFactor := claim.RewardIndexes[index].RewardFactor - rewardsAccumulatedFactor := globalRewardFactor.Sub(userRewardFactor) - if rewardsAccumulatedFactor.IsZero() { - continue - } - - claim.RewardIndexes[index].RewardFactor = globalRewardFactor - - cdp, found := k.cdpKeeper.GetCdpByOwnerAndCollateralType(ctx, claim.GetOwner(), ri.CollateralType) - if !found { - continue - } - newRewardsAmount := rewardsAccumulatedFactor.Mul(cdp.GetTotalPrincipal().Amount.ToDec()).RoundInt() - if newRewardsAmount.IsZero() { - continue - } - newRewardsCoin := sdk.NewCoin(types.USDXMintingRewardDenom, newRewardsAmount) - claim.Reward = claim.Reward.Add(newRewardsCoin) - } - - return claim -} - -// Set setDifference: A - B -func setDifference(a, b []string) (diff []string) { - m := make(map[string]bool) - - for _, item := range b { - m[item] = true - } - - for _, item := range a { - if _, ok := m[item]; !ok { - diff = append(diff, item) - } - } - return -} - -func getDenoms(coins sdk.Coins) []string { - denoms := []string{} - for _, coin := range coins { - denoms = append(denoms, coin.Denom) - } - return denoms -} diff --git a/x/incentive/keeper/rewards_borrow.go b/x/incentive/keeper/rewards_borrow.go new file mode 100644 index 00000000..8706f45f --- /dev/null +++ b/x/incentive/keeper/rewards_borrow.go @@ -0,0 +1,194 @@ +package keeper + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + + hardtypes "github.com/kava-labs/kava/x/hard/types" + "github.com/kava-labs/kava/x/incentive/types" +) + +// AccumulateHardBorrowRewards updates the rewards accumulated for the input reward period +func (k Keeper) AccumulateHardBorrowRewards(ctx sdk.Context, rewardPeriod types.MultiRewardPeriod) error { + previousAccrualTime, found := k.GetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType) + if !found { + k.SetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) + return nil + } + timeElapsed := CalculateTimeElapsed(rewardPeriod.Start, rewardPeriod.End, ctx.BlockTime(), previousAccrualTime) + if timeElapsed.IsZero() { + return nil + } + if rewardPeriod.RewardsPerSecond.IsZero() { + k.SetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) + return nil + } + + totalBorrowedCoins, foundTotalBorrowedCoins := k.hardKeeper.GetBorrowedCoins(ctx) + if !foundTotalBorrowedCoins { + k.SetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) + return nil + } + + totalBorrowed := totalBorrowedCoins.AmountOf(rewardPeriod.CollateralType).ToDec() + if totalBorrowed.IsZero() { + k.SetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) + return nil + } + + previousRewardIndexes, found := k.GetHardBorrowRewardIndexes(ctx, rewardPeriod.CollateralType) + if !found { + for _, rewardCoin := range rewardPeriod.RewardsPerSecond { + rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) + previousRewardIndexes = append(previousRewardIndexes, rewardIndex) + } + k.SetHardBorrowRewardIndexes(ctx, rewardPeriod.CollateralType, previousRewardIndexes) + } + hardFactor, found := k.hardKeeper.GetBorrowInterestFactor(ctx, rewardPeriod.CollateralType) + if !found { + k.SetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) + return nil + } + + newRewardIndexes := previousRewardIndexes + for _, rewardCoin := range rewardPeriod.RewardsPerSecond { + newRewards := rewardCoin.Amount.ToDec().Mul(timeElapsed.ToDec()) + previousRewardIndex, found := previousRewardIndexes.GetRewardIndex(rewardCoin.Denom) + if !found { + previousRewardIndex = types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) + } + + // Calculate new reward factor and update reward index + rewardFactor := newRewards.Mul(hardFactor).Quo(totalBorrowed) + newRewardFactorValue := previousRewardIndex.RewardFactor.Add(rewardFactor) + newRewardIndex := types.NewRewardIndex(rewardCoin.Denom, newRewardFactorValue) + i, found := newRewardIndexes.GetFactorIndex(rewardCoin.Denom) + if found { + newRewardIndexes[i] = newRewardIndex + } else { + newRewardIndexes = append(newRewardIndexes, newRewardIndex) + } + } + k.SetHardBorrowRewardIndexes(ctx, rewardPeriod.CollateralType, newRewardIndexes) + k.SetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) + return nil +} + +// 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, nil) + } + + var borrowRewardIndexes types.MultiRewardIndexes + for _, coin := range borrow.Amount { + globalRewardIndexes, foundGlobalRewardIndexes := k.GetHardBorrowRewardIndexes(ctx, coin.Denom) + var multiRewardIndex types.MultiRewardIndex + if foundGlobalRewardIndexes { + multiRewardIndex = types.NewMultiRewardIndex(coin.Denom, globalRewardIndexes) + } else { + multiRewardIndex = types.NewMultiRewardIndex(coin.Denom, types.RewardIndexes{}) + } + borrowRewardIndexes = append(borrowRewardIndexes, multiRewardIndex) + } + + 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, foundGlobalRewardIndexes := k.GetHardBorrowRewardIndexes(ctx, coin.Denom) + if !foundGlobalRewardIndexes { + continue + } + + userMultiRewardIndex, foundUserMultiRewardIndex := claim.BorrowRewardIndexes.GetRewardIndex(coin.Denom) + if !foundUserMultiRewardIndex { + continue + } + + userRewardIndexIndex, foundUserRewardIndexIndex := claim.BorrowRewardIndexes.GetRewardIndexIndex(coin.Denom) + if !foundUserRewardIndexIndex { + continue + } + + for _, globalRewardIndex := range globalRewardIndexes { + userRewardIndex, foundUserRewardIndex := userMultiRewardIndex.RewardIndexes.GetRewardIndex(globalRewardIndex.CollateralType) + if !foundUserRewardIndex { + // User borrowed this coin type before it had rewards. When new rewards are added, legacy borrowers + // should immediately begin earning rewards. Enable users to do so by updating their claim with the global + // reward index denom and start their reward factor at 0.0 + userRewardIndex = types.NewRewardIndex(globalRewardIndex.CollateralType, sdk.ZeroDec()) + userMultiRewardIndex.RewardIndexes = append(userMultiRewardIndex.RewardIndexes, userRewardIndex) + claim.BorrowRewardIndexes[userRewardIndexIndex] = userMultiRewardIndex + } + + globalRewardFactor := globalRewardIndex.RewardFactor + userRewardFactor := userRewardIndex.RewardFactor + rewardsAccumulatedFactor := globalRewardFactor.Sub(userRewardFactor) + if rewardsAccumulatedFactor.IsNegative() { + panic(fmt.Sprintf("reward accumulation factor cannot be negative: %s", rewardsAccumulatedFactor)) + } + + newRewardsAmount := rewardsAccumulatedFactor.Mul(borrow.Amount.AmountOf(coin.Denom).ToDec()).RoundInt() + + factorIndex, foundFactorIndex := userMultiRewardIndex.RewardIndexes.GetFactorIndex(globalRewardIndex.CollateralType) + if !foundFactorIndex { // should never trigger + continue + } + claim.BorrowRewardIndexes[userRewardIndexIndex].RewardIndexes[factorIndex].RewardFactor = globalRewardIndex.RewardFactor + newRewardsCoin := sdk.NewCoin(userRewardIndex.CollateralType, newRewardsAmount) + claim.Reward = claim.Reward.Add(newRewardsCoin) + } + } + 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, nil) + } + + borrowDenoms := getDenoms(borrow.Amount) + borrowRewardIndexDenoms := claim.BorrowRewardIndexes.GetCollateralTypes() + + uniqueBorrowDenoms := setDifference(borrowDenoms, borrowRewardIndexDenoms) + uniqueBorrowRewardDenoms := setDifference(borrowRewardIndexDenoms, borrowDenoms) + + borrowRewardIndexes := claim.BorrowRewardIndexes + // Create a new multi-reward index in the claim for every new borrow denom + for _, denom := range uniqueBorrowDenoms { + _, foundUserRewardIndexes := claim.BorrowRewardIndexes.GetRewardIndex(denom) + if !foundUserRewardIndexes { + globalBorrowRewardIndexes, foundGlobalBorrowRewardIndexes := k.GetHardBorrowRewardIndexes(ctx, denom) + var multiRewardIndex types.MultiRewardIndex + if foundGlobalBorrowRewardIndexes { + multiRewardIndex = types.NewMultiRewardIndex(denom, globalBorrowRewardIndexes) + } else { + multiRewardIndex = types.NewMultiRewardIndex(denom, types.RewardIndexes{}) + } + borrowRewardIndexes = append(borrowRewardIndexes, multiRewardIndex) + } + } + + // Delete multi-reward index from claim if the collateral type is no longer borrowed + for _, denom := range uniqueBorrowRewardDenoms { + borrowRewardIndexes = borrowRewardIndexes.RemoveRewardIndex(denom) + } + + claim.BorrowRewardIndexes = borrowRewardIndexes + k.SetHardLiquidityProviderClaim(ctx, claim) +} diff --git a/x/incentive/keeper/rewards_borrow_test.go b/x/incentive/keeper/rewards_borrow_test.go new file mode 100644 index 00000000..31799ea2 --- /dev/null +++ b/x/incentive/keeper/rewards_borrow_test.go @@ -0,0 +1,1160 @@ +package keeper_test + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/params" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/x/committee" + "github.com/kava-labs/kava/x/hard" + hardtypes "github.com/kava-labs/kava/x/hard/types" + "github.com/kava-labs/kava/x/incentive/types" +) + +func (suite *KeeperTestSuite) TestAccumulateHardBorrowRewards() { + type args struct { + borrow sdk.Coin + rewardsPerSecond sdk.Coins + initialTime time.Time + timeElapsed int + expectedRewardIndexes types.RewardIndexes + } + type test struct { + name string + args args + } + testCases := []test{ + { + "single reward denom: 7 seconds", + args{ + borrow: c("bnb", 1000000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + timeElapsed: 7, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.000000856478000001"))}, + }, + }, + { + "single reward denom: 1 day", + args{ + borrow: c("bnb", 1000000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + timeElapsed: 86400, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.010571385600010177"))}, + }, + }, + { + "single reward denom: 0 seconds", + args{ + borrow: c("bnb", 1000000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + timeElapsed: 0, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.0"))}, + }, + }, + { + "multiple reward denoms: 7 seconds", + args{ + borrow: c("bnb", 1000000000000), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + timeElapsed: 7, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.000000856478000001")), + types.NewRewardIndex("ukava", d("0.000000856478000001")), + }, + }, + }, + { + "multiple reward denoms: 1 day", + args{ + borrow: c("bnb", 1000000000000), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + timeElapsed: 86400, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.010571385600010177")), + types.NewRewardIndex("ukava", d("0.010571385600010177")), + }, + }, + }, + { + "multiple reward denoms: 0 seconds", + args{ + borrow: c("bnb", 1000000000000), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + timeElapsed: 0, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + types.NewRewardIndex("ukava", d("0.0")), + }, + }, + }, + { + "multiple reward denoms with different rewards per second: 1 day", + args{ + borrow: c("bnb", 1000000000000), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 555555)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + timeElapsed: 86400, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.010571385600010177")), + types.NewRewardIndex("ukava", d("0.047999952000046210")), + }, + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWithGenState() + suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) + + // Mint coins to hard module account + supplyKeeper := suite.app.GetSupplyKeeper() + hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) + supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) + + // setup incentive state + params := types.NewParams( + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, + types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + tc.args.initialTime.Add(time.Hour*24*365*5), + ) + suite.keeper.SetParams(suite.ctx, params) + suite.keeper.SetPreviousHardBorrowRewardAccrualTime(suite.ctx, tc.args.borrow.Denom, tc.args.initialTime) + var rewardIndexes types.RewardIndexes + for _, rewardCoin := range tc.args.rewardsPerSecond { + rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) + rewardIndexes = append(rewardIndexes, rewardIndex) + } + suite.keeper.SetHardBorrowRewardIndexes(suite.ctx, tc.args.borrow.Denom, rewardIndexes) + + // Set up hard state (interest factor for the relevant denom) + suite.hardKeeper.SetSupplyInterestFactor(suite.ctx, tc.args.borrow.Denom, sdk.MustNewDecFromStr("1.0")) + suite.hardKeeper.SetBorrowInterestFactor(suite.ctx, tc.args.borrow.Denom, sdk.MustNewDecFromStr("1.0")) + suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.borrow.Denom, tc.args.initialTime) + + // User deposits and borrows to increase total borrowed amount + hardKeeper := suite.app.GetHardKeeper() + userAddr := suite.addrs[3] + err := hardKeeper.Deposit(suite.ctx, userAddr, sdk.NewCoins(sdk.NewCoin(tc.args.borrow.Denom, tc.args.borrow.Amount.Mul(sdk.NewInt(2))))) + suite.Require().NoError(err) + err = hardKeeper.Borrow(suite.ctx, userAddr, sdk.NewCoins(tc.args.borrow)) + suite.Require().NoError(err) + + // Set up chain context at future time + runAtTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * tc.args.timeElapsed)) + runCtx := suite.ctx.WithBlockTime(runAtTime) + + // Run Hard begin blocker in order to update the denom's index factor + hard.BeginBlocker(runCtx, suite.hardKeeper) + + // Accumulate hard borrow rewards for the deposit denom + multiRewardPeriod, found := suite.keeper.GetHardBorrowRewardPeriods(runCtx, tc.args.borrow.Denom) + suite.Require().True(found) + err = suite.keeper.AccumulateHardBorrowRewards(runCtx, multiRewardPeriod) + suite.Require().NoError(err) + + // Check that each expected reward index matches the current stored reward index for the denom + globalRewardIndexes, found := suite.keeper.GetHardBorrowRewardIndexes(runCtx, tc.args.borrow.Denom) + suite.Require().True(found) + for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { + globalRewardIndex, found := globalRewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(expectedRewardIndex, globalRewardIndex) + } + }) + } +} + +func (suite *KeeperTestSuite) TestInitializeHardBorrowRewards() { + + type args struct { + moneyMarketRewardDenoms map[string][]string + deposit sdk.Coins + borrow sdk.Coins + initialTime time.Time + expectedClaimBorrowRewardIndexes types.MultiRewardIndexes + } + type test struct { + name string + args args + } + + standardMoneyMarketRewardDenoms := map[string][]string{ + "bnb": {"hard"}, + "btcb": {"hard", "ukava"}, + "xrp": {}, + } + + testCases := []test{ + { + "single deposit denom, single reward denom", + args{ + moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, + deposit: cs(c("bnb", 1000000000000)), + borrow: cs(c("bnb", 100000000000)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedClaimBorrowRewardIndexes: types.MultiRewardIndexes{ + types.NewMultiRewardIndex( + "bnb", + types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + }, + ), + }, + }, + }, + { + "single deposit denom, multiple reward denoms", + args{ + moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, + deposit: cs(c("btcb", 1000000000000)), + borrow: cs(c("btcb", 100000000000)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedClaimBorrowRewardIndexes: types.MultiRewardIndexes{ + types.NewMultiRewardIndex( + "btcb", + types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + types.NewRewardIndex("ukava", d("0.0")), + }, + ), + }, + }, + }, + { + "single deposit denom, no reward denoms", + args{ + moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, + deposit: cs(c("xrp", 1000000000000)), + borrow: cs(c("xrp", 100000000000)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedClaimBorrowRewardIndexes: types.MultiRewardIndexes{ + types.NewMultiRewardIndex( + "xrp", + nil, + ), + }, + }, + }, + { + "multiple deposit denoms, multiple overlapping reward denoms", + args{ + moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, + deposit: cs(c("bnb", 1000000000000), c("btcb", 1000000000000)), + borrow: cs(c("bnb", 100000000000), c("btcb", 100000000000)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedClaimBorrowRewardIndexes: types.MultiRewardIndexes{ + types.NewMultiRewardIndex( + "bnb", + types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + }, + ), + types.NewMultiRewardIndex( + "btcb", + types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + types.NewRewardIndex("ukava", d("0.0")), + }, + ), + }, + }, + }, + { + "multiple deposit denoms, correct discrete reward denoms", + args{ + moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, + deposit: cs(c("bnb", 1000000000000), c("xrp", 1000000000000)), + borrow: cs(c("bnb", 100000000000), c("xrp", 100000000000)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedClaimBorrowRewardIndexes: types.MultiRewardIndexes{ + types.NewMultiRewardIndex( + "bnb", + types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + }, + ), + types.NewMultiRewardIndex( + "xrp", + nil, + ), + }, + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWithGenState() + suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) + + // Mint coins to hard module account + supplyKeeper := suite.app.GetSupplyKeeper() + hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) + supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) + + userAddr := suite.addrs[3] + + // Prepare money market + reward params + i := 0 + var multiRewardPeriods types.MultiRewardPeriods + var rewardPeriods types.RewardPeriods + for moneyMarketDenom, rewardDenoms := range tc.args.moneyMarketRewardDenoms { + // Set up multi reward periods for supply/borrow indexes with dynamic money market denoms/reward denoms + var rewardsPerSecond sdk.Coins + for _, rewardDenom := range rewardDenoms { + rewardsPerSecond = append(rewardsPerSecond, sdk.NewCoin(rewardDenom, sdk.OneInt())) + } + multiRewardPeriod := types.NewMultiRewardPeriod(true, moneyMarketDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), rewardsPerSecond) + multiRewardPeriods = append(multiRewardPeriods, multiRewardPeriod) + + // Set up generic reward periods for usdx minting/delegator indexes + if i == 0 && len(rewardDenoms) > 0 { + rewardPeriod := types.NewRewardPeriod(true, moneyMarketDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), rewardsPerSecond[i]) + rewardPeriods = append(rewardPeriods, rewardPeriod) + i++ + } + } + + // Initialize and set incentive params + params := types.NewParams( + rewardPeriods, multiRewardPeriods, multiRewardPeriods, rewardPeriods, + types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + tc.args.initialTime.Add(time.Hour*24*365*5), + ) + suite.keeper.SetParams(suite.ctx, params) + + // Set each money market's previous accrual time and supply reward indexes + for moneyMarketDenom, rewardDenoms := range tc.args.moneyMarketRewardDenoms { + var rewardIndexes types.RewardIndexes + for _, rewardDenom := range rewardDenoms { + rewardIndex := types.NewRewardIndex(rewardDenom, sdk.ZeroDec()) + rewardIndexes = append(rewardIndexes, rewardIndex) + } + suite.keeper.SetPreviousHardBorrowRewardAccrualTime(suite.ctx, moneyMarketDenom, tc.args.initialTime) + if len(rewardIndexes) > 0 { + suite.keeper.SetHardBorrowRewardIndexes(suite.ctx, moneyMarketDenom, rewardIndexes) + } + } + + hardKeeper := suite.app.GetHardKeeper() + // User deposits + err := hardKeeper.Deposit(suite.ctx, userAddr, tc.args.deposit) + suite.Require().NoError(err) + // User borrows + err = hardKeeper.Borrow(suite.ctx, userAddr, tc.args.borrow) + suite.Require().NoError(err) + + claim, foundClaim := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) + suite.Require().True(foundClaim) + suite.Require().Equal(tc.args.expectedClaimBorrowRewardIndexes, claim.BorrowRewardIndexes) + }) + } +} + +func (suite *KeeperTestSuite) TestSynchronizeHardBorrowReward() { + type args struct { + incentiveBorrowRewardDenom string + borrow sdk.Coin + rewardsPerSecond sdk.Coins + initialTime time.Time + blockTimes []int + expectedRewardIndexes types.RewardIndexes + expectedRewards sdk.Coins + updateRewardsViaCommmittee bool + updatedBaseDenom string + updatedRewardsPerSecond sdk.Coins + updatedExpectedRewardIndexes types.RewardIndexes + updatedExpectedRewards sdk.Coins + updatedTimeDuration int + } + type test struct { + name string + args args + } + + testCases := []test{ + { + "single reward denom: 10 blocks", + args{ + incentiveBorrowRewardDenom: "bnb", + borrow: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.001223540000173228"))}, + expectedRewards: cs(c("hard", 12235400)), + updateRewardsViaCommmittee: false, + }, + }, + { + "single reward denom: 10 blocks - long block time", + args{ + incentiveBorrowRewardDenom: "bnb", + borrow: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("10.571385603126235340"))}, + expectedRewards: cs(c("hard", 105713856031)), + }, + }, + { + "single reward denom: user reward index updated when reward is zero", + args{ + incentiveBorrowRewardDenom: "ukava", + borrow: c("ukava", 1), // borrow a tiny amount so that rewards round to zero + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.122354003908172328"))}, + expectedRewards: cs(), + updateRewardsViaCommmittee: false, + }, + }, + { + "multiple reward denoms: 10 blocks", + args{ + incentiveBorrowRewardDenom: "bnb", + borrow: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.001223540000173228")), + types.NewRewardIndex("ukava", d("0.001223540000173228")), + }, + expectedRewards: cs(c("hard", 12235400), c("ukava", 12235400)), + }, + }, + { + "multiple reward denoms: 10 blocks - long block time", + args{ + incentiveBorrowRewardDenom: "bnb", + borrow: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("10.571385603126235340")), + types.NewRewardIndex("ukava", d("10.571385603126235340")), + }, + expectedRewards: cs(c("hard", 105713856031), c("ukava", 105713856031)), + }, + }, + { + "multiple reward denoms with different rewards per second: 10 blocks", + args{ + incentiveBorrowRewardDenom: "bnb", + borrow: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 555555)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.001223540000173228")), + types.NewRewardIndex("ukava", d("0.005555550000786558")), + }, + expectedRewards: cs(c("hard", 12235400), c("ukava", 55555500)), + }, + }, + { + "denom is in incentive's hard borrow reward params but it has no rewards; add reward", + args{ + incentiveBorrowRewardDenom: "bnb", + borrow: c("bnb", 10000000000), + rewardsPerSecond: sdk.Coins{}, + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{100}, + expectedRewardIndexes: types.RewardIndexes{}, + expectedRewards: sdk.Coins{}, + updateRewardsViaCommmittee: true, + updatedBaseDenom: "bnb", + updatedRewardsPerSecond: cs(c("hard", 100000)), + updatedExpectedRewards: cs(c("hard", 8640000000)), + updatedExpectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.864000000049803065")), + }, + updatedTimeDuration: 86400, + }, + }, + { + "denom is in incentive's hard borrow reward params and has rewards; add new reward type", + args{ + incentiveBorrowRewardDenom: "bnb", + borrow: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{86400}, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("1.057138560060101160")), + }, + expectedRewards: cs(c("hard", 10571385601)), + updateRewardsViaCommmittee: true, + updatedBaseDenom: "bnb", + updatedRewardsPerSecond: cs(c("hard", 122354), c("ukava", 100000)), + updatedExpectedRewards: cs(c("hard", 21142771202), c("ukava", 8640000000)), + updatedExpectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("2.114277120120202320")), + types.NewRewardIndex("ukava", d("0.864000000049120715")), + }, + updatedTimeDuration: 86400, + }, + }, + { + "denom is in hard's money market params but not in incentive's hard supply reward params; add reward", + args{ + incentiveBorrowRewardDenom: "bnb", + borrow: c("zzz", 10000000000), + rewardsPerSecond: sdk.Coins{}, + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{100}, + expectedRewardIndexes: types.RewardIndexes{}, + expectedRewards: sdk.Coins{}, + updateRewardsViaCommmittee: true, + updatedBaseDenom: "zzz", + updatedRewardsPerSecond: cs(c("hard", 100000)), + updatedExpectedRewards: cs(c("hard", 8640000000)), + updatedExpectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.864000000049803065")), + }, + updatedTimeDuration: 86400, + }, + }, + { + "denom incentive's hard borrow reward params but it has no rewards; add multiple reward types", + args{ + incentiveBorrowRewardDenom: "bnb", + borrow: c("bnb", 10000000000), + rewardsPerSecond: sdk.Coins{}, + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{100}, + expectedRewardIndexes: types.RewardIndexes{}, + expectedRewards: sdk.Coins{}, + updateRewardsViaCommmittee: true, + updatedBaseDenom: "bnb", + updatedRewardsPerSecond: cs(c("hard", 100000), c("ukava", 100500), c("swap", 500)), + updatedExpectedRewards: cs(c("hard", 8640000000), c("ukava", 8683200001), c("swap", 43200000)), + updatedExpectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.864000000049803065")), + types.NewRewardIndex("ukava", d("0.868320000050052081")), + types.NewRewardIndex("swap", d("0.004320000000249015")), + }, + updatedTimeDuration: 86400, + }, + }, + { + "denom is in hard's money market params but not in incentive's hard supply reward params; add multiple reward types", + args{ + incentiveBorrowRewardDenom: "bnb", + borrow: c("zzz", 10000000000), + rewardsPerSecond: sdk.Coins{}, + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{100}, + expectedRewardIndexes: types.RewardIndexes{}, + expectedRewards: sdk.Coins{}, + updateRewardsViaCommmittee: true, + updatedBaseDenom: "zzz", + updatedRewardsPerSecond: cs(c("hard", 100000), c("ukava", 100500), c("swap", 500)), + updatedExpectedRewards: cs(c("hard", 8640000000), c("ukava", 8683200001), c("swap", 43200000)), + updatedExpectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.864000000049803065")), + types.NewRewardIndex("ukava", d("0.868320000050052081")), + types.NewRewardIndex("swap", d("0.004320000000249015")), + }, + updatedTimeDuration: 86400, + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWithGenState() + suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) + + // Mint coins to hard module account + supplyKeeper := suite.app.GetSupplyKeeper() + hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) + supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) + + // Set up incentive state + incentiveParams := types.NewParams( + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.incentiveBorrowRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), c("hard", 1))}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.incentiveBorrowRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), sdk.Coins{})}, // Don't set any supply rewards for easier accounting + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.incentiveBorrowRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.incentiveBorrowRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), c("hard", 1))}, + types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + tc.args.initialTime.Add(time.Hour*24*365*5), + ) + suite.keeper.SetParams(suite.ctx, incentiveParams) + suite.keeper.SetPreviousHardBorrowRewardAccrualTime(suite.ctx, tc.args.incentiveBorrowRewardDenom, tc.args.initialTime) + var rewardIndexes types.RewardIndexes + for _, rewardCoin := range tc.args.rewardsPerSecond { + rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) + rewardIndexes = append(rewardIndexes, rewardIndex) + } + if len(rewardIndexes) > 0 { + suite.keeper.SetHardBorrowRewardIndexes(suite.ctx, tc.args.incentiveBorrowRewardDenom, rewardIndexes) + } + + // Set up hard state (interest factor for the relevant denom) + suite.hardKeeper.SetSupplyInterestFactor(suite.ctx, tc.args.borrow.Denom, sdk.MustNewDecFromStr("1.0")) + suite.hardKeeper.SetBorrowInterestFactor(suite.ctx, tc.args.borrow.Denom, sdk.MustNewDecFromStr("1.0")) + suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.borrow.Denom, tc.args.initialTime) + // Set the minimum borrow to 0 to allow testing small borrows + hardParams := suite.hardKeeper.GetParams(suite.ctx) + hardParams.MinimumBorrowUSDValue = sdk.ZeroDec() + suite.hardKeeper.SetParams(suite.ctx, hardParams) + + // Borrow a fixed amount from another user to dilute primary user's rewards per second. + suite.Require().NoError( + suite.hardKeeper.Deposit(suite.ctx, suite.addrs[2], cs(c("ukava", 200_000_000))), + ) + suite.Require().NoError( + suite.hardKeeper.Borrow(suite.ctx, suite.addrs[2], cs(c("ukava", 100_000_000))), + ) + + // User deposits and borrows to increase total borrowed amount + hardKeeper := suite.app.GetHardKeeper() + userAddr := suite.addrs[3] + err := hardKeeper.Deposit(suite.ctx, userAddr, sdk.NewCoins(sdk.NewCoin(tc.args.borrow.Denom, tc.args.borrow.Amount.Mul(sdk.NewInt(2))))) + suite.Require().NoError(err) + err = hardKeeper.Borrow(suite.ctx, userAddr, sdk.NewCoins(tc.args.borrow)) + suite.Require().NoError(err) + + // Check that Hard hooks initialized a HardLiquidityProviderClaim + claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) + suite.Require().True(found) + multiRewardIndex, _ := claim.BorrowRewardIndexes.GetRewardIndex(tc.args.borrow.Denom) + for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { + currRewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(sdk.ZeroDec(), currRewardIndex.RewardFactor) + } + + // Run accumulator at several intervals + var timeElapsed int + previousBlockTime := suite.ctx.BlockTime() + for _, t := range tc.args.blockTimes { + timeElapsed += t + updatedBlockTime := previousBlockTime.Add(time.Duration(int(time.Second) * t)) + previousBlockTime = updatedBlockTime + blockCtx := suite.ctx.WithBlockTime(updatedBlockTime) + + // Run Hard begin blocker for each block ctx to update denom's interest factor + hard.BeginBlocker(blockCtx, suite.hardKeeper) + + // Accumulate hard borrow-side rewards + multiRewardPeriod, found := suite.keeper.GetHardBorrowRewardPeriods(blockCtx, tc.args.borrow.Denom) + if found { + err := suite.keeper.AccumulateHardBorrowRewards(blockCtx, multiRewardPeriod) + suite.Require().NoError(err) + } + } + updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed)) + suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) + + // After we've accumulated, run synchronize + borrow, found := hardKeeper.GetBorrow(suite.ctx, userAddr) + suite.Require().True(found) + suite.Require().NotPanics(func() { + suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow) + }) + + // Check that the global reward index's reward factor and user's claim have been updated as expected + claim, found = suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) + suite.Require().True(found) + globalRewardIndexes, foundGlobalRewardIndexes := suite.keeper.GetHardBorrowRewardIndexes(suite.ctx, tc.args.borrow.Denom) + if len(tc.args.rewardsPerSecond) > 0 { + suite.Require().True(foundGlobalRewardIndexes) + for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { + // Check that global reward index has been updated as expected + globalRewardIndex, found := globalRewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(expectedRewardIndex, globalRewardIndex) + + // Check that the user's claim's reward index matches the corresponding global reward index + multiRewardIndex, found := claim.BorrowRewardIndexes.GetRewardIndex(tc.args.borrow.Denom) + suite.Require().True(found) + rewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(expectedRewardIndex, rewardIndex) + + // Check that the user's claim holds the expected amount of reward coins + suite.Require().Equal( + tc.args.expectedRewards.AmountOf(expectedRewardIndex.CollateralType), + claim.Reward.AmountOf(expectedRewardIndex.CollateralType), + ) + } + } + + // Only test cases with reward param updates continue past this point + if !tc.args.updateRewardsViaCommmittee { + return + } + + // If are no initial rewards per second, add new rewards through a committee param change + // 1. Construct incentive's new HardBorrowRewardPeriods param + currIncentiveHardBorrowRewardPeriods := suite.keeper.GetParams(suite.ctx).HardBorrowRewardPeriods + multiRewardPeriod, found := currIncentiveHardBorrowRewardPeriods.GetMultiRewardPeriod(tc.args.borrow.Denom) + if found { + // Borrow denom's reward period exists, but it doesn't have any rewards per second + index, found := currIncentiveHardBorrowRewardPeriods.GetMultiRewardPeriodIndex(tc.args.borrow.Denom) + suite.Require().True(found) + multiRewardPeriod.RewardsPerSecond = tc.args.updatedRewardsPerSecond + currIncentiveHardBorrowRewardPeriods[index] = multiRewardPeriod + } else { + // Borrow denom's reward period does not exist + _, found := currIncentiveHardBorrowRewardPeriods.GetMultiRewardPeriodIndex(tc.args.borrow.Denom) + suite.Require().False(found) + newMultiRewardPeriod := types.NewMultiRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.updatedRewardsPerSecond) + currIncentiveHardBorrowRewardPeriods = append(currIncentiveHardBorrowRewardPeriods, newMultiRewardPeriod) + } + + // 2. Construct the parameter change proposal to update HardBorrowRewardPeriods param + pubProposal := params.NewParameterChangeProposal( + "Update hard borrow rewards", "Adds a new reward coin to the incentive module's hard borrow rewards.", + []params.ParamChange{ + { + Subspace: types.ModuleName, // target incentive module + Key: string(types.KeyHardBorrowRewardPeriods), // target hard borrow rewards key + Value: string(suite.app.Codec().MustMarshalJSON(currIncentiveHardBorrowRewardPeriods)), + }, + }, + ) + + // 3. Ensure proposal is properly formed + err = suite.committeeKeeper.ValidatePubProposal(suite.ctx, pubProposal) + suite.Require().NoError(err) + + // 4. Committee creates proposal + committeeMemberOne := suite.addrs[0] + committeeMemberTwo := suite.addrs[1] + proposalID, err := suite.committeeKeeper.SubmitProposal(suite.ctx, committeeMemberOne, 1, pubProposal) + suite.Require().NoError(err) + + // 5. Committee votes and passes proposal + err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberOne) + err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberTwo) + + // 6. Check proposal passed + proposalPasses, err := suite.committeeKeeper.GetProposalResult(suite.ctx, proposalID) + suite.Require().NoError(err) + suite.Require().True(proposalPasses) + + // 7. Run committee module's begin blocker to enact proposal + suite.NotPanics(func() { + committee.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}, suite.committeeKeeper) + }) + + // We need to accumulate hard supply-side rewards again + multiRewardPeriod, found = suite.keeper.GetHardBorrowRewardPeriods(suite.ctx, tc.args.borrow.Denom) + suite.Require().True(found) + + // But new borrow denoms don't have their PreviousHardBorrowRewardAccrualTime set yet, + // so we need to call the accumulation method once to set the initial reward accrual time + if tc.args.borrow.Denom != tc.args.incentiveBorrowRewardDenom { + err = suite.keeper.AccumulateHardBorrowRewards(suite.ctx, multiRewardPeriod) + suite.Require().NoError(err) + } + + // Now we can jump forward in time and accumulate rewards + updatedBlockTime = previousBlockTime.Add(time.Duration(int(time.Second) * tc.args.updatedTimeDuration)) + suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) + err = suite.keeper.AccumulateHardBorrowRewards(suite.ctx, multiRewardPeriod) + suite.Require().NoError(err) + + // After we've accumulated, run synchronize + borrow, found = hardKeeper.GetBorrow(suite.ctx, userAddr) + suite.Require().True(found) + suite.Require().NotPanics(func() { + suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow) + }) + + // Check that the global reward index's reward factor and user's claim have been updated as expected + globalRewardIndexes, found = suite.keeper.GetHardBorrowRewardIndexes(suite.ctx, tc.args.borrow.Denom) + suite.Require().True(found) + claim, found = suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) + suite.Require().True(found) + + for _, expectedRewardIndex := range tc.args.updatedExpectedRewardIndexes { + // Check that global reward index has been updated as expected + globalRewardIndex, found := globalRewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(expectedRewardIndex, globalRewardIndex) + // Check that the user's claim's reward index matches the corresponding global reward index + multiRewardIndex, found := claim.BorrowRewardIndexes.GetRewardIndex(tc.args.borrow.Denom) + suite.Require().True(found) + rewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(expectedRewardIndex, rewardIndex) + + // Check that the user's claim holds the expected amount of reward coins + suite.Require().Equal( + tc.args.updatedExpectedRewards.AmountOf(expectedRewardIndex.CollateralType), + claim.Reward.AmountOf(expectedRewardIndex.CollateralType), + ) + } + }) + } +} + +func (suite *KeeperTestSuite) TestUpdateHardBorrowIndexDenoms() { + type withdrawModification struct { + coins sdk.Coins + repay bool + } + + type args struct { + initialDeposit sdk.Coins + firstBorrow sdk.Coins + modification withdrawModification + rewardsPerSecond sdk.Coins + initialTime time.Time + expectedBorrowIndexDenoms []string + } + type test struct { + name string + args args + } + + testCases := []test{ + { + "single reward denom: update adds one borrow reward index", + args{ + initialDeposit: cs(c("bnb", 10000000000)), + firstBorrow: cs(c("bnb", 50000000)), + modification: withdrawModification{coins: cs(c("ukava", 500000000))}, + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedBorrowIndexDenoms: []string{"bnb", "ukava"}, + }, + }, + { + "single reward denom: update adds multiple borrow supply reward indexes", + args{ + initialDeposit: cs(c("btcb", 10000000000)), + firstBorrow: cs(c("btcb", 50000000)), + modification: withdrawModification{coins: cs(c("ukava", 500000000), c("bnb", 50000000000), c("xrp", 50000000000))}, + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedBorrowIndexDenoms: []string{"btcb", "ukava", "bnb", "xrp"}, + }, + }, + { + "single reward denom: update doesn't add duplicate borrow reward index for same denom", + args{ + initialDeposit: cs(c("bnb", 100000000000)), + firstBorrow: cs(c("bnb", 50000000)), + modification: withdrawModification{coins: cs(c("bnb", 50000000000))}, + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedBorrowIndexDenoms: []string{"bnb"}, + }, + }, + { + "multiple reward denoms: update adds one borrow reward index", + args{ + initialDeposit: cs(c("bnb", 10000000000)), + firstBorrow: cs(c("bnb", 50000000)), + modification: withdrawModification{coins: cs(c("ukava", 500000000))}, + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedBorrowIndexDenoms: []string{"bnb", "ukava"}, + }, + }, + { + "multiple reward denoms: update adds multiple borrow supply reward indexes", + args{ + initialDeposit: cs(c("btcb", 10000000000)), + firstBorrow: cs(c("btcb", 50000000)), + modification: withdrawModification{coins: cs(c("ukava", 500000000), c("bnb", 50000000000), c("xrp", 50000000000))}, + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedBorrowIndexDenoms: []string{"btcb", "ukava", "bnb", "xrp"}, + }, + }, + { + "multiple reward denoms: update doesn't add duplicate borrow reward index for same denom", + args{ + initialDeposit: cs(c("bnb", 100000000000)), + firstBorrow: cs(c("bnb", 50000000)), + modification: withdrawModification{coins: cs(c("bnb", 50000000000))}, + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedBorrowIndexDenoms: []string{"bnb"}, + }, + }, + { + "single reward denom: fully repaying a denom deletes the denom's supply reward index", + args{ + initialDeposit: cs(c("bnb", 1000000000)), + firstBorrow: cs(c("bnb", 100000000)), + modification: withdrawModification{coins: cs(c("bnb", 1100000000)), repay: true}, + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedBorrowIndexDenoms: []string{}, + }, + }, + { + "single reward denom: fully repaying a denom deletes only the denom's supply reward index", + args{ + initialDeposit: cs(c("bnb", 1000000000)), + firstBorrow: cs(c("bnb", 100000000), c("ukava", 10000000)), + modification: withdrawModification{coins: cs(c("bnb", 1100000000)), repay: true}, + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedBorrowIndexDenoms: []string{"ukava"}, + }, + }, + { + "multiple reward denoms: fully repaying a denom deletes the denom's supply reward index", + args{ + initialDeposit: cs(c("bnb", 1000000000)), + firstBorrow: cs(c("bnb", 100000000), c("ukava", 10000000)), + modification: withdrawModification{coins: cs(c("bnb", 1100000000)), repay: true}, + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedBorrowIndexDenoms: []string{"ukava"}, + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWithGenState() + suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) + + // Mint coins to hard module account so it can service borrow requests + supplyKeeper := suite.app.GetSupplyKeeper() + hardMaccCoins := tc.args.firstBorrow.Add(tc.args.modification.coins...) + supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) + + // Set up generic reward periods + var multiRewardPeriods types.MultiRewardPeriods + var rewardPeriods types.RewardPeriods + for i, denom := range tc.args.expectedBorrowIndexDenoms { + // Create just one reward period for USDX Minting / Hard Delegator reward periods (otherwise params will panic on duplicate) + if i == 0 { + rewardPeriod := types.NewRewardPeriod(true, denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[i]) + rewardPeriods = append(rewardPeriods, rewardPeriod) + } + multiRewardPeriod := types.NewMultiRewardPeriod(true, denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond) + multiRewardPeriods = append(multiRewardPeriods, multiRewardPeriod) + } + + // Setup incentive state + params := types.NewParams( + rewardPeriods, multiRewardPeriods, multiRewardPeriods, rewardPeriods, + types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + tc.args.initialTime.Add(time.Hour*24*365*5), + ) + suite.keeper.SetParams(suite.ctx, params) + + // Set each expected borrow denom's previous accrual time and borrow reward factor + var rewardIndexes types.RewardIndexes + for _, rewardCoin := range tc.args.rewardsPerSecond { + rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) + rewardIndexes = append(rewardIndexes, rewardIndex) + } + for _, denom := range tc.args.expectedBorrowIndexDenoms { + suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, denom, tc.args.initialTime) + suite.keeper.SetHardBorrowRewardIndexes(suite.ctx, denom, rewardIndexes) + } + + // User deposits initial funds (so that user can borrow) + hardKeeper := suite.app.GetHardKeeper() + userAddr := suite.addrs[3] + err := hardKeeper.Deposit(suite.ctx, userAddr, tc.args.initialDeposit) + suite.Require().NoError(err) + + // Confirm that claim exists but no borrow reward indexes have been added + claimAfterDeposit, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) + suite.Require().True(found) + suite.Require().Equal(0, len(claimAfterDeposit.BorrowRewardIndexes)) + + // User borrows (first time) + err = hardKeeper.Borrow(suite.ctx, userAddr, tc.args.firstBorrow) + suite.Require().NoError(err) + + // Confirm that claim's borrow reward indexes have been updated + claimAfterFirstBorrow, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) + suite.Require().True(found) + for _, coin := range tc.args.firstBorrow { + _, hasIndex := claimAfterFirstBorrow.HasBorrowRewardIndex(coin.Denom) + suite.Require().True(hasIndex) + } + suite.Require().True(len(claimAfterFirstBorrow.BorrowRewardIndexes) == len(tc.args.firstBorrow)) + + // User modifies their Borrow by either repaying or borrowing more + if tc.args.modification.repay { + err = hardKeeper.Repay(suite.ctx, userAddr, userAddr, tc.args.modification.coins) + } else { + err = hardKeeper.Borrow(suite.ctx, userAddr, tc.args.modification.coins) + } + suite.Require().NoError(err) + + // Confirm that claim's borrow reward indexes contain expected values + claimAfterModification, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) + suite.Require().True(found) + for _, coin := range tc.args.modification.coins { + _, hasIndex := claimAfterModification.HasBorrowRewardIndex(coin.Denom) + if tc.args.modification.repay { + // Only false if denom is repaid in full + if tc.args.modification.coins.AmountOf(coin.Denom).GTE(tc.args.firstBorrow.AmountOf(coin.Denom)) { + suite.Require().False(hasIndex) + } + } else { + suite.Require().True(hasIndex) + } + } + suite.Require().True(len(claimAfterModification.BorrowRewardIndexes) == len(tc.args.expectedBorrowIndexDenoms)) + }) + } +} + +func (suite *KeeperTestSuite) TestSimulateHardBorrowRewardSynchronization() { + type args struct { + borrow sdk.Coin + rewardsPerSecond sdk.Coins + initialTime time.Time + blockTimes []int + expectedRewardIndexes types.RewardIndexes + expectedRewards sdk.Coins + } + type test struct { + name string + args args + } + + testCases := []test{ + { + "10 blocks", + args{ + borrow: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.001223540000173228"))}, + expectedRewards: cs(c("hard", 12235400)), + }, + }, + { + "10 blocks - long block time", + args{ + borrow: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("10.571385603126235340"))}, + expectedRewards: cs(c("hard", 105713856031)), + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWithGenState() + suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) + + // Mint coins to hard module account + supplyKeeper := suite.app.GetSupplyKeeper() + hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) + supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) + + // setup incentive state + params := types.NewParams( + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, + types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + tc.args.initialTime.Add(time.Hour*24*365*5), + ) + suite.keeper.SetParams(suite.ctx, params) + suite.keeper.SetPreviousHardBorrowRewardAccrualTime(suite.ctx, tc.args.borrow.Denom, tc.args.initialTime) + var rewardIndexes types.RewardIndexes + for _, rewardCoin := range tc.args.rewardsPerSecond { + rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) + rewardIndexes = append(rewardIndexes, rewardIndex) + } + suite.keeper.SetHardBorrowRewardIndexes(suite.ctx, tc.args.borrow.Denom, rewardIndexes) + + // Set up hard state (interest factor for the relevant denom) + suite.hardKeeper.SetSupplyInterestFactor(suite.ctx, tc.args.borrow.Denom, sdk.MustNewDecFromStr("1.0")) + suite.hardKeeper.SetBorrowInterestFactor(suite.ctx, tc.args.borrow.Denom, sdk.MustNewDecFromStr("1.0")) + suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.borrow.Denom, tc.args.initialTime) + + // User deposits and borrows to increase total borrowed amount + hardKeeper := suite.app.GetHardKeeper() + userAddr := suite.addrs[3] + err := hardKeeper.Deposit(suite.ctx, userAddr, sdk.NewCoins(sdk.NewCoin(tc.args.borrow.Denom, tc.args.borrow.Amount.Mul(sdk.NewInt(2))))) + suite.Require().NoError(err) + err = hardKeeper.Borrow(suite.ctx, userAddr, sdk.NewCoins(tc.args.borrow)) + suite.Require().NoError(err) + + // Check that Hard hooks initialized a HardLiquidityProviderClaim + claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) + suite.Require().True(found) + multiRewardIndex, _ := claim.BorrowRewardIndexes.GetRewardIndex(tc.args.borrow.Denom) + for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { + currRewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(sdk.ZeroDec(), currRewardIndex.RewardFactor) + } + + // Run accumulator at several intervals + var timeElapsed int + previousBlockTime := suite.ctx.BlockTime() + for _, t := range tc.args.blockTimes { + timeElapsed += t + updatedBlockTime := previousBlockTime.Add(time.Duration(int(time.Second) * t)) + previousBlockTime = updatedBlockTime + blockCtx := suite.ctx.WithBlockTime(updatedBlockTime) + + // Run Hard begin blocker for each block ctx to update denom's interest factor + hard.BeginBlocker(blockCtx, suite.hardKeeper) + + // Accumulate hard borrow-side rewards + multiRewardPeriod, found := suite.keeper.GetHardBorrowRewardPeriods(blockCtx, tc.args.borrow.Denom) + suite.Require().True(found) + err := suite.keeper.AccumulateHardBorrowRewards(blockCtx, multiRewardPeriod) + suite.Require().NoError(err) + } + updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed)) + suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) + + // Confirm that the user's claim hasn't been synced + claimPre, foundPre := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) + suite.Require().True(foundPre) + multiRewardIndexPre, _ := claimPre.BorrowRewardIndexes.GetRewardIndex(tc.args.borrow.Denom) + for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { + currRewardIndex, found := multiRewardIndexPre.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(sdk.ZeroDec(), currRewardIndex.RewardFactor) + } + + // Check that the synced claim held in memory has properly simulated syncing + syncedClaim := suite.keeper.SimulateHardSynchronization(suite.ctx, claim) + for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { + // Check that the user's claim's reward index matches the expected reward index + multiRewardIndex, found := syncedClaim.BorrowRewardIndexes.GetRewardIndex(tc.args.borrow.Denom) + suite.Require().True(found) + rewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(expectedRewardIndex, rewardIndex) + + // Check that the user's claim holds the expected amount of reward coins + suite.Require().Equal( + tc.args.expectedRewards.AmountOf(expectedRewardIndex.CollateralType), + syncedClaim.Reward.AmountOf(expectedRewardIndex.CollateralType), + ) + } + }) + } +} diff --git a/x/incentive/keeper/rewards_delegator.go b/x/incentive/keeper/rewards_delegator.go new file mode 100644 index 00000000..fc0a149c --- /dev/null +++ b/x/incentive/keeper/rewards_delegator.go @@ -0,0 +1,131 @@ +package keeper + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/incentive/types" +) + +// AccumulateHardDelegatorRewards updates the rewards accumulated for the input reward period +func (k Keeper) AccumulateHardDelegatorRewards(ctx sdk.Context, rewardPeriod types.RewardPeriod) error { + previousAccrualTime, found := k.GetPreviousHardDelegatorRewardAccrualTime(ctx, rewardPeriod.CollateralType) + if !found { + k.SetPreviousHardDelegatorRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) + return nil + } + timeElapsed := CalculateTimeElapsed(rewardPeriod.Start, rewardPeriod.End, ctx.BlockTime(), previousAccrualTime) + if timeElapsed.IsZero() { + return nil + } + if rewardPeriod.RewardsPerSecond.Amount.IsZero() { + k.SetPreviousHardDelegatorRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) + return nil + } + + totalBonded := k.stakingKeeper.TotalBondedTokens(ctx).ToDec() + if totalBonded.IsZero() { + k.SetPreviousHardDelegatorRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) + return nil + } + + newRewards := timeElapsed.Mul(rewardPeriod.RewardsPerSecond.Amount) + rewardFactor := newRewards.ToDec().Quo(totalBonded) + + previousRewardFactor, found := k.GetHardDelegatorRewardFactor(ctx, rewardPeriod.CollateralType) + if !found { + previousRewardFactor = sdk.ZeroDec() + } + newRewardFactor := previousRewardFactor.Add(rewardFactor) + k.SetHardDelegatorRewardFactor(ctx, rewardPeriod.CollateralType, newRewardFactor) + k.SetPreviousHardDelegatorRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) + return nil +} + +// InitializeHardDelegatorReward initializes the delegator reward index of a hard claim +func (k Keeper) InitializeHardDelegatorReward(ctx sdk.Context, delegator sdk.AccAddress) { + delegatorFactor, foundDelegatorFactor := k.GetHardDelegatorRewardFactor(ctx, types.BondDenom) + if !foundDelegatorFactor { // Should always be found... + delegatorFactor = sdk.ZeroDec() + } + + delegatorRewardIndexes := types.NewRewardIndex(types.BondDenom, delegatorFactor) + + claim, found := k.GetHardLiquidityProviderClaim(ctx, delegator) + if !found { + // Instantiate claim object + claim = types.NewHardLiquidityProviderClaim(delegator, sdk.Coins{}, nil, nil, nil) + } else { + k.SynchronizeHardDelegatorRewards(ctx, delegator, nil, false) + claim, _ = k.GetHardLiquidityProviderClaim(ctx, delegator) + } + + claim.DelegatorRewardIndexes = types.RewardIndexes{delegatorRewardIndexes} + k.SetHardLiquidityProviderClaim(ctx, claim) +} + +// SynchronizeHardDelegatorRewards updates the claim object by adding any accumulated rewards, and setting the reward indexes to the global values. +// valAddr and shouldIncludeValidator are used to ignore or include delegations to a particular validator when summing up the total delegation. +// Normally only delegations to Bonded validators are included in the total. This is needed as staking hooks are sometimes called on the wrong side of a validator's state update (from this module's perspective). +func (k Keeper) SynchronizeHardDelegatorRewards(ctx sdk.Context, delegator sdk.AccAddress, valAddr sdk.ValAddress, shouldIncludeValidator bool) { + claim, found := k.GetHardLiquidityProviderClaim(ctx, delegator) + if !found { + return + } + + delagatorFactor, found := k.GetHardDelegatorRewardFactor(ctx, types.BondDenom) + if !found { + return + } + + delegatorIndex, hasDelegatorRewardIndex := claim.HasDelegatorRewardIndex(types.BondDenom) + if !hasDelegatorRewardIndex { + return + } + + userRewardFactor := claim.DelegatorRewardIndexes[delegatorIndex].RewardFactor + rewardsAccumulatedFactor := delagatorFactor.Sub(userRewardFactor) + if rewardsAccumulatedFactor.IsNegative() { + panic(fmt.Sprintf("reward accumulation factor cannot be negative: %s", rewardsAccumulatedFactor)) + } + claim.DelegatorRewardIndexes[delegatorIndex].RewardFactor = delagatorFactor + + totalDelegated := sdk.ZeroDec() + + delegations := k.stakingKeeper.GetDelegatorDelegations(ctx, delegator, 200) + for _, delegation := range delegations { + validator, found := k.stakingKeeper.GetValidator(ctx, delegation.GetValidatorAddr()) + if !found { + continue + } + + if valAddr == nil { + // Delegators don't accumulate rewards if their validator is unbonded + if validator.GetStatus() != sdk.Bonded { + continue + } + } else { + if !shouldIncludeValidator && validator.OperatorAddress.Equals(valAddr) { + // ignore tokens delegated to the validator + continue + } + } + + if validator.GetTokens().IsZero() { + continue + } + + delegatedTokens := validator.TokensFromShares(delegation.GetShares()) + if delegatedTokens.IsNegative() { + continue + } + totalDelegated = totalDelegated.Add(delegatedTokens) + } + rewardsEarned := rewardsAccumulatedFactor.Mul(totalDelegated).RoundInt() + + // Add rewards to delegator's hard claim + newRewardsCoin := sdk.NewCoin(types.HardLiquidityRewardDenom, rewardsEarned) + claim.Reward = claim.Reward.Add(newRewardsCoin) + k.SetHardLiquidityProviderClaim(ctx, claim) +} diff --git a/x/incentive/keeper/rewards_delegator_test.go b/x/incentive/keeper/rewards_delegator_test.go new file mode 100644 index 00000000..96d2e862 --- /dev/null +++ b/x/incentive/keeper/rewards_delegator_test.go @@ -0,0 +1,731 @@ +package keeper_test + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/staking" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto/ed25519" + + "github.com/kava-labs/kava/x/hard" + hardtypes "github.com/kava-labs/kava/x/hard/types" + "github.com/kava-labs/kava/x/incentive/types" +) + +func (suite *KeeperTestSuite) TestAccumulateHardDelegatorRewards() { + type args struct { + delegation sdk.Coin + rewardsPerSecond sdk.Coin + initialTime time.Time + timeElapsed int + expectedRewardFactor sdk.Dec + } + type test struct { + name string + args args + } + testCases := []test{ + { + "7 seconds", + args{ + delegation: c("ukava", 1_000_000), + rewardsPerSecond: c("hard", 122354), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + timeElapsed: 7, + expectedRewardFactor: d("0.428239000000000000"), + }, + }, + { + "1 day", + args{ + delegation: c("ukava", 1_000_000), + rewardsPerSecond: c("hard", 122354), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + timeElapsed: 86400, + expectedRewardFactor: d("5285.692800000000000000"), + }, + }, + { + "0 seconds", + args{ + delegation: c("ukava", 1_000_000), + rewardsPerSecond: c("hard", 122354), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + timeElapsed: 0, + expectedRewardFactor: d("0.0"), + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWithGenState() + suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) + + // Mint coins to hard module account + supplyKeeper := suite.app.GetSupplyKeeper() + hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) + supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) + + // Set up incentive state + params := types.NewParams( + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), cs(tc.args.rewardsPerSecond))}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), cs(tc.args.rewardsPerSecond))}, + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + tc.args.initialTime.Add(time.Hour*24*365*5), + ) + suite.keeper.SetParams(suite.ctx, params) + suite.keeper.SetPreviousHardDelegatorRewardAccrualTime(suite.ctx, tc.args.delegation.Denom, tc.args.initialTime) + suite.keeper.SetHardDelegatorRewardFactor(suite.ctx, tc.args.delegation.Denom, sdk.ZeroDec()) + + // Set up hard state (interest factor for the relevant denom) + suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.delegation.Denom, tc.args.initialTime) + + err := suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[0], tc.args.delegation) + suite.Require().NoError(err) + err = suite.deliverMsgDelegate(suite.ctx, suite.addrs[0], suite.validatorAddrs[0], tc.args.delegation) + suite.Require().NoError(err) + + staking.EndBlocker(suite.ctx, suite.stakingKeeper) + + // Set up chain context at future time + runAtTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * tc.args.timeElapsed)) + runCtx := suite.ctx.WithBlockTime(runAtTime) + + // Run Hard begin blocker in order to update the denom's index factor + hard.BeginBlocker(runCtx, suite.hardKeeper) + + rewardPeriod, found := suite.keeper.GetHardDelegatorRewardPeriod(runCtx, tc.args.delegation.Denom) + suite.Require().True(found) + err = suite.keeper.AccumulateHardDelegatorRewards(runCtx, rewardPeriod) + suite.Require().NoError(err) + + rewardFactor, found := suite.keeper.GetHardDelegatorRewardFactor(runCtx, tc.args.delegation.Denom) + suite.Require().Equal(tc.args.expectedRewardFactor, rewardFactor) + }) + } +} + +func (suite *KeeperTestSuite) TestSynchronizeHardDelegatorReward() { + type args struct { + delegation sdk.Coin + rewardsPerSecond sdk.Coin + initialTime time.Time + blockTimes []int + expectedRewardFactor sdk.Dec + expectedRewards sdk.Coins + } + type test struct { + name string + args args + } + + testCases := []test{ + { + "10 blocks", + args{ + delegation: c("ukava", 1_000_000), + rewardsPerSecond: c("hard", 122354), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + expectedRewardFactor: d("6.117700000000000000"), + expectedRewards: cs(c("hard", 6117700)), + }, + }, + { + "10 blocks - long block time", + args{ + delegation: c("ukava", 1_000_000), + rewardsPerSecond: c("hard", 122354), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, + expectedRewardFactor: d("52856.928000000000000000"), + expectedRewards: cs(c("hard", 52856928000)), + }, + }, + { + "delegator reward index updated when reward is zero", + args{ + delegation: c("ukava", 1), + rewardsPerSecond: c("hard", 1), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + expectedRewardFactor: d("0.000099999900000100"), + expectedRewards: nil, + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWithGenState() + suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) + + // Mint coins to hard module account + supplyKeeper := suite.app.GetSupplyKeeper() + hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) + supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) + + // setup incentive state + params := types.NewParams( + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), cs(tc.args.rewardsPerSecond))}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), cs(tc.args.rewardsPerSecond))}, + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + tc.args.initialTime.Add(time.Hour*24*365*5), + ) + suite.keeper.SetParams(suite.ctx, params) + suite.keeper.SetPreviousHardDelegatorRewardAccrualTime(suite.ctx, tc.args.delegation.Denom, tc.args.initialTime) + suite.keeper.SetHardDelegatorRewardFactor(suite.ctx, tc.args.delegation.Denom, sdk.ZeroDec()) + + // Set up hard state (interest factor for the relevant denom) + suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.delegation.Denom, tc.args.initialTime) + + // Create validator account + staking.BeginBlocker(suite.ctx, suite.stakingKeeper) + selfDelegationCoins := c("ukava", 1_000_000) + err := suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[0], selfDelegationCoins) + suite.Require().NoError(err) + staking.EndBlocker(suite.ctx, suite.stakingKeeper) + + // Delegator delegates + err = suite.deliverMsgDelegate(suite.ctx, suite.addrs[0], suite.validatorAddrs[0], tc.args.delegation) + suite.Require().NoError(err) + + // Check that validator account has been created and delegation was successful + valAcc, found := suite.stakingKeeper.GetValidator(suite.ctx, suite.validatorAddrs[0]) + suite.True(found) + suite.Require().Equal(valAcc.Status, sdk.Bonded) + suite.Require().Equal(valAcc.Tokens, tc.args.delegation.Amount.Add(selfDelegationCoins.Amount)) + + // Check that Staking hooks initialized a HardLiquidityProviderClaim + claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0]) + suite.Require().True(found) + suite.Require().Equal(sdk.ZeroDec(), claim.DelegatorRewardIndexes[0].RewardFactor) + + // Run accumulator at several intervals + var timeElapsed int + previousBlockTime := suite.ctx.BlockTime() + for _, t := range tc.args.blockTimes { + timeElapsed += t + updatedBlockTime := previousBlockTime.Add(time.Duration(int(time.Second) * t)) + previousBlockTime = updatedBlockTime + blockCtx := suite.ctx.WithBlockTime(updatedBlockTime) + + // Run Hard begin blocker for each block ctx to update denom's interest factor + hard.BeginBlocker(blockCtx, suite.hardKeeper) + + rewardPeriod, found := suite.keeper.GetHardDelegatorRewardPeriod(blockCtx, tc.args.delegation.Denom) + suite.Require().True(found) + + err := suite.keeper.AccumulateHardDelegatorRewards(blockCtx, rewardPeriod) + suite.Require().NoError(err) + } + updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed)) + suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) + + // After we've accumulated, run synchronize + suite.Require().NotPanics(func() { + suite.keeper.SynchronizeHardDelegatorRewards(suite.ctx, suite.addrs[0], nil, false) + }) + + // Check that reward factor and claim have been updated as expected + rewardFactor, found := suite.keeper.GetHardDelegatorRewardFactor(suite.ctx, tc.args.delegation.Denom) + suite.Require().Equal(tc.args.expectedRewardFactor, rewardFactor) + + claim, found = suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0]) + suite.Require().True(found) + suite.Require().Equal(tc.args.expectedRewardFactor, claim.DelegatorRewardIndexes[0].RewardFactor) + suite.Require().Equal(tc.args.expectedRewards, claim.Reward) + }) + } +} + +func (suite *KeeperTestSuite) TestSimulateHardDelegatorRewardSynchronization() { + type args struct { + delegation sdk.Coin + rewardsPerSecond sdk.Coins + initialTime time.Time + blockTimes []int + expectedRewardIndexes types.RewardIndexes + expectedRewards sdk.Coins + } + type test struct { + name string + args args + } + + testCases := []test{ + { + "10 blocks", + args{ + delegation: c("ukava", 1_000_000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("ukava", d("6.117700000000000000"))}, // Here the reward index stores data differently than inside a MultiRewardIndex + expectedRewards: cs(c("hard", 6117700)), + }, + }, + { + "10 blocks - long block time", + args{ + delegation: c("ukava", 1_000_000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("ukava", d("52856.928000000000000000"))}, + expectedRewards: cs(c("hard", 52856928000)), + }, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWithGenState() + suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) + + // Mint coins to hard module account + supplyKeeper := suite.app.GetSupplyKeeper() + hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) + supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) + + // setup incentive state + params := types.NewParams( + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, + types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + tc.args.initialTime.Add(time.Hour*24*365*5), + ) + suite.keeper.SetParams(suite.ctx, params) + suite.keeper.SetPreviousHardDelegatorRewardAccrualTime(suite.ctx, tc.args.delegation.Denom, tc.args.initialTime) + suite.keeper.SetHardDelegatorRewardFactor(suite.ctx, tc.args.delegation.Denom, sdk.ZeroDec()) + + // Set up hard state (interest factor for the relevant denom) + suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.delegation.Denom, tc.args.initialTime) + + // Delegator delegates + err := suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[0], tc.args.delegation) + suite.Require().NoError(err) + err = suite.deliverMsgDelegate(suite.ctx, suite.addrs[0], suite.validatorAddrs[0], tc.args.delegation) + suite.Require().NoError(err) + + staking.EndBlocker(suite.ctx, suite.stakingKeeper) + + // Check that Staking hooks initialized a HardLiquidityProviderClaim + claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0]) + suite.Require().True(found) + suite.Require().Equal(sdk.ZeroDec(), claim.DelegatorRewardIndexes[0].RewardFactor) + + // Run accumulator at several intervals + var timeElapsed int + previousBlockTime := suite.ctx.BlockTime() + for _, t := range tc.args.blockTimes { + timeElapsed += t + updatedBlockTime := previousBlockTime.Add(time.Duration(int(time.Second) * t)) + previousBlockTime = updatedBlockTime + blockCtx := suite.ctx.WithBlockTime(updatedBlockTime) + + // Run Hard begin blocker for each block ctx to update denom's interest factor + hard.BeginBlocker(blockCtx, suite.hardKeeper) + + // Accumulate hard delegator rewards + rewardPeriod, found := suite.keeper.GetHardDelegatorRewardPeriod(blockCtx, tc.args.delegation.Denom) + suite.Require().True(found) + err := suite.keeper.AccumulateHardDelegatorRewards(blockCtx, rewardPeriod) + suite.Require().NoError(err) + } + updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed)) + suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) + + // Check that the synced claim held in memory has properly simulated syncing + syncedClaim := suite.keeper.SimulateHardSynchronization(suite.ctx, claim) + for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { + // Check that the user's claim's reward index matches the expected reward index + rewardIndex, found := syncedClaim.DelegatorRewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(expectedRewardIndex, rewardIndex) + + // Check that the user's claim holds the expected amount of reward coins + suite.Require().Equal( + tc.args.expectedRewards.AmountOf(expectedRewardIndex.CollateralType), + syncedClaim.Reward.AmountOf(expectedRewardIndex.CollateralType), + ) + } + }) + } +} + +func (suite *KeeperTestSuite) deliverMsgCreateValidator(ctx sdk.Context, address sdk.ValAddress, selfDelegation sdk.Coin) error { + msg := staking.NewMsgCreateValidator( + address, + ed25519.GenPrivKey().PubKey(), + selfDelegation, + staking.Description{}, + staking.NewCommissionRates(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()), + sdk.NewInt(1_000_000), + ) + handleStakingMsg := staking.NewHandler(suite.stakingKeeper) + _, err := handleStakingMsg(ctx, msg) + return err +} + +func (suite *KeeperTestSuite) deliverMsgDelegate(ctx sdk.Context, delegator sdk.AccAddress, validator sdk.ValAddress, amount sdk.Coin) error { + msg := staking.NewMsgDelegate( + delegator, + validator, + amount, + ) + handleStakingMsg := staking.NewHandler(suite.stakingKeeper) + _, err := handleStakingMsg(ctx, msg) + return err +} + +func (suite *KeeperTestSuite) deliverMsgRedelegate(ctx sdk.Context, delegator sdk.AccAddress, sourceValidator, destinationValidator sdk.ValAddress, amount sdk.Coin) error { + msg := staking.NewMsgBeginRedelegate( + delegator, + sourceValidator, + destinationValidator, + amount, + ) + handleStakingMsg := staking.NewHandler(suite.stakingKeeper) + _, err := handleStakingMsg(ctx, msg) + return err +} + +// given a user has a delegation to a bonded validator, when the validator starts unbonding, the user does not accumulate rewards +func (suite *KeeperTestSuite) TestUnbondingValidatorSyncsClaim() { + suite.SetupWithGenState() + initialTime := time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC) + suite.ctx = suite.ctx.WithBlockTime(initialTime) + blockDuration := 10 * time.Second + + // Setup incentive state + rewardsPerSecond := c("hard", 122354) + bondDenom := "ukava" + params := types.NewParams( + nil, + nil, + nil, + types.RewardPeriods{ + types.NewRewardPeriod(true, bondDenom, initialTime.Add(-1*oneYear), initialTime.Add(4*oneYear), rewardsPerSecond), + }, + types.DefaultMultipliers, + initialTime.Add(5*oneYear), + ) + suite.keeper.SetParams(suite.ctx, params) + suite.keeper.SetPreviousHardDelegatorRewardAccrualTime(suite.ctx, bondDenom, initialTime) + suite.keeper.SetHardDelegatorRewardFactor(suite.ctx, bondDenom, sdk.ZeroDec()) + + // Reduce the size of the validator set + stakingParams := suite.app.GetStakingKeeper().GetParams(suite.ctx) + stakingParams.MaxValidators = 2 + suite.app.GetStakingKeeper().SetParams(suite.ctx, stakingParams) + + // Create 3 validators + err := suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[0], c(bondDenom, 10_000_000)) + suite.Require().NoError(err) + err = suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[1], c(bondDenom, 5_000_000)) + suite.Require().NoError(err) + err = suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[2], c(bondDenom, 1_000_000)) + suite.Require().NoError(err) + + // End the block so top validators become bonded + _ = suite.app.EndBlocker(suite.ctx, abci.RequestEndBlock{}) + + suite.ctx = suite.ctx.WithBlockTime(initialTime.Add(1 * blockDuration)) + _ = suite.app.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}) // height and time in header are ignored by module begin blockers + + // Delegate to a bonded validator from the test user. This will initialize their incentive claim. + err = suite.deliverMsgDelegate(suite.ctx, suite.addrs[0], suite.validatorAddrs[1], c(bondDenom, 1_000_000)) + suite.Require().NoError(err) + + // Start a new block to accumulate some delegation rewards for the user. + _ = suite.app.EndBlocker(suite.ctx, abci.RequestEndBlock{}) + suite.ctx = suite.ctx.WithBlockTime(initialTime.Add(2 * blockDuration)) + _ = suite.app.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}) // height and time in header are ignored by module begin blockers + + // Delegate to the unbonded validator to push it into the bonded validator set, pushing out the user's delegated validator + err = suite.deliverMsgDelegate(suite.ctx, suite.addrs[2], suite.validatorAddrs[2], c(bondDenom, 8_000_000)) + suite.Require().NoError(err) + + // End the block to start unbonding the user's validator + _ = suite.app.EndBlocker(suite.ctx, abci.RequestEndBlock{}) + // but don't start the next block as it will accumulate delegator rewards and we won't be able to tell if the user's reward was synced. + + // Check that the user's claim has been synced. ie rewards added, index updated + claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0]) + suite.Require().True(found) + + globalIndex, found := suite.keeper.GetHardDelegatorRewardFactor(suite.ctx, bondDenom) + suite.Require().True(found) + claimIndex, found := claim.DelegatorRewardIndexes.GetRewardIndex(bondDenom) + suite.Require().True(found) + suite.Require().Equal(globalIndex, claimIndex.RewardFactor) + + suite.Require().Equal( + cs(c(rewardsPerSecond.Denom, 76471)), + claim.Reward, + ) + + // Run another block and check the claim is not accumulating more rewards + suite.ctx = suite.ctx.WithBlockTime(initialTime.Add(3 * blockDuration)) + _ = suite.app.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}) + + suite.keeper.SynchronizeHardDelegatorRewards(suite.ctx, suite.addrs[0], nil, false) + + // rewards are the same as before + laterClaim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0]) + suite.Require().True(found) + suite.Require().Equal(claim.Reward, laterClaim.Reward) + + // claim index has been updated to latest global value + laterClaimIndex, found := laterClaim.DelegatorRewardIndexes.GetRewardIndex(bondDenom) + suite.Require().True(found) + globalIndex, found = suite.keeper.GetHardDelegatorRewardFactor(suite.ctx, bondDenom) + suite.Require().True(found) + suite.Require().Equal(globalIndex, laterClaimIndex.RewardFactor) +} + +// given a user has a delegation to an unbonded validator, when the validator becomes bonded, the user starts accumulating rewards +func (suite *KeeperTestSuite) TestBondingValidatorSyncsClaim() { + suite.SetupWithGenState() + initialTime := time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC) + suite.ctx = suite.ctx.WithBlockTime(initialTime) + blockDuration := 10 * time.Second + + // Setup incentive state + rewardsPerSecond := c("hard", 122354) + bondDenom := "ukava" + params := types.NewParams( + nil, + nil, + nil, + types.RewardPeriods{ + types.NewRewardPeriod(true, bondDenom, initialTime.Add(-1*oneYear), initialTime.Add(4*oneYear), rewardsPerSecond), + }, + types.DefaultMultipliers, + initialTime.Add(5*oneYear), + ) + suite.keeper.SetParams(suite.ctx, params) + suite.keeper.SetPreviousHardDelegatorRewardAccrualTime(suite.ctx, bondDenom, initialTime) + suite.keeper.SetHardDelegatorRewardFactor(suite.ctx, bondDenom, sdk.ZeroDec()) + + // Reduce the size of the validator set + stakingParams := suite.app.GetStakingKeeper().GetParams(suite.ctx) + stakingParams.MaxValidators = 2 + suite.app.GetStakingKeeper().SetParams(suite.ctx, stakingParams) + + // Create 3 validators + err := suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[0], c(bondDenom, 10_000_000)) + suite.Require().NoError(err) + err = suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[1], c(bondDenom, 5_000_000)) + suite.Require().NoError(err) + err = suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[2], c(bondDenom, 1_000_000)) + suite.Require().NoError(err) + + // End the block so top validators become bonded + _ = suite.app.EndBlocker(suite.ctx, abci.RequestEndBlock{}) + + suite.ctx = suite.ctx.WithBlockTime(initialTime.Add(1 * blockDuration)) + _ = suite.app.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}) // height and time in header are ignored by module begin blockers + + // Delegate to an unbonded validator from the test user. This will initialize their incentive claim. + err = suite.deliverMsgDelegate(suite.ctx, suite.addrs[0], suite.validatorAddrs[2], c(bondDenom, 1_000_000)) + suite.Require().NoError(err) + + // Start a new block to accumulate some delegation rewards globally. + _ = suite.app.EndBlocker(suite.ctx, abci.RequestEndBlock{}) + suite.ctx = suite.ctx.WithBlockTime(initialTime.Add(2 * blockDuration)) + _ = suite.app.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}) + + // Delegate to the user's unbonded validator to push it into the bonded validator set + err = suite.deliverMsgDelegate(suite.ctx, suite.addrs[2], suite.validatorAddrs[2], c(bondDenom, 4_000_000)) + suite.Require().NoError(err) + + // End the block to bond the user's validator + _ = suite.app.EndBlocker(suite.ctx, abci.RequestEndBlock{}) + // but don't start the next block as it will accumulate delegator rewards and we won't be able to tell if the user's reward was synced. + + // Check that the user's claim has been synced. ie rewards added, index updated + claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0]) + suite.Require().True(found) + + globalIndex, found := suite.keeper.GetHardDelegatorRewardFactor(suite.ctx, bondDenom) + suite.Require().True(found) + claimIndex, found := claim.DelegatorRewardIndexes.GetRewardIndex(bondDenom) + suite.Require().True(found) + suite.Require().Equal(globalIndex, claimIndex.RewardFactor) + + suite.Require().Equal( + sdk.Coins(nil), + claim.Reward, + ) + + // Run another block and check the claim is accumulating more rewards + suite.ctx = suite.ctx.WithBlockTime(initialTime.Add(3 * blockDuration)) + _ = suite.app.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}) + + suite.keeper.SynchronizeHardDelegatorRewards(suite.ctx, suite.addrs[0], nil, false) + + // rewards are greater than before + laterClaim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0]) + suite.Require().True(found) + suite.Require().True(laterClaim.Reward.IsAllGT(claim.Reward)) + + // claim index has been updated to latest global value + laterClaimIndex, found := laterClaim.DelegatorRewardIndexes.GetRewardIndex(bondDenom) + suite.Require().True(found) + globalIndex, found = suite.keeper.GetHardDelegatorRewardFactor(suite.ctx, bondDenom) + suite.Require().True(found) + suite.Require().Equal(globalIndex, laterClaimIndex.RewardFactor) +} + +// If a validator is slashed delegators should have their claims synced +func (suite *KeeperTestSuite) TestSlashingValidatorSyncsClaim() { + suite.SetupWithGenState() + initialTime := time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC) + suite.ctx = suite.ctx.WithBlockTime(initialTime) + blockDuration := 10 * time.Second + + // Setup incentive state + rewardsPerSecond := c("hard", 122354) + bondDenom := "ukava" + params := types.NewParams( + nil, + nil, + nil, + types.RewardPeriods{ + types.NewRewardPeriod(true, bondDenom, initialTime.Add(-1*oneYear), initialTime.Add(4*oneYear), rewardsPerSecond), + }, + types.DefaultMultipliers, + initialTime.Add(5*oneYear), + ) + suite.keeper.SetParams(suite.ctx, params) + suite.keeper.SetPreviousHardDelegatorRewardAccrualTime(suite.ctx, bondDenom, initialTime.Add(-1*blockDuration)) + suite.keeper.SetHardDelegatorRewardFactor(suite.ctx, bondDenom, sdk.ZeroDec()) + + // Reduce the size of the validator set + stakingParams := suite.app.GetStakingKeeper().GetParams(suite.ctx) + stakingParams.MaxValidators = 2 + suite.app.GetStakingKeeper().SetParams(suite.ctx, stakingParams) + + // Create 2 validators + err := suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[0], c(bondDenom, 10_000_000)) + suite.Require().NoError(err) + err = suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[1], c(bondDenom, 10_000_000)) + suite.Require().NoError(err) + + // End the block so validators become bonded + _ = suite.app.EndBlocker(suite.ctx, abci.RequestEndBlock{}) + + suite.ctx = suite.ctx.WithBlockTime(initialTime.Add(1 * blockDuration)) + _ = suite.app.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}) // height and time in header are ignored by module begin blockers + + // Delegate to a bonded validator from the test user. This will initialize their incentive claim. + err = suite.deliverMsgDelegate(suite.ctx, suite.addrs[0], suite.validatorAddrs[1], c(bondDenom, 1_000_000)) + suite.Require().NoError(err) + + // Check that claim has been created with synced reward index but no reward coins + initialClaim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0]) + suite.True(found) + initialGlobalIndex, found := suite.keeper.GetHardDelegatorRewardFactor(suite.ctx, bondDenom) + suite.True(found) + initialClaimIndex, found := initialClaim.DelegatorRewardIndexes.GetRewardIndex(bondDenom) + suite.True(found) + suite.Require().Equal(initialGlobalIndex, initialClaimIndex.RewardFactor) + suite.True(initialClaim.Reward.Empty()) // Initial claim should not have any rewards + + // Start a new block to accumulate some delegation rewards for the user. + _ = suite.app.EndBlocker(suite.ctx, abci.RequestEndBlock{}) + suite.ctx = suite.ctx.WithBlockTime(initialTime.Add(2 * blockDuration)) + _ = suite.app.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}) // height and time in header are ignored by module begin blockers + + // Fetch validator and slash them + stakingKeeper := suite.app.GetStakingKeeper() + validator, found := stakingKeeper.GetValidator(suite.ctx, suite.validatorAddrs[1]) + suite.Require().True(found) + suite.Require().True(validator.GetTokens().IsPositive()) + fraction := sdk.NewDecWithPrec(5, 1) + stakingKeeper.Slash(suite.ctx, validator.ConsAddress(), suite.ctx.BlockHeight(), 10, fraction) + + // Check that the user's claim has been synced. ie rewards added, index updated + claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0]) + suite.Require().True(found) + globalIndex, found := suite.keeper.GetHardDelegatorRewardFactor(suite.ctx, bondDenom) + suite.Require().True(found) + claimIndex, found := claim.DelegatorRewardIndexes.GetRewardIndex(bondDenom) + suite.Require().True(found) + suite.Require().Equal(globalIndex, claimIndex.RewardFactor) + + // Check that rewards were added + suite.Require().Equal( + cs(c(rewardsPerSecond.Denom, 58264)), + claim.Reward, + ) + + // Check that reward factor increased from initial value + suite.True(claimIndex.RewardFactor.GT(initialClaimIndex.RewardFactor)) +} + +// Given a delegation to a bonded validator, when a user redelegates everything to another (bonded) validator, the user's claim is synced +func (suite *KeeperTestSuite) TestRedelegationSyncsClaim() { + suite.SetupWithGenState() + initialTime := time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC) + suite.ctx = suite.ctx.WithBlockTime(initialTime) + blockDuration := 10 * time.Second + + // Setup incentive state + rewardsPerSecond := c("hard", 122354) + bondDenom := "ukava" + params := types.NewParams( + nil, + nil, + nil, + types.RewardPeriods{ + types.NewRewardPeriod(true, bondDenom, initialTime.Add(-1*oneYear), initialTime.Add(4*oneYear), rewardsPerSecond), + }, + types.DefaultMultipliers, + initialTime.Add(5*oneYear), + ) + suite.keeper.SetParams(suite.ctx, params) + suite.keeper.SetPreviousHardDelegatorRewardAccrualTime(suite.ctx, bondDenom, initialTime) + suite.keeper.SetHardDelegatorRewardFactor(suite.ctx, bondDenom, sdk.ZeroDec()) + + // Create 2 validators + err := suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[0], c(bondDenom, 10_000_000)) + suite.Require().NoError(err) + err = suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[1], c(bondDenom, 5_000_000)) + suite.Require().NoError(err) + + // Delegatefrom the test user. This will initialize their incentive claim. + err = suite.deliverMsgDelegate(suite.ctx, suite.addrs[0], suite.validatorAddrs[0], c(bondDenom, 1_000_000)) + suite.Require().NoError(err) + + // Start a new block to accumulate some delegation rewards globally. + _ = suite.app.EndBlocker(suite.ctx, abci.RequestEndBlock{}) + suite.ctx = suite.ctx.WithBlockTime(initialTime.Add(1 * blockDuration)) + _ = suite.app.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}) // height and time in header are ignored by module begin blockers + + // Redelegate the user's delegation between the two validators. This should trigger hooks that sync the user's claim. + err = suite.deliverMsgRedelegate(suite.ctx, suite.addrs[0], suite.validatorAddrs[0], suite.validatorAddrs[1], c(bondDenom, 1_000_000)) + suite.Require().NoError(err) + + // Check that the user's claim has been synced. ie rewards added, index updated + claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0]) + suite.Require().True(found) + + globalIndex, found := suite.keeper.GetHardDelegatorRewardFactor(suite.ctx, bondDenom) + suite.Require().True(found) + claimIndex, found := claim.DelegatorRewardIndexes.GetRewardIndex(bondDenom) + suite.Require().True(found) + suite.Require().Equal(globalIndex, claimIndex.RewardFactor) + suite.Require().Equal( + cs(c(rewardsPerSecond.Denom, 76471)), + claim.Reward, + ) +} diff --git a/x/incentive/keeper/rewards_supply.go b/x/incentive/keeper/rewards_supply.go new file mode 100644 index 00000000..56d432fa --- /dev/null +++ b/x/incentive/keeper/rewards_supply.go @@ -0,0 +1,427 @@ +package keeper + +import ( + "fmt" + "math" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + + hardtypes "github.com/kava-labs/kava/x/hard/types" + "github.com/kava-labs/kava/x/incentive/types" +) + +// AccumulateHardSupplyRewards updates the rewards accumulated for the input reward period +func (k Keeper) AccumulateHardSupplyRewards(ctx sdk.Context, rewardPeriod types.MultiRewardPeriod) error { + previousAccrualTime, found := k.GetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType) + if !found { + k.SetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) + return nil + } + timeElapsed := CalculateTimeElapsed(rewardPeriod.Start, rewardPeriod.End, ctx.BlockTime(), previousAccrualTime) + if timeElapsed.IsZero() { + return nil + } + if rewardPeriod.RewardsPerSecond.IsZero() { + k.SetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) + return nil + } + + totalSuppliedCoins, foundTotalSuppliedCoins := k.hardKeeper.GetSuppliedCoins(ctx) + if !foundTotalSuppliedCoins { + k.SetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) + return nil + } + + totalSupplied := totalSuppliedCoins.AmountOf(rewardPeriod.CollateralType).ToDec() + if totalSupplied.IsZero() { + k.SetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) + return nil + } + + previousRewardIndexes, found := k.GetHardSupplyRewardIndexes(ctx, rewardPeriod.CollateralType) + if !found { + for _, rewardCoin := range rewardPeriod.RewardsPerSecond { + rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) + previousRewardIndexes = append(previousRewardIndexes, rewardIndex) + } + k.SetHardSupplyRewardIndexes(ctx, rewardPeriod.CollateralType, previousRewardIndexes) + } + hardFactor, found := k.hardKeeper.GetSupplyInterestFactor(ctx, rewardPeriod.CollateralType) + if !found { + k.SetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) + return nil + } + + newRewardIndexes := previousRewardIndexes + for _, rewardCoin := range rewardPeriod.RewardsPerSecond { + newRewards := rewardCoin.Amount.ToDec().Mul(timeElapsed.ToDec()) + previousRewardIndex, found := previousRewardIndexes.GetRewardIndex(rewardCoin.Denom) + if !found { + previousRewardIndex = types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) + } + + // Calculate new reward factor and update reward index + rewardFactor := newRewards.Mul(hardFactor).Quo(totalSupplied) + newRewardFactorValue := previousRewardIndex.RewardFactor.Add(rewardFactor) + newRewardIndex := types.NewRewardIndex(rewardCoin.Denom, newRewardFactorValue) + i, found := newRewardIndexes.GetFactorIndex(rewardCoin.Denom) + if found { + newRewardIndexes[i] = newRewardIndex + } else { + newRewardIndexes = append(newRewardIndexes, newRewardIndex) + } + } + k.SetHardSupplyRewardIndexes(ctx, rewardPeriod.CollateralType, newRewardIndexes) + k.SetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) + return nil +} + +// InitializeHardSupplyReward initializes the supply-side of a hard liquidity provider claim +// by creating the claim and setting the supply reward factor index +func (k Keeper) InitializeHardSupplyReward(ctx sdk.Context, deposit hardtypes.Deposit) { + var supplyRewardIndexes types.MultiRewardIndexes + for _, coin := range deposit.Amount { + globalRewardIndexes, foundGlobalRewardIndexes := k.GetHardSupplyRewardIndexes(ctx, coin.Denom) + var multiRewardIndex types.MultiRewardIndex + if foundGlobalRewardIndexes { + multiRewardIndex = types.NewMultiRewardIndex(coin.Denom, globalRewardIndexes) + } else { + multiRewardIndex = types.NewMultiRewardIndex(coin.Denom, types.RewardIndexes{}) + } + supplyRewardIndexes = append(supplyRewardIndexes, multiRewardIndex) + } + + claim, found := k.GetHardLiquidityProviderClaim(ctx, deposit.Depositor) + if !found { + // Instantiate claim object + claim = types.NewHardLiquidityProviderClaim(deposit.Depositor, sdk.Coins{}, nil, nil, nil) + } + + claim.SupplyRewardIndexes = supplyRewardIndexes + k.SetHardLiquidityProviderClaim(ctx, claim) +} + +// SynchronizeHardSupplyReward updates the claim object by adding any accumulated rewards +// and updating the reward index value +func (k Keeper) SynchronizeHardSupplyReward(ctx sdk.Context, deposit hardtypes.Deposit) { + claim, found := k.GetHardLiquidityProviderClaim(ctx, deposit.Depositor) + if !found { + return + } + + for _, coin := range deposit.Amount { + globalRewardIndexes, foundGlobalRewardIndexes := k.GetHardSupplyRewardIndexes(ctx, coin.Denom) + if !foundGlobalRewardIndexes { + continue + } + + userMultiRewardIndex, foundUserMultiRewardIndex := claim.SupplyRewardIndexes.GetRewardIndex(coin.Denom) + if !foundUserMultiRewardIndex { + continue + } + + userRewardIndexIndex, foundUserRewardIndexIndex := claim.SupplyRewardIndexes.GetRewardIndexIndex(coin.Denom) + if !foundUserRewardIndexIndex { + continue + } + + for _, globalRewardIndex := range globalRewardIndexes { + userRewardIndex, foundUserRewardIndex := userMultiRewardIndex.RewardIndexes.GetRewardIndex(globalRewardIndex.CollateralType) + if !foundUserRewardIndex { + // User deposited this coin type before it had rewards. When new rewards are added, legacy depositors + // should immediately begin earning rewards. Enable users to do so by updating their claim with the global + // reward index denom and start their reward factor at 0.0 + userRewardIndex = types.NewRewardIndex(globalRewardIndex.CollateralType, sdk.ZeroDec()) + userMultiRewardIndex.RewardIndexes = append(userMultiRewardIndex.RewardIndexes, userRewardIndex) + claim.SupplyRewardIndexes[userRewardIndexIndex] = userMultiRewardIndex + } + + globalRewardFactor := globalRewardIndex.RewardFactor + userRewardFactor := userRewardIndex.RewardFactor + rewardsAccumulatedFactor := globalRewardFactor.Sub(userRewardFactor) + if rewardsAccumulatedFactor.IsNegative() { + panic(fmt.Sprintf("reward accumulation factor cannot be negative: %s", rewardsAccumulatedFactor)) + } + + newRewardsAmount := rewardsAccumulatedFactor.Mul(deposit.Amount.AmountOf(coin.Denom).ToDec()).RoundInt() + + factorIndex, foundFactorIndex := userMultiRewardIndex.RewardIndexes.GetFactorIndex(globalRewardIndex.CollateralType) + if !foundFactorIndex { // should never trigger, as we basically do this check at the start of this loop + continue + } + claim.SupplyRewardIndexes[userRewardIndexIndex].RewardIndexes[factorIndex].RewardFactor = globalRewardIndex.RewardFactor + + newRewardsCoin := sdk.NewCoin(userRewardIndex.CollateralType, newRewardsAmount) + claim.Reward = claim.Reward.Add(newRewardsCoin) + } + } + k.SetHardLiquidityProviderClaim(ctx, claim) +} + +// UpdateHardSupplyIndexDenoms adds any new deposit denoms to the claim's supply reward index +func (k Keeper) UpdateHardSupplyIndexDenoms(ctx sdk.Context, deposit hardtypes.Deposit) { + claim, found := k.GetHardLiquidityProviderClaim(ctx, deposit.Depositor) + if !found { + claim = types.NewHardLiquidityProviderClaim(deposit.Depositor, sdk.Coins{}, nil, nil, nil) + } + + depositDenoms := getDenoms(deposit.Amount) + supplyRewardIndexDenoms := claim.SupplyRewardIndexes.GetCollateralTypes() + + uniqueDepositDenoms := setDifference(depositDenoms, supplyRewardIndexDenoms) + uniqueSupplyRewardDenoms := setDifference(supplyRewardIndexDenoms, depositDenoms) + + supplyRewardIndexes := claim.SupplyRewardIndexes + // Create a new multi-reward index in the claim for every new deposit denom + for _, denom := range uniqueDepositDenoms { + _, foundUserRewardIndexes := claim.SupplyRewardIndexes.GetRewardIndex(denom) + if !foundUserRewardIndexes { + globalSupplyRewardIndexes, foundGlobalSupplyRewardIndexes := k.GetHardSupplyRewardIndexes(ctx, denom) + var multiRewardIndex types.MultiRewardIndex + if foundGlobalSupplyRewardIndexes { + multiRewardIndex = types.NewMultiRewardIndex(denom, globalSupplyRewardIndexes) + } else { + multiRewardIndex = types.NewMultiRewardIndex(denom, types.RewardIndexes{}) + } + supplyRewardIndexes = append(supplyRewardIndexes, multiRewardIndex) + } + } + + // Delete multi-reward index from claim if the collateral type is no longer deposited + for _, denom := range uniqueSupplyRewardDenoms { + supplyRewardIndexes = supplyRewardIndexes.RemoveRewardIndex(denom) + } + + claim.SupplyRewardIndexes = supplyRewardIndexes + k.SetHardLiquidityProviderClaim(ctx, claim) +} + +// SynchronizeHardLiquidityProviderClaim adds any accumulated rewards +func (k Keeper) SynchronizeHardLiquidityProviderClaim(ctx sdk.Context, owner sdk.AccAddress) { + // Synchronize any hard liquidity supply-side rewards + deposit, foundDeposit := k.hardKeeper.GetDeposit(ctx, owner) + if foundDeposit { + k.SynchronizeHardSupplyReward(ctx, deposit) + } + + // Synchronize any hard liquidity borrow-side rewards + borrow, foundBorrow := k.hardKeeper.GetBorrow(ctx, owner) + if foundBorrow { + k.SynchronizeHardBorrowReward(ctx, borrow) + } + + // Synchronize any hard delegator rewards + k.SynchronizeHardDelegatorRewards(ctx, owner, nil, false) +} + +// ZeroHardLiquidityProviderClaim zeroes out the claim object's rewards and returns the updated claim object +func (k Keeper) ZeroHardLiquidityProviderClaim(ctx sdk.Context, claim types.HardLiquidityProviderClaim) types.HardLiquidityProviderClaim { + claim.Reward = sdk.NewCoins() + k.SetHardLiquidityProviderClaim(ctx, claim) + return claim +} + +// SimulateHardSynchronization calculates a user's outstanding hard rewards by simulating reward synchronization +func (k Keeper) SimulateHardSynchronization(ctx sdk.Context, claim types.HardLiquidityProviderClaim) types.HardLiquidityProviderClaim { + // 1. Simulate Hard supply-side rewards + for _, ri := range claim.SupplyRewardIndexes { + globalRewardIndexes, foundGlobalRewardIndexes := k.GetHardSupplyRewardIndexes(ctx, ri.CollateralType) + if !foundGlobalRewardIndexes { + continue + } + + userRewardIndexes, foundUserRewardIndexes := claim.SupplyRewardIndexes.GetRewardIndex(ri.CollateralType) + if !foundUserRewardIndexes { + continue + } + + userRewardIndexIndex, foundUserRewardIndexIndex := claim.SupplyRewardIndexes.GetRewardIndexIndex(ri.CollateralType) + if !foundUserRewardIndexIndex { + continue + } + + for _, globalRewardIndex := range globalRewardIndexes { + userRewardIndex, foundUserRewardIndex := userRewardIndexes.RewardIndexes.GetRewardIndex(globalRewardIndex.CollateralType) + if !foundUserRewardIndex { + userRewardIndex = types.NewRewardIndex(globalRewardIndex.CollateralType, sdk.ZeroDec()) + userRewardIndexes.RewardIndexes = append(userRewardIndexes.RewardIndexes, userRewardIndex) + claim.SupplyRewardIndexes[userRewardIndexIndex].RewardIndexes = append(claim.SupplyRewardIndexes[userRewardIndexIndex].RewardIndexes, userRewardIndex) + } + + globalRewardFactor := globalRewardIndex.RewardFactor + userRewardFactor := userRewardIndex.RewardFactor + rewardsAccumulatedFactor := globalRewardFactor.Sub(userRewardFactor) + if rewardsAccumulatedFactor.IsZero() { + continue + } + deposit, found := k.hardKeeper.GetDeposit(ctx, claim.GetOwner()) + if !found { + continue + } + newRewardsAmount := rewardsAccumulatedFactor.Mul(deposit.Amount.AmountOf(ri.CollateralType).ToDec()).RoundInt() + if newRewardsAmount.IsZero() || newRewardsAmount.IsNegative() { + continue + } + + factorIndex, foundFactorIndex := userRewardIndexes.RewardIndexes.GetFactorIndex(globalRewardIndex.CollateralType) + if !foundFactorIndex { + continue + } + claim.SupplyRewardIndexes[userRewardIndexIndex].RewardIndexes[factorIndex].RewardFactor = globalRewardIndex.RewardFactor + newRewardsCoin := sdk.NewCoin(userRewardIndex.CollateralType, newRewardsAmount) + claim.Reward = claim.Reward.Add(newRewardsCoin) + } + } + + // 2. Simulate Hard borrow-side rewards + for _, ri := range claim.BorrowRewardIndexes { + globalRewardIndexes, foundGlobalRewardIndexes := k.GetHardBorrowRewardIndexes(ctx, ri.CollateralType) + if !foundGlobalRewardIndexes { + continue + } + + userRewardIndexes, foundUserRewardIndexes := claim.BorrowRewardIndexes.GetRewardIndex(ri.CollateralType) + if !foundUserRewardIndexes { + continue + } + + userRewardIndexIndex, foundUserRewardIndexIndex := claim.BorrowRewardIndexes.GetRewardIndexIndex(ri.CollateralType) + if !foundUserRewardIndexIndex { + continue + } + + for _, globalRewardIndex := range globalRewardIndexes { + userRewardIndex, foundUserRewardIndex := userRewardIndexes.RewardIndexes.GetRewardIndex(globalRewardIndex.CollateralType) + if !foundUserRewardIndex { + userRewardIndex = types.NewRewardIndex(globalRewardIndex.CollateralType, sdk.ZeroDec()) + userRewardIndexes.RewardIndexes = append(userRewardIndexes.RewardIndexes, userRewardIndex) + claim.BorrowRewardIndexes[userRewardIndexIndex].RewardIndexes = append(claim.BorrowRewardIndexes[userRewardIndexIndex].RewardIndexes, userRewardIndex) + } + + globalRewardFactor := globalRewardIndex.RewardFactor + userRewardFactor := userRewardIndex.RewardFactor + rewardsAccumulatedFactor := globalRewardFactor.Sub(userRewardFactor) + if rewardsAccumulatedFactor.IsZero() { + continue + } + borrow, found := k.hardKeeper.GetBorrow(ctx, claim.GetOwner()) + if !found { + continue + } + newRewardsAmount := rewardsAccumulatedFactor.Mul(borrow.Amount.AmountOf(ri.CollateralType).ToDec()).RoundInt() + if newRewardsAmount.IsZero() || newRewardsAmount.IsNegative() { + continue + } + + factorIndex, foundFactorIndex := userRewardIndexes.RewardIndexes.GetFactorIndex(globalRewardIndex.CollateralType) + if !foundFactorIndex { + continue + } + claim.BorrowRewardIndexes[userRewardIndexIndex].RewardIndexes[factorIndex].RewardFactor = globalRewardIndex.RewardFactor + newRewardsCoin := sdk.NewCoin(userRewardIndex.CollateralType, newRewardsAmount) + claim.Reward = claim.Reward.Add(newRewardsCoin) + } + } + + // 3. Simulate Hard delegator rewards + delagatorFactor, found := k.GetHardDelegatorRewardFactor(ctx, types.BondDenom) + if !found { + return claim + } + + delegatorIndex, hasDelegatorRewardIndex := claim.HasDelegatorRewardIndex(types.BondDenom) + if !hasDelegatorRewardIndex { + return claim + } + + userRewardFactor := claim.DelegatorRewardIndexes[delegatorIndex].RewardFactor + rewardsAccumulatedFactor := delagatorFactor.Sub(userRewardFactor) + if rewardsAccumulatedFactor.IsZero() { + return claim + } + claim.DelegatorRewardIndexes[delegatorIndex].RewardFactor = delagatorFactor + + totalDelegated := sdk.ZeroDec() + + delegations := k.stakingKeeper.GetDelegatorDelegations(ctx, claim.GetOwner(), 200) + for _, delegation := range delegations { + validator, found := k.stakingKeeper.GetValidator(ctx, delegation.GetValidatorAddr()) + if !found { + continue + } + + // Delegators don't accumulate rewards if their validator is unbonded/slashed + if validator.GetStatus() != sdk.Bonded { + continue + } + + if validator.GetTokens().IsZero() { + continue + } + + delegatedTokens := validator.TokensFromShares(delegation.GetShares()) + if delegatedTokens.IsZero() || delegatedTokens.IsNegative() { + continue + } + totalDelegated = totalDelegated.Add(delegatedTokens) + } + + rewardsEarned := rewardsAccumulatedFactor.Mul(totalDelegated).RoundInt() + if rewardsEarned.IsZero() || rewardsEarned.IsNegative() { + return claim + } + + // Add rewards to delegator's hard claim + newRewardsCoin := sdk.NewCoin(types.HardLiquidityRewardDenom, rewardsEarned) + claim.Reward = claim.Reward.Add(newRewardsCoin) + + return claim +} + +// CalculateTimeElapsed calculates the number of reward-eligible seconds that have passed since the previous +// time rewards were accrued, taking into account the end time of the reward period +func CalculateTimeElapsed(start, end, blockTime time.Time, previousAccrualTime time.Time) sdk.Int { + if (end.Before(blockTime) && + (end.Before(previousAccrualTime) || end.Equal(previousAccrualTime))) || + (start.After(blockTime)) || + (start.Equal(blockTime)) { + return sdk.ZeroInt() + } + if start.After(previousAccrualTime) && start.Before(blockTime) { + previousAccrualTime = start + } + + if end.Before(blockTime) { + return sdk.MaxInt(sdk.ZeroInt(), sdk.NewInt(int64(math.RoundToEven( + end.Sub(previousAccrualTime).Seconds(), + )))) + } + return sdk.MaxInt(sdk.ZeroInt(), sdk.NewInt(int64(math.RoundToEven( + blockTime.Sub(previousAccrualTime).Seconds(), + )))) +} + +// Set setDifference: A - B +func setDifference(a, b []string) (diff []string) { + m := make(map[string]bool) + + for _, item := range b { + m[item] = true + } + + for _, item := range a { + if _, ok := m[item]; !ok { + diff = append(diff, item) + } + } + return +} + +func getDenoms(coins sdk.Coins) []string { + denoms := []string{} + for _, coin := range coins { + denoms = append(denoms, coin.Denom) + } + return denoms +} diff --git a/x/incentive/keeper/rewards_supply_test.go b/x/incentive/keeper/rewards_supply_test.go new file mode 100644 index 00000000..7b76ef35 --- /dev/null +++ b/x/incentive/keeper/rewards_supply_test.go @@ -0,0 +1,1131 @@ +package keeper_test + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/params" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/x/committee" + "github.com/kava-labs/kava/x/hard" + hardtypes "github.com/kava-labs/kava/x/hard/types" + "github.com/kava-labs/kava/x/incentive/types" +) + +func (suite *KeeperTestSuite) TestAccumulateHardSupplyRewards() { + type args struct { + deposit sdk.Coin + rewardsPerSecond sdk.Coins + initialTime time.Time + timeElapsed int + expectedRewardIndexes types.RewardIndexes + } + type test struct { + name string + args args + } + testCases := []test{ + { + "single reward denom: 7 seconds", + args{ + deposit: c("bnb", 1000000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + timeElapsed: 7, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.000000856478000000"))}, + }, + }, + { + "single reward denom: 1 day", + args{ + deposit: c("bnb", 1000000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + timeElapsed: 86400, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.010571385600000000"))}, + }, + }, + { + "single reward denom: 0 seconds", + args{ + deposit: c("bnb", 1000000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + timeElapsed: 0, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.0"))}, + }, + }, + { + "multiple reward denoms: 7 seconds", + args{ + deposit: c("bnb", 1000000000000), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + timeElapsed: 7, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.000000856478000000")), + types.NewRewardIndex("ukava", d("0.000000856478000000")), + }, + }, + }, + { + "multiple reward denoms: 1 day", + args{ + deposit: c("bnb", 1000000000000), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + timeElapsed: 86400, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.010571385600000000")), + types.NewRewardIndex("ukava", d("0.010571385600000000")), + }, + }, + }, + { + "multiple reward denoms: 0 seconds", + args{ + deposit: c("bnb", 1000000000000), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + timeElapsed: 0, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + types.NewRewardIndex("ukava", d("0.0")), + }, + }, + }, + { + "multiple reward denoms with different rewards per second: 1 day", + args{ + deposit: c("bnb", 1000000000000), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 555555)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + timeElapsed: 86400, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.010571385600000000")), + types.NewRewardIndex("ukava", d("0.047999952000000000")), + }, + }, + }, + { + "single reward denom, no rewards", + args{ + deposit: c("bnb", 1000000000000), + rewardsPerSecond: sdk.Coins{}, + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + timeElapsed: 7, + expectedRewardIndexes: types.RewardIndexes{}, + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWithGenState() + suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) + + // Mint coins to hard module account + supplyKeeper := suite.app.GetSupplyKeeper() + hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) + supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) + + // Set up incentive state + params := types.NewParams( + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), c("hard", 1))}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), c("hard", 1))}, + types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + tc.args.initialTime.Add(time.Hour*24*365*5), + ) + suite.keeper.SetParams(suite.ctx, params) + suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, tc.args.deposit.Denom, tc.args.initialTime) + var rewardIndexes types.RewardIndexes + for _, rewardCoin := range tc.args.rewardsPerSecond { + rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) + rewardIndexes = append(rewardIndexes, rewardIndex) + } + if len(rewardIndexes) > 0 { + suite.keeper.SetHardSupplyRewardIndexes(suite.ctx, tc.args.deposit.Denom, rewardIndexes) + } + + // Set up hard state (interest factor for the relevant denom) + suite.hardKeeper.SetSupplyInterestFactor(suite.ctx, tc.args.deposit.Denom, sdk.MustNewDecFromStr("1.0")) + suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.deposit.Denom, tc.args.initialTime) + + // User deposits to increase total supplied amount + hardKeeper := suite.app.GetHardKeeper() + userAddr := suite.addrs[3] + err := hardKeeper.Deposit(suite.ctx, userAddr, sdk.NewCoins(tc.args.deposit)) + suite.Require().NoError(err) + + // Set up chain context at future time + runAtTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * tc.args.timeElapsed)) + runCtx := suite.ctx.WithBlockTime(runAtTime) + + // Run Hard begin blocker in order to update the denom's index factor + hard.BeginBlocker(runCtx, suite.hardKeeper) + + // Accumulate hard supply rewards for the deposit denom + multiRewardPeriod, found := suite.keeper.GetHardSupplyRewardPeriods(runCtx, tc.args.deposit.Denom) + suite.Require().True(found) + err = suite.keeper.AccumulateHardSupplyRewards(runCtx, multiRewardPeriod) + suite.Require().NoError(err) + + // Check that each expected reward index matches the current stored reward index for the denom + globalRewardIndexes, found := suite.keeper.GetHardSupplyRewardIndexes(runCtx, tc.args.deposit.Denom) + if len(tc.args.rewardsPerSecond) > 0 { + suite.Require().True(found) + for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { + globalRewardIndex, found := globalRewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(expectedRewardIndex, globalRewardIndex) + } + } else { + suite.Require().False(found) + } + + }) + } +} + +func (suite *KeeperTestSuite) TestInitializeHardSupplyRewards() { + + type args struct { + moneyMarketRewardDenoms map[string][]string + deposit sdk.Coins + initialTime time.Time + expectedClaimSupplyRewardIndexes types.MultiRewardIndexes + } + type test struct { + name string + args args + } + + standardMoneyMarketRewardDenoms := map[string][]string{ + "bnb": {"hard"}, + "btcb": {"hard", "ukava"}, + "xrp": {}, + } + + testCases := []test{ + { + "single deposit denom, single reward denom", + args{ + moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, + deposit: cs(c("bnb", 1000000000000)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedClaimSupplyRewardIndexes: types.MultiRewardIndexes{ + types.NewMultiRewardIndex( + "bnb", + types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + }, + ), + }, + }, + }, + { + "single deposit denom, multiple reward denoms", + args{ + moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, + deposit: cs(c("btcb", 1000000000000)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedClaimSupplyRewardIndexes: types.MultiRewardIndexes{ + types.NewMultiRewardIndex( + "btcb", + types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + types.NewRewardIndex("ukava", d("0.0")), + }, + ), + }, + }, + }, + { + "single deposit denom, no reward denoms", + args{ + moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, + deposit: cs(c("xrp", 1000000000000)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedClaimSupplyRewardIndexes: types.MultiRewardIndexes{ + types.NewMultiRewardIndex( + "xrp", + nil, + ), + }, + }, + }, + { + "multiple deposit denoms, multiple overlapping reward denoms", + args{ + moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, + deposit: cs(c("bnb", 1000000000000), c("btcb", 1000000000000)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedClaimSupplyRewardIndexes: types.MultiRewardIndexes{ + types.NewMultiRewardIndex( + "bnb", + types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + }, + ), + types.NewMultiRewardIndex( + "btcb", + types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + types.NewRewardIndex("ukava", d("0.0")), + }, + ), + }, + }, + }, + { + "multiple deposit denoms, correct discrete reward denoms", + args{ + moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, + deposit: cs(c("bnb", 1000000000000), c("xrp", 1000000000000)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedClaimSupplyRewardIndexes: types.MultiRewardIndexes{ + types.NewMultiRewardIndex( + "bnb", + types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + }, + ), + types.NewMultiRewardIndex( + "xrp", + nil, + ), + }, + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWithGenState() + suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) + + // Mint coins to hard module account + supplyKeeper := suite.app.GetSupplyKeeper() + hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) + supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) + + userAddr := suite.addrs[3] + + // Prepare money market + reward params + i := 0 + var multiRewardPeriods types.MultiRewardPeriods + var rewardPeriods types.RewardPeriods + for moneyMarketDenom, rewardDenoms := range tc.args.moneyMarketRewardDenoms { + // Set up multi reward periods for supply/borrow indexes with dynamic money market denoms/reward denoms + var rewardsPerSecond sdk.Coins + for _, rewardDenom := range rewardDenoms { + rewardsPerSecond = append(rewardsPerSecond, sdk.NewCoin(rewardDenom, sdk.OneInt())) + } + multiRewardPeriod := types.NewMultiRewardPeriod(true, moneyMarketDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), rewardsPerSecond) + multiRewardPeriods = append(multiRewardPeriods, multiRewardPeriod) + + // Set up generic reward periods for usdx minting/delegator indexes + if i == 0 && len(rewardDenoms) > 0 { + rewardPeriod := types.NewRewardPeriod(true, moneyMarketDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), rewardsPerSecond[i]) + rewardPeriods = append(rewardPeriods, rewardPeriod) + i++ + } + } + + // Initialize and set incentive params + params := types.NewParams( + rewardPeriods, multiRewardPeriods, multiRewardPeriods, rewardPeriods, + types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + tc.args.initialTime.Add(time.Hour*24*365*5), + ) + suite.keeper.SetParams(suite.ctx, params) + + // Set each money market's previous accrual time and supply reward indexes + for moneyMarketDenom, rewardDenoms := range tc.args.moneyMarketRewardDenoms { + var rewardIndexes types.RewardIndexes + for _, rewardDenom := range rewardDenoms { + rewardIndex := types.NewRewardIndex(rewardDenom, sdk.ZeroDec()) + rewardIndexes = append(rewardIndexes, rewardIndex) + } + suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, moneyMarketDenom, tc.args.initialTime) + if len(rewardIndexes) > 0 { + suite.keeper.SetHardSupplyRewardIndexes(suite.ctx, moneyMarketDenom, rewardIndexes) + } + } + + // User deposits + hardKeeper := suite.app.GetHardKeeper() + err := hardKeeper.Deposit(suite.ctx, userAddr, tc.args.deposit) + suite.Require().NoError(err) + + claim, foundClaim := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) + suite.Require().True(foundClaim) + suite.Require().Equal(tc.args.expectedClaimSupplyRewardIndexes, claim.SupplyRewardIndexes) + }) + } +} + +func (suite *KeeperTestSuite) TestSynchronizeHardSupplyReward() { + type args struct { + incentiveSupplyRewardDenom string + deposit sdk.Coin + rewardsPerSecond sdk.Coins + initialTime time.Time + blockTimes []int + expectedRewardIndexes types.RewardIndexes + expectedRewards sdk.Coins + updateRewardsViaCommmittee bool + updatedBaseDenom string + updatedRewardsPerSecond sdk.Coins + updatedExpectedRewardIndexes types.RewardIndexes + updatedExpectedRewards sdk.Coins + updatedTimeDuration int + } + type test struct { + name string + args args + } + + testCases := []test{ + { + "single reward denom: 10 blocks", + args{ + incentiveSupplyRewardDenom: "bnb", + deposit: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.001223540000000000"))}, + expectedRewards: cs(c("hard", 12235400)), + updateRewardsViaCommmittee: false, + }, + }, + { + "single reward denom: 10 blocks - long block time", + args{ + incentiveSupplyRewardDenom: "bnb", + deposit: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("10.571385600000000000"))}, + expectedRewards: cs(c("hard", 105713856000)), + updateRewardsViaCommmittee: false, + }, + }, + { + "single reward denom: user reward index updated when reward is zero", + args{ + incentiveSupplyRewardDenom: "ukava", + deposit: c("ukava", 1), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.122353998776460010"))}, + expectedRewards: cs(), + updateRewardsViaCommmittee: false, + }, + }, + { + "multiple reward denoms: 10 blocks", + args{ + incentiveSupplyRewardDenom: "bnb", + deposit: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.001223540000000000")), + types.NewRewardIndex("ukava", d("0.001223540000000000")), + }, + expectedRewards: cs(c("hard", 12235400), c("ukava", 12235400)), + updateRewardsViaCommmittee: false, + }, + }, + { + "multiple reward denoms: 10 blocks - long block time", + args{ + incentiveSupplyRewardDenom: "bnb", + deposit: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("10.571385600000000000")), + types.NewRewardIndex("ukava", d("10.571385600000000000")), + }, + expectedRewards: cs(c("hard", 105713856000), c("ukava", 105713856000)), + updateRewardsViaCommmittee: false, + }, + }, + { + "multiple reward denoms with different rewards per second: 10 blocks", + args{ + incentiveSupplyRewardDenom: "bnb", + deposit: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 555555)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.001223540000000000")), + types.NewRewardIndex("ukava", d("0.005555550000000000")), + }, + expectedRewards: cs(c("hard", 12235400), c("ukava", 55555500)), + updateRewardsViaCommmittee: false, + }, + }, + { + "denom is in incentive's hard supply reward params but it has no rewards; add reward", + args{ + incentiveSupplyRewardDenom: "bnb", + deposit: c("bnb", 10000000000), + rewardsPerSecond: sdk.Coins{}, + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{100}, + expectedRewardIndexes: types.RewardIndexes{}, + expectedRewards: sdk.Coins{}, + updateRewardsViaCommmittee: true, + updatedBaseDenom: "bnb", + updatedRewardsPerSecond: cs(c("hard", 100000)), + updatedExpectedRewards: cs(c("hard", 8640000000)), + updatedExpectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.864")), + }, + updatedTimeDuration: 86400, + }, + }, + { + "denom is in incentive's hard supply reward params and has rewards; add new reward type", + args{ + incentiveSupplyRewardDenom: "bnb", + deposit: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{86400}, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("1.057138560000000000")), + }, + expectedRewards: cs(c("hard", 10571385600)), + updateRewardsViaCommmittee: true, + updatedBaseDenom: "bnb", + updatedRewardsPerSecond: cs(c("hard", 122354), c("ukava", 100000)), + updatedExpectedRewards: cs(c("hard", 21142771200), c("ukava", 8640000000)), + updatedExpectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("2.114277120000000000")), + types.NewRewardIndex("ukava", d("0.864000000000000000")), + }, + updatedTimeDuration: 86400, + }, + }, + { + "denom is in hard's money market params but not in incentive's hard supply reward params; add reward", + args{ + incentiveSupplyRewardDenom: "bnb", + deposit: c("zzz", 10000000000), + rewardsPerSecond: sdk.Coins{}, + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{100}, + expectedRewardIndexes: types.RewardIndexes{}, + expectedRewards: sdk.Coins{}, + updateRewardsViaCommmittee: true, + updatedBaseDenom: "zzz", + updatedRewardsPerSecond: cs(c("hard", 100000)), + updatedExpectedRewards: cs(c("hard", 8640000000)), + updatedExpectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.864")), + }, + updatedTimeDuration: 86400, + }, + }, + { + "denom incentive's hard supply reward params but it has no rewards; add multiple reward types", + args{ + incentiveSupplyRewardDenom: "bnb", + deposit: c("bnb", 10000000000), + rewardsPerSecond: sdk.Coins{}, + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{100}, + expectedRewardIndexes: types.RewardIndexes{}, + expectedRewards: sdk.Coins{}, + updateRewardsViaCommmittee: true, + updatedBaseDenom: "bnb", + updatedRewardsPerSecond: cs(c("hard", 100000), c("ukava", 100500), c("swap", 500)), + updatedExpectedRewards: cs(c("hard", 8640000000), c("ukava", 8683200000), c("swap", 43200000)), + updatedExpectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.864")), + types.NewRewardIndex("ukava", d("0.86832")), + types.NewRewardIndex("swap", d("0.00432")), + }, + updatedTimeDuration: 86400, + }, + }, + { + "denom is in hard's money market params but not in incentive's hard supply reward params; add multiple reward types", + args{ + incentiveSupplyRewardDenom: "bnb", + deposit: c("zzz", 10000000000), + rewardsPerSecond: sdk.Coins{}, + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{100}, + expectedRewardIndexes: types.RewardIndexes{}, + expectedRewards: sdk.Coins{}, + updateRewardsViaCommmittee: true, + updatedBaseDenom: "zzz", + updatedRewardsPerSecond: cs(c("hard", 100000), c("ukava", 100500), c("swap", 500)), + updatedExpectedRewards: cs(c("hard", 8640000000), c("ukava", 8683200000), c("swap", 43200000)), + updatedExpectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.864")), + types.NewRewardIndex("ukava", d("0.86832")), + types.NewRewardIndex("swap", d("0.00432")), + }, + updatedTimeDuration: 86400, + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWithGenState() + suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) + + // Mint coins to hard module account + supplyKeeper := suite.app.GetSupplyKeeper() + hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) + supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) + + // Set up incentive state + incentiveParams := types.NewParams( + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.incentiveSupplyRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), c("hard", 1))}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.incentiveSupplyRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.incentiveSupplyRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.incentiveSupplyRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), c("hard", 1))}, + types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + tc.args.initialTime.Add(time.Hour*24*365*5), + ) + suite.keeper.SetParams(suite.ctx, incentiveParams) + suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, tc.args.incentiveSupplyRewardDenom, tc.args.initialTime) + var rewardIndexes types.RewardIndexes + for _, rewardCoin := range tc.args.rewardsPerSecond { + rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) + rewardIndexes = append(rewardIndexes, rewardIndex) + } + if len(rewardIndexes) > 0 { + suite.keeper.SetHardSupplyRewardIndexes(suite.ctx, tc.args.incentiveSupplyRewardDenom, rewardIndexes) + } + + // Set up hard state (interest factor for the relevant denom) + suite.hardKeeper.SetSupplyInterestFactor(suite.ctx, tc.args.incentiveSupplyRewardDenom, sdk.MustNewDecFromStr("1.0")) + suite.hardKeeper.SetBorrowInterestFactor(suite.ctx, tc.args.incentiveSupplyRewardDenom, sdk.MustNewDecFromStr("1.0")) + suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.incentiveSupplyRewardDenom, tc.args.initialTime) + + // Deposit a fixed amount from another user to dilute primary user's rewards per second. + suite.Require().NoError( + suite.hardKeeper.Deposit(suite.ctx, suite.addrs[2], cs(c("ukava", 100_000_000))), + ) + + // User deposits and borrows to increase total borrowed amount + hardKeeper := suite.app.GetHardKeeper() + userAddr := suite.addrs[3] + err := hardKeeper.Deposit(suite.ctx, userAddr, sdk.NewCoins(tc.args.deposit)) + suite.Require().NoError(err) + + // Check that Hard hooks initialized a HardLiquidityProviderClaim with 0 reward indexes + claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) + suite.Require().True(found) + multiRewardIndex, _ := claim.SupplyRewardIndexes.GetRewardIndex(tc.args.deposit.Denom) + for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { + currRewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(sdk.ZeroDec(), currRewardIndex.RewardFactor) + } + + // Run accumulator at several intervals + var timeElapsed int + previousBlockTime := suite.ctx.BlockTime() + for _, t := range tc.args.blockTimes { + timeElapsed += t + updatedBlockTime := previousBlockTime.Add(time.Duration(int(time.Second) * t)) + previousBlockTime = updatedBlockTime + blockCtx := suite.ctx.WithBlockTime(updatedBlockTime) + + // Run Hard begin blocker for each block ctx to update denom's interest factor + hard.BeginBlocker(blockCtx, suite.hardKeeper) + + // Accumulate hard supply-side rewards + multiRewardPeriod, found := suite.keeper.GetHardSupplyRewardPeriods(blockCtx, tc.args.deposit.Denom) + if found { + err := suite.keeper.AccumulateHardSupplyRewards(blockCtx, multiRewardPeriod) + suite.Require().NoError(err) + } + } + updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed)) + suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) + + // After we've accumulated, run synchronize + deposit, found := hardKeeper.GetDeposit(suite.ctx, userAddr) + suite.Require().True(found) + suite.Require().NotPanics(func() { + suite.keeper.SynchronizeHardSupplyReward(suite.ctx, deposit) + }) + + // Check that the global reward index's reward factor and user's claim have been updated as expected + claim, found = suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) + suite.Require().True(found) + globalRewardIndexes, foundGlobalRewardIndexes := suite.keeper.GetHardSupplyRewardIndexes(suite.ctx, tc.args.deposit.Denom) + if len(tc.args.rewardsPerSecond) > 0 { + suite.Require().True(foundGlobalRewardIndexes) + for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { + // Check that global reward index has been updated as expected + globalRewardIndex, found := globalRewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(expectedRewardIndex, globalRewardIndex) + + // Check that the user's claim's reward index matches the corresponding global reward index + multiRewardIndex, found := claim.SupplyRewardIndexes.GetRewardIndex(tc.args.deposit.Denom) + suite.Require().True(found) + rewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(expectedRewardIndex, rewardIndex) + + // Check that the user's claim holds the expected amount of reward coins + suite.Require().Equal( + tc.args.expectedRewards.AmountOf(expectedRewardIndex.CollateralType), + claim.Reward.AmountOf(expectedRewardIndex.CollateralType), + ) + } + } + + // Only test cases with reward param updates continue past this point + if !tc.args.updateRewardsViaCommmittee { + return + } + + // If are no initial rewards per second, add new rewards through a committee param change + // 1. Construct incentive's new HardSupplyRewardPeriods param + currIncentiveHardSupplyRewardPeriods := suite.keeper.GetParams(suite.ctx).HardSupplyRewardPeriods + multiRewardPeriod, found := currIncentiveHardSupplyRewardPeriods.GetMultiRewardPeriod(tc.args.deposit.Denom) + if found { + // Deposit denom's reward period exists, but it doesn't have any rewards per second + index, found := currIncentiveHardSupplyRewardPeriods.GetMultiRewardPeriodIndex(tc.args.deposit.Denom) + suite.Require().True(found) + multiRewardPeriod.RewardsPerSecond = tc.args.updatedRewardsPerSecond + currIncentiveHardSupplyRewardPeriods[index] = multiRewardPeriod + } else { + // Deposit denom's reward period does not exist + _, found := currIncentiveHardSupplyRewardPeriods.GetMultiRewardPeriodIndex(tc.args.deposit.Denom) + suite.Require().False(found) + newMultiRewardPeriod := types.NewMultiRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.updatedRewardsPerSecond) + currIncentiveHardSupplyRewardPeriods = append(currIncentiveHardSupplyRewardPeriods, newMultiRewardPeriod) + } + + // 2. Construct the parameter change proposal to update HardSupplyRewardPeriods param + pubProposal := params.NewParameterChangeProposal( + "Update hard supply rewards", "Adds a new reward coin to the incentive module's hard supply rewards.", + []params.ParamChange{ + { + Subspace: types.ModuleName, // target incentive module + Key: string(types.KeyHardSupplyRewardPeriods), // target hard supply rewards key + Value: string(suite.app.Codec().MustMarshalJSON(currIncentiveHardSupplyRewardPeriods)), + }, + }, + ) + + // 3. Ensure proposal is properly formed + err = suite.committeeKeeper.ValidatePubProposal(suite.ctx, pubProposal) + suite.Require().NoError(err) + + // 4. Committee creates proposal + committeeMemberOne := suite.addrs[0] + committeeMemberTwo := suite.addrs[1] + proposalID, err := suite.committeeKeeper.SubmitProposal(suite.ctx, committeeMemberOne, 1, pubProposal) + suite.Require().NoError(err) + + // 5. Committee votes and passes proposal + err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberOne) + err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberTwo) + + // 6. Check proposal passed + proposalPasses, err := suite.committeeKeeper.GetProposalResult(suite.ctx, proposalID) + suite.Require().NoError(err) + suite.Require().True(proposalPasses) + + // 7. Run committee module's begin blocker to enact proposal + suite.NotPanics(func() { + committee.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}, suite.committeeKeeper) + }) + + // We need to accumulate hard supply-side rewards again + multiRewardPeriod, found = suite.keeper.GetHardSupplyRewardPeriods(suite.ctx, tc.args.deposit.Denom) + suite.Require().True(found) + + // But new deposit denoms don't have their PreviousHardSupplyRewardAccrualTime set yet, + // so we need to call the accumulation method once to set the initial reward accrual time + if tc.args.deposit.Denom != tc.args.incentiveSupplyRewardDenom { + err = suite.keeper.AccumulateHardSupplyRewards(suite.ctx, multiRewardPeriod) + suite.Require().NoError(err) + } + + // Now we can jump forward in time and accumulate rewards + updatedBlockTime = previousBlockTime.Add(time.Duration(int(time.Second) * tc.args.updatedTimeDuration)) + suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) + err = suite.keeper.AccumulateHardSupplyRewards(suite.ctx, multiRewardPeriod) + suite.Require().NoError(err) + + // After we've accumulated, run synchronize + deposit, found = hardKeeper.GetDeposit(suite.ctx, userAddr) + suite.Require().True(found) + suite.Require().NotPanics(func() { + suite.keeper.SynchronizeHardSupplyReward(suite.ctx, deposit) + }) + + // Check that the global reward index's reward factor and user's claim have been updated as expected + globalRewardIndexes, found = suite.keeper.GetHardSupplyRewardIndexes(suite.ctx, tc.args.deposit.Denom) + suite.Require().True(found) + claim, found = suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) + suite.Require().True(found) + for _, expectedRewardIndex := range tc.args.updatedExpectedRewardIndexes { + // Check that global reward index has been updated as expected + globalRewardIndex, found := globalRewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(expectedRewardIndex, globalRewardIndex) + + // Check that the user's claim's reward index matches the corresponding global reward index + multiRewardIndex, found := claim.SupplyRewardIndexes.GetRewardIndex(tc.args.deposit.Denom) + suite.Require().True(found) + rewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(expectedRewardIndex, rewardIndex) + + // Check that the user's claim holds the expected amount of reward coins + suite.Require().Equal( + tc.args.updatedExpectedRewards.AmountOf(expectedRewardIndex.CollateralType), + claim.Reward.AmountOf(expectedRewardIndex.CollateralType), + ) + } + }) + } +} + +func (suite *KeeperTestSuite) TestUpdateHardSupplyIndexDenoms() { + type depositModification struct { + coins sdk.Coins + withdraw bool + } + + type args struct { + firstDeposit sdk.Coins + modification depositModification + rewardsPerSecond sdk.Coins + initialTime time.Time + expectedSupplyIndexDenoms []string + } + type test struct { + name string + args args + } + + testCases := []test{ + { + "single reward denom: update adds one supply reward index", + args{ + firstDeposit: cs(c("bnb", 10000000000)), + modification: depositModification{coins: cs(c("ukava", 10000000000))}, + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedSupplyIndexDenoms: []string{"bnb", "ukava"}, + }, + }, + { + "single reward denom: update adds multiple supply reward indexes", + args{ + firstDeposit: cs(c("bnb", 10000000000)), + modification: depositModification{coins: cs(c("ukava", 10000000000), c("btcb", 10000000000), c("xrp", 10000000000))}, + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedSupplyIndexDenoms: []string{"bnb", "ukava", "btcb", "xrp"}, + }, + }, + { + "single reward denom: update doesn't add duplicate supply reward index for same denom", + args{ + firstDeposit: cs(c("bnb", 10000000000)), + modification: depositModification{coins: cs(c("bnb", 5000000000))}, + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedSupplyIndexDenoms: []string{"bnb"}, + }, + }, + { + "multiple reward denoms: update adds one supply reward index", + args{ + firstDeposit: cs(c("bnb", 10000000000)), + modification: depositModification{coins: cs(c("ukava", 10000000000))}, + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedSupplyIndexDenoms: []string{"bnb", "ukava"}, + }, + }, + { + "multiple reward denoms: update adds multiple supply reward indexes", + args{ + firstDeposit: cs(c("bnb", 10000000000)), + modification: depositModification{coins: cs(c("ukava", 10000000000), c("btcb", 10000000000), c("xrp", 10000000000))}, + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedSupplyIndexDenoms: []string{"bnb", "ukava", "btcb", "xrp"}, + }, + }, + { + "multiple reward denoms: update doesn't add duplicate supply reward index for same denom", + args{ + firstDeposit: cs(c("bnb", 10000000000)), + modification: depositModification{coins: cs(c("bnb", 5000000000))}, + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedSupplyIndexDenoms: []string{"bnb"}, + }, + }, + { + "single reward denom: fully withdrawing a denom deletes the denom's supply reward index", + args{ + firstDeposit: cs(c("bnb", 1000000000)), + modification: depositModification{coins: cs(c("bnb", 1100000000)), withdraw: true}, + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedSupplyIndexDenoms: []string{}, + }, + }, + { + "single reward denom: fully withdrawing a denom deletes only the denom's supply reward index", + args{ + firstDeposit: cs(c("bnb", 1000000000), c("ukava", 100000000)), + modification: depositModification{coins: cs(c("bnb", 1100000000)), withdraw: true}, + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedSupplyIndexDenoms: []string{"ukava"}, + }, + }, + { + "multiple reward denoms: fully repaying a denom deletes the denom's supply reward index", + args{ + firstDeposit: cs(c("bnb", 1000000000)), + modification: depositModification{coins: cs(c("bnb", 1100000000)), withdraw: true}, + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedSupplyIndexDenoms: []string{}, + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWithGenState() + suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) + + // Mint coins to hard module account + supplyKeeper := suite.app.GetSupplyKeeper() + hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) + supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) + + // Set up generic reward periods + var multiRewardPeriods types.MultiRewardPeriods + var rewardPeriods types.RewardPeriods + for i, denom := range tc.args.expectedSupplyIndexDenoms { + // Create just one reward period for USDX Minting / Hard Delegator reward periods (otherwise params will panic on duplicate) + if i == 0 { + rewardPeriod := types.NewRewardPeriod(true, denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[i]) + rewardPeriods = append(rewardPeriods, rewardPeriod) + } + multiRewardPeriod := types.NewMultiRewardPeriod(true, denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond) + multiRewardPeriods = append(multiRewardPeriods, multiRewardPeriod) + } + + // Setup incentive state + params := types.NewParams( + rewardPeriods, multiRewardPeriods, multiRewardPeriods, rewardPeriods, + types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + tc.args.initialTime.Add(time.Hour*24*365*5), + ) + suite.keeper.SetParams(suite.ctx, params) + + // Set each denom's previous accrual time and supply reward factor + var rewardIndexes types.RewardIndexes + for _, rewardCoin := range tc.args.rewardsPerSecond { + rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) + rewardIndexes = append(rewardIndexes, rewardIndex) + } + for _, denom := range tc.args.expectedSupplyIndexDenoms { + suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, denom, tc.args.initialTime) + suite.keeper.SetHardSupplyRewardIndexes(suite.ctx, denom, rewardIndexes) + } + + // User deposits (first time) + hardKeeper := suite.app.GetHardKeeper() + userAddr := suite.addrs[3] + err := hardKeeper.Deposit(suite.ctx, userAddr, tc.args.firstDeposit) + suite.Require().NoError(err) + + // Confirm that a claim was created and populated with the correct supply indexes + claimAfterFirstDeposit, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) + suite.Require().True(found) + for _, coin := range tc.args.firstDeposit { + _, hasIndex := claimAfterFirstDeposit.HasSupplyRewardIndex(coin.Denom) + suite.Require().True(hasIndex) + } + suite.Require().True(len(claimAfterFirstDeposit.SupplyRewardIndexes) == len(tc.args.firstDeposit)) + + // User modifies their Deposit by withdrawing or depositing more + if tc.args.modification.withdraw { + err = hardKeeper.Withdraw(suite.ctx, userAddr, tc.args.modification.coins) + } else { + err = hardKeeper.Deposit(suite.ctx, userAddr, tc.args.modification.coins) + } + suite.Require().NoError(err) + + // Confirm that the claim contains all expected supply indexes + claimAfterModification, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) + suite.Require().True(found) + for _, denom := range tc.args.expectedSupplyIndexDenoms { + _, hasIndex := claimAfterModification.HasSupplyRewardIndex(denom) + suite.Require().True(hasIndex) + } + suite.Require().True(len(claimAfterModification.SupplyRewardIndexes) == len(tc.args.expectedSupplyIndexDenoms)) + }) + } +} + +func (suite *KeeperTestSuite) TestSimulateHardSupplyRewardSynchronization() { + type args struct { + deposit sdk.Coin + rewardsPerSecond sdk.Coins + initialTime time.Time + blockTimes []int + expectedRewardIndexes types.RewardIndexes + expectedRewards sdk.Coins + } + type test struct { + name string + args args + } + + testCases := []test{ + { + "10 blocks", + args{ + deposit: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.001223540000000000"))}, + expectedRewards: cs(c("hard", 12235400)), + }, + }, + { + "10 blocks - long block time", + args{ + deposit: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("10.571385600000000000"))}, + expectedRewards: cs(c("hard", 105713856000)), + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWithGenState() + suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) + + // Mint coins to hard module account + supplyKeeper := suite.app.GetSupplyKeeper() + hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) + supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) + + // Set up incentive state + params := types.NewParams( + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, + types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + tc.args.initialTime.Add(time.Hour*24*365*5), + ) + suite.keeper.SetParams(suite.ctx, params) + suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, tc.args.deposit.Denom, tc.args.initialTime) + var rewardIndexes types.RewardIndexes + for _, rewardCoin := range tc.args.rewardsPerSecond { + rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) + rewardIndexes = append(rewardIndexes, rewardIndex) + } + suite.keeper.SetHardSupplyRewardIndexes(suite.ctx, tc.args.deposit.Denom, rewardIndexes) + + // Set up hard state (interest factor for the relevant denom) + suite.hardKeeper.SetSupplyInterestFactor(suite.ctx, tc.args.deposit.Denom, sdk.MustNewDecFromStr("1.0")) + suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.deposit.Denom, tc.args.initialTime) + + // User deposits and borrows to increase total borrowed amount + hardKeeper := suite.app.GetHardKeeper() + userAddr := suite.addrs[3] + err := hardKeeper.Deposit(suite.ctx, userAddr, sdk.NewCoins(tc.args.deposit)) + suite.Require().NoError(err) + + // Check that Hard hooks initialized a HardLiquidityProviderClaim + claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) + suite.Require().True(found) + multiRewardIndex, _ := claim.SupplyRewardIndexes.GetRewardIndex(tc.args.deposit.Denom) + for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { + currRewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(sdk.ZeroDec(), currRewardIndex.RewardFactor) + } + + // Run accumulator at several intervals + var timeElapsed int + previousBlockTime := suite.ctx.BlockTime() + for _, t := range tc.args.blockTimes { + timeElapsed += t + updatedBlockTime := previousBlockTime.Add(time.Duration(int(time.Second) * t)) + previousBlockTime = updatedBlockTime + blockCtx := suite.ctx.WithBlockTime(updatedBlockTime) + + // Run Hard begin blocker for each block ctx to update denom's interest factor + hard.BeginBlocker(blockCtx, suite.hardKeeper) + + // Accumulate hard supply-side rewards + multiRewardPeriod, found := suite.keeper.GetHardSupplyRewardPeriods(blockCtx, tc.args.deposit.Denom) + suite.Require().True(found) + err := suite.keeper.AccumulateHardSupplyRewards(blockCtx, multiRewardPeriod) + suite.Require().NoError(err) + } + updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed)) + suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) + + // Confirm that the user's claim hasn't been synced + claimPre, foundPre := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) + suite.Require().True(foundPre) + multiRewardIndexPre, _ := claimPre.SupplyRewardIndexes.GetRewardIndex(tc.args.deposit.Denom) + for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { + currRewardIndex, found := multiRewardIndexPre.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(sdk.ZeroDec(), currRewardIndex.RewardFactor) + } + + // Check that the synced claim held in memory has properly simulated syncing + syncedClaim := suite.keeper.SimulateHardSynchronization(suite.ctx, claimPre) + for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { + // Check that the user's claim's reward index matches the expected reward index + multiRewardIndex, found := syncedClaim.SupplyRewardIndexes.GetRewardIndex(tc.args.deposit.Denom) + suite.Require().True(found) + rewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(expectedRewardIndex, rewardIndex) + + // Check that the user's claim holds the expected amount of reward coins + suite.Require().Equal( + tc.args.expectedRewards.AmountOf(expectedRewardIndex.CollateralType), + syncedClaim.Reward.AmountOf(expectedRewardIndex.CollateralType), + ) + } + }) + } +} diff --git a/x/incentive/keeper/rewards_test.go b/x/incentive/keeper/rewards_test.go deleted file mode 100644 index 2cae8f3b..00000000 --- a/x/incentive/keeper/rewards_test.go +++ /dev/null @@ -1,3295 +0,0 @@ -package keeper_test - -import ( - "time" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/params" - "github.com/cosmos/cosmos-sdk/x/staking" - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/crypto/ed25519" - - cdptypes "github.com/kava-labs/kava/x/cdp/types" - "github.com/kava-labs/kava/x/committee" - "github.com/kava-labs/kava/x/hard" - hardtypes "github.com/kava-labs/kava/x/hard/types" - "github.com/kava-labs/kava/x/incentive/types" -) - -const ( - oneYear time.Duration = time.Hour * 24 * 365 -) - -func (suite *KeeperTestSuite) TestAccumulateUSDXMintingRewards() { - type args struct { - ctype string - rewardsPerSecond sdk.Coin - initialTime time.Time - initialTotalPrincipal sdk.Coin - timeElapsed int - expectedRewardFactor sdk.Dec - } - type test struct { - name string - args args - } - testCases := []test{ - { - "7 seconds", - args{ - ctype: "bnb-a", - rewardsPerSecond: c("ukava", 122354), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - initialTotalPrincipal: c("usdx", 1000000000000), - timeElapsed: 7, - expectedRewardFactor: d("0.000000856478000000"), - }, - }, - { - "1 day", - args{ - ctype: "bnb-a", - rewardsPerSecond: c("ukava", 122354), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - initialTotalPrincipal: c("usdx", 1000000000000), - timeElapsed: 86400, - expectedRewardFactor: d("0.0105713856"), - }, - }, - { - "0 seconds", - args{ - ctype: "bnb-a", - rewardsPerSecond: c("ukava", 122354), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - initialTotalPrincipal: c("usdx", 1000000000000), - timeElapsed: 0, - expectedRewardFactor: d("0.0"), - }, - }, - } - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.SetupWithGenState() - suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) - - // setup cdp state - cdpKeeper := suite.app.GetCDPKeeper() - cdpKeeper.SetTotalPrincipal(suite.ctx, tc.args.ctype, cdptypes.DefaultStableDenom, tc.args.initialTotalPrincipal.Amount) - - // setup incentive state - params := types.NewParams( - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), cs(tc.args.rewardsPerSecond))}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), cs(tc.args.rewardsPerSecond))}, - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - tc.args.initialTime.Add(time.Hour*24*365*5), - ) - suite.keeper.SetParams(suite.ctx, params) - suite.keeper.SetPreviousUSDXMintingAccrualTime(suite.ctx, tc.args.ctype, tc.args.initialTime) - suite.keeper.SetUSDXMintingRewardFactor(suite.ctx, tc.args.ctype, sdk.ZeroDec()) - - updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * tc.args.timeElapsed)) - suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) - rewardPeriod, found := suite.keeper.GetUSDXMintingRewardPeriod(suite.ctx, tc.args.ctype) - suite.Require().True(found) - err := suite.keeper.AccumulateUSDXMintingRewards(suite.ctx, rewardPeriod) - suite.Require().NoError(err) - - rewardFactor, found := suite.keeper.GetUSDXMintingRewardFactor(suite.ctx, tc.args.ctype) - suite.Require().Equal(tc.args.expectedRewardFactor, rewardFactor) - }) - } -} - -func (suite *KeeperTestSuite) TestSynchronizeUSDXMintingReward() { - type args struct { - ctype string - rewardsPerSecond sdk.Coin - initialTime time.Time - initialCollateral sdk.Coin - initialPrincipal sdk.Coin - blockTimes []int - expectedRewardFactor sdk.Dec - expectedRewards sdk.Coin - } - type test struct { - name string - args args - } - - testCases := []test{ - { - "10 blocks", - args{ - ctype: "bnb-a", - rewardsPerSecond: c("ukava", 122354), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - initialCollateral: c("bnb", 1000000000000), - initialPrincipal: c("usdx", 10000000000), - blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, - expectedRewardFactor: d("0.001223540000000000"), - expectedRewards: c("ukava", 12235400), - }, - }, - { - "10 blocks - long block time", - args{ - ctype: "bnb-a", - rewardsPerSecond: c("ukava", 122354), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - initialCollateral: c("bnb", 1000000000000), - initialPrincipal: c("usdx", 10000000000), - blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, - expectedRewardFactor: d("10.57138560000000000"), - expectedRewards: c("ukava", 105713856000), - }, - }, - } - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.SetupWithGenState() - suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) - - // setup incentive state - params := types.NewParams( - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), cs(tc.args.rewardsPerSecond))}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), cs(tc.args.rewardsPerSecond))}, - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - tc.args.initialTime.Add(time.Hour*24*365*5), - ) - suite.keeper.SetParams(suite.ctx, params) - suite.keeper.SetPreviousUSDXMintingAccrualTime(suite.ctx, tc.args.ctype, tc.args.initialTime) - suite.keeper.SetUSDXMintingRewardFactor(suite.ctx, tc.args.ctype, sdk.ZeroDec()) - - // setup account state - sk := suite.app.GetSupplyKeeper() - sk.MintCoins(suite.ctx, cdptypes.ModuleName, sdk.NewCoins(tc.args.initialCollateral)) - sk.SendCoinsFromModuleToAccount(suite.ctx, cdptypes.ModuleName, suite.addrs[0], sdk.NewCoins(tc.args.initialCollateral)) - - // setup cdp state - cdpKeeper := suite.app.GetCDPKeeper() - err := cdpKeeper.AddCdp(suite.ctx, suite.addrs[0], tc.args.initialCollateral, tc.args.initialPrincipal, tc.args.ctype) - suite.Require().NoError(err) - - claim, found := suite.keeper.GetUSDXMintingClaim(suite.ctx, suite.addrs[0]) - suite.Require().True(found) - suite.Require().Equal(sdk.ZeroDec(), claim.RewardIndexes[0].RewardFactor) - - var timeElapsed int - previousBlockTime := suite.ctx.BlockTime() - for _, t := range tc.args.blockTimes { - timeElapsed += t - updatedBlockTime := previousBlockTime.Add(time.Duration(int(time.Second) * t)) - previousBlockTime = updatedBlockTime - blockCtx := suite.ctx.WithBlockTime(updatedBlockTime) - rewardPeriod, found := suite.keeper.GetUSDXMintingRewardPeriod(blockCtx, tc.args.ctype) - suite.Require().True(found) - err := suite.keeper.AccumulateUSDXMintingRewards(blockCtx, rewardPeriod) - suite.Require().NoError(err) - } - updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed)) - suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) - cdp, found := cdpKeeper.GetCdpByOwnerAndCollateralType(suite.ctx, suite.addrs[0], tc.args.ctype) - suite.Require().True(found) - suite.Require().NotPanics(func() { - suite.keeper.SynchronizeUSDXMintingReward(suite.ctx, cdp) - }) - - rewardFactor, found := suite.keeper.GetUSDXMintingRewardFactor(suite.ctx, tc.args.ctype) - suite.Require().Equal(tc.args.expectedRewardFactor, rewardFactor) - - claim, found = suite.keeper.GetUSDXMintingClaim(suite.ctx, suite.addrs[0]) - suite.Require().True(found) - suite.Require().Equal(tc.args.expectedRewardFactor, claim.RewardIndexes[0].RewardFactor) - suite.Require().Equal(tc.args.expectedRewards, claim.Reward) - }) - } -} - -func (suite *KeeperTestSuite) TestAccumulateHardBorrowRewards() { - type args struct { - borrow sdk.Coin - rewardsPerSecond sdk.Coins - initialTime time.Time - timeElapsed int - expectedRewardIndexes types.RewardIndexes - } - type test struct { - name string - args args - } - testCases := []test{ - { - "single reward denom: 7 seconds", - args{ - borrow: c("bnb", 1000000000000), - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - timeElapsed: 7, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.000000856478000001"))}, - }, - }, - { - "single reward denom: 1 day", - args{ - borrow: c("bnb", 1000000000000), - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - timeElapsed: 86400, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.010571385600010177"))}, - }, - }, - { - "single reward denom: 0 seconds", - args{ - borrow: c("bnb", 1000000000000), - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - timeElapsed: 0, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.0"))}, - }, - }, - { - "multiple reward denoms: 7 seconds", - args{ - borrow: c("bnb", 1000000000000), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - timeElapsed: 7, - expectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.000000856478000001")), - types.NewRewardIndex("ukava", d("0.000000856478000001")), - }, - }, - }, - { - "multiple reward denoms: 1 day", - args{ - borrow: c("bnb", 1000000000000), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - timeElapsed: 86400, - expectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.010571385600010177")), - types.NewRewardIndex("ukava", d("0.010571385600010177")), - }, - }, - }, - { - "multiple reward denoms: 0 seconds", - args{ - borrow: c("bnb", 1000000000000), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - timeElapsed: 0, - expectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.0")), - types.NewRewardIndex("ukava", d("0.0")), - }, - }, - }, - { - "multiple reward denoms with different rewards per second: 1 day", - args{ - borrow: c("bnb", 1000000000000), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 555555)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - timeElapsed: 86400, - expectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.010571385600010177")), - types.NewRewardIndex("ukava", d("0.047999952000046210")), - }, - }, - }, - } - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.SetupWithGenState() - suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) - - // Mint coins to hard module account - supplyKeeper := suite.app.GetSupplyKeeper() - hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) - supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) - - // setup incentive state - params := types.NewParams( - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, - types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - tc.args.initialTime.Add(time.Hour*24*365*5), - ) - suite.keeper.SetParams(suite.ctx, params) - suite.keeper.SetPreviousHardBorrowRewardAccrualTime(suite.ctx, tc.args.borrow.Denom, tc.args.initialTime) - var rewardIndexes types.RewardIndexes - for _, rewardCoin := range tc.args.rewardsPerSecond { - rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) - rewardIndexes = append(rewardIndexes, rewardIndex) - } - suite.keeper.SetHardBorrowRewardIndexes(suite.ctx, tc.args.borrow.Denom, rewardIndexes) - - // Set up hard state (interest factor for the relevant denom) - suite.hardKeeper.SetSupplyInterestFactor(suite.ctx, tc.args.borrow.Denom, sdk.MustNewDecFromStr("1.0")) - suite.hardKeeper.SetBorrowInterestFactor(suite.ctx, tc.args.borrow.Denom, sdk.MustNewDecFromStr("1.0")) - suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.borrow.Denom, tc.args.initialTime) - - // User deposits and borrows to increase total borrowed amount - hardKeeper := suite.app.GetHardKeeper() - userAddr := suite.addrs[3] - err := hardKeeper.Deposit(suite.ctx, userAddr, sdk.NewCoins(sdk.NewCoin(tc.args.borrow.Denom, tc.args.borrow.Amount.Mul(sdk.NewInt(2))))) - suite.Require().NoError(err) - err = hardKeeper.Borrow(suite.ctx, userAddr, sdk.NewCoins(tc.args.borrow)) - suite.Require().NoError(err) - - // Set up chain context at future time - runAtTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * tc.args.timeElapsed)) - runCtx := suite.ctx.WithBlockTime(runAtTime) - - // Run Hard begin blocker in order to update the denom's index factor - hard.BeginBlocker(runCtx, suite.hardKeeper) - - // Accumulate hard borrow rewards for the deposit denom - multiRewardPeriod, found := suite.keeper.GetHardBorrowRewardPeriods(runCtx, tc.args.borrow.Denom) - suite.Require().True(found) - err = suite.keeper.AccumulateHardBorrowRewards(runCtx, multiRewardPeriod) - suite.Require().NoError(err) - - // Check that each expected reward index matches the current stored reward index for the denom - globalRewardIndexes, found := suite.keeper.GetHardBorrowRewardIndexes(runCtx, tc.args.borrow.Denom) - suite.Require().True(found) - for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { - globalRewardIndex, found := globalRewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) - suite.Require().True(found) - suite.Require().Equal(expectedRewardIndex, globalRewardIndex) - } - }) - } -} - -func (suite *KeeperTestSuite) TestSynchronizeHardBorrowReward() { - type args struct { - incentiveBorrowRewardDenom string - borrow sdk.Coin - rewardsPerSecond sdk.Coins - initialTime time.Time - blockTimes []int - expectedRewardIndexes types.RewardIndexes - expectedRewards sdk.Coins - updateRewardsViaCommmittee bool - updatedBaseDenom string - updatedRewardsPerSecond sdk.Coins - updatedExpectedRewardIndexes types.RewardIndexes - updatedExpectedRewards sdk.Coins - updatedTimeDuration int - } - type test struct { - name string - args args - } - - testCases := []test{ - { - "single reward denom: 10 blocks", - args{ - incentiveBorrowRewardDenom: "bnb", - borrow: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.001223540000173228"))}, - expectedRewards: cs(c("hard", 12235400)), - updateRewardsViaCommmittee: false, - }, - }, - { - "single reward denom: 10 blocks - long block time", - args{ - incentiveBorrowRewardDenom: "bnb", - borrow: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("10.571385603126235340"))}, - expectedRewards: cs(c("hard", 105713856031)), - }, - }, - { - "single reward denom: user reward index updated when reward is zero", - args{ - incentiveBorrowRewardDenom: "ukava", - borrow: c("ukava", 1), // borrow a tiny amount so that rewards round to zero - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.122354003908172328"))}, - expectedRewards: cs(), - updateRewardsViaCommmittee: false, - }, - }, - { - "multiple reward denoms: 10 blocks", - args{ - incentiveBorrowRewardDenom: "bnb", - borrow: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, - expectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.001223540000173228")), - types.NewRewardIndex("ukava", d("0.001223540000173228")), - }, - expectedRewards: cs(c("hard", 12235400), c("ukava", 12235400)), - }, - }, - { - "multiple reward denoms: 10 blocks - long block time", - args{ - incentiveBorrowRewardDenom: "bnb", - borrow: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, - expectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("10.571385603126235340")), - types.NewRewardIndex("ukava", d("10.571385603126235340")), - }, - expectedRewards: cs(c("hard", 105713856031), c("ukava", 105713856031)), - }, - }, - { - "multiple reward denoms with different rewards per second: 10 blocks", - args{ - incentiveBorrowRewardDenom: "bnb", - borrow: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 555555)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, - expectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.001223540000173228")), - types.NewRewardIndex("ukava", d("0.005555550000786558")), - }, - expectedRewards: cs(c("hard", 12235400), c("ukava", 55555500)), - }, - }, - { - "denom is in incentive's hard borrow reward params but it has no rewards; add reward", - args{ - incentiveBorrowRewardDenom: "bnb", - borrow: c("bnb", 10000000000), - rewardsPerSecond: sdk.Coins{}, - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{100}, - expectedRewardIndexes: types.RewardIndexes{}, - expectedRewards: sdk.Coins{}, - updateRewardsViaCommmittee: true, - updatedBaseDenom: "bnb", - updatedRewardsPerSecond: cs(c("hard", 100000)), - updatedExpectedRewards: cs(c("hard", 8640000000)), - updatedExpectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.864000000049803065")), - }, - updatedTimeDuration: 86400, - }, - }, - { - "denom is in incentive's hard borrow reward params and has rewards; add new reward type", - args{ - incentiveBorrowRewardDenom: "bnb", - borrow: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{86400}, - expectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("1.057138560060101160")), - }, - expectedRewards: cs(c("hard", 10571385601)), - updateRewardsViaCommmittee: true, - updatedBaseDenom: "bnb", - updatedRewardsPerSecond: cs(c("hard", 122354), c("ukava", 100000)), - updatedExpectedRewards: cs(c("hard", 21142771202), c("ukava", 8640000000)), - updatedExpectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("2.114277120120202320")), - types.NewRewardIndex("ukava", d("0.864000000049120715")), - }, - updatedTimeDuration: 86400, - }, - }, - { - "denom is in hard's money market params but not in incentive's hard supply reward params; add reward", - args{ - incentiveBorrowRewardDenom: "bnb", - borrow: c("zzz", 10000000000), - rewardsPerSecond: sdk.Coins{}, - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{100}, - expectedRewardIndexes: types.RewardIndexes{}, - expectedRewards: sdk.Coins{}, - updateRewardsViaCommmittee: true, - updatedBaseDenom: "zzz", - updatedRewardsPerSecond: cs(c("hard", 100000)), - updatedExpectedRewards: cs(c("hard", 8640000000)), - updatedExpectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.864000000049803065")), - }, - updatedTimeDuration: 86400, - }, - }, - { - "denom incentive's hard borrow reward params but it has no rewards; add multiple reward types", - args{ - incentiveBorrowRewardDenom: "bnb", - borrow: c("bnb", 10000000000), - rewardsPerSecond: sdk.Coins{}, - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{100}, - expectedRewardIndexes: types.RewardIndexes{}, - expectedRewards: sdk.Coins{}, - updateRewardsViaCommmittee: true, - updatedBaseDenom: "bnb", - updatedRewardsPerSecond: cs(c("hard", 100000), c("ukava", 100500), c("swap", 500)), - updatedExpectedRewards: cs(c("hard", 8640000000), c("ukava", 8683200001), c("swap", 43200000)), - updatedExpectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.864000000049803065")), - types.NewRewardIndex("ukava", d("0.868320000050052081")), - types.NewRewardIndex("swap", d("0.004320000000249015")), - }, - updatedTimeDuration: 86400, - }, - }, - { - "denom is in hard's money market params but not in incentive's hard supply reward params; add multiple reward types", - args{ - incentiveBorrowRewardDenom: "bnb", - borrow: c("zzz", 10000000000), - rewardsPerSecond: sdk.Coins{}, - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{100}, - expectedRewardIndexes: types.RewardIndexes{}, - expectedRewards: sdk.Coins{}, - updateRewardsViaCommmittee: true, - updatedBaseDenom: "zzz", - updatedRewardsPerSecond: cs(c("hard", 100000), c("ukava", 100500), c("swap", 500)), - updatedExpectedRewards: cs(c("hard", 8640000000), c("ukava", 8683200001), c("swap", 43200000)), - updatedExpectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.864000000049803065")), - types.NewRewardIndex("ukava", d("0.868320000050052081")), - types.NewRewardIndex("swap", d("0.004320000000249015")), - }, - updatedTimeDuration: 86400, - }, - }, - } - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.SetupWithGenState() - suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) - - // Mint coins to hard module account - supplyKeeper := suite.app.GetSupplyKeeper() - hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) - supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) - - // Set up incentive state - incentiveParams := types.NewParams( - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.incentiveBorrowRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), c("hard", 1))}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.incentiveBorrowRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), sdk.Coins{})}, // Don't set any supply rewards for easier accounting - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.incentiveBorrowRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.incentiveBorrowRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), c("hard", 1))}, - types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - tc.args.initialTime.Add(time.Hour*24*365*5), - ) - suite.keeper.SetParams(suite.ctx, incentiveParams) - suite.keeper.SetPreviousHardBorrowRewardAccrualTime(suite.ctx, tc.args.incentiveBorrowRewardDenom, tc.args.initialTime) - var rewardIndexes types.RewardIndexes - for _, rewardCoin := range tc.args.rewardsPerSecond { - rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) - rewardIndexes = append(rewardIndexes, rewardIndex) - } - if len(rewardIndexes) > 0 { - suite.keeper.SetHardBorrowRewardIndexes(suite.ctx, tc.args.incentiveBorrowRewardDenom, rewardIndexes) - } - - // Set up hard state (interest factor for the relevant denom) - suite.hardKeeper.SetSupplyInterestFactor(suite.ctx, tc.args.borrow.Denom, sdk.MustNewDecFromStr("1.0")) - suite.hardKeeper.SetBorrowInterestFactor(suite.ctx, tc.args.borrow.Denom, sdk.MustNewDecFromStr("1.0")) - suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.borrow.Denom, tc.args.initialTime) - // Set the minimum borrow to 0 to allow testing small borrows - hardParams := suite.hardKeeper.GetParams(suite.ctx) - hardParams.MinimumBorrowUSDValue = sdk.ZeroDec() - suite.hardKeeper.SetParams(suite.ctx, hardParams) - - // Borrow a fixed amount from another user to dilute primary user's rewards per second. - suite.Require().NoError( - suite.hardKeeper.Deposit(suite.ctx, suite.addrs[2], cs(c("ukava", 200_000_000))), - ) - suite.Require().NoError( - suite.hardKeeper.Borrow(suite.ctx, suite.addrs[2], cs(c("ukava", 100_000_000))), - ) - - // User deposits and borrows to increase total borrowed amount - hardKeeper := suite.app.GetHardKeeper() - userAddr := suite.addrs[3] - err := hardKeeper.Deposit(suite.ctx, userAddr, sdk.NewCoins(sdk.NewCoin(tc.args.borrow.Denom, tc.args.borrow.Amount.Mul(sdk.NewInt(2))))) - suite.Require().NoError(err) - err = hardKeeper.Borrow(suite.ctx, userAddr, sdk.NewCoins(tc.args.borrow)) - suite.Require().NoError(err) - - // Check that Hard hooks initialized a HardLiquidityProviderClaim - claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) - suite.Require().True(found) - multiRewardIndex, _ := claim.BorrowRewardIndexes.GetRewardIndex(tc.args.borrow.Denom) - for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { - currRewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) - suite.Require().True(found) - suite.Require().Equal(sdk.ZeroDec(), currRewardIndex.RewardFactor) - } - - // Run accumulator at several intervals - var timeElapsed int - previousBlockTime := suite.ctx.BlockTime() - for _, t := range tc.args.blockTimes { - timeElapsed += t - updatedBlockTime := previousBlockTime.Add(time.Duration(int(time.Second) * t)) - previousBlockTime = updatedBlockTime - blockCtx := suite.ctx.WithBlockTime(updatedBlockTime) - - // Run Hard begin blocker for each block ctx to update denom's interest factor - hard.BeginBlocker(blockCtx, suite.hardKeeper) - - // Accumulate hard borrow-side rewards - multiRewardPeriod, found := suite.keeper.GetHardBorrowRewardPeriods(blockCtx, tc.args.borrow.Denom) - if found { - err := suite.keeper.AccumulateHardBorrowRewards(blockCtx, multiRewardPeriod) - suite.Require().NoError(err) - } - } - updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed)) - suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) - - // After we've accumulated, run synchronize - borrow, found := hardKeeper.GetBorrow(suite.ctx, userAddr) - suite.Require().True(found) - suite.Require().NotPanics(func() { - suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow) - }) - - // Check that the global reward index's reward factor and user's claim have been updated as expected - claim, found = suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) - suite.Require().True(found) - globalRewardIndexes, foundGlobalRewardIndexes := suite.keeper.GetHardBorrowRewardIndexes(suite.ctx, tc.args.borrow.Denom) - if len(tc.args.rewardsPerSecond) > 0 { - suite.Require().True(foundGlobalRewardIndexes) - for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { - // Check that global reward index has been updated as expected - globalRewardIndex, found := globalRewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) - suite.Require().True(found) - suite.Require().Equal(expectedRewardIndex, globalRewardIndex) - - // Check that the user's claim's reward index matches the corresponding global reward index - multiRewardIndex, found := claim.BorrowRewardIndexes.GetRewardIndex(tc.args.borrow.Denom) - suite.Require().True(found) - rewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) - suite.Require().True(found) - suite.Require().Equal(expectedRewardIndex, rewardIndex) - - // Check that the user's claim holds the expected amount of reward coins - suite.Require().Equal( - tc.args.expectedRewards.AmountOf(expectedRewardIndex.CollateralType), - claim.Reward.AmountOf(expectedRewardIndex.CollateralType), - ) - } - } - - // Only test cases with reward param updates continue past this point - if !tc.args.updateRewardsViaCommmittee { - return - } - - // If are no initial rewards per second, add new rewards through a committee param change - // 1. Construct incentive's new HardBorrowRewardPeriods param - currIncentiveHardBorrowRewardPeriods := suite.keeper.GetParams(suite.ctx).HardBorrowRewardPeriods - multiRewardPeriod, found := currIncentiveHardBorrowRewardPeriods.GetMultiRewardPeriod(tc.args.borrow.Denom) - if found { - // Borrow denom's reward period exists, but it doesn't have any rewards per second - index, found := currIncentiveHardBorrowRewardPeriods.GetMultiRewardPeriodIndex(tc.args.borrow.Denom) - suite.Require().True(found) - multiRewardPeriod.RewardsPerSecond = tc.args.updatedRewardsPerSecond - currIncentiveHardBorrowRewardPeriods[index] = multiRewardPeriod - } else { - // Borrow denom's reward period does not exist - _, found := currIncentiveHardBorrowRewardPeriods.GetMultiRewardPeriodIndex(tc.args.borrow.Denom) - suite.Require().False(found) - newMultiRewardPeriod := types.NewMultiRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.updatedRewardsPerSecond) - currIncentiveHardBorrowRewardPeriods = append(currIncentiveHardBorrowRewardPeriods, newMultiRewardPeriod) - } - - // 2. Construct the parameter change proposal to update HardBorrowRewardPeriods param - pubProposal := params.NewParameterChangeProposal( - "Update hard borrow rewards", "Adds a new reward coin to the incentive module's hard borrow rewards.", - []params.ParamChange{ - { - Subspace: types.ModuleName, // target incentive module - Key: string(types.KeyHardBorrowRewardPeriods), // target hard borrow rewards key - Value: string(suite.app.Codec().MustMarshalJSON(currIncentiveHardBorrowRewardPeriods)), - }, - }, - ) - - // 3. Ensure proposal is properly formed - err = suite.committeeKeeper.ValidatePubProposal(suite.ctx, pubProposal) - suite.Require().NoError(err) - - // 4. Committee creates proposal - committeeMemberOne := suite.addrs[0] - committeeMemberTwo := suite.addrs[1] - proposalID, err := suite.committeeKeeper.SubmitProposal(suite.ctx, committeeMemberOne, 1, pubProposal) - suite.Require().NoError(err) - - // 5. Committee votes and passes proposal - err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberOne) - err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberTwo) - - // 6. Check proposal passed - proposalPasses, err := suite.committeeKeeper.GetProposalResult(suite.ctx, proposalID) - suite.Require().NoError(err) - suite.Require().True(proposalPasses) - - // 7. Run committee module's begin blocker to enact proposal - suite.NotPanics(func() { - committee.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}, suite.committeeKeeper) - }) - - // We need to accumulate hard supply-side rewards again - multiRewardPeriod, found = suite.keeper.GetHardBorrowRewardPeriods(suite.ctx, tc.args.borrow.Denom) - suite.Require().True(found) - - // But new borrow denoms don't have their PreviousHardBorrowRewardAccrualTime set yet, - // so we need to call the accumulation method once to set the initial reward accrual time - if tc.args.borrow.Denom != tc.args.incentiveBorrowRewardDenom { - err = suite.keeper.AccumulateHardBorrowRewards(suite.ctx, multiRewardPeriod) - suite.Require().NoError(err) - } - - // Now we can jump forward in time and accumulate rewards - updatedBlockTime = previousBlockTime.Add(time.Duration(int(time.Second) * tc.args.updatedTimeDuration)) - suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) - err = suite.keeper.AccumulateHardBorrowRewards(suite.ctx, multiRewardPeriod) - suite.Require().NoError(err) - - // After we've accumulated, run synchronize - borrow, found = hardKeeper.GetBorrow(suite.ctx, userAddr) - suite.Require().True(found) - suite.Require().NotPanics(func() { - suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow) - }) - - // Check that the global reward index's reward factor and user's claim have been updated as expected - globalRewardIndexes, found = suite.keeper.GetHardBorrowRewardIndexes(suite.ctx, tc.args.borrow.Denom) - suite.Require().True(found) - claim, found = suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) - suite.Require().True(found) - - for _, expectedRewardIndex := range tc.args.updatedExpectedRewardIndexes { - // Check that global reward index has been updated as expected - globalRewardIndex, found := globalRewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) - suite.Require().True(found) - suite.Require().Equal(expectedRewardIndex, globalRewardIndex) - // Check that the user's claim's reward index matches the corresponding global reward index - multiRewardIndex, found := claim.BorrowRewardIndexes.GetRewardIndex(tc.args.borrow.Denom) - suite.Require().True(found) - rewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) - suite.Require().True(found) - suite.Require().Equal(expectedRewardIndex, rewardIndex) - - // Check that the user's claim holds the expected amount of reward coins - suite.Require().Equal( - tc.args.updatedExpectedRewards.AmountOf(expectedRewardIndex.CollateralType), - claim.Reward.AmountOf(expectedRewardIndex.CollateralType), - ) - } - }) - } -} - -func (suite *KeeperTestSuite) TestAccumulateHardDelegatorRewards() { - type args struct { - delegation sdk.Coin - rewardsPerSecond sdk.Coin - initialTime time.Time - timeElapsed int - expectedRewardFactor sdk.Dec - } - type test struct { - name string - args args - } - testCases := []test{ - { - "7 seconds", - args{ - delegation: c("ukava", 1_000_000), - rewardsPerSecond: c("hard", 122354), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - timeElapsed: 7, - expectedRewardFactor: d("0.428239000000000000"), - }, - }, - { - "1 day", - args{ - delegation: c("ukava", 1_000_000), - rewardsPerSecond: c("hard", 122354), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - timeElapsed: 86400, - expectedRewardFactor: d("5285.692800000000000000"), - }, - }, - { - "0 seconds", - args{ - delegation: c("ukava", 1_000_000), - rewardsPerSecond: c("hard", 122354), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - timeElapsed: 0, - expectedRewardFactor: d("0.0"), - }, - }, - } - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.SetupWithGenState() - suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) - - // Mint coins to hard module account - supplyKeeper := suite.app.GetSupplyKeeper() - hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) - supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) - - // Set up incentive state - params := types.NewParams( - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), cs(tc.args.rewardsPerSecond))}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), cs(tc.args.rewardsPerSecond))}, - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - tc.args.initialTime.Add(time.Hour*24*365*5), - ) - suite.keeper.SetParams(suite.ctx, params) - suite.keeper.SetPreviousHardDelegatorRewardAccrualTime(suite.ctx, tc.args.delegation.Denom, tc.args.initialTime) - suite.keeper.SetHardDelegatorRewardFactor(suite.ctx, tc.args.delegation.Denom, sdk.ZeroDec()) - - // Set up hard state (interest factor for the relevant denom) - suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.delegation.Denom, tc.args.initialTime) - - err := suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[0], tc.args.delegation) - suite.Require().NoError(err) - err = suite.deliverMsgDelegate(suite.ctx, suite.addrs[0], suite.validatorAddrs[0], tc.args.delegation) - suite.Require().NoError(err) - - staking.EndBlocker(suite.ctx, suite.stakingKeeper) - - // Set up chain context at future time - runAtTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * tc.args.timeElapsed)) - runCtx := suite.ctx.WithBlockTime(runAtTime) - - // Run Hard begin blocker in order to update the denom's index factor - hard.BeginBlocker(runCtx, suite.hardKeeper) - - rewardPeriod, found := suite.keeper.GetHardDelegatorRewardPeriod(runCtx, tc.args.delegation.Denom) - suite.Require().True(found) - err = suite.keeper.AccumulateHardDelegatorRewards(runCtx, rewardPeriod) - suite.Require().NoError(err) - - rewardFactor, found := suite.keeper.GetHardDelegatorRewardFactor(runCtx, tc.args.delegation.Denom) - suite.Require().Equal(tc.args.expectedRewardFactor, rewardFactor) - }) - } -} - -func (suite *KeeperTestSuite) TestInitializeHardSupplyRewards() { - - type args struct { - moneyMarketRewardDenoms map[string][]string - deposit sdk.Coins - initialTime time.Time - expectedClaimSupplyRewardIndexes types.MultiRewardIndexes - } - type test struct { - name string - args args - } - - standardMoneyMarketRewardDenoms := map[string][]string{ - "bnb": {"hard"}, - "btcb": {"hard", "ukava"}, - "xrp": {}, - } - - testCases := []test{ - { - "single deposit denom, single reward denom", - args{ - moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, - deposit: cs(c("bnb", 1000000000000)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedClaimSupplyRewardIndexes: types.MultiRewardIndexes{ - types.NewMultiRewardIndex( - "bnb", - types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.0")), - }, - ), - }, - }, - }, - { - "single deposit denom, multiple reward denoms", - args{ - moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, - deposit: cs(c("btcb", 1000000000000)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedClaimSupplyRewardIndexes: types.MultiRewardIndexes{ - types.NewMultiRewardIndex( - "btcb", - types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.0")), - types.NewRewardIndex("ukava", d("0.0")), - }, - ), - }, - }, - }, - { - "single deposit denom, no reward denoms", - args{ - moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, - deposit: cs(c("xrp", 1000000000000)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedClaimSupplyRewardIndexes: types.MultiRewardIndexes{ - types.NewMultiRewardIndex( - "xrp", - nil, - ), - }, - }, - }, - { - "multiple deposit denoms, multiple overlapping reward denoms", - args{ - moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, - deposit: cs(c("bnb", 1000000000000), c("btcb", 1000000000000)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedClaimSupplyRewardIndexes: types.MultiRewardIndexes{ - types.NewMultiRewardIndex( - "bnb", - types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.0")), - }, - ), - types.NewMultiRewardIndex( - "btcb", - types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.0")), - types.NewRewardIndex("ukava", d("0.0")), - }, - ), - }, - }, - }, - { - "multiple deposit denoms, correct discrete reward denoms", - args{ - moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, - deposit: cs(c("bnb", 1000000000000), c("xrp", 1000000000000)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedClaimSupplyRewardIndexes: types.MultiRewardIndexes{ - types.NewMultiRewardIndex( - "bnb", - types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.0")), - }, - ), - types.NewMultiRewardIndex( - "xrp", - nil, - ), - }, - }, - }, - } - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.SetupWithGenState() - suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) - - // Mint coins to hard module account - supplyKeeper := suite.app.GetSupplyKeeper() - hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) - supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) - - userAddr := suite.addrs[3] - - // Prepare money market + reward params - i := 0 - var multiRewardPeriods types.MultiRewardPeriods - var rewardPeriods types.RewardPeriods - for moneyMarketDenom, rewardDenoms := range tc.args.moneyMarketRewardDenoms { - // Set up multi reward periods for supply/borrow indexes with dynamic money market denoms/reward denoms - var rewardsPerSecond sdk.Coins - for _, rewardDenom := range rewardDenoms { - rewardsPerSecond = append(rewardsPerSecond, sdk.NewCoin(rewardDenom, sdk.OneInt())) - } - multiRewardPeriod := types.NewMultiRewardPeriod(true, moneyMarketDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), rewardsPerSecond) - multiRewardPeriods = append(multiRewardPeriods, multiRewardPeriod) - - // Set up generic reward periods for usdx minting/delegator indexes - if i == 0 && len(rewardDenoms) > 0 { - rewardPeriod := types.NewRewardPeriod(true, moneyMarketDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), rewardsPerSecond[i]) - rewardPeriods = append(rewardPeriods, rewardPeriod) - i++ - } - } - - // Initialize and set incentive params - params := types.NewParams( - rewardPeriods, multiRewardPeriods, multiRewardPeriods, rewardPeriods, - types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - tc.args.initialTime.Add(time.Hour*24*365*5), - ) - suite.keeper.SetParams(suite.ctx, params) - - // Set each money market's previous accrual time and supply reward indexes - for moneyMarketDenom, rewardDenoms := range tc.args.moneyMarketRewardDenoms { - var rewardIndexes types.RewardIndexes - for _, rewardDenom := range rewardDenoms { - rewardIndex := types.NewRewardIndex(rewardDenom, sdk.ZeroDec()) - rewardIndexes = append(rewardIndexes, rewardIndex) - } - suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, moneyMarketDenom, tc.args.initialTime) - if len(rewardIndexes) > 0 { - suite.keeper.SetHardSupplyRewardIndexes(suite.ctx, moneyMarketDenom, rewardIndexes) - } - } - - // User deposits - hardKeeper := suite.app.GetHardKeeper() - err := hardKeeper.Deposit(suite.ctx, userAddr, tc.args.deposit) - suite.Require().NoError(err) - - claim, foundClaim := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) - suite.Require().True(foundClaim) - suite.Require().Equal(tc.args.expectedClaimSupplyRewardIndexes, claim.SupplyRewardIndexes) - }) - } -} - -func (suite *KeeperTestSuite) TestAccumulateHardSupplyRewards() { - type args struct { - deposit sdk.Coin - rewardsPerSecond sdk.Coins - initialTime time.Time - timeElapsed int - expectedRewardIndexes types.RewardIndexes - } - type test struct { - name string - args args - } - testCases := []test{ - { - "single reward denom: 7 seconds", - args{ - deposit: c("bnb", 1000000000000), - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - timeElapsed: 7, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.000000856478000000"))}, - }, - }, - { - "single reward denom: 1 day", - args{ - deposit: c("bnb", 1000000000000), - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - timeElapsed: 86400, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.010571385600000000"))}, - }, - }, - { - "single reward denom: 0 seconds", - args{ - deposit: c("bnb", 1000000000000), - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - timeElapsed: 0, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.0"))}, - }, - }, - { - "multiple reward denoms: 7 seconds", - args{ - deposit: c("bnb", 1000000000000), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - timeElapsed: 7, - expectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.000000856478000000")), - types.NewRewardIndex("ukava", d("0.000000856478000000")), - }, - }, - }, - { - "multiple reward denoms: 1 day", - args{ - deposit: c("bnb", 1000000000000), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - timeElapsed: 86400, - expectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.010571385600000000")), - types.NewRewardIndex("ukava", d("0.010571385600000000")), - }, - }, - }, - { - "multiple reward denoms: 0 seconds", - args{ - deposit: c("bnb", 1000000000000), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - timeElapsed: 0, - expectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.0")), - types.NewRewardIndex("ukava", d("0.0")), - }, - }, - }, - { - "multiple reward denoms with different rewards per second: 1 day", - args{ - deposit: c("bnb", 1000000000000), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 555555)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - timeElapsed: 86400, - expectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.010571385600000000")), - types.NewRewardIndex("ukava", d("0.047999952000000000")), - }, - }, - }, - { - "single reward denom, no rewards", - args{ - deposit: c("bnb", 1000000000000), - rewardsPerSecond: sdk.Coins{}, - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - timeElapsed: 7, - expectedRewardIndexes: types.RewardIndexes{}, - }, - }, - } - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.SetupWithGenState() - suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) - - // Mint coins to hard module account - supplyKeeper := suite.app.GetSupplyKeeper() - hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) - supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) - - // Set up incentive state - params := types.NewParams( - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), c("hard", 1))}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), c("hard", 1))}, - types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - tc.args.initialTime.Add(time.Hour*24*365*5), - ) - suite.keeper.SetParams(suite.ctx, params) - suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, tc.args.deposit.Denom, tc.args.initialTime) - var rewardIndexes types.RewardIndexes - for _, rewardCoin := range tc.args.rewardsPerSecond { - rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) - rewardIndexes = append(rewardIndexes, rewardIndex) - } - if len(rewardIndexes) > 0 { - suite.keeper.SetHardSupplyRewardIndexes(suite.ctx, tc.args.deposit.Denom, rewardIndexes) - } - - // Set up hard state (interest factor for the relevant denom) - suite.hardKeeper.SetSupplyInterestFactor(suite.ctx, tc.args.deposit.Denom, sdk.MustNewDecFromStr("1.0")) - suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.deposit.Denom, tc.args.initialTime) - - // User deposits to increase total supplied amount - hardKeeper := suite.app.GetHardKeeper() - userAddr := suite.addrs[3] - err := hardKeeper.Deposit(suite.ctx, userAddr, sdk.NewCoins(tc.args.deposit)) - suite.Require().NoError(err) - - // Set up chain context at future time - runAtTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * tc.args.timeElapsed)) - runCtx := suite.ctx.WithBlockTime(runAtTime) - - // Run Hard begin blocker in order to update the denom's index factor - hard.BeginBlocker(runCtx, suite.hardKeeper) - - // Accumulate hard supply rewards for the deposit denom - multiRewardPeriod, found := suite.keeper.GetHardSupplyRewardPeriods(runCtx, tc.args.deposit.Denom) - suite.Require().True(found) - err = suite.keeper.AccumulateHardSupplyRewards(runCtx, multiRewardPeriod) - suite.Require().NoError(err) - - // Check that each expected reward index matches the current stored reward index for the denom - globalRewardIndexes, found := suite.keeper.GetHardSupplyRewardIndexes(runCtx, tc.args.deposit.Denom) - if len(tc.args.rewardsPerSecond) > 0 { - suite.Require().True(found) - for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { - globalRewardIndex, found := globalRewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) - suite.Require().True(found) - suite.Require().Equal(expectedRewardIndex, globalRewardIndex) - } - } else { - suite.Require().False(found) - } - - }) - } -} - -func (suite *KeeperTestSuite) TestSynchronizeHardSupplyReward() { - type args struct { - incentiveSupplyRewardDenom string - deposit sdk.Coin - rewardsPerSecond sdk.Coins - initialTime time.Time - blockTimes []int - expectedRewardIndexes types.RewardIndexes - expectedRewards sdk.Coins - updateRewardsViaCommmittee bool - updatedBaseDenom string - updatedRewardsPerSecond sdk.Coins - updatedExpectedRewardIndexes types.RewardIndexes - updatedExpectedRewards sdk.Coins - updatedTimeDuration int - } - type test struct { - name string - args args - } - - testCases := []test{ - { - "single reward denom: 10 blocks", - args{ - incentiveSupplyRewardDenom: "bnb", - deposit: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.001223540000000000"))}, - expectedRewards: cs(c("hard", 12235400)), - updateRewardsViaCommmittee: false, - }, - }, - { - "single reward denom: 10 blocks - long block time", - args{ - incentiveSupplyRewardDenom: "bnb", - deposit: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("10.571385600000000000"))}, - expectedRewards: cs(c("hard", 105713856000)), - updateRewardsViaCommmittee: false, - }, - }, - { - "single reward denom: user reward index updated when reward is zero", - args{ - incentiveSupplyRewardDenom: "ukava", - deposit: c("ukava", 1), - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.122353998776460010"))}, - expectedRewards: cs(), - updateRewardsViaCommmittee: false, - }, - }, - { - "multiple reward denoms: 10 blocks", - args{ - incentiveSupplyRewardDenom: "bnb", - deposit: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, - expectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.001223540000000000")), - types.NewRewardIndex("ukava", d("0.001223540000000000")), - }, - expectedRewards: cs(c("hard", 12235400), c("ukava", 12235400)), - updateRewardsViaCommmittee: false, - }, - }, - { - "multiple reward denoms: 10 blocks - long block time", - args{ - incentiveSupplyRewardDenom: "bnb", - deposit: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, - expectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("10.571385600000000000")), - types.NewRewardIndex("ukava", d("10.571385600000000000")), - }, - expectedRewards: cs(c("hard", 105713856000), c("ukava", 105713856000)), - updateRewardsViaCommmittee: false, - }, - }, - { - "multiple reward denoms with different rewards per second: 10 blocks", - args{ - incentiveSupplyRewardDenom: "bnb", - deposit: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 555555)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, - expectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.001223540000000000")), - types.NewRewardIndex("ukava", d("0.005555550000000000")), - }, - expectedRewards: cs(c("hard", 12235400), c("ukava", 55555500)), - updateRewardsViaCommmittee: false, - }, - }, - { - "denom is in incentive's hard supply reward params but it has no rewards; add reward", - args{ - incentiveSupplyRewardDenom: "bnb", - deposit: c("bnb", 10000000000), - rewardsPerSecond: sdk.Coins{}, - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{100}, - expectedRewardIndexes: types.RewardIndexes{}, - expectedRewards: sdk.Coins{}, - updateRewardsViaCommmittee: true, - updatedBaseDenom: "bnb", - updatedRewardsPerSecond: cs(c("hard", 100000)), - updatedExpectedRewards: cs(c("hard", 8640000000)), - updatedExpectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.864")), - }, - updatedTimeDuration: 86400, - }, - }, - { - "denom is in incentive's hard supply reward params and has rewards; add new reward type", - args{ - incentiveSupplyRewardDenom: "bnb", - deposit: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{86400}, - expectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("1.057138560000000000")), - }, - expectedRewards: cs(c("hard", 10571385600)), - updateRewardsViaCommmittee: true, - updatedBaseDenom: "bnb", - updatedRewardsPerSecond: cs(c("hard", 122354), c("ukava", 100000)), - updatedExpectedRewards: cs(c("hard", 21142771200), c("ukava", 8640000000)), - updatedExpectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("2.114277120000000000")), - types.NewRewardIndex("ukava", d("0.864000000000000000")), - }, - updatedTimeDuration: 86400, - }, - }, - { - "denom is in hard's money market params but not in incentive's hard supply reward params; add reward", - args{ - incentiveSupplyRewardDenom: "bnb", - deposit: c("zzz", 10000000000), - rewardsPerSecond: sdk.Coins{}, - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{100}, - expectedRewardIndexes: types.RewardIndexes{}, - expectedRewards: sdk.Coins{}, - updateRewardsViaCommmittee: true, - updatedBaseDenom: "zzz", - updatedRewardsPerSecond: cs(c("hard", 100000)), - updatedExpectedRewards: cs(c("hard", 8640000000)), - updatedExpectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.864")), - }, - updatedTimeDuration: 86400, - }, - }, - { - "denom incentive's hard supply reward params but it has no rewards; add multiple reward types", - args{ - incentiveSupplyRewardDenom: "bnb", - deposit: c("bnb", 10000000000), - rewardsPerSecond: sdk.Coins{}, - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{100}, - expectedRewardIndexes: types.RewardIndexes{}, - expectedRewards: sdk.Coins{}, - updateRewardsViaCommmittee: true, - updatedBaseDenom: "bnb", - updatedRewardsPerSecond: cs(c("hard", 100000), c("ukava", 100500), c("swap", 500)), - updatedExpectedRewards: cs(c("hard", 8640000000), c("ukava", 8683200000), c("swap", 43200000)), - updatedExpectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.864")), - types.NewRewardIndex("ukava", d("0.86832")), - types.NewRewardIndex("swap", d("0.00432")), - }, - updatedTimeDuration: 86400, - }, - }, - { - "denom is in hard's money market params but not in incentive's hard supply reward params; add multiple reward types", - args{ - incentiveSupplyRewardDenom: "bnb", - deposit: c("zzz", 10000000000), - rewardsPerSecond: sdk.Coins{}, - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{100}, - expectedRewardIndexes: types.RewardIndexes{}, - expectedRewards: sdk.Coins{}, - updateRewardsViaCommmittee: true, - updatedBaseDenom: "zzz", - updatedRewardsPerSecond: cs(c("hard", 100000), c("ukava", 100500), c("swap", 500)), - updatedExpectedRewards: cs(c("hard", 8640000000), c("ukava", 8683200000), c("swap", 43200000)), - updatedExpectedRewardIndexes: types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.864")), - types.NewRewardIndex("ukava", d("0.86832")), - types.NewRewardIndex("swap", d("0.00432")), - }, - updatedTimeDuration: 86400, - }, - }, - } - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.SetupWithGenState() - suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) - - // Mint coins to hard module account - supplyKeeper := suite.app.GetSupplyKeeper() - hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) - supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) - - // Set up incentive state - incentiveParams := types.NewParams( - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.incentiveSupplyRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), c("hard", 1))}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.incentiveSupplyRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.incentiveSupplyRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.incentiveSupplyRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), c("hard", 1))}, - types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - tc.args.initialTime.Add(time.Hour*24*365*5), - ) - suite.keeper.SetParams(suite.ctx, incentiveParams) - suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, tc.args.incentiveSupplyRewardDenom, tc.args.initialTime) - var rewardIndexes types.RewardIndexes - for _, rewardCoin := range tc.args.rewardsPerSecond { - rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) - rewardIndexes = append(rewardIndexes, rewardIndex) - } - if len(rewardIndexes) > 0 { - suite.keeper.SetHardSupplyRewardIndexes(suite.ctx, tc.args.incentiveSupplyRewardDenom, rewardIndexes) - } - - // Set up hard state (interest factor for the relevant denom) - suite.hardKeeper.SetSupplyInterestFactor(suite.ctx, tc.args.incentiveSupplyRewardDenom, sdk.MustNewDecFromStr("1.0")) - suite.hardKeeper.SetBorrowInterestFactor(suite.ctx, tc.args.incentiveSupplyRewardDenom, sdk.MustNewDecFromStr("1.0")) - suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.incentiveSupplyRewardDenom, tc.args.initialTime) - - // Deposit a fixed amount from another user to dilute primary user's rewards per second. - suite.Require().NoError( - suite.hardKeeper.Deposit(suite.ctx, suite.addrs[2], cs(c("ukava", 100_000_000))), - ) - - // User deposits and borrows to increase total borrowed amount - hardKeeper := suite.app.GetHardKeeper() - userAddr := suite.addrs[3] - err := hardKeeper.Deposit(suite.ctx, userAddr, sdk.NewCoins(tc.args.deposit)) - suite.Require().NoError(err) - - // Check that Hard hooks initialized a HardLiquidityProviderClaim with 0 reward indexes - claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) - suite.Require().True(found) - multiRewardIndex, _ := claim.SupplyRewardIndexes.GetRewardIndex(tc.args.deposit.Denom) - for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { - currRewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) - suite.Require().True(found) - suite.Require().Equal(sdk.ZeroDec(), currRewardIndex.RewardFactor) - } - - // Run accumulator at several intervals - var timeElapsed int - previousBlockTime := suite.ctx.BlockTime() - for _, t := range tc.args.blockTimes { - timeElapsed += t - updatedBlockTime := previousBlockTime.Add(time.Duration(int(time.Second) * t)) - previousBlockTime = updatedBlockTime - blockCtx := suite.ctx.WithBlockTime(updatedBlockTime) - - // Run Hard begin blocker for each block ctx to update denom's interest factor - hard.BeginBlocker(blockCtx, suite.hardKeeper) - - // Accumulate hard supply-side rewards - multiRewardPeriod, found := suite.keeper.GetHardSupplyRewardPeriods(blockCtx, tc.args.deposit.Denom) - if found { - err := suite.keeper.AccumulateHardSupplyRewards(blockCtx, multiRewardPeriod) - suite.Require().NoError(err) - } - } - updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed)) - suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) - - // After we've accumulated, run synchronize - deposit, found := hardKeeper.GetDeposit(suite.ctx, userAddr) - suite.Require().True(found) - suite.Require().NotPanics(func() { - suite.keeper.SynchronizeHardSupplyReward(suite.ctx, deposit) - }) - - // Check that the global reward index's reward factor and user's claim have been updated as expected - claim, found = suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) - suite.Require().True(found) - globalRewardIndexes, foundGlobalRewardIndexes := suite.keeper.GetHardSupplyRewardIndexes(suite.ctx, tc.args.deposit.Denom) - if len(tc.args.rewardsPerSecond) > 0 { - suite.Require().True(foundGlobalRewardIndexes) - for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { - // Check that global reward index has been updated as expected - globalRewardIndex, found := globalRewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) - suite.Require().True(found) - suite.Require().Equal(expectedRewardIndex, globalRewardIndex) - - // Check that the user's claim's reward index matches the corresponding global reward index - multiRewardIndex, found := claim.SupplyRewardIndexes.GetRewardIndex(tc.args.deposit.Denom) - suite.Require().True(found) - rewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) - suite.Require().True(found) - suite.Require().Equal(expectedRewardIndex, rewardIndex) - - // Check that the user's claim holds the expected amount of reward coins - suite.Require().Equal( - tc.args.expectedRewards.AmountOf(expectedRewardIndex.CollateralType), - claim.Reward.AmountOf(expectedRewardIndex.CollateralType), - ) - } - } - - // Only test cases with reward param updates continue past this point - if !tc.args.updateRewardsViaCommmittee { - return - } - - // If are no initial rewards per second, add new rewards through a committee param change - // 1. Construct incentive's new HardSupplyRewardPeriods param - currIncentiveHardSupplyRewardPeriods := suite.keeper.GetParams(suite.ctx).HardSupplyRewardPeriods - multiRewardPeriod, found := currIncentiveHardSupplyRewardPeriods.GetMultiRewardPeriod(tc.args.deposit.Denom) - if found { - // Deposit denom's reward period exists, but it doesn't have any rewards per second - index, found := currIncentiveHardSupplyRewardPeriods.GetMultiRewardPeriodIndex(tc.args.deposit.Denom) - suite.Require().True(found) - multiRewardPeriod.RewardsPerSecond = tc.args.updatedRewardsPerSecond - currIncentiveHardSupplyRewardPeriods[index] = multiRewardPeriod - } else { - // Deposit denom's reward period does not exist - _, found := currIncentiveHardSupplyRewardPeriods.GetMultiRewardPeriodIndex(tc.args.deposit.Denom) - suite.Require().False(found) - newMultiRewardPeriod := types.NewMultiRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.updatedRewardsPerSecond) - currIncentiveHardSupplyRewardPeriods = append(currIncentiveHardSupplyRewardPeriods, newMultiRewardPeriod) - } - - // 2. Construct the parameter change proposal to update HardSupplyRewardPeriods param - pubProposal := params.NewParameterChangeProposal( - "Update hard supply rewards", "Adds a new reward coin to the incentive module's hard supply rewards.", - []params.ParamChange{ - { - Subspace: types.ModuleName, // target incentive module - Key: string(types.KeyHardSupplyRewardPeriods), // target hard supply rewards key - Value: string(suite.app.Codec().MustMarshalJSON(currIncentiveHardSupplyRewardPeriods)), - }, - }, - ) - - // 3. Ensure proposal is properly formed - err = suite.committeeKeeper.ValidatePubProposal(suite.ctx, pubProposal) - suite.Require().NoError(err) - - // 4. Committee creates proposal - committeeMemberOne := suite.addrs[0] - committeeMemberTwo := suite.addrs[1] - proposalID, err := suite.committeeKeeper.SubmitProposal(suite.ctx, committeeMemberOne, 1, pubProposal) - suite.Require().NoError(err) - - // 5. Committee votes and passes proposal - err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberOne) - err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberTwo) - - // 6. Check proposal passed - proposalPasses, err := suite.committeeKeeper.GetProposalResult(suite.ctx, proposalID) - suite.Require().NoError(err) - suite.Require().True(proposalPasses) - - // 7. Run committee module's begin blocker to enact proposal - suite.NotPanics(func() { - committee.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}, suite.committeeKeeper) - }) - - // We need to accumulate hard supply-side rewards again - multiRewardPeriod, found = suite.keeper.GetHardSupplyRewardPeriods(suite.ctx, tc.args.deposit.Denom) - suite.Require().True(found) - - // But new deposit denoms don't have their PreviousHardSupplyRewardAccrualTime set yet, - // so we need to call the accumulation method once to set the initial reward accrual time - if tc.args.deposit.Denom != tc.args.incentiveSupplyRewardDenom { - err = suite.keeper.AccumulateHardSupplyRewards(suite.ctx, multiRewardPeriod) - suite.Require().NoError(err) - } - - // Now we can jump forward in time and accumulate rewards - updatedBlockTime = previousBlockTime.Add(time.Duration(int(time.Second) * tc.args.updatedTimeDuration)) - suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) - err = suite.keeper.AccumulateHardSupplyRewards(suite.ctx, multiRewardPeriod) - suite.Require().NoError(err) - - // After we've accumulated, run synchronize - deposit, found = hardKeeper.GetDeposit(suite.ctx, userAddr) - suite.Require().True(found) - suite.Require().NotPanics(func() { - suite.keeper.SynchronizeHardSupplyReward(suite.ctx, deposit) - }) - - // Check that the global reward index's reward factor and user's claim have been updated as expected - globalRewardIndexes, found = suite.keeper.GetHardSupplyRewardIndexes(suite.ctx, tc.args.deposit.Denom) - suite.Require().True(found) - claim, found = suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) - suite.Require().True(found) - for _, expectedRewardIndex := range tc.args.updatedExpectedRewardIndexes { - // Check that global reward index has been updated as expected - globalRewardIndex, found := globalRewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) - suite.Require().True(found) - suite.Require().Equal(expectedRewardIndex, globalRewardIndex) - - // Check that the user's claim's reward index matches the corresponding global reward index - multiRewardIndex, found := claim.SupplyRewardIndexes.GetRewardIndex(tc.args.deposit.Denom) - suite.Require().True(found) - rewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) - suite.Require().True(found) - suite.Require().Equal(expectedRewardIndex, rewardIndex) - - // Check that the user's claim holds the expected amount of reward coins - suite.Require().Equal( - tc.args.updatedExpectedRewards.AmountOf(expectedRewardIndex.CollateralType), - claim.Reward.AmountOf(expectedRewardIndex.CollateralType), - ) - } - }) - } -} - -func (suite *KeeperTestSuite) TestUpdateHardSupplyIndexDenoms() { - type depositModification struct { - coins sdk.Coins - withdraw bool - } - - type args struct { - firstDeposit sdk.Coins - modification depositModification - rewardsPerSecond sdk.Coins - initialTime time.Time - expectedSupplyIndexDenoms []string - } - type test struct { - name string - args args - } - - testCases := []test{ - { - "single reward denom: update adds one supply reward index", - args{ - firstDeposit: cs(c("bnb", 10000000000)), - modification: depositModification{coins: cs(c("ukava", 10000000000))}, - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedSupplyIndexDenoms: []string{"bnb", "ukava"}, - }, - }, - { - "single reward denom: update adds multiple supply reward indexes", - args{ - firstDeposit: cs(c("bnb", 10000000000)), - modification: depositModification{coins: cs(c("ukava", 10000000000), c("btcb", 10000000000), c("xrp", 10000000000))}, - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedSupplyIndexDenoms: []string{"bnb", "ukava", "btcb", "xrp"}, - }, - }, - { - "single reward denom: update doesn't add duplicate supply reward index for same denom", - args{ - firstDeposit: cs(c("bnb", 10000000000)), - modification: depositModification{coins: cs(c("bnb", 5000000000))}, - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedSupplyIndexDenoms: []string{"bnb"}, - }, - }, - { - "multiple reward denoms: update adds one supply reward index", - args{ - firstDeposit: cs(c("bnb", 10000000000)), - modification: depositModification{coins: cs(c("ukava", 10000000000))}, - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedSupplyIndexDenoms: []string{"bnb", "ukava"}, - }, - }, - { - "multiple reward denoms: update adds multiple supply reward indexes", - args{ - firstDeposit: cs(c("bnb", 10000000000)), - modification: depositModification{coins: cs(c("ukava", 10000000000), c("btcb", 10000000000), c("xrp", 10000000000))}, - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedSupplyIndexDenoms: []string{"bnb", "ukava", "btcb", "xrp"}, - }, - }, - { - "multiple reward denoms: update doesn't add duplicate supply reward index for same denom", - args{ - firstDeposit: cs(c("bnb", 10000000000)), - modification: depositModification{coins: cs(c("bnb", 5000000000))}, - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedSupplyIndexDenoms: []string{"bnb"}, - }, - }, - { - "single reward denom: fully withdrawing a denom deletes the denom's supply reward index", - args{ - firstDeposit: cs(c("bnb", 1000000000)), - modification: depositModification{coins: cs(c("bnb", 1100000000)), withdraw: true}, - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedSupplyIndexDenoms: []string{}, - }, - }, - { - "single reward denom: fully withdrawing a denom deletes only the denom's supply reward index", - args{ - firstDeposit: cs(c("bnb", 1000000000), c("ukava", 100000000)), - modification: depositModification{coins: cs(c("bnb", 1100000000)), withdraw: true}, - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedSupplyIndexDenoms: []string{"ukava"}, - }, - }, - { - "multiple reward denoms: fully repaying a denom deletes the denom's supply reward index", - args{ - firstDeposit: cs(c("bnb", 1000000000)), - modification: depositModification{coins: cs(c("bnb", 1100000000)), withdraw: true}, - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedSupplyIndexDenoms: []string{}, - }, - }, - } - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.SetupWithGenState() - suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) - - // Mint coins to hard module account - supplyKeeper := suite.app.GetSupplyKeeper() - hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) - supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) - - // Set up generic reward periods - var multiRewardPeriods types.MultiRewardPeriods - var rewardPeriods types.RewardPeriods - for i, denom := range tc.args.expectedSupplyIndexDenoms { - // Create just one reward period for USDX Minting / Hard Delegator reward periods (otherwise params will panic on duplicate) - if i == 0 { - rewardPeriod := types.NewRewardPeriod(true, denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[i]) - rewardPeriods = append(rewardPeriods, rewardPeriod) - } - multiRewardPeriod := types.NewMultiRewardPeriod(true, denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond) - multiRewardPeriods = append(multiRewardPeriods, multiRewardPeriod) - } - - // Setup incentive state - params := types.NewParams( - rewardPeriods, multiRewardPeriods, multiRewardPeriods, rewardPeriods, - types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - tc.args.initialTime.Add(time.Hour*24*365*5), - ) - suite.keeper.SetParams(suite.ctx, params) - - // Set each denom's previous accrual time and supply reward factor - var rewardIndexes types.RewardIndexes - for _, rewardCoin := range tc.args.rewardsPerSecond { - rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) - rewardIndexes = append(rewardIndexes, rewardIndex) - } - for _, denom := range tc.args.expectedSupplyIndexDenoms { - suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, denom, tc.args.initialTime) - suite.keeper.SetHardSupplyRewardIndexes(suite.ctx, denom, rewardIndexes) - } - - // User deposits (first time) - hardKeeper := suite.app.GetHardKeeper() - userAddr := suite.addrs[3] - err := hardKeeper.Deposit(suite.ctx, userAddr, tc.args.firstDeposit) - suite.Require().NoError(err) - - // Confirm that a claim was created and populated with the correct supply indexes - claimAfterFirstDeposit, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) - suite.Require().True(found) - for _, coin := range tc.args.firstDeposit { - _, hasIndex := claimAfterFirstDeposit.HasSupplyRewardIndex(coin.Denom) - suite.Require().True(hasIndex) - } - suite.Require().True(len(claimAfterFirstDeposit.SupplyRewardIndexes) == len(tc.args.firstDeposit)) - - // User modifies their Deposit by withdrawing or depositing more - if tc.args.modification.withdraw { - err = hardKeeper.Withdraw(suite.ctx, userAddr, tc.args.modification.coins) - } else { - err = hardKeeper.Deposit(suite.ctx, userAddr, tc.args.modification.coins) - } - suite.Require().NoError(err) - - // Confirm that the claim contains all expected supply indexes - claimAfterModification, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) - suite.Require().True(found) - for _, denom := range tc.args.expectedSupplyIndexDenoms { - _, hasIndex := claimAfterModification.HasSupplyRewardIndex(denom) - suite.Require().True(hasIndex) - } - suite.Require().True(len(claimAfterModification.SupplyRewardIndexes) == len(tc.args.expectedSupplyIndexDenoms)) - }) - } -} - -func (suite *KeeperTestSuite) TestInitializeHardBorrowRewards() { - - type args struct { - moneyMarketRewardDenoms map[string][]string - deposit sdk.Coins - borrow sdk.Coins - initialTime time.Time - expectedClaimBorrowRewardIndexes types.MultiRewardIndexes - } - type test struct { - name string - args args - } - - standardMoneyMarketRewardDenoms := map[string][]string{ - "bnb": {"hard"}, - "btcb": {"hard", "ukava"}, - "xrp": {}, - } - - testCases := []test{ - { - "single deposit denom, single reward denom", - args{ - moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, - deposit: cs(c("bnb", 1000000000000)), - borrow: cs(c("bnb", 100000000000)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedClaimBorrowRewardIndexes: types.MultiRewardIndexes{ - types.NewMultiRewardIndex( - "bnb", - types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.0")), - }, - ), - }, - }, - }, - { - "single deposit denom, multiple reward denoms", - args{ - moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, - deposit: cs(c("btcb", 1000000000000)), - borrow: cs(c("btcb", 100000000000)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedClaimBorrowRewardIndexes: types.MultiRewardIndexes{ - types.NewMultiRewardIndex( - "btcb", - types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.0")), - types.NewRewardIndex("ukava", d("0.0")), - }, - ), - }, - }, - }, - { - "single deposit denom, no reward denoms", - args{ - moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, - deposit: cs(c("xrp", 1000000000000)), - borrow: cs(c("xrp", 100000000000)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedClaimBorrowRewardIndexes: types.MultiRewardIndexes{ - types.NewMultiRewardIndex( - "xrp", - nil, - ), - }, - }, - }, - { - "multiple deposit denoms, multiple overlapping reward denoms", - args{ - moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, - deposit: cs(c("bnb", 1000000000000), c("btcb", 1000000000000)), - borrow: cs(c("bnb", 100000000000), c("btcb", 100000000000)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedClaimBorrowRewardIndexes: types.MultiRewardIndexes{ - types.NewMultiRewardIndex( - "bnb", - types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.0")), - }, - ), - types.NewMultiRewardIndex( - "btcb", - types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.0")), - types.NewRewardIndex("ukava", d("0.0")), - }, - ), - }, - }, - }, - { - "multiple deposit denoms, correct discrete reward denoms", - args{ - moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, - deposit: cs(c("bnb", 1000000000000), c("xrp", 1000000000000)), - borrow: cs(c("bnb", 100000000000), c("xrp", 100000000000)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedClaimBorrowRewardIndexes: types.MultiRewardIndexes{ - types.NewMultiRewardIndex( - "bnb", - types.RewardIndexes{ - types.NewRewardIndex("hard", d("0.0")), - }, - ), - types.NewMultiRewardIndex( - "xrp", - nil, - ), - }, - }, - }, - } - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.SetupWithGenState() - suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) - - // Mint coins to hard module account - supplyKeeper := suite.app.GetSupplyKeeper() - hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) - supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) - - userAddr := suite.addrs[3] - - // Prepare money market + reward params - i := 0 - var multiRewardPeriods types.MultiRewardPeriods - var rewardPeriods types.RewardPeriods - for moneyMarketDenom, rewardDenoms := range tc.args.moneyMarketRewardDenoms { - // Set up multi reward periods for supply/borrow indexes with dynamic money market denoms/reward denoms - var rewardsPerSecond sdk.Coins - for _, rewardDenom := range rewardDenoms { - rewardsPerSecond = append(rewardsPerSecond, sdk.NewCoin(rewardDenom, sdk.OneInt())) - } - multiRewardPeriod := types.NewMultiRewardPeriod(true, moneyMarketDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), rewardsPerSecond) - multiRewardPeriods = append(multiRewardPeriods, multiRewardPeriod) - - // Set up generic reward periods for usdx minting/delegator indexes - if i == 0 && len(rewardDenoms) > 0 { - rewardPeriod := types.NewRewardPeriod(true, moneyMarketDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), rewardsPerSecond[i]) - rewardPeriods = append(rewardPeriods, rewardPeriod) - i++ - } - } - - // Initialize and set incentive params - params := types.NewParams( - rewardPeriods, multiRewardPeriods, multiRewardPeriods, rewardPeriods, - types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - tc.args.initialTime.Add(time.Hour*24*365*5), - ) - suite.keeper.SetParams(suite.ctx, params) - - // Set each money market's previous accrual time and supply reward indexes - for moneyMarketDenom, rewardDenoms := range tc.args.moneyMarketRewardDenoms { - var rewardIndexes types.RewardIndexes - for _, rewardDenom := range rewardDenoms { - rewardIndex := types.NewRewardIndex(rewardDenom, sdk.ZeroDec()) - rewardIndexes = append(rewardIndexes, rewardIndex) - } - suite.keeper.SetPreviousHardBorrowRewardAccrualTime(suite.ctx, moneyMarketDenom, tc.args.initialTime) - if len(rewardIndexes) > 0 { - suite.keeper.SetHardBorrowRewardIndexes(suite.ctx, moneyMarketDenom, rewardIndexes) - } - } - - hardKeeper := suite.app.GetHardKeeper() - // User deposits - err := hardKeeper.Deposit(suite.ctx, userAddr, tc.args.deposit) - suite.Require().NoError(err) - // User borrows - err = hardKeeper.Borrow(suite.ctx, userAddr, tc.args.borrow) - suite.Require().NoError(err) - - claim, foundClaim := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) - suite.Require().True(foundClaim) - suite.Require().Equal(tc.args.expectedClaimBorrowRewardIndexes, claim.BorrowRewardIndexes) - }) - } -} - -func (suite *KeeperTestSuite) TestUpdateHardBorrowIndexDenoms() { - type withdrawModification struct { - coins sdk.Coins - repay bool - } - - type args struct { - initialDeposit sdk.Coins - firstBorrow sdk.Coins - modification withdrawModification - rewardsPerSecond sdk.Coins - initialTime time.Time - expectedBorrowIndexDenoms []string - } - type test struct { - name string - args args - } - - testCases := []test{ - { - "single reward denom: update adds one borrow reward index", - args{ - initialDeposit: cs(c("bnb", 10000000000)), - firstBorrow: cs(c("bnb", 50000000)), - modification: withdrawModification{coins: cs(c("ukava", 500000000))}, - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedBorrowIndexDenoms: []string{"bnb", "ukava"}, - }, - }, - { - "single reward denom: update adds multiple borrow supply reward indexes", - args{ - initialDeposit: cs(c("btcb", 10000000000)), - firstBorrow: cs(c("btcb", 50000000)), - modification: withdrawModification{coins: cs(c("ukava", 500000000), c("bnb", 50000000000), c("xrp", 50000000000))}, - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedBorrowIndexDenoms: []string{"btcb", "ukava", "bnb", "xrp"}, - }, - }, - { - "single reward denom: update doesn't add duplicate borrow reward index for same denom", - args{ - initialDeposit: cs(c("bnb", 100000000000)), - firstBorrow: cs(c("bnb", 50000000)), - modification: withdrawModification{coins: cs(c("bnb", 50000000000))}, - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedBorrowIndexDenoms: []string{"bnb"}, - }, - }, - { - "multiple reward denoms: update adds one borrow reward index", - args{ - initialDeposit: cs(c("bnb", 10000000000)), - firstBorrow: cs(c("bnb", 50000000)), - modification: withdrawModification{coins: cs(c("ukava", 500000000))}, - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedBorrowIndexDenoms: []string{"bnb", "ukava"}, - }, - }, - { - "multiple reward denoms: update adds multiple borrow supply reward indexes", - args{ - initialDeposit: cs(c("btcb", 10000000000)), - firstBorrow: cs(c("btcb", 50000000)), - modification: withdrawModification{coins: cs(c("ukava", 500000000), c("bnb", 50000000000), c("xrp", 50000000000))}, - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedBorrowIndexDenoms: []string{"btcb", "ukava", "bnb", "xrp"}, - }, - }, - { - "multiple reward denoms: update doesn't add duplicate borrow reward index for same denom", - args{ - initialDeposit: cs(c("bnb", 100000000000)), - firstBorrow: cs(c("bnb", 50000000)), - modification: withdrawModification{coins: cs(c("bnb", 50000000000))}, - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedBorrowIndexDenoms: []string{"bnb"}, - }, - }, - { - "single reward denom: fully repaying a denom deletes the denom's supply reward index", - args{ - initialDeposit: cs(c("bnb", 1000000000)), - firstBorrow: cs(c("bnb", 100000000)), - modification: withdrawModification{coins: cs(c("bnb", 1100000000)), repay: true}, - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedBorrowIndexDenoms: []string{}, - }, - }, - { - "single reward denom: fully repaying a denom deletes only the denom's supply reward index", - args{ - initialDeposit: cs(c("bnb", 1000000000)), - firstBorrow: cs(c("bnb", 100000000), c("ukava", 10000000)), - modification: withdrawModification{coins: cs(c("bnb", 1100000000)), repay: true}, - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedBorrowIndexDenoms: []string{"ukava"}, - }, - }, - { - "multiple reward denoms: fully repaying a denom deletes the denom's supply reward index", - args{ - initialDeposit: cs(c("bnb", 1000000000)), - firstBorrow: cs(c("bnb", 100000000), c("ukava", 10000000)), - modification: withdrawModification{coins: cs(c("bnb", 1100000000)), repay: true}, - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - expectedBorrowIndexDenoms: []string{"ukava"}, - }, - }, - } - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.SetupWithGenState() - suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) - - // Mint coins to hard module account so it can service borrow requests - supplyKeeper := suite.app.GetSupplyKeeper() - hardMaccCoins := tc.args.firstBorrow.Add(tc.args.modification.coins...) - supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) - - // Set up generic reward periods - var multiRewardPeriods types.MultiRewardPeriods - var rewardPeriods types.RewardPeriods - for i, denom := range tc.args.expectedBorrowIndexDenoms { - // Create just one reward period for USDX Minting / Hard Delegator reward periods (otherwise params will panic on duplicate) - if i == 0 { - rewardPeriod := types.NewRewardPeriod(true, denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[i]) - rewardPeriods = append(rewardPeriods, rewardPeriod) - } - multiRewardPeriod := types.NewMultiRewardPeriod(true, denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond) - multiRewardPeriods = append(multiRewardPeriods, multiRewardPeriod) - } - - // Setup incentive state - params := types.NewParams( - rewardPeriods, multiRewardPeriods, multiRewardPeriods, rewardPeriods, - types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - tc.args.initialTime.Add(time.Hour*24*365*5), - ) - suite.keeper.SetParams(suite.ctx, params) - - // Set each expected borrow denom's previous accrual time and borrow reward factor - var rewardIndexes types.RewardIndexes - for _, rewardCoin := range tc.args.rewardsPerSecond { - rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) - rewardIndexes = append(rewardIndexes, rewardIndex) - } - for _, denom := range tc.args.expectedBorrowIndexDenoms { - suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, denom, tc.args.initialTime) - suite.keeper.SetHardBorrowRewardIndexes(suite.ctx, denom, rewardIndexes) - } - - // User deposits initial funds (so that user can borrow) - hardKeeper := suite.app.GetHardKeeper() - userAddr := suite.addrs[3] - err := hardKeeper.Deposit(suite.ctx, userAddr, tc.args.initialDeposit) - suite.Require().NoError(err) - - // Confirm that claim exists but no borrow reward indexes have been added - claimAfterDeposit, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) - suite.Require().True(found) - suite.Require().Equal(0, len(claimAfterDeposit.BorrowRewardIndexes)) - - // User borrows (first time) - err = hardKeeper.Borrow(suite.ctx, userAddr, tc.args.firstBorrow) - suite.Require().NoError(err) - - // Confirm that claim's borrow reward indexes have been updated - claimAfterFirstBorrow, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) - suite.Require().True(found) - for _, coin := range tc.args.firstBorrow { - _, hasIndex := claimAfterFirstBorrow.HasBorrowRewardIndex(coin.Denom) - suite.Require().True(hasIndex) - } - suite.Require().True(len(claimAfterFirstBorrow.BorrowRewardIndexes) == len(tc.args.firstBorrow)) - - // User modifies their Borrow by either repaying or borrowing more - if tc.args.modification.repay { - err = hardKeeper.Repay(suite.ctx, userAddr, userAddr, tc.args.modification.coins) - } else { - err = hardKeeper.Borrow(suite.ctx, userAddr, tc.args.modification.coins) - } - suite.Require().NoError(err) - - // Confirm that claim's borrow reward indexes contain expected values - claimAfterModification, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) - suite.Require().True(found) - for _, coin := range tc.args.modification.coins { - _, hasIndex := claimAfterModification.HasBorrowRewardIndex(coin.Denom) - if tc.args.modification.repay { - // Only false if denom is repaid in full - if tc.args.modification.coins.AmountOf(coin.Denom).GTE(tc.args.firstBorrow.AmountOf(coin.Denom)) { - suite.Require().False(hasIndex) - } - } else { - suite.Require().True(hasIndex) - } - } - suite.Require().True(len(claimAfterModification.BorrowRewardIndexes) == len(tc.args.expectedBorrowIndexDenoms)) - }) - } -} - -func (suite *KeeperTestSuite) TestSynchronizeHardDelegatorReward() { - type args struct { - delegation sdk.Coin - rewardsPerSecond sdk.Coin - initialTime time.Time - blockTimes []int - expectedRewardFactor sdk.Dec - expectedRewards sdk.Coins - } - type test struct { - name string - args args - } - - testCases := []test{ - { - "10 blocks", - args{ - delegation: c("ukava", 1_000_000), - rewardsPerSecond: c("hard", 122354), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, - expectedRewardFactor: d("6.117700000000000000"), - expectedRewards: cs(c("hard", 6117700)), - }, - }, - { - "10 blocks - long block time", - args{ - delegation: c("ukava", 1_000_000), - rewardsPerSecond: c("hard", 122354), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, - expectedRewardFactor: d("52856.928000000000000000"), - expectedRewards: cs(c("hard", 52856928000)), - }, - }, - { - "delegator reward index updated when reward is zero", - args{ - delegation: c("ukava", 1), - rewardsPerSecond: c("hard", 1), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, - expectedRewardFactor: d("0.000099999900000100"), - expectedRewards: nil, - }, - }, - } - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.SetupWithGenState() - suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) - - // Mint coins to hard module account - supplyKeeper := suite.app.GetSupplyKeeper() - hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) - supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) - - // setup incentive state - params := types.NewParams( - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), cs(tc.args.rewardsPerSecond))}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), cs(tc.args.rewardsPerSecond))}, - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - tc.args.initialTime.Add(time.Hour*24*365*5), - ) - suite.keeper.SetParams(suite.ctx, params) - suite.keeper.SetPreviousHardDelegatorRewardAccrualTime(suite.ctx, tc.args.delegation.Denom, tc.args.initialTime) - suite.keeper.SetHardDelegatorRewardFactor(suite.ctx, tc.args.delegation.Denom, sdk.ZeroDec()) - - // Set up hard state (interest factor for the relevant denom) - suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.delegation.Denom, tc.args.initialTime) - - // Create validator account - staking.BeginBlocker(suite.ctx, suite.stakingKeeper) - selfDelegationCoins := c("ukava", 1_000_000) - err := suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[0], selfDelegationCoins) - suite.Require().NoError(err) - staking.EndBlocker(suite.ctx, suite.stakingKeeper) - - // Delegator delegates - err = suite.deliverMsgDelegate(suite.ctx, suite.addrs[0], suite.validatorAddrs[0], tc.args.delegation) - suite.Require().NoError(err) - - // Check that validator account has been created and delegation was successful - valAcc, found := suite.stakingKeeper.GetValidator(suite.ctx, suite.validatorAddrs[0]) - suite.True(found) - suite.Require().Equal(valAcc.Status, sdk.Bonded) - suite.Require().Equal(valAcc.Tokens, tc.args.delegation.Amount.Add(selfDelegationCoins.Amount)) - - // Check that Staking hooks initialized a HardLiquidityProviderClaim - claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0]) - suite.Require().True(found) - suite.Require().Equal(sdk.ZeroDec(), claim.DelegatorRewardIndexes[0].RewardFactor) - - // Run accumulator at several intervals - var timeElapsed int - previousBlockTime := suite.ctx.BlockTime() - for _, t := range tc.args.blockTimes { - timeElapsed += t - updatedBlockTime := previousBlockTime.Add(time.Duration(int(time.Second) * t)) - previousBlockTime = updatedBlockTime - blockCtx := suite.ctx.WithBlockTime(updatedBlockTime) - - // Run Hard begin blocker for each block ctx to update denom's interest factor - hard.BeginBlocker(blockCtx, suite.hardKeeper) - - rewardPeriod, found := suite.keeper.GetHardDelegatorRewardPeriod(blockCtx, tc.args.delegation.Denom) - suite.Require().True(found) - - err := suite.keeper.AccumulateHardDelegatorRewards(blockCtx, rewardPeriod) - suite.Require().NoError(err) - } - updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed)) - suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) - - // After we've accumulated, run synchronize - suite.Require().NotPanics(func() { - suite.keeper.SynchronizeHardDelegatorRewards(suite.ctx, suite.addrs[0], nil, false) - }) - - // Check that reward factor and claim have been updated as expected - rewardFactor, found := suite.keeper.GetHardDelegatorRewardFactor(suite.ctx, tc.args.delegation.Denom) - suite.Require().Equal(tc.args.expectedRewardFactor, rewardFactor) - - claim, found = suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0]) - suite.Require().True(found) - suite.Require().Equal(tc.args.expectedRewardFactor, claim.DelegatorRewardIndexes[0].RewardFactor) - suite.Require().Equal(tc.args.expectedRewards, claim.Reward) - }) - } -} - -func (suite *KeeperTestSuite) TestSimulateHardSupplyRewardSynchronization() { - type args struct { - deposit sdk.Coin - rewardsPerSecond sdk.Coins - initialTime time.Time - blockTimes []int - expectedRewardIndexes types.RewardIndexes - expectedRewards sdk.Coins - } - type test struct { - name string - args args - } - - testCases := []test{ - { - "10 blocks", - args{ - deposit: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.001223540000000000"))}, - expectedRewards: cs(c("hard", 12235400)), - }, - }, - { - "10 blocks - long block time", - args{ - deposit: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("10.571385600000000000"))}, - expectedRewards: cs(c("hard", 105713856000)), - }, - }, - } - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.SetupWithGenState() - suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) - - // Mint coins to hard module account - supplyKeeper := suite.app.GetSupplyKeeper() - hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) - supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) - - // Set up incentive state - params := types.NewParams( - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, - types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - tc.args.initialTime.Add(time.Hour*24*365*5), - ) - suite.keeper.SetParams(suite.ctx, params) - suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, tc.args.deposit.Denom, tc.args.initialTime) - var rewardIndexes types.RewardIndexes - for _, rewardCoin := range tc.args.rewardsPerSecond { - rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) - rewardIndexes = append(rewardIndexes, rewardIndex) - } - suite.keeper.SetHardSupplyRewardIndexes(suite.ctx, tc.args.deposit.Denom, rewardIndexes) - - // Set up hard state (interest factor for the relevant denom) - suite.hardKeeper.SetSupplyInterestFactor(suite.ctx, tc.args.deposit.Denom, sdk.MustNewDecFromStr("1.0")) - suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.deposit.Denom, tc.args.initialTime) - - // User deposits and borrows to increase total borrowed amount - hardKeeper := suite.app.GetHardKeeper() - userAddr := suite.addrs[3] - err := hardKeeper.Deposit(suite.ctx, userAddr, sdk.NewCoins(tc.args.deposit)) - suite.Require().NoError(err) - - // Check that Hard hooks initialized a HardLiquidityProviderClaim - claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) - suite.Require().True(found) - multiRewardIndex, _ := claim.SupplyRewardIndexes.GetRewardIndex(tc.args.deposit.Denom) - for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { - currRewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) - suite.Require().True(found) - suite.Require().Equal(sdk.ZeroDec(), currRewardIndex.RewardFactor) - } - - // Run accumulator at several intervals - var timeElapsed int - previousBlockTime := suite.ctx.BlockTime() - for _, t := range tc.args.blockTimes { - timeElapsed += t - updatedBlockTime := previousBlockTime.Add(time.Duration(int(time.Second) * t)) - previousBlockTime = updatedBlockTime - blockCtx := suite.ctx.WithBlockTime(updatedBlockTime) - - // Run Hard begin blocker for each block ctx to update denom's interest factor - hard.BeginBlocker(blockCtx, suite.hardKeeper) - - // Accumulate hard supply-side rewards - multiRewardPeriod, found := suite.keeper.GetHardSupplyRewardPeriods(blockCtx, tc.args.deposit.Denom) - suite.Require().True(found) - err := suite.keeper.AccumulateHardSupplyRewards(blockCtx, multiRewardPeriod) - suite.Require().NoError(err) - } - updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed)) - suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) - - // Confirm that the user's claim hasn't been synced - claimPre, foundPre := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) - suite.Require().True(foundPre) - multiRewardIndexPre, _ := claimPre.SupplyRewardIndexes.GetRewardIndex(tc.args.deposit.Denom) - for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { - currRewardIndex, found := multiRewardIndexPre.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) - suite.Require().True(found) - suite.Require().Equal(sdk.ZeroDec(), currRewardIndex.RewardFactor) - } - - // Check that the synced claim held in memory has properly simulated syncing - syncedClaim := suite.keeper.SimulateHardSynchronization(suite.ctx, claimPre) - for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { - // Check that the user's claim's reward index matches the expected reward index - multiRewardIndex, found := syncedClaim.SupplyRewardIndexes.GetRewardIndex(tc.args.deposit.Denom) - suite.Require().True(found) - rewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) - suite.Require().True(found) - suite.Require().Equal(expectedRewardIndex, rewardIndex) - - // Check that the user's claim holds the expected amount of reward coins - suite.Require().Equal( - tc.args.expectedRewards.AmountOf(expectedRewardIndex.CollateralType), - syncedClaim.Reward.AmountOf(expectedRewardIndex.CollateralType), - ) - } - }) - } -} - -func (suite *KeeperTestSuite) TestSimulateHardBorrowRewardSynchronization() { - type args struct { - borrow sdk.Coin - rewardsPerSecond sdk.Coins - initialTime time.Time - blockTimes []int - expectedRewardIndexes types.RewardIndexes - expectedRewards sdk.Coins - } - type test struct { - name string - args args - } - - testCases := []test{ - { - "10 blocks", - args{ - borrow: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.001223540000173228"))}, - expectedRewards: cs(c("hard", 12235400)), - }, - }, - { - "10 blocks - long block time", - args{ - borrow: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("10.571385603126235340"))}, - expectedRewards: cs(c("hard", 105713856031)), - }, - }, - } - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.SetupWithGenState() - suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) - - // Mint coins to hard module account - supplyKeeper := suite.app.GetSupplyKeeper() - hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) - supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) - - // setup incentive state - params := types.NewParams( - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, - types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - tc.args.initialTime.Add(time.Hour*24*365*5), - ) - suite.keeper.SetParams(suite.ctx, params) - suite.keeper.SetPreviousHardBorrowRewardAccrualTime(suite.ctx, tc.args.borrow.Denom, tc.args.initialTime) - var rewardIndexes types.RewardIndexes - for _, rewardCoin := range tc.args.rewardsPerSecond { - rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) - rewardIndexes = append(rewardIndexes, rewardIndex) - } - suite.keeper.SetHardBorrowRewardIndexes(suite.ctx, tc.args.borrow.Denom, rewardIndexes) - - // Set up hard state (interest factor for the relevant denom) - suite.hardKeeper.SetSupplyInterestFactor(suite.ctx, tc.args.borrow.Denom, sdk.MustNewDecFromStr("1.0")) - suite.hardKeeper.SetBorrowInterestFactor(suite.ctx, tc.args.borrow.Denom, sdk.MustNewDecFromStr("1.0")) - suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.borrow.Denom, tc.args.initialTime) - - // User deposits and borrows to increase total borrowed amount - hardKeeper := suite.app.GetHardKeeper() - userAddr := suite.addrs[3] - err := hardKeeper.Deposit(suite.ctx, userAddr, sdk.NewCoins(sdk.NewCoin(tc.args.borrow.Denom, tc.args.borrow.Amount.Mul(sdk.NewInt(2))))) - suite.Require().NoError(err) - err = hardKeeper.Borrow(suite.ctx, userAddr, sdk.NewCoins(tc.args.borrow)) - suite.Require().NoError(err) - - // Check that Hard hooks initialized a HardLiquidityProviderClaim - claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) - suite.Require().True(found) - multiRewardIndex, _ := claim.BorrowRewardIndexes.GetRewardIndex(tc.args.borrow.Denom) - for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { - currRewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) - suite.Require().True(found) - suite.Require().Equal(sdk.ZeroDec(), currRewardIndex.RewardFactor) - } - - // Run accumulator at several intervals - var timeElapsed int - previousBlockTime := suite.ctx.BlockTime() - for _, t := range tc.args.blockTimes { - timeElapsed += t - updatedBlockTime := previousBlockTime.Add(time.Duration(int(time.Second) * t)) - previousBlockTime = updatedBlockTime - blockCtx := suite.ctx.WithBlockTime(updatedBlockTime) - - // Run Hard begin blocker for each block ctx to update denom's interest factor - hard.BeginBlocker(blockCtx, suite.hardKeeper) - - // Accumulate hard borrow-side rewards - multiRewardPeriod, found := suite.keeper.GetHardBorrowRewardPeriods(blockCtx, tc.args.borrow.Denom) - suite.Require().True(found) - err := suite.keeper.AccumulateHardBorrowRewards(blockCtx, multiRewardPeriod) - suite.Require().NoError(err) - } - updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed)) - suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) - - // Confirm that the user's claim hasn't been synced - claimPre, foundPre := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) - suite.Require().True(foundPre) - multiRewardIndexPre, _ := claimPre.BorrowRewardIndexes.GetRewardIndex(tc.args.borrow.Denom) - for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { - currRewardIndex, found := multiRewardIndexPre.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) - suite.Require().True(found) - suite.Require().Equal(sdk.ZeroDec(), currRewardIndex.RewardFactor) - } - - // Check that the synced claim held in memory has properly simulated syncing - syncedClaim := suite.keeper.SimulateHardSynchronization(suite.ctx, claim) - for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { - // Check that the user's claim's reward index matches the expected reward index - multiRewardIndex, found := syncedClaim.BorrowRewardIndexes.GetRewardIndex(tc.args.borrow.Denom) - suite.Require().True(found) - rewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) - suite.Require().True(found) - suite.Require().Equal(expectedRewardIndex, rewardIndex) - - // Check that the user's claim holds the expected amount of reward coins - suite.Require().Equal( - tc.args.expectedRewards.AmountOf(expectedRewardIndex.CollateralType), - syncedClaim.Reward.AmountOf(expectedRewardIndex.CollateralType), - ) - } - }) - } -} - -func (suite *KeeperTestSuite) TestSimulateHardDelegatorRewardSynchronization() { - type args struct { - delegation sdk.Coin - rewardsPerSecond sdk.Coins - initialTime time.Time - blockTimes []int - expectedRewardIndexes types.RewardIndexes - expectedRewards sdk.Coins - } - type test struct { - name string - args args - } - - testCases := []test{ - { - "10 blocks", - args{ - delegation: c("ukava", 1_000_000), - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("ukava", d("6.117700000000000000"))}, // Here the reward index stores data differently than inside a MultiRewardIndex - expectedRewards: cs(c("hard", 6117700)), - }, - }, - { - "10 blocks - long block time", - args{ - delegation: c("ukava", 1_000_000), - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("ukava", d("52856.928000000000000000"))}, - expectedRewards: cs(c("hard", 52856928000)), - }, - }, - } - - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.SetupWithGenState() - suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) - - // Mint coins to hard module account - supplyKeeper := suite.app.GetSupplyKeeper() - hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) - supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) - - // setup incentive state - params := types.NewParams( - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, - types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - tc.args.initialTime.Add(time.Hour*24*365*5), - ) - suite.keeper.SetParams(suite.ctx, params) - suite.keeper.SetPreviousHardDelegatorRewardAccrualTime(suite.ctx, tc.args.delegation.Denom, tc.args.initialTime) - suite.keeper.SetHardDelegatorRewardFactor(suite.ctx, tc.args.delegation.Denom, sdk.ZeroDec()) - - // Set up hard state (interest factor for the relevant denom) - suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.delegation.Denom, tc.args.initialTime) - - // Delegator delegates - err := suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[0], tc.args.delegation) - suite.Require().NoError(err) - err = suite.deliverMsgDelegate(suite.ctx, suite.addrs[0], suite.validatorAddrs[0], tc.args.delegation) - suite.Require().NoError(err) - - staking.EndBlocker(suite.ctx, suite.stakingKeeper) - - // Check that Staking hooks initialized a HardLiquidityProviderClaim - claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0]) - suite.Require().True(found) - suite.Require().Equal(sdk.ZeroDec(), claim.DelegatorRewardIndexes[0].RewardFactor) - - // Run accumulator at several intervals - var timeElapsed int - previousBlockTime := suite.ctx.BlockTime() - for _, t := range tc.args.blockTimes { - timeElapsed += t - updatedBlockTime := previousBlockTime.Add(time.Duration(int(time.Second) * t)) - previousBlockTime = updatedBlockTime - blockCtx := suite.ctx.WithBlockTime(updatedBlockTime) - - // Run Hard begin blocker for each block ctx to update denom's interest factor - hard.BeginBlocker(blockCtx, suite.hardKeeper) - - // Accumulate hard delegator rewards - rewardPeriod, found := suite.keeper.GetHardDelegatorRewardPeriod(blockCtx, tc.args.delegation.Denom) - suite.Require().True(found) - err := suite.keeper.AccumulateHardDelegatorRewards(blockCtx, rewardPeriod) - suite.Require().NoError(err) - } - updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed)) - suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) - - // Check that the synced claim held in memory has properly simulated syncing - syncedClaim := suite.keeper.SimulateHardSynchronization(suite.ctx, claim) - for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { - // Check that the user's claim's reward index matches the expected reward index - rewardIndex, found := syncedClaim.DelegatorRewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) - suite.Require().True(found) - suite.Require().Equal(expectedRewardIndex, rewardIndex) - - // Check that the user's claim holds the expected amount of reward coins - suite.Require().Equal( - tc.args.expectedRewards.AmountOf(expectedRewardIndex.CollateralType), - syncedClaim.Reward.AmountOf(expectedRewardIndex.CollateralType), - ) - } - }) - } -} - -func (suite *KeeperTestSuite) TestSimulateUSDXMintingRewardSynchronization() { - type args struct { - ctype string - rewardsPerSecond sdk.Coins - initialTime time.Time - initialCollateral sdk.Coin - initialPrincipal sdk.Coin - blockTimes []int - expectedRewardFactor sdk.Dec - expectedRewards sdk.Coin - } - type test struct { - name string - args args - } - - testCases := []test{ - { - "10 blocks", - args{ - ctype: "bnb-a", - rewardsPerSecond: cs(c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - initialCollateral: c("bnb", 1000000000000), - initialPrincipal: c("usdx", 10000000000), - blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, - expectedRewardFactor: d("0.001223540000000000"), - expectedRewards: c("ukava", 12235400), - }, - }, - { - "10 blocks - long block time", - args{ - ctype: "bnb-a", - rewardsPerSecond: cs(c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - initialCollateral: c("bnb", 1000000000000), - initialPrincipal: c("usdx", 10000000000), - blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, - expectedRewardFactor: d("10.57138560000000000"), - expectedRewards: c("ukava", 105713856000), - }, - }, - } - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.SetupWithGenState() - suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) - - // setup incentive state - params := types.NewParams( - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, - types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - tc.args.initialTime.Add(time.Hour*24*365*5), - ) - suite.keeper.SetParams(suite.ctx, params) - suite.keeper.SetParams(suite.ctx, params) - suite.keeper.SetPreviousUSDXMintingAccrualTime(suite.ctx, tc.args.ctype, tc.args.initialTime) - suite.keeper.SetUSDXMintingRewardFactor(suite.ctx, tc.args.ctype, sdk.ZeroDec()) - - // setup account state - sk := suite.app.GetSupplyKeeper() - sk.MintCoins(suite.ctx, cdptypes.ModuleName, sdk.NewCoins(tc.args.initialCollateral)) - sk.SendCoinsFromModuleToAccount(suite.ctx, cdptypes.ModuleName, suite.addrs[0], sdk.NewCoins(tc.args.initialCollateral)) - - // setup cdp state - cdpKeeper := suite.app.GetCDPKeeper() - err := cdpKeeper.AddCdp(suite.ctx, suite.addrs[0], tc.args.initialCollateral, tc.args.initialPrincipal, tc.args.ctype) - suite.Require().NoError(err) - - claim, found := suite.keeper.GetUSDXMintingClaim(suite.ctx, suite.addrs[0]) - suite.Require().True(found) - suite.Require().Equal(sdk.ZeroDec(), claim.RewardIndexes[0].RewardFactor) - - var timeElapsed int - previousBlockTime := suite.ctx.BlockTime() - for _, t := range tc.args.blockTimes { - timeElapsed += t - updatedBlockTime := previousBlockTime.Add(time.Duration(int(time.Second) * t)) - previousBlockTime = updatedBlockTime - blockCtx := suite.ctx.WithBlockTime(updatedBlockTime) - rewardPeriod, found := suite.keeper.GetUSDXMintingRewardPeriod(blockCtx, tc.args.ctype) - suite.Require().True(found) - err := suite.keeper.AccumulateUSDXMintingRewards(blockCtx, rewardPeriod) - suite.Require().NoError(err) - } - updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed)) - suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) - - claim, found = suite.keeper.GetUSDXMintingClaim(suite.ctx, suite.addrs[0]) - suite.Require().True(found) - suite.Require().Equal(claim.RewardIndexes[0].RewardFactor, sdk.ZeroDec()) - suite.Require().Equal(claim.Reward, sdk.NewCoin("ukava", sdk.ZeroInt())) - - updatedClaim := suite.keeper.SimulateUSDXMintingSynchronization(suite.ctx, claim) - suite.Require().Equal(tc.args.expectedRewardFactor, updatedClaim.RewardIndexes[0].RewardFactor) - suite.Require().Equal(tc.args.expectedRewards, updatedClaim.Reward) - }) - } -} - -func (suite *KeeperTestSuite) deliverMsgCreateValidator(ctx sdk.Context, address sdk.ValAddress, selfDelegation sdk.Coin) error { - msg := staking.NewMsgCreateValidator( - address, - ed25519.GenPrivKey().PubKey(), - selfDelegation, - staking.Description{}, - staking.NewCommissionRates(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()), - sdk.NewInt(1_000_000), - ) - handleStakingMsg := staking.NewHandler(suite.stakingKeeper) - _, err := handleStakingMsg(ctx, msg) - return err -} - -func (suite *KeeperTestSuite) deliverMsgDelegate(ctx sdk.Context, delegator sdk.AccAddress, validator sdk.ValAddress, amount sdk.Coin) error { - msg := staking.NewMsgDelegate( - delegator, - validator, - amount, - ) - handleStakingMsg := staking.NewHandler(suite.stakingKeeper) - _, err := handleStakingMsg(ctx, msg) - return err -} - -func (suite *KeeperTestSuite) deliverMsgRedelegate(ctx sdk.Context, delegator sdk.AccAddress, sourceValidator, destinationValidator sdk.ValAddress, amount sdk.Coin) error { - msg := staking.NewMsgBeginRedelegate( - delegator, - sourceValidator, - destinationValidator, - amount, - ) - handleStakingMsg := staking.NewHandler(suite.stakingKeeper) - _, err := handleStakingMsg(ctx, msg) - return err -} - -// given a user has a delegation to a bonded validator, when the validator starts unbonding, the user does not accumulate rewards -func (suite *KeeperTestSuite) TestUnbondingValidatorSyncsClaim() { - suite.SetupWithGenState() - initialTime := time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC) - suite.ctx = suite.ctx.WithBlockTime(initialTime) - blockDuration := 10 * time.Second - - // Setup incentive state - rewardsPerSecond := c("hard", 122354) - bondDenom := "ukava" - params := types.NewParams( - nil, - nil, - nil, - types.RewardPeriods{ - types.NewRewardPeriod(true, bondDenom, initialTime.Add(-1*oneYear), initialTime.Add(4*oneYear), rewardsPerSecond), - }, - types.DefaultMultipliers, - initialTime.Add(5*oneYear), - ) - suite.keeper.SetParams(suite.ctx, params) - suite.keeper.SetPreviousHardDelegatorRewardAccrualTime(suite.ctx, bondDenom, initialTime) - suite.keeper.SetHardDelegatorRewardFactor(suite.ctx, bondDenom, sdk.ZeroDec()) - - // Reduce the size of the validator set - stakingParams := suite.app.GetStakingKeeper().GetParams(suite.ctx) - stakingParams.MaxValidators = 2 - suite.app.GetStakingKeeper().SetParams(suite.ctx, stakingParams) - - // Create 3 validators - err := suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[0], c(bondDenom, 10_000_000)) - suite.Require().NoError(err) - err = suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[1], c(bondDenom, 5_000_000)) - suite.Require().NoError(err) - err = suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[2], c(bondDenom, 1_000_000)) - suite.Require().NoError(err) - - // End the block so top validators become bonded - _ = suite.app.EndBlocker(suite.ctx, abci.RequestEndBlock{}) - - suite.ctx = suite.ctx.WithBlockTime(initialTime.Add(1 * blockDuration)) - _ = suite.app.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}) // height and time in header are ignored by module begin blockers - - // Delegate to a bonded validator from the test user. This will initialize their incentive claim. - err = suite.deliverMsgDelegate(suite.ctx, suite.addrs[0], suite.validatorAddrs[1], c(bondDenom, 1_000_000)) - suite.Require().NoError(err) - - // Start a new block to accumulate some delegation rewards for the user. - _ = suite.app.EndBlocker(suite.ctx, abci.RequestEndBlock{}) - suite.ctx = suite.ctx.WithBlockTime(initialTime.Add(2 * blockDuration)) - _ = suite.app.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}) // height and time in header are ignored by module begin blockers - - // Delegate to the unbonded validator to push it into the bonded validator set, pushing out the user's delegated validator - err = suite.deliverMsgDelegate(suite.ctx, suite.addrs[2], suite.validatorAddrs[2], c(bondDenom, 8_000_000)) - suite.Require().NoError(err) - - // End the block to start unbonding the user's validator - _ = suite.app.EndBlocker(suite.ctx, abci.RequestEndBlock{}) - // but don't start the next block as it will accumulate delegator rewards and we won't be able to tell if the user's reward was synced. - - // Check that the user's claim has been synced. ie rewards added, index updated - claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0]) - suite.Require().True(found) - - globalIndex, found := suite.keeper.GetHardDelegatorRewardFactor(suite.ctx, bondDenom) - suite.Require().True(found) - claimIndex, found := claim.DelegatorRewardIndexes.GetRewardIndex(bondDenom) - suite.Require().True(found) - suite.Require().Equal(globalIndex, claimIndex.RewardFactor) - - suite.Require().Equal( - cs(c(rewardsPerSecond.Denom, 76471)), - claim.Reward, - ) - - // Run another block and check the claim is not accumulating more rewards - suite.ctx = suite.ctx.WithBlockTime(initialTime.Add(3 * blockDuration)) - _ = suite.app.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}) - - suite.keeper.SynchronizeHardDelegatorRewards(suite.ctx, suite.addrs[0], nil, false) - - // rewards are the same as before - laterClaim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0]) - suite.Require().True(found) - suite.Require().Equal(claim.Reward, laterClaim.Reward) - - // claim index has been updated to latest global value - laterClaimIndex, found := laterClaim.DelegatorRewardIndexes.GetRewardIndex(bondDenom) - suite.Require().True(found) - globalIndex, found = suite.keeper.GetHardDelegatorRewardFactor(suite.ctx, bondDenom) - suite.Require().True(found) - suite.Require().Equal(globalIndex, laterClaimIndex.RewardFactor) -} - -// given a user has a delegation to an unbonded validator, when the validator becomes bonded, the user starts accumulating rewards -func (suite *KeeperTestSuite) TestBondingValidatorSyncsClaim() { - suite.SetupWithGenState() - initialTime := time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC) - suite.ctx = suite.ctx.WithBlockTime(initialTime) - blockDuration := 10 * time.Second - - // Setup incentive state - rewardsPerSecond := c("hard", 122354) - bondDenom := "ukava" - params := types.NewParams( - nil, - nil, - nil, - types.RewardPeriods{ - types.NewRewardPeriod(true, bondDenom, initialTime.Add(-1*oneYear), initialTime.Add(4*oneYear), rewardsPerSecond), - }, - types.DefaultMultipliers, - initialTime.Add(5*oneYear), - ) - suite.keeper.SetParams(suite.ctx, params) - suite.keeper.SetPreviousHardDelegatorRewardAccrualTime(suite.ctx, bondDenom, initialTime) - suite.keeper.SetHardDelegatorRewardFactor(suite.ctx, bondDenom, sdk.ZeroDec()) - - // Reduce the size of the validator set - stakingParams := suite.app.GetStakingKeeper().GetParams(suite.ctx) - stakingParams.MaxValidators = 2 - suite.app.GetStakingKeeper().SetParams(suite.ctx, stakingParams) - - // Create 3 validators - err := suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[0], c(bondDenom, 10_000_000)) - suite.Require().NoError(err) - err = suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[1], c(bondDenom, 5_000_000)) - suite.Require().NoError(err) - err = suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[2], c(bondDenom, 1_000_000)) - suite.Require().NoError(err) - - // End the block so top validators become bonded - _ = suite.app.EndBlocker(suite.ctx, abci.RequestEndBlock{}) - - suite.ctx = suite.ctx.WithBlockTime(initialTime.Add(1 * blockDuration)) - _ = suite.app.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}) // height and time in header are ignored by module begin blockers - - // Delegate to an unbonded validator from the test user. This will initialize their incentive claim. - err = suite.deliverMsgDelegate(suite.ctx, suite.addrs[0], suite.validatorAddrs[2], c(bondDenom, 1_000_000)) - suite.Require().NoError(err) - - // Start a new block to accumulate some delegation rewards globally. - _ = suite.app.EndBlocker(suite.ctx, abci.RequestEndBlock{}) - suite.ctx = suite.ctx.WithBlockTime(initialTime.Add(2 * blockDuration)) - _ = suite.app.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}) - - // Delegate to the user's unbonded validator to push it into the bonded validator set - err = suite.deliverMsgDelegate(suite.ctx, suite.addrs[2], suite.validatorAddrs[2], c(bondDenom, 4_000_000)) - suite.Require().NoError(err) - - // End the block to bond the user's validator - _ = suite.app.EndBlocker(suite.ctx, abci.RequestEndBlock{}) - // but don't start the next block as it will accumulate delegator rewards and we won't be able to tell if the user's reward was synced. - - // Check that the user's claim has been synced. ie rewards added, index updated - claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0]) - suite.Require().True(found) - - globalIndex, found := suite.keeper.GetHardDelegatorRewardFactor(suite.ctx, bondDenom) - suite.Require().True(found) - claimIndex, found := claim.DelegatorRewardIndexes.GetRewardIndex(bondDenom) - suite.Require().True(found) - suite.Require().Equal(globalIndex, claimIndex.RewardFactor) - - suite.Require().Equal( - sdk.Coins(nil), - claim.Reward, - ) - - // Run another block and check the claim is accumulating more rewards - suite.ctx = suite.ctx.WithBlockTime(initialTime.Add(3 * blockDuration)) - _ = suite.app.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}) - - suite.keeper.SynchronizeHardDelegatorRewards(suite.ctx, suite.addrs[0], nil, false) - - // rewards are greater than before - laterClaim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0]) - suite.Require().True(found) - suite.Require().True(laterClaim.Reward.IsAllGT(claim.Reward)) - - // claim index has been updated to latest global value - laterClaimIndex, found := laterClaim.DelegatorRewardIndexes.GetRewardIndex(bondDenom) - suite.Require().True(found) - globalIndex, found = suite.keeper.GetHardDelegatorRewardFactor(suite.ctx, bondDenom) - suite.Require().True(found) - suite.Require().Equal(globalIndex, laterClaimIndex.RewardFactor) -} - -// If a validator is slashed delegators should have their claims synced -func (suite *KeeperTestSuite) TestSlashingValidatorSyncsClaim() { - suite.SetupWithGenState() - initialTime := time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC) - suite.ctx = suite.ctx.WithBlockTime(initialTime) - blockDuration := 10 * time.Second - - // Setup incentive state - rewardsPerSecond := c("hard", 122354) - bondDenom := "ukava" - params := types.NewParams( - nil, - nil, - nil, - types.RewardPeriods{ - types.NewRewardPeriod(true, bondDenom, initialTime.Add(-1*oneYear), initialTime.Add(4*oneYear), rewardsPerSecond), - }, - types.DefaultMultipliers, - initialTime.Add(5*oneYear), - ) - suite.keeper.SetParams(suite.ctx, params) - suite.keeper.SetPreviousHardDelegatorRewardAccrualTime(suite.ctx, bondDenom, initialTime.Add(-1*blockDuration)) - suite.keeper.SetHardDelegatorRewardFactor(suite.ctx, bondDenom, sdk.ZeroDec()) - - // Reduce the size of the validator set - stakingParams := suite.app.GetStakingKeeper().GetParams(suite.ctx) - stakingParams.MaxValidators = 2 - suite.app.GetStakingKeeper().SetParams(suite.ctx, stakingParams) - - // Create 2 validators - err := suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[0], c(bondDenom, 10_000_000)) - suite.Require().NoError(err) - err = suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[1], c(bondDenom, 10_000_000)) - suite.Require().NoError(err) - - // End the block so validators become bonded - _ = suite.app.EndBlocker(suite.ctx, abci.RequestEndBlock{}) - - suite.ctx = suite.ctx.WithBlockTime(initialTime.Add(1 * blockDuration)) - _ = suite.app.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}) // height and time in header are ignored by module begin blockers - - // Delegate to a bonded validator from the test user. This will initialize their incentive claim. - err = suite.deliverMsgDelegate(suite.ctx, suite.addrs[0], suite.validatorAddrs[1], c(bondDenom, 1_000_000)) - suite.Require().NoError(err) - - // Check that claim has been created with synced reward index but no reward coins - initialClaim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0]) - suite.True(found) - initialGlobalIndex, found := suite.keeper.GetHardDelegatorRewardFactor(suite.ctx, bondDenom) - suite.True(found) - initialClaimIndex, found := initialClaim.DelegatorRewardIndexes.GetRewardIndex(bondDenom) - suite.True(found) - suite.Require().Equal(initialGlobalIndex, initialClaimIndex.RewardFactor) - suite.True(initialClaim.Reward.Empty()) // Initial claim should not have any rewards - - // Start a new block to accumulate some delegation rewards for the user. - _ = suite.app.EndBlocker(suite.ctx, abci.RequestEndBlock{}) - suite.ctx = suite.ctx.WithBlockTime(initialTime.Add(2 * blockDuration)) - _ = suite.app.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}) // height and time in header are ignored by module begin blockers - - // Fetch validator and slash them - stakingKeeper := suite.app.GetStakingKeeper() - validator, found := stakingKeeper.GetValidator(suite.ctx, suite.validatorAddrs[1]) - suite.Require().True(found) - suite.Require().True(validator.GetTokens().IsPositive()) - fraction := sdk.NewDecWithPrec(5, 1) - stakingKeeper.Slash(suite.ctx, validator.ConsAddress(), suite.ctx.BlockHeight(), 10, fraction) - - // Check that the user's claim has been synced. ie rewards added, index updated - claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0]) - suite.Require().True(found) - globalIndex, found := suite.keeper.GetHardDelegatorRewardFactor(suite.ctx, bondDenom) - suite.Require().True(found) - claimIndex, found := claim.DelegatorRewardIndexes.GetRewardIndex(bondDenom) - suite.Require().True(found) - suite.Require().Equal(globalIndex, claimIndex.RewardFactor) - - // Check that rewards were added - suite.Require().Equal( - cs(c(rewardsPerSecond.Denom, 58264)), - claim.Reward, - ) - - // Check that reward factor increased from initial value - suite.True(claimIndex.RewardFactor.GT(initialClaimIndex.RewardFactor)) -} - -// Given a delegation to a bonded validator, when a user redelegates everything to another (bonded) validator, the user's claim is synced -func (suite *KeeperTestSuite) TestRedelegationSyncsClaim() { - suite.SetupWithGenState() - initialTime := time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC) - suite.ctx = suite.ctx.WithBlockTime(initialTime) - blockDuration := 10 * time.Second - - // Setup incentive state - rewardsPerSecond := c("hard", 122354) - bondDenom := "ukava" - params := types.NewParams( - nil, - nil, - nil, - types.RewardPeriods{ - types.NewRewardPeriod(true, bondDenom, initialTime.Add(-1*oneYear), initialTime.Add(4*oneYear), rewardsPerSecond), - }, - types.DefaultMultipliers, - initialTime.Add(5*oneYear), - ) - suite.keeper.SetParams(suite.ctx, params) - suite.keeper.SetPreviousHardDelegatorRewardAccrualTime(suite.ctx, bondDenom, initialTime) - suite.keeper.SetHardDelegatorRewardFactor(suite.ctx, bondDenom, sdk.ZeroDec()) - - // Create 2 validators - err := suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[0], c(bondDenom, 10_000_000)) - suite.Require().NoError(err) - err = suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[1], c(bondDenom, 5_000_000)) - suite.Require().NoError(err) - - // Delegatefrom the test user. This will initialize their incentive claim. - err = suite.deliverMsgDelegate(suite.ctx, suite.addrs[0], suite.validatorAddrs[0], c(bondDenom, 1_000_000)) - suite.Require().NoError(err) - - // Start a new block to accumulate some delegation rewards globally. - _ = suite.app.EndBlocker(suite.ctx, abci.RequestEndBlock{}) - suite.ctx = suite.ctx.WithBlockTime(initialTime.Add(1 * blockDuration)) - _ = suite.app.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}) // height and time in header are ignored by module begin blockers - - // Redelegate the user's delegation between the two validators. This should trigger hooks that sync the user's claim. - err = suite.deliverMsgRedelegate(suite.ctx, suite.addrs[0], suite.validatorAddrs[0], suite.validatorAddrs[1], c(bondDenom, 1_000_000)) - suite.Require().NoError(err) - - // Check that the user's claim has been synced. ie rewards added, index updated - claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0]) - suite.Require().True(found) - - globalIndex, found := suite.keeper.GetHardDelegatorRewardFactor(suite.ctx, bondDenom) - suite.Require().True(found) - claimIndex, found := claim.DelegatorRewardIndexes.GetRewardIndex(bondDenom) - suite.Require().True(found) - suite.Require().Equal(globalIndex, claimIndex.RewardFactor) - suite.Require().Equal( - cs(c(rewardsPerSecond.Denom, 76471)), - claim.Reward, - ) -} diff --git a/x/incentive/keeper/rewards_usdx.go b/x/incentive/keeper/rewards_usdx.go new file mode 100644 index 00000000..a16c315c --- /dev/null +++ b/x/incentive/keeper/rewards_usdx.go @@ -0,0 +1,188 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + cdptypes "github.com/kava-labs/kava/x/cdp/types" + "github.com/kava-labs/kava/x/incentive/types" +) + +// AccumulateUSDXMintingRewards updates the rewards accumulated for the input reward period +func (k Keeper) AccumulateUSDXMintingRewards(ctx sdk.Context, rewardPeriod types.RewardPeriod) error { + previousAccrualTime, found := k.GetPreviousUSDXMintingAccrualTime(ctx, rewardPeriod.CollateralType) + if !found { + k.SetPreviousUSDXMintingAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) + return nil + } + timeElapsed := CalculateTimeElapsed(rewardPeriod.Start, rewardPeriod.End, ctx.BlockTime(), previousAccrualTime) + if timeElapsed.IsZero() { + return nil + } + if rewardPeriod.RewardsPerSecond.Amount.IsZero() { + k.SetPreviousUSDXMintingAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) + return nil + } + totalPrincipal := k.cdpKeeper.GetTotalPrincipal(ctx, rewardPeriod.CollateralType, types.PrincipalDenom).ToDec() + if totalPrincipal.IsZero() { + k.SetPreviousUSDXMintingAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) + return nil + } + newRewards := timeElapsed.Mul(rewardPeriod.RewardsPerSecond.Amount) + cdpFactor, found := k.cdpKeeper.GetInterestFactor(ctx, rewardPeriod.CollateralType) + if !found { + k.SetPreviousUSDXMintingAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) + return nil + } + rewardFactor := newRewards.ToDec().Mul(cdpFactor).Quo(totalPrincipal) + + previousRewardFactor, found := k.GetUSDXMintingRewardFactor(ctx, rewardPeriod.CollateralType) + if !found { + previousRewardFactor = sdk.ZeroDec() + } + newRewardFactor := previousRewardFactor.Add(rewardFactor) + k.SetUSDXMintingRewardFactor(ctx, rewardPeriod.CollateralType, newRewardFactor) + k.SetPreviousUSDXMintingAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime()) + return nil +} + +// InitializeUSDXMintingClaim creates or updates a claim such that no new rewards are accrued, but any existing rewards are not lost. +// this function should be called after a cdp is created. If a user previously had a cdp, then closed it, they shouldn't +// accrue rewards during the period the cdp was closed. By setting the reward factor to the current global reward factor, +// any unclaimed rewards are preserved, but no new rewards are added. +func (k Keeper) InitializeUSDXMintingClaim(ctx sdk.Context, cdp cdptypes.CDP) { + _, found := k.GetUSDXMintingRewardPeriod(ctx, cdp.Type) + if !found { + // this collateral type is not incentivized, do nothing + return + } + rewardFactor, found := k.GetUSDXMintingRewardFactor(ctx, cdp.Type) + if !found { + rewardFactor = sdk.ZeroDec() + } + claim, found := k.GetUSDXMintingClaim(ctx, cdp.Owner) + if !found { // this is the owner's first usdx minting reward claim + claim = types.NewUSDXMintingClaim(cdp.Owner, sdk.NewCoin(types.USDXMintingRewardDenom, sdk.ZeroInt()), types.RewardIndexes{types.NewRewardIndex(cdp.Type, rewardFactor)}) + k.SetUSDXMintingClaim(ctx, claim) + return + } + // the owner has an existing usdx minting reward claim + index, hasRewardIndex := claim.HasRewardIndex(cdp.Type) + if !hasRewardIndex { // this is the owner's first usdx minting reward for this collateral type + claim.RewardIndexes = append(claim.RewardIndexes, types.NewRewardIndex(cdp.Type, rewardFactor)) + } else { // the owner has a previous usdx minting reward for this collateral type + claim.RewardIndexes[index] = types.NewRewardIndex(cdp.Type, rewardFactor) + } + k.SetUSDXMintingClaim(ctx, claim) +} + +// SynchronizeUSDXMintingReward updates the claim object by adding any accumulated rewards and updating the reward index value. +// this should be called before a cdp is modified, immediately after the 'SynchronizeInterest' method is called in the cdp module +func (k Keeper) SynchronizeUSDXMintingReward(ctx sdk.Context, cdp cdptypes.CDP) { + _, found := k.GetUSDXMintingRewardPeriod(ctx, cdp.Type) + if !found { + // this collateral type is not incentivized, do nothing + return + } + + globalRewardFactor, found := k.GetUSDXMintingRewardFactor(ctx, cdp.Type) + if !found { + globalRewardFactor = sdk.ZeroDec() + } + claim, found := k.GetUSDXMintingClaim(ctx, cdp.Owner) + if !found { + claim = types.NewUSDXMintingClaim(cdp.Owner, sdk.NewCoin(types.USDXMintingRewardDenom, sdk.ZeroInt()), types.RewardIndexes{types.NewRewardIndex(cdp.Type, globalRewardFactor)}) + k.SetUSDXMintingClaim(ctx, claim) + return + } + + // the owner has an existing usdx minting reward claim + index, hasRewardIndex := claim.HasRewardIndex(cdp.Type) + if !hasRewardIndex { // this is the owner's first usdx minting reward for this collateral type + claim.RewardIndexes = append(claim.RewardIndexes, types.NewRewardIndex(cdp.Type, globalRewardFactor)) + k.SetUSDXMintingClaim(ctx, claim) + return + } + userRewardFactor := claim.RewardIndexes[index].RewardFactor + rewardsAccumulatedFactor := globalRewardFactor.Sub(userRewardFactor) + if rewardsAccumulatedFactor.IsZero() { + return + } + claim.RewardIndexes[index].RewardFactor = globalRewardFactor + newRewardsAmount := rewardsAccumulatedFactor.Mul(cdp.GetTotalPrincipal().Amount.ToDec()).RoundInt() + if newRewardsAmount.IsZero() { + k.SetUSDXMintingClaim(ctx, claim) + return + } + newRewardsCoin := sdk.NewCoin(types.USDXMintingRewardDenom, newRewardsAmount) + claim.Reward = claim.Reward.Add(newRewardsCoin) + k.SetUSDXMintingClaim(ctx, claim) +} + +// SimulateUSDXMintingSynchronization calculates a user's outstanding USDX minting rewards by simulating reward synchronization +func (k Keeper) SimulateUSDXMintingSynchronization(ctx sdk.Context, claim types.USDXMintingClaim) types.USDXMintingClaim { + for _, ri := range claim.RewardIndexes { + _, found := k.GetUSDXMintingRewardPeriod(ctx, ri.CollateralType) + if !found { + continue + } + + globalRewardFactor, found := k.GetUSDXMintingRewardFactor(ctx, ri.CollateralType) + if !found { + globalRewardFactor = sdk.ZeroDec() + } + + // the owner has an existing usdx minting reward claim + index, hasRewardIndex := claim.HasRewardIndex(ri.CollateralType) + if !hasRewardIndex { // this is the owner's first usdx minting reward for this collateral type + claim.RewardIndexes = append(claim.RewardIndexes, types.NewRewardIndex(ri.CollateralType, globalRewardFactor)) + } + userRewardFactor := claim.RewardIndexes[index].RewardFactor + rewardsAccumulatedFactor := globalRewardFactor.Sub(userRewardFactor) + if rewardsAccumulatedFactor.IsZero() { + continue + } + + claim.RewardIndexes[index].RewardFactor = globalRewardFactor + + cdp, found := k.cdpKeeper.GetCdpByOwnerAndCollateralType(ctx, claim.GetOwner(), ri.CollateralType) + if !found { + continue + } + newRewardsAmount := rewardsAccumulatedFactor.Mul(cdp.GetTotalPrincipal().Amount.ToDec()).RoundInt() + if newRewardsAmount.IsZero() { + continue + } + newRewardsCoin := sdk.NewCoin(types.USDXMintingRewardDenom, newRewardsAmount) + claim.Reward = claim.Reward.Add(newRewardsCoin) + } + + return claim +} + +// SynchronizeUSDXMintingClaim updates the claim object by adding any rewards that have accumulated. +// Returns the updated claim object +func (k Keeper) SynchronizeUSDXMintingClaim(ctx sdk.Context, claim types.USDXMintingClaim) (types.USDXMintingClaim, error) { + for _, ri := range claim.RewardIndexes { + cdp, found := k.cdpKeeper.GetCdpByOwnerAndCollateralType(ctx, claim.Owner, ri.CollateralType) + if !found { + // if the cdp for this collateral type has been closed, no updates are needed + continue + } + claim = k.synchronizeRewardAndReturnClaim(ctx, cdp) + } + return claim, nil +} + +// this function assumes a claim already exists, so don't call it if that's not the case +func (k Keeper) synchronizeRewardAndReturnClaim(ctx sdk.Context, cdp cdptypes.CDP) types.USDXMintingClaim { + k.SynchronizeUSDXMintingReward(ctx, cdp) + claim, _ := k.GetUSDXMintingClaim(ctx, cdp.Owner) + return claim +} + +// ZeroUSDXMintingClaim zeroes out the claim object's rewards and returns the updated claim object +func (k Keeper) ZeroUSDXMintingClaim(ctx sdk.Context, claim types.USDXMintingClaim) types.USDXMintingClaim { + claim.Reward = sdk.NewCoin(claim.Reward.Denom, sdk.ZeroInt()) + k.SetUSDXMintingClaim(ctx, claim) + return claim +} diff --git a/x/incentive/keeper/rewards_usdx_test.go b/x/incentive/keeper/rewards_usdx_test.go new file mode 100644 index 00000000..e7654ab0 --- /dev/null +++ b/x/incentive/keeper/rewards_usdx_test.go @@ -0,0 +1,308 @@ +package keeper_test + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + + cdptypes "github.com/kava-labs/kava/x/cdp/types" + "github.com/kava-labs/kava/x/incentive/types" +) + +const ( + oneYear time.Duration = time.Hour * 24 * 365 +) + +func (suite *KeeperTestSuite) TestAccumulateUSDXMintingRewards() { + type args struct { + ctype string + rewardsPerSecond sdk.Coin + initialTime time.Time + initialTotalPrincipal sdk.Coin + timeElapsed int + expectedRewardFactor sdk.Dec + } + type test struct { + name string + args args + } + testCases := []test{ + { + "7 seconds", + args{ + ctype: "bnb-a", + rewardsPerSecond: c("ukava", 122354), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + initialTotalPrincipal: c("usdx", 1000000000000), + timeElapsed: 7, + expectedRewardFactor: d("0.000000856478000000"), + }, + }, + { + "1 day", + args{ + ctype: "bnb-a", + rewardsPerSecond: c("ukava", 122354), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + initialTotalPrincipal: c("usdx", 1000000000000), + timeElapsed: 86400, + expectedRewardFactor: d("0.0105713856"), + }, + }, + { + "0 seconds", + args{ + ctype: "bnb-a", + rewardsPerSecond: c("ukava", 122354), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + initialTotalPrincipal: c("usdx", 1000000000000), + timeElapsed: 0, + expectedRewardFactor: d("0.0"), + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWithGenState() + suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) + + // setup cdp state + cdpKeeper := suite.app.GetCDPKeeper() + cdpKeeper.SetTotalPrincipal(suite.ctx, tc.args.ctype, cdptypes.DefaultStableDenom, tc.args.initialTotalPrincipal.Amount) + + // setup incentive state + params := types.NewParams( + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), cs(tc.args.rewardsPerSecond))}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), cs(tc.args.rewardsPerSecond))}, + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + tc.args.initialTime.Add(time.Hour*24*365*5), + ) + suite.keeper.SetParams(suite.ctx, params) + suite.keeper.SetPreviousUSDXMintingAccrualTime(suite.ctx, tc.args.ctype, tc.args.initialTime) + suite.keeper.SetUSDXMintingRewardFactor(suite.ctx, tc.args.ctype, sdk.ZeroDec()) + + updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * tc.args.timeElapsed)) + suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) + rewardPeriod, found := suite.keeper.GetUSDXMintingRewardPeriod(suite.ctx, tc.args.ctype) + suite.Require().True(found) + err := suite.keeper.AccumulateUSDXMintingRewards(suite.ctx, rewardPeriod) + suite.Require().NoError(err) + + rewardFactor, found := suite.keeper.GetUSDXMintingRewardFactor(suite.ctx, tc.args.ctype) + suite.Require().Equal(tc.args.expectedRewardFactor, rewardFactor) + }) + } +} + +func (suite *KeeperTestSuite) TestSynchronizeUSDXMintingReward() { + type args struct { + ctype string + rewardsPerSecond sdk.Coin + initialTime time.Time + initialCollateral sdk.Coin + initialPrincipal sdk.Coin + blockTimes []int + expectedRewardFactor sdk.Dec + expectedRewards sdk.Coin + } + type test struct { + name string + args args + } + + testCases := []test{ + { + "10 blocks", + args{ + ctype: "bnb-a", + rewardsPerSecond: c("ukava", 122354), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + initialCollateral: c("bnb", 1000000000000), + initialPrincipal: c("usdx", 10000000000), + blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + expectedRewardFactor: d("0.001223540000000000"), + expectedRewards: c("ukava", 12235400), + }, + }, + { + "10 blocks - long block time", + args{ + ctype: "bnb-a", + rewardsPerSecond: c("ukava", 122354), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + initialCollateral: c("bnb", 1000000000000), + initialPrincipal: c("usdx", 10000000000), + blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, + expectedRewardFactor: d("10.57138560000000000"), + expectedRewards: c("ukava", 105713856000), + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWithGenState() + suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) + + // setup incentive state + params := types.NewParams( + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), cs(tc.args.rewardsPerSecond))}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), cs(tc.args.rewardsPerSecond))}, + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + tc.args.initialTime.Add(time.Hour*24*365*5), + ) + suite.keeper.SetParams(suite.ctx, params) + suite.keeper.SetPreviousUSDXMintingAccrualTime(suite.ctx, tc.args.ctype, tc.args.initialTime) + suite.keeper.SetUSDXMintingRewardFactor(suite.ctx, tc.args.ctype, sdk.ZeroDec()) + + // setup account state + sk := suite.app.GetSupplyKeeper() + sk.MintCoins(suite.ctx, cdptypes.ModuleName, sdk.NewCoins(tc.args.initialCollateral)) + sk.SendCoinsFromModuleToAccount(suite.ctx, cdptypes.ModuleName, suite.addrs[0], sdk.NewCoins(tc.args.initialCollateral)) + + // setup cdp state + cdpKeeper := suite.app.GetCDPKeeper() + err := cdpKeeper.AddCdp(suite.ctx, suite.addrs[0], tc.args.initialCollateral, tc.args.initialPrincipal, tc.args.ctype) + suite.Require().NoError(err) + + claim, found := suite.keeper.GetUSDXMintingClaim(suite.ctx, suite.addrs[0]) + suite.Require().True(found) + suite.Require().Equal(sdk.ZeroDec(), claim.RewardIndexes[0].RewardFactor) + + var timeElapsed int + previousBlockTime := suite.ctx.BlockTime() + for _, t := range tc.args.blockTimes { + timeElapsed += t + updatedBlockTime := previousBlockTime.Add(time.Duration(int(time.Second) * t)) + previousBlockTime = updatedBlockTime + blockCtx := suite.ctx.WithBlockTime(updatedBlockTime) + rewardPeriod, found := suite.keeper.GetUSDXMintingRewardPeriod(blockCtx, tc.args.ctype) + suite.Require().True(found) + err := suite.keeper.AccumulateUSDXMintingRewards(blockCtx, rewardPeriod) + suite.Require().NoError(err) + } + updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed)) + suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) + cdp, found := cdpKeeper.GetCdpByOwnerAndCollateralType(suite.ctx, suite.addrs[0], tc.args.ctype) + suite.Require().True(found) + suite.Require().NotPanics(func() { + suite.keeper.SynchronizeUSDXMintingReward(suite.ctx, cdp) + }) + + rewardFactor, found := suite.keeper.GetUSDXMintingRewardFactor(suite.ctx, tc.args.ctype) + suite.Require().Equal(tc.args.expectedRewardFactor, rewardFactor) + + claim, found = suite.keeper.GetUSDXMintingClaim(suite.ctx, suite.addrs[0]) + suite.Require().True(found) + suite.Require().Equal(tc.args.expectedRewardFactor, claim.RewardIndexes[0].RewardFactor) + suite.Require().Equal(tc.args.expectedRewards, claim.Reward) + }) + } +} + +func (suite *KeeperTestSuite) TestSimulateUSDXMintingRewardSynchronization() { + type args struct { + ctype string + rewardsPerSecond sdk.Coins + initialTime time.Time + initialCollateral sdk.Coin + initialPrincipal sdk.Coin + blockTimes []int + expectedRewardFactor sdk.Dec + expectedRewards sdk.Coin + } + type test struct { + name string + args args + } + + testCases := []test{ + { + "10 blocks", + args{ + ctype: "bnb-a", + rewardsPerSecond: cs(c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + initialCollateral: c("bnb", 1000000000000), + initialPrincipal: c("usdx", 10000000000), + blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + expectedRewardFactor: d("0.001223540000000000"), + expectedRewards: c("ukava", 12235400), + }, + }, + { + "10 blocks - long block time", + args{ + ctype: "bnb-a", + rewardsPerSecond: cs(c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + initialCollateral: c("bnb", 1000000000000), + initialPrincipal: c("usdx", 10000000000), + blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, + expectedRewardFactor: d("10.57138560000000000"), + expectedRewards: c("ukava", 105713856000), + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWithGenState() + suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) + + // setup incentive state + params := types.NewParams( + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, + types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + tc.args.initialTime.Add(time.Hour*24*365*5), + ) + suite.keeper.SetParams(suite.ctx, params) + suite.keeper.SetParams(suite.ctx, params) + suite.keeper.SetPreviousUSDXMintingAccrualTime(suite.ctx, tc.args.ctype, tc.args.initialTime) + suite.keeper.SetUSDXMintingRewardFactor(suite.ctx, tc.args.ctype, sdk.ZeroDec()) + + // setup account state + sk := suite.app.GetSupplyKeeper() + sk.MintCoins(suite.ctx, cdptypes.ModuleName, sdk.NewCoins(tc.args.initialCollateral)) + sk.SendCoinsFromModuleToAccount(suite.ctx, cdptypes.ModuleName, suite.addrs[0], sdk.NewCoins(tc.args.initialCollateral)) + + // setup cdp state + cdpKeeper := suite.app.GetCDPKeeper() + err := cdpKeeper.AddCdp(suite.ctx, suite.addrs[0], tc.args.initialCollateral, tc.args.initialPrincipal, tc.args.ctype) + suite.Require().NoError(err) + + claim, found := suite.keeper.GetUSDXMintingClaim(suite.ctx, suite.addrs[0]) + suite.Require().True(found) + suite.Require().Equal(sdk.ZeroDec(), claim.RewardIndexes[0].RewardFactor) + + var timeElapsed int + previousBlockTime := suite.ctx.BlockTime() + for _, t := range tc.args.blockTimes { + timeElapsed += t + updatedBlockTime := previousBlockTime.Add(time.Duration(int(time.Second) * t)) + previousBlockTime = updatedBlockTime + blockCtx := suite.ctx.WithBlockTime(updatedBlockTime) + rewardPeriod, found := suite.keeper.GetUSDXMintingRewardPeriod(blockCtx, tc.args.ctype) + suite.Require().True(found) + err := suite.keeper.AccumulateUSDXMintingRewards(blockCtx, rewardPeriod) + suite.Require().NoError(err) + } + updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed)) + suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) + + claim, found = suite.keeper.GetUSDXMintingClaim(suite.ctx, suite.addrs[0]) + suite.Require().True(found) + suite.Require().Equal(claim.RewardIndexes[0].RewardFactor, sdk.ZeroDec()) + suite.Require().Equal(claim.Reward, sdk.NewCoin("ukava", sdk.ZeroInt())) + + updatedClaim := suite.keeper.SimulateUSDXMintingSynchronization(suite.ctx, claim) + suite.Require().Equal(tc.args.expectedRewardFactor, updatedClaim.RewardIndexes[0].RewardFactor) + suite.Require().Equal(tc.args.expectedRewards, updatedClaim.Reward) + }) + } +}