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

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}