Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}