state.gno
22.20 Kb · 667 lines
1package staker
2
3import (
4 "errors"
5 "math"
6
7 gnsmath "gno.land/p/gnoswap/gnsmath/v1"
8 bptree "gno.land/p/nt/bptree/v0"
9 ufmt "gno.land/p/nt/ufmt/v0"
10
11 "gno.land/r/gnoswap/emission"
12 "gno.land/r/gnoswap/gov/staker"
13 pf "gno.land/r/gnoswap/protocol_fee"
14)
15
16// setUnDelegationLockupPeriod updates the undelegation lockup period.
17// This affects all future undelegation operations.
18//
19// Parameters:
20// - period: new lockup period in seconds
21func (g *govStakerV1) setUnDelegationLockupPeriod(_ int, rlm realm, period int64) {
22 if err := g.store.SetUnDelegationLockupPeriod(0, rlm, period); err != nil {
23 panic(err)
24 }
25}
26
27// nextDelegationID generates and returns the next unique delegation ID.
28//
29// Returns:
30// - int64: next available delegation ID
31func (g *govStakerV1) nextDelegationID() int64 {
32 counter := g.store.GetDelegationCounter()
33
34 return counter.Next()
35}
36
37// getDelegation retrieves a delegation by its ID.
38//
39// Parameters:
40// - delegationID: unique identifier of the delegation
41//
42// Returns:
43// - *Delegation: delegation instance or nil if not found
44func (g *govStakerV1) getDelegation(delegationID int64) *staker.Delegation {
45 delegation, exists := g.store.GetDelegation(delegationID)
46 if !exists {
47 return nil
48 }
49 return delegation
50}
51
52// setDelegation stores or updates a delegation in the storage tree.
53//
54// Parameters:
55// - delegationID: unique identifier of the delegation
56// - delegation: delegation instance to store
57//
58// Returns:
59// - error: nil on success, otherwise the storage write error
60func (g *govStakerV1) setDelegation(_ int, rlm realm, delegationID int64, delegation *staker.Delegation) error {
61 return g.store.SetDelegation(0, rlm, delegationID, delegation)
62}
63
64// addDelegation adds a new delegation to storage and updates the delegation manager.
65//
66// Parameters:
67// - delegationID: unique identifier of the delegation to add
68// - delegation: delegation instance to add
69//
70// Returns:
71// - error: nil on success, otherwise the delegation or manager storage error
72func (g *govStakerV1) addDelegation(_ int, rlm realm, delegationID int64, delegation *staker.Delegation) error {
73 if err := g.setDelegation(0, rlm, delegationID, delegation); err != nil {
74 return err
75 }
76
77 delegationManager := g.store.GetDelegationManager()
78 resolvedManager := NewDelegationManagerResolver(delegationManager)
79 resolvedManager.addDelegation(
80 delegation.DelegateFrom(),
81 delegation.DelegateTo(),
82 delegationID,
83 )
84
85 return g.store.SetDelegationManager(0, rlm, delegationManager)
86}
87
88// removeDelegation removes a delegation from storage and updates the delegation manager.
89//
90// Parameters:
91// - delegationID: unique identifier of the delegation to remove
92//
93// Returns:
94// - error: nil on success, otherwise an error when the delegation is absent or storage fails
95func (g *govStakerV1) removeDelegation(_ int, rlm realm, delegationID int64) error {
96 delegation := g.getDelegation(delegationID)
97 if delegation == nil {
98 return errors.New(errDelegationNotFound)
99 }
100
101 if err := g.store.RemoveDelegation(0, rlm, delegationID); err != nil {
102 return err
103 }
104
105 delegationManager := g.store.GetDelegationManager()
106 resolvedManager := NewDelegationManagerResolver(delegationManager)
107 resolvedManager.removeDelegation(
108 delegation.DelegateFrom(),
109 delegation.DelegateTo(),
110 delegationID,
111 )
112
113 return g.store.SetDelegationManager(0, rlm, delegationManager)
114}
115
116// getUserDelegations retrieves all delegations for a specific user.
117//
118// Parameters:
119// - user: user's address
120//
121// Returns:
122// - *bptree.BPTree: tree of user's delegations
123func (g *govStakerV1) getUserDelegations(user address) *bptree.BPTree {
124 delegationManager := g.store.GetDelegationManager()
125
126 userDelegations, exists := delegationManager.GetDelegatorDelegations(user.String())
127 if !exists {
128 return staker.NewUserDelegationTree()
129 }
130
131 return userDelegations
132}
133
134// getUserDelegationsWithDelegatee retrieves all delegations from a user to a specific delegatee.
135//
136// Parameters:
137// - user: user's address
138// - delegatee: delegatee's address
139//
140// Returns:
141// - []int64: list of user's delegation IDs to the delegatee
142func (g *govStakerV1) getUserDelegationIDsWithDelegatee(user address, delegatee address) []int64 {
143 delegationManager := g.store.GetDelegationManager()
144
145 userDelegations, exists := delegationManager.GetDelegatorDelegations(user.String())
146 if !exists {
147 return nil
148 }
149
150 delegateeStr := delegatee.String()
151 delegationIDs := userDelegations.Get(delegateeStr)
152 if delegationIDs == nil {
153 return nil
154 }
155
156 ids, ok := delegationIDs.([]int64)
157 if !ok {
158 return nil
159 }
160
161 return ids
162}
163
164// addDelegationRecord records a delegation change in the history.
165// Updates both total and user delegation histories with cumulative values.
166//
167// Parameters:
168// - delegateeAddr: address of the delegatee
169// - amount: amount change (positive for delegate, negative for undelegate)
170// - timestamp: timestamp of the delegation change
171func (g *govStakerV1) addDelegationRecord(_ int, rlm realm, delegateeAddr address, amount int64, timestamp int64) {
172 // Update total delegation history
173 g.updateTotalDelegationHistory(0, rlm, amount, timestamp)
174
175 // Update user delegation history
176 g.updateUserDelegationHistory(0, rlm, delegateeAddr, amount, timestamp)
177}
178
179// updateTotalDelegationHistory updates the total delegation history with cumulative value.
180//
181// Parameters:
182// - amount: amount change (positive for delegate, negative for undelegate)
183// - timestamp: timestamp of the change
184func (g *govStakerV1) updateTotalDelegationHistory(_ int, rlm realm, amount int64, timestamp int64) {
185 history := g.store.GetTotalDelegationHistory()
186
187 // Get current total from the most recent entry
188 currentTotal := g.getLatestTotalDelegation(history)
189 newTotal := gnsmath.SafeAddInt64(currentTotal, amount)
190
191 if newTotal < 0 {
192 newTotal = 0
193 }
194
195 history.Set(timestamp, newTotal)
196
197 if err := g.store.SetTotalDelegationHistory(0, rlm, history); err != nil {
198 panic(err)
199 }
200}
201
202// updateUserDelegationHistory updates the user delegation history with cumulative values.
203// Structure: single BPTree keyed by composite key "addrStr|paddedTimestamp" -> int64
204//
205// Parameters:
206// - delegateeAddr: address of the delegatee
207// - amount: amount change (positive for delegate, negative for undelegate)
208// - timestamp: timestamp of the change
209func (g *govStakerV1) updateUserDelegationHistory(_ int, rlm realm, delegateeAddr address, amount int64, timestamp int64) {
210 history := g.store.GetUserDelegationHistory()
211 addrStr := delegateeAddr.String()
212
213 // Get current amount from the most recent entry for this user
214 currentAmount := g.getLatestUserDelegationByAddress(history, addrStr)
215 newAmount := gnsmath.SafeAddInt64(currentAmount, amount)
216 if newAmount < 0 {
217 newAmount = 0
218 }
219
220 // Store new cumulative value under composite key
221 history.Set(makeUserHistoryKey(addrStr, timestamp), newAmount)
222
223 if err := g.store.SetUserDelegationHistory(0, rlm, history); err != nil {
224 panic(err)
225 }
226}
227
228// getLatestTotalDelegation gets the latest total delegation amount from history.
229func (g *govStakerV1) getLatestTotalDelegation(history *staker.UintTree) int64 {
230 if history.Size() == 0 {
231 return 0
232 }
233
234 latestTotal := int64(0)
235
236 history.ReverseIterate(0, math.MaxInt64, func(key int64, value any) bool {
237 totalInt, ok := value.(int64)
238 if !ok {
239 panic(ufmt.Sprintf("invalid total type: %T", value))
240 }
241
242 latestTotal = totalInt
243
244 return true // stop after first (most recent) entry
245 })
246
247 return latestTotal
248}
249
250// getLatestUserDelegationByAddress gets the latest delegation amount for a user
251// from the composite-keyed user delegation history.
252func (g *govStakerV1) getLatestUserDelegationByAddress(history *bptree.BPTree, addrStr string) int64 {
253 lo, hi := userHistoryKeyRange(addrStr)
254
255 latestAmount := int64(0)
256
257 history.ReverseIterate(lo, hi, func(_ string, value any) bool {
258 amountInt, ok := value.(int64)
259 if !ok {
260 panic(ufmt.Sprintf("invalid amount type: %T", value))
261 }
262
263 latestAmount = amountInt
264
265 return true // stop after first (most recent) entry
266 })
267
268 return latestAmount
269}
270
271// addStakeEmissionReward adds stake to emission reward tracking for an address.
272// This method updates the emission reward distribution state and adds stake for the specified address.
273//
274// Parameters:
275// - address: staker's address
276// - amount: amount of stake to add
277// - currentTimestamp: current timestamp
278func (g *govStakerV1) addStakeEmissionReward(_ int, rlm realm, address string, amount int64, currentTimestamp int64) error {
279 distributedAmount := emission.GetAccuDistributedToGovStaker()
280
281 emissionRewardManager := g.store.GetEmissionRewardManager()
282 resolvedManager := NewEmissionRewardManagerResolver(emissionRewardManager)
283
284 if err := resolvedManager.updateAccumulatedRewardX128PerStake(distributedAmount, currentTimestamp); err != nil {
285 return err
286 }
287
288 if err := resolvedManager.addStake(address, amount, currentTimestamp); err != nil {
289 return err
290 }
291
292 err := g.store.SetEmissionRewardManager(0, rlm, emissionRewardManager)
293 if err != nil {
294 return err
295 }
296
297 // Emit the emission reward accumulation
298 emitUpdateEmissionRewardAccumulation(emissionRewardManager)
299
300 return nil
301}
302
303// removeStakeEmissionReward removes stake from emission reward tracking for an address.
304// This method updates the emission reward distribution state and removes stake for the specified address.
305//
306// Parameters:
307// - address: staker's address
308// - amount: amount of stake to remove
309// - currentTimestamp: current timestamp
310func (g *govStakerV1) removeStakeEmissionReward(_ int, rlm realm, address string, amount int64, currentTimestamp int64) error {
311 distributedAmount := emission.GetAccuDistributedToGovStaker()
312
313 emissionRewardManager := g.store.GetEmissionRewardManager()
314 resolvedManager := NewEmissionRewardManagerResolver(emissionRewardManager)
315
316 if err := resolvedManager.updateAccumulatedRewardX128PerStake(distributedAmount, currentTimestamp); err != nil {
317 return err
318 }
319
320 if err := resolvedManager.removeStake(address, amount, currentTimestamp); err != nil {
321 return err
322 }
323
324 if err := g.store.SetEmissionRewardManager(0, rlm, emissionRewardManager); err != nil {
325 return err
326 }
327
328 // Emit the emission reward accumulation
329 emitUpdateEmissionRewardAccumulation(emissionRewardManager)
330
331 return nil
332}
333
334// claimRewardsEmissionReward claims emission rewards for an address.
335// This method updates the emission reward distribution state and processes reward claiming.
336//
337// Parameters:
338// - address: staker's address claiming rewards
339// - currentTimestamp: current timestamp
340//
341// Returns:
342// - int64: amount of emission rewards claimed
343// - error: nil on success, error if claiming fails
344func (g *govStakerV1) claimRewardsEmissionReward(_ int, rlm realm, address string, currentTimestamp int64) (int64, error) {
345 distributedAmount := emission.GetAccuDistributedToGovStaker()
346
347 emissionRewardManager := g.store.GetEmissionRewardManager()
348 resolvedManager := NewEmissionRewardManagerResolver(emissionRewardManager)
349
350 if err := resolvedManager.updateAccumulatedRewardX128PerStake(distributedAmount, currentTimestamp); err != nil {
351 return 0, err
352 }
353
354 amount, err := resolvedManager.claimRewards(address, currentTimestamp)
355 if err != nil {
356 return 0, err
357 }
358
359 if err := g.store.SetEmissionRewardManager(0, rlm, emissionRewardManager); err != nil {
360 return 0, err
361 }
362
363 // Emit the emission reward accumulation
364 emitUpdateEmissionRewardAccumulation(emissionRewardManager)
365
366 return amount, nil
367}
368
369// removeLaunchpadProjectDeposit removes a launchpad project deposit record.
370//
371// Parameters:
372// - ownerAddress: project owner's address identifier
373//
374// Returns:
375// - bool: true if successfully removed
376func (g *govStakerV1) removeLaunchpadProjectDeposit(ownerAddress string) bool {
377 launchpadProjectDeposits := g.store.GetLaunchpadProjectDeposits()
378 return launchpadProjectDeposits.RemoveDeposit(ownerAddress)
379}
380
381// addStakeProtocolFeeReward records a stake increase for protocol fee rewards.
382//
383// The change only opens a new accrual epoch and appends one stake event, so its cost
384// does not depend on how many tokens collected fees.
385//
386// Parameters:
387// - address: staker's address
388// - amount: amount of stake to add
389// - currentTimestamp: current timestamp
390func (g *govStakerV1) addStakeProtocolFeeReward(_ int, rlm realm, address string, amount int64, currentTimestamp int64) error {
391 epoch := pf.AdvanceAccrualEpoch(cross(rlm))
392
393 protocolFeeRewardManager := g.store.GetProtocolFeeRewardManager()
394 resolvedManager := NewProtocolFeeRewardManagerResolver(protocolFeeRewardManager)
395
396 if err := resolvedManager.addStake(address, amount, epoch); err != nil {
397 return err
398 }
399 resolvedManager.SetAccumulatedTimestamp(currentTimestamp)
400
401 if err := g.store.SetProtocolFeeRewardManager(0, rlm, protocolFeeRewardManager); err != nil {
402 return err
403 }
404
405 emitUpdateProtocolFeeStakeAccrual(protocolFeeRewardManager)
406
407 return nil
408}
409
410// removeStakeProtocolFeeReward records a stake decrease for protocol fee rewards.
411// Like addStakeProtocolFeeReward it never folds fees, so a withdrawal never pays for
412// the reward calculation; that happens on collect, one token at a time if need be.
413//
414// Parameters:
415// - address: staker's address
416// - amount: amount of stake to remove
417// - currentTimestamp: current timestamp
418func (g *govStakerV1) removeStakeProtocolFeeReward(_ int, rlm realm, address string, amount int64, currentTimestamp int64) error {
419 epoch := pf.AdvanceAccrualEpoch(cross(rlm))
420
421 protocolFeeRewardManager := g.store.GetProtocolFeeRewardManager()
422 resolvedManager := NewProtocolFeeRewardManagerResolver(protocolFeeRewardManager)
423
424 if err := resolvedManager.removeStake(address, amount, epoch); err != nil {
425 return err
426 }
427 resolvedManager.SetAccumulatedTimestamp(currentTimestamp)
428
429 if err := g.store.SetProtocolFeeRewardManager(0, rlm, protocolFeeRewardManager); err != nil {
430 return err
431 }
432
433 emitUpdateProtocolFeeStakeAccrual(protocolFeeRewardManager)
434
435 return nil
436}
437
438// foldProtocolFeeAccrual consumes up to limit pending accrual buckets of tokenPath from
439// the protocol fee realm and folds them into the token's accumulator. A limit of zero
440// or less consumes every pending bucket.
441func (g *govStakerV1) foldProtocolFeeAccrual(_ int, rlm realm, resolvedManager *ProtocolFeeRewardManagerResolver, tokenPath string, limit int, currentTimestamp int64) error {
442 epochs, amounts := pf.ConsumeAccrualBuckets(cross(rlm), tokenPath, limit)
443 exhausted := limit <= 0 || len(epochs) < limit
444
445 return resolvedManager.applyAccrualBuckets(tokenPath, epochs, amounts, exhausted, currentTimestamp)
446}
447
448// claimRewardsProtocolFeeReward claims protocol fee rewards for an address in every token.
449// It folds every pending token first, so its cost grows with the number of tokens
450// that collected a fee. claimRewardProtocolFeeRewardByTokenPath is the bounded path.
451//
452// Parameters:
453// - address: staker's address claiming rewards
454// - currentTimestamp: current timestamp
455//
456// Returns:
457// - map[string]int64: protocol fee rewards claimed by token
458// - error: nil on success, error if claiming fails
459func (g *govStakerV1) claimRewardsProtocolFeeReward(_ int, rlm realm, address string, currentTimestamp int64) (map[string]int64, error) {
460 // Claiming pays out of gov/staker's own balance, so the reserved fees are pulled in
461 // from the protocol fee realm first.
462 pf.DistributeProtocolFee(cross(rlm))
463
464 protocolFeeRewardManager := g.store.GetProtocolFeeRewardManager()
465 resolvedManager := NewProtocolFeeRewardManagerResolver(protocolFeeRewardManager)
466
467 // Every pending token is consumed from the protocol fee realm. A known token with
468 // nothing pending has no bucket in any epoch since its last fold, so an empty
469 // exhaustive fold is enough to mark every closed epoch as settled for it.
470 pendingTokenPaths := pf.GetAccrualPendingTokens()
471 pending := make(map[string]bool, len(pendingTokenPaths))
472 for _, tokenPath := range pendingTokenPaths {
473 pending[tokenPath] = true
474 if err := g.foldProtocolFeeAccrual(0, rlm, resolvedManager, tokenPath, 0, currentTimestamp); err != nil {
475 return nil, err
476 }
477 }
478
479 foldedTokenPaths := pendingTokenPaths
480 for _, tokenPath := range protocolFeeRewardManager.GetTokenPaths() {
481 if pending[tokenPath] {
482 continue
483 }
484 if err := resolvedManager.applyAccrualBuckets(tokenPath, []int64{}, []int64{}, true, currentTimestamp); err != nil {
485 return nil, err
486 }
487 foldedTokenPaths = append(foldedTokenPaths, tokenPath)
488 }
489
490 rewards, err := resolvedManager.claimRewards(address, currentTimestamp)
491 if err != nil {
492 return nil, err
493 }
494
495 if err := g.store.SetProtocolFeeRewardManager(0, rlm, protocolFeeRewardManager); err != nil {
496 return nil, err
497 }
498
499 emitUpdateProtocolFeeRewardAccumulation(protocolFeeRewardManager, foldedTokenPaths)
500
501 return rewards, nil
502}
503
504// claimRewardProtocolFeeRewardByTokenPath claims one protocol fee reward token for an address.
505//
506// Both the fold and the settlement are bounded per call (maxAccrualBucketsPerCollect,
507// maxStakeEventsPerCollect); a token with more pending work pays what is settled and
508// keeps the rest for a later collect.
509func (g *govStakerV1) claimRewardProtocolFeeRewardByTokenPath(_ int, rlm realm, address string, tokenPath string, currentTimestamp int64) (int64, error) {
510 pf.DistributeProtocolFeeByTokenPath(cross(rlm), tokenPath)
511
512 protocolFeeRewardManager := g.store.GetProtocolFeeRewardManager()
513 resolvedManager := NewProtocolFeeRewardManagerResolver(protocolFeeRewardManager)
514
515 if err := g.foldProtocolFeeAccrual(0, rlm, resolvedManager, tokenPath, maxAccrualBucketsPerCollect, currentTimestamp); err != nil {
516 return 0, err
517 }
518
519 reward, err := resolvedManager.claimTokenReward(address, tokenPath, maxStakeEventsPerCollect)
520 if err != nil {
521 return 0, err
522 }
523
524 if err := g.store.SetProtocolFeeRewardManager(0, rlm, protocolFeeRewardManager); err != nil {
525 return 0, err
526 }
527
528 emitUpdateProtocolFeeRewardAccumulation(protocolFeeRewardManager, []string{tokenPath})
529
530 return reward, nil
531}
532
533// getClaimableProtocolFeeRewards reports what collecting every token would pay for a
534// reward ID right now, pending accrual buckets included, without changing any state.
535func (g *govStakerV1) getClaimableProtocolFeeRewards(rewardID string) (map[string]int64, error) {
536 protocolFeeRewardManager := g.store.GetProtocolFeeRewardManager()
537 resolvedManager := NewProtocolFeeRewardManagerResolver(protocolFeeRewardManager)
538
539 rewards := make(map[string]int64)
540 tokenPaths := protocolFeeRewardManager.GetTokenPaths()
541 for _, tokenPath := range pf.GetAccrualPendingTokens() {
542 if _, ok := protocolFeeRewardManager.GetTokenAccumulator(tokenPath); !ok {
543 tokenPaths = append(tokenPaths, tokenPath)
544 }
545 }
546
547 for _, tokenPath := range tokenPaths {
548 pendingEpochs, pendingAmounts := pf.GetAccrualBuckets(tokenPath, 0)
549 reward, err := resolvedManager.GetClaimableRewardAmount(rewardID, tokenPath, pendingEpochs, pendingAmounts)
550 if err != nil {
551 return nil, err
552 }
553 rewards[tokenPath] = reward
554 }
555
556 return rewards, nil
557}
558
559// getLaunchpadProjectDeposit retrieves the deposit amount for a launchpad project.
560//
561// Parameters:
562// - ownerAddress: project owner's address identifier
563//
564// Returns:
565// - int64: deposit amount
566// - bool: true if project exists, false otherwise
567func (g *govStakerV1) getLaunchpadProjectDeposit(ownerAddress string) (int64, bool) {
568 launchpadDeposits := g.store.GetLaunchpadProjectDeposits()
569 resolvedDeposits := NewLaunchpadProjectDepositsResolver(launchpadDeposits)
570 return resolvedDeposits.getLaunchpadProjectDeposit(ownerAddress)
571}
572
573// setLaunchpadProjectDeposit sets the deposit amount for a launchpad project.
574//
575// Parameters:
576// - ownerAddress: project owner's address identifier
577// - deposit: deposit amount to set
578//
579// Returns:
580// - error: nil on success, otherwise the storage write error
581func (g *govStakerV1) setLaunchpadProjectDeposit(_ int, rlm realm, ownerAddress string, deposit int64) error {
582 launchpadDeposits := g.store.GetLaunchpadProjectDeposits()
583 resolvedDeposits := NewLaunchpadProjectDepositsResolver(launchpadDeposits)
584 resolvedDeposits.setLaunchpadProjectDeposit(ownerAddress, deposit)
585
586 return g.store.SetLaunchpadProjectDeposits(0, rlm, launchpadDeposits)
587}
588
589// addStakeFromLaunchpad adds stake for a launchpad project and updates reward tracking.
590// This method creates a special reward ID for launchpad projects and manages their deposit tracking.
591//
592// Parameters:
593// - address: project wallet address
594// - amount: amount of stake to add
595// - currentTimestamp: current timestamp
596func (g *govStakerV1) addStakeFromLaunchpad(_ int, rlm realm, address string, amount int64, currentTimestamp int64) error {
597 launchpadRewardID := g.makeLaunchpadRewardID(address)
598 err := g.addStakeEmissionReward(0, rlm, launchpadRewardID, amount, currentTimestamp)
599 if err != nil {
600 return err
601 }
602
603 err = g.addStakeProtocolFeeReward(0, rlm, launchpadRewardID, amount, currentTimestamp)
604 if err != nil {
605 return err
606 }
607
608 deposit, exists := g.getLaunchpadProjectDeposit(launchpadRewardID)
609 if !exists {
610 deposit = 0
611 }
612
613 deposit = gnsmath.SafeAddInt64(deposit, amount)
614 if err := g.setLaunchpadProjectDeposit(0, rlm, launchpadRewardID, deposit); err != nil {
615 return err
616 }
617
618 return nil
619}
620
621// removeStakeFromLaunchpad removes stake for a launchpad project and updates reward tracking.
622// This method manages launchpad project deposit tracking and ensures non-negative deposits.
623//
624// Parameters:
625// - address: project wallet address
626// - amount: amount of stake to remove
627// - currentTimestamp: current timestamp
628func (g *govStakerV1) removeStakeFromLaunchpad(_ int, rlm realm, address string, amount int64, currentTimestamp int64) error {
629 launchpadRewardID := g.makeLaunchpadRewardID(address)
630 err := g.removeStakeEmissionReward(0, rlm, launchpadRewardID, amount, currentTimestamp)
631 if err != nil {
632 return err
633 }
634
635 err = g.removeStakeProtocolFeeReward(0, rlm, launchpadRewardID, amount, currentTimestamp)
636 if err != nil {
637 return err
638 }
639
640 deposit, exists := g.getLaunchpadProjectDeposit(launchpadRewardID)
641 if !exists {
642 deposit = 0
643 }
644
645 deposit = gnsmath.SafeSubInt64(deposit, amount)
646 if deposit < 0 {
647 deposit = 0
648 }
649
650 if err := g.setLaunchpadProjectDeposit(0, rlm, launchpadRewardID, deposit); err != nil {
651 return err
652 }
653
654 return nil
655}
656
657// makeLaunchpadRewardID creates a special reward identifier for launchpad projects.
658// This ensures launchpad project rewards are tracked separately from regular user stakes.
659//
660// Parameters:
661// - address: project wallet address
662//
663// Returns:
664// - string: formatted launchpad reward ID
665func (g *govStakerV1) makeLaunchpadRewardID(address string) string {
666 return "launchpad:" + address
667}