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