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