-
Notifications
You must be signed in to change notification settings - Fork 560
/
spread_rewards.go
331 lines (287 loc) · 15.5 KB
/
spread_rewards.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
package concentrated_liquidity
import (
"strconv"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/osmosis-labs/osmosis/osmoutils/accum"
"github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/types"
)
var emptyCoins = sdk.DecCoins(nil)
// createSpreadRewardAccumulator creates an accumulator object in the store using the given poolId.
// The accumulator is initialized with the default(zero) values.
func (k Keeper) createSpreadRewardAccumulator(ctx sdk.Context, poolId uint64) error {
err := accum.MakeAccumulator(ctx.KVStore(k.storeKey), types.KeySpreadRewardPoolAccumulator(poolId))
if err != nil {
return err
}
return nil
}
// GetSpreadRewardAccumulator gets the spread reward accumulator object using the given poolOd
// returns error if accumulator for the given poolId does not exist.
func (k Keeper) GetSpreadRewardAccumulator(ctx sdk.Context, poolId uint64) (accum.AccumulatorObject, error) {
acc, err := accum.GetAccumulator(ctx.KVStore(k.storeKey), types.KeySpreadRewardPoolAccumulator(poolId))
if err != nil {
return accum.AccumulatorObject{}, err
}
return acc, nil
}
// initOrUpdatePositionSpreadRewardAccumulator mutates the spread reward accumulator position by either creating or updating it
// for the given pool id in the range specified by the given lower and upper ticks, position id and liquidityDelta.
// If liquidityDelta is positive, it adds liquidity. If liquidityDelta is negative, it removes liquidity.
// If this is a new position, liqudityDelta must be positive.
// It checks if the position exists in the spread reward accumulator. If it does not exist, it creates a new position.
// If it exists, it updates the shares of the position's accumulator in the spread reward accumulator.
// Upon calling this method, the position's spread reward accumulator is equal to the spread reward growth inside the tick range.
// On update, the rewards are moved into the position's unclaimed rewards. See internal method comments for details.
//
// Returns nil on success. Returns error if:
// - fails to get an accumulator for a given pool id
// - fails to determine whether the positive with the given id exists in the accumulator.
// - fails to calculate spread reward growth outside of the tick range.
// - fails to create a new position.
// - fails to prepare the accumulator for update.
// - fails to update the position's accumulator.
func (k Keeper) initOrUpdatePositionSpreadRewardAccumulator(ctx sdk.Context, poolId uint64, lowerTick, upperTick int64, positionId uint64, liquidityDelta sdk.Dec) error {
// Get the spread reward accumulator for the position's pool.
spreadRewardAccumulator, err := k.GetSpreadRewardAccumulator(ctx, poolId)
if err != nil {
return err
}
// Get the key for the position's accumulator in the spread reward accumulator.
positionKey := types.KeySpreadRewardPositionAccumulator(positionId)
hasPosition, err := spreadRewardAccumulator.HasPosition(positionKey)
if err != nil {
return err
}
spreadRewardGrowthOutside, err := k.getSpreadRewardGrowthOutside(ctx, poolId, lowerTick, upperTick)
if err != nil {
return err
}
spreadRewardGrowthInside := spreadRewardAccumulator.GetValue().Sub(spreadRewardGrowthOutside)
if !hasPosition {
if !liquidityDelta.IsPositive() {
return types.NonPositiveLiquidityForNewPositionError{LiquidityDelta: liquidityDelta, PositionId: positionId}
}
// Initialize the position with the spread reward growth inside the tick range
if err := spreadRewardAccumulator.NewPositionIntervalAccumulation(positionKey, liquidityDelta, spreadRewardGrowthInside, nil); err != nil {
return err
}
} else {
// Replace the position's accumulator in the spread reward accumulator with a new one
// that has the latest spread reward growth outside of the tick range.
// Assume the last time the position was created or modified was at time t.
// At time t, we track spread reward growth inside from 0 to t.
// Then, the update happens at time t + 1. The call below makes the position's
// accumulator to be "spread reward growth inside from 0 to t + spread reward growth outside from 0 to t + 1".
err = updatePositionToInitValuePlusGrowthOutside(spreadRewardAccumulator, positionKey, spreadRewardGrowthOutside)
if err != nil {
return err
}
// Update the position's initialSpreadRewardAccumulatorValue in the spread reward accumulator with spread reward growth inside,
// taking into account the change in liquidity of the position.
// Prior to mutating the accumulator, it moves the accumulated rewards into the accumulator position's unclaimed rewards.
// The move happens by subtracting the "spread reward growth inside from 0 to t + spread reward growth outside from 0 to t + 1" from the global
// spread reward accumulator growth at time t + 1. This yields the "spread reward growth inside from t to t + 1". That is, the unclaimed spread reward growth
// from the last time the position was either modified or created.
err = spreadRewardAccumulator.UpdatePositionIntervalAccumulation(positionKey, liquidityDelta, spreadRewardGrowthInside)
if err != nil {
return err
}
}
return nil
}
// getSpreadRewardGrowthOutside returns the sum of spread reward growth above the upper tick and spread reward growth below the lower tick
// WARNING: this method may mutate the pool, make sure to refetch the pool after calling this method.
// Currently, the call to GetTickInfo() may mutate state.
func (k Keeper) getSpreadRewardGrowthOutside(ctx sdk.Context, poolId uint64, lowerTick, upperTick int64) (sdk.DecCoins, error) {
pool, err := k.getPoolById(ctx, poolId)
if err != nil {
return sdk.DecCoins{}, err
}
currentTick := pool.GetCurrentTick()
// get lower, upper tick info
lowerTickInfo, err := k.GetTickInfo(ctx, poolId, lowerTick)
if err != nil {
return sdk.DecCoins{}, err
}
upperTickInfo, err := k.GetTickInfo(ctx, poolId, upperTick)
if err != nil {
return sdk.DecCoins{}, err
}
poolSpreadRewardAccumulator, err := k.GetSpreadRewardAccumulator(ctx, poolId)
if err != nil {
return sdk.DecCoins{}, err
}
poolSpreadRewardGrowth := poolSpreadRewardAccumulator.GetValue()
spreadRewardGrowthAboveUpperTick := calculateSpreadRewardGrowth(upperTick, upperTickInfo.SpreadRewardGrowthOppositeDirectionOfLastTraversal, currentTick, poolSpreadRewardGrowth, true)
spreadRewardGrowthBelowLowerTick := calculateSpreadRewardGrowth(lowerTick, lowerTickInfo.SpreadRewardGrowthOppositeDirectionOfLastTraversal, currentTick, poolSpreadRewardGrowth, false)
return spreadRewardGrowthAboveUpperTick.Add(spreadRewardGrowthBelowLowerTick...), nil
}
// getInitialSpreadRewardGrowthOppositeDirectionOfLastTraversalForTick returns what the initial value of the spread reward growth opposite direction of last traversal field should be for a given tick.
// This value depends on the provided tick's location relative to the current tick. If the provided tick is greater than the current tick,
// then the value is zero. Otherwise, the value is the value of the current global spread reward growth.
//
// The value is chosen as if all of the spread rewards earned to date had occurred below the tick.
// Returns error if the pool with the given id does exist or if fails to get the spread reward accumulator.
func (k Keeper) getInitialSpreadRewardGrowthOppositeDirectionOfLastTraversalForTick(ctx sdk.Context, poolId uint64, tick int64) (sdk.DecCoins, error) {
pool, err := k.getPoolById(ctx, poolId)
if err != nil {
return sdk.DecCoins{}, err
}
currentTick := pool.GetCurrentTick()
if currentTick >= tick {
spreadRewardAccumulator, err := k.GetSpreadRewardAccumulator(ctx, poolId)
if err != nil {
return sdk.DecCoins{}, err
}
return spreadRewardAccumulator.GetValue(), nil
}
return emptyCoins, nil
}
// collectSpreadRewards collects the spread reward earned by a position and sends them to the owner's account.
// Returns error if the position with the given id does not exist or if fails to get the spread reward accumulator.
func (k Keeper) collectSpreadRewards(ctx sdk.Context, sender sdk.AccAddress, positionId uint64) (sdk.Coins, error) {
position, err := k.GetPosition(ctx, positionId)
if err != nil {
return sdk.Coins{}, err
}
// Spread reward collector must be the owner of the position.
isOwner, err := k.isPositionOwner(ctx, sender, position.PoolId, positionId)
if err != nil {
return sdk.Coins{}, err
}
if !isOwner {
return sdk.Coins{}, types.NotPositionOwnerError{Address: sender.String(), PositionId: positionId}
}
// Get the amount of spread rewards that the position is eligible to claim.
// This also mutates the internal state of the spread reward accumulator.
spreadRewardsClaimed, err := k.prepareClaimableSpreadRewards(ctx, positionId)
if err != nil {
return sdk.Coins{}, err
}
// Send the claimed spread rewards from the pool's address to the owner's address.
pool, err := k.getPoolById(ctx, position.PoolId)
if err != nil {
return sdk.Coins{}, err
}
if err := k.bankKeeper.SendCoins(ctx, pool.GetSpreadRewardsAddress(), sender, spreadRewardsClaimed); err != nil {
return sdk.Coins{}, err
}
// Emit an event for the spread rewards collected.
ctx.EventManager().EmitEvents(sdk.Events{
sdk.NewEvent(
types.TypeEvtCollectSpreadRewards,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(types.AttributeKeyPositionId, strconv.FormatUint(positionId, 10)),
sdk.NewAttribute(types.AttributeKeyTokensOut, spreadRewardsClaimed.String()),
),
})
return spreadRewardsClaimed, nil
}
// GetClaimableSpreadRewards returns the amount of spread rewards that a position is eligible to claim.
//
// Returns error if:
// - pool with the given id does not exist
// - position given by pool id, owner, lower tick and upper tick does not exist
// - other internal database or math errors.
func (k Keeper) GetClaimableSpreadRewards(ctx sdk.Context, positionId uint64) (sdk.Coins, error) {
// Since this is a query, we don't want to modify the state and therefore use a cache context.
cacheCtx, _ := ctx.CacheContext()
return k.prepareClaimableSpreadRewards(cacheCtx, positionId)
}
// prepareClaimableSpreadRewards returns the amount of spread rewards that a position is eligible to claim.
// Note that it mutates the internal state of the spread reward accumulator by setting the position's
// unclaimed rewards to zero and update the position's accumulator value to reflect the
// current pool spread reward accumulator value. If there is any dust left over, it is added back to the
// global accumulator as long as there are shares remaining in the accumulator. If not, the dust
// is ignored.
//
// Returns error if:
// - pool with the given id does not exist
// - position given by pool id, owner, lower tick and upper tick does not exist
// - other internal database or math errors.
func (k Keeper) prepareClaimableSpreadRewards(ctx sdk.Context, positionId uint64) (sdk.Coins, error) {
// Get the position with the given ID.
position, err := k.GetPosition(ctx, positionId)
if err != nil {
return nil, err
}
// Get the spread reward accumulator for the position's pool.
spreadRewardAccumulator, err := k.GetSpreadRewardAccumulator(ctx, position.PoolId)
if err != nil {
return nil, err
}
// Get the key for the position's accumulator in the spread reward accumulator.
positionKey := types.KeySpreadRewardPositionAccumulator(positionId)
// Check if the position exists in the spread reward accumulator.
hasPosition, err := spreadRewardAccumulator.HasPosition(positionKey)
if err != nil {
return nil, err
}
if !hasPosition {
return nil, types.SpreadRewardPositionNotFoundError{PositionId: positionId}
}
// Compute the spread reward growth outside of the range between the position's lower and upper ticks.
spreadRewardGrowthOutside, err := k.getSpreadRewardGrowthOutside(ctx, position.PoolId, position.LowerTick, position.UpperTick)
if err != nil {
return nil, err
}
// Claim rewards, set the unclaimed rewards to zero, and update the position's accumulator value to reflect the current accumulator value.
spreadRewardsClaimed, forfeitedDust, err := updateAccumAndClaimRewards(spreadRewardAccumulator, positionKey, spreadRewardGrowthOutside)
if err != nil {
return nil, err
}
// add foreited dust back to the global accumulator
if !forfeitedDust.IsZero() {
// Refetch the spread reward accumulator as the number of shares has changed after claiming.
spreadRewardAccumulator, err := k.GetSpreadRewardAccumulator(ctx, position.PoolId)
if err != nil {
return nil, err
}
totalSharesRemaining, err := spreadRewardAccumulator.GetTotalShares()
if err != nil {
return nil, err
}
// if there are no shares remaining, the dust is ignored. Otherwise, it is added back to the global accumulator.
// Total shares remaining can be zero if we claim in withdrawPosition for the last position in the pool.
// The shares are decremented in osmoutils/accum.ClaimRewards.
if !totalSharesRemaining.IsZero() {
forfeitedDustPerShare := forfeitedDust.QuoDecTruncate(totalSharesRemaining)
spreadRewardAccumulator.AddToAccumulator(forfeitedDustPerShare)
}
}
return spreadRewardsClaimed, nil
}
// calculateSpreadRewardGrowth above or below the given tick.
// If calculating spread reward growth for an upper tick, we consider the following two cases
// 1. currentTick >= upperTick: If current Tick is GTE than the upper Tick, the spread reward growth would be pool spread reward growth - uppertick's spread reward growth outside
// 2. currentTick < upperTick: If current tick is smaller than upper tick, spread reward growth would be the upper tick's spread reward growth outside
// this goes vice versa for calculating spread reward growth for lower tick.
func calculateSpreadRewardGrowth(targetTick int64, ticksSpreadRewardGrowthOppositeDirectionOfLastTraversal sdk.DecCoins, currentTick int64, spreadRewardsGrowthGlobal sdk.DecCoins, isUpperTick bool) sdk.DecCoins {
if (isUpperTick && currentTick >= targetTick) || (!isUpperTick && currentTick < targetTick) {
return spreadRewardsGrowthGlobal.Sub(ticksSpreadRewardGrowthOppositeDirectionOfLastTraversal)
}
return ticksSpreadRewardGrowthOppositeDirectionOfLastTraversal
}
// updatePositionToInitValuePlusGrowthOutside is called prior to updating unclaimed rewards,
// as we must set the position's accumulator value to the sum of
// - the spread reward/uptime growth inside at position creation time (position.InitAccumValue)
// - spread reward/uptime growth outside at the current block time (spreadRewardGrowthOutside/uptimeGrowthOutside)
func updatePositionToInitValuePlusGrowthOutside(accumulator accum.AccumulatorObject, positionKey string, growthOutside sdk.DecCoins) error {
position, err := accum.GetPosition(accumulator, positionKey)
if err != nil {
return err
}
// The reason for adding the growth outside to the position's initial accumulator value per share is as follows:
// - At any time in-between position updates or claiming, a position must have its AccumValuePerShare be equal to growth_inside_at_{last time of update}.
// - Prior to claiming (the logic below), the position's accumulator is updated to:
// growth_inside_at_{last time of update} + growth_outside_at_{current block time of update}
// - Then, during claiming in osmoutils.ClaimRewards, we perform the following computation:
// growth_global_at{current block time} - (growth_inside_at_{last time of update} + growth_outside_at_{current block time of update}})
// which ends up being equal to growth_inside_from_{last_time_of_update}_to_{current block time of update}}.
intervalAccumulationOutside := position.AccumValuePerShare.Add(growthOutside...)
err = accumulator.SetPositionIntervalAccumulation(positionKey, intervalAccumulationOutside)
if err != nil {
return err
}
return nil
}