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

13.29 Kb · 438 lines
  1package staker
  2
  3import (
  4	"errors"
  5
  6	"gno.land/p/gnoswap/gnsmath"
  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
 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)
 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, getHalvingBlocksInRange func(start, end int64) ([]int64, []int64)) *PoolTier {
 89	result := &PoolTier{
 90		membership:               sr.NewBPTreeN(16),
 91		tierRatio:                TierRatioFromCounts(1, 0, 0),
 92		lastRewardCacheTimestamp: gnsmath.SafeAddInt64(currentTime, 1),
 93		getEmission:              getEmission,
 94		getHalvingBlocksInRange:  getHalvingBlocksInRange,
 95		currentEmission:          getEmission(),
 96	}
 97
 98	pools.set(initialPoolPath, sr.NewPool(initialPoolPath, currentTime+1))
 99	result.changeTier(currentTime+1, pools, initialPoolPath, 1)
100	return result
101}
102
103func NewPoolTierBy(
104	membership *bptree.BPTree,
105	tierRatio sr.TierRatio,
106	counts [AllTierCount]uint64,
107	lastRewardCacheTimestamp int64,
108	currentEmission int64,
109	getEmission func() int64,
110	getHalvingBlocksInRange func(start, end int64) ([]int64, []int64),
111) *PoolTier {
112	return &PoolTier{
113		membership:               membership,
114		tierRatio:                tierRatio,
115		counts:                   counts,
116		lastRewardCacheTimestamp: lastRewardCacheTimestamp,
117		getEmission:              getEmission,
118		getHalvingBlocksInRange:  getHalvingBlocksInRange,
119		currentEmission:          currentEmission,
120	}
121}
122
123// CurrentReward returns the current per-pool reward for the given tier.
124func (self *PoolTier) CurrentReward(tier uint64) int64 {
125	currentEmission := self.getEmission()
126	tierRatio, err := self.tierRatio.Get(tier)
127	if err != nil {
128		panic(makeErrorWithDetails(errInvalidPoolTier, err.Error()))
129	}
130
131	tierRatioInt64 := int64(tierRatio)
132	count := int64(self.CurrentCount(tier))
133
134	return calculatePoolReward(currentEmission, tierRatioInt64, count)
135}
136
137// CurrentCount returns the current count of pools in the given tier.
138func (self *PoolTier) CurrentCount(tier uint64) int {
139	if tier >= AllTierCount {
140		return 0
141	}
142	return int(self.counts[tier])
143}
144
145// CurrentAllTierCounts returns the current count of pools in each tier.
146func (self *PoolTier) CurrentAllTierCounts() []uint64 {
147	out := make([]uint64, AllTierCount)
148	copy(out, self.counts[:])
149	return out // returning snapshot
150}
151
152// CurrentTier returns the tier of the given pool.
153func (self *PoolTier) CurrentTier(poolPath string) (tier uint64) {
154	if tierI := self.membership.Get(poolPath); tierI == nil {
155		return 0
156	} else {
157		var ok bool
158		tier, ok = tierI.(uint64)
159		if !ok {
160			panic("failed to cast tier to uint64")
161		}
162		return tier
163	}
164}
165
166// changeTier updates the tier of a pool, recalculates ratios, and applies
167// updated per-pool reward to each of the pools.
168func (self *PoolTier) changeTier(currentTime int64, pools *Pools, poolPath string, nextTier uint64) map[uint64]int64 {
169	currentTier := self.CurrentTier(poolPath)
170	if currentTier == nextTier {
171		// no change, return
172		return make(map[uint64]int64)
173	}
174	assertTier1HasSparePool(currentTier, self.counts[Tier1])
175
176	self.cacheReward(currentTime, pools)
177
178	// decrement count from current tier if it exists
179	if currentTier > 0 {
180		if self.counts[currentTier] == 0 {
181			panic("counts underflow: removing from empty tier")
182		}
183		self.counts[currentTier]--
184	}
185
186	if nextTier == 0 {
187		// removed from the tier
188		self.membership.Remove(poolPath)
189		pool, ok := pools.Get(poolPath)
190		if !ok {
191			panic("changeTier: pool not found")
192		}
193		poolResolver := NewPoolResolver(pool)
194		// prevent new rewards from accumulating after tier removal
195		poolResolver.cacheReward(currentTime, 0)
196	} else {
197		// handle all move/add operations
198		self.membership.Set(poolPath, nextTier)
199		self.counts[nextTier]++
200	}
201
202	self.tierRatio = TierRatioFromCounts(self.counts[Tier1], self.counts[Tier2], self.counts[Tier3])
203	currentEmission := self.getEmission()
204	tierRewards := self.computeTierRewards(currentEmission)
205
206	// Cache updated reward for each tiered pool
207	self.membership.Iterate("", "", func(key string, value any) bool {
208		pool, ok := pools.Get(key)
209		if !ok {
210			panic("changeTier: pool not found")
211		}
212		tier, ok := value.(uint64)
213		if !ok {
214			panic("failed to cast value to uint64")
215		}
216
217		poolReward, ok := tierRewards[tier]
218		if !ok {
219			return false // Skip if no pools in tier
220		}
221
222		poolResolver := NewPoolResolver(pool)
223		poolResolver.cacheReward(currentTime, poolReward)
224		return false
225	})
226
227	self.currentEmission = currentEmission
228
229	return tierRewards
230}
231
232// cacheReward MUST be called before calculating any position reward.
233// cacheReward updates the reward cache for each pool, accounting for any halving events
234// that occurred between the last cached timestamp and the current timestamp.
235// Note: Block height is used only for event tracking purposes.
236func (self *PoolTier) cacheReward(currentTimestamp int64, pools *Pools) {
237	lastTimestamp := self.lastRewardCacheTimestamp
238
239	if currentTimestamp <= lastTimestamp {
240		// no need to check
241		return
242	}
243
244	// find halving blocks in range
245	halvingTimestamps, halvingEmissions := self.getHalvingBlocksInRange(lastTimestamp, currentTimestamp)
246
247	if len(halvingTimestamps) == 0 {
248		self.applyCacheToAllPools(pools, currentTimestamp, self.currentEmission)
249		self.lastRewardCacheTimestamp = currentTimestamp
250		return
251	}
252
253	for i, hvTimestamp := range halvingTimestamps {
254		emission := halvingEmissions[i]
255		// caching: [lastTimestamp, hvTimestamp)
256		self.applyCacheToAllPools(pools, hvTimestamp, emission)
257
258		// halve emissions when halvingBlock is reached
259		self.currentEmission = emission
260	}
261
262	// remaining range [lastTimestamp, currentTimestamp)
263	self.applyCacheToAllPools(pools, currentTimestamp, self.currentEmission)
264
265	self.lastRewardCacheTimestamp = currentTimestamp
266}
267
268// cacheRewardForPool caches internal reward/accumulators for a single pool only.
269// This avoids iterating all tiered pools on every position reward calculation.
270func (self *PoolTier) cacheRewardForPool(currentTimestamp int64, pools *Pools, poolPath string) {
271	pool, ok := pools.Get(poolPath)
272	if !ok {
273		return
274	}
275
276	tierNum := self.CurrentTier(poolPath)
277	// Pool not in the internal-incentive system.
278	if tierNum == 0 {
279		return
280	}
281
282	// Find the latest reward cache timestamp for this pool.
283	lastTimestamp := int64(0)
284	hasLast := false
285	pool.RewardCache().ReverseIterate(0, currentTimestamp, func(key int64, _ any) bool {
286		lastTimestamp = key
287		hasLast = true
288		return true
289	})
290
291	if !hasLast {
292		// Fallback to global tier cache cursor.
293		lastTimestamp = self.lastRewardCacheTimestamp
294	}
295
296	if currentTimestamp <= lastTimestamp {
297		return
298	}
299
300	// Determine halving boundaries since the pool's last cached reward timestamp.
301	halvingTimestamps, halvingEmissions := self.getHalvingBlocksInRange(lastTimestamp, currentTimestamp)
302	poolResolver := NewPoolResolver(pool)
303
304	if len(halvingTimestamps) == 0 {
305		// No emission change within the range => use current emission.
306		self.applyCacheToPool(poolResolver, tierNum, currentTimestamp, self.currentEmission)
307		return
308	}
309
310	// Apply caching at every halving boundary.
311	currentEmission := int64(0)
312	for i, hvTimestamp := range halvingTimestamps {
313		currentEmission = halvingEmissions[i]
314		self.applyCacheToPool(poolResolver, tierNum, hvTimestamp, currentEmission)
315	}
316
317	// Remaining range [lastTimestamp, currentTimestamp).
318	self.applyCacheToPool(poolResolver, tierNum, currentTimestamp, currentEmission)
319}
320
321// applyCacheToPool applies the cached reward to all tiered pool.
322func (self *PoolTier) applyCacheToPool(poolResolver *PoolResolver, tierNum uint64, currentTimestamp, emissionInThisInterval int64) {
323	tierRewards := self.computeTierRewards(emissionInThisInterval)
324	poolReward, ok := tierRewards[tierNum]
325	if !ok {
326		return
327	}
328
329	poolResolver.cacheReward(currentTimestamp, poolReward)
330}
331
332// applyCacheToAllPools applies the cached reward to all tiered pools.
333func (self *PoolTier) applyCacheToAllPools(pools *Pools, currentTimestamp, emissionInThisInterval int64) {
334	// calculate denominator and number of pools in each tier
335	counts := self.CurrentAllTierCounts()
336	tierRewards := self.computeTierRewards(emissionInThisInterval)
337
338	// apply cache to all pools
339	self.membership.Iterate("", "", func(key string, value any) bool {
340		pool, ok := pools.Get(key)
341		if !ok {
342			return false
343		}
344
345		tierNum, ok := value.(uint64)
346		if !ok {
347			panic("failed to cast value to uint64")
348		}
349		// Skip pools with tier 0 (removed from tier system)
350		if tierNum == 0 {
351			return false
352		}
353
354		if counts[tierNum] == 0 {
355			return false // Skip if no pools in tier
356		}
357
358		poolReward, ok := tierRewards[tierNum]
359		if !ok {
360			return false
361		}
362
363		// accumulate the reward for the interval (startBlock to endBlock) in the Pool
364		poolResolver := NewPoolResolver(pool)
365		poolResolver.cacheReward(currentTimestamp, poolReward)
366		return false
367	})
368}
369
370// IsInternallyIncentivizedPool returns true if the pool is in a tier.
371func (self *PoolTier) IsInternallyIncentivizedPool(poolPath string) bool {
372	return self.CurrentTier(poolPath) > 0
373}
374
375func (self *PoolTier) CurrentRewardPerPool(poolPath string) int64 {
376	tierNum := self.CurrentTier(poolPath)
377	if tierNum == 0 {
378		return 0 // Pool not in any tier
379	}
380
381	tierRatio, err := self.tierRatio.Get(tierNum)
382	if err != nil {
383		panic(makeErrorWithDetails(errInvalidPoolTier, err.Error()))
384	}
385	tierRatioInt64 := int64(tierRatio)
386
387	counts := self.CurrentAllTierCounts()
388	tierCount := int64(counts[tierNum])
389	if tierCount == 0 {
390		return 0 // No pools in tier
391	}
392
393	return calculatePoolReward(self.getEmission(), tierRatioInt64, tierCount)
394}
395
396// calculatePoolReward calculates the reward for a pool based on the emission, tier ratio, and tier count.
397//
398// Parameters:
399// - emission: The emission for the pool.
400// - tierRatio: The tier ratio for the pool.
401// - tierCount: The tier count for the pool.
402//
403// Returns:
404// - int64: The reward for the pool.
405func calculatePoolReward(emission int64, tierRatio int64, tierCount int64) int64 {
406	if emission < 0 || tierRatio < 0 || tierCount < 0 {
407		panic(errors.New(errCalculationError))
408	}
409
410	if emission == 0 || tierRatio == 0 || tierCount == 0 {
411		return 0
412	}
413
414	tierReward := gnsmath.SafeMulDivInt64(emission, tierRatio, 100)
415
416	return tierReward / tierCount
417}
418
419// computeTierRewards caches per-tier pool rewards to avoid recalculating for each pool iteration.
420func (self *PoolTier) computeTierRewards(emission int64) map[uint64]int64 {
421	tierRewards := make(map[uint64]int64, AllTierCount-1)
422
423	for tierNum := uint64(1); tierNum < AllTierCount; tierNum++ {
424		tierCount := int64(self.counts[tierNum])
425		if tierCount == 0 {
426			continue
427		}
428
429		tierRatio, err := self.tierRatio.Get(tierNum)
430		if err != nil {
431			panic(makeErrorWithDetails(errInvalidPoolTier, err.Error()))
432		}
433
434		tierRewards[tierNum] = calculatePoolReward(emission, int64(tierRatio), tierCount)
435	}
436
437	return tierRewards
438}