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}