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_incentives.gno

7.42 Kb · 222 lines
  1package staker
  2
  3import (
  4	"gno.land/p/gnoswap/gnsmath"
  5	u256 "gno.land/p/gnoswap/uint256"
  6	bptree "gno.land/p/nt/bptree/v0"
  7
  8	sr "gno.land/r/gnoswap/staker"
  9)
 10
 11type IncentivesResolver struct {
 12	*sr.Incentives
 13}
 14
 15func NewIncentivesResolver(incentives *sr.Incentives) *IncentivesResolver {
 16	return &IncentivesResolver{
 17		Incentives: incentives,
 18	}
 19}
 20
 21// Get incentive by incentiveId
 22func (self *IncentivesResolver) Get(incentiveId string) (*sr.ExternalIncentive, bool) {
 23	return retrieveIncentive(self.IncentiveTrees(), incentiveId)
 24}
 25
 26func (self *IncentivesResolver) GetIncentiveResolver(incentiveId string) (*ExternalIncentiveResolver, bool) {
 27	if incentive, ok := self.Get(incentiveId); ok {
 28		return NewExternalIncentiveResolver(incentive), true
 29	}
 30	return nil, false
 31}
 32
 33func retrieveIncentive(tree *bptree.BPTree, id string) (*sr.ExternalIncentive, bool) {
 34	value := tree.Get(id)
 35	if value == nil {
 36		return nil, false
 37	}
 38	v, ok := value.(*sr.ExternalIncentive)
 39	if !ok {
 40		panic("failed to cast value to *sr.ExternalIncentive")
 41	}
 42	return v, true
 43}
 44
 45// Create a new external incentive
 46// Panics if the incentive already exists.
 47func (self *IncentivesResolver) create(incentive *sr.ExternalIncentive) {
 48	self.Incentives.SetIncentive(incentive.IncentiveId(), incentive)
 49	self.Incentives.AddIncentiveByStartTime(incentive.StartTimestamp(), incentive.IncentiveId())
 50}
 51
 52// update updates an existing incentive with new information
 53func (self *IncentivesResolver) update(incentive *sr.ExternalIncentive) {
 54	self.Incentives.SetIncentive(incentive.IncentiveId(), incentive)
 55}
 56
 57// starts incentive unclaimable period for this pool
 58func (self *IncentivesResolver) startUnclaimablePeriod(startTimestamp int64) {
 59	self.Incentives.SetUnclaimablePeriod(startTimestamp, int64(0))
 60}
 61
 62// ends incentive unclaimable period for this pool
 63// ignores if currently not in unclaimable period
 64func (self *IncentivesResolver) endUnclaimablePeriod(endTimestamp int64) {
 65	startTimestamp := int64(0)
 66	self.UnclaimablePeriods().ReverseIterate(0, endTimestamp, func(key int64, value any) bool {
 67		v, ok := value.(int64)
 68		if !ok {
 69			panic("failed to cast value to int64")
 70		}
 71		if v != 0 {
 72			// Already ended, no need to update
 73			// keeping startTimestamp as 0 to indicate this
 74			return true
 75		}
 76		startTimestamp = key
 77		return true
 78	})
 79
 80	if startTimestamp == 0 {
 81		// No ongoing unclaimable period found
 82		return
 83	}
 84
 85	if startTimestamp == endTimestamp {
 86		self.Incentives.RemoveUnclaimablePeriod(startTimestamp)
 87	} else {
 88		self.Incentives.SetUnclaimablePeriod(startTimestamp, endTimestamp)
 89	}
 90
 91	// Accumulate the just-closed period into every active incentive
 92	self.accumulateUnclaimableSeconds(startTimestamp, endTimestamp)
 93}
 94
 95// accumulateUnclaimableSeconds adds the unclaimable duration between
 96// startTimestamp and endTimestamp to every non-refunded incentive whose window
 97// overlaps the period. The accumulator is updated in place on the stored
 98// incentive pointers, so no tree write is required here.
 99func (self *IncentivesResolver) accumulateUnclaimableSeconds(startTimestamp, endTimestamp int64) {
100	self.Incentives.IterateIncentives(func(_ string, incentive *sr.ExternalIncentive) bool {
101		if incentive.Refunded() {
102			return false
103		}
104
105		duration := calculateUnClaimableDuration(
106			startTimestamp,
107			endTimestamp,
108			incentive.StartTimestamp(),
109			incentive.EndTimestamp(),
110		)
111		if duration > 0 {
112			incentive.SetUnclaimableSeconds(gnsmath.SafeAddInt64(incentive.UnclaimableSeconds(), duration))
113		}
114		return false
115	})
116}
117
118// calculate unclaimable reward from the per-incentive unclaimable seconds
119// accumulator instead of scanning the unbounded unclaimable periods tree.
120func (self *IncentivesResolver) calculateUnclaimableReward(incentiveId string) int64 {
121	incentive, ok := self.Get(incentiveId)
122	if !ok {
123		return 0
124	}
125
126	// The accumulator holds the duration of every closed unclaimable period
127	// that overlaps the incentive window.
128	timeDiff := incentive.UnclaimableSeconds()
129
130	// Ongoing unclaimable periods (end == 0) are not yet accumulated because
131	// their end is unknown. They are resolved here by treating them as
132	// extending to the incentive end. Unclaimable periods never overlap and
133	// are recorded in ascending order, so an ongoing period is always the
134	// most recently started one and therefore the highest key in the tree.
135	// ReverseIterate visits the highest key first, so each scan below finds
136	// the only ongoing period that can affect the reward (if any) on its
137	// first visit and stops - the scan is bounded regardless of how many
138	// periods exist in the tree.
139	//
140	// Tail 1: the ongoing period that started before the incentive window
141	// (e.g. the initial pool-creation period). If the highest period starting
142	// before the window is already closed, no ongoing period can overlap the
143	// incentive start and there is nothing to add.
144	self.UnclaimablePeriods().ReverseIterate(0, incentive.StartTimestamp()-1, func(startTimestamp int64, value any) bool {
145		endTimestamp, ok := value.(int64)
146		if !ok {
147			panic("failed to cast value to int64")
148		}
149		if endTimestamp != 0 {
150			// Already closed - the duration is already accumulated in
151			// UnclaimableSeconds(), and being the highest key it is also the
152			// latest period, so no ongoing period can exist below it.
153			return true
154		}
155
156		timeDiff = gnsmath.SafeAddInt64(timeDiff, calculateUnClaimableDuration(
157			startTimestamp,
158			incentive.EndTimestamp(),
159			incentive.StartTimestamp(),
160			incentive.EndTimestamp(),
161		))
162		return true
163	})
164
165	// Tail 2: the ongoing period that started within the incentive window.
166	// The upper bound is endTime-1 to mirror the Iterate(start, end)
167	// half-open range, which excludes a period starting exactly at endTime
168	// (e.g. the initial pool-creation period that coincides with endTime).
169	self.UnclaimablePeriods().ReverseIterate(incentive.StartTimestamp(), incentive.EndTimestamp()-1, func(startTimestamp int64, value any) bool {
170		endTimestamp, ok := value.(int64)
171		if !ok {
172			panic("failed to cast value to int64")
173		}
174		if endTimestamp != 0 {
175			// Already closed - the duration is already accumulated in
176			// UnclaimableSeconds(), and being the highest key it is also the
177			// latest period, so no ongoing period can exist below it.
178			return true
179		}
180
181		timeDiff = gnsmath.SafeAddInt64(timeDiff, calculateUnClaimableDuration(
182			startTimestamp,
183			incentive.EndTimestamp(),
184			incentive.StartTimestamp(),
185			incentive.EndTimestamp(),
186		))
187		return true
188	})
189
190	// rewardPerSecondX128 = rps << 128, so dividing by q128 here recovers the
191	// floor of (timeDiff * rps) without the truncation that an int64 rps would
192	// have introduced at incentive-creation time.
193	unclaimable := u256.MulDiv(
194		u256.NewUintFromInt64(timeDiff),
195		incentive.RewardPerSecondX128(),
196		q128,
197	)
198	return gnsmath.SafeConvertToInt64(unclaimable)
199}
200
201// calculateUnClaimableDuration calculates the duration of overlap between an unclaimable period and incentive period
202func calculateUnClaimableDuration(unclaimableStart, unclaimableEnd, incentiveStartTimestamp, incentiveEndTimestamp int64) int64 {
203	// Use later timestamp between unclaimable start and incentive start
204	startTime := unclaimableStart
205	if startTime < incentiveStartTimestamp {
206		startTime = incentiveStartTimestamp
207	}
208
209	// Use earlier timestamp between unclaimable end and incentive end
210	endTime := unclaimableEnd
211	if endTime > incentiveEndTimestamp {
212		endTime = incentiveEndTimestamp
213	}
214
215	// Return 0 if no overlap
216	if endTime < startTime {
217		return 0
218	}
219
220	// Calculate overlap duration
221	return gnsmath.SafeSubInt64(endTime, startTime)
222}