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.00 Kb · 307 lines
  1package staker
  2
  3import (
  4	"errors"
  5
  6	"gno.land/p/gnoswap/consts/v1"
  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// ProtocolFeeRewardStateResolver settles one staker's protocol fee rewards.
 14//
 15// A staker's reward for a token is the sum, over the segments between their stake
 16// events, of the stake held during the segment times the growth of the token's
 17// accumulated fee per stake across it. Segments are folded in event order, so the work
 18// of settling a token is proportional to the staker's own stake changes since the token
 19// was last settled, never to the number of tokens.
 20type ProtocolFeeRewardStateResolver struct {
 21	*staker.ProtocolFeeRewardState
 22}
 23
 24// NewProtocolFeeRewardStateResolver wraps one staker's protocol fee reward state
 25// with settlement operations.
 26//
 27// Parameters:
 28//   - protocolFeeRewardState: staker reward state to resolve and mutate.
 29//
 30// Returns:
 31//   - *ProtocolFeeRewardStateResolver: resolver backed by protocolFeeRewardState.
 32func NewProtocolFeeRewardStateResolver(protocolFeeRewardState *staker.ProtocolFeeRewardState) *ProtocolFeeRewardStateResolver {
 33	return &ProtocolFeeRewardStateResolver{
 34		ProtocolFeeRewardState: protocolFeeRewardState,
 35	}
 36}
 37
 38// protocolFeeAccumulatorView reads a token accumulator as it stands, optionally
 39// extended in memory with buckets that are still pending on the protocol fee realm.
 40// The extension lets a query report what a collect would pay without folding anything.
 41type protocolFeeAccumulatorView struct {
 42	accumulator *staker.ProtocolFeeTokenAccumulator // nil when the token was never folded
 43	// extraEpochs and extraValues hold projected history entries, in ascending epoch order
 44	extraEpochs []int64
 45	extraValues []*u256.Uint
 46	// current is the latest accumulated value, projection included
 47	current *u256.Uint
 48	// firstEpoch is the first epoch a fee was folded for, projection included, or -1
 49	firstEpoch int64
 50	// foldedEpoch is the last epoch whose fees are known to be fully folded, or -1
 51	foldedEpoch int64
 52	// currentEpoch is the accrual epoch in force on the manager
 53	currentEpoch int64
 54}
 55
 56// at returns the accumulated value after the fees of the latest known epoch at or
 57// before epoch.
 58func (v *protocolFeeAccumulatorView) at(epoch int64) *u256.Uint {
 59	for i := len(v.extraEpochs) - 1; i >= 0; i-- {
 60		if v.extraEpochs[i] <= epoch {
 61			return v.extraValues[i]
 62		}
 63	}
 64
 65	if v.accumulator == nil {
 66		return u256.Zero()
 67	}
 68
 69	return v.accumulator.GetAccumulatedX128PerStakeAt(epoch)
 70}
 71
 72// protocolFeeTokenSettlement is the in-memory working copy of a token's settlement
 73// state. It is written back only on a collect, so queries never touch storage.
 74type protocolFeeTokenSettlement struct {
 75	eventCursor         int64
 76	segmentStakedAmount int64
 77	segmentStartX128    *u256.Uint
 78	earnedX128          *u256.Uint
 79}
 80
 81// loadTokenSettlement returns the settlement state of tokenPath, initialising it when
 82// the token was never settled for this staker.
 83//
 84// A fresh state skips every stake event at or before the accumulator's first epoch:
 85// the accumulator was zero throughout, so those segments earned nothing and the open
 86// segment can start at the last of them. When no fee was folded at all, the skip
 87// covers every event, but only once every closed epoch is known to be folded: a
 88// bucket still pending from a closed epoch could later credit one of those events,
 89// so until then they are settled one by one through settleEvents, which is gated by
 90// foldedEpoch and yields nothing while the accumulator stays at zero.
 91func (p *ProtocolFeeRewardStateResolver) loadTokenSettlement(tokenPath string, view *protocolFeeAccumulatorView) *protocolFeeTokenSettlement {
 92	if tokenState, ok := p.GetTokenState(tokenPath); ok {
 93		return &protocolFeeTokenSettlement{
 94			eventCursor:         tokenState.GetEventCursor(),
 95			segmentStakedAmount: tokenState.GetSegmentStakedAmount(),
 96			segmentStartX128:    tokenState.GetSegmentStartX128().Clone(),
 97			earnedX128:          tokenState.GetEarnedX128().Clone(),
 98		}
 99	}
100
101	settlement := &protocolFeeTokenSettlement{
102		eventCursor:         0,
103		segmentStakedAmount: 0,
104		segmentStartX128:    u256.Zero(),
105		earnedX128:          u256.Zero(),
106	}
107
108	eventCount := p.GetStakeEventCount()
109	if view.firstEpoch < 0 {
110		if view.foldedEpoch >= view.currentEpoch-1 {
111			// No fee was ever folded and nothing from a closed epoch is pending, so
112			// every recorded event earned nothing.
113			settlement.eventCursor = eventCount
114			settlement.segmentStakedAmount = p.GetStakedAmount()
115		}
116		return settlement
117	}
118
119	// Stake events are appended in epoch order, so the last event at or before
120	// firstEpoch is found by binary search: the lookup costs O(log n) reads no matter
121	// how many stake changes the staker made since the token's first fee.
122	lo, hi := int64(0), eventCount
123	for lo < hi {
124		mid := lo + (hi-lo)/2
125		event, ok := p.GetStakeEvent(mid)
126		if !ok {
127			break
128		}
129		if event.GetEpoch() <= view.firstEpoch {
130			lo = mid + 1
131		} else {
132			hi = mid
133		}
134	}
135
136	if lo > 0 {
137		if event, ok := p.GetStakeEvent(lo - 1); ok {
138			settlement.eventCursor = lo
139			settlement.segmentStakedAmount = event.GetStakedAmount()
140		}
141	}
142
143	return settlement
144}
145
146// settleEvents folds up to maxEvents pending stake events into the settlement. An
147// event can only be folded once the epoch before it is fully folded on the
148// accumulator, because the segment it closes ends at that epoch's value. A maxEvents
149// of zero or less folds every event it can. Returns true when no event is left pending.
150func (p *ProtocolFeeRewardStateResolver) settleEvents(settlement *protocolFeeTokenSettlement, view *protocolFeeAccumulatorView, maxEvents int64) bool {
151	eventCount := p.GetStakeEventCount()
152	processed := int64(0)
153
154	for settlement.eventCursor < eventCount {
155		if maxEvents > 0 && processed >= maxEvents {
156			return false
157		}
158
159		event, ok := p.GetStakeEvent(settlement.eventCursor)
160		if !ok {
161			return false
162		}
163		if event.GetEpoch()-1 > view.foldedEpoch {
164			return false
165		}
166
167		segmentEndX128 := view.at(event.GetEpoch() - 1)
168		settlement.earnedX128 = u256.Zero().Add(
169			settlement.earnedX128,
170			segmentRewardX128(settlement.segmentStakedAmount, settlement.segmentStartX128, segmentEndX128),
171		)
172		settlement.segmentStartX128 = segmentEndX128.Clone()
173		settlement.segmentStakedAmount = event.GetStakedAmount()
174		settlement.eventCursor++
175		processed++
176	}
177
178	return true
179}
180
181// closeOpenSegment folds the open segment up to the accumulator's latest value. It may
182// only be called once every stake event is settled, so that the open segment really
183// ends at the present.
184func closeOpenSegment(settlement *protocolFeeTokenSettlement, view *protocolFeeAccumulatorView) {
185	settlement.earnedX128 = u256.Zero().Add(
186		settlement.earnedX128,
187		segmentRewardX128(settlement.segmentStakedAmount, settlement.segmentStartX128, view.current),
188	)
189	settlement.segmentStartX128 = view.current.Clone()
190}
191
192// takeReward removes the whole-unit part of the earned reward from the settlement and
193// returns it. The sub-unit remainder stays earned so that no fee share is lost.
194func takeReward(settlement *protocolFeeTokenSettlement) int64 {
195	rewardX128 := u256.Zero().Rsh(settlement.earnedX128, 128)
196	reward := gnsmath.SafeConvertToInt64(rewardX128)
197	settlement.earnedX128 = u256.Zero().Sub(settlement.earnedX128, u256.Zero().Lsh(rewardX128, 128))
198
199	return reward
200}
201
202// segmentRewardX128 returns stakedAmount times the accumulator growth across a segment.
203func segmentRewardX128(stakedAmount int64, startX128, endX128 *u256.Uint) *u256.Uint {
204	if stakedAmount <= 0 || !endX128.Gt(startX128) {
205		return u256.Zero()
206	}
207
208	growthX128 := u256.Zero().Sub(endX128, startX128)
209
210	return u256.Zero().Mul(growthX128, u256.NewUintFromInt64(stakedAmount))
211}
212
213// storeTokenSettlement writes the settlement back to the staker's state.
214func (p *ProtocolFeeRewardStateResolver) storeTokenSettlement(tokenPath string, settlement *protocolFeeTokenSettlement) {
215	tokenState, ok := p.GetTokenState(tokenPath)
216	if !ok {
217		tokenState = staker.NewProtocolFeeTokenRewardState(settlement.eventCursor, settlement.segmentStakedAmount, settlement.segmentStartX128)
218		tokenState.SetEarnedX128(settlement.earnedX128)
219		p.SetTokenState(tokenPath, tokenState)
220		return
221	}
222
223	tokenState.SetSegment(settlement.eventCursor, settlement.segmentStakedAmount, settlement.segmentStartX128)
224	tokenState.SetEarnedX128(settlement.earnedX128)
225}
226
227// claimTokenReward settles tokenPath and returns the reward collected.
228//
229// Up to maxEvents stake events are folded; whatever is settled by then is paid, and
230// the rest stays pending for a later collect. A maxEvents of zero or less folds
231// everything.
232func (p *ProtocolFeeRewardStateResolver) claimTokenReward(tokenPath string, view *protocolFeeAccumulatorView, maxEvents int64) (int64, error) {
233	// A token that never collected a fee has nothing to settle; skipping it keeps the
234	// staker's state free of an entry per token merely queried.
235	if _, ok := p.GetTokenState(tokenPath); !ok && view.firstEpoch < 0 {
236		return 0, nil
237	}
238
239	settlement := p.loadTokenSettlement(tokenPath, view)
240
241	if p.settleEvents(settlement, view, maxEvents) {
242		closeOpenSegment(settlement, view)
243	}
244
245	reward := takeReward(settlement)
246	p.storeTokenSettlement(tokenPath, settlement)
247
248	if reward > 0 {
249		if tokenState, ok := p.GetTokenState(tokenPath); ok {
250			tokenState.SetClaimedReward(gnsmath.SafeAddInt64(tokenState.GetClaimedReward(), reward))
251		}
252	}
253
254	return reward, nil
255}
256
257// getClaimableReward returns what a collect of tokenPath would pay right now, without
258// changing any state.
259func (p *ProtocolFeeRewardStateResolver) getClaimableReward(tokenPath string, view *protocolFeeAccumulatorView) int64 {
260	settlement := p.loadTokenSettlement(tokenPath, view)
261
262	if p.settleEvents(settlement, view, 0) {
263		closeOpenSegment(settlement, view)
264	}
265
266	return gnsmath.SafeConvertToInt64(u256.Zero().Div(settlement.earnedX128, consts.Q128()))
267}
268
269// addStake increases the staked amount and records the change from epoch on.
270func (p *ProtocolFeeRewardStateResolver) addStake(amount int64, epoch int64) error {
271	if amount <= 0 {
272		return errors.New(errAmountMustBePositive)
273	}
274
275	stakedAmount := gnsmath.SafeAddInt64(p.GetStakedAmount(), amount)
276	p.SetStakedAmount(stakedAmount)
277	p.AppendStakeEvent(epoch, stakedAmount)
278
279	return nil
280}
281
282// removeStake decreases the staked amount and records the change from epoch on.
283func (p *ProtocolFeeRewardStateResolver) removeStake(amount int64, epoch int64) error {
284	if amount < 0 {
285		return errors.New(errAmountMustBeNonNegative)
286	}
287	if amount > p.GetStakedAmount() {
288		return errors.New(errRemoveAmountExceedsStaked)
289	}
290
291	stakedAmount := gnsmath.SafeSubInt64(p.GetStakedAmount(), amount)
292	p.SetStakedAmount(stakedAmount)
293	p.AppendStakeEvent(epoch, stakedAmount)
294
295	return nil
296}
297
298// IsClaimable reports whether every token may be collected at once at currentTimestamp.
299//
300// Parameters:
301//   - currentTimestamp: current Unix timestamp used to compare the last collection.
302//
303// Returns:
304//   - bool: true when currentTimestamp is later than the state's claimed timestamp.
305func (p *ProtocolFeeRewardStateResolver) IsClaimable(currentTimestamp int64) bool {
306	return p.GetClaimedTimestamp() < currentTimestamp
307}