protocol_fee_reward_state.gno
11.96 Kb · 352 lines
1package staker
2
3import (
4 "errors"
5
6 "gno.land/p/gnoswap/consts"
7 gnsmath "gno.land/p/gnoswap/gnsmath"
8 u256 "gno.land/p/gnoswap/uint256"
9 "gno.land/r/gnoswap/gov/staker"
10)
11
12type ProtocolFeeRewardStateResolver struct {
13 *staker.ProtocolFeeRewardState
14}
15
16func NewProtocolFeeRewardStateResolver(protocolFeeRewardState *staker.ProtocolFeeRewardState) *ProtocolFeeRewardStateResolver {
17 return &ProtocolFeeRewardStateResolver{protocolFeeRewardState}
18}
19
20// IsClaimable checks if rewards can be claimed at the given timestamp.
21// Rewards are claimable if the current timestamp is greater than the last claimed timestamp.
22//
23// Parameters:
24// - currentTimestamp: current timestamp to check against
25//
26// Returns:
27// - bool: true if rewards can be claimed, false otherwise
28func (p *ProtocolFeeRewardStateResolver) IsClaimable(currentTimestamp int64) bool {
29 return p.GetClaimedTimestamp() < currentTimestamp
30}
31
32// GetClaimableRewardAmounts calculates the claimable reward amounts for all tokens.
33// This includes both accumulated rewards and newly earned rewards based on current state.
34//
35// Parameters:
36// - accumulatedRewardsX128PerStake: current system-wide accumulated rewards per stake for all tokens
37// - currentTimestamp: current timestamp
38//
39// Returns:
40// - map[string]int64: map of token path to claimable reward amount
41// - error: nil on success, error if claiming is not allowed
42func (p *ProtocolFeeRewardStateResolver) GetClaimableRewardAmounts(
43 accumulatedRewardsX128PerStake map[string]*u256.Uint,
44 currentTimestamp int64,
45) (map[string]int64, error) {
46 newlyEarnedRewards, err := p.calculateClaimableRewards(accumulatedRewardsX128PerStake, currentTimestamp)
47 if err != nil {
48 return nil, err
49 }
50
51 claimableRewards := make(map[string]int64)
52 accumulatedRewards := p.GetAccumulatedRewards()
53 claimedRewards := p.GetClaimedRewards()
54
55 for token, accumulatedReward := range accumulatedRewards {
56 claimableRewards[token] = gnsmath.SafeSubInt64(accumulatedReward, claimedRewards[token])
57 }
58
59 for token, newlyEarnedReward := range newlyEarnedRewards {
60 claimableRewards[token] = gnsmath.SafeAddInt64(claimableRewards[token], newlyEarnedReward)
61 }
62
63 return claimableRewards, nil
64}
65
66// calculateClaimableRewards calculates newly earned rewards for all tokens since the last update.
67// This method uses the difference between current and stored reward debt to calculate earnings.
68//
69// Parameters:
70// - accumulatedRewardsX128PerStake: current system-wide accumulated rewards per stake for all tokens
71// - currentTimestamp: current timestamp
72//
73// Returns:
74// - map[string]int64: map of token path to newly earned reward amount
75func (p *ProtocolFeeRewardStateResolver) calculateClaimableRewards(
76 accumulatedRewardsX128PerStake map[string]*u256.Uint,
77 currentTimestamp int64,
78) (map[string]int64, error) {
79 // Don't calculate rewards for past timestamps
80 if p.GetAccumulatedTimestamp() > currentTimestamp {
81 return make(map[string]int64), nil
82 }
83
84 rewardAmounts := make(map[string]int64)
85 stakedAmount := p.GetStakedAmount()
86
87 // Calculate rewards for each token type
88 for token, accumulatedRewardX128PerStake := range accumulatedRewardsX128PerStake {
89 // Get reward debt for this token
90 rewardDebtX128 := p.GetRewardDebtX128ForToken(token)
91 if rewardDebtX128 == nil {
92 rewardDebtX128 = u256.Zero()
93 }
94
95 // Calculate the difference in accumulated rewards per stake since last update
96 // Using modular arithmetic for accumulator values - underflow is allowed and handled correctly
97 rewardDebtDeltaX128 := u256.Zero().Sub(
98 accumulatedRewardX128PerStake,
99 rewardDebtX128,
100 )
101
102 // Skip the 256-bit MulDiv for tokens with no newly distributed fees for this staker: the per-stake
103 // accumulator is unchanged since the staker's reward debt, so the reward is exactly zero. The zero
104 // is still recorded so the result map is unchanged; only the expensive computation is avoided,
105 // keeping cost proportional to the tokens that actually received fees (gas report issue 1 / §1.2).
106 if rewardDebtDeltaX128.IsZero() {
107 rewardAmounts[token] = 0
108 continue
109 }
110
111 // Multiply by staked amount to get total reward for this staker and token
112 rewardAmount := u256.MulDiv(
113 rewardDebtDeltaX128,
114 u256.NewUintFromInt64(stakedAmount),
115 consts.Q128(),
116 )
117
118 rewardAmounts[token] = gnsmath.SafeConvertToInt64(rewardAmount)
119 }
120
121 return rewardAmounts, nil
122}
123
124// calculateClaimableRewardForToken calculates newly earned rewards for exactly one token.
125func (p *ProtocolFeeRewardStateResolver) calculateClaimableRewardForToken(
126 tokenPath string,
127 accumulatedRewardX128PerStake *u256.Uint,
128) (int64, error) {
129 if accumulatedRewardX128PerStake == nil {
130 accumulatedRewardX128PerStake = u256.Zero()
131 }
132
133 rewardDebtX128 := p.GetRewardDebtX128ForToken(tokenPath)
134 if rewardDebtX128 == nil {
135 rewardDebtX128 = u256.Zero()
136 }
137
138 rewardDebtDeltaX128 := u256.Zero().Sub(
139 accumulatedRewardX128PerStake,
140 rewardDebtX128,
141 )
142
143 rewardAmount := u256.MulDiv(
144 rewardDebtDeltaX128,
145 u256.NewUintFromInt64(p.GetStakedAmount()),
146 consts.Q128(),
147 )
148
149 return gnsmath.SafeConvertToInt64(rewardAmount), nil
150}
151
152// addStake increases the staked amount for this address.
153// This method should be called when a user increases their stake.
154//
155// Parameters:
156// - amount: amount of stake to add
157func (p *ProtocolFeeRewardStateResolver) addStake(amount int64) {
158 p.SetStakedAmount(gnsmath.SafeAddInt64(p.GetStakedAmount(), amount))
159}
160
161// removeStake decreases the staked amount for this address.
162// This method should be called when a user decreases their stake.
163//
164// Parameters:
165// - amount: amount of stake to remove
166func (p *ProtocolFeeRewardStateResolver) removeStake(amount int64) {
167 newAmount := gnsmath.SafeSubInt64(p.GetStakedAmount(), amount)
168 if newAmount < 0 {
169 newAmount = 0
170 }
171 p.SetStakedAmount(newAmount)
172}
173
174// claimRewards processes reward claiming for all tokens and updates the claim state.
175// This method validates claimability and transfers accumulated rewards to claimed status.
176//
177// Parameters:
178// - currentTimestamp: current timestamp
179//
180// Returns:
181// - map[string]int64: map of token path to claimed reward amount
182// - error: nil on success, error if reward debt is stale
183func (p *ProtocolFeeRewardStateResolver) claimRewards(currentTimestamp int64) (map[string]int64, error) {
184 if !p.IsClaimable(currentTimestamp) {
185 return make(map[string]int64), nil
186 }
187
188 if p.GetAccumulatedTimestamp() < currentTimestamp {
189 return nil, errors.New("must update reward debt before claiming rewards")
190 }
191
192 currentClaimedRewards := make(map[string]int64)
193 accumulatedRewards := p.GetAccumulatedRewards()
194 claimedRewards := p.GetClaimedRewards()
195
196 // Calculate and update claimed amounts for each token
197 for token, rewardAmount := range accumulatedRewards {
198 claimedAmount := claimedRewards[token]
199 currentClaimedRewards[token] = gnsmath.SafeSubInt64(rewardAmount, claimedAmount)
200 p.SetClaimedRewardForToken(token, rewardAmount)
201 }
202
203 p.SetClaimedTimestamp(currentTimestamp)
204
205 return currentClaimedRewards, nil
206}
207
208// claimRewardForToken claims exactly one token path without using the shared claimedTimestamp as a gate.
209func (p *ProtocolFeeRewardStateResolver) claimRewardForToken(currentTimestamp int64, tokenPath string) (int64, error) {
210 accumulatedReward := p.GetAccumulatedRewardForToken(tokenPath)
211 claimedReward := p.GetClaimedRewardForToken(tokenPath)
212 claimableReward := gnsmath.SafeSubInt64(accumulatedReward, claimedReward)
213
214 p.SetClaimedRewardForToken(tokenPath, accumulatedReward)
215
216 return claimableReward, nil
217}
218
219// updateRewardDebtX128 updates the reward debt and accumulates new rewards for all tokens.
220// This method should be called before any stake changes to ensure accurate reward tracking.
221//
222// Parameters:
223// - accumulatedProtocolFeeX128PerStake: current system-wide accumulated protocol fees per stake for all tokens
224// - currentTimestamp: current timestamp
225func (p *ProtocolFeeRewardStateResolver) updateRewardDebtX128(
226 accumulatedProtocolFeeX128PerStake map[string]*u256.Uint,
227 currentTimestamp int64,
228) error {
229 // Don't update if we're looking at a past timestamp
230 if p.GetAccumulatedTimestamp() > currentTimestamp {
231 return nil
232 }
233
234 // Calculate and accumulate new rewards for all tokens
235 rewardAmounts, err := p.calculateClaimableRewards(accumulatedProtocolFeeX128PerStake, currentTimestamp)
236 if err != nil {
237 return err
238 }
239
240 // Update reward debt for all tokens
241 p.SetRewardDebtX128(accumulatedProtocolFeeX128PerStake)
242
243 // Add newly calculated rewards to accumulated amounts
244 accumulatedRewards := p.GetAccumulatedRewards()
245 for token, rewardAmount := range rewardAmounts {
246 p.SetAccumulatedRewardForToken(token, gnsmath.SafeAddInt64(accumulatedRewards[token], rewardAmount))
247 }
248
249 p.SetAccumulatedTimestamp(currentTimestamp)
250
251 return nil
252}
253
254// updateRewardDebtX128ForToken updates reward debt and accumulated rewards for exactly one token.
255func (p *ProtocolFeeRewardStateResolver) updateRewardDebtX128ForToken(
256 tokenPath string,
257 accumulatedProtocolFeeX128PerStake *u256.Uint,
258 currentTimestamp int64,
259) error {
260 if accumulatedProtocolFeeX128PerStake == nil {
261 accumulatedProtocolFeeX128PerStake = u256.Zero()
262 }
263
264 rewardAmount, err := p.calculateClaimableRewardForToken(tokenPath, accumulatedProtocolFeeX128PerStake)
265 if err != nil {
266 return err
267 }
268
269 p.SetRewardDebtX128ForToken(tokenPath, accumulatedProtocolFeeX128PerStake)
270 p.SetAccumulatedRewardForToken(
271 tokenPath,
272 gnsmath.SafeAddInt64(p.GetAccumulatedRewardForToken(tokenPath), rewardAmount),
273 )
274
275 return nil
276}
277
278// addStakeWithUpdateRewardDebtX128 adds stake and updates reward debt in one operation.
279// This ensures rewards are properly calculated before the stake change takes effect.
280//
281// Parameters:
282// - amount: amount of stake to add
283// - accumulatedProtocolFeeX128PerStake: current system-wide accumulated protocol fees per stake
284// - currentTimestamp: current timestamp
285func (p *ProtocolFeeRewardStateResolver) addStakeWithUpdateRewardDebtX128(
286 amount int64,
287 accumulatedProtocolFeeX128PerStake map[string]*u256.Uint,
288 currentTimestamp int64,
289) error {
290 err := p.updateRewardDebtX128(accumulatedProtocolFeeX128PerStake, currentTimestamp)
291 if err != nil {
292 return err
293 }
294
295 p.addStake(amount)
296
297 return nil
298}
299
300// removeStakeWithUpdateRewardDebtX128 removes stake and updates reward debt in one operation.
301// This ensures rewards are properly calculated before the stake change takes effect.
302//
303// Parameters:
304// - amount: amount of stake to remove
305// - accumulatedProtocolFeeX128PerStake: current system-wide accumulated protocol fees per stake
306// - currentTimestamp: current timestamp
307func (p *ProtocolFeeRewardStateResolver) removeStakeWithUpdateRewardDebtX128(
308 amount int64,
309 accumulatedProtocolFeeX128PerStake map[string]*u256.Uint,
310 currentTimestamp int64,
311) error {
312 err := p.updateRewardDebtX128(accumulatedProtocolFeeX128PerStake, currentTimestamp)
313 if err != nil {
314 return err
315 }
316
317 p.removeStake(amount)
318
319 return nil
320}
321
322// claimRewardsWithUpdateRewardDebtX128 claims rewards and updates reward debt in one operation.
323// This ensures all rewards are properly calculated before claiming.
324//
325// Parameters:
326// - accumulatedProtocolFeeX128PerStake: current system-wide accumulated protocol fees per stake
327// - currentTimestamp: current timestamp
328//
329// Returns:
330// - map[string]int64: map of token path to claimed reward amount
331// - error: nil on success, error if claiming fails
332func (p *ProtocolFeeRewardStateResolver) claimRewardsWithUpdateRewardDebtX128(
333 accumulatedProtocolFeeX128PerStake map[string]*u256.Uint,
334 currentTimestamp int64,
335) (map[string]int64, error) {
336 p.updateRewardDebtX128(accumulatedProtocolFeeX128PerStake, currentTimestamp)
337
338 return p.claimRewards(currentTimestamp)
339}
340
341// claimRewardForTokenWithUpdateRewardDebtX128 claims exactly one protocol fee token.
342func (p *ProtocolFeeRewardStateResolver) claimRewardForTokenWithUpdateRewardDebtX128(
343 tokenPath string,
344 accumulatedProtocolFeeX128PerStake *u256.Uint,
345 currentTimestamp int64,
346) (int64, error) {
347 if err := p.updateRewardDebtX128ForToken(tokenPath, accumulatedProtocolFeeX128PerStake, currentTimestamp); err != nil {
348 return 0, err
349 }
350
351 return p.claimRewardForToken(currentTimestamp, tokenPath)
352}