Search Apps Documentation Source Content File Folder Download Copy Actions Download

calculate_pool_position_reward.gno

6.99 Kb · 214 lines
  1package v1
  2
  3import (
  4	bptree "gno.land/p/nt/bptree/v0"
  5	sr "gno.land/r/gnoswap/staker"
  6)
  7
  8// Reward is a struct for storing reward for a position.
  9// Internal reward is the GNS reward, external reward is the reward for other incentives.
 10// Penalties are the amount that is deducted from the reward due to the position's warmup.
 11type Reward struct {
 12	Internal        int64
 13	InternalPenalty int64
 14	External        map[string]int64 // Incentive ID -> TokenAmount
 15	ExternalPenalty map[string]int64 // Incentive ID -> TokenAmount
 16}
 17
 18// calculate total position rewards and penalties
 19func (s *stakerV1) calcPositionReward(currentHeight, currentTimestamp int64, positionId uint64) Reward {
 20	rewards := s.calculatePositionReward(&CalcPositionRewardParam{
 21		CurrentHeight: currentHeight,
 22		CurrentTime:   currentTimestamp,
 23		Deposits:      s.getDeposits(),
 24		Pools:         s.getPools(),
 25		PoolTier:      s.getPoolTier(),
 26		PositionId:    positionId,
 27	})
 28
 29	internal := int64(0)
 30	internalPenalty := int64(0)
 31
 32	rewardLen := len(rewards)
 33	externalReward := make(map[string]int64, rewardLen)
 34	externalPenalty := make(map[string]int64, rewardLen)
 35
 36	for _, reward := range rewards {
 37		internal = safeAddInt64(internal, reward.Internal)
 38		internalPenalty = safeAddInt64(internalPenalty, reward.InternalPenalty)
 39
 40		for incentive, amount := range reward.External {
 41			externalReward[incentive] = safeAddInt64(externalReward[incentive], amount)
 42		}
 43
 44		for incentive, penalty := range reward.ExternalPenalty {
 45			externalPenalty[incentive] = safeAddInt64(externalPenalty[incentive], penalty)
 46		}
 47	}
 48
 49	return Reward{
 50		Internal:        internal,
 51		InternalPenalty: internalPenalty,
 52		External:        externalReward,
 53		ExternalPenalty: externalPenalty,
 54	}
 55}
 56
 57// CalcPositionRewardParam is a struct for calculating position reward
 58type CalcPositionRewardParam struct {
 59	// Environmental variables
 60	CurrentHeight int64
 61	CurrentTime   int64
 62	Deposits      *Deposits
 63	Pools         *Pools
 64	PoolTier      *PoolTier
 65
 66	// Position variables
 67	PositionId uint64
 68}
 69
 70func (s *stakerV1) calculatePositionReward(param *CalcPositionRewardParam) []Reward {
 71	deposit := param.Deposits.get(param.PositionId)
 72	depositResolver := NewDepositResolver(deposit)
 73	poolPath := deposit.TargetPoolPath()
 74
 75	pool, ok := param.Pools.Get(poolPath)
 76	if !ok {
 77		pool = sr.NewPool(poolPath, param.CurrentTime)
 78		param.Pools.set(poolPath, pool)
 79	}
 80	poolResolver := NewPoolResolver(pool)
 81
 82	// Cache reward/accumulators only for the pool we are currently calculating.
 83	param.PoolTier.cacheRewardForPool(param.CurrentTime, param.Pools, poolPath)
 84
 85	lastCollectTime := depositResolver.InternalRewardLastCollectTime()
 86
 87	// Initializes reward/penalty arrays for rewards and penalties for each warmup
 88	rewardState := poolResolver.RewardStateOf(deposit)
 89
 90	// Calculate internal rewards regardless of current tier status
 91	// The reward cache system will automatically handle periods with 0 rewards
 92	// This allows collecting rewards earned while the pool was in a tier,
 93	// while preventing new rewards after tier removal
 94	calculatedInternalRewards, calculatedInternalPenalties := rewardState.calculateInternalReward(lastCollectTime, param.CurrentTime)
 95
 96	warmupLen := len(deposit.Warmups())
 97	rewards := make([]Reward, warmupLen)
 98	for i := 0; i < warmupLen; i++ {
 99		rewards[i] = Reward{
100			Internal:        calculatedInternalRewards[i],
101			InternalPenalty: calculatedInternalPenalties[i],
102			External:        make(map[string]int64),
103			ExternalPenalty: make(map[string]int64),
104		}
105	}
106	rewardState.reset()
107
108	lastExternalIncentiveUpdatedAt := depositResolver.LastExternalIncentiveUpdatedAt()
109
110	// update deposit's incentive list with new incentives created since last update
111	if lastExternalIncentiveUpdatedAt < param.CurrentTime {
112		// get new incentives created since last update
113		currentIncentiveIds := s.getExternalIncentiveIdsBy(poolPath, lastExternalIncentiveUpdatedAt, param.CurrentTime)
114
115		// add new created incentives to deposit
116		for _, incentiveId := range currentIncentiveIds {
117			deposit.AddExternalIncentiveId(incentiveId)
118		}
119
120		deposit.SetLastExternalIncentiveUpdatedAt(param.CurrentTime)
121	}
122
123	incentivesResolver := poolResolver.IncentivesResolver()
124
125	// Use deposit's indexed incentive IDs instead of iterating all pool incentives
126	deposit.IterateExternalIncentiveIds(func(incentiveId string) bool {
127		incentive, ok := incentivesResolver.Get(incentiveId)
128		if !ok {
129			return false
130		}
131
132		incentiveResolver := NewExternalIncentiveResolver(incentive)
133
134		// Check if incentive is active during this specific collection period
135		if !incentiveResolver.IsStarted(param.CurrentTime) {
136			return false
137		}
138
139		// External incentivized pool.
140		// Calculate reward for each warmup using per-incentive lastCollectTime
141		externalLastCollectTime := depositResolver.ExternalRewardLastCollectTime(incentiveId)
142		externalReward, externalPenalty := rewardState.calculateExternalReward(externalLastCollectTime, param.CurrentTime, incentive)
143
144		for i := range externalReward {
145			if externalReward[i] > 0 || externalPenalty[i] > 0 {
146				rewards[i].External[incentiveId] = externalReward[i]
147				rewards[i].ExternalPenalty[incentiveId] = externalPenalty[i]
148			}
149		}
150
151		rewardState.reset()
152
153		return false
154	})
155
156	return rewards
157}
158
159// calculates internal unclaimable reward for the pool
160func (s *stakerV1) processUnClaimableReward(poolPath string, endTimestamp int64) int64 {
161	pool, ok := s.getPools().Get(poolPath)
162	if !ok {
163		return 0
164	}
165	poolResolver := NewPoolResolver(pool)
166	return poolResolver.processUnclaimableReward(endTimestamp)
167}
168
169// update deposit's incentive list with new incentives created since last update
170func (s *stakerV1) getExternalIncentiveIdsBy(poolPath string, startTime, endTime int64) []string {
171	currentIncentiveIds := make([]string, 0)
172
173	incentivesByTime := s.getExternalIncentivesByCreationTime()
174
175	incentivesByTime.Iterate(startTime, endTime, func(_ int64, value any) bool {
176		// Value is a slice of incentive IDs (handles timestamp collisions)
177		poolIncentiveIds, ok := value.(*bptree.BPTree)
178		if !ok {
179			return false
180		}
181
182		incentiveIdsValue, exists := poolIncentiveIds.Get(poolPath)
183		if !exists {
184			return false
185		}
186
187		incentiveIds, ok := incentiveIdsValue.([]string)
188		if !ok {
189			return false
190		}
191
192		currentIncentiveIds = append(currentIncentiveIds, incentiveIds...)
193
194		return false
195	})
196
197	return currentIncentiveIds
198}
199
200// getInitialCollectTime determines the initial collection time for an incentive
201// by taking the maximum of the deposit's stake time and the incentive's start time.
202// This ensures rewards are only calculated from when both conditions are met:
203// - The position must be staked (deposit.stakeTime)
204// - The incentive must be active (incentive.startTimestamp)
205//
206// This function is used for lazy initialization when a position collects
207// from an incentive for the first time, avoiding the need to iterate through
208// all deposits when a new incentive is created.
209func getInitialCollectTime(deposit *sr.Deposit, incentive *sr.ExternalIncentive) int64 {
210	if deposit.StakeTime() > incentive.StartTimestamp() {
211		return deposit.StakeTime()
212	}
213	return incentive.StartTimestamp()
214}