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_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}