Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}