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