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