reward_manager.gno
10.20 Kb · 338 lines
1package launchpad
2
3import (
4 "errors"
5
6 ufmt "gno.land/p/nt/ufmt/v0"
7 "gno.land/r/gnoswap/launchpad"
8
9 gnsmath "gno.land/p/gnoswap/gnsmath/v1"
10 u256 "gno.land/p/gnoswap/uint256/v1"
11)
12
13// Helper functions for RewardManager
14
15func isRewardManagerInitialized(r *launchpad.RewardManager) bool {
16 return r.Rewards().Size() > 0
17}
18
19// isPreDepositRewardRefundable reports whether rewards accrued without any
20// deposit are left unallocated. newRewardManager marks this by starting the
21// accumulated time at the distribute start time.
22func isPreDepositRewardRefundable(r *launchpad.RewardManager) bool {
23 return r.AccumulatedTime() > 0
24}
25
26func getDepositRewardState(r *launchpad.RewardManager, depositId string) (*launchpad.RewardState, error) {
27 rewardStateI := r.Rewards().Get(depositId)
28 if rewardStateI == nil {
29 return nil, makeErrorWithDetails(errNotExistDeposit, ufmt.Sprintf("(%s)", depositId))
30 }
31
32 rewardState, ok := rewardStateI.(*launchpad.RewardState)
33 if !ok {
34 return nil, ufmt.Errorf("failed to cast rewardState to *launchpad.RewardState: %T", rewardStateI)
35 }
36
37 return rewardState, nil
38}
39
40func calculateRewardPerDepositX128(r *launchpad.RewardManager, rewardPerSecondX128 *u256.Uint, totalStaked int64, currentTime int64) (*u256.Uint, error) {
41 accumulatedTime := r.AccumulatedTime()
42 if r.DistributeStartTime() > accumulatedTime {
43 accumulatedTime = r.DistributeStartTime()
44 }
45
46 // not started yet
47 if currentTime < accumulatedTime {
48 return u256.Zero(), nil
49 }
50
51 // past distribute end time
52 if accumulatedTime > r.DistributeEndTime() {
53 return u256.Zero(), nil
54 }
55
56 // past distribute end time, set to distribute end time
57 if currentTime > r.DistributeEndTime() {
58 currentTime = r.DistributeEndTime()
59 }
60
61 if rewardPerSecondX128.IsZero() {
62 return nil, makeErrorWithDetails(
63 errNoLeftReward,
64 ufmt.Sprintf("rewardPerSecond(%d)", rewardPerSecondX128),
65 )
66 }
67
68 // no left reward
69 if totalStaked == 0 {
70 return u256.Zero(), nil
71 }
72
73 // timeDuration * rewardPerSecond / totalStaked
74 timeDuration := currentTime - accumulatedTime
75 rewardPerDepositX128 := u256.MulDiv(
76 u256.NewUintFromInt64(timeDuration),
77 rewardPerSecondX128,
78 u256.NewUintFromInt64(totalStaked),
79 )
80
81 return rewardPerDepositX128, nil
82}
83
84func addRewardStateByDeposit(r *launchpad.RewardManager, deposit launchpad.Deposit) *launchpad.RewardState {
85 // Pre-start deposits accrue rewards from the distribute start time.
86 adjustedDistributeStartTime := deposit.CreatedAt()
87 if adjustedDistributeStartTime < r.DistributeStartTime() {
88 adjustedDistributeStartTime = r.DistributeStartTime()
89 }
90
91 claimableTime := adjustedDistributeStartTime + r.RewardClaimableDuration()
92 if claimableTime > r.DistributeEndTime() {
93 claimableTime = r.DistributeEndTime()
94 }
95
96 rewardState := launchpad.NewRewardState(
97 r.AccumulatedRewardPerDepositX128().Clone(),
98 deposit.DepositAmount(),
99 adjustedDistributeStartTime,
100 r.DistributeEndTime(),
101 claimableTime,
102 )
103
104 // if the first deposit, set the distribute start time
105 if !isRewardManagerInitialized(r) {
106 rewardState.SetDistributeStartTime(r.DistributeStartTime())
107 rewardState.SetDistributeEndTime(r.DistributeEndTime())
108 rewardState.SetAccumulatedTime(r.DistributeStartTime())
109 rewardState.SetPriceDebtX128(u256.Zero())
110 }
111
112 return addRewardState(r, deposit, rewardState)
113}
114
115func addRewardState(r *launchpad.RewardManager, deposit launchpad.Deposit, rewardState *launchpad.RewardState) *launchpad.RewardState {
116 addActiveRewardState(r, rewardState)
117 r.SetReward(deposit.ID(), rewardState)
118
119 return rewardState
120}
121
122func removeRewardState(r *launchpad.RewardManager, depositId string) error {
123 rewardState, err := getDepositRewardState(r, depositId)
124 if err != nil {
125 return err
126 }
127
128 removeActiveRewardState(r, rewardState)
129 r.RemoveReward(depositId)
130 return nil
131}
132
133func addActiveRewardState(r *launchpad.RewardManager, rewardState *launchpad.RewardState) {
134 debt, overflow := u256.Zero().MulOverflow(
135 rewardState.PriceDebtX128(),
136 u256.NewUintFromInt64(rewardState.DepositAmount()),
137 )
138 if overflow {
139 panic(errors.New(errOverflow))
140 }
141
142 r.SetActiveDepositAmount(gnsmath.SafeAddInt64(r.ActiveDepositAmount(), rewardState.DepositAmount()))
143 r.SetActivePriceDebtX128(u256.Zero().Add(r.ActivePriceDebtX128(), debt))
144}
145
146func removeActiveRewardState(r *launchpad.RewardManager, rewardState *launchpad.RewardState) {
147 debt, overflow := u256.Zero().MulOverflow(
148 rewardState.PriceDebtX128(),
149 u256.NewUintFromInt64(rewardState.DepositAmount()),
150 )
151 if overflow {
152 panic(errors.New(errOverflow))
153 }
154
155 r.SetActiveDepositAmount(gnsmath.SafeSubInt64(r.ActiveDepositAmount(), rewardState.DepositAmount()))
156 r.SetActiveClaimedAmount(gnsmath.SafeSubInt64(r.ActiveClaimedAmount(), rewardState.ClaimedAmount()))
157 r.SetActivePriceDebtX128(u256.Zero().Sub(r.ActivePriceDebtX128(), debt))
158}
159
160func addRewardPerDepositX128(r *launchpad.RewardManager, rewardPerDepositX128 *u256.Uint, currentTime int64) error {
161 if rewardPerDepositX128.IsZero() {
162 return nil
163 }
164
165 if r.AccumulatedTime() > currentTime || r.DistributeStartTime() > currentTime {
166 return nil
167 }
168
169 if currentTime > r.DistributeEndTime() {
170 currentTime = r.DistributeEndTime()
171 }
172
173 accumulated := u256.Zero().Add(r.AccumulatedRewardPerDepositX128(), rewardPerDepositX128)
174 r.SetAccumulatedRewardPerDepositX128(accumulated)
175 r.SetAccumulatedTime(currentTime)
176
177 return nil
178}
179
180// updateRewardPerDepositX128 updates the reward per deposit state.
181// This function calculates and updates the accumulated reward per deposit
182// based on the current total deposit amount and time.
183//
184// Parameters:
185// - totalDepositAmount (int64): Current total deposit amount
186// - time (int64): Current timestamp
187//
188// Returns:
189// - error: If the update fails
190func updateRewardPerDepositX128(r *launchpad.RewardManager, totalDepositAmount int64, currentTime int64) error {
191 if currentTime <= 0 {
192 return makeErrorWithDetails(errInvalidTime, "time must be positive")
193 }
194
195 // Calculate and update rewards
196 rewardPerDepositX128, err := calculateRewardPerDepositX128(
197 r,
198 r.DistributeAmountPerSecondX128(),
199 totalDepositAmount,
200 currentTime,
201 )
202 if err != nil {
203 return err
204 }
205
206 // Without deposits, only advance the accumulated time so that the rewards
207 // of this period stay refundable instead of going to the next depositor.
208 if totalDepositAmount == 0 && isPreDepositRewardRefundable(r) {
209 if currentTime > r.DistributeEndTime() {
210 currentTime = r.DistributeEndTime()
211 }
212
213 if currentTime > r.AccumulatedTime() {
214 r.SetAccumulatedTime(currentTime)
215 }
216
217 return nil
218 }
219
220 err = addRewardPerDepositX128(r, rewardPerDepositX128, currentTime)
221 if err != nil {
222 return err
223 }
224
225 return nil
226}
227
228func updateDistributeAmountPerSecondX128(r *launchpad.RewardManager, totalDistributeAmount int64, distributeStartTime int64, distributeEndTime int64) {
229 // Use time duration for per-second calculation
230 timeDuration := distributeEndTime - distributeStartTime
231 if timeDuration <= 0 {
232 return
233 }
234
235 totalDistributeAmountX128 := u256.Zero().Lsh(
236 u256.NewUintFromInt64(totalDistributeAmount),
237 128,
238 )
239
240 // Divide by time duration in seconds
241 amountPerSecondX128 := u256.Zero().Div(
242 totalDistributeAmountX128,
243 u256.NewUintFromInt64(timeDuration),
244 )
245
246 r.SetDistributeAmountPerSecondX128(amountPerSecondX128)
247 r.SetDistributeStartTime(distributeStartTime)
248 r.SetDistributeEndTime(distributeEndTime)
249}
250
251// collectReward processes the reward collection for a specific deposit.
252// This function ensures that the reward collection is valid and updates
253// the claimed amount accordingly.
254//
255// Parameters:
256// - depositId (string): The ID of the deposit
257// - currentTime (int64): Current timestamp
258//
259// Returns:
260// - int64: The amount of reward collected
261// - error: If the collection fails
262func collectReward(r *launchpad.RewardManager, depositId string, currentTime int64) (int64, error) {
263 if currentTime < r.AccumulatedTime() {
264 return 0, makeErrorWithDetails(
265 errInvalidRewardState,
266 ufmt.Sprintf("currentTime %d is less than AccumulatedTime %d", currentTime, r.AccumulatedTime()),
267 )
268 }
269
270 rewardState, err := getDepositRewardState(r, depositId)
271 if err != nil {
272 return 0, err
273 }
274
275 if !isRewardStateClaimable(rewardState, currentTime) {
276 return 0, makeErrorWithDetails(
277 errInvalidRewardState,
278 ufmt.Sprintf("currentTime %d is less than claimableTime %d", currentTime, rewardState.ClaimableTime()),
279 )
280 }
281
282 if currentTime < rewardState.DistributeStartTime() {
283 return 0, makeErrorWithDetails(
284 errInvalidRewardState,
285 ufmt.Sprintf("currentTime %d is less than DistributeStartTime %d", currentTime, rewardState.DistributeStartTime()),
286 )
287 }
288
289 claimableReward := calculateClaimableReward(rewardState, r.AccumulatedRewardPerDepositX128())
290 if claimableReward == 0 {
291 return 0, nil
292 }
293
294 rewardState.SetClaimedAmount(rewardState.ClaimedAmount() + claimableReward)
295 rewards := r.Rewards()
296 rewards.Set(depositId, rewardState)
297 r.SetRewards(rewards)
298 r.SetTotalClaimedAmount(r.TotalClaimedAmount() + claimableReward)
299 r.SetActiveClaimedAmount(gnsmath.SafeAddInt64(r.ActiveClaimedAmount(), claimableReward))
300
301 return claimableReward, nil
302}
303
304// newRewardManager returns a pointer to a new RewardManager with the given values.
305func newRewardManager(
306 totalDistributeAmount int64,
307 distributeStartTime int64,
308 distributeEndTime int64,
309 rewardCollectableDuration int64,
310) *launchpad.RewardManager {
311 manager := launchpad.NewRewardManager(totalDistributeAmount, distributeStartTime, distributeEndTime, rewardCollectableDuration)
312
313 updateDistributeAmountPerSecondX128(manager, totalDistributeAmount, distributeStartTime, distributeEndTime)
314 manager.SetAccumulatedTime(distributeStartTime)
315
316 return manager
317}
318
319// calculateMaximumClaimableRewardsForActiveDeposits reserves every active
320// claim without traversing the attacker-controlled reward tree. Aggregating
321// before Q128 truncation retains rounding dust but cannot under-reserve claims.
322func calculateMaximumClaimableRewardsForActiveDeposits(r *launchpad.RewardManager) int64 {
323 reward, overflow := u256.Zero().MulOverflow(
324 r.AccumulatedRewardPerDepositX128(),
325 u256.NewUintFromInt64(r.ActiveDepositAmount()),
326 )
327 if overflow {
328 panic(errors.New(errOverflow))
329 }
330
331 reward = reward.Sub(reward, r.ActivePriceDebtX128()).Rsh(reward, 128)
332 totalReward := gnsmath.SafeConvertToInt64(reward)
333 if totalReward <= r.ActiveClaimedAmount() {
334 return 0
335 }
336
337 return gnsmath.SafeSubInt64(totalReward, r.ActiveClaimedAmount())
338}