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