package staker
import (
"chain"
"chain/runtime"
"errors"
"time"
gnsmath "gno.land/p/gnoswap/gnsmath"
"gno.land/p/gnoswap/utils"
"gno.land/r/gnoswap/access"
"gno.land/r/gnoswap/emission"
"gno.land/r/gnoswap/gns"
"gno.land/r/gnoswap/gov/staker"
"gno.land/r/gnoswap/gov/xgns"
"gno.land/r/gnoswap/halt"
"gno.land/r/gnoswap/referral"
)
// Spoofed-realm guards on entry points reject any caller that fabricates a
// realm value distinct from the current crossing frame. The proxy in
// gno.land/r/gnoswap/gov/staker always forwards its own `cur`, so a mismatch
// here means somebody bypassed the proxy and threaded a fake realm directly
// into the implementation.
// Delegate delegates GNS tokens to an address.
//
// Converts GNS to xGNS and assigns voting power.
// Primary mechanism for participating in governance.
// Can delegate to self or any other address.
//
// Parameters:
// - to: Address to receive voting power (can be self)
// - amount: Amount of GNS to stake and delegate
// - referrer: Optional referral address for tracking
//
// Process:
// 1. Transfers GNS from caller
// 2. Mints equivalent xGNS (1:1 ratio)
// 3. Assigns voting power to target address
// 4. Creates delegation snapshot for voting
//
// Requirements:
// - Minimum 1 GNS delegation
// - Valid target address
// - Sufficient GNS balance
// - Approval for GNS transfer
//
// Returns delegated amount.
func (gs *govStakerV1) Delegate(
_ int,
rlm realm,
to address,
amount int64,
referrer string,
) int64 {
if !rlm.IsCurrent() {
panic(errors.New(errSpoofedRealm))
}
halt.AssertIsNotHaltedGovStaker()
prev := rlm.Previous()
access.AssertIsValidAddress(to)
assertIsValidDelegateAmount(amount)
caller := prev.Address()
from := caller
currentHeight := runtime.ChainHeight()
currentTimestamp := time.Now().Unix()
emission.MintAndDistributeGns(cross(rlm))
delegation, err := gs.delegate(
0,
rlm,
from,
to,
amount,
currentHeight,
currentTimestamp,
)
if err != nil {
panic(err)
}
if err := gs.increaseTotalDelegatedAmount(0, rlm, amount); err != nil {
panic(err)
}
if err := gs.increaseTotalLockedAmount(0, rlm, amount); err != nil {
panic(err)
}
gns.TransferFrom(cross(rlm), from, rlm.Address(), amount)
xgns.Mint(cross(rlm), from, amount)
registeredReferrer := referral.TryRegister(cross(rlm), caller, referrer)
resolver := NewDelegationResolver(delegation)
chain.Emit(
"Delegate",
"prevAddr", prev.Address().String(),
"prevRealm", prev.PkgPath(),
"from", resolver.delegation.DelegateFrom().String(),
"to", resolver.delegation.DelegateTo().String(),
"amount", utils.FormatInt(resolver.DelegatedAmount()),
"totalDelegatedAmount", utils.FormatInt(gs.store.GetTotalDelegatedAmount()),
"referrer", registeredReferrer,
)
return amount
}
// Undelegate undelegates xGNS from the existing delegate.
//
// Initiates withdrawal of staked GNS with lockup period.
// Voting power removed immediately, tokens locked for configurable period.
// Prevents governance attacks through time delay.
//
// Parameters:
// - from: Address currently delegated to
// - amount: Amount of xGNS to undelegate
//
// Process:
// 1. Removes voting power immediately
// 2. Creates withdrawal request with timestamp
// 3. Locks GNS for configurable cooldown period
//
// Requirements:
// - Must have delegated to target address
// - Sufficient delegated amount
//
// After lockup period ends, use CollectUndelegatedGns() to claim GNS.
// Returns undelegated amount.
func (gs *govStakerV1) Undelegate(
_ int,
rlm realm,
from address,
amount int64,
) int64 {
if !rlm.IsCurrent() {
panic(errors.New(errSpoofedRealm))
}
halt.AssertIsNotHaltedWithdraw()
prev := rlm.Previous()
caller := prev.Address()
access.AssertIsValidAddress(from)
assertIsValidDelegateAmount(amount)
currentHeight := runtime.ChainHeight()
currentTimestamp := time.Now().Unix()
emission.MintAndDistributeGns(cross(rlm))
unDelegationAmount, err := gs.unDelegate(
0,
rlm,
caller,
from,
amount,
currentHeight,
currentTimestamp,
)
if err != nil {
panic(err)
}
if err := gs.decreaseTotalDelegatedAmount(0, rlm, unDelegationAmount); err != nil {
panic(err)
}
chain.Emit(
"Undelegate",
"prevAddr", prev.Address().String(),
"prevRealm", prev.PkgPath(),
"from", caller.String(),
"to", from.String(),
"amount", utils.FormatInt(unDelegationAmount),
"totalDelegatedAmount", utils.FormatInt(gs.store.GetTotalDelegatedAmount()),
)
return unDelegationAmount
}
// Redelegate redelegates xGNS from existing delegate to another.
//
// Atomic operation to change delegation target.
// Maintains voting power continuity without unstaking.
// Useful for vote delegation services and dao coordination.
//
// Parameters:
// - delegatee: Current address delegated to
// - newDelegatee: New address to delegate to
// - amount: Amount of xGNS to redelegate
//
// Process:
// 1. Validates current delegation exists
// 2. Removes voting power from old delegatee
// 3. Assigns voting power to new delegatee
// 4. Updates delegation snapshots
//
// Requirements:
// - Must have active delegation to current delegatee
// - Both addresses must be valid
// - Amount must not exceed current delegation
// - Cannot redelegate to same address
//
// No lockup period - instant redelegation.
// Returns redelegated amount.
func (gs *govStakerV1) Redelegate(
_ int,
rlm realm,
delegatee,
newDelegatee address,
amount int64,
) int64 {
if !rlm.IsCurrent() {
panic(errors.New(errSpoofedRealm))
}
halt.AssertIsNotHaltedGovStaker()
prev := rlm.Previous()
caller := prev.Address()
access.AssertIsValidAddress(delegatee)
access.AssertIsValidAddress(newDelegatee)
assertIsValidDelegateAmount(amount)
assertNoSameDelegatee(delegatee, newDelegatee)
currentHeight := runtime.ChainHeight()
currentTimestamp := time.Now().Unix()
delegator := caller
emission.MintAndDistributeGns(cross(rlm))
unDelegationAmount, err := gs.unDelegateWithoutLockup(
0,
rlm,
delegator,
delegatee,
amount,
currentHeight,
currentTimestamp,
)
if err != nil {
panic(err)
}
delegation, err := gs.delegate(
0,
rlm,
delegator,
newDelegatee,
unDelegationAmount,
currentHeight,
currentTimestamp,
)
if err != nil {
panic(err)
}
resolver := NewDelegationResolver(delegation)
chain.Emit(
"Redelegate",
"prevAddr", prev.Address().String(),
"prevRealm", prev.PkgPath(),
"from", delegator.String(),
"previousDelegatee", delegatee.String(),
"newDelegatee", newDelegatee.String(),
"amount", utils.FormatInt(resolver.DelegatedAmount()),
)
return amount
}
// CollectUndelegatedGns collects undelegated GNS tokens.
// Allows users to collect GNS tokens that completed undelegation lockup period.
// Burns xGNS and returns GNS tokens.
func (gs *govStakerV1) CollectUndelegatedGns(_ int, rlm realm) int64 {
if !rlm.IsCurrent() {
panic(errors.New(errSpoofedRealm))
}
halt.AssertIsNotHaltedWithdraw()
prev := rlm.Previous()
caller := prev.Address()
currentTime := time.Now().Unix()
emission.MintAndDistributeGns(cross(rlm))
collectedAmount, err := gs.collectDelegations(0, rlm, caller, currentTime)
if err != nil {
panic(err)
}
if collectedAmount == 0 {
return 0
}
if err := gs.decreaseTotalLockedAmount(0, rlm, collectedAmount); err != nil {
panic(err)
}
xgns.Burn(cross(rlm), caller, collectedAmount)
gns.Transfer(cross(rlm), caller, collectedAmount)
chain.Emit(
"CollectUndelegatedGns",
"prevAddr", prev.Address().String(),
"prevRealm", prev.PkgPath(),
"from", prev.Address().String(),
"to", caller.String(),
"collectedAmount", utils.FormatInt(collectedAmount),
)
return collectedAmount
}
// delegate processes delegation operations.
// Validates delegation amount, creates delegation records, and updates reward tracking.
func (gs *govStakerV1) delegate(
_ int,
rlm realm,
from address,
to address,
amount,
currentHeight,
currentTimestamp int64,
) (*staker.Delegation, error) {
delegationID := gs.nextDelegationID()
delegation := staker.NewDelegation(
delegationID,
from,
to,
amount,
currentHeight,
currentTimestamp,
)
delegationResolver := NewDelegationResolver(delegation)
delegatedAmount := delegationResolver.DelegatedAmount()
if delegatedAmount < 0 {
return nil, errors.New("delegated amount cannot be negative")
}
gs.addDelegation(0, rlm, delegationID, delegation)
gs.addDelegationRecord(0, rlm, to, delegatedAmount, currentTimestamp)
gs.addStakeEmissionReward(0, rlm, from.String(), amount, currentTimestamp)
gs.addStakeProtocolFeeReward(0, rlm, from.String(), amount, currentTimestamp)
return delegation, nil
}
// unDelegate processes undelegation operations with lockup.
// Validates undelegation amount, processes withdrawals, and updates reward tracking.
func (gs *govStakerV1) unDelegate(
_ int,
rlm realm,
delegator,
delegatee address,
amount,
currentHeight,
currentTimestamp int64,
) (int64, error) {
delegationIDs := gs.getUserDelegationIDsWithDelegatee(delegator, delegatee)
if len(delegationIDs) == 0 {
return 0, nil
}
unDelegationAmount := amount
lockupPeriod := gs.store.GetUnDelegationLockupPeriod()
totalDelegated := int64(0)
delegations := make([]*staker.Delegation, 0, len(delegationIDs))
for _, id := range delegationIDs {
delegation, exists := gs.store.GetDelegation(id)
if !exists {
continue
}
totalDelegated = gnsmath.SafeAddInt64(totalDelegated, NewDelegationResolver(delegation).DelegatedAmount())
delegations = append(delegations, delegation)
}
if amount > totalDelegated {
return 0, errors.New(errNotEnoughDelegated)
}
// Process undelegation across multiple delegation records if necessary
for _, delegation := range delegations {
resolver := NewDelegationResolver(delegation)
if resolver.IsEmpty() {
gs.removeDelegation(0, rlm, delegation.ID())
continue
}
currentUnDelegationAmount := unDelegationAmount
if currentUnDelegationAmount > resolver.DelegatedAmount() {
currentUnDelegationAmount = resolver.DelegatedAmount()
}
if currentUnDelegationAmount < 0 {
return 0, errors.New("undelegation amount cannot be negative")
}
resolver.UnDelegate(
currentUnDelegationAmount,
currentHeight,
currentTimestamp,
lockupPeriod,
)
gs.setDelegation(0, rlm, delegation.ID(), delegation)
gs.addDelegationRecord(0, rlm, delegatee, -currentUnDelegationAmount, currentTimestamp)
gs.removeStakeEmissionReward(0, rlm, delegator.String(), currentUnDelegationAmount, currentTimestamp)
gs.removeStakeProtocolFeeReward(0, rlm, delegator.String(), currentUnDelegationAmount, currentTimestamp)
unDelegationAmount = gnsmath.SafeSubInt64(unDelegationAmount, currentUnDelegationAmount)
if unDelegationAmount <= 0 {
break
}
}
return amount, nil
}
// unDelegateWithoutLockup processes undelegation without lockup.
// Used for redelegation where tokens are immediately available.
func (gs *govStakerV1) unDelegateWithoutLockup(
_ int,
rlm realm,
delegator,
delegatee address,
amount,
currentHeight,
currentTime int64,
) (int64, error) {
delegationIDs := gs.getUserDelegationIDsWithDelegatee(delegator, delegatee)
if len(delegationIDs) == 0 {
return 0, errors.New(errNotEnoughDelegated)
}
unDelegationAmount := amount
totalDelegated := int64(0)
delegations := make([]*staker.Delegation, 0, len(delegationIDs))
for _, id := range delegationIDs {
delegation, exists := gs.store.GetDelegation(id)
if !exists {
continue
}
totalDelegated = gnsmath.SafeAddInt64(totalDelegated, NewDelegationResolver(delegation).DelegatedAmount())
delegations = append(delegations, delegation)
}
if amount > totalDelegated {
return 0, errors.New(errNotEnoughDelegated)
}
// Process undelegation across multiple delegation records if necessary
for _, delegation := range delegations {
resolver := NewDelegationResolver(delegation)
if resolver.IsEmpty() {
gs.removeDelegation(0, rlm, delegation.ID())
continue
}
currentUnDelegationAmount := unDelegationAmount
if currentUnDelegationAmount > resolver.DelegatedAmount() {
currentUnDelegationAmount = resolver.DelegatedAmount()
}
resolver.UnDelegateWithoutLockup(
currentUnDelegationAmount,
currentHeight,
currentTime,
)
if resolver.IsEmpty() {
gs.removeDelegation(0, rlm, delegation.ID())
} else {
gs.setDelegation(0, rlm, delegation.ID(), delegation)
}
gs.addDelegationRecord(0, rlm, delegatee, -currentUnDelegationAmount, currentTime)
gs.removeStakeEmissionReward(0, rlm, delegator.String(), currentUnDelegationAmount, currentTime)
gs.removeStakeProtocolFeeReward(0, rlm, delegator.String(), currentUnDelegationAmount, currentTime)
unDelegationAmount = gnsmath.SafeSubInt64(unDelegationAmount, currentUnDelegationAmount)
if unDelegationAmount <= 0 {
break
}
}
return amount, nil
}
func (gs *govStakerV1) increaseTotalDelegatedAmount(_ int, rlm realm, amount int64) error {
currentDelegated := gs.store.GetTotalDelegatedAmount()
if err := gs.store.SetTotalDelegatedAmount(0, rlm, gnsmath.SafeAddInt64(currentDelegated, amount)); err != nil {
return err
}
return nil
}
func (gs *govStakerV1) decreaseTotalDelegatedAmount(_ int, rlm realm, amount int64) error {
currentDelegated := gs.store.GetTotalDelegatedAmount()
newDelegated := gnsmath.SafeSubInt64(currentDelegated, amount)
if newDelegated < 0 {
newDelegated = 0
}
if err := gs.store.SetTotalDelegatedAmount(0, rlm, newDelegated); err != nil {
return err
}
return nil
}
func (gs *govStakerV1) increaseTotalLockedAmount(_ int, rlm realm, amount int64) error {
currentLocked := gs.store.GetTotalLockedAmount()
if err := gs.store.SetTotalLockedAmount(0, rlm, gnsmath.SafeAddInt64(currentLocked, amount)); err != nil {
return err
}
return nil
}
func (gs *govStakerV1) decreaseTotalLockedAmount(_ int, rlm realm, amount int64) error {
currentLocked := gs.store.GetTotalLockedAmount()
newLocked := gnsmath.SafeSubInt64(currentLocked, amount)
if newLocked < 0 {
newLocked = 0
}
if err := gs.store.SetTotalLockedAmount(0, rlm, newLocked); err != nil {
return err
}
return nil
}
// collectDelegations processes collection of undelegated tokens.
// Iterates through user delegations and collects available amounts.
func (gs *govStakerV1) collectDelegations(_ int, rlm realm, user address, currentTime int64) (int64, error) {
totalCollectedAmount := int64(0)
delegationTree := gs.getUserDelegations(user)
var err error
var idsToRemove []int64
allDelegations := gs.store.GetAllDelegations()
// Collect from all available delegations
delegationTree.Iterate("", "", func(delegatee string, value any) bool {
delegationIDs, ok := value.([]int64)
if !ok {
return false
}
if len(delegationIDs) == 0 {
return false
}
for _, id := range delegationIDs {
delegationRaw := allDelegations.Get(utils.FormatInt(id))
if delegationRaw == nil {
continue
}
delegation, ok := delegationRaw.(*staker.Delegation)
if !ok {
continue
}
resolver := NewDelegationResolver(delegation)
collectedAmount, iErr := resolver.processCollection(currentTime)
if iErr != nil {
err = iErr
return true
}
// Simple addition since addToCollectedAmount was removed
totalCollectedAmount = gnsmath.SafeAddInt64(totalCollectedAmount, collectedAmount)
// Save updated delegation state after collection
if resolver.IsEmpty() {
idsToRemove = append(idsToRemove, delegation.ID())
} else {
gs.setDelegation(0, rlm, delegation.ID(), delegation)
}
}
return false
})
for _, id := range idsToRemove {
gs.removeDelegation(0, rlm, id)
}
if err != nil {
return totalCollectedAmount, makeErrorWithDetails(errInvalidAmount, err.Error())
}
return totalCollectedAmount, nil
}