Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}