Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}