Transaction

7EA5D69C978863…E93D68B1C779

Block 77,270 · index 0 · indexed

Summary

Hash
7EA5D69C9788633B6017629B72C6B26869DE34824B8ED1D746A8E93D68B1C779
Block
77,270
Size
152937 bytes
Gas used
193,333,214 / 231,999,797
Fee
232000 ugnot
Status
success

Messages

#1AddPackagegno.land/r/gnoswap/gov/staker/v124 arguments

Arguments · 24

  1. #1staker
  2. #2README.md
  3. #3# Gov Staker Governance delegation and xGNS-based voting power management. ## Overview Gov Staker accepts GNS staking, mints and burns xGNS, maintains delegatee balances and timestamped delegation history, tracks undelegation lockups, and settles two reward streams for stakers: GNS emission rewards and protocol-fee rewards in any registered token. ## Configuration - **Undelegation Lockup**: configurable; the default is 7 days before undelegated GNS can be collected. - **Reward Sources**: GNS emission (single token) and protocol-fee distribution (one accumulator per fee token). - **Delegation State**: per-delegator/delegatee records, total and user timestamp histories, and reward stake events. ## Core Features ### Delegation - Delegate GNS to any valid address; the delegated amount is mirrored as xGNS voting power. - Redelegate between delegatees without a lockup. Redelegation performs an immediate remove-plus-add for reward accounting. - Track undelegated balances until the configured lockup expires; collecting then burns the corresponding xGNS and returns GNS. ### Rewards - `CollectReward` claims both the emission stream and all known protocol-fee tokens. - `CollectEmissionReward` claims only GNS emission rewards. - `CollectProtocolFeeReward` claims one protocol-fee token; its bucket and stake-event work is bounded per call, so remaining work is collected later. - Launchpad can collect the matching emission and protocol-fee rewards for a registered project wallet through launchpad-only entry points. Protocol-fee rewards are not calculated as a single current-balance share. Each token's fee buckets are divided by the total stake in force during their accrual epochs, accumulated in Q128 fixed point, and settled across each staker's timestamped stake-event segments. Fractional remainders remain in state for later collection. ### History and Snapshots - Historical delegation snapshots are timestamp lookups used by governance's configured smoothing calculation; they are not block-height snapshots. - Cleanup functions preserve history needed by active proposals. ## Key Functions ### `Delegate` Transfers GNS from the caller, mints the same amount of xGNS, and assigns the delegated voting power to a target address. The caller must approve the GNS transfer first. ### `Undelegate` Removes voting power immediately and creates a withdrawal subject to the configured lockup. ### `Redelegate` Moves delegated balance from one delegatee to another immediately, without creating a user-facing lockup. ### `CollectReward` Claims the caller's GNS emission rewards and all known protocol-fee rewards. The all-token path intentionally grows with the number of known fee tokens. ### `CollectEmissionReward` Claims only the caller's accumulated GNS emission reward. ### `CollectProtocolFeeReward` Claims one token path's accumulated protocol-fee reward. A later call may be required when pending accrual buckets or stake events exceed the per-call bound. ### `CollectUndelegatedGns` Collects GNS after the configured undelegation lockup has passed and burns the corresponding xGNS. ### `CollectRewardFromLaunchPad` Collects both reward streams for a registered launchpad project wallet. This entry point is callable only by the launchpad contract and sends rewards to the supplied project-wallet address. ## Delegation Logic ### Delegation Flow 1. Approve GNS spending by the gov/staker realm. 2. Delegate GNS to a delegatee; xGNS is minted and timestamped history is updated. 3. Governance reads the delegatee's history at proposal-defined timestamps for vote weight. 4. Undelegate to start the lockup, or redelegate immediately to another delegatee. 5. After the lockup expires, collect undelegated GNS. ## Usage These snippets call the public domain proxy from a realm function with a current `cur` token. Import the proxy package and qualify its function names in integrating code. ```go // Delegate GNS to another address; xGNS voting power is minted 1:1. delegatedAmount := Delegate(cross(cur), delegatee, 1_000_000_000, "g1referrer...") // Redelegate part of the active balance immediately. Redelegate(cross(cur), delegatee, newDelegatee, 500_000_000) // Claim both GNS emission and all known protocol-fee tokens. CollectReward(cross(cur)) // Or claim only one stream/token. CollectEmissionReward(cross(cur)) CollectProtocolFeeReward(cross(cur), tokenPath) // Start undelegation. Collect only after the configured lockup (7 days by default). Undelegate(cross(cur), delegatee, 250_000_000) // ...wait until the lockup has expired... CollectUndelegatedGns(cross(cur)) ``` ## Security - Timestamped delegation history and configurable smoothing reduce flash-loan-style voting manipulation; they do not provide a block snapshot. - Undelegation removes voting power immediately, while the lockup delays GNS withdrawal. - Protocol-fee stake changes must remain independent of the number of fee tokens; only collection folds fee buckets. - Launchpad reward entry points are restricted to the launchpad contract and registered project wallets. - Snapshot cleanup must preserve data still needed by active proposals.
  4. #4assert.gno
  5. #5package staker import ( "errors" "time" ufmt "gno.land/p/nt/ufmt/v0" "gno.land/r/gnoswap/gov/governance" ) // assertIsValidDelegateAmount validates that the delegation amount meets system requirements. // This function checks minimum amount and multiple requirements. // // Parameters: // - amount: amount to validate // // Returns: // - error: nil if valid, error describing validation failure func assertIsValidDelegateAmount(amount int64) { if amount < minimumAmount { panic(makeErrorWithDetails( errLessThanMinimum, ufmt.Sprintf("minimum amount to delegate is %d (requested:%d)", minimumAmount, amount), )) } if amount%minimumAmount != 0 { panic(makeErrorWithDetails( errInvalidAmount, ufmt.Sprintf("amount must be multiple of %d", minimumAmount), )) } } func assertIsValidSnapshotTime(snapshotTime int64) { if snapshotTime < 0 { panic(makeErrorWithDetails( errInvalidSnapshotTime, ufmt.Sprintf("snapshot time must be greater than 0 (requested:%d)", snapshotTime), )) } currentTime := time.Now().Unix() maxSmoothingPeriod := governance.GetMaxSmoothingPeriod() if snapshotTime > currentTime-maxSmoothingPeriod { panic(makeErrorWithDetails( errInvalidSnapshotTime, ufmt.Sprintf("snapshot time must be less than %d (requested:%d)", currentTime-maxSmoothingPeriod, snapshotTime), )) } } // assertIsAvailableCleanupSnapshotTime checks that no active proposals need func assertIsAvailableCleanupSnapshotTime(cleanupSnapshotTime int64) { oldestActiveSnapshotTime, hasActiveProposal, err := governance.GetOldestActiveProposalSnapshotTime() if err != nil { panic(err) } if !hasActiveProposal { // No active proposals, cleanup is safe return } if cleanupSnapshotTime > oldestActiveSnapshotTime { panic(makeErrorWithDetails( errInvalidSnapshotTime, ufmt.Sprintf( "cannot cleanup delegation history: active proposal requires data from snapshot time %d, but cleanup would remove data before %d", oldestActiveSnapshotTime, cleanupSnapshotTime, ), )) } } func assertNoSameDelegatee(delegatee, newDelegatee address) { if delegatee == newDelegatee { panic(errors.New(errSameDelegatee)) } } func assertNotImplementYet() { panic("NotImplementYet") }
  6. #6consts.gno
  7. #7package staker const ( minimumAmount = 1_000_000 // 1 GNS // maxAccrualBucketsPerCollect bounds how many pending accrual buckets one // single-token collect folds. Whatever is left stays pending for the next collect. maxAccrualBucketsPerCollect = 256 // maxStakeEventsPerCollect bounds how many of the staker's own stake events one // single-token collect settles. Whatever is left stays pending for the next collect. maxStakeEventsPerCollect = 256 )
  8. #8delegation.gno
  9. #9package staker import ( "errors" gnsmath "gno.land/p/gnoswap/gnsmath/v1" "gno.land/r/gnoswap/gov/staker" ) const errCollectAmountExceedsCollectable = "amount to collect is greater than collectable amount" type DelegationResolver struct { delegation *staker.Delegation } // NewDelegationResolver wraps a delegation in a resolver for derived amounts and withdrawals. // // Parameters: // - delegation: delegation state to resolve // // Returns: // - *DelegationResolver: resolver backed by delegation func NewDelegationResolver(delegation *staker.Delegation) *DelegationResolver { return &DelegationResolver{delegation} } // Get returns the underlying delegation state. // // Returns: // - *staker.Delegation: wrapped delegation instance func (r *DelegationResolver) Get() *staker.Delegation { return r.delegation } // DelegatedAmount returns total delegated amount minus amount already undelegated. // // Returns: // - int64: amount still delegated func (r *DelegationResolver) DelegatedAmount() int64 { return gnsmath.SafeSubInt64(r.delegation.TotalDelegatedAmount(), r.delegation.UnDelegatedAmount()) } // LockedAmount returns total delegated amount minus amount already collected. // // Returns: // - int64: amount still locked in the delegation func (r *DelegationResolver) LockedAmount() int64 { return gnsmath.SafeSubInt64(r.delegation.TotalDelegatedAmount(), r.delegation.CollectedAmount()) } // IsEmpty reports whether no locked delegation amount remains. // // Returns: // - bool: true when LockedAmount is zero, otherwise false func (r *DelegationResolver) IsEmpty() bool { return r.LockedAmount() == 0 } // CollectableAmount calculates the total amount that can be collected at the given time. // // Parameters: // - currentTime: current Unix timestamp used to evaluate each withdrawal's lockup // // Returns: // - total: sum of all withdrawal amounts currently available for collection func (r *DelegationResolver) CollectableAmount(currentTime int64) (total int64) { for _, withdraw := range r.delegation.Withdraws() { total = gnsmath.SafeAddInt64(total, NewDelegationWithdrawResolver(&withdraw).CollectableAmount(currentTime)) } return total } // UnDelegate processes an undelegation with a time-based lockup period. // // Parameters: // - amount: amount to undelegate and place into a withdrawal // - currentHeight: block height at which the undelegation is recorded // - currentTimestamp: Unix timestamp at which the undelegation is recorded // - unDelegationLockupPeriod: lockup duration in seconds before the withdrawal is collectible func (r *DelegationResolver) UnDelegate( amount, currentHeight, currentTimestamp, unDelegationLockupPeriod int64, ) { r.delegation.SetUnDelegateAmount(gnsmath.SafeAddInt64(r.delegation.UnDelegatedAmount(), amount)) withdraw := NewDelegationWithdraw( r.delegation.ID(), amount, currentHeight, currentTimestamp, unDelegationLockupPeriod, ) r.delegation.AddWithdraw(withdraw) } // UnDelegateWithoutLockup processes an immediate undelegation without lockup. // // Parameters: // - amount: amount to undelegate and mark as collected immediately // - currentHeight: current block height context; retained for API symmetry and unused by this immediate path // - currentTime: current Unix timestamp context; retained for API symmetry and unused by this immediate path func (r *DelegationResolver) UnDelegateWithoutLockup( amount, currentHeight, currentTime int64, ) { r.delegation.SetUnDelegateAmount(gnsmath.SafeAddInt64(r.delegation.UnDelegatedAmount(), amount)) r.delegation.SetCollectedAmount(gnsmath.SafeAddInt64(r.delegation.CollectedAmount(), amount)) } // processCollection handles the actual collection logic func (r *DelegationResolver) processCollection(currentTime int64) (int64, error) { collectedAmount := int64(0) withdraws := r.delegation.Withdraws() for i := range withdraws { resolver := NewDelegationWithdrawResolver(&withdraws[i]) collectableAmount := resolver.CollectableAmount(currentTime) if collectableAmount <= 0 { continue } err := resolver.Collect(collectableAmount, currentTime) if err != nil { return 0, err } updatedAmount, err := addToCollectedAmount(r.delegation.CollectedAmount(), collectableAmount) if err != nil { return 0, err } r.delegation.SetCollectedAmount(updatedAmount) collectedAmount = gnsmath.SafeAddInt64(collectedAmount, collectableAmount) } // Skip filtering if nothing was collected if collectedAmount == 0 { return collectedAmount, nil } currentIndex := 0 for i := range withdraws { if !withdraws[i].IsCollected() { r.delegation.SetWithdraw(currentIndex, withdraws[i]) currentIndex++ } } r.delegation.SetWithdraws(withdraws[:currentIndex]) return collectedAmount, nil } func addToCollectedAmount(collectedAmount, amount int64) (int64, error) { if amount < 0 { return 0, errors.New(errAmountMustBeNonNegative) } return gnsmath.SafeAddInt64(collectedAmount, amount), nil } // NewDelegation creates a new delegation. // This is a convenience wrapper around staker.NewDelegation. // // Parameters: // - id: delegation ID // - delegateFrom: delegator's address // - delegateTo: delegatee's address // - delegateAmount: amount to delegate // - createdHeight: creation block height // - createdAt: creation timestamp // // Returns: // - *staker.Delegation: new delegation instance func NewDelegation( id int64, delegateFrom, delegateTo address, delegateAmount, createdHeight, createdAt int64, ) *staker.Delegation { return staker.NewDelegation( id, delegateFrom, delegateTo, delegateAmount, createdHeight, createdAt, ) }
  10. #10delegation_manager.gno
  11. #11package staker import ( "gno.land/r/gnoswap/gov/staker" ) type DelegationManagerResolver struct { *staker.DelegationManager } // NewDelegationManagerResolver creates a resolver over an existing delegation manager. // // Parameters: // - delegationManager: manager whose domain-owned delegation trees will be queried or updated. // // Returns: // - *DelegationManagerResolver: resolver exposing address-typed delegation operations. func NewDelegationManagerResolver(delegationManager *staker.DelegationManager) *DelegationManagerResolver { return &DelegationManagerResolver{delegationManager} } // GetUserDelegationIDsWithDelegatee retrieves all delegation IDs for a specific delegator-delegatee pair. // This method is used to find delegations from a specific user to a specific delegate. // // Parameters: // - delegator: address of the user who delegated tokens // - delegatee: address of the user who received the delegation // // Returns: // - []int64: matching delegation IDs, or an empty slice when no pair is stored. func (dm *DelegationManagerResolver) GetUserDelegationIDsWithDelegatee(delegator, delegatee address) []int64 { delegatorAddress := delegator.String() delegateeAddress := delegatee.String() ids, exists := dm.GetDelegationIDs(delegatorAddress, delegateeAddress) if !exists { return []int64{} } return ids } // GetUserDelegationIDs retrieves all delegation IDs for a specific delegator across all delegatees. // This method is used to find all delegations made by a specific user. // // Parameters: // - delegator: address of the user whose delegations to retrieve // // Returns: // - []int64: concatenated delegation IDs from every stored delegatee entry, or an empty slice when absent. func (dm *DelegationManagerResolver) GetUserDelegationIDs(delegator address) []int64 { delegatorAddress := delegator.String() delegationIDs := make([]int64, 0) // Get all delegations for this delegator delegations, exists := dm.GetDelegatorDelegations(delegatorAddress) if !exists { return delegationIDs } // Collect delegation IDs from all delegatees by iterating the BPTree delegations.Iterate("", "", func(key string, value interface{}) bool { if toDelegations, ok := value.([]int64); ok { delegationIDs = append(delegationIDs, toDelegations...) } return false }) return delegationIDs } // addDelegation adds a delegation ID to the manager's tracking system. // This method creates the necessary nested map structure if it doesn't exist // and ensures no duplicate delegation IDs are stored. // // Parameters: // - delegator: address of the user who made the delegation // - delegatee: address of the user who received the delegation // - delegationID: unique identifier for the delegation func (dm *DelegationManagerResolver) addDelegation(delegator, delegatee address, delegationID int64) { // Delegate the read-modify-write to the domain method so the nested BPTree // mutation stays within the owning realm; splitting the get and set across // the realm boundary here would taint the re-fetched inner tree. dm.AddDelegationID(delegator.String(), delegatee.String(), delegationID) } // removeDelegation removes a delegation ID from the manager's tracking system. // This method finds and removes the specified delegation ID from the appropriate slice. // // Parameters: // - delegator: address of the user who made the delegation // - delegatee: address of the user who received the delegation // - delegationID: unique identifier for the delegation to remove func (dm *DelegationManagerResolver) removeDelegation(delegator, delegatee address, delegationID int64) { // Delegate the read-modify-write to the domain method so the nested BPTree // mutation stays within the owning realm. dm.RemoveDelegationID(delegator.String(), delegatee.String(), delegationID) }
  12. #12delegation_withdraw.gno
  13. #13package staker import ( "errors" gnsmath "gno.land/p/gnoswap/gnsmath/v1" "gno.land/r/gnoswap/gov/staker" ) type DelegationWithdrawResolver struct { withdraw *staker.DelegationWithdraw } // NewDelegationWithdrawResolver wraps a delegation withdrawal for collection // state and lockup calculations. // // Parameters: // - withdraw: delegation withdrawal state to resolve // // Returns: // - *DelegationWithdrawResolver: resolver retaining the supplied withdrawal pointer func NewDelegationWithdrawResolver(withdraw *staker.DelegationWithdraw) *DelegationWithdrawResolver { return &DelegationWithdrawResolver{withdraw} } // Get returns the wrapped delegation withdrawal value. // // Returns: // - *staker.DelegationWithdraw: wrapped withdrawal pointer, or nil when the resolver was constructed with nil func (r *DelegationWithdrawResolver) Get() *staker.DelegationWithdraw { return r.withdraw } // CollectableAmount calculates the amount available for collection at the given time. // Returns zero if the withdrawal is not yet collectable or has been fully collected. // // Parameters: // - currentTime: current timestamp to check collectability against // // Returns: // - int64: amount available for collection func (r *DelegationWithdrawResolver) CollectableAmount(currentTime int64) int64 { if r.IsCollectable(currentTime) { return gnsmath.SafeSubInt64(r.withdraw.UnDelegateAmount(), r.withdraw.CollectedAmount()) } return 0 } // IsCollectable determines whether the withdrawal can be collected at the given time. // A withdrawal is collectable if: // - The undelegated amount is positive // - There is remaining uncollected amount // - The current time is at or after the collectable time // // Parameters: // - currentTime: current timestamp to check against // // Returns: // - bool: true if the withdrawal can be collected, false otherwise func (r *DelegationWithdrawResolver) IsCollectable(currentTime int64) bool { if r.withdraw.UnDelegateAmount() <= 0 { return false } remaining := gnsmath.SafeSubInt64(r.withdraw.UnDelegateAmount(), r.withdraw.CollectedAmount()) if remaining <= 0 { return false } if currentTime < r.withdraw.CollectableTime() { return false } return true } // IsCollected returns whether the withdrawal has been fully collected. // // Returns: // - bool: true if fully collected, false otherwise func (r *DelegationWithdrawResolver) IsCollected() bool { return r.withdraw.IsCollected() } // collect processes the collection of the specified amount from this withdrawal. // This method validates collectability and updates the collection state. // // Parameters: // - amount: GNS amount to add to this withdrawal's collected amount // - currentTime: Unix timestamp used to check lockup expiry and record collection time // // Returns: // - error: nil after the collection state is updated; errWithdrawNotCollectable when the withdrawal is not collectable func (r *DelegationWithdrawResolver) Collect(amount int64, currentTime int64) error { if !r.IsCollectable(currentTime) { return errors.New(errWithdrawNotCollectable) } r.withdraw.SetCollectedAmount(gnsmath.SafeAddInt64(r.withdraw.CollectedAmount(), amount)) if r.withdraw.UnDelegateAmount() == r.withdraw.CollectedAmount() { r.withdraw.SetCollected(true) r.withdraw.SetCollectedAt(currentTime) } return nil } // NewDelegationWithdraw creates a new delegation withdrawal with lockup period. // This is a convenience wrapper around staker.NewDelegationWithdraw. // // Parameters: // - delegationID: unique identifier of the associated delegation // - unDelegateAmount: amount being withdrawn // - createdHeight: height when the withdrawal was created // - createdAt: timestamp when the withdrawal was created // - unDelegationLockupPeriod: duration of the lockup period in seconds // // Returns: // - staker.DelegationWithdraw: new withdrawal instance with lockup func NewDelegationWithdraw( delegationID, unDelegateAmount, createdHeight, createdAt, unDelegationLockupPeriod int64, ) staker.DelegationWithdraw { return staker.NewDelegationWithdraw( delegationID, unDelegateAmount, createdHeight, createdAt, unDelegationLockupPeriod, ) } // NewDelegationWithdrawWithoutLockup creates a new delegation withdrawal that is immediately collectable. // This is a convenience wrapper around staker.NewDelegationWithdrawWithoutLockup. // // Parameters: // - delegationID: unique identifier of the associated delegation // - unDelegateAmount: amount being withdrawn // - createdHeight: height when the withdrawal was created // - createdAt: timestamp when the withdrawal was created // // Returns: // - staker.DelegationWithdraw: new withdrawal instance that is immediately collectable func NewDelegationWithdrawWithoutLockup( delegationID, unDelegateAmount, createdHeight, createdAt int64, ) staker.DelegationWithdraw { return staker.NewDelegationWithdrawWithoutLockup( delegationID, unDelegateAmount, createdHeight, createdAt, ) }
  14. #14doc.gno
  15. #15// Package v1 manages GNS delegation and xGNS accounting. // It maintains timestamped delegation history, distributes GNS emission and // protocol-fee rewards, and enforces a configurable undelegation lockup // (7 days by default; xGNS is burned when undelegated GNS is collected). package staker
  16. #16emission_reward_manager.gno
  17. #17package staker import ( "errors" "math" gnsmath "gno.land/p/gnoswap/gnsmath/v1" u256 "gno.land/p/gnoswap/uint256/v1" "gno.land/r/gnoswap/gov/staker" ) type EmissionRewardManagerResolver struct { *staker.EmissionRewardManager } // NewEmissionRewardManagerResolver wraps an emission reward manager with reward // calculation and stake-accounting helpers. // // Parameters: // - emissionRewardManager: manager state to resolve and mutate. // // Returns: // - *EmissionRewardManagerResolver: resolver backed by emissionRewardManager. func NewEmissionRewardManagerResolver(emissionRewardManager *staker.EmissionRewardManager) *EmissionRewardManagerResolver { return &EmissionRewardManagerResolver{emissionRewardManager} } // GetClaimableRewardAmount calculates the claimable reward amount for a specific address. // // Parameters: // - currentDistributedAmount: total emission amount distributed to the staker // manager at the current observation point. // - address: reward-state identifier whose claimable amount is requested. // - currentTimestamp: Unix timestamp used to settle the address's reward state. // // Returns: // - int64: emission reward currently claimable by address, or zero when no // reward state exists. // - error: nil on success; an error from accumulated-reward or reward-state // resolution when calculation fails. func (self *EmissionRewardManagerResolver) GetClaimableRewardAmount( currentDistributedAmount int64, address string, currentTimestamp int64, ) (int64, error) { rewardState, ok, err := self.GetRewardState(address) if !ok { return 0, nil } accumulatedRewardX128PerStake, err := self.calculateAccumulatedRewardX128PerStake( currentDistributedAmount, currentTimestamp, ) if err != nil { return 0, err } resolvedState := NewEmissionRewardStateResolver(rewardState) return resolvedState.GetClaimableRewardAmount(accumulatedRewardX128PerStake, currentTimestamp) } // calculateAccumulatedRewardX128PerStake calculates the updated accumulated reward per stake. func (self *EmissionRewardManagerResolver) calculateAccumulatedRewardX128PerStake( currentDistributedAmount int64, currentTimestamp int64, ) (*u256.Uint, error) { // If we're looking at a past timestamp, return current state if currentTimestamp < self.GetAccumulatedTimestamp() { return self.GetAccumulatedRewardX128PerStake(), nil } // With no stake there is nobody to credit, so the delta accrued over that window is // dropped and the caller advances the cursor past it. The protocol fee accumulator // applies the same policy, see ProtocolFeeRewardManagerResolver. totalStakedAmount := self.GetTotalStakedAmount() if totalStakedAmount == 0 { return self.GetAccumulatedRewardX128PerStake(), nil } // Newly distributed rewards since last update distributedAmountDelta := gnsmath.SafeSubInt64(currentDistributedAmount, self.GetDistributedAmount()) if distributedAmountDelta <= 0 { // Non-positive delta. nothing to do more. return self.GetAccumulatedRewardX128PerStake(), nil } // Reward per stake for the new distribution distributedAmountDeltaX128PerStake := u256.Zero().Div( u256.Zero().Lsh(u256.NewUintFromInt64(distributedAmountDelta), 128), u256.NewUintFromInt64(totalStakedAmount), ) // Add to accumulated reward per stake accumulatedReward := u256.Zero().Add(self.GetAccumulatedRewardX128PerStake(), distributedAmountDeltaX128PerStake) return accumulatedReward, nil } // updateAccumulatedRewardX128PerStake updates the internal accumulated reward state. // This method should be called before any stake changes to ensure accurate reward calculations. // Updates accumulated reward per stake with current distribution data. func (self *EmissionRewardManagerResolver) updateAccumulatedRewardX128PerStake( currentDistributedAmount int64, currentTimestamp int64, ) error { // DO NOT apply out-of-order timestamps if currentTimestamp < self.GetAccumulatedTimestamp() { return nil } // Update accumulated reward state accumulatedRewardX128PerStake, err := self.calculateAccumulatedRewardX128PerStake( currentDistributedAmount, currentTimestamp, ) if err != nil { return err } self.setAccumulatedRewardX128PerStake(accumulatedRewardX128PerStake.Clone()) self.setDistributedAmount(currentDistributedAmount) self.setAccumulatedTimestamp(currentTimestamp) return nil } // addStake adds a stake for an address and updates their reward state. // This method ensures rewards are properly calculated before the stake change. // Adds stake for specified address and updates reward calculations. func (self *EmissionRewardManagerResolver) addStake(address string, amount int64, currentTimestamp int64) error { if amount < 0 { return errors.New(errAmountMustBeNonNegative) } accumulatedReward := self.GetAccumulatedRewardX128PerStake() rewardState, ok, err := self.GetRewardState(address) if err != nil { return err } if !ok { rewardState = staker.NewEmissionRewardState(accumulatedReward) } resolvedState := NewEmissionRewardStateResolver(rewardState) err = resolvedState.addStakeWithUpdateRewardDebtX128(amount, accumulatedReward, currentTimestamp) if err != nil { return err } self.setRewardStates(address, rewardState) currentTotal := self.GetTotalStakedAmount() if amount > 0 && currentTotal > math.MaxInt64-amount { return errors.New(errTotalStakedAmountOverflow) } self.SetTotalStakedAmount(gnsmath.SafeAddInt64(currentTotal, amount)) return nil } // removeStake removes a stake for an address and updates their reward state. // This method ensures rewards are properly calculated before the stake change. // Removes stake for specified address and updates reward calculations. func (self *EmissionRewardManagerResolver) removeStake(address string, amount int64, currentTimestamp int64) error { if amount < 0 { return errors.New(errAmountMustBeNonNegative) } updatedTotalStakedAmount := gnsmath.SafeSubInt64(self.GetTotalStakedAmount(), amount) if updatedTotalStakedAmount < 0 { return errors.New(errRemoveAmountExceedsTotalStaked) } accumulatedReward := self.GetAccumulatedRewardX128PerStake() rewardState, ok, err := self.GetRewardState(address) if err != nil { return err } if !ok { rewardState = staker.NewEmissionRewardState(accumulatedReward) } resolvedState := NewEmissionRewardStateResolver(rewardState) err = resolvedState.removeStakeWithUpdateRewardDebtX128(amount, accumulatedReward, currentTimestamp) if err != nil { return err } // persist updated state self.setRewardStates(address, rewardState) self.SetTotalStakedAmount(updatedTotalStakedAmount) return nil } // claimRewards processes reward claiming for an address. // This method calculates and returns the amount of rewards claimed. // Claims available rewards for specified address. func (self *EmissionRewardManagerResolver) claimRewards(address string, currentTimestamp int64) (claimedRewardAmount int64, err error) { rewardState, ok, err := self.GetRewardState(address) if err != nil || !ok { return 0, err } resolvedState := NewEmissionRewardStateResolver(rewardState) claimedRewardAmount, cErr := resolvedState.claimRewardsWithUpdateRewardDebtX128(self.GetAccumulatedRewardX128PerStake(), currentTimestamp) if cErr != nil { return 0, cErr } self.setRewardStates(address, rewardState) return claimedRewardAmount, nil } func (self *EmissionRewardManagerResolver) setRewardStates(address string, rewardState *staker.EmissionRewardState) { self.SetRewardState(address, rewardState) } func (self *EmissionRewardManagerResolver) setAccumulatedRewardX128PerStake(accumulatedRewardX128PerStake *u256.Uint) { self.SetAccumulatedRewardX128PerStake(accumulatedRewardX128PerStake) } func (self *EmissionRewardManagerResolver) setDistributedAmount(distributedAmount int64) { self.SetDistributedAmount(distributedAmount) } func (self *EmissionRewardManagerResolver) setAccumulatedTimestamp(accumulatedTimestamp int64) { self.SetAccumulatedTimestamp(accumulatedTimestamp) }
  18. #18emission_reward_state.gno
  19. #19package staker import ( "errors" "gno.land/p/gnoswap/consts/v1" gnsmath "gno.land/p/gnoswap/gnsmath/v1" u256 "gno.land/p/gnoswap/uint256/v1" "gno.land/r/gnoswap/gov/staker" ) type EmissionRewardStateResolver struct { *staker.EmissionRewardState } // NewEmissionRewardStateResolver wraps an on-chain emission reward state for // reward calculation and stake/claim updates. // // Parameters: // - emissionRewardState: persisted reward state to expose through the resolver // // Returns: // - *EmissionRewardStateResolver: resolver backed by emissionRewardState func NewEmissionRewardStateResolver(emissionRewardState *staker.EmissionRewardState) *EmissionRewardStateResolver { return &EmissionRewardStateResolver{emissionRewardState} } // IsClaimable checks if rewards can be claimed at the given timestamp. // Rewards are claimable only when currentTimestamp is later than the stored // claimed timestamp. // // Parameters: // - currentTimestamp: timestamp at which claimability is evaluated // // Returns: // - bool: true when currentTimestamp is greater than the last claimed timestamp func (self *EmissionRewardStateResolver) IsClaimable(currentTimestamp int64) bool { return self.GetClaimedTimestamp() < currentTimestamp } // GetClaimableRewardAmount calculates the total amount of rewards that can be claimed. // It combines newly earned rewards with accumulated rewards not yet claimed. // // Parameters: // - accumulatedRewardX128PerStake: system-wide accumulated reward per stake, scaled by 2^128 // - currentTimestamp: timestamp through which newly earned rewards are calculated // // Returns: // - int64: total claimable amount, including accumulated-but-unclaimed and newly earned rewards // - error: nil on success; an error propagated from reward calculation otherwise func (self *EmissionRewardStateResolver) GetClaimableRewardAmount( accumulatedRewardX128PerStake *u256.Uint, currentTimestamp int64, ) (int64, error) { rewardAmount, err := self.calculateClaimableRewards(accumulatedRewardX128PerStake, currentTimestamp) if err != nil { return 0, err } accumulatedUnclaimedReward := gnsmath.SafeSubInt64( self.GetAccumulatedRewardAmount(), self.GetClaimedRewardAmount(), ) return gnsmath.SafeAddInt64(accumulatedUnclaimedReward, rewardAmount), nil } // calculateClaimableRewards calculates newly earned rewards since the last update. // Uses the difference between current and stored reward debt to calculate earnings. // // Parameters: // - accumulatedRewardX128PerStake: current system-wide accumulated reward per stake // - currentTimestamp: current timestamp // // Returns: // - int64: newly earned reward amount since last update // - error: nil on success, error if calculation fails func (self *EmissionRewardStateResolver) calculateClaimableRewards( accumulatedRewardX128PerStake *u256.Uint, currentTimestamp int64, ) (int64, error) { stakedAmount := self.GetStakedAmount() // Don't calculate rewards for past timestamps or when nothing is staked if currentTimestamp < self.GetAccumulatedTimestamp() || stakedAmount == 0 { return 0, nil } // Calculate the difference in accumulated rewards per stake since last update // Using modular arithmetic for accumulator values - underflow is allowed and handled correctly rewardDebtDeltaX128 := u256.Zero().Sub( accumulatedRewardX128PerStake, self.GetRewardDebtX128(), ) // Calculate reward amount by multiplying reward debt delta by staked amount and dividing by Q128 // rewardAmount = (rewardDebtDeltaX128 * stakedAmount) / Q128 rewardAmount := u256.MulDiv( rewardDebtDeltaX128, u256.NewUintFromInt64(stakedAmount), consts.Q128(), ) return gnsmath.SafeConvertToInt64(rewardAmount), nil } // addStake increases the staked amount for this address. // This method should be called when a user increases their stake. // // Parameters: // - amount: amount of stake to add func (self *EmissionRewardStateResolver) addStake(amount int64) error { if amount < 0 { return errors.New(errAmountMustBeNonNegative) } self.SetStakedAmount(gnsmath.SafeAddInt64(self.GetStakedAmount(), amount)) return nil } // removeStake decreases the staked amount for this address. // This method should be called when a user decreases their stake. // // Parameters: // - amount: amount of stake to remove func (self *EmissionRewardStateResolver) removeStake(amount int64) error { if amount < 0 { return errors.New(errAmountMustBeNonNegative) } if amount > self.GetStakedAmount() { return errors.New(errRemoveAmountExceedsStaked) } self.SetStakedAmount(gnsmath.SafeSubInt64(self.GetStakedAmount(), amount)) return nil } // claimRewards processes reward claiming and updates the claim state. // This method validates claimability and transfers accumulated rewards to claimed status. // // Parameters: // - currentTimestamp: current timestamp // // Returns: // - int64: amount of rewards claimed (0 if already claimed at this timestamp) // - error: always nil in the current implementation func (self *EmissionRewardStateResolver) claimRewards(currentTimestamp int64) (int64, error) { if !self.IsClaimable(currentTimestamp) { return 0, nil } accumulatedRewardAmount := self.GetAccumulatedRewardAmount() previousClaimedAmount := self.GetClaimedRewardAmount() claimableAmount := gnsmath.SafeSubInt64(accumulatedRewardAmount, previousClaimedAmount) self.SetClaimedRewardAmount(accumulatedRewardAmount) self.SetClaimedTimestamp(currentTimestamp) return claimableAmount, nil } // updateRewardDebtX128 updates the reward debt and accumulates new rewards. // This method should be called before any stake changes to ensure accurate reward tracking. // // Parameters: // - accumulatedRewardX128PerStake: current system-wide accumulated reward per stake // - currentTimestamp: current timestamp func (self *EmissionRewardStateResolver) updateRewardDebtX128( accumulatedRewardX128PerStake *u256.Uint, currentTimestamp int64, ) error { rewardAmount, err := self.calculateClaimableRewards(accumulatedRewardX128PerStake, currentTimestamp) if err != nil { return err } // Accumulate newly earned rewards if rewardAmount != 0 { self.SetAccumulatedRewardAmount(gnsmath.SafeAddInt64(self.GetAccumulatedRewardAmount(), rewardAmount)) } // Deep copy to avoid aliasing with external state self.SetRewardDebtX128(accumulatedRewardX128PerStake.Clone()) self.SetAccumulatedTimestamp(currentTimestamp) return nil } // addStakeWithUpdateRewardDebtX128 adds stake and updates reward debt in one operation. // This ensures rewards are properly calculated before the stake change takes effect. // // Parameters: // - amount: amount of stake to add // - accumulatedRewardX128PerStake: current system-wide accumulated reward per stake // - currentTimestamp: current timestamp func (self *EmissionRewardStateResolver) addStakeWithUpdateRewardDebtX128( amount int64, accumulatedRewardX128PerStake *u256.Uint, currentTimestamp int64, ) error { if err := self.updateRewardDebtX128(accumulatedRewardX128PerStake, currentTimestamp); err != nil { return err } return self.addStake(amount) } // removeStakeWithUpdateRewardDebtX128 removes stake and updates reward debt in one operation. // This ensures rewards are properly calculated before the stake change takes effect. // // Parameters: // - amount: amount of stake to remove // - accumulatedRewardX128PerStake: current system-wide accumulated reward per stake // - currentTimestamp: current timestamp func (self *EmissionRewardStateResolver) removeStakeWithUpdateRewardDebtX128( amount int64, accumulatedRewardX128PerStake *u256.Uint, currentTimestamp int64, ) error { if err := self.updateRewardDebtX128(accumulatedRewardX128PerStake, currentTimestamp); err != nil { return err } return self.removeStake(amount) } // claimRewardsWithUpdateRewardDebtX128 claims rewards and updates reward debt in one operation. // This ensures all rewards are properly calculated before claiming. // // Parameters: // - accumulatedRewardX128PerStake: current system-wide accumulated reward per stake // - currentTimestamp: current timestamp // // Returns: // - int64: amount of rewards claimed // - error: nil on success, error if claiming fails func (self *EmissionRewardStateResolver) claimRewardsWithUpdateRewardDebtX128( accumulatedRewardX128PerStake *u256.Uint, currentTimestamp int64, ) (int64, error) { if err := self.updateRewardDebtX128(accumulatedRewardX128PerStake, currentTimestamp); err != nil { return 0, err } return self.claimRewards(currentTimestamp) }
  20. #20emits.gno
  21. #21package staker import ( "chain" "gno.land/p/gnoswap/utils/v1" "gno.land/r/gnoswap/gov/staker" ) // emitUpdateEmissionRewardAccumulation emits the persisted global emission reward // accumulation state so that off-chain indexers can consume the authoritative // accumulator instead of re-deriving it. It must be emitted after every path that // calls updateAccumulatedRewardX128PerStake (add/remove/claim), otherwise the // off-chain accumulator drifts from the contract. // // All values are read from the manager's persisted state (post-update): // - distributedAmount is GetDistributedAmount() (the stored baseline the contract // will use for the next delta), not the live emission distribution snapshot. // - totalStakedAmount reflects the current total after the stake change and is the // divisor for the next accumulation delta. func emitUpdateEmissionRewardAccumulation(manager *staker.EmissionRewardManager) { chain.Emit( "UpdateEmissionRewardAccumulation", "totalStakedAmount", utils.FormatInt(manager.GetTotalStakedAmount()), "distributedAmount", utils.FormatInt(manager.GetDistributedAmount()), "accumulatedRewardX128PerStake", manager.GetAccumulatedRewardX128PerStake().ToString(), "accumulatedTimestamp", utils.FormatInt(manager.GetAccumulatedTimestamp()), ) } // emitUpdateProtocolFeeStakeAccrual emits the accrual epoch opened by a stake change // together with the total stake in force from it, so that off-chain indexers can // attribute fees bucketed under that epoch without re-deriving the stake history. func emitUpdateProtocolFeeStakeAccrual(manager *staker.ProtocolFeeRewardManager) { chain.Emit( "UpdateProtocolFeeStakeAccrual", "epoch", utils.FormatInt(manager.GetCurrentEpoch()), "totalStakedAmount", utils.FormatInt(manager.GetTotalStakedAmount()), ) } // emitUpdateProtocolFeeRewardAccumulation emits the persisted accumulation state of // every token that was just folded. Like the emission variant it must be emitted after // every path that folds accrual buckets, otherwise the off-chain accumulator drifts // from the contract. func emitUpdateProtocolFeeRewardAccumulation(manager *staker.ProtocolFeeRewardManager, tokenPaths []string) { for _, tokenPath := range tokenPaths { accumulatedProtocolFeeX128PerStake := "0" foldedEpoch := int64(-1) if accumulator, ok := manager.GetTokenAccumulator(tokenPath); ok { accumulatedProtocolFeeX128PerStake = accumulator.GetAccumulatedX128PerStake().ToString() foldedEpoch = accumulator.GetFoldedEpoch() } chain.Emit( "UpdateProtocolFeeRewardAccumulation", "tokenPath", tokenPath, "totalStakedAmount", utils.FormatInt(manager.GetTotalStakedAmount()), "protocolFeeAmount", utils.FormatInt(manager.GetProtocolFeeAmount(tokenPath)), "accumulatedProtocolFeeX128PerStake", accumulatedProtocolFeeX128PerStake, "foldedEpoch", utils.FormatInt(foldedEpoch), "accumulatedTimestamp", utils.FormatInt(manager.GetAccumulatedTimestamp()), ) } }
  22. #22errors.gno
  23. #23package staker import ( ufmt "gno.land/p/nt/ufmt/v0" ) const ( errDataNotFound = "[GNOSWAP-GOV_STAKER-001] requested data not found" errInvalidAmount = "[GNOSWAP-GOV_STAKER-002] invalid amount" errNoDelegatedAmount = "[GNOSWAP-GOV_STAKER-003] zero delegated amount" errNotEnoughDelegated = "[GNOSWAP-GOV_STAKER-004] not enough delegated" errInvalidAddress = "[GNOSWAP-GOV_STAKER-005] invalid address" errNotEnoughBalance = "[GNOSWAP-GOV_STAKER-006] not enough balance" errLessThanMinimum = "[GNOSWAP-GOV_STAKER-007] cannot delegate less than minimum amount" errInvalidSnapshotTime = "[GNOSWAP-GOV_STAKER-008] invalid snapshot time" errSameDelegatee = "[GNOSWAP-GOV_STAKER-009] cannot redelegate to same address" errWithdrawNotCollectable = "[GNOSWAP-GOV_STAKER-010] withdraw is not collectable" errAmountMustBeNonNegative = "[GNOSWAP-GOV_STAKER-011] amount must be non-negative" errTotalStakedAmountOverflow = "[GNOSWAP-GOV_STAKER-012] total staked amount would overflow" errRemoveAmountExceedsTotalStaked = "[GNOSWAP-GOV_STAKER-013] remove amount exceeds total staked amount" errRemoveAmountExceedsStaked = "[GNOSWAP-GOV_STAKER-014] remove amount exceeds staked amount" errAmountMustBePositive = "[GNOSWAP-GOV_STAKER-015] amount must be positive" errRewardDebtUpdateRequired = "[GNOSWAP-GOV_STAKER-016] must update reward debt before claiming rewards" errDelegatedAmountNegative = "[GNOSWAP-GOV_STAKER-017] delegated amount cannot be negative" errUndelegationAmountNegative = "[GNOSWAP-GOV_STAKER-018] undelegation amount cannot be negative" errDelegationNotFound = "[GNOSWAP-GOV_STAKER-019] delegation not found" errInvalidAccrualEpoch = "[GNOSWAP-GOV_STAKER-020] accrual epoch is older than the epoch in force" errInvalidAccrualBuckets = "[GNOSWAP-GOV_STAKER-021] invalid accrual buckets" ) func makeErrorWithDetails(message string, detail string) error { return ufmt.Errorf("%s || %s", message, detail) }
  24. #24getter.gno

Result log

msg:0,success:true,log:,events:[]