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

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}