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

unstaked_position.gno

8.14 Kb · 259 lines
  1package staker
  2
  3import (
  4	"chain"
  5	"chain/runtime"
  6	"time"
  7
  8	bptree "gno.land/p/nt/bptree/v0"
  9	ufmt "gno.land/p/nt/ufmt/v0"
 10
 11	"gno.land/p/gnoswap/gnsmath/v1"
 12	"gno.land/p/gnoswap/utils/v1"
 13
 14	sr "gno.land/r/gnoswap/staker"
 15)
 16
 17// UnstakedPositions manages exit checkpoints.
 18type UnstakedPositions struct {
 19	tree *bptree.BPTree
 20}
 21
 22// NewUnstakedPositions creates a new UnstakedPositions instance.
 23//
 24// Returns:
 25//   - *UnstakedPositions: Empty exit-checkpoint manager backed by a new B+ tree.
 26func NewUnstakedPositions() *UnstakedPositions {
 27	return &UnstakedPositions{
 28		tree: sr.NewBPTreeN(16), // positionId -> *UnstakedPosition
 29	}
 30}
 31
 32// Has checks if a position ID has an exit checkpoint.
 33//
 34// Parameters:
 35//   - positionId: Position ID whose exit-checkpoint membership is checked.
 36//
 37// Returns:
 38//   - bool: true when an exit checkpoint exists for positionId; false otherwise.
 39func (self *UnstakedPositions) Has(positionId uint64) bool {
 40	return self.tree.Has(utils.EncodeUint64(positionId))
 41}
 42
 43// get retrieves an exit checkpoint by position ID, or nil when there is none.
 44func (self *UnstakedPositions) get(positionId uint64) *sr.UnstakedPosition {
 45	value := self.tree.Get(utils.EncodeUint64(positionId))
 46	if value == nil {
 47		return nil
 48	}
 49
 50	position, ok := value.(*sr.UnstakedPosition)
 51	if !ok {
 52		panic(ufmt.Sprintf("failed to cast value to *UnstakedPosition: %T", value))
 53	}
 54
 55	return position
 56}
 57
 58// set stores an exit checkpoint for a position ID.
 59func (self *UnstakedPositions) set(positionId uint64, position *sr.UnstakedPosition) {
 60	self.tree.Set(utils.EncodeUint64(positionId), position)
 61}
 62
 63// remove deletes an exit checkpoint by position ID.
 64func (self *UnstakedPositions) remove(positionId uint64) {
 65	self.tree.Remove(utils.EncodeUint64(positionId))
 66}
 67
 68// getUnstakedPositions returns the exit checkpoints backed by the store.
 69func (s *stakerV1) getUnstakedPositions() *UnstakedPositions {
 70	return &UnstakedPositions{tree: s.store.GetUnstakedPositions()}
 71}
 72
 73// uncollectedIncentiveCountOf returns how many checkpoints still owe the incentive.
 74//
 75// The count is per incentive, not per pool: one checkpoint that can never be funded would
 76// otherwise lock the refunds of every other incentive in the same pool.
 77func (s *stakerV1) uncollectedIncentiveCountOf(incentiveId string) int64 {
 78	value := s.store.GetUncollectedIncentiveCounts().Get(incentiveId)
 79	if value == nil {
 80		return 0
 81	}
 82
 83	count, ok := value.(int64)
 84	if !ok {
 85		panic(ufmt.Sprintf("failed to cast uncollected incentive count to int64: %T", value))
 86	}
 87
 88	return count
 89}
 90
 91// addUncollectedIncentiveCount moves the incentive's count by delta, dropping the entry at zero.
 92func (s *stakerV1) addUncollectedIncentiveCount(incentiveId string, delta int64) {
 93	addUncollectedIncentiveCountTo(s.store.GetUncollectedIncentiveCounts(), incentiveId, delta)
 94}
 95
 96func addUncollectedIncentiveCountTo(counts *bptree.BPTree, incentiveId string, delta int64) {
 97	count := int64(0)
 98	if value := counts.Get(incentiveId); value != nil {
 99		casted, ok := value.(int64)
100		if !ok {
101			panic(ufmt.Sprintf("failed to cast uncollected incentive count to int64: %T", value))
102		}
103		count = casted
104	}
105
106	next := gnsmath.SafeAddInt64(count, delta)
107	if next < 0 {
108		panic(makeErrorWithDetails(
109			errCalculationError,
110			ufmt.Sprintf("uncollected count of incentive(%s) went negative", incentiveId),
111		))
112	}
113
114	if next == 0 {
115		counts.Remove(incentiveId)
116		return
117	}
118
119	counts.Set(incentiveId, next)
120}
121
122// recordUnstakedPosition writes the exit checkpoint before the teardown touches the pool.
123// The deposit's incentive index is brought up to date first, so the checkpoint owes exactly
124// the incentives the position accrued from.
125func (s *stakerV1) recordUnstakedPosition(_ int, rlm realm, positionId uint64, deposit *sr.Deposit, exitTime int64) *sr.UnstakedPosition {
126	poolPath := deposit.TargetPoolPath()
127
128	pool, ok := s.getPools().Get(poolPath)
129	if !ok {
130		panic(makeErrorWithDetails(
131			errDataNotFound,
132			ufmt.Sprintf("pool(%s) does not exist", poolPath),
133		))
134	}
135
136	poolResolver := NewPoolResolver(pool)
137	depositResolver := NewDepositResolver(deposit)
138
139	lastUpdatedAt := depositResolver.LastExternalIncentiveUpdatedAt()
140	if lastUpdatedAt < exitTime {
141		poolResolver.IncentivesResolver().IterateIncentiveIdsByTime(lastUpdatedAt, exitTime, func(incentiveId string) bool {
142			deposit.AddExternalIncentiveId(incentiveId)
143			return false
144		})
145		deposit.SetLastExternalIncentiveUpdatedAt(exitTime)
146	}
147
148	tier, tierRatio, tierCount, err := s.getPoolTier().tierContextOf(poolPath)
149	if err != nil {
150		panic(err)
151	}
152
153	lowerTick := poolResolver.GetOrNewTick(deposit.TickLower())
154	upperTick := poolResolver.GetOrNewTick(deposit.TickUpper())
155	incentiveIds := deposit.GetExternalIncentiveIdList()
156
157	position := sr.NewUnstakedPosition(
158		deposit,
159		exitTime,
160		poolResolver.CurrentTick(exitTime),
161		lowerTick,
162		upperTick,
163		NewTickResolver(lowerTick).CurrentOutsideAccumulation(exitTime).ToString(),
164		NewTickResolver(upperTick).CurrentOutsideAccumulation(exitTime).ToString(),
165		tier,
166		tierRatio,
167		tierCount,
168		s.GetUnstakingFee(),
169		incentiveIds,
170	)
171
172	s.getUnstakedPositions().set(positionId, position)
173
174	counts := s.store.GetUncollectedIncentiveCounts()
175	for _, incentiveId := range incentiveIds {
176		addUncollectedIncentiveCountTo(counts, incentiveId, 1)
177	}
178
179	return position
180}
181
182// dropUnstakedPositionIfCollected removes the checkpoint once every reward source is collected.
183func (s *stakerV1) dropUnstakedPositionIfCollected(_ int, rlm realm, target *collectTarget) {
184	if !target.checkpoint.FullyCollected() {
185		return
186	}
187
188	s.getUnstakedPositions().remove(target.positionId)
189
190	previousRealm := rlm.Previous()
191	chain.Emit(
192		"ClearUnstakedPosition",
193		"prevAddr", previousRealm.Address().String(),
194		"prevRealm", previousRealm.PkgPath(),
195		"positionId", utils.FormatUint(target.positionId),
196		"poolPath", target.deposit.TargetPoolPath(),
197		"exitTime", utils.FormatInt(target.checkpoint.ExitTime()),
198		"currentTime", utils.FormatInt(time.Now().Unix()),
199		"currentHeight", utils.FormatInt(runtime.ChainHeight()),
200	)
201}
202
203// HasUnstakedPosition reports whether a position is staked no longer but still has uncollected rewards.
204//
205// Parameters:
206//   - positionId: Position ID whose exit-checkpoint membership is checked.
207//
208// Returns:
209//   - bool: true when positionId has an exit checkpoint; false otherwise.
210func (s *stakerV1) HasUnstakedPosition(positionId uint64) bool {
211	return s.getUnstakedPositions().Has(positionId)
212}
213
214// GetUnstakedPositionExitTime returns when an unstaked position stopped accruing rewards.
215//
216// Parameters:
217//   - positionId: Position ID whose exit checkpoint is queried.
218//
219// Returns:
220//   - int64: Exit timestamp in Unix seconds.
221//   - error: Non-nil when positionId has no uncollected exit checkpoint; nil on success.
222func (s *stakerV1) GetUnstakedPositionExitTime(positionId uint64) (int64, error) {
223	position := s.getUnstakedPositions().get(positionId)
224	if position == nil {
225		return 0, ufmt.Errorf("%v: positionId(%d) has no uncollected reward", errDataNotFound, positionId)
226	}
227
228	return position.ExitTime(), nil
229}
230
231// GetUnstakedPositionPendingIncentives returns the incentives an unstaked position has yet to
232// collect.
233//
234// Parameters:
235//   - positionId: Position ID whose pending incentive IDs are queried.
236//
237// Returns:
238//   - []string: External incentive IDs with rewards still pending for the checkpoint.
239//   - error: Non-nil when positionId has no uncollected exit checkpoint; nil on success.
240func (s *stakerV1) GetUnstakedPositionPendingIncentives(positionId uint64) ([]string, error) {
241	position := s.getUnstakedPositions().get(positionId)
242	if position == nil {
243		return nil, ufmt.Errorf("%v: positionId(%d) has no uncollected reward", errDataNotFound, positionId)
244	}
245
246	return position.PendingIncentiveIdList(), nil
247}
248
249// GetUncollectedIncentiveCount returns how many unstaked positions still owe a reward from the
250// incentive; EndExternalIncentive is refused while it is non-zero.
251//
252// Parameters:
253//   - incentiveId: External incentive identifier whose outstanding checkpoint count is queried.
254//
255// Returns:
256//   - int64: Number of exit checkpoints still owing incentiveId; zero permits incentive finalization.
257func (s *stakerV1) GetUncollectedIncentiveCount(incentiveId string) int64 {
258	return s.uncollectedIncentiveCountOf(incentiveId)
259}