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

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