diff --git a/x/incentive/abci.go b/x/incentive/abci.go index 89866bf4..d6aa570d 100644 --- a/x/incentive/abci.go +++ b/x/incentive/abci.go @@ -27,4 +27,10 @@ func BeginBlocker(ctx sdk.Context, k keeper.Keeper) { panic(err) } } + for _, rp := range params.HardDelegatorRewardPeriods { + err := k.AccumulateHardDelegatorRewards(ctx, rp) + if err != nil { + panic(err) + } + } } diff --git a/x/incentive/keeper/querier.go b/x/incentive/keeper/querier.go index f80a5b5f..70a96e6e 100644 --- a/x/incentive/keeper/querier.go +++ b/x/incentive/keeper/querier.go @@ -67,8 +67,14 @@ func queryGetHardRewards(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]by paginatedHardClaims = hardClaims[startH:endH] } + var augmentedHardClaims types.HardLiquidityProviderClaims + for _, claim := range paginatedHardClaims { + augmentedClaim := k.SimulateHardSynchronization(ctx, claim) + augmentedHardClaims = append(augmentedHardClaims, augmentedClaim) + } + // Marshal Hard claims - bz, err := codec.MarshalJSONIndent(k.cdc, paginatedHardClaims) + bz, err := codec.MarshalJSONIndent(k.cdc, augmentedHardClaims) if err != nil { return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) } @@ -102,8 +108,14 @@ func queryGetUSDXMintingRewards(ctx sdk.Context, req abci.RequestQuery, k Keeper paginatedUsdxMintingClaims = usdxMintingClaims[startU:endU] } + var augmentedUsdxMintingClaims types.USDXMintingClaims + for _, claim := range paginatedUsdxMintingClaims { + augmentedClaim := k.SimulateUSDXMintingSynchronization(ctx, claim) + augmentedUsdxMintingClaims = append(augmentedUsdxMintingClaims, augmentedClaim) + } + // Marshal USDX minting claims - bz, err := codec.MarshalJSONIndent(k.cdc, paginatedUsdxMintingClaims) + bz, err := codec.MarshalJSONIndent(k.cdc, augmentedUsdxMintingClaims) if err != nil { return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) } diff --git a/x/incentive/keeper/rewards.go b/x/incentive/keeper/rewards.go index f8280efb..ee9e10e4 100644 --- a/x/incentive/keeper/rewards.go +++ b/x/incentive/keeper/rewards.go @@ -587,3 +587,171 @@ func CalculateTimeElapsed(rewardPeriod types.RewardPeriod, blockTime time.Time, 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 { + supplyFactor, found := k.GetHardSupplyRewardFactor(ctx, ri.CollateralType) + if !found { + continue + } + + supplyIndex, hasSupplyRewardIndex := claim.HasSupplyRewardIndex(ri.CollateralType) + if !hasSupplyRewardIndex { + continue + } + claim.SupplyRewardIndexes[supplyIndex].RewardFactor = supplyFactor + + rewardsAccumulatedFactor := supplyFactor.Sub(ri.RewardFactor) + if rewardsAccumulatedFactor.IsZero() { + continue + } + + deposit, found := k.hardKeeper.GetDeposit(ctx, claim.GetOwner()) + if !found { + continue + } + + var newRewardsAmount sdk.Int + if deposit.Amount.AmountOf(ri.CollateralType).GT(sdk.ZeroInt()) { + newRewardsAmount = rewardsAccumulatedFactor.Mul(deposit.Amount.AmountOf(ri.CollateralType).ToDec()).RoundInt() + if newRewardsAmount.IsZero() || newRewardsAmount.IsNegative() { + continue + } + } + newRewardsCoin := sdk.NewCoin(types.HardLiquidityRewardDenom, newRewardsAmount) + claim.Reward = claim.Reward.Add(newRewardsCoin) + } + + // 2. Simulate Hard borrow-side rewards + for _, ri := range claim.BorrowRewardIndexes { + borrowFactor, found := k.GetHardBorrowRewardFactor(ctx, ri.CollateralType) + if !found { + continue + } + + borrowIndex, hasBorrowRewardIndex := claim.HasBorrowRewardIndex(ri.CollateralType) + if !hasBorrowRewardIndex { + continue + } + claim.BorrowRewardIndexes[borrowIndex].RewardFactor = borrowFactor + + rewardsAccumulatedFactor := borrowFactor.Sub(ri.RewardFactor) + if rewardsAccumulatedFactor.IsZero() { + continue + } + + borrow, found := k.hardKeeper.GetBorrow(ctx, claim.GetOwner()) + if !found { + continue + } + + var newRewardsAmount sdk.Int + if borrow.Amount.AmountOf(ri.CollateralType).GT(sdk.ZeroInt()) { + newRewardsAmount = rewardsAccumulatedFactor.Mul(borrow.Amount.AmountOf(ri.CollateralType).ToDec()).RoundInt() + if newRewardsAmount.IsZero() || newRewardsAmount.IsNegative() { + continue + } + } + newRewardsCoin := sdk.NewCoin(types.HardLiquidityRewardDenom, 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() + + // TODO: set reasonable max limit on delegation iteration + maxUInt := ^uint16(0) + delegations := k.stakingKeeper.GetDelegatorDelegations(ctx, claim.GetOwner(), maxUInt) + 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 +} diff --git a/x/incentive/keeper/rewards_test.go b/x/incentive/keeper/rewards_test.go index 290d4887..266bda9d 100644 --- a/x/incentive/keeper/rewards_test.go +++ b/x/incentive/keeper/rewards_test.go @@ -1077,6 +1077,442 @@ func (suite *KeeperTestSuite) TestSynchronizeHardDelegatorReward() { } } +func (suite *KeeperTestSuite) TestSimulateHardSupplyRewardSynchronization() { + type args struct { + deposit sdk.Coin + rewardsPerSecond sdk.Coin + initialTime time.Time + blockTimes []int + expectedRewardFactor sdk.Dec + expectedRewards sdk.Coin + } + type test struct { + name string + args args + } + + testCases := []test{ + { + "10 blocks", + args{ + deposit: c("bnb", 10000000000), + 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("0.001223540000000000"), + expectedRewards: c("hard", 12235400), + }, + }, + { + "10 blocks - long block time", + args{ + deposit: c("bnb", 10000000000), + 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("10.571385600000000000"), + expectedRewards: 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) + + // setup 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)}, + types.RewardPeriods{types.NewRewardPeriod(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)}, + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.deposit.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.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, tc.args.deposit.Denom, tc.args.initialTime) + suite.keeper.SetHardSupplyRewardFactor(suite.ctx, tc.args.deposit.Denom, sdk.ZeroDec()) + + // 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.SetBorrowInterestFactor(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) + suite.Require().Equal(sdk.ZeroDec(), claim.SupplyRewardIndexes[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.GetHardSupplyRewardPeriod(blockCtx, tc.args.deposit.Denom) + suite.Require().True(found) + + err := suite.keeper.AccumulateHardSupplyRewards(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.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) + suite.Require().True(found) + suite.Require().Equal(claim.SupplyRewardIndexes[0].RewardFactor, sdk.ZeroDec()) + suite.Require().Equal(claim.Reward, sdk.NewCoin("hard", sdk.ZeroInt())) + + updatedClaim := suite.keeper.SimulateHardSynchronization(suite.ctx, claim) + suite.Require().Equal(updatedClaim.SupplyRewardIndexes[0].RewardFactor, tc.args.expectedRewardFactor) + suite.Require().Equal(updatedClaim.Reward, tc.args.expectedRewards) + }) + } +} + +func (suite *KeeperTestSuite) TestSimulateHardBorrowRewardSynchronization() { + type args struct { + borrow sdk.Coin + rewardsPerSecond sdk.Coin + initialTime time.Time + blockTimes []int + expectedRewardFactor sdk.Dec + expectedRewards sdk.Coin + } + type test struct { + name string + args args + } + + testCases := []test{ + { + "10 blocks", + args{ + borrow: c("bnb", 10000000000), + 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("0.001223540000173228"), + expectedRewards: c("hard", 12235400), + }, + }, + { + "10 blocks - long block time", + args{ + borrow: c("bnb", 10000000000), + 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("10.571385603126235340"), + expectedRewards: 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)}, + types.RewardPeriods{types.NewRewardPeriod(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)}, + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.borrow.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.SetPreviousHardBorrowRewardAccrualTime(suite.ctx, tc.args.borrow.Denom, tc.args.initialTime) + suite.keeper.SetHardBorrowRewardFactor(suite.ctx, tc.args.borrow.Denom, sdk.ZeroDec()) + + // 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) + suite.Require().Equal(sdk.ZeroDec(), claim.BorrowRewardIndexes[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.GetHardBorrowRewardPeriod(blockCtx, tc.args.borrow.Denom) + suite.Require().True(found) + + err := suite.keeper.AccumulateHardBorrowRewards(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.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) + suite.Require().True(found) + suite.Require().Equal(claim.BorrowRewardIndexes[0].RewardFactor, sdk.ZeroDec()) + suite.Require().Equal(claim.Reward, sdk.NewCoin("hard", sdk.ZeroInt())) + + updatedClaim := suite.keeper.SimulateHardSynchronization(suite.ctx, claim) + suite.Require().Equal(updatedClaim.BorrowRewardIndexes[0].RewardFactor, tc.args.expectedRewardFactor) + suite.Require().Equal(updatedClaim.Reward, tc.args.expectedRewards) + }) + } +} + +func (suite *KeeperTestSuite) TestSimulateHardDelegatorRewardSynchronization() { + type args struct { + delegation sdk.Coin + rewardsPerSecond sdk.Coin + initialTime time.Time + blockTimes []int + expectedRewardFactor sdk.Dec + expectedRewards sdk.Coin + } + 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: 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: 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)}, + 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.RewardPeriods{types.NewRewardPeriod(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)}, + 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) + 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) + + 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) + + claim, found = suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0]) + suite.Require().True(found) + suite.Require().Equal(claim.DelegatorRewardIndexes[0].RewardFactor, sdk.ZeroDec()) + suite.Require().Equal(claim.Reward, sdk.NewCoin("hard", sdk.ZeroInt())) + + updatedClaim := suite.keeper.SimulateHardSynchronization(suite.ctx, claim) + suite.Require().Equal(updatedClaim.DelegatorRewardIndexes[0].RewardFactor, tc.args.expectedRewardFactor) + suite.Require().Equal(updatedClaim.Reward, tc.args.expectedRewards) + }) + } +} + +func (suite *KeeperTestSuite) TestSimulateUSDXMintingRewardSynchronization() { + 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.RewardPeriods{types.NewRewardPeriod(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)}, + 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) + + 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) SetupWithGenState() { config := sdk.GetConfig() app.SetBech32AddressPrefixes(config)