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}