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}