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

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}