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

staker_delegate.gno

17.22 Kb · 677 lines
  1package staker
  2
  3import (
  4	"chain"
  5	"chain/runtime"
  6	"errors"
  7	"time"
  8
  9	gnsmath "gno.land/p/gnoswap/gnsmath/v1"
 10	"gno.land/p/gnoswap/utils/v1"
 11	"gno.land/r/gnoswap/access/v1"
 12	"gno.land/r/gnoswap/emission"
 13	"gno.land/r/gnoswap/gns"
 14
 15	"gno.land/r/gnoswap/gov/staker"
 16	"gno.land/r/gnoswap/gov/xgns"
 17	"gno.land/r/gnoswap/halt/v1"
 18	"gno.land/r/gnoswap/referral/v1"
 19)
 20
 21// Spoofed-realm guards on entry points reject any caller that fabricates a
 22// realm value distinct from the current crossing frame. The proxy in
 23// gno.land/r/gnoswap/gov/staker always forwards its own `cur`, so a mismatch
 24// here means somebody bypassed the proxy and threaded a fake realm directly
 25// into the implementation.
 26
 27// Delegate delegates GNS tokens to an address.
 28//
 29// Converts GNS to xGNS and assigns voting power.
 30// Primary mechanism for participating in governance.
 31// Can delegate to self or any other address.
 32//
 33// Parameters:
 34//
 35//   - _: Noncrossing implementation-call discriminator; pass 0.
 36//
 37//   - rlm: Current realm context forwarded unchanged by the governance-staker proxy.
 38//
 39//   - to: Address to receive voting power (can be self)
 40//
 41//   - amount: Amount of GNS to stake and delegate
 42//
 43//   - referrer: Optional referral address for tracking
 44//
 45// Process:
 46//  1. Transfers GNS from caller
 47//  2. Mints equivalent xGNS (1:1 ratio)
 48//  3. Assigns voting power to target address
 49//  4. Creates delegation snapshot for voting
 50//
 51// Requirements:
 52//   - Minimum 1 GNS delegation
 53//   - Valid target address
 54//   - Sufficient GNS balance
 55//   - Approval for GNS transfer
 56//
 57// Returns:
 58//   - delegatedAmount: amount of GNS delegated and mirrored as xGNS
 59func (gs *govStakerV1) Delegate(
 60	_ int,
 61	rlm realm,
 62	to address,
 63	amount int64,
 64	referrer string,
 65) int64 {
 66	access.AssertIsRlmCurrent(0, rlm)
 67
 68	halt.AssertIsNotHaltedGovStaker()
 69
 70	prev := rlm.Previous()
 71	access.AssertIsValidAddress(to)
 72
 73	assertIsValidDelegateAmount(amount)
 74
 75	caller := prev.Address()
 76	from := caller
 77	currentHeight := runtime.ChainHeight()
 78	currentTimestamp := time.Now().Unix()
 79
 80	emission.MintAndDistributeGns(cross(rlm))
 81
 82	delegation, err := gs.delegate(
 83		0,
 84		rlm,
 85		from,
 86		to,
 87		amount,
 88		currentHeight,
 89		currentTimestamp,
 90	)
 91	if err != nil {
 92		panic(err)
 93	}
 94
 95	if err := gs.increaseTotalDelegatedAmount(0, rlm, amount); err != nil {
 96		panic(err)
 97	}
 98	if err := gs.increaseTotalLockedAmount(0, rlm, amount); err != nil {
 99		panic(err)
100	}
101
102	gns.TransferFrom(cross(rlm), from, rlm.Address(), amount)
103	xgns.Mint(cross(rlm), from, amount)
104
105	registeredReferrer := referral.TryRegister(cross(rlm), caller, referrer)
106
107	resolver := NewDelegationResolver(delegation)
108
109	chain.Emit(
110		"Delegate",
111		"prevAddr", prev.Address().String(),
112		"prevRealm", prev.PkgPath(),
113		"from", resolver.delegation.DelegateFrom().String(),
114		"to", resolver.delegation.DelegateTo().String(),
115		"amount", utils.FormatInt(resolver.DelegatedAmount()),
116		"totalDelegatedAmount", utils.FormatInt(gs.store.GetTotalDelegatedAmount()),
117		"referrer", registeredReferrer,
118	)
119
120	return amount
121}
122
123// Undelegate undelegates xGNS from the existing delegate.
124//
125// Initiates withdrawal of staked GNS with lockup period.
126// Voting power removed immediately, tokens locked for configurable period.
127// Prevents governance attacks through time delay.
128//
129// Parameters:
130//
131//   - _: Noncrossing implementation-call discriminator; pass 0.
132//
133//   - rlm: Current realm context forwarded unchanged by the governance-staker proxy.
134//
135//   - from: Address currently delegated to
136//
137//   - amount: Amount of xGNS to undelegate
138//
139// Process:
140//  1. Removes voting power immediately
141//  2. Creates withdrawal request with timestamp
142//  3. Locks GNS for configurable cooldown period
143//
144// Requirements:
145//   - Must have delegated to target address
146//   - Sufficient delegated amount
147//
148// After lockup period ends, use CollectUndelegatedGns() to claim GNS.
149//
150// Returns:
151//   - undelegatedAmount: amount moved out of active delegation into undelegation lockup
152func (gs *govStakerV1) Undelegate(
153	_ int,
154	rlm realm,
155	from address,
156	amount int64,
157) int64 {
158	access.AssertIsRlmCurrent(0, rlm)
159
160	halt.AssertIsNotHaltedWithdraw()
161
162	prev := rlm.Previous()
163	caller := prev.Address()
164	access.AssertIsValidAddress(from)
165
166	assertIsValidDelegateAmount(amount)
167
168	currentHeight := runtime.ChainHeight()
169	currentTimestamp := time.Now().Unix()
170
171	emission.MintAndDistributeGns(cross(rlm))
172
173	unDelegationAmount, err := gs.unDelegate(
174		0,
175		rlm,
176		caller,
177		from,
178		amount,
179		currentHeight,
180		currentTimestamp,
181	)
182	if err != nil {
183		panic(err)
184	}
185
186	if err := gs.decreaseTotalDelegatedAmount(0, rlm, unDelegationAmount); err != nil {
187		panic(err)
188	}
189
190	chain.Emit(
191		"Undelegate",
192		"prevAddr", prev.Address().String(),
193		"prevRealm", prev.PkgPath(),
194		"from", caller.String(),
195		"to", from.String(),
196		"amount", utils.FormatInt(unDelegationAmount),
197		"totalDelegatedAmount", utils.FormatInt(gs.store.GetTotalDelegatedAmount()),
198	)
199
200	return unDelegationAmount
201}
202
203// Redelegate redelegates xGNS from an existing delegate to another.
204//
205// Atomic operation to change delegation target.
206// Maintains voting power continuity without unstaking.
207// Useful for vote delegation services and DAO coordination.
208//
209// Parameters:
210//
211//   - _: Noncrossing implementation-call discriminator; pass 0.
212//
213//   - rlm: Current realm context forwarded unchanged by the governance-staker proxy.
214//
215//   - delegatee: Current address delegated to
216//
217//   - newDelegatee: New address to delegate to
218//
219//   - amount: Amount of xGNS to redelegate
220//
221// Process:
222//  1. Validates current delegation exists
223//  2. Removes voting power from the old delegatee without a lockup
224//  3. Assigns the same amount to the new delegatee
225//  4. Updates delegation history and reward stake events
226//
227// Requirements:
228//   - Must have active delegation to current delegatee
229//   - Both addresses must be valid
230//   - Amount must not exceed current delegation
231//   - Cannot redelegate to same address
232//
233// No user-facing lockup period; reward accounting records one removal and one
234// addition across the corresponding accrual epochs.
235//
236// Returns:
237//   - redelegatedAmount: amount moved from the current delegatee to the new delegatee
238func (gs *govStakerV1) Redelegate(
239	_ int,
240	rlm realm,
241	delegatee,
242	newDelegatee address,
243	amount int64,
244) int64 {
245	access.AssertIsRlmCurrent(0, rlm)
246
247	halt.AssertIsNotHaltedGovStaker()
248
249	prev := rlm.Previous()
250	caller := prev.Address()
251	access.AssertIsValidAddress(delegatee)
252	access.AssertIsValidAddress(newDelegatee)
253
254	assertIsValidDelegateAmount(amount)
255	assertNoSameDelegatee(delegatee, newDelegatee)
256
257	currentHeight := runtime.ChainHeight()
258	currentTimestamp := time.Now().Unix()
259	delegator := caller
260
261	emission.MintAndDistributeGns(cross(rlm))
262
263	unDelegationAmount, err := gs.unDelegateWithoutLockup(
264		0,
265		rlm,
266		delegator,
267		delegatee,
268		amount,
269		currentHeight,
270		currentTimestamp,
271	)
272	if err != nil {
273		panic(err)
274	}
275
276	delegation, err := gs.delegate(
277		0,
278		rlm,
279		delegator,
280		newDelegatee,
281		unDelegationAmount,
282		currentHeight,
283		currentTimestamp,
284	)
285	if err != nil {
286		panic(err)
287	}
288
289	resolver := NewDelegationResolver(delegation)
290	chain.Emit(
291		"Redelegate",
292		"prevAddr", prev.Address().String(),
293		"prevRealm", prev.PkgPath(),
294		"from", delegator.String(),
295		"previousDelegatee", delegatee.String(),
296		"newDelegatee", newDelegatee.String(),
297		"amount", utils.FormatInt(resolver.DelegatedAmount()),
298	)
299
300	return amount
301}
302
303// CollectUndelegatedGns collects undelegated GNS tokens.
304// Allows users to collect GNS tokens that completed undelegation lockup period.
305// Burns xGNS and returns GNS tokens.
306//
307// Parameters:
308//   - _: Noncrossing implementation-call discriminator; pass 0.
309//   - rlm: Current realm context forwarded unchanged by the governance-staker proxy.
310//
311// Returns:
312//   - collectedAmount: amount of undelegated GNS released to the caller after lockup expiry
313func (gs *govStakerV1) CollectUndelegatedGns(_ int, rlm realm) int64 {
314	access.AssertIsRlmCurrent(0, rlm)
315
316	halt.AssertIsNotHaltedWithdraw()
317
318	prev := rlm.Previous()
319	caller := prev.Address()
320	currentTime := time.Now().Unix()
321
322	emission.MintAndDistributeGns(cross(rlm))
323
324	collectedAmount, err := gs.collectDelegations(0, rlm, caller, currentTime)
325	if err != nil {
326		panic(err)
327	}
328
329	if collectedAmount == 0 {
330		return 0
331	}
332
333	if err := gs.decreaseTotalLockedAmount(0, rlm, collectedAmount); err != nil {
334		panic(err)
335	}
336
337	xgns.Burn(cross(rlm), caller, collectedAmount)
338	gns.Transfer(cross(rlm), caller, collectedAmount)
339
340	chain.Emit(
341		"CollectUndelegatedGns",
342		"prevAddr", prev.Address().String(),
343		"prevRealm", prev.PkgPath(),
344		"from", prev.Address().String(),
345		"to", caller.String(),
346		"collectedAmount", utils.FormatInt(collectedAmount),
347	)
348
349	return collectedAmount
350}
351
352// delegate processes delegation operations.
353// Validates delegation amount, creates delegation records, and updates reward tracking.
354func (gs *govStakerV1) delegate(
355	_ int,
356	rlm realm,
357	from address,
358	to address,
359	amount,
360	currentHeight,
361	currentTimestamp int64,
362) (*staker.Delegation, error) {
363	delegationID := gs.nextDelegationID()
364	delegation := staker.NewDelegation(
365		delegationID,
366		from,
367		to,
368		amount,
369		currentHeight,
370		currentTimestamp,
371	)
372	delegationResolver := NewDelegationResolver(delegation)
373	delegatedAmount := delegationResolver.DelegatedAmount()
374	if delegatedAmount < 0 {
375		return nil, errors.New(errDelegatedAmountNegative)
376	}
377
378	if err := gs.addDelegation(0, rlm, delegationID, delegation); err != nil {
379		return nil, err
380	}
381	gs.addDelegationRecord(0, rlm, to, delegatedAmount, currentTimestamp)
382	if err := gs.addStakeEmissionReward(0, rlm, from.String(), amount, currentTimestamp); err != nil {
383		return nil, err
384	}
385	if err := gs.addStakeProtocolFeeReward(0, rlm, from.String(), amount, currentTimestamp); err != nil {
386		return nil, err
387	}
388
389	return delegation, nil
390}
391
392// unDelegate processes undelegation operations with lockup.
393// Validates undelegation amount, processes withdrawals, and updates reward tracking.
394func (gs *govStakerV1) unDelegate(
395	_ int,
396	rlm realm,
397	delegator,
398	delegatee address,
399	amount,
400	currentHeight,
401	currentTimestamp int64,
402) (int64, error) {
403	delegationIDs := gs.getUserDelegationIDsWithDelegatee(delegator, delegatee)
404	if len(delegationIDs) == 0 {
405		return 0, nil
406	}
407
408	unDelegationAmount := amount
409	lockupPeriod := gs.store.GetUnDelegationLockupPeriod()
410	totalDelegated := int64(0)
411	delegations := make([]*staker.Delegation, 0, len(delegationIDs))
412
413	for _, id := range delegationIDs {
414		delegation, exists := gs.store.GetDelegation(id)
415		if !exists {
416			continue
417		}
418
419		totalDelegated = gnsmath.SafeAddInt64(totalDelegated, NewDelegationResolver(delegation).DelegatedAmount())
420		delegations = append(delegations, delegation)
421	}
422
423	if amount > totalDelegated {
424		return 0, errors.New(errNotEnoughDelegated)
425	}
426
427	// Process undelegation across multiple delegation records if necessary
428	for _, delegation := range delegations {
429		resolver := NewDelegationResolver(delegation)
430		if resolver.IsEmpty() {
431			if err := gs.removeDelegation(0, rlm, delegation.ID()); err != nil {
432				return 0, err
433			}
434			continue
435		}
436
437		currentUnDelegationAmount := unDelegationAmount
438
439		if currentUnDelegationAmount > resolver.DelegatedAmount() {
440			currentUnDelegationAmount = resolver.DelegatedAmount()
441		}
442
443		if currentUnDelegationAmount < 0 {
444			return 0, errors.New(errUndelegationAmountNegative)
445		}
446
447		if currentUnDelegationAmount == 0 {
448			continue
449		}
450
451		resolver.UnDelegate(
452			currentUnDelegationAmount,
453			currentHeight,
454			currentTimestamp,
455			lockupPeriod,
456		)
457
458		if err := gs.setDelegation(0, rlm, delegation.ID(), delegation); err != nil {
459			return 0, err
460		}
461		gs.addDelegationRecord(0, rlm, delegatee, -currentUnDelegationAmount, currentTimestamp)
462		if err := gs.removeStakeEmissionReward(0, rlm, delegator.String(), currentUnDelegationAmount, currentTimestamp); err != nil {
463			return 0, err
464		}
465		if err := gs.removeStakeProtocolFeeReward(0, rlm, delegator.String(), currentUnDelegationAmount, currentTimestamp); err != nil {
466			return 0, err
467		}
468
469		unDelegationAmount = gnsmath.SafeSubInt64(unDelegationAmount, currentUnDelegationAmount)
470		if unDelegationAmount <= 0 {
471			break
472		}
473	}
474
475	return amount, nil
476}
477
478// unDelegateWithoutLockup processes undelegation without lockup.
479// Used for redelegation where tokens are immediately available.
480func (gs *govStakerV1) unDelegateWithoutLockup(
481	_ int,
482	rlm realm,
483	delegator,
484	delegatee address,
485	amount,
486	currentHeight,
487	currentTime int64,
488) (int64, error) {
489	delegationIDs := gs.getUserDelegationIDsWithDelegatee(delegator, delegatee)
490	if len(delegationIDs) == 0 {
491		return 0, errors.New(errNotEnoughDelegated)
492	}
493
494	unDelegationAmount := amount
495	totalDelegated := int64(0)
496	delegations := make([]*staker.Delegation, 0, len(delegationIDs))
497
498	for _, id := range delegationIDs {
499		delegation, exists := gs.store.GetDelegation(id)
500		if !exists {
501			continue
502		}
503
504		totalDelegated = gnsmath.SafeAddInt64(totalDelegated, NewDelegationResolver(delegation).DelegatedAmount())
505		delegations = append(delegations, delegation)
506	}
507
508	if amount > totalDelegated {
509		return 0, errors.New(errNotEnoughDelegated)
510	}
511
512	// Process undelegation across multiple delegation records if necessary
513	for _, delegation := range delegations {
514		resolver := NewDelegationResolver(delegation)
515		if resolver.IsEmpty() {
516			if err := gs.removeDelegation(0, rlm, delegation.ID()); err != nil {
517				return 0, err
518			}
519			continue
520		}
521
522		currentUnDelegationAmount := unDelegationAmount
523
524		if currentUnDelegationAmount > resolver.DelegatedAmount() {
525			currentUnDelegationAmount = resolver.DelegatedAmount()
526		}
527
528		if currentUnDelegationAmount == 0 {
529			continue
530		}
531
532		resolver.UnDelegateWithoutLockup(
533			currentUnDelegationAmount,
534			currentHeight,
535			currentTime,
536		)
537
538		if resolver.IsEmpty() {
539			if err := gs.removeDelegation(0, rlm, delegation.ID()); err != nil {
540				return 0, err
541			}
542		} else if err := gs.setDelegation(0, rlm, delegation.ID(), delegation); err != nil {
543			return 0, err
544		}
545		gs.addDelegationRecord(0, rlm, delegatee, -currentUnDelegationAmount, currentTime)
546		if err := gs.removeStakeEmissionReward(0, rlm, delegator.String(), currentUnDelegationAmount, currentTime); err != nil {
547			return 0, err
548		}
549		if err := gs.removeStakeProtocolFeeReward(0, rlm, delegator.String(), currentUnDelegationAmount, currentTime); err != nil {
550			return 0, err
551		}
552
553		unDelegationAmount = gnsmath.SafeSubInt64(unDelegationAmount, currentUnDelegationAmount)
554		if unDelegationAmount <= 0 {
555			break
556		}
557	}
558
559	return amount, nil
560}
561
562func (gs *govStakerV1) increaseTotalDelegatedAmount(_ int, rlm realm, amount int64) error {
563	currentDelegated := gs.store.GetTotalDelegatedAmount()
564
565	if err := gs.store.SetTotalDelegatedAmount(0, rlm, gnsmath.SafeAddInt64(currentDelegated, amount)); err != nil {
566		return err
567	}
568
569	return nil
570}
571
572func (gs *govStakerV1) decreaseTotalDelegatedAmount(_ int, rlm realm, amount int64) error {
573	currentDelegated := gs.store.GetTotalDelegatedAmount()
574
575	newDelegated := gnsmath.SafeSubInt64(currentDelegated, amount)
576	if newDelegated < 0 {
577		newDelegated = 0
578	}
579	if err := gs.store.SetTotalDelegatedAmount(0, rlm, newDelegated); err != nil {
580		return err
581	}
582
583	return nil
584}
585
586func (gs *govStakerV1) increaseTotalLockedAmount(_ int, rlm realm, amount int64) error {
587	currentLocked := gs.store.GetTotalLockedAmount()
588
589	if err := gs.store.SetTotalLockedAmount(0, rlm, gnsmath.SafeAddInt64(currentLocked, amount)); err != nil {
590		return err
591	}
592
593	return nil
594}
595
596func (gs *govStakerV1) decreaseTotalLockedAmount(_ int, rlm realm, amount int64) error {
597	currentLocked := gs.store.GetTotalLockedAmount()
598
599	newLocked := gnsmath.SafeSubInt64(currentLocked, amount)
600	if newLocked < 0 {
601		newLocked = 0
602	}
603	if err := gs.store.SetTotalLockedAmount(0, rlm, newLocked); err != nil {
604		return err
605	}
606
607	return nil
608}
609
610// collectDelegations processes collection of undelegated tokens.
611// Iterates through user delegations and collects available amounts.
612func (gs *govStakerV1) collectDelegations(_ int, rlm realm, user address, currentTime int64) (int64, error) {
613	totalCollectedAmount := int64(0)
614
615	delegationTree := gs.getUserDelegations(user)
616
617	var err error
618	var idsToRemove []int64
619	allDelegations := gs.store.GetAllDelegations()
620
621	// Collect from all available delegations
622	delegationTree.Iterate("", "", func(delegatee string, value any) bool {
623		delegationIDs, ok := value.([]int64)
624		if !ok {
625			return false
626		}
627
628		if len(delegationIDs) == 0 {
629			return false
630		}
631		for _, id := range delegationIDs {
632			delegationRaw := allDelegations.Get(utils.FormatInt(id))
633			if delegationRaw == nil {
634				continue
635			}
636			delegation, ok := delegationRaw.(*staker.Delegation)
637			if !ok {
638				continue
639			}
640
641			resolver := NewDelegationResolver(delegation)
642
643			collectedAmount, iErr := resolver.processCollection(currentTime)
644			if iErr != nil {
645				err = iErr
646				return true
647			}
648
649			// Simple addition since addToCollectedAmount was removed
650			totalCollectedAmount = gnsmath.SafeAddInt64(totalCollectedAmount, collectedAmount)
651
652			// Save updated delegation state after collection
653			if resolver.IsEmpty() {
654				idsToRemove = append(idsToRemove, delegation.ID())
655			} else {
656				if iErr := gs.setDelegation(0, rlm, delegation.ID(), delegation); iErr != nil {
657					err = iErr
658					return true
659				}
660			}
661		}
662
663		return false
664	})
665
666	if err != nil {
667		return totalCollectedAmount, makeErrorWithDetails(errInvalidAmount, err.Error())
668	}
669
670	for _, id := range idsToRemove {
671		if err := gs.removeDelegation(0, rlm, id); err != nil {
672			return totalCollectedAmount, err
673		}
674	}
675
676	return totalCollectedAmount, nil
677}