Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

calculate_pool_position_reward.gno

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