-
Notifications
You must be signed in to change notification settings - Fork 3.7k
/
delegation.go
214 lines (184 loc) · 8.64 KB
/
delegation.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
package keeper
import (
"fmt"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/distribution/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
)
// initialize starting info for a new delegation
func (k Keeper) initializeDelegation(ctx sdk.Context, val sdk.ValAddress, del sdk.AccAddress) {
// period has already been incremented - we want to store the period ended by this delegation action
previousPeriod := k.GetValidatorCurrentRewards(ctx, val).Period - 1
// increment reference count for the period we're going to track
k.incrementReferenceCount(ctx, val, previousPeriod)
validator := k.stakingKeeper.Validator(ctx, val)
delegation := k.stakingKeeper.Delegation(ctx, del, val)
// calculate delegation stake in tokens
// we don't store directly, so multiply delegation shares * (tokens per share)
// note: necessary to truncate so we don't allow withdrawing more rewards than owed
stake := validator.TokensFromSharesTruncated(delegation.GetShares())
k.SetDelegatorStartingInfo(ctx, val, del, types.NewDelegatorStartingInfo(previousPeriod, stake, uint64(ctx.BlockHeight())))
}
// calculate the rewards accrued by a delegation between two periods
func (k Keeper) calculateDelegationRewardsBetween(ctx sdk.Context, val stakingtypes.ValidatorI,
startingPeriod, endingPeriod uint64, stake sdk.Dec,
) (rewards sdk.DecCoins) {
// sanity check
if startingPeriod > endingPeriod {
panic("startingPeriod cannot be greater than endingPeriod")
}
// sanity check
if stake.IsNegative() {
panic("stake should not be negative")
}
// return staking * (ending - starting)
starting := k.GetValidatorHistoricalRewards(ctx, val.GetOperator(), startingPeriod)
ending := k.GetValidatorHistoricalRewards(ctx, val.GetOperator(), endingPeriod)
difference := ending.CumulativeRewardRatio.Sub(starting.CumulativeRewardRatio)
if difference.IsAnyNegative() {
panic("negative rewards should not be possible")
}
// note: necessary to truncate so we don't allow withdrawing more rewards than owed
rewards = difference.MulDecTruncate(stake)
return
}
// calculate the total rewards accrued by a delegation
func (k Keeper) CalculateDelegationRewards(ctx sdk.Context, val stakingtypes.ValidatorI, del stakingtypes.DelegationI, endingPeriod uint64) (rewards sdk.DecCoins) {
// fetch starting info for delegation
startingInfo := k.GetDelegatorStartingInfo(ctx, del.GetValidatorAddr(), del.GetDelegatorAddr())
if startingInfo.Height == uint64(ctx.BlockHeight()) {
// started this height, no rewards yet
return
}
startingPeriod := startingInfo.PreviousPeriod
stake := startingInfo.Stake
// Iterate through slashes and withdraw with calculated staking for
// distribution periods. These period offsets are dependent on *when* slashes
// happen - namely, in BeginBlock, after rewards are allocated...
// Slashes which happened in the first block would have been before this
// delegation existed, UNLESS they were slashes of a redelegation to this
// validator which was itself slashed (from a fault committed by the
// redelegation source validator) earlier in the same BeginBlock.
startingHeight := startingInfo.Height
// Slashes this block happened after reward allocation, but we have to account
// for them for the stake sanity check below.
endingHeight := uint64(ctx.BlockHeight())
if endingHeight > startingHeight {
k.IterateValidatorSlashEventsBetween(ctx, del.GetValidatorAddr(), startingHeight, endingHeight,
func(height uint64, event types.ValidatorSlashEvent) (stop bool) {
endingPeriod := event.ValidatorPeriod
if endingPeriod > startingPeriod {
rewards = rewards.Add(k.calculateDelegationRewardsBetween(ctx, val, startingPeriod, endingPeriod, stake)...)
// Note: It is necessary to truncate so we don't allow withdrawing
// more rewards than owed.
stake = stake.MulTruncate(math.LegacyOneDec().Sub(event.Fraction))
startingPeriod = endingPeriod
}
return false
},
)
}
// A total stake sanity check; Recalculated final stake should be less than or
// equal to current stake here. We cannot use Equals because stake is truncated
// when multiplied by slash fractions (see above). We could only use equals if
// we had arbitrary-precision rationals.
currentStake := val.TokensFromShares(del.GetShares())
if stake.GT(currentStake) {
// AccountI for rounding inconsistencies between:
//
// currentStake: calculated as in staking with a single computation
// stake: calculated as an accumulation of stake
// calculations across validator's distribution periods
//
// These inconsistencies are due to differing order of operations which
// will inevitably have different accumulated rounding and may lead to
// the smallest decimal place being one greater in stake than
// currentStake. When we calculated slashing by period, even if we
// round down for each slash fraction, it's possible due to how much is
// being rounded that we slash less when slashing by period instead of
// for when we slash without periods. In other words, the single slash,
// and the slashing by period could both be rounding down but the
// slashing by period is simply rounding down less, thus making stake >
// currentStake
//
// A small amount of this error is tolerated and corrected for,
// however any greater amount should be considered a breach in expected
// behaviour.
marginOfErr := sdk.SmallestDec().MulInt64(3)
if stake.LTE(currentStake.Add(marginOfErr)) {
stake = currentStake
} else {
panic(fmt.Sprintf("calculated final stake for delegator %s greater than current stake"+
"\n\tfinal stake:\t%s"+
"\n\tcurrent stake:\t%s",
del.GetDelegatorAddr(), stake, currentStake))
}
}
// calculate rewards for final period
rewards = rewards.Add(k.calculateDelegationRewardsBetween(ctx, val, startingPeriod, endingPeriod, stake)...)
return rewards
}
func (k Keeper) withdrawDelegationRewards(ctx sdk.Context, val stakingtypes.ValidatorI, del stakingtypes.DelegationI) (sdk.Coins, error) {
// check existence of delegator starting info
if !k.HasDelegatorStartingInfo(ctx, del.GetValidatorAddr(), del.GetDelegatorAddr()) {
return nil, types.ErrEmptyDelegationDistInfo
}
// end current period and calculate rewards
endingPeriod := k.IncrementValidatorPeriod(ctx, val)
rewardsRaw := k.CalculateDelegationRewards(ctx, val, del, endingPeriod)
outstanding := k.GetValidatorOutstandingRewardsCoins(ctx, del.GetValidatorAddr())
// defensive edge case may happen on the very final digits
// of the decCoins due to operation order of the distribution mechanism.
rewards := rewardsRaw.Intersect(outstanding)
if !rewards.IsEqual(rewardsRaw) {
logger := k.Logger(ctx)
logger.Info(
"rounding error withdrawing rewards from validator",
"delegator", del.GetDelegatorAddr().String(),
"validator", val.GetOperator().String(),
"got", rewards.String(),
"expected", rewardsRaw.String(),
)
}
// truncate reward dec coins, return remainder to community pool
finalRewards, remainder := rewards.TruncateDecimal()
// add coins to user account
if !finalRewards.IsZero() {
withdrawAddr := k.GetDelegatorWithdrawAddr(ctx, del.GetDelegatorAddr())
err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, withdrawAddr, finalRewards)
if err != nil {
return nil, err
}
}
// update the outstanding rewards and the community pool only if the
// transaction was successful
k.SetValidatorOutstandingRewards(ctx, del.GetValidatorAddr(), types.ValidatorOutstandingRewards{Rewards: outstanding.Sub(rewards)})
feePool := k.GetFeePool(ctx)
feePool.CommunityPool = feePool.CommunityPool.Add(remainder...)
k.SetFeePool(ctx, feePool)
// decrement reference count of starting period
startingInfo := k.GetDelegatorStartingInfo(ctx, del.GetValidatorAddr(), del.GetDelegatorAddr())
startingPeriod := startingInfo.PreviousPeriod
k.decrementReferenceCount(ctx, del.GetValidatorAddr(), startingPeriod)
// remove delegator starting info
k.DeleteDelegatorStartingInfo(ctx, del.GetValidatorAddr(), del.GetDelegatorAddr())
if finalRewards.IsZero() {
baseDenom, _ := sdk.GetBaseDenom()
if baseDenom == "" {
baseDenom = sdk.DefaultBondDenom
}
// Note, we do not call the NewCoins constructor as we do not want the zero
// coin removed.
finalRewards = sdk.Coins{sdk.NewCoin(baseDenom, math.ZeroInt())}
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeWithdrawRewards,
sdk.NewAttribute(sdk.AttributeKeyAmount, finalRewards.String()),
sdk.NewAttribute(types.AttributeKeyValidator, val.GetOperator().String()),
sdk.NewAttribute(types.AttributeKeyDelegator, del.GetDelegatorAddr().String()),
),
)
return finalRewards, nil
}