pool.gno
28.26 Kb · 937 lines
1package staker
2
3import (
4 "errors"
5 "strconv"
6 "time"
7
8 "gno.land/p/gnoswap/consts"
9 i256 "gno.land/p/gnoswap/int256"
10 u256 "gno.land/p/gnoswap/uint256"
11 bptree "gno.land/p/nt/bptree/v0"
12 ufmt "gno.land/p/nt/ufmt/v0"
13)
14
15const AllTierCount = 4 // 0, 1, 2, 3
16
17// Pool is a struct for storing an incentivized pool information
18// Each pool stores Incentives and Ticks associated with it.
19//
20// Fields:
21// - poolPath: The path of the pool.
22//
23// - currentStakedLiquidity:
24// The current total staked liquidity of the in-range positions for the pool.
25// Updated when tick cross happens or stake/unstake happens.
26// Used to calculate the global reward ratio accumulation or
27// decide whether to enter/exit unclaimable period.
28//
29// - lastUnclaimableTime:
30// The time at which the unclaimable period started.
31// Set to 0 when the pool is not in an unclaimable period.
32//
33// - unclaimableAcc:
34// The accumulated undisributed unclaimable reward.
35// Reset to 0 when processUnclaimableReward is called and sent to community pool.
36//
37// - rewardCache:
38// The cached per-second reward emitted for this pool.
39// Stores new entry only when the reward is changed.
40// PoolTier.cacheReward() updates this.
41//
42// - incentives: The external incentives associated with the pool.
43//
44// - ticks: The Ticks associated with the pool.
45//
46// - globalRewardRatioAccumulation:
47// Global ratio of Time / TotalStake accumulation(since the pool creation)
48// Stores new entry only when tick cross or stake/unstake happens.
49// It is used to calculate the reward for a staked position at certain time.
50//
51// - historicalTick:
52// The historical tick for the pool at a given time.
53// It does not reflect the exact tick at the timestamp,
54// but it provides correct ordering for the staked position's ticks.
55// Therefore, you should not compare it for equality, only for ordering.
56// Set when tick cross happens or a new position is created.
57type Pool struct {
58 poolPath string
59
60 stakedLiquidity *UintTree // uint64 timestamp -> *u256.Uint(Q128)
61
62 lastUnclaimableTime int64
63 unclaimableAcc int64
64
65 rewardCache *UintTree // uint64 timestamp -> int64 gnsReward
66
67 incentives *Incentives
68
69 ticks Ticks // int32 tickId -> Tick tick
70
71 globalRewardRatioAccumulation *UintTree // uint64 timestamp -> *u256.Uint(Q128) rewardRatioAccumulation
72
73 historicalTick *UintTree // uint64 timestamp -> int32 tickId
74}
75
76// Pool Getter/Setter methods
77
78// PoolPath returns the pool path
79func (p *Pool) PoolPath() string {
80 return p.poolPath
81}
82
83// SetPoolPath sets the pool path
84func (p *Pool) SetPoolPath(poolPath string) {
85 p.poolPath = poolPath
86}
87
88// StakedLiquidity returns the staked liquidity tree
89func (p *Pool) StakedLiquidity() *UintTree {
90 return p.stakedLiquidity
91}
92
93// SetStakedLiquidity sets the staked liquidity tree
94func (p *Pool) SetStakedLiquidity(stakedLiquidity *UintTree) {
95 p.stakedLiquidity = stakedLiquidity
96}
97
98func (p *Pool) SetStakedLiquidityAt(currentTime int64, delta *u256.Uint) {
99 p.StakedLiquidity().Set(currentTime, u256.Zero().Set(delta))
100}
101
102// LastUnclaimableTime returns the last unclaimable time
103func (p *Pool) LastUnclaimableTime() int64 {
104 return p.lastUnclaimableTime
105}
106
107// SetLastUnclaimableTime sets the last unclaimable time
108func (p *Pool) SetLastUnclaimableTime(lastUnclaimableTime int64) {
109 p.lastUnclaimableTime = lastUnclaimableTime
110}
111
112// UnclaimableAcc returns the unclaimable accumulation
113func (p *Pool) UnclaimableAcc() int64 {
114 return p.unclaimableAcc
115}
116
117// SetUnclaimableAcc sets the unclaimable accumulation
118func (p *Pool) SetUnclaimableAcc(unclaimableAcc int64) {
119 p.unclaimableAcc = unclaimableAcc
120}
121
122// RewardCache returns the reward cache tree
123func (p *Pool) RewardCache() *UintTree {
124 return p.rewardCache
125}
126
127// SetRewardCache sets the reward cache tree
128func (p *Pool) SetRewardCache(rewardCache *UintTree) {
129 p.rewardCache = rewardCache
130}
131
132func (p *Pool) SetRewardCacheAt(currentTime int64, reward int64) {
133 p.RewardCache().Set(currentTime, reward)
134}
135
136// Incentives returns the incentives
137func (p *Pool) Incentives() *Incentives {
138 return p.incentives
139}
140
141// SetIncentives sets the incentives
142func (p *Pool) SetIncentives(incentives *Incentives) {
143 p.incentives = incentives
144}
145
146// Ticks returns the ticks
147func (p *Pool) Ticks() *Ticks {
148 return &p.ticks
149}
150
151// SetTicks sets the ticks
152func (p *Pool) SetTicks(ticks Ticks) {
153 p.ticks = ticks
154}
155
156// GlobalRewardRatioAccumulation returns the global reward ratio accumulation tree
157func (p *Pool) GlobalRewardRatioAccumulation() *UintTree {
158 return p.globalRewardRatioAccumulation
159}
160
161// SetGlobalRewardRatioAccumulation sets the global reward ratio accumulation tree
162func (p *Pool) SetGlobalRewardRatioAccumulation(globalRewardRatioAccumulation *UintTree) {
163 p.globalRewardRatioAccumulation = globalRewardRatioAccumulation
164}
165
166func (p *Pool) SetGlobalRewardRatioAccumulationAt(currentTime int64, acc string) {
167 p.GlobalRewardRatioAccumulation().Set(currentTime, acc)
168}
169
170// HistoricalTick returns the historical tick tree
171func (p *Pool) HistoricalTick() *UintTree {
172 return p.historicalTick
173}
174
175// SetHistoricalTick sets the historical tick tree
176func (p *Pool) SetHistoricalTick(historicalTick *UintTree) {
177 p.historicalTick = historicalTick
178}
179
180func (p *Pool) SetHistoricalTickAt(currentTime int64, tick int32) {
181 p.HistoricalTick().Set(currentTime, tick)
182}
183
184// Clone returns a deep copy of the pool.
185func (p *Pool) Clone() *Pool {
186 if p == nil {
187 return nil
188 }
189
190 return &Pool{
191 poolPath: p.poolPath,
192 stakedLiquidity: nil,
193 lastUnclaimableTime: p.lastUnclaimableTime,
194 unclaimableAcc: p.unclaimableAcc,
195 rewardCache: nil,
196 incentives: nil,
197 ticks: NewTicks(),
198 globalRewardRatioAccumulation: nil,
199 historicalTick: nil,
200 }
201}
202
203// NewPool creates a new pool with the given poolPath and currentHeight.
204func NewPool(poolPath string, currentTime int64) *Pool {
205 pool := &Pool{
206 poolPath: poolPath,
207 stakedLiquidity: NewUintTreeN(64),
208 // lastUnclaimableTime is initialized to 0, which means "tracking not started yet".
209 // When the pool receives a tier assignment (or external incentive), `cacheReward` will be called,
210 // which will automatically call `startUnclaimablePeriod` if the pool has zero liquidity.
211 // This ensures proper unclaimable period tracking from the moment rewards start emitting.
212 lastUnclaimableTime: 0,
213 unclaimableAcc: 0,
214 rewardCache: NewUintTreeN(64),
215 incentives: NewIncentives(poolPath),
216 ticks: NewTicks(),
217 globalRewardRatioAccumulation: NewUintTreeN(64),
218 historicalTick: NewUintTreeN(64),
219 }
220
221 pool.SetGlobalRewardRatioAccumulationAt(currentTime, "0")
222
223 // Initialize rewardCache to 0 to ensure `cacheReward` will trigger on first tier assignment
224 pool.SetRewardCacheAt(currentTime, int64(0))
225 pool.SetStakedLiquidityAt(currentTime, u256.Zero())
226
227 return pool
228}
229
230// Incentives represents a collection of external incentives for a specific pool.
231//
232// Fields:
233//
234// - incentives: BPTree storing ExternalIncentive objects indexed by incentiveId
235// The incentiveId serves as the key to efficiently lookup incentive details
236//
237// - targetPoolPath: String identifier for the pool this incentive collection belongs to
238// Used to associate incentives with their corresponding liquidity pool
239//
240// - unclaimablePeriods: Tree storing periods when rewards cannot be claimed
241// Maps start timestamp (key) to end timestamp (value)
242// An end timestamp of 0 indicates an ongoing unclaimable period
243// Used to track intervals when staking rewards are not claimable
244//
245// - byStartTime: Per-pool start-time index mapping an incentive's start
246// timestamp (key) to the incentive IDs that start at that timestamp
247// (value). This mirrors the lazy-discovery lookup previously served by a
248// global creation-time index, but scoped to this pool's own incentives, so
249// discovery cost is bounded by this pool's incentives instead of growing
250// with the total number of incentives system-wide.
251type Incentives struct {
252 incentives *bptree.BPTree // (incentiveId) => ExternalIncentive
253
254 targetPoolPath string // The target pool path for this incentive collection
255
256 unclaimablePeriods *UintTree // blockTimestamp -> any
257
258 byStartTime *UintTree // startTimestamp -> []incentiveId
259}
260
261// Incentives Getter/Setter methods
262
263// Incentives returns the incentives tree
264func (i *Incentives) IncentiveTrees() *bptree.BPTree {
265 return i.incentives
266}
267
268// SetIncentives sets the incentives tree
269func (i *Incentives) SetIncentives(incentives *bptree.BPTree) {
270 i.incentives = incentives
271}
272
273// TargetPoolPath returns the target pool path
274func (i *Incentives) TargetPoolPath() string {
275 return i.targetPoolPath
276}
277
278// SetTargetPoolPath sets the target pool path
279func (i *Incentives) SetTargetPoolPath(targetPoolPath string) {
280 i.targetPoolPath = targetPoolPath
281}
282
283// UnclaimablePeriods returns the unclaimable periods tree
284func (i *Incentives) UnclaimablePeriods() *UintTree {
285 return i.unclaimablePeriods
286}
287
288// SetUnclaimablePeriods sets the unclaimable periods tree
289func (i *Incentives) SetUnclaimablePeriods(unclaimablePeriods *UintTree) {
290 i.unclaimablePeriods = unclaimablePeriods
291}
292
293// Incentive returns an incentive by ID
294func (i *Incentives) Incentive(incentiveId string) (*ExternalIncentive, bool) {
295 value := i.incentives.Get(incentiveId)
296 if value == nil {
297 return nil, false
298 }
299 incentive, ok := value.(*ExternalIncentive)
300 return incentive, ok
301}
302
303// SetIncentive sets an incentive by ID
304func (i *Incentives) SetIncentive(incentiveId string, incentive *ExternalIncentive) {
305 i.incentives.Set(incentiveId, incentive)
306}
307
308func (i *Incentives) SetUnclaimablePeriod(startTimestamp int64, endTimestamp int64) {
309 i.unclaimablePeriods.Set(startTimestamp, endTimestamp)
310}
311
312func (i *Incentives) RemoveUnclaimablePeriod(startTimestamp int64) {
313 i.unclaimablePeriods.Remove(startTimestamp)
314}
315
316// IterateIncentives iterates over all incentives
317func (i *Incentives) IterateIncentives(fn func(incentiveId string, incentive *ExternalIncentive) bool) {
318 i.incentives.Iterate("", "", func(key string, value interface{}) bool {
319 if incentive, ok := value.(*ExternalIncentive); ok {
320 return fn(key, incentive)
321 }
322 return false
323 })
324}
325
326// AddIncentiveByStartTime registers an incentive ID under its start timestamp
327// in the per-pool start-time index. Multiple incentives starting at the same
328// timestamp are accumulated as a list.
329func (i *Incentives) AddIncentiveByStartTime(startTimestamp int64, incentiveId string) {
330 var incentiveIds []string
331 if value, ok := i.byStartTime.Get(startTimestamp); ok {
332 if ids, ok := value.([]string); ok {
333 incentiveIds = ids
334 }
335 }
336 incentiveIds = append(incentiveIds, incentiveId)
337 i.byStartTime.Set(startTimestamp, incentiveIds)
338}
339
340// IterateIncentiveIdsByTime iterates over the incentive IDs that start within
341// the inclusive [startTime, endTime] range, visiting only the buckets that
342// fall in the range. ReverseIterate is used because it is inclusive on both
343// ends, matching the discovery semantics previously implemented as
344// (startTimestamp >= startTime && startTimestamp <= endTime).
345func (i *Incentives) IterateIncentiveIdsByTime(startTime, endTime int64, fn func(incentiveId string) bool) {
346 i.byStartTime.ReverseIterate(startTime, endTime, func(_ int64, value any) bool {
347 incentiveIds, ok := value.([]string)
348 if !ok {
349 return false
350 }
351 for _, incentiveId := range incentiveIds {
352 if fn(incentiveId) {
353 return true
354 }
355 }
356 return false
357 })
358}
359
360func NewIncentives(targetPoolPath string) *Incentives {
361 result := &Incentives{
362 targetPoolPath: targetPoolPath,
363 unclaimablePeriods: NewUintTreeN(64),
364 incentives: bptree.NewBPTreeN(16),
365 byStartTime: NewUintTreeN(64),
366 }
367
368 // initial unclaimable period starts, as there cannot be any staked positions yet.
369 currentTimestamp := time.Now().Unix()
370 result.SetUnclaimablePeriod(currentTimestamp, int64(0))
371 return result
372}
373
374type ExternalIncentive struct {
375 incentiveId string // incentive id
376 startTimestamp int64 // start time for external reward
377 endTimestamp int64 // end time for external reward
378 createdHeight int64 // block height when the incentive was created
379 createdTimestamp int64 // timestamp when the incentive was created
380 depositGnsAmount int64 // deposited gns amount
381 targetPoolPath string // external reward target pool path
382 rewardToken string // external reward token path
383 totalRewardAmount int64 // total reward amount
384 rewardAmount int64 // to be distributed reward amount
385 rewardPerSecondX128 *u256.Uint // reward per second, scaled by 2^128 to preserve sub-second precision
386 distributedRewardAmount int64 // distributed reward amount, when un-staked and refunded
387 accumulatedPenaltyAmount int64 // accumulated warmup penalty from CollectReward
388 creator address // creator address
389
390 refunded bool // whether incentive has been refunded (includes GNS deposit and unclaimed rewards)
391
392 unclaimableSeconds int64 // accumulated seconds of unclaimable periods overlapping the incentive window
393}
394
395// ExternalIncentive Getter/Setter methods
396
397// IncentiveId returns the incentive ID
398func (e *ExternalIncentive) IncentiveId() string {
399 return e.incentiveId
400}
401
402// SetIncentiveId sets the incentive ID
403func (e *ExternalIncentive) SetIncentiveId(incentiveId string) {
404 e.incentiveId = incentiveId
405}
406
407// StartTimestamp returns the start timestamp.
408//
409// It keys the byStartTime discovery index and must stay immutable after the
410// incentive is registered, so no setter is exposed.
411func (e *ExternalIncentive) StartTimestamp() int64 {
412 return e.startTimestamp
413}
414
415// EndTimestamp returns the end timestamp
416func (e *ExternalIncentive) EndTimestamp() int64 {
417 return e.endTimestamp
418}
419
420// SetEndTimestamp sets the end timestamp
421func (e *ExternalIncentive) SetEndTimestamp(endTimestamp int64) {
422 e.endTimestamp = endTimestamp
423}
424
425// CreatedHeight returns the created height
426func (e *ExternalIncentive) CreatedHeight() int64 {
427 return e.createdHeight
428}
429
430// SetCreatedHeight sets the created height
431func (e *ExternalIncentive) SetCreatedHeight(createdHeight int64) {
432 e.createdHeight = createdHeight
433}
434
435// CreatedTimestamp returns the created timestamp
436func (e *ExternalIncentive) CreatedTimestamp() int64 {
437 return e.createdTimestamp
438}
439
440// SetCreatedTimestamp sets the created timestamp
441func (e *ExternalIncentive) SetCreatedTimestamp(createdTimestamp int64) {
442 e.createdTimestamp = createdTimestamp
443}
444
445// DepositGnsAmount returns the deposit GNS amount
446func (e *ExternalIncentive) DepositGnsAmount() int64 {
447 return e.depositGnsAmount
448}
449
450// SetDepositGnsAmount sets the deposit GNS amount
451func (e *ExternalIncentive) SetDepositGnsAmount(depositGnsAmount int64) {
452 e.depositGnsAmount = depositGnsAmount
453}
454
455// TargetPoolPath returns the target pool path
456func (e *ExternalIncentive) TargetPoolPath() string {
457 return e.targetPoolPath
458}
459
460// SetTargetPoolPath sets the target pool path
461func (e *ExternalIncentive) SetTargetPoolPath(targetPoolPath string) {
462 e.targetPoolPath = targetPoolPath
463}
464
465// RewardToken returns the reward token
466func (e *ExternalIncentive) RewardToken() string {
467 return e.rewardToken
468}
469
470// SetRewardToken sets the reward token
471func (e *ExternalIncentive) SetRewardToken(rewardToken string) {
472 e.rewardToken = rewardToken
473}
474
475// TotalRewardAmount returns the total reward amount
476func (e *ExternalIncentive) TotalRewardAmount() int64 {
477 return e.totalRewardAmount
478}
479
480// SetTotalRewardAmount sets the total reward amount
481func (e *ExternalIncentive) SetTotalRewardAmount(totalRewardAmount int64) {
482 e.totalRewardAmount = totalRewardAmount
483}
484
485// RewardAmount returns the reward amount
486func (e *ExternalIncentive) RewardAmount() int64 {
487 return e.rewardAmount
488}
489
490// SetRewardAmount sets the reward amount
491func (e *ExternalIncentive) SetRewardAmount(rewardAmount int64) {
492 e.rewardAmount = rewardAmount
493}
494
495// RewardPerSecondX128 returns the Q128-scaled reward per second.
496// The underlying value is (rewardAmount << 128) / duration.
497func (e *ExternalIncentive) RewardPerSecondX128() *u256.Uint {
498 return e.rewardPerSecondX128
499}
500
501// SetRewardPerSecondX128 sets the Q128-scaled reward per second.
502func (e *ExternalIncentive) SetRewardPerSecondX128(rewardPerSecondX128 *u256.Uint) {
503 e.rewardPerSecondX128 = u256.Zero().Set(rewardPerSecondX128)
504}
505
506// DistributedRewardAmount returns the distributed reward amount
507func (e *ExternalIncentive) DistributedRewardAmount() int64 {
508 return e.distributedRewardAmount
509}
510
511// SetDistributedRewardAmount sets the distributed reward amount
512func (e *ExternalIncentive) SetDistributedRewardAmount(distributedRewardAmount int64) {
513 e.distributedRewardAmount = distributedRewardAmount
514}
515
516// AccumulatedPenaltyAmount returns the accumulated warmup penalty amount
517func (e *ExternalIncentive) AccumulatedPenaltyAmount() int64 {
518 return e.accumulatedPenaltyAmount
519}
520
521// SetAccumulatedPenaltyAmount sets the accumulated warmup penalty amount
522func (e *ExternalIncentive) SetAccumulatedPenaltyAmount(accumulatedPenaltyAmount int64) {
523 e.accumulatedPenaltyAmount = accumulatedPenaltyAmount
524}
525
526// Creator returns the creator address
527func (e *ExternalIncentive) Creator() address {
528 return e.creator
529}
530
531// SetCreator sets the creator address
532func (e *ExternalIncentive) SetCreator(creator address) {
533 e.creator = creator
534}
535
536// Refunded returns the refunded status
537func (e *ExternalIncentive) Refunded() bool {
538 return e.refunded
539}
540
541// SetRefunded sets the refunded status
542func (e *ExternalIncentive) SetRefunded(refunded bool) {
543 e.refunded = refunded
544}
545
546// UnclaimableSeconds returns the accumulated seconds of unclaimable periods
547// that overlap the incentive window. It is updated whenever an unclaimable
548// period closes and is backfilled once from the historical unclaimable
549// periods tree after an upgrade.
550func (e *ExternalIncentive) UnclaimableSeconds() int64 {
551 return e.unclaimableSeconds
552}
553
554// SetUnclaimableSeconds sets the accumulated unclaimable seconds.
555func (e *ExternalIncentive) SetUnclaimableSeconds(unclaimableSeconds int64) {
556 e.unclaimableSeconds = unclaimableSeconds
557}
558
559func (e *ExternalIncentive) Clone() *ExternalIncentive {
560 rewardPerSecondX128 := u256.Zero()
561
562 if e.rewardPerSecondX128 != nil {
563 rewardPerSecondX128 = e.rewardPerSecondX128.Clone()
564 }
565
566 return &ExternalIncentive{
567 incentiveId: e.incentiveId,
568 startTimestamp: e.startTimestamp,
569 endTimestamp: e.endTimestamp,
570 createdHeight: e.createdHeight,
571 createdTimestamp: e.createdTimestamp,
572 depositGnsAmount: e.depositGnsAmount,
573 targetPoolPath: e.targetPoolPath,
574 rewardToken: e.rewardToken,
575 totalRewardAmount: e.totalRewardAmount,
576 rewardAmount: e.rewardAmount,
577 rewardPerSecondX128: rewardPerSecondX128,
578 creator: e.creator,
579 refunded: e.refunded,
580 unclaimableSeconds: e.unclaimableSeconds,
581 distributedRewardAmount: e.distributedRewardAmount,
582 accumulatedPenaltyAmount: e.accumulatedPenaltyAmount,
583 }
584}
585
586// NewExternalIncentive creates a new external incentive
587func NewExternalIncentive(
588 incentiveId string,
589 targetPoolPath string,
590 rewardToken string,
591 rewardAmount int64,
592 startTimestamp int64, // timestamp is in unix time(seconds)
593 endTimestamp int64,
594 creator address,
595 depositGnsAmount int64,
596 createdHeight int64,
597 currentTime int64, // current time in unix time(seconds)
598) *ExternalIncentive {
599 incentiveDuration := endTimestamp - startTimestamp
600
601 // Compute reward per second scaled by 2^128 to preserve sub-second precision.
602 // rewardPerSecondX128 = (rewardAmount << 128) / incentiveDuration.
603 // Consumers must divide by 2^128 when materializing back to a plain integer.
604 rewardPerSecondX128 := u256.MulDiv(
605 u256.NewUintFromInt64(rewardAmount),
606 consts.Q128(),
607 u256.NewUintFromInt64(incentiveDuration),
608 )
609
610 return &ExternalIncentive{
611 incentiveId: incentiveId,
612 targetPoolPath: targetPoolPath,
613 rewardToken: rewardToken,
614 totalRewardAmount: rewardAmount,
615 rewardAmount: rewardAmount,
616 startTimestamp: startTimestamp,
617 endTimestamp: endTimestamp,
618 rewardPerSecondX128: rewardPerSecondX128,
619 distributedRewardAmount: 0,
620 accumulatedPenaltyAmount: 0,
621 creator: creator,
622 createdHeight: createdHeight,
623 createdTimestamp: currentTime,
624 depositGnsAmount: depositGnsAmount,
625 refunded: false,
626 unclaimableSeconds: 0,
627 }
628}
629
630// Tick mapping for each pool
631type Ticks struct {
632 tree *bptree.BPTree // int32 tickId -> tick
633}
634
635// Ticks Getter/Setter methods
636
637// Tree returns the ticks tree
638func (t *Ticks) Tree() *bptree.BPTree {
639 return t.tree
640}
641
642// SetTree sets the ticks tree
643func (t *Ticks) SetTree(tree *bptree.BPTree) {
644 t.tree = tree
645}
646
647// Get returns the tick for the given tickId, or nil if it does not exist.
648func (t *Ticks) Get(tickId int32) *Tick {
649 v := t.tree.Get(EncodeInt(tickId))
650 if v == nil {
651 return nil
652 }
653
654 tick, ok := v.(*Tick)
655 if !ok {
656 panic("failed to cast value to *Tick")
657 }
658 return tick
659}
660
661func (self *Ticks) Has(tickId int32) bool {
662 return self.tree.Has(EncodeInt(tickId))
663}
664
665// SetTick sets a tick by ID
666func (t *Ticks) SetTick(tickId int32, tick *Tick) {
667 if tick.stakedLiquidityGross.IsZero() {
668 t.tree.Remove(EncodeInt(tickId))
669 return
670 }
671
672 t.tree.Set(EncodeInt(tickId), tick)
673}
674
675// IterateTicks iterates over all ticks
676func (t *Ticks) IterateTicks(fn func(tickId int32, tick *Tick) bool) {
677 t.tree.Iterate("", "", func(key string, value interface{}) bool {
678 tick, ok := value.(*Tick)
679 if !ok {
680 return false
681 }
682
683 // Convert string key back to int32
684 tickId, err := strconv.Atoi(key)
685 if err != nil {
686 return false // skip invalid keys
687 }
688
689 return fn(int32(tickId), tick)
690 })
691}
692
693// Clone returns a deep copy of ticks.
694func (t Ticks) Clone() Ticks {
695 cloned := bptree.NewBPTreeN(16)
696 t.tree.Iterate("", "", func(key string, value any) bool {
697 tick, ok := value.(*Tick)
698 if !ok {
699 panic("failed to cast value to *Tick")
700 }
701 cloned.Set(key, tick.Clone())
702 return false
703 })
704 return Ticks{tree: cloned}
705}
706
707func NewTicks() Ticks {
708 return Ticks{
709 tree: bptree.NewBPTreeN(16),
710 }
711}
712
713// Tick represents the state of a specific tick in a pool.
714//
715// Fields:
716// - id (int32): The ID of the tick.
717// - stakedLiquidityGross (*u256.Uint): Total gross staked liquidity at this tick.
718// - stakedLiquidityDelta (*i256.Int): Net change in staked liquidity at this tick.
719// - outsideAccumulation (*UintTree): RewardRatioAccumulation outside the tick.
720type Tick struct {
721 id int32
722
723 // conceptually equal with Pool.liquidityGross but only for the staked positions
724 stakedLiquidityGross *u256.Uint
725
726 // conceptually equal with Pool.liquidityNet but only for the staked positions
727 stakedLiquidityDelta *i256.Int
728
729 // currentOutsideAccumulation is the accumulation of the time / TotalStake outside the tick.
730 // It is calculated by subtracting the current tick's currentOutsideAccumulation from the global reward ratio accumulation.
731 outsideAccumulation *UintTree // timestamp -> *u256.Uint
732}
733
734// Tick Getter/Setter methods
735
736// Id returns the tick ID
737func (t *Tick) Id() int32 {
738 return t.id
739}
740
741// SetId sets the tick ID
742func (t *Tick) SetId(id int32) {
743 t.id = id
744}
745
746// StakedLiquidityGross returns the staked liquidity gross
747func (t *Tick) StakedLiquidityGross() *u256.Uint {
748 return t.stakedLiquidityGross
749}
750
751// SetStakedLiquidityGross sets the staked liquidity gross
752func (t *Tick) SetStakedLiquidityGross(stakedLiquidityGross *u256.Uint) {
753 t.stakedLiquidityGross = u256.Zero().Set(stakedLiquidityGross)
754}
755
756// StakedLiquidityDelta returns the staked liquidity delta
757func (t *Tick) StakedLiquidityDelta() *i256.Int {
758 return t.stakedLiquidityDelta
759}
760
761// SetStakedLiquidityDelta sets the staked liquidity delta
762func (t *Tick) SetStakedLiquidityDelta(stakedLiquidityDelta *i256.Int) {
763 t.stakedLiquidityDelta = i256.Zero().Set(stakedLiquidityDelta)
764}
765
766// OutsideAccumulation returns the outside accumulation tree
767func (t *Tick) OutsideAccumulation() *UintTree {
768 return t.outsideAccumulation
769}
770
771// SetOutsideAccumulation sets the outside accumulation tree
772func (t *Tick) SetOutsideAccumulation(outsideAccumulation *UintTree) {
773 t.outsideAccumulation = outsideAccumulation
774}
775
776// SetOutsideAccumulationAt sets the outside accumulation at the timestamp.
777func (t *Tick) SetOutsideAccumulationAt(timestamp int64, acc *u256.Uint) {
778 t.outsideAccumulation.Set(timestamp, u256.Zero().Set(acc))
779}
780
781// Clone returns a deep copy of the tick.
782func (t *Tick) Clone() *Tick {
783 if t == nil {
784 return nil
785 }
786
787 return &Tick{
788 id: t.id,
789 stakedLiquidityGross: t.stakedLiquidityGross.Clone(),
790 stakedLiquidityDelta: t.stakedLiquidityDelta.Clone(),
791 outsideAccumulation: t.outsideAccumulation.Clone(),
792 }
793}
794
795func NewTick(tickId int32) *Tick {
796 return &Tick{
797 id: tickId,
798 stakedLiquidityGross: u256.Zero(),
799 stakedLiquidityDelta: i256.Zero(),
800 outsideAccumulation: NewUintTreeN(64),
801 }
802}
803
804// 100%, 0%, 0% if no tier2 and tier3
805// 80%, 0%, 20% if no tier2
806// 70%, 30%, 0% if no tier3
807// 50%, 30%, 20% if has tier2 and tier3
808type TierRatio struct {
809 Tier1 uint64
810 Tier2 uint64
811 Tier3 uint64
812}
813
814func NewTierRatio(tier1, tier2, tier3 uint64) TierRatio {
815 return TierRatio{
816 Tier1: tier1,
817 Tier2: tier2,
818 Tier3: tier3,
819 }
820}
821
822// Get returns the ratio(scaled up by 100) for the given tier.
823func (ratio *TierRatio) Get(tier uint64) (uint64, error) {
824 switch tier {
825 case 1:
826 return ratio.Tier1, nil
827 case 2:
828 return ratio.Tier2, nil
829 case 3:
830 return ratio.Tier3, nil
831 default:
832 return 0, errors.New(ufmt.Sprintf("unsupported tier(%d)", tier))
833 }
834}
835
836// SwapBatchProcessor processes tick crosses in batch for a swap
837// This processor accumulates all tick crosses that occur during a single swap
838// and processes them together at the end, reducing redundant calculations
839// and state updates that would occur with individual tick processing
840type SwapBatchProcessor struct {
841 poolPath string // The pool path identifier for this swap
842 pool *Pool // Reference to the pool being swapped in
843 crosses []*SwapTickCross // Accumulated tick crosses during the swap
844 timestamp int64 // Timestamp when the swap started
845 isActive bool // Flag to prevent accumulation after swap ends
846}
847
848func (s *SwapBatchProcessor) PoolPath() string {
849 return s.poolPath
850}
851
852func (s *SwapBatchProcessor) SetPoolPath(poolPath string) {
853 s.poolPath = poolPath
854}
855
856func (s *SwapBatchProcessor) Pool() *Pool {
857 return s.pool
858}
859
860func (s *SwapBatchProcessor) SetPool(pool *Pool) {
861 s.pool = pool
862}
863
864func (s *SwapBatchProcessor) Crosses() []*SwapTickCross {
865 return s.crosses
866}
867
868func (s *SwapBatchProcessor) SetCrosses(crosses []*SwapTickCross) {
869 s.crosses = crosses
870}
871
872func (s *SwapBatchProcessor) Timestamp() int64 {
873 return s.timestamp
874}
875
876func (s *SwapBatchProcessor) SetTimestamp(timestamp int64) {
877 s.timestamp = timestamp
878}
879
880func (s *SwapBatchProcessor) IsActive() bool {
881 return s.isActive
882}
883
884func (s *SwapBatchProcessor) SetIsActive(isActive bool) {
885 s.isActive = isActive
886}
887
888func (s *SwapBatchProcessor) LastCross() *SwapTickCross {
889 if len(s.crosses) == 0 {
890 return nil
891 }
892
893 return s.crosses[len(s.crosses)-1]
894}
895
896func (s *SwapBatchProcessor) AddCross(tickCross *SwapTickCross) {
897 s.crosses = append(s.crosses, tickCross)
898}
899
900func NewSwapBatchProcessor(poolPath string, pool *Pool, timestamp int64) *SwapBatchProcessor {
901 return &SwapBatchProcessor{
902 poolPath: poolPath,
903 pool: pool,
904 crosses: make([]*SwapTickCross, 0),
905 timestamp: timestamp,
906 isActive: true,
907 }
908}
909
910// SwapTickCross stores information about a tick cross during a swap
911// This struct is used to accumulate tick cross events during a single swap transaction
912// for batch processing to optimize gas usage and computational efficiency
913type SwapTickCross struct {
914 tickID int32 // The tick index that was crossed
915 zeroForOne bool // Direction of the swap (true: token0->token1, false: token1->token0)
916 delta *i256.Int // Pre-calculated liquidity delta for this tick cross
917}
918
919func (s *SwapTickCross) TickID() int32 {
920 return s.tickID
921}
922
923func (s *SwapTickCross) ZeroForOne() bool {
924 return s.zeroForOne
925}
926
927func (s *SwapTickCross) Delta() *i256.Int {
928 return s.delta
929}
930
931func NewSwapTickCross(tickID int32, zeroForOne bool, delta *i256.Int) *SwapTickCross {
932 return &SwapTickCross{
933 tickID: tickID,
934 zeroForOne: zeroForOne,
935 delta: delta,
936 }
937}