reward_calculation_pool_tier.gno
15.41 Kb · 494 lines
1package staker
2
3import (
4 "errors"
5
6 "gno.land/p/gnoswap/gnsmath/v1"
7 bptree "gno.land/p/nt/bptree/v0"
8
9 sr "gno.land/r/gnoswap/staker"
10)
11
12const (
13 AllTierCount = 4 // 0, 1, 2, 3
14 Tier1 = 1
15 Tier2 = 2
16 Tier3 = 3
17)
18
19// TierRatioFromCounts calculates the ratio distribution for each tier based on pool counts.
20//
21// Parameters:
22// - tier1Count (uint64): Number of pools in tier 1.
23// - tier2Count (uint64): Number of pools in tier 2.
24// - tier3Count (uint64): Number of pools in tier 3.
25//
26// Returns:
27// - TierRatio: The ratio distribution across tier 1, 2, and 3, scaled up by 100.
28func TierRatioFromCounts(tier1Count, tier2Count, tier3Count uint64) sr.TierRatio {
29 // tier1 always exists.
30 //
31 // TierRatio is declared in /r/gnoswap/staker; constructing it via a
32 // composite literal here (/r/gnoswap/staker/v1) trips the construction-time
33 // check ("cannot allocate ... in realm ..."). Route through the domain
34 // constructor sr.NewTierRatio so allocation happens in the declaring realm.
35 if tier2Count == 0 && tier3Count == 0 {
36 return sr.NewTierRatio(100, 0, 0)
37 }
38 if tier2Count == 0 {
39 return sr.NewTierRatio(80, 0, 20)
40 }
41 if tier3Count == 0 {
42 return sr.NewTierRatio(70, 30, 0)
43 }
44 return sr.NewTierRatio(50, 30, 20)
45}
46
47// PoolTier manages pool counts, ratios, and rewards for different tiers.
48//
49// Fields:
50// - membership: Tracks which tier a pool belongs to (poolPath -> blockNumber -> tier).
51//
52// Methods:
53// - CurrentCount: Returns the current count of pools in a tier at a specific timestamp.
54// - CurrentRatio: Returns the current ratio for a tier at a specific timestamp.
55// - CurrentTier: Returns the tier of a specific pool at a given timestamp.
56// - CurrentReward: Retrieves the reward for a tier at a specific timestamp.
57// - changeTier: Updates the tier of a pool and recalculates ratios.
58type PoolTier struct {
59 membership *bptree.BPTree // poolPath -> tier(1, 2, 3)
60
61 tierRatio sr.TierRatio
62
63 counts [AllTierCount]uint64
64
65 lastRewardCacheTimestamp int64
66
67 currentEmission int64
68
69 // returns current emission.
70 getEmission func() (int64, error)
71 // Returns a list of halving timestamps and their emission amounts within the interval [start, end) in ascending order.
72 // The first return value is a list of timestamps where halving occurs.
73 // The second return value is a list of emission amounts corresponding to each halving timestamp.
74 getHalvingBlocksInRange func(start, end int64) ([]int64, []int64, error)
75}
76
77// NewPoolTier creates a new PoolTier instance with single initial 1 tier pool.
78//
79// Parameters:
80// - pools: The pool collection.
81// - currentTime: The current block time.
82// - initialPoolPath: The path of the initial pool.
83// - getEmission: A function that returns the current emission to the staker contract.
84// - getHalvingBlocksInRange: A function that returns a list of halving blocks within the interval [start, end) in ascending order.
85//
86// Returns:
87// - *PoolTier: The new PoolTier instance.
88func NewPoolTier(pools *Pools, currentTime int64, initialPoolPath string, getEmission func() (int64, error), getHalvingBlocksInRange func(start, end int64) ([]int64, []int64, error)) *PoolTier {
89 currentEmission, err := getEmission()
90 if err != nil {
91 panic(err)
92 }
93
94 result := &PoolTier{
95 membership: sr.NewBPTreeN(16),
96 tierRatio: TierRatioFromCounts(1, 0, 0),
97 lastRewardCacheTimestamp: gnsmath.SafeAddInt64(currentTime, 1),
98 getEmission: getEmission,
99 getHalvingBlocksInRange: getHalvingBlocksInRange,
100 currentEmission: currentEmission,
101 }
102
103 pools.set(initialPoolPath, sr.NewPool(initialPoolPath, currentTime+1))
104 result.changeTier(currentTime+1, pools, initialPoolPath, 1)
105 return result
106}
107
108// NewPoolTierBy reconstructs a PoolTier from persisted membership, ratios, counts, and callbacks.
109//
110// Parameters:
111// - membership: Persisted pool-path-to-tier mapping.
112// - tierRatio: Persisted reward-share ratio for each tier.
113// - counts: Persisted number of pools in each tier index.
114// - lastRewardCacheTimestamp: Timestamp through which reward caches have been materialized.
115// - currentEmission: Emission rate currently used for reward accrual.
116// - getEmission: Callback returning the current staker emission rate or an error.
117// - getHalvingBlocksInRange: Callback returning halving timestamps and corresponding emissions in [start, end).
118//
119// Returns:
120// - *PoolTier: Reconstructed tier manager using the supplied persisted state and callbacks.
121func NewPoolTierBy(
122 membership *bptree.BPTree,
123 tierRatio sr.TierRatio,
124 counts [AllTierCount]uint64,
125 lastRewardCacheTimestamp int64,
126 currentEmission int64,
127 getEmission func() (int64, error),
128 getHalvingBlocksInRange func(start, end int64) ([]int64, []int64, error),
129) *PoolTier {
130 return &PoolTier{
131 membership: membership,
132 tierRatio: tierRatio,
133 counts: counts,
134 lastRewardCacheTimestamp: lastRewardCacheTimestamp,
135 getEmission: getEmission,
136 getHalvingBlocksInRange: getHalvingBlocksInRange,
137 currentEmission: currentEmission,
138 }
139}
140
141// CurrentReward returns the current per-pool reward for the given tier.
142// Parameters:
143// - tier: Tier number whose current emission reward is requested.
144//
145// Returns:
146// - int64: Current per-pool reward for the tier, or zero when the calculation cannot produce a reward.
147// - error: Error from emission lookup, invalid-tier lookup, or reward calculation; nil when the reward calculation succeeds.
148func (self *PoolTier) CurrentReward(tier uint64) (int64, error) {
149 currentEmission, err := self.getEmission()
150 if err != nil {
151 return 0, err
152 }
153 tierRatio, err := self.tierRatio.Get(tier)
154 if err != nil {
155 return 0, makeErrorWithDetails(errInvalidPoolTier, err.Error())
156 }
157
158 tierRatioInt64 := int64(tierRatio)
159 count := int64(self.CurrentCount(tier))
160
161 return calculatePoolReward(currentEmission, tierRatioInt64, count)
162}
163
164// CurrentCount returns the current count of pools in the given tier.
165// Parameters:
166// - tier: Tier index whose pool count is requested; out-of-range indexes return zero.
167//
168// Returns:
169// - int: Number of pools currently assigned to tier.
170func (self *PoolTier) CurrentCount(tier uint64) int {
171 if tier >= AllTierCount {
172 return 0
173 }
174 return int(self.counts[tier])
175}
176
177// CurrentAllTierCounts returns the current count of pools in each tier.
178// Returns:
179// - []uint64: Snapshot of pool counts for tier indexes 0 through AllTierCount-1.
180func (self *PoolTier) CurrentAllTierCounts() []uint64 {
181 out := make([]uint64, AllTierCount)
182 copy(out, self.counts[:])
183 return out // returning snapshot
184}
185
186// CurrentTier returns the tier of the given pool.
187// Parameters:
188// - poolPath: Pool path whose current tier membership is requested.
189//
190// Returns:
191// - tier: Current tier number, or zero when the pool is not in the membership tree.
192func (self *PoolTier) CurrentTier(poolPath string) (tier uint64) {
193 if tierI := self.membership.Get(poolPath); tierI == nil {
194 return 0
195 } else {
196 var ok bool
197 tier, ok = tierI.(uint64)
198 if !ok {
199 panic("failed to cast tier to uint64")
200 }
201 return tier
202 }
203}
204
205// changeTier updates the tier of a pool, recalculates ratios, and applies
206// updated per-pool reward to each of the pools.
207func (self *PoolTier) changeTier(currentTime int64, pools *Pools, poolPath string, nextTier uint64) map[uint64]int64 {
208 currentTier := self.CurrentTier(poolPath)
209 if currentTier == nextTier {
210 // no change, return
211 return make(map[uint64]int64)
212 }
213 assertTier1HasSparePool(currentTier, self.counts[Tier1])
214
215 self.cacheReward(currentTime, pools)
216
217 // decrement count from current tier if it exists
218 if currentTier > 0 {
219 if self.counts[currentTier] == 0 {
220 panic("counts underflow: removing from empty tier")
221 }
222 self.counts[currentTier]--
223 }
224
225 if nextTier == 0 {
226 // removed from the tier
227 self.membership.Remove(poolPath)
228 pool, ok := pools.Get(poolPath)
229 if !ok {
230 panic("changeTier: pool not found")
231 }
232 poolResolver := NewPoolResolver(pool)
233 // prevent new rewards from accumulating after tier removal
234 poolResolver.cacheReward(currentTime, 0)
235 } else {
236 // handle all move/add operations
237 self.membership.Set(poolPath, nextTier)
238 self.counts[nextTier]++
239 }
240
241 self.tierRatio = TierRatioFromCounts(self.counts[Tier1], self.counts[Tier2], self.counts[Tier3])
242 currentEmission, err := self.getEmission()
243 if err != nil {
244 panic(err)
245 }
246 tierRewards := self.computeTierRewards(currentEmission)
247
248 // Cache updated reward for each tiered pool
249 self.membership.Iterate("", "", func(key string, value any) bool {
250 pool, ok := pools.Get(key)
251 if !ok {
252 panic("changeTier: pool not found")
253 }
254 tier, ok := value.(uint64)
255 if !ok {
256 panic("failed to cast value to uint64")
257 }
258
259 poolReward, ok := tierRewards[tier]
260 if !ok {
261 return false // Skip if no pools in tier
262 }
263
264 poolResolver := NewPoolResolver(pool)
265 poolResolver.cacheReward(currentTime, poolReward)
266 return false
267 })
268
269 self.currentEmission = currentEmission
270
271 return tierRewards
272}
273
274// cacheReward MUST be called before calculating any position reward.
275// cacheReward updates the reward cache for each pool, accounting for any halving events
276// that occurred between the last cached timestamp and the current timestamp.
277// Note: Block height is used only for event tracking purposes.
278// tierContextOf returns the pool's tier and the ratio/count setting its current emission rate.
279func (self *PoolTier) tierContextOf(poolPath string) (uint64, uint64, uint64, error) {
280 tier := self.CurrentTier(poolPath)
281 if tier == 0 || tier >= AllTierCount {
282 return tier, 0, 0, nil
283 }
284
285 ratio, err := self.tierRatio.Get(tier)
286 if err != nil {
287 return 0, 0, 0, makeErrorWithDetails(errInvalidPoolTier, err.Error())
288 }
289
290 return tier, ratio, self.counts[tier], nil
291}
292
293func (self *PoolTier) cacheReward(currentTimestamp int64, pools *Pools) {
294 lastTimestamp := self.lastRewardCacheTimestamp
295
296 if currentTimestamp <= lastTimestamp {
297 // no need to check
298 return
299 }
300
301 // find halving blocks in range
302 halvingTimestamps, halvingEmissions, err := self.getHalvingBlocksInRange(lastTimestamp, currentTimestamp)
303 if err != nil {
304 panic(err)
305 }
306
307 if len(halvingTimestamps) == 0 {
308 self.applyCacheToAllPools(pools, currentTimestamp, self.currentEmission)
309 self.lastRewardCacheTimestamp = currentTimestamp
310 return
311 }
312
313 for i, hvTimestamp := range halvingTimestamps {
314 emission := halvingEmissions[i]
315 // caching: [lastTimestamp, hvTimestamp)
316 self.applyCacheToAllPools(pools, hvTimestamp, emission)
317
318 // halve emissions when halvingBlock is reached
319 self.currentEmission = emission
320 }
321
322 // remaining range [lastTimestamp, currentTimestamp)
323 self.applyCacheToAllPools(pools, currentTimestamp, self.currentEmission)
324
325 self.lastRewardCacheTimestamp = currentTimestamp
326}
327
328// cacheRewardForPool caches internal reward/accumulators for a single pool only.
329// This avoids iterating all tiered pools on every position reward calculation.
330func (self *PoolTier) cacheRewardForPool(currentTimestamp int64, pools *Pools, poolPath string) {
331 pool, ok := pools.Get(poolPath)
332 if !ok {
333 return
334 }
335
336 tierNum := self.CurrentTier(poolPath)
337 // Pool not in the internal-incentive system.
338 if tierNum == 0 {
339 return
340 }
341
342 // Find the latest reward cache timestamp for this pool.
343 lastTimestamp := int64(0)
344 hasLast := false
345 pool.RewardCache().ReverseIterate(0, currentTimestamp, func(key int64, _ any) bool {
346 lastTimestamp = key
347 hasLast = true
348 return true
349 })
350
351 if !hasLast {
352 // Fallback to global tier cache cursor.
353 lastTimestamp = self.lastRewardCacheTimestamp
354 }
355
356 if currentTimestamp <= lastTimestamp {
357 return
358 }
359
360 // Determine halving boundaries since the pool's last cached reward timestamp.
361 halvingTimestamps, halvingEmissions, err := self.getHalvingBlocksInRange(lastTimestamp, currentTimestamp)
362 if err != nil {
363 panic(err)
364 }
365 poolResolver := NewPoolResolver(pool)
366
367 if len(halvingTimestamps) == 0 {
368 // No emission change within the range => use the live emission.
369 currentEmission, err := self.getEmission()
370 if err != nil {
371 panic(err)
372 }
373 self.applyCacheToPool(poolResolver, tierNum, currentTimestamp, currentEmission)
374 return
375 }
376
377 // Apply caching at every halving boundary.
378 currentEmission := int64(0)
379 for i, hvTimestamp := range halvingTimestamps {
380 currentEmission = halvingEmissions[i]
381 self.applyCacheToPool(poolResolver, tierNum, hvTimestamp, currentEmission)
382 }
383
384 // Remaining range [lastTimestamp, currentTimestamp).
385 self.applyCacheToPool(poolResolver, tierNum, currentTimestamp, currentEmission)
386}
387
388// applyCacheToPool applies the cached reward to all tiered pool.
389func (self *PoolTier) applyCacheToPool(poolResolver *PoolResolver, tierNum uint64, currentTimestamp, emissionInThisInterval int64) {
390 tierRewards := self.computeTierRewards(emissionInThisInterval)
391 poolReward, ok := tierRewards[tierNum]
392 if !ok {
393 return
394 }
395
396 poolResolver.cacheReward(currentTimestamp, poolReward)
397}
398
399// applyCacheToAllPools applies the cached reward to all tiered pools.
400func (self *PoolTier) applyCacheToAllPools(pools *Pools, currentTimestamp, emissionInThisInterval int64) {
401 // calculate denominator and number of pools in each tier
402 counts := self.CurrentAllTierCounts()
403 tierRewards := self.computeTierRewards(emissionInThisInterval)
404
405 // apply cache to all pools
406 self.membership.Iterate("", "", func(key string, value any) bool {
407 pool, ok := pools.Get(key)
408 if !ok {
409 return false
410 }
411
412 tierNum, ok := value.(uint64)
413 if !ok {
414 panic("failed to cast value to uint64")
415 }
416 // Skip pools with tier 0 (removed from tier system)
417 if tierNum == 0 {
418 return false
419 }
420
421 if counts[tierNum] == 0 {
422 return false // Skip if no pools in tier
423 }
424
425 poolReward, ok := tierRewards[tierNum]
426 if !ok {
427 return false
428 }
429
430 // accumulate the reward for the interval (startBlock to endBlock) in the Pool
431 poolResolver := NewPoolResolver(pool)
432 poolResolver.cacheReward(currentTimestamp, poolReward)
433 return false
434 })
435}
436
437// IsInternallyIncentivizedPool returns true if the pool is in a tier.
438// Parameters:
439// - poolPath: Pool path whose membership is checked.
440//
441// Returns:
442// - bool: True when poolPath belongs to a nonzero internal-incentive tier; false otherwise.
443func (self *PoolTier) IsInternallyIncentivizedPool(poolPath string) bool {
444 return self.CurrentTier(poolPath) > 0
445}
446
447// calculatePoolReward calculates the reward for a pool based on the emission, tier ratio, and tier count.
448//
449// Parameters:
450// - emission: The emission for the pool.
451// - tierRatio: The tier ratio for the pool.
452// - tierCount: The tier count for the pool.
453//
454// Returns:
455// - int64: The reward for the pool.
456// - error: An invalid calculation input.
457func calculatePoolReward(emission int64, tierRatio int64, tierCount int64) (int64, error) {
458 if emission < 0 || tierRatio < 0 || tierCount < 0 {
459 return 0, errors.New(errCalculationError)
460 }
461
462 if emission == 0 || tierRatio == 0 || tierCount == 0 {
463 return 0, nil
464 }
465
466 tierReward := gnsmath.SafeMulDivInt64(emission, tierRatio, 100)
467
468 return tierReward / tierCount, nil
469}
470
471// computeTierRewards caches per-tier pool rewards to avoid recalculating for each pool iteration.
472func (self *PoolTier) computeTierRewards(emission int64) map[uint64]int64 {
473 tierRewards := make(map[uint64]int64, AllTierCount-1)
474
475 for tierNum := uint64(1); tierNum < AllTierCount; tierNum++ {
476 tierCount := int64(self.counts[tierNum])
477 if tierCount == 0 {
478 continue
479 }
480
481 tierRatio, err := self.tierRatio.Get(tierNum)
482 if err != nil {
483 panic(makeErrorWithDetails(errInvalidPoolTier, err.Error()))
484 }
485
486 reward, err := calculatePoolReward(emission, int64(tierRatio), tierCount)
487 if err != nil {
488 panic(err)
489 }
490 tierRewards[tierNum] = reward
491 }
492
493 return tierRewards
494}