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

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}