From 54c0793ced9d9e5ea89822b519bf556244b0f097 Mon Sep 17 00:00:00 2001 From: Ruaridh Date: Thu, 25 Mar 2021 06:10:13 +0000 Subject: [PATCH] Fix delegation claim syncing (#889) * sync claims on validator state changes and slashes * add test notes * update missed sync delegator calls * tidy up suite addresses initialization * test claim synced when validator bonds/unbonds * test validator slashed * check reward factor increased * test redelegation sync claim * revert mistake * resolve trailing TODOs * call incentive hooks after hard liquidation * check global index in tests after delegator reward sync Co-authored-by: denalimarsh Co-authored-by: karzak --- x/hard/keeper/liquidation.go | 5 + x/incentive/keeper/hooks.go | 49 +++- x/incentive/keeper/integration_test.go | 11 +- x/incentive/keeper/keeper_test.go | 24 +- x/incentive/keeper/rewards.go | 28 +- x/incentive/keeper/rewards_test.go | 355 ++++++++++++++++++++++++- x/incentive/types/expected_keepers.go | 1 + 7 files changed, 440 insertions(+), 33 deletions(-) diff --git a/x/hard/keeper/liquidation.go b/x/hard/keeper/liquidation.go index 67daa6c0..b2260cb9 100644 --- a/x/hard/keeper/liquidation.go +++ b/x/hard/keeper/liquidation.go @@ -61,8 +61,13 @@ func (k Keeper) AttemptKeeperLiquidation(ctx sdk.Context, keeper sdk.AccAddress, return err } + deposit.Amount = sdk.NewCoins() k.DeleteDeposit(ctx, deposit) + k.AfterDepositModified(ctx, deposit) + + borrow.Amount = sdk.NewCoins() k.DeleteBorrow(ctx, borrow) + k.AfterBorrowModified(ctx, borrow) return nil } diff --git a/x/incentive/keeper/hooks.go b/x/incentive/keeper/hooks.go index 33b8cd98..2118a10b 100644 --- a/x/incentive/keeper/hooks.go +++ b/x/incentive/keeper/hooks.go @@ -66,31 +66,70 @@ func (h Hooks) AfterBorrowModified(ctx sdk.Context, borrow hardtypes.Borrow) { h.k.UpdateHardBorrowIndexDenoms(ctx, borrow) } -// ------------------- Staking Module Hooks ------------------- +/* ------------------- Staking Module Hooks ------------------- + +Rewards are calculated based on total delegated tokens to bonded validators (not shares). +We need to sync the claim before the user's delegated tokens are changed. + +When delegated tokens (to bonded validators) are changed: +- user creates new delegation + - total bonded delegation increases +- user delegates or beginUnbonding or beginRedelegate an existing delegation + - total bonded delegation increases or decreases +- validator is slashed and Jailed/Tombstoned (tokens reduce, and validator is unbonded) + - slash: total bonded delegation decreases (less tokens) + - jail: total bonded delegation decreases (tokens no longer bonded (after end blocker runs)) +- validator becomes unbonded (ie when they drop out of the top 100) + - total bonded delegation decreases (tokens no longer bonded) + +*/ // BeforeDelegationCreated runs before a delegation is created func (h Hooks) BeforeDelegationCreated(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) { + // Add a claim if one doesn't exist, otherwise sync the existing. h.k.InitializeHardDelegatorReward(ctx, delAddr) } // BeforeDelegationSharesModified runs before an existing delegation is modified func (h Hooks) BeforeDelegationSharesModified(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) { - h.k.SynchronizeHardDelegatorRewards(ctx, delAddr) + // Sync rewards based on total delegated to bonded validators. + h.k.SynchronizeHardDelegatorRewards(ctx, delAddr, nil, false) } -// NOTE: following hooks are just implemented to ensure StakingHooks interface compliance - // BeforeValidatorSlashed is called before a validator is slashed -func (h Hooks) BeforeValidatorSlashed(ctx sdk.Context, valAddr sdk.ValAddress, fraction sdk.Dec) {} +// Validator status is not updated when Slash or Jail is called +func (h Hooks) BeforeValidatorSlashed(ctx sdk.Context, valAddr sdk.ValAddress, fraction sdk.Dec) { + // Sync all claims for users delegated to this validator. + // For each claim, sync based on the total delegated to bonded validators. + for _, delegation := range h.k.stakingKeeper.GetValidatorDelegations(ctx, valAddr) { + h.k.SynchronizeHardDelegatorRewards(ctx, delegation.DelegatorAddress, nil, false) + } +} // AfterValidatorBeginUnbonding is called after a validator begins unbonding +// Validator status is set to Unbonding prior to hook running func (h Hooks) AfterValidatorBeginUnbonding(ctx sdk.Context, consAddr sdk.ConsAddress, valAddr sdk.ValAddress) { + // Sync all claims for users delegated to this validator. + // For each claim, sync based on the total delegated to bonded validators, and also delegations to valAddr. + // valAddr's status has just been set to Unbonding, but we want to include delegations to it in the sync. + for _, delegation := range h.k.stakingKeeper.GetValidatorDelegations(ctx, valAddr) { + h.k.SynchronizeHardDelegatorRewards(ctx, delegation.DelegatorAddress, valAddr, true) + } } // AfterValidatorBonded is called after a validator is bonded +// Validator status is set to Bonded prior to hook running func (h Hooks) AfterValidatorBonded(ctx sdk.Context, consAddr sdk.ConsAddress, valAddr sdk.ValAddress) { + // Sync all claims for users delegated to this validator. + // For each claim, sync based on the total delegated to bonded validators, except for delegations to valAddr. + // valAddr's status has just been set to Bonded, but we don't want to include delegations to it in the sync + for _, delegation := range h.k.stakingKeeper.GetValidatorDelegations(ctx, valAddr) { + h.k.SynchronizeHardDelegatorRewards(ctx, delegation.DelegatorAddress, valAddr, false) + } } +// NOTE: following hooks are just implemented to ensure StakingHooks interface compliance + // AfterDelegationModified runs after a delegation is modified func (h Hooks) AfterDelegationModified(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) { } diff --git a/x/incentive/keeper/integration_test.go b/x/incentive/keeper/integration_test.go index b2294682..91d40f97 100644 --- a/x/incentive/keeper/integration_test.go +++ b/x/incentive/keeper/integration_test.go @@ -209,20 +209,11 @@ func NewStakingGenesisState() app.GenesisState { } func (suite *KeeperTestSuite) SetupWithGenState() { - config := sdk.GetConfig() - app.SetBech32AddressPrefixes(config) - - _, allAddrs := app.GeneratePrivKeyAddressPairs(10) - suite.addrs = allAddrs[:5] - for _, a := range allAddrs[5:] { - suite.validatorAddrs = append(suite.validatorAddrs, sdk.ValAddress(a)) - } - tApp := app.NewTestApp() ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) tApp.InitializeFromGenesisStates( - NewAuthGenState(allAddrs, cs(c("ukava", 1_000_000_000))), + NewAuthGenState(suite.getAllAddrs(), cs(c("ukava", 1_000_000_000))), NewStakingGenesisState(), NewPricefeedGenStateMulti(), NewCDPGenStateMulti(), diff --git a/x/incentive/keeper/keeper_test.go b/x/incentive/keeper/keeper_test.go index e58d59dc..146d7143 100644 --- a/x/incentive/keeper/keeper_test.go +++ b/x/incentive/keeper/keeper_test.go @@ -38,17 +38,33 @@ type KeeperTestSuite struct { // The default state used by each test func (suite *KeeperTestSuite) SetupTest() { + config := sdk.GetConfig() + app.SetBech32AddressPrefixes(config) + + _, allAddrs := app.GeneratePrivKeyAddressPairs(10) + suite.addrs = allAddrs[:5] + for _, a := range allAddrs[5:] { + suite.validatorAddrs = append(suite.validatorAddrs, sdk.ValAddress(a)) + } + tApp := app.NewTestApp() ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) tApp.InitializeFromGenesisStates() - _, addrs := app.GeneratePrivKeyAddressPairs(5) - keeper := tApp.GetIncentiveKeeper() + suite.keeper = tApp.GetIncentiveKeeper() suite.app = tApp suite.ctx = ctx - suite.keeper = keeper - suite.addrs = addrs +} + +// getAllAddrs returns all user and validator addresses in the suite +func (suite *KeeperTestSuite) getAllAddrs() []sdk.AccAddress { + accAddrs := []sdk.AccAddress{} // initialize new slice to avoid accidental modifications to underlying + accAddrs = append(accAddrs, suite.addrs...) + for _, a := range suite.validatorAddrs { + accAddrs = append(accAddrs, sdk.AccAddress(a)) + } + return accAddrs } func (suite *KeeperTestSuite) getAccount(addr sdk.AccAddress) authexported.Account { diff --git a/x/incentive/keeper/rewards.go b/x/incentive/keeper/rewards.go index 57a79e2f..48931a0b 100644 --- a/x/incentive/keeper/rewards.go +++ b/x/incentive/keeper/rewards.go @@ -271,10 +271,7 @@ func (k Keeper) InitializeHardSupplyReward(ctx sdk.Context, deposit hardtypes.De } claim, found := k.GetHardLiquidityProviderClaim(ctx, deposit.Depositor) - if found { - // Reset borrow reward indexes - claim.BorrowRewardIndexes = types.MultiRewardIndexes{} - } else { + if !found { // Instantiate claim object claim = types.NewHardLiquidityProviderClaim(deposit.Depositor, sdk.Coins{}, nil, nil, nil) } @@ -496,8 +493,10 @@ func (k Keeper) UpdateHardBorrowIndexDenoms(ctx sdk.Context, borrow hardtypes.Bo k.SetHardLiquidityProviderClaim(ctx, claim) } -// SynchronizeHardDelegatorRewards updates the claim object by adding any accumulated rewards -func (k Keeper) SynchronizeHardDelegatorRewards(ctx sdk.Context, delegator sdk.AccAddress) { +// 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 @@ -529,9 +528,16 @@ func (k Keeper) SynchronizeHardDelegatorRewards(ctx sdk.Context, delegator sdk.A continue } - // Delegators don't accumulate rewards if their validator is unbonded/slashed - if validator.GetStatus() != sdk.Bonded { - 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() { @@ -601,7 +607,7 @@ func (k Keeper) InitializeHardDelegatorReward(ctx sdk.Context, delegator sdk.Acc // Instantiate claim object claim = types.NewHardLiquidityProviderClaim(delegator, sdk.Coins{}, nil, nil, nil) } else { - k.SynchronizeHardDelegatorRewards(ctx, delegator) + k.SynchronizeHardDelegatorRewards(ctx, delegator, nil, false) claim, _ = k.GetHardLiquidityProviderClaim(ctx, delegator) } @@ -652,7 +658,7 @@ func (k Keeper) SynchronizeHardLiquidityProviderClaim(ctx sdk.Context, owner sdk } // Synchronize any hard delegator rewards - k.SynchronizeHardDelegatorRewards(ctx, owner) + k.SynchronizeHardDelegatorRewards(ctx, owner, nil, false) } // ZeroHardLiquidityProviderClaim zeroes out the claim object's rewards and returns the updated claim object diff --git a/x/incentive/keeper/rewards_test.go b/x/incentive/keeper/rewards_test.go index 02062602..2cae8f3b 100644 --- a/x/incentive/keeper/rewards_test.go +++ b/x/incentive/keeper/rewards_test.go @@ -16,6 +16,10 @@ import ( "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 @@ -885,7 +889,7 @@ func (suite *KeeperTestSuite) TestAccumulateHardDelegatorRewards() { 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) + err = suite.deliverMsgDelegate(suite.ctx, suite.addrs[0], suite.validatorAddrs[0], tc.args.delegation) suite.Require().NoError(err) staking.EndBlocker(suite.ctx, suite.stakingKeeper) @@ -2407,7 +2411,7 @@ func (suite *KeeperTestSuite) TestSynchronizeHardDelegatorReward() { // After we've accumulated, run synchronize suite.Require().NotPanics(func() { - suite.keeper.SynchronizeHardDelegatorRewards(suite.ctx, suite.addrs[0]) + suite.keeper.SynchronizeHardDelegatorRewards(suite.ctx, suite.addrs[0], nil, false) }) // Check that reward factor and claim have been updated as expected @@ -2767,7 +2771,7 @@ func (suite *KeeperTestSuite) TestSimulateHardDelegatorRewardSynchronization() { // 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) + err = suite.deliverMsgDelegate(suite.ctx, suite.addrs[0], suite.validatorAddrs[0], tc.args.delegation) suite.Require().NoError(err) staking.EndBlocker(suite.ctx, suite.stakingKeeper) @@ -2944,3 +2948,348 @@ func (suite *KeeperTestSuite) deliverMsgDelegate(ctx sdk.Context, delegator sdk. _, 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/types/expected_keepers.go b/x/incentive/types/expected_keepers.go index 6725e706..21adf68b 100644 --- a/x/incentive/types/expected_keepers.go +++ b/x/incentive/types/expected_keepers.go @@ -19,6 +19,7 @@ type SupplyKeeper interface { // StakingKeeper defines the expected staking keeper for module accounts type StakingKeeper interface { GetDelegatorDelegations(ctx sdk.Context, delegator sdk.AccAddress, maxRetrieve uint16) (delegations []stakingtypes.Delegation) + GetValidatorDelegations(ctx sdk.Context, valAddr sdk.ValAddress) (delegations []stakingtypes.Delegation) GetValidator(ctx sdk.Context, addr sdk.ValAddress) (validator stakingtypes.Validator, found bool) TotalBondedTokens(ctx sdk.Context) sdk.Int }