package v1 import ( "chain" "chain/runtime" "time" bptree "gno.land/p/nt/bptree/v0" ufmt "gno.land/p/nt/ufmt/v0" "gno.land/p/gnoswap/gnsmath" prbac "gno.land/p/gnoswap/rbac" "gno.land/r/gnoswap/access" _ "gno.land/r/gnoswap/rbac" "gno.land/r/gnoswap/common" "gno.land/r/gnoswap/halt" sr "gno.land/r/gnoswap/staker" "gno.land/r/gnoswap/gns" en "gno.land/r/gnoswap/emission" pn "gno.land/r/gnoswap/position" i256 "gno.land/p/gnoswap/int256" u256 "gno.land/p/gnoswap/uint256" "gno.land/r/gnoswap/referral" ) const ZERO_ADDRESS = address("") // Deposits manages all staked positions. type Deposits struct { tree *bptree.BPTree } // NewDeposits creates a new Deposits instance. func NewDeposits() *Deposits { return &Deposits{ tree: sr.NewBPTreeN(16), // positionId -> *Deposit } } // Has checks if a position ID exists in deposits. func (self *Deposits) Has(positionId uint64) bool { return self.tree.Has(EncodeUint(positionId)) } // Iterate traverses deposits within the specified range. func (self *Deposits) Iterate(start uint64, end uint64, fn func(positionId uint64, deposit *sr.Deposit) bool) { self.tree.Iterate(EncodeUint(start), EncodeUint(end), func(positionId string, depositI any) bool { dpst := retrieveDeposit(depositI) return fn(DecodeUint(positionId), dpst) }) } func (self *Deposits) IterateByPoolPath(start, end uint64, poolPath string, fn func(positionId uint64, deposit *sr.Deposit) bool) { self.tree.Iterate(EncodeUint(start), EncodeUint(end), func(positionId string, depositI any) bool { deposit := retrieveDeposit(depositI) if deposit.TargetPoolPath() != poolPath { return false } return fn(DecodeUint(positionId), deposit) }) } // Size returns the number of deposits. func (self *Deposits) Size() int { return self.tree.Size() } // get retrieves a deposit by position ID. func (self *Deposits) get(positionId uint64) *sr.Deposit { depositI, ok := self.tree.Get(EncodeUint(positionId)) if !ok { panic(makeErrorWithDetails( errDataNotFound, ufmt.Sprintf("positionId(%d) not found", positionId), )) } return retrieveDeposit(depositI) } // retrieveDeposit safely casts data to Deposit type. func retrieveDeposit(data any) *sr.Deposit { deposit, ok := data.(*sr.Deposit) if !ok { panic("failed to cast value to *Deposit") } return deposit } // set stores a deposit for a position ID. func (self *Deposits) set(positionId uint64, deposit *sr.Deposit) { self.tree.Set(EncodeUint(positionId), deposit) } // remove deletes a deposit by position ID. func (self *Deposits) remove(positionId uint64) { self.tree.Remove(EncodeUint(positionId)) } // ExternalIncentives manages external incentive programs. type ExternalIncentives struct { tree *bptree.BPTree } // NewExternalIncentives creates a new ExternalIncentives instance. func NewExternalIncentives() *ExternalIncentives { return &ExternalIncentives{ tree: sr.NewBPTreeN(16), } } // Has checks if an incentive ID exists. func (self *ExternalIncentives) Has(incentiveId string) bool { return self.tree.Has(incentiveId) } // Size returns the number of external incentives. func (self *ExternalIncentives) Size() int { return self.tree.Size() } // get retrieves an external incentive by ID. func (self *ExternalIncentives) get(incentiveId string) *sr.ExternalIncentive { incentiveI, ok := self.tree.Get(incentiveId) if !ok { panic(makeErrorWithDetails( errDataNotFound, ufmt.Sprintf("incentiveId(%s) not found", incentiveId), )) } incentive, ok := incentiveI.(*sr.ExternalIncentive) if !ok { panic("failed to cast value to *ExternalIncentive") } return incentive } // set stores an external incentive. func (self *ExternalIncentives) set(incentiveId string, incentive *sr.ExternalIncentive) { self.tree.Set(incentiveId, incentive) } // remove deletes an external incentive by ID. func (self *ExternalIncentives) remove(incentiveId string) { self.tree.Remove(incentiveId) } // EmissionCacheUpdateHook updates the emission cache when called. // This follows the same pattern as other hooks in the staker contract. func (s *stakerV1) emissionCacheUpdateHook(_ int, rlm realm, emissionAmountPerSecond int64) { poolTier := s.getPoolTier() if poolTier != nil { currentTime := time.Now().Unix() pools := s.getPools() // First cache the current rewards before updating emission poolTier.cacheReward(currentTime, pools) // Update the current emission cache with the latest value poolTier.currentEmission = emissionAmountPerSecond // Now apply the new emission rate to each pool individually poolTier.applyCacheToAllPools(pools, currentTime, emissionAmountPerSecond) s.updatePoolTier(0, rlm, poolTier) } } // StakeToken stakes an LP position NFT to earn rewards. // // Transfers position NFT to staker and begins reward accumulation. // Eligible for internal incentives (GNS emission) and external rewards. // Position must have liquidity and be in eligible pool tier. // // Parameters: // - positionId: LP position NFT token ID to stake // - referrer: Optional referral address for tracking // // Returns: // - poolPath: Pool identifier (token0:token1:fee) // // Requirements: // - Caller must own the position NFT // - Position must have active liquidity // - Pool must be in tier 1, 2, or 3 // - Position not already staked // // Note: Out-of-range positions earn no rewards but can be staked. func (s *stakerV1) StakeToken(_ int, rlm realm, positionId uint64, referrer string) string { if !rlm.IsCurrent() { panic(errSpoofedRealm) } halt.AssertIsNotHaltedStaker() assertIsNotStaked(s, positionId) en.MintAndDistributeGns(cross(rlm)) previousRealm := rlm.Previous() caller := previousRealm.Address() currentTime := time.Now().Unix() owner := s.nftAccessor.MustOwnerOf(positionIdFrom(positionId)) assertIsPositionOwner(owner, caller) actualReferrer := referral.TryRegister(cross(rlm), caller, referrer) if err := tokenHasLiquidity(positionId); err != nil { panic(err.Error()) } // check pool path from positionId poolPath := pn.GetPositionPoolKey(positionId) pools := s.getPools() pool, ok := pools.Get(poolPath) if !ok { panic(makeErrorWithDetails( errNonIncentivizedPool, ufmt.Sprintf("cannot stake position to non existing pool(%s)", poolPath), )) } err := s.poolHasIncentives(pool) if err != nil { panic(err.Error()) } liquidity := getLiquidity(positionId) tickLower, tickUpper := getTickOf(positionId) warmups := s.store.GetWarmupTemplate() currentWarmups := instantiateWarmup(warmups, currentTime) // staked status deposit := sr.NewDeposit( caller, poolPath, liquidity, currentTime, tickLower, tickUpper, currentWarmups, ) // when staking, add new created incentives to deposit currentIncentiveIds := s.getExternalIncentiveIdsBy(poolPath, 0, currentTime) for _, incentiveId := range currentIncentiveIds { incentive := s.getExternalIncentives().get(incentiveId) // If incentive is ended, not available to collect reward if currentTime > incentive.EndTimestamp() { continue } deposit.AddExternalIncentiveId(incentiveId) } // set last external incentive ids updated at deposit.SetLastExternalIncentiveUpdatedAt(currentTime) deposits := s.getDeposits() deposits.set(positionId, deposit) // transfer NFT ownership to staker contract stakerAddr := access.MustGetAddress(prbac.ROLE_STAKER.String()) if err := s.transferDeposit(0, rlm, positionId, owner, caller, stakerAddr); err != nil { panic(err.Error()) } // after transfer, set caller(user) as position operator (to collect fee and reward) pn.SetPositionOperator(cross(rlm), positionId, caller) poolTier := s.getPoolTier() poolTier.cacheReward(currentTime, pools) s.updatePoolTier(0, rlm, poolTier) signedLiquidity := i256.FromUint256(liquidity) currentTick := s.poolAccessor.GetSlot0Tick(poolPath) poolResolver := NewPoolResolver(pool) isInRange := false if pn.IsInRange(positionId) { isInRange = true poolResolver.modifyDeposit(signedLiquidity, currentTime, currentTick) } // historical tick must be set regardless of the deposit's range if poolResolver.isChangedTick(currentTime, currentTick) { poolResolver.Pool.SetHistoricalTickAt(currentTime, currentTick) } // This could happen because of how position stores the ticks. // Ticks are negated if the token1 < token0. poolResolver.TickResolver(tickUpper).modifyDepositUpper(currentTime, signedLiquidity) poolResolver.TickResolver(tickLower).modifyDepositLower(currentTime, signedLiquidity) s.getPools().set(poolPath, pool) amount0, amount1 := s.calculateAmounts(poolPath, tickLower, tickUpper, liquidity) // Get accumulator values for reward calculation tracking _, globalAccX128 := poolResolver.CurrentGlobalRewardRatioAccumulation(currentTime) stakedLiquidity := poolResolver.CurrentStakedLiquidity(currentTime) lowerTickResolver := poolResolver.TickResolver(tickLower) upperTickResolver := poolResolver.TickResolver(tickUpper) lowerOutsideAccX128 := lowerTickResolver.CurrentOutsideAccumulation(currentTime) upperOutsideAccX128 := upperTickResolver.CurrentOutsideAccumulation(currentTime) chain.Emit( "StakeToken", "prevAddr", previousRealm.Address().String(), "prevRealm", previousRealm.PkgPath(), "positionId", formatUint(positionId), "poolPath", poolPath, "liquidity", liquidity.ToString(), "positionUpperTick", formatAnyInt(tickUpper), "positionLowerTick", formatAnyInt(tickLower), "currentTick", formatAnyInt(currentTick), "isInRange", formatBool(isInRange), "referrer", actualReferrer, "amount0", amount0.ToString(), "amount1", amount1.ToString(), "stakedLiquidity", stakedLiquidity.ToString(), "globalRewardRatioAccX128", globalAccX128, "lowerTickOutsideAccX128", lowerOutsideAccX128.ToString(), "upperTickOutsideAccX128", upperOutsideAccX128.ToString(), ) return poolPath } // transferDeposit transfers deposit ownership to a new address. // // Manages NFT custody during staking operations. // Transfers ownership to staker contract for reward eligibility. // Handles cases where the staker already holds custody. // // Parameters: // - positionId: The ID of the position NFT to transfer // - owner: The current owner of the position // - caller: The entity initiating the transfer // - to: The recipient address (usually staker contract) // // Security Features: // - Prevents self-transfer exploits // - Validates ownership before transfer // - Atomic operation with staking // - No transfer if owner == to (already in custody) // // Returns: // - nil: If owner and recipient are same // - error: If caller unauthorized or transfer fails // // NFT remains locked in staker until unstaking. // Otherwise delegates the transfer to `gnft.TransferFrom`. func (s *stakerV1) transferDeposit(_ int, rlm realm, positionId uint64, owner, caller, to address) error { // If the recipient already owns the NFT, no transfer is needed. if owner == to { return nil } if caller == to { return ufmt.Errorf( "%v: only owner(%s) can transfer positionId(%d), called from %s", errNoPermission, owner, positionId, caller, ) } // transfer NFT ownership return s.nftAccessor.TransferFrom(0, rlm, owner, to, positionIdFrom(positionId)) } // CollectReward harvests accumulated rewards for a staked position. This includes both // internal GNS emission and external incentive rewards. // // State Transition: // 1. Warm-up amounts are clears for both internal and external rewards // 2. Reward tokens are transferred to the owner // 3. Penalty fees are transferred to protocol/community addresses // 4. GNS balance is recalculated // // Requirements: // - Contract must not be halted // - Caller must be the position owner // - Position must be staked (have a deposit record) // // Parameters: // CollectReward claims accumulated rewards without unstaking. // // Parameters: // - positionId: LP position NFT token ID // // Returns poolPath, gnsAmount, externalRewards map, externalPenalties map. func (s *stakerV1) CollectReward(_ int, rlm realm, positionId uint64) (string, string, map[string]int64, map[string]int64) { if !rlm.IsCurrent() { panic(errSpoofedRealm) } halt.AssertIsNotHaltedWithdraw() caller := rlm.Previous().Address() assertIsDepositor(s, caller, positionId) deposit := s.getDeposits().get(positionId) depositResolver := NewDepositResolver(deposit) en.MintAndDistributeGns(cross(rlm)) currentTime := time.Now().Unix() blockHeight := runtime.ChainHeight() previousRealm := rlm.Previous() // get all internal and external rewards reward := s.calcPositionReward(blockHeight, currentTime, positionId) // transfer external rewards to user communityPoolAddr := access.MustGetAddress(prbac.ROLE_COMMUNITY_POOL.String()) toUserExternalReward := make(map[string]int64) toUserExternalPenalty := make(map[string]int64) for incentiveId, rewardAmount := range reward.External { // Skip when user reward is zero. // Do not update last collect time so the reward accrues until // the next collection where a non-zero amount can be delivered. if rewardAmount == 0 { continue } incentive := s.getExternalIncentives().get(incentiveId) if incentive == nil { // Incentive could be missing; skip to keep collection working. chain.Emit( "SkippedMissingIncentive", "prevAddr", previousRealm.Address().String(), "prevRealm", previousRealm.PkgPath(), "positionId", formatUint(positionId), "incentiveId", incentiveId, "currentTime", formatAnyInt(currentTime), "currentHeight", formatAnyInt(blockHeight), ) continue } incentiveResolver := NewExternalIncentiveResolver(incentive) if !incentiveResolver.IsStarted(currentTime) { continue } externalPenalty := reward.ExternalPenalty[incentiveId] totalRewardAmount := safeAddInt64(rewardAmount, externalPenalty) if incentiveResolver.RewardAmount() < totalRewardAmount { // Do not update last collect time here; insufficient funds should // leave the incentive collectible when refilled or corrected. chain.Emit( "InsufficientExternalReward", "prevAddr", previousRealm.Address().String(), "prevRealm", previousRealm.PkgPath(), "positionId", formatUint(positionId), "incentiveId", incentiveId, "requiredAmount", formatAnyInt(totalRewardAmount), "availableAmount", formatAnyInt(incentiveResolver.RewardAmount()), "currentTime", formatAnyInt(currentTime), "currentHeight", formatAnyInt(blockHeight), ) continue } // process reward states rewardToken := incentive.RewardToken() toUserExternalReward[rewardToken] = safeAddInt64(toUserExternalReward[rewardToken], rewardAmount) toUserExternalPenalty[rewardToken] = safeAddInt64(toUserExternalPenalty[rewardToken], externalPenalty) incentive.SetRewardAmount(safeSubInt64(incentive.RewardAmount(), totalRewardAmount)) incentiveResolver.addDistributedRewardAmount(rewardAmount) incentiveResolver.addAccumulatedPenaltyAmount(externalPenalty) depositResolver.addCollectedExternalReward(incentiveId, totalRewardAmount) // Update the last collect time ONLY for this specific incentive // This happens only if the reward was successfully transferred. err := depositResolver.updateExternalRewardLastCollectTime(incentiveId, currentTime) if err != nil { panic(err) } // If incentive ended and user already collected after end, remove from index // This ensures deposit's incentive list shrinks over time as incentives complete if depositResolver.ExternalRewardLastCollectTime(incentiveId) > incentiveResolver.EndTimestamp() { deposit.RemoveExternalIncentiveId(incentiveId) } // update s.getExternalIncentives().set(incentiveId, incentive) toUser, feeAmount, err := s.handleStakingRewardFee(0, rlm, rewardToken, rewardAmount, false) if err != nil { panic(err.Error()) } if toUser > 0 { common.SafeGRC20Transfer(cross(rlm), rewardToken, deposit.Owner(), toUser) } chain.Emit( "ProtocolFeeExternalReward", "prevAddr", previousRealm.Address().String(), "prevRealm", previousRealm.PkgPath(), "fromPositionId", formatUint(positionId), "fromPoolPath", incentive.TargetPoolPath(), "feeTokenPath", rewardToken, "feeAmount", formatAnyInt(feeAmount), "currentTime", formatAnyInt(currentTime), "currentHeight", formatAnyInt(blockHeight), ) pool, _ := s.getPools().Get(deposit.TargetPoolPath()) poolResolver := NewPoolResolver(pool) _, globalAccX128 := poolResolver.CurrentGlobalRewardRatioAccumulation(currentTime) stakedLiquidity := poolResolver.CurrentStakedLiquidity(currentTime) tickLower := deposit.TickLower() tickUpper := deposit.TickUpper() lowerOutsideAccX128 := poolResolver.TickResolver(tickLower).CurrentOutsideAccumulation(currentTime) upperOutsideAccX128 := poolResolver.TickResolver(tickUpper).CurrentOutsideAccumulation(currentTime) chain.Emit( "CollectReward", "prevAddr", previousRealm.Address().String(), "prevRealm", previousRealm.PkgPath(), "positionId", formatUint(positionId), "poolPath", deposit.TargetPoolPath(), "recipient", deposit.Owner().String(), "incentiveId", incentiveId, "rewardToken", rewardToken, "rewardAmount", formatAnyInt(rewardAmount), "rewardToUser", formatAnyInt(toUser), "rewardToFee", formatAnyInt(rewardAmount-toUser), "rewardPenalty", formatAnyInt(externalPenalty), "currentTime", formatAnyInt(currentTime), "currentHeight", formatAnyInt(blockHeight), "stakedLiquidity", stakedLiquidity.ToString(), "globalRewardRatioAccX128", globalAccX128, "lowerTickOutsideAccX128", lowerOutsideAccX128.ToString(), "upperTickOutsideAccX128", upperOutsideAccX128.ToString(), ) } internalReward := int64(0) internalRewardToUser := int64(0) internalRewardToFee := int64(0) internalRewardPenalty := int64(0) // Skip internal reward state update when user reward is zero (only penalty). // Do not update last collect time so the reward accrues until the next // collection where a non-zero amount can be delivered. skipInternalUpdate := reward.Internal == 0 // internal reward to user if !skipInternalUpdate { toUser, feeAmount, err := s.handleStakingRewardFee(0, rlm, GNS_PATH, reward.Internal, true) if err != nil { panic(err.Error()) } internalReward = reward.Internal internalRewardToUser = toUser internalRewardToFee = feeAmount internalRewardPenalty = reward.InternalPenalty chain.Emit( "ProtocolFeeInternalReward", "prevAddr", previousRealm.Address().String(), "prevRealm", previousRealm.PkgPath(), "fromPositionId", formatUint(positionId), "fromPoolPath", deposit.TargetPoolPath(), "feeTokenPath", GNS_PATH, "feeAmount", formatAnyInt(internalRewardToFee), "currentTime", formatAnyInt(currentTime), "currentHeight", formatAnyInt(blockHeight), ) } totalEmissionSent := s.store.GetTotalEmissionSent() if internalRewardToUser > 0 { // internal reward to user totalEmissionSent = safeAddInt64(totalEmissionSent, internalRewardToUser) depositResolver.addCollectedInternalReward(reward.Internal) } if internalRewardPenalty > 0 { // internal penalty to community pool totalEmissionSent = safeAddInt64(totalEmissionSent, internalRewardPenalty) depositResolver.addCollectedInternalReward(internalRewardPenalty) } // Unclaimable must be processed after regular rewards so that accumulated // unclaimable amounts are reset in the same collect window. unClaimableInternal := s.processUnClaimableReward(depositResolver.TargetPoolPath(), currentTime) if unClaimableInternal > 0 { totalEmissionSent = safeAddInt64(totalEmissionSent, unClaimableInternal) } err := s.store.SetTotalEmissionSent(0, rlm, totalEmissionSent) if err != nil { panic(err) } if !skipInternalUpdate { // Update lastCollectTime for internal rewards (GNS emissions) err = depositResolver.updateInternalRewardLastCollectTime(currentTime) if err != nil { panic(err) } } deposits := s.getDeposits() deposits.set(positionId, deposit) if internalRewardToUser > 0 { gns.Transfer(cross(rlm), deposit.Owner(), internalRewardToUser) } if internalRewardPenalty > 0 { gns.Transfer(cross(rlm), communityPoolAddr, internalRewardPenalty) } if unClaimableInternal > 0 { gns.Transfer(cross(rlm), communityPoolAddr, unClaimableInternal) } rewardToUser := formatAnyInt(internalRewardToUser) rewardPenalty := formatAnyInt(internalRewardPenalty) if !skipInternalUpdate { poolPath := depositResolver.TargetPoolPath() pool, _ := s.getPools().Get(poolPath) poolResolver := NewPoolResolver(pool) // Get accumulator values for reward calculation tracking _, globalAccX128 := poolResolver.CurrentGlobalRewardRatioAccumulation(currentTime) stakedLiquidity := poolResolver.CurrentStakedLiquidity(currentTime) lowerTickResolver := poolResolver.TickResolver(deposit.TickLower()) upperTickResolver := poolResolver.TickResolver(deposit.TickUpper()) lowerOutsideAccX128 := lowerTickResolver.CurrentOutsideAccumulation(currentTime) upperOutsideAccX128 := upperTickResolver.CurrentOutsideAccumulation(currentTime) chain.Emit( "CollectReward", "prevAddr", previousRealm.Address().String(), "prevRealm", previousRealm.PkgPath(), "positionId", formatUint(positionId), "poolPath", depositResolver.TargetPoolPath(), "recipient", depositResolver.Owner().String(), "rewardToken", GNS_PATH, "rewardAmount", formatAnyInt(internalReward), "rewardToUser", rewardToUser, "rewardToFee", formatAnyInt(internalRewardToFee), "rewardPenalty", rewardPenalty, "rewardUnClaimableAmount", formatAnyInt(unClaimableInternal), "currentTime", formatAnyInt(currentTime), "currentHeight", formatAnyInt(blockHeight), "stakedLiquidity", stakedLiquidity.ToString(), "globalRewardRatioAccX128", globalAccX128, "lowerTickOutsideAccX128", lowerOutsideAccX128.ToString(), "upperTickOutsideAccX128", upperOutsideAccX128.ToString(), ) } return rewardToUser, rewardPenalty, toUserExternalReward, toUserExternalPenalty } // UnStakeToken withdraws an LP token from staking, collecting all pending rewards // and returning the token to its original owner. // // Parameters: // - positionId: LP position NFT token ID to unstake // - unwrapResult: Convert WUGNOT to GNOT if true // // Process: // 1. Collects all pending rewards (GNS + external) // 2. Transfers NFT ownership back to original owner // 3. Clears position operator rights // 4. Removes from reward tracking systems // 5. Cleans up all staking metadata // // Returns: // - poolPath: Pool identifier where position was staked // // Requirements: // - Caller must be the depositor // - Position must be currently staked func (s *stakerV1) UnStakeToken(_ int, rlm realm, positionId uint64) string { // poolPath if !rlm.IsCurrent() { panic(errSpoofedRealm) } caller := rlm.Previous().Address() halt.AssertIsNotHaltedWithdraw() assertIsDepositor(s, caller, positionId) deposit := s.getDeposits().get(positionId) // unStaked status poolPath := deposit.TargetPoolPath() // claim All Rewards s.CollectReward(0, rlm, positionId) if err := s.applyUnStake(positionId); err != nil { panic(err) } // transfer NFT ownership to origin owner stakerAddr := access.MustGetAddress(prbac.ROLE_STAKER.String()) s.nftAccessor.TransferFrom(0, rlm, stakerAddr, deposit.Owner(), positionIdFrom(positionId)) pn.SetPositionOperator(cross(rlm), positionId, ZERO_ADDRESS) // get position information for event liquidity := getLiquidity(positionId) tickLower, tickUpper := getTickOf(positionId) amount0, amount1 := s.calculateAmounts(poolPath, tickLower, tickUpper, liquidity) // Get pool and accumulator values for reward calculation tracking currentTime := time.Now().Unix() pool, _ := s.getPools().Get(poolPath) poolResolver := NewPoolResolver(pool) currentTick := s.poolAccessor.GetSlot0Tick(poolPath) _, globalAccX128 := poolResolver.CurrentGlobalRewardRatioAccumulation(currentTime) stakedLiquidity := poolResolver.CurrentStakedLiquidity(currentTime) previousRealm := rlm.Previous() chain.Emit( "UnStakeToken", "prevAddr", previousRealm.Address().String(), "prevRealm", previousRealm.PkgPath(), "positionId", formatUint(positionId), "poolPath", poolPath, "liquidity", liquidity.ToString(), "amount0", amount0.ToString(), "amount1", amount1.ToString(), "from", stakerAddr.String(), "to", deposit.Owner().String(), "currentTick", formatAnyInt(currentTick), "stakedLiquidity", stakedLiquidity.ToString(), "globalRewardRatioAccX128", globalAccX128, ) return poolPath } func (s *stakerV1) applyUnStake(positionId uint64) error { deposit := s.getDeposits().get(positionId) depositResolver := NewDepositResolver(deposit) pool, ok := s.getPools().Get(depositResolver.TargetPoolPath()) poolResolver := NewPoolResolver(pool) if !ok { return ufmt.Errorf( "%v: pool(%s) does not exist", errDataNotFound, depositResolver.TargetPoolPath(), ) } currentTime := time.Now().Unix() currentTick := s.poolAccessor.GetSlot0Tick(depositResolver.TargetPoolPath()) signedLiquidity := i256.Zero().Neg(i256.FromUint256(depositResolver.Liquidity())) if pn.IsInRange(positionId) { poolResolver.modifyDeposit(signedLiquidity, currentTime, currentTick) } upperTick := poolResolver.TickResolver(depositResolver.TickUpper()) lowerTick := poolResolver.TickResolver(depositResolver.TickLower()) upperTick.modifyDepositUpper(currentTime, signedLiquidity) lowerTick.modifyDepositLower(currentTime, signedLiquidity) s.getDeposits().remove(positionId) return nil } // poolHasIncentives checks if the pool has any stakeable incentives (internal or external). // External incentive eligibility (active or within short future window) is handled inside IsExternallyIncentivizedPool. func (s *stakerV1) poolHasIncentives(pool *sr.Pool) error { poolPath := pool.PoolPath() hasInternal := s.getPoolTier().IsInternallyIncentivizedPool(poolPath) hasExternal := NewPoolResolver(pool).IsExternallyIncentivizedPool() if !hasInternal && !hasExternal { return ufmt.Errorf( "%v: cannot stake position to non incentivized pool(%s)", errNonIncentivizedPool, poolPath, ) } return nil } // tokenHasLiquidity checks if the target positionId has non-zero liquidity func tokenHasLiquidity(positionId uint64) error { if getLiquidity(positionId).Lte(zeroUint256) { return ufmt.Errorf( "%v: positionId(%d) has no liquidity", errZeroLiquidity, positionId, ) } return nil } func getLiquidity(positionId uint64) *u256.Uint { return u256.MustFromDecimal(pn.GetPositionLiquidity(positionId)) } func getTickOf(positionId uint64) (int32, int32) { tickLower := pn.GetPositionTickLower(positionId) tickUpper := pn.GetPositionTickUpper(positionId) if tickUpper < tickLower { panic(ufmt.Sprintf("tickUpper(%d) is less than tickLower(%d)", tickUpper, tickLower)) } return tickLower, tickUpper } // calculateAmounts calculates the amounts of token0 and token1 for a given liquidity and range. func (s *stakerV1) calculateAmounts(poolPath string, tickLower, tickUpper int32, liquidity *u256.Uint) (*u256.Uint, *u256.Uint) { sqrtPriceX96 := u256.MustFromDecimal(s.poolAccessor.GetSlot0SqrtPriceX96(poolPath)) sqrtPriceLowerX96 := gnsmath.TickMathGetSqrtRatioAtTick(tickLower) sqrtPriceUpperX96 := gnsmath.TickMathGetSqrtRatioAtTick(tickUpper) return gnsmath.GetAmountsForLiquidity(sqrtPriceX96, sqrtPriceLowerX96, sqrtPriceUpperX96, liquidity) }