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

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}