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 <denalimarsh@gmail.com>
Co-authored-by: karzak <kjydavis3@gmail.com>
This commit is contained in:
Ruaridh 2021-03-25 06:10:13 +00:00 committed by GitHub
parent bf15ec4f8d
commit 54c0793ced
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 440 additions and 33 deletions

View File

@ -61,8 +61,13 @@ func (k Keeper) AttemptKeeperLiquidation(ctx sdk.Context, keeper sdk.AccAddress,
return err return err
} }
deposit.Amount = sdk.NewCoins()
k.DeleteDeposit(ctx, deposit) k.DeleteDeposit(ctx, deposit)
k.AfterDepositModified(ctx, deposit)
borrow.Amount = sdk.NewCoins()
k.DeleteBorrow(ctx, borrow) k.DeleteBorrow(ctx, borrow)
k.AfterBorrowModified(ctx, borrow)
return nil return nil
} }

View File

@ -66,30 +66,69 @@ func (h Hooks) AfterBorrowModified(ctx sdk.Context, borrow hardtypes.Borrow) {
h.k.UpdateHardBorrowIndexDenoms(ctx, 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 // BeforeDelegationCreated runs before a delegation is created
func (h Hooks) BeforeDelegationCreated(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) { 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) h.k.InitializeHardDelegatorReward(ctx, delAddr)
} }
// BeforeDelegationSharesModified runs before an existing delegation is modified // BeforeDelegationSharesModified runs before an existing delegation is modified
func (h Hooks) BeforeDelegationSharesModified(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) { 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 // 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 // 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) { 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 // 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) { 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 // AfterDelegationModified runs after a delegation is modified
func (h Hooks) AfterDelegationModified(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) { func (h Hooks) AfterDelegationModified(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) {

View File

@ -209,20 +209,11 @@ func NewStakingGenesisState() app.GenesisState {
} }
func (suite *KeeperTestSuite) SetupWithGenState() { 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() tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
tApp.InitializeFromGenesisStates( tApp.InitializeFromGenesisStates(
NewAuthGenState(allAddrs, cs(c("ukava", 1_000_000_000))), NewAuthGenState(suite.getAllAddrs(), cs(c("ukava", 1_000_000_000))),
NewStakingGenesisState(), NewStakingGenesisState(),
NewPricefeedGenStateMulti(), NewPricefeedGenStateMulti(),
NewCDPGenStateMulti(), NewCDPGenStateMulti(),

View File

@ -38,17 +38,33 @@ type KeeperTestSuite struct {
// The default state used by each test // The default state used by each test
func (suite *KeeperTestSuite) SetupTest() { 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() tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
tApp.InitializeFromGenesisStates() tApp.InitializeFromGenesisStates()
_, addrs := app.GeneratePrivKeyAddressPairs(5) suite.keeper = tApp.GetIncentiveKeeper()
keeper := tApp.GetIncentiveKeeper()
suite.app = tApp suite.app = tApp
suite.ctx = ctx 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 { func (suite *KeeperTestSuite) getAccount(addr sdk.AccAddress) authexported.Account {

View File

@ -271,10 +271,7 @@ func (k Keeper) InitializeHardSupplyReward(ctx sdk.Context, deposit hardtypes.De
} }
claim, found := k.GetHardLiquidityProviderClaim(ctx, deposit.Depositor) claim, found := k.GetHardLiquidityProviderClaim(ctx, deposit.Depositor)
if found { if !found {
// Reset borrow reward indexes
claim.BorrowRewardIndexes = types.MultiRewardIndexes{}
} else {
// Instantiate claim object // Instantiate claim object
claim = types.NewHardLiquidityProviderClaim(deposit.Depositor, sdk.Coins{}, nil, nil, nil) 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) k.SetHardLiquidityProviderClaim(ctx, claim)
} }
// SynchronizeHardDelegatorRewards updates the claim object by adding any accumulated rewards // SynchronizeHardDelegatorRewards updates the claim object by adding any accumulated rewards, and setting the reward indexes to the global values.
func (k Keeper) SynchronizeHardDelegatorRewards(ctx sdk.Context, delegator sdk.AccAddress) { // 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) claim, found := k.GetHardLiquidityProviderClaim(ctx, delegator)
if !found { if !found {
return return
@ -529,10 +528,17 @@ func (k Keeper) SynchronizeHardDelegatorRewards(ctx sdk.Context, delegator sdk.A
continue continue
} }
// Delegators don't accumulate rewards if their validator is unbonded/slashed if valAddr == nil {
// Delegators don't accumulate rewards if their validator is unbonded
if validator.GetStatus() != sdk.Bonded { if validator.GetStatus() != sdk.Bonded {
continue continue
} }
} else {
if !shouldIncludeValidator && validator.OperatorAddress.Equals(valAddr) {
// ignore tokens delegated to the validator
continue
}
}
if validator.GetTokens().IsZero() { if validator.GetTokens().IsZero() {
continue continue
@ -601,7 +607,7 @@ func (k Keeper) InitializeHardDelegatorReward(ctx sdk.Context, delegator sdk.Acc
// Instantiate claim object // Instantiate claim object
claim = types.NewHardLiquidityProviderClaim(delegator, sdk.Coins{}, nil, nil, nil) claim = types.NewHardLiquidityProviderClaim(delegator, sdk.Coins{}, nil, nil, nil)
} else { } else {
k.SynchronizeHardDelegatorRewards(ctx, delegator) k.SynchronizeHardDelegatorRewards(ctx, delegator, nil, false)
claim, _ = k.GetHardLiquidityProviderClaim(ctx, delegator) claim, _ = k.GetHardLiquidityProviderClaim(ctx, delegator)
} }
@ -652,7 +658,7 @@ func (k Keeper) SynchronizeHardLiquidityProviderClaim(ctx sdk.Context, owner sdk
} }
// Synchronize any hard delegator rewards // 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 // ZeroHardLiquidityProviderClaim zeroes out the claim object's rewards and returns the updated claim object

View File

@ -16,6 +16,10 @@ import (
"github.com/kava-labs/kava/x/incentive/types" "github.com/kava-labs/kava/x/incentive/types"
) )
const (
oneYear time.Duration = time.Hour * 24 * 365
)
func (suite *KeeperTestSuite) TestAccumulateUSDXMintingRewards() { func (suite *KeeperTestSuite) TestAccumulateUSDXMintingRewards() {
type args struct { type args struct {
ctype string ctype string
@ -885,7 +889,7 @@ func (suite *KeeperTestSuite) TestAccumulateHardDelegatorRewards() {
err := suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[0], tc.args.delegation) err := suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[0], tc.args.delegation)
suite.Require().NoError(err) 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) suite.Require().NoError(err)
staking.EndBlocker(suite.ctx, suite.stakingKeeper) staking.EndBlocker(suite.ctx, suite.stakingKeeper)
@ -2407,7 +2411,7 @@ func (suite *KeeperTestSuite) TestSynchronizeHardDelegatorReward() {
// After we've accumulated, run synchronize // After we've accumulated, run synchronize
suite.Require().NotPanics(func() { 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 // Check that reward factor and claim have been updated as expected
@ -2767,7 +2771,7 @@ func (suite *KeeperTestSuite) TestSimulateHardDelegatorRewardSynchronization() {
// Delegator delegates // Delegator delegates
err := suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[0], tc.args.delegation) err := suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[0], tc.args.delegation)
suite.Require().NoError(err) 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) suite.Require().NoError(err)
staking.EndBlocker(suite.ctx, suite.stakingKeeper) staking.EndBlocker(suite.ctx, suite.stakingKeeper)
@ -2944,3 +2948,348 @@ func (suite *KeeperTestSuite) deliverMsgDelegate(ctx sdk.Context, delegator sdk.
_, err := handleStakingMsg(ctx, msg) _, err := handleStakingMsg(ctx, msg)
return err 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,
)
}

View File

@ -19,6 +19,7 @@ type SupplyKeeper interface {
// StakingKeeper defines the expected staking keeper for module accounts // StakingKeeper defines the expected staking keeper for module accounts
type StakingKeeper interface { type StakingKeeper interface {
GetDelegatorDelegations(ctx sdk.Context, delegator sdk.AccAddress, maxRetrieve uint16) (delegations []stakingtypes.Delegation) 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) GetValidator(ctx sdk.Context, addr sdk.ValAddress) (validator stakingtypes.Validator, found bool)
TotalBondedTokens(ctx sdk.Context) sdk.Int TotalBondedTokens(ctx sdk.Context) sdk.Int
} }