Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}