protocol_fee_reward_manager.gno
10.52 Kb · 326 lines
1package staker
2
3import (
4 "errors"
5 "math"
6
7 "gno.land/p/gnoswap/gnsmath/v1"
8 u256 "gno.land/p/gnoswap/uint256/v1"
9
10 "gno.land/r/gnoswap/gov/staker"
11)
12
13// ProtocolFeeRewardManagerResolver drives the ProtocolFeeRewardManager.
14//
15// Stake changes only move the epoch, the total stake history and the staker's own
16// event list. Fees are folded into a token's accumulator when that token is collected,
17// each accrual bucket against the total stake in force during its epoch.
18type ProtocolFeeRewardManagerResolver struct {
19 *staker.ProtocolFeeRewardManager
20}
21
22// NewProtocolFeeRewardManagerResolver wraps the shared protocol-fee reward manager.
23//
24// Parameters:
25// - manager: Protocol-fee reward state to resolve and mutate.
26//
27// Returns:
28// - *ProtocolFeeRewardManagerResolver: resolver operating on manager's accounting state.
29func NewProtocolFeeRewardManagerResolver(manager *staker.ProtocolFeeRewardManager) *ProtocolFeeRewardManagerResolver {
30 return &ProtocolFeeRewardManagerResolver{
31 ProtocolFeeRewardManager: manager,
32 }
33}
34
35// addStake records that rewardID staked amount more from epoch on.
36//
37// epoch is the accrual epoch protocol_fee just opened for this change, so it must not
38// be older than the epoch in force.
39func (self *ProtocolFeeRewardManagerResolver) addStake(rewardID string, amount int64, epoch int64) error {
40 if amount <= 0 {
41 return errors.New(errAmountMustBePositive)
42 }
43 if epoch < self.GetCurrentEpoch() {
44 return errors.New(errInvalidAccrualEpoch)
45 }
46
47 currentTotal := self.GetTotalStakedAmount()
48 if currentTotal > math.MaxInt64-amount {
49 return errors.New(errTotalStakedAmountOverflow)
50 }
51
52 rewardState, ok, err := self.GetRewardState(rewardID)
53 if err != nil {
54 return err
55 }
56 if !ok {
57 rewardState = staker.NewProtocolFeeRewardState()
58 }
59
60 resolvedState := NewProtocolFeeRewardStateResolver(rewardState)
61 if err := resolvedState.addStake(amount, epoch); err != nil {
62 return err
63 }
64
65 self.recordTotalStakedAmount(epoch, gnsmath.SafeAddInt64(currentTotal, amount))
66 self.SetRewardState(rewardID, rewardState)
67
68 return nil
69}
70
71// removeStake records that rewardID staked amount less from epoch on.
72func (self *ProtocolFeeRewardManagerResolver) removeStake(rewardID string, amount int64, epoch int64) error {
73 if amount < 0 {
74 return errors.New(errAmountMustBeNonNegative)
75 }
76 if epoch < self.GetCurrentEpoch() {
77 return errors.New(errInvalidAccrualEpoch)
78 }
79
80 updatedTotal := gnsmath.SafeSubInt64(self.GetTotalStakedAmount(), amount)
81 if updatedTotal < 0 {
82 return errors.New(errRemoveAmountExceedsTotalStaked)
83 }
84
85 rewardState, ok, err := self.GetRewardState(rewardID)
86 if err != nil {
87 return err
88 }
89 if !ok {
90 rewardState = staker.NewProtocolFeeRewardState()
91 }
92
93 resolvedState := NewProtocolFeeRewardStateResolver(rewardState)
94 if err := resolvedState.removeStake(amount, epoch); err != nil {
95 return err
96 }
97
98 self.recordTotalStakedAmount(epoch, updatedTotal)
99 self.SetRewardState(rewardID, rewardState)
100
101 return nil
102}
103
104func (self *ProtocolFeeRewardManagerResolver) recordTotalStakedAmount(epoch int64, totalStakedAmount int64) {
105 self.SetCurrentEpoch(epoch)
106 self.SetTotalStakedAmount(totalStakedAmount)
107 self.SetTotalStakedAmountAt(epoch, totalStakedAmount)
108}
109
110// applyAccrualBuckets folds consumed accrual buckets of tokenPath into its accumulator.
111//
112// epochs and amounts are parallel and in ascending epoch order. Each amount is divided
113// by the total stake in force during its epoch; with nothing staked the amount is
114// dropped, the same policy the emission accumulator applies. exhausted reports that
115// no bucket is left pending, which lets the accumulator mark every closed epoch as
116// fully folded.
117func (self *ProtocolFeeRewardManagerResolver) applyAccrualBuckets(
118 tokenPath string,
119 epochs []int64,
120 amounts []int64,
121 exhausted bool,
122 currentTimestamp int64,
123) error {
124 if len(epochs) != len(amounts) {
125 return errors.New(errInvalidAccrualBuckets)
126 }
127
128 accumulator, ok := self.GetTokenAccumulator(tokenPath)
129 if !ok {
130 if len(epochs) == 0 {
131 return nil
132 }
133 accumulator = staker.NewProtocolFeeTokenAccumulator()
134 self.SetTokenAccumulator(tokenPath, accumulator)
135 }
136
137 currentX128 := accumulator.GetAccumulatedX128PerStake().Clone()
138 feeAmount := accumulator.GetProtocolFeeAmount()
139 lastEpoch := int64(-1)
140
141 for i, epoch := range epochs {
142 if epoch < lastEpoch || epoch > self.GetCurrentEpoch() {
143 return errors.New(errInvalidAccrualBuckets)
144 }
145 lastEpoch = epoch
146
147 amount := amounts[i]
148 if amount < 0 {
149 return errors.New(errInvalidAccrualBuckets)
150 }
151 feeAmount = gnsmath.SafeAddInt64(feeAmount, amount)
152
153 totalStakedAmount := self.GetTotalStakedAmountAt(epoch)
154 if totalStakedAmount <= 0 || amount == 0 {
155 continue
156 }
157
158 // Scale the amount by 2^128 before dividing by the total stake. The amount
159 // fits in 63 bits, so the product fits in 191 bits and cannot overflow.
160 amountX128 := u256.Zero().Lsh(u256.NewUintFromInt64(amount), 128)
161 perStakeX128 := u256.Zero().Div(amountX128, u256.NewUintFromInt64(totalStakedAmount))
162 currentX128 = u256.Zero().Add(currentX128, perStakeX128)
163
164 accumulator.SetAccumulatedX128PerStakeAt(epoch, currentX128)
165 }
166
167 accumulator.SetProtocolFeeAmount(feeAmount)
168
169 // Every epoch before the one in force is closed, so it is fully folded once no bucket
170 // is left. Otherwise only the epochs up to the last consumed bucket are complete.
171 foldedEpoch := accumulator.GetFoldedEpoch()
172 if exhausted {
173 foldedEpoch = maxInt64(foldedEpoch, self.GetCurrentEpoch()-1)
174 } else if lastEpoch >= 0 {
175 foldedEpoch = maxInt64(foldedEpoch, minInt64(lastEpoch, self.GetCurrentEpoch()-1))
176 }
177 accumulator.SetFoldedEpoch(foldedEpoch)
178
179 self.SetAccumulatedTimestamp(currentTimestamp)
180
181 return nil
182}
183
184// accumulatorView returns a read view of tokenPath's accumulator.
185//
186// With pendingComplete false the view reflects the accumulator exactly as folded so
187// far, which is what a collect must settle against. With pendingComplete true the
188// given buckets are the whole pending set of the token: they are projected on top, as
189// applyAccrualBuckets would fold them, and every closed epoch counts as folded, which
190// is what an exhaustive fold would leave behind. A query uses the latter so that it
191// reports what a collect would pay even when a previous bounded fold consumed exactly
192// its limit and left foldedEpoch behind the epoch in force.
193func (self *ProtocolFeeRewardManagerResolver) accumulatorView(tokenPath string, pendingEpochs []int64, pendingAmounts []int64, pendingComplete bool) *protocolFeeAccumulatorView {
194 view := &protocolFeeAccumulatorView{
195 extraEpochs: []int64{},
196 extraValues: []*u256.Uint{},
197 current: u256.Zero(),
198 firstEpoch: -1,
199 foldedEpoch: -1,
200 currentEpoch: self.GetCurrentEpoch(),
201 }
202
203 if accumulator, ok := self.GetTokenAccumulator(tokenPath); ok {
204 view.accumulator = accumulator
205 view.current = accumulator.GetAccumulatedX128PerStake().Clone()
206 view.firstEpoch = accumulator.GetFirstEpoch()
207 view.foldedEpoch = accumulator.GetFoldedEpoch()
208 }
209
210 if pendingComplete {
211 // Projecting the whole pending set folds every closed epoch.
212 view.foldedEpoch = maxInt64(view.foldedEpoch, view.currentEpoch-1)
213 }
214
215 for i, epoch := range pendingEpochs {
216 totalStakedAmount := self.GetTotalStakedAmountAt(epoch)
217 if totalStakedAmount <= 0 || pendingAmounts[i] <= 0 {
218 continue
219 }
220
221 amountX128 := u256.Zero().Lsh(u256.NewUintFromInt64(pendingAmounts[i]), 128)
222 perStakeX128 := u256.Zero().Div(amountX128, u256.NewUintFromInt64(totalStakedAmount))
223 view.current = u256.Zero().Add(view.current, perStakeX128)
224 view.extraEpochs = append(view.extraEpochs, epoch)
225 view.extraValues = append(view.extraValues, view.current.Clone())
226 if view.firstEpoch < 0 {
227 view.firstEpoch = epoch
228 }
229 }
230
231 return view
232}
233
234// claimTokenReward settles tokenPath for rewardID and returns the reward collected.
235// See ProtocolFeeRewardStateResolver.claimTokenReward for the maxEvents bound.
236func (self *ProtocolFeeRewardManagerResolver) claimTokenReward(rewardID string, tokenPath string, maxEvents int64) (int64, error) {
237 rewardState, ok, err := self.GetRewardState(rewardID)
238 if err != nil {
239 return 0, err
240 }
241 if !ok {
242 return 0, nil
243 }
244
245 resolvedState := NewProtocolFeeRewardStateResolver(rewardState)
246 reward, err := resolvedState.claimTokenReward(tokenPath, self.accumulatorView(tokenPath, nil, nil, false), maxEvents)
247 if err != nil {
248 return 0, err
249 }
250
251 self.SetRewardState(rewardID, rewardState)
252
253 return reward, nil
254}
255
256// claimRewards settles every folded token for rewardID and returns the reward
257// collected per token. Its cost grows with the number of tokens that ever collected a
258// fee; claimTokenReward is the bounded alternative.
259func (self *ProtocolFeeRewardManagerResolver) claimRewards(rewardID string, currentTimestamp int64) (map[string]int64, error) {
260 rewards := make(map[string]int64)
261
262 rewardState, ok, err := self.GetRewardState(rewardID)
263 if err != nil {
264 return nil, err
265 }
266 if !ok {
267 return rewards, nil
268 }
269
270 resolvedState := NewProtocolFeeRewardStateResolver(rewardState)
271 if !resolvedState.IsClaimable(currentTimestamp) {
272 return rewards, nil
273 }
274
275 for _, tokenPath := range self.GetTokenPaths() {
276 reward, err := resolvedState.claimTokenReward(tokenPath, self.accumulatorView(tokenPath, nil, nil, false), 0)
277 if err != nil {
278 return nil, err
279 }
280 rewards[tokenPath] = reward
281 }
282
283 resolvedState.SetClaimedTimestamp(currentTimestamp)
284 self.SetRewardState(rewardID, rewardState)
285
286 return rewards, nil
287}
288
289// GetClaimableRewardAmount returns what collecting tokenPath for rewardID would pay,
290// with the given pending buckets projected on top of the accumulator.
291// Parameters:
292// - rewardID: Identifier of the staker whose claimable reward is quoted.
293// - tokenPath: Token path whose claimable protocol-fee reward is quoted.
294// - pendingEpochs: Pending bucket epochs projected on the accumulator.
295// - pendingAmounts: Pending fee amounts parallel to pendingEpochs.
296//
297// Returns:
298// - int64: protocol-fee amount that collecting tokenPath would pay in token base units.
299// - error: nil on success; non-nil when reward-state lookup or projection fails.
300func (self *ProtocolFeeRewardManagerResolver) GetClaimableRewardAmount(rewardID string, tokenPath string, pendingEpochs []int64, pendingAmounts []int64) (int64, error) {
301 rewardState, ok, err := self.GetRewardState(rewardID)
302 if err != nil {
303 return 0, err
304 }
305 if !ok {
306 return 0, nil
307 }
308
309 resolvedState := NewProtocolFeeRewardStateResolver(rewardState)
310
311 return resolvedState.getClaimableReward(tokenPath, self.accumulatorView(tokenPath, pendingEpochs, pendingAmounts, true)), nil
312}
313
314func maxInt64(a, b int64) int64 {
315 if a > b {
316 return a
317 }
318 return b
319}
320
321func minInt64(a, b int64) int64 {
322 if a < b {
323 return a
324 }
325 return b
326}