Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}