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}