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