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}