forked from cosmos/cosmos-sdk
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathliquid_stake.go
443 lines (367 loc) · 17.5 KB
/
liquid_stake.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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
package keeper
import (
"time"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/staking/types"
)
// SetTotalLiquidStakedTokens stores the total outstanding tokens owned by a liquid staking provider
func (k Keeper) SetTotalLiquidStakedTokens(ctx sdk.Context, tokens math.Int) {
store := ctx.KVStore(k.storeKey)
tokensBz, err := tokens.Marshal()
if err != nil {
panic(err)
}
store.Set(types.TotalLiquidStakedTokensKey, tokensBz)
}
// GetTotalLiquidStakedTokens returns the total outstanding tokens owned by a liquid staking provider
// Returns zero if the total liquid stake amount has not been initialized
func (k Keeper) GetTotalLiquidStakedTokens(ctx sdk.Context) math.Int {
store := ctx.KVStore(k.storeKey)
tokensBz := store.Get(types.TotalLiquidStakedTokensKey)
if tokensBz == nil {
return sdk.ZeroInt()
}
var tokens math.Int
if err := tokens.Unmarshal(tokensBz); err != nil {
panic(err)
}
return tokens
}
// Checks if an account associated with a given delegation is related to liquid staking
//
// This is determined by checking if the account has a 32-length address
// which will identify the following scenarios:
// - An account has tokenized their shares, and thus the delegation is
// owned by the tokenize share record module account
// - A liquid staking provider is delegating through an ICA account
//
// Both ICA accounts and tokenize share record module accounts have 32-length addresses
// NOTE: This will have to be refactored before adapting it to chains beyond gaia
// as other chains may have 32-length addresses that are not related to the above scenarios
func (k Keeper) DelegatorIsLiquidStaker(delegatorAddress sdk.AccAddress) bool {
return len(delegatorAddress) == 32
}
// CheckExceedsGlobalLiquidStakingCap checks if a liquid delegation would cause the
// global liquid staking cap to be exceeded
// A liquid delegation is defined as either tokenized shares, or a delegation from an ICA Account
// The total stake is determined by the balance of the bonded pool
// If the delegation's shares are already bonded (e.g. in the event of a tokenized share)
// the tokens are already included in the bonded pool
// If the delegation's shares are not bonded (e.g. normal delegation),
// we need to add the tokens to the current bonded pool balance to get the total staked
func (k Keeper) CheckExceedsGlobalLiquidStakingCap(ctx sdk.Context, tokens math.Int, sharesAlreadyBonded bool) bool {
liquidStakingCap := k.GlobalLiquidStakingCap(ctx)
liquidStakedAmount := k.GetTotalLiquidStakedTokens(ctx)
// Determine the total stake from the balance of the bonded pool
// If this is not a tokenized delegation, we need to add the tokens to the pool balance since
// they would not have been counted yet
// If this is for a tokenized delegation, the tokens are already included in the pool balance
totalStakedAmount := k.TotalBondedTokens(ctx)
if !sharesAlreadyBonded {
totalStakedAmount = totalStakedAmount.Add(tokens)
}
// Calculate the percentage of stake that is liquid
updatedLiquidStaked := sdk.NewDecFromInt(liquidStakedAmount.Add(tokens))
liquidStakePercent := updatedLiquidStaked.Quo(sdk.NewDecFromInt(totalStakedAmount))
return liquidStakePercent.GT(liquidStakingCap)
}
// CheckExceedsValidatorBondCap checks if a liquid delegation to a validator would cause
// the liquid shares to exceed the validator bond factor
// A liquid delegation is defined as either tokenized shares, or a delegation from an ICA Account
// Returns true if the cap is exceeded
func (k Keeper) CheckExceedsValidatorBondCap(ctx sdk.Context, validator types.Validator, shares sdk.Dec) bool {
validatorBondFactor := k.ValidatorBondFactor(ctx)
if validatorBondFactor.Equal(types.ValidatorBondCapDisabled) {
return false
}
maxValLiquidShares := validator.ValidatorBondShares.Mul(validatorBondFactor)
return validator.LiquidShares.Add(shares).GT(maxValLiquidShares)
}
// CheckExceedsValidatorLiquidStakingCap checks if a liquid delegation could cause the
// total liquid shares to exceed the liquid staking cap
// A liquid delegation is defined as either tokenized shares, or a delegation from an ICA Account
// If the liquid delegation's shares are already bonded (e.g. in the event of a tokenized share)
// the tokens are already included in the validator's delegator shares
// If the liquid delegation's shares are not bonded (e.g. normal delegation),
// we need to add the shares to the current validator's delegator shares to get the total shares
// Returns true if the cap is exceeded
func (k Keeper) CheckExceedsValidatorLiquidStakingCap(ctx sdk.Context, validator types.Validator, shares sdk.Dec, sharesAlreadyBonded bool) bool {
updatedLiquidShares := validator.LiquidShares.Add(shares)
updatedTotalShares := validator.DelegatorShares
if !sharesAlreadyBonded {
updatedTotalShares = updatedTotalShares.Add(shares)
}
liquidStakePercent := updatedLiquidShares.Quo(updatedTotalShares)
liquidStakingCap := k.ValidatorLiquidStakingCap(ctx)
return liquidStakePercent.GT(liquidStakingCap)
}
// SafelyIncreaseTotalLiquidStakedTokens increments the total liquid staked tokens
// if the global cap is not surpassed by this delegation
//
// The percentage of liquid staked tokens must be less than the GlobalLiquidStakingCap:
// (TotalLiquidStakedTokens / TotalStakedTokens) <= GlobalLiquidStakingCap
func (k Keeper) SafelyIncreaseTotalLiquidStakedTokens(ctx sdk.Context, amount math.Int, sharesAlreadyBonded bool) error {
if k.CheckExceedsGlobalLiquidStakingCap(ctx, amount, sharesAlreadyBonded) {
return types.ErrGlobalLiquidStakingCapExceeded
}
k.SetTotalLiquidStakedTokens(ctx, k.GetTotalLiquidStakedTokens(ctx).Add(amount))
return nil
}
// DecreaseTotalLiquidStakedTokens decrements the total liquid staked tokens
func (k Keeper) DecreaseTotalLiquidStakedTokens(ctx sdk.Context, amount math.Int) error {
totalLiquidStake := k.GetTotalLiquidStakedTokens(ctx)
if amount.GT(totalLiquidStake) {
return types.ErrTotalLiquidStakedUnderflow
}
k.SetTotalLiquidStakedTokens(ctx, totalLiquidStake.Sub(amount))
return nil
}
// SafelyIncreaseValidatorLiquidShares increments the liquid shares on a validator, if:
// the validator bond factor and validator liquid staking cap will not be exceeded by this delegation
//
// The percentage of validator liquid shares must be less than the ValidatorLiquidStakingCap,
// and the total liquid staked shares cannot exceed the validator bond cap
// 1. (TotalLiquidStakedTokens / TotalStakedTokens) <= ValidatorLiquidStakingCap
// 2. LiquidShares <= (ValidatorBondShares * ValidatorBondFactor)
func (k Keeper) SafelyIncreaseValidatorLiquidShares(ctx sdk.Context, valAddress sdk.ValAddress, shares sdk.Dec, sharesAlreadyBonded bool) (types.Validator, error) {
validator, found := k.GetValidator(ctx, valAddress)
if !found {
return validator, types.ErrNoValidatorFound
}
// Confirm the validator bond factor and validator liquid staking cap will not be exceeded
if k.CheckExceedsValidatorBondCap(ctx, validator, shares) {
return validator, types.ErrInsufficientValidatorBondShares
}
if k.CheckExceedsValidatorLiquidStakingCap(ctx, validator, shares, false) {
return validator, types.ErrValidatorLiquidStakingCapExceeded
}
// Increment the validator's liquid shares
validator.LiquidShares = validator.LiquidShares.Add(shares)
k.SetValidator(ctx, validator)
return validator, nil
}
// DecreaseValidatorLiquidShares decrements the liquid shares on a validator
func (k Keeper) DecreaseValidatorLiquidShares(ctx sdk.Context, valAddress sdk.ValAddress, shares sdk.Dec) (types.Validator, error) {
validator, found := k.GetValidator(ctx, valAddress)
if !found {
return validator, types.ErrNoValidatorFound
}
if shares.GT(validator.LiquidShares) {
return validator, types.ErrValidatorLiquidSharesUnderflow
}
validator.LiquidShares = validator.LiquidShares.Sub(shares)
k.SetValidator(ctx, validator)
return validator, nil
}
// Increase validator bond shares increments the validator's self bond
// in the event that the delegation amount on a validator bond delegation is increased
func (k Keeper) IncreaseValidatorBondShares(ctx sdk.Context, valAddress sdk.ValAddress, shares sdk.Dec) error {
validator, found := k.GetValidator(ctx, valAddress)
if !found {
return types.ErrNoValidatorFound
}
validator.ValidatorBondShares = validator.ValidatorBondShares.Add(shares)
k.SetValidator(ctx, validator)
return nil
}
// SafelyDecreaseValidatorBond decrements the validator's self bond
// so long as it will not cause the current delegations to exceed the threshold
// set by validator bond factor
func (k Keeper) SafelyDecreaseValidatorBond(ctx sdk.Context, valAddress sdk.ValAddress, shares sdk.Dec) error {
validator, found := k.GetValidator(ctx, valAddress)
if !found {
return types.ErrNoValidatorFound
}
// Check if the decreased self bond will cause the validator bond threshold to be exceeded
validatorBondFactor := k.ValidatorBondFactor(ctx)
validatorBondEnabled := !validatorBondFactor.Equal(types.ValidatorBondCapDisabled)
maxValTotalShare := validator.ValidatorBondShares.Sub(shares).Mul(validatorBondFactor)
if validatorBondEnabled && validator.LiquidShares.GT(maxValTotalShare) {
return types.ErrInsufficientValidatorBondShares
}
// Decrement the validator's self bond
validator.ValidatorBondShares = validator.ValidatorBondShares.Sub(shares)
k.SetValidator(ctx, validator)
return nil
}
// Adds a lock that prevents tokenizing shares for an account
// The tokenize share lock store is implemented by keying on the account address
// and storing a timestamp as the value. The timestamp is empty when the lock is
// set and gets populated with the unlock completion time once the unlock has started
func (k Keeper) AddTokenizeSharesLock(ctx sdk.Context, address sdk.AccAddress) {
store := ctx.KVStore(k.storeKey)
key := types.GetTokenizeSharesLockKey(address)
store.Set(key, sdk.FormatTimeBytes(time.Time{}))
}
// Removes the tokenize share lock for an account to enable tokenizing shares
func (k Keeper) RemoveTokenizeSharesLock(ctx sdk.Context, address sdk.AccAddress) {
store := ctx.KVStore(k.storeKey)
key := types.GetTokenizeSharesLockKey(address)
store.Delete(key)
}
// Updates the timestamp associated with a lock to the time at which the lock expires
func (k Keeper) SetTokenizeSharesUnlockTime(ctx sdk.Context, address sdk.AccAddress, completionTime time.Time) {
store := ctx.KVStore(k.storeKey)
key := types.GetTokenizeSharesLockKey(address)
store.Set(key, sdk.FormatTimeBytes(completionTime))
}
// Checks if there is currently a tokenize share lock for a given account
// Returns the status indicating whether the account is locked, unlocked,
// or as a lock expiring. If the lock is expiring, the expiration time is returned
func (k Keeper) GetTokenizeSharesLock(ctx sdk.Context, address sdk.AccAddress) (status types.TokenizeShareLockStatus, unlockTime time.Time) {
store := ctx.KVStore(k.storeKey)
key := types.GetTokenizeSharesLockKey(address)
bz := store.Get(key)
if len(bz) == 0 {
return types.TOKENIZE_SHARE_LOCK_STATUS_UNLOCKED, time.Time{}
}
unlockTime, err := sdk.ParseTimeBytes(bz)
if err != nil {
panic(err)
}
if unlockTime.IsZero() {
return types.TOKENIZE_SHARE_LOCK_STATUS_LOCKED, time.Time{}
}
return types.TOKENIZE_SHARE_LOCK_STATUS_LOCK_EXPIRING, unlockTime
}
// Returns all tokenize share locks
func (k Keeper) GetAllTokenizeSharesLocks(ctx sdk.Context) (tokenizeShareLocks []types.TokenizeShareLock) {
store := ctx.KVStore(k.storeKey)
iterator := sdk.KVStorePrefixIterator(store, types.TokenizeSharesLockPrefix)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
addressBz := iterator.Key()[2:] // remove prefix bytes and address length
unlockTime, err := sdk.ParseTimeBytes(iterator.Value())
if err != nil {
panic(err)
}
var status types.TokenizeShareLockStatus
if unlockTime.IsZero() {
status = types.TOKENIZE_SHARE_LOCK_STATUS_LOCKED
} else {
status = types.TOKENIZE_SHARE_LOCK_STATUS_LOCK_EXPIRING
}
bechPrefix := sdk.GetConfig().GetBech32AccountAddrPrefix()
lock := types.TokenizeShareLock{
Address: sdk.MustBech32ifyAddressBytes(bechPrefix, addressBz),
Status: status.String(),
CompletionTime: unlockTime,
}
tokenizeShareLocks = append(tokenizeShareLocks, lock)
}
return tokenizeShareLocks
}
// Stores a list of addresses pending tokenize share unlocking at the same time
func (k Keeper) SetPendingTokenizeShareAuthorizations(ctx sdk.Context, completionTime time.Time, authorizations types.PendingTokenizeShareAuthorizations) {
store := ctx.KVStore(k.storeKey)
timeKey := types.GetTokenizeShareAuthorizationTimeKey(completionTime)
bz := k.cdc.MustMarshal(&authorizations)
store.Set(timeKey, bz)
}
// Returns a list of addresses pending tokenize share unlocking at the same time
func (k Keeper) GetPendingTokenizeShareAuthorizations(ctx sdk.Context, completionTime time.Time) types.PendingTokenizeShareAuthorizations {
store := ctx.KVStore(k.storeKey)
timeKey := types.GetTokenizeShareAuthorizationTimeKey(completionTime)
bz := store.Get(timeKey)
authorizations := types.PendingTokenizeShareAuthorizations{Addresses: []string{}}
if len(bz) == 0 {
return authorizations
}
k.cdc.MustUnmarshal(bz, &authorizations)
return authorizations
}
// Inserts the address into a queue where it will sit for 1 unbonding period
// before the tokenize share lock is removed
// Returns the completion time
func (k Keeper) QueueTokenizeSharesAuthorization(ctx sdk.Context, address sdk.AccAddress) time.Time {
params := k.GetParams(ctx)
completionTime := ctx.BlockTime().Add(params.UnbondingTime)
// Append the address to the list of addresses that also unlock at this time
authorizations := k.GetPendingTokenizeShareAuthorizations(ctx, completionTime)
authorizations.Addresses = append(authorizations.Addresses, address.String())
k.SetPendingTokenizeShareAuthorizations(ctx, completionTime, authorizations)
k.SetTokenizeSharesUnlockTime(ctx, address, completionTime)
return completionTime
}
// Cancels a pending tokenize share authorization by removing the lock from the queue
func (k Keeper) CancelTokenizeShareLockExpiration(ctx sdk.Context, address sdk.AccAddress, completionTime time.Time) {
authorizations := k.GetPendingTokenizeShareAuthorizations(ctx, completionTime)
updatedAddresses := []string{}
for _, expiringAddress := range authorizations.Addresses {
if address.String() != expiringAddress {
updatedAddresses = append(updatedAddresses, expiringAddress)
}
}
authorizations.Addresses = updatedAddresses
k.SetPendingTokenizeShareAuthorizations(ctx, completionTime, authorizations)
}
// Unlocks all queued tokenize share authorizations that have matured
// (i.e. have waited the full unbonding period)
func (k Keeper) RemoveExpiredTokenizeShareLocks(ctx sdk.Context, blockTime time.Time) (unlockedAddresses []string) {
store := ctx.KVStore(k.storeKey)
// iterators all time slices from time 0 until the current block time
prefixEnd := sdk.InclusiveEndBytes(types.GetTokenizeShareAuthorizationTimeKey(blockTime))
iterator := store.Iterator(types.TokenizeSharesUnlockQueuePrefix, prefixEnd)
defer iterator.Close()
// collect all unlocked addresses
unlockedAddresses = []string{}
keys := [][]byte{}
for ; iterator.Valid(); iterator.Next() {
authorizations := types.PendingTokenizeShareAuthorizations{}
k.cdc.MustUnmarshal(iterator.Value(), &authorizations)
unlockedAddresses = append(unlockedAddresses, authorizations.Addresses...)
keys = append(keys, iterator.Key())
}
// delete unlocked addresses keys
for _, k := range keys {
store.Delete(k)
}
// remove the lock from each unlocked address
for _, unlockedAddress := range unlockedAddresses {
k.RemoveTokenizeSharesLock(ctx, sdk.MustAccAddressFromBech32(unlockedAddress))
}
return unlockedAddresses
}
// Calculates and sets the global liquid staked tokens and liquid shares by validator
// The totals are determined by looping each delegation record and summing the stake
// if the delegator has a 32-length address. Checking for a 32-length address will capture
// ICA accounts, as well as tokenized delegations which are owned by module accounts
// under the hood
// This function must be called in the upgrade handler which onboards LSM
func (k Keeper) RefreshTotalLiquidStaked(ctx sdk.Context) error {
// First reset each validator's liquid shares to 0
for _, validator := range k.GetAllValidators(ctx) {
validator.LiquidShares = sdk.ZeroDec()
k.SetValidator(ctx, validator)
}
// Sum up the total liquid tokens and increment each validator's liquid shares
totalLiquidStakedTokens := sdk.ZeroInt()
for _, delegation := range k.GetAllDelegations(ctx) {
delegatorAddress, err := sdk.AccAddressFromBech32(delegation.DelegatorAddress)
if err != nil {
return err
}
// If the delegator is either an ICA account or a tokenize share module account,
// the delegation should be considered to be associated with liquid staking
// Consequently, the global number of liquid staked tokens, and the total
// liquid shares on the validator should be incremented
if k.DelegatorIsLiquidStaker(delegatorAddress) {
validatorAddress, err := sdk.ValAddressFromBech32(delegation.ValidatorAddress)
if err != nil {
return err
}
validator, found := k.GetValidator(ctx, validatorAddress)
if !found {
return types.ErrNoValidatorFound
}
liquidShares := delegation.Shares
liquidTokens := validator.TokensFromShares(liquidShares).TruncateInt()
validator.LiquidShares = validator.LiquidShares.Add(liquidShares)
k.SetValidator(ctx, validator)
totalLiquidStakedTokens = totalLiquidStakedTokens.Add(liquidTokens)
}
}
k.SetTotalLiquidStakedTokens(ctx, totalLiquidStakedTokens)
return nil
}