swap.gno
24.15 Kb · 785 lines
1package pool
2
3import (
4 "chain"
5 "errors"
6 "strconv"
7 "time"
8
9 ufmt "gno.land/p/nt/ufmt/v0"
10 "gno.land/r/gnoswap/access"
11 "gno.land/r/gnoswap/halt"
12
13 "gno.land/p/gnoswap/consts"
14 "gno.land/p/gnoswap/gnsmath"
15 i256 "gno.land/p/gnoswap/int256"
16 u256 "gno.land/p/gnoswap/uint256"
17 "gno.land/p/gnoswap/utils"
18
19 pl "gno.land/r/gnoswap/pool"
20)
21
22// Hook functions allow external contracts to be notified of swap events.
23var (
24 // MUST BE IMMUTABLE.
25 // DO NOT USE THIS VALUE IN ANY ARITHMETIC OPERATIONS' INITIALIZATION
26 zero = u256.Zero()
27 zeroI256 = i256.Zero() /* readonly */
28 fixedPointQ128 = u256.MustFromDecimal(Q128)
29
30 maxInt256 = u256.MustFromDecimal(MAX_INT256)
31 maxInt64 = i256.Zero().SetInt64(INT64_MAX)
32 minInt64 = i256.Zero().SetInt64(INT64_MIN)
33)
34
35// SetTickCrossHook sets the hook function called when a tick is crossed during swaps.
36//
37// Allows staker to monitor liquidity changes at price levels.
38// Used for reward calculation when positions enter/exit range.
39//
40// Only callable by staker contract.
41func (i *poolV1) SetTickCrossHook(_ int, rlm realm, hook func(cur realm, poolPath string, tickId int32, zeroForOne bool, timestamp int64)) {
42 if !rlm.IsCurrent() {
43 panic(errors.New(errSpoofedRealm))
44 }
45
46 i.assertPoolUnlocked()
47 halt.AssertIsNotHaltedPool()
48
49 caller := rlm.Previous().Address()
50 access.AssertIsStaker(caller)
51
52 i.lockPool(0, rlm)
53 defer i.unlockPool(0, rlm)
54
55 err := i.store.SetTickCrossHook(0, rlm, hook)
56 if err != nil {
57 panic(err)
58 }
59}
60
61// SetSwapStartHook sets the hook function called at the beginning of a swap.
62//
63// Enables pre-swap state tracking for reward distribution.
64// Captures timestamp for time-weighted calculations.
65//
66// Only callable by staker contract.
67func (i *poolV1) SetSwapStartHook(_ int, rlm realm, hook func(cur realm, poolPath string, timestamp int64)) {
68 if !rlm.IsCurrent() {
69 panic(errors.New(errSpoofedRealm))
70 }
71
72 i.assertPoolUnlocked()
73 halt.AssertIsNotHaltedPool()
74
75 caller := rlm.Previous().Address()
76 access.AssertIsStaker(caller)
77
78 i.lockPool(0, rlm)
79 defer i.unlockPool(0, rlm)
80
81 err := i.store.SetSwapStartHook(0, rlm, hook)
82 if err != nil {
83 panic(err)
84 }
85}
86
87// SetSwapEndHook sets the hook function called at the end of a swap.
88//
89// Finalizes reward calculations after swap completion.
90// Allows error propagation to revert invalid swaps.
91//
92// Only callable by staker contract.
93func (i *poolV1) SetSwapEndHook(_ int, rlm realm, hook func(cur realm, poolPath string) error) {
94 if !rlm.IsCurrent() {
95 panic(errors.New(errSpoofedRealm))
96 }
97
98 i.assertPoolUnlocked()
99 halt.AssertIsNotHaltedPool()
100
101 caller := rlm.Previous().Address()
102 access.AssertIsStaker(caller)
103
104 i.lockPool(0, rlm)
105 defer i.unlockPool(0, rlm)
106
107 err := i.store.SetSwapEndHook(0, rlm, hook)
108 if err != nil {
109 panic(err)
110 }
111}
112
113// SwapResult encapsulates all state changes from a swap.
114// It ensures atomic state transitions that can be applied at once.
115type SwapResult struct {
116 Amount0 *i256.Int
117 Amount1 *i256.Int
118 NewSqrtPrice *u256.Uint
119 NewTick int32
120 NewLiquidity *u256.Uint
121 NewProtocolFeeToken0 int64
122 NewProtocolFeeToken1 int64
123 FeeGrowthGlobal0X128 *u256.Uint
124 FeeGrowthGlobal1X128 *u256.Uint
125}
126
127// SwapComputation encapsulates the pure computation logic for swaps.
128type SwapComputation struct {
129 AmountSpecified *i256.Int
130 SqrtPriceLimitX96 *u256.Uint
131 ZeroForOne bool
132 ExactInput bool
133 InitialState SwapState
134 Cache *SwapCache
135}
136
137// Swap executes a swap with callback pattern for optimistic transfers.
138// This allows flash swaps where tokens are sent before payment is received.
139//
140// The flow is:
141// 1. Pool sends output tokens to recipient
142// 2. Pool calls callback on msg.sender
143// 3. Callback must ensure pool receives input tokens
144// 4. Pool validates its balance increased correctly
145//
146// Parameters:
147// - token0Path: Path of token0 in the pool
148// - token1Path: Path of token1 in the pool
149// - fee: Pool fee tier
150// - recipient: Address to receive output tokens
151// - zeroForOne: Direction of swap (true = token0 to token1)
152// - amountSpecified: Exact input (positive) or exact output (negative)
153// - sqrtPriceLimitX96: Price limit for the swap
154// - swapCallback: Callback function to handle token transfers
155//
156// Returns amount0 and amount1 deltas as strings.
157func (i *poolV1) Swap(
158 _ int,
159 rlm realm,
160 token0Path string,
161 token1Path string,
162 fee uint32,
163 recipient address,
164 zeroForOne bool,
165 amountSpecified string,
166 sqrtPriceLimitX96 string,
167 swapCallback func(cur realm, amount0Delta, amount1Delta int64, _ *pl.CallbackMarker) error,
168) (string, string) {
169 if !rlm.IsCurrent() {
170 panic(errors.New(errSpoofedRealm))
171 }
172
173 i.assertPoolUnlocked()
174 halt.AssertIsNotHaltedPool()
175
176 assertIsNotUserCall(0, rlm)
177 assertIsValidTokenOrder(token0Path, token1Path)
178
179 amounts := i256.MustFromDecimal(amountSpecified)
180 if amounts.IsZero() {
181 panic(newErrorWithDetail(
182 errInvalidSwapAmount,
183 "amountSpecified == 0",
184 ))
185 }
186
187 pool := i.mustGetPoolBy(token0Path, token1Path, fee)
188
189 slot0Start := pool.Slot0()
190 i.lockPool(0, rlm)
191 defer i.unlockPool(0, rlm)
192
193 // no liquidity -> no swap, return zero amounts
194 if pool.Liquidity().IsZero() {
195 return "0", "0"
196 }
197
198 blockTimestamp := time.Now().Unix()
199
200 // Call swap start hook if set
201 if i.store.HasSwapStartHook() {
202 swapStartHook := i.store.GetSwapStartHook()
203
204 if swapStartHook != nil {
205 swapStartHook(cross(rlm), pool.PoolPath(), blockTimestamp)
206 }
207 }
208
209 defer func() {
210 if i.store.HasSwapEndHook() {
211 swapEndHook := i.store.GetSwapEndHook()
212
213 if swapEndHook != nil {
214 err := swapEndHook(cross(rlm), pool.PoolPath())
215 if err != nil {
216 panic(err)
217 }
218 }
219 }
220 }()
221
222 sqrtPriceLimit := u256.MustFromDecimal(sqrtPriceLimitX96)
223 validatePriceLimits(slot0Start, zeroForOne, sqrtPriceLimit)
224
225 feeGrowthGlobalX128 := getFeeGrowthGlobal(pool, zeroForOne)
226 feeProtocol := getFeeProtocol(slot0Start, zeroForOne)
227 cache := newSwapCache(feeProtocol, pool.Liquidity().Clone(), blockTimestamp)
228 state := newSwapState(amounts, feeGrowthGlobalX128, cache.liquidityStart.Clone(), slot0Start)
229
230 comp := SwapComputation{
231 AmountSpecified: amounts,
232 SqrtPriceLimitX96: sqrtPriceLimit,
233 ZeroForOne: zeroForOne,
234 ExactInput: amounts.Gt(zeroI256),
235 InitialState: state,
236 Cache: cache,
237 }
238
239 var hook func(cur realm, poolPath string, tickId int32, zeroForOne bool, timestamp int64)
240 if i.store.HasTickCrossHook() {
241 hook = i.store.GetTickCrossHook()
242 }
243 onTickCross := func(pool *pl.Pool, tickId int32, zeroForOne bool, timestamp int64) {
244 chain.Emit(
245 "PoolTickCross",
246 "poolPath", pool.PoolPath(),
247 "tick", NewTickEventInfo(tickId, *mustGetTick(pool, tickId)).ToString(),
248 )
249 if hook != nil {
250 hook(cross(rlm), pool.PoolPath(), tickId, zeroForOne, timestamp)
251 }
252 }
253
254 result, err := i.computeSwap(pool, comp, onTickCross)
255 if err != nil {
256 panic(err)
257 }
258
259 // Update oracle BEFORE applying swap result (using pre-swap state)
260 if result.NewTick != pool.Slot0Tick() {
261 err := writeObservationByPool(pool, cache.blockTimestamp, pool.Slot0Tick(), pool.Liquidity())
262 if err != nil {
263 panic(err)
264 }
265 }
266
267 applySwapResult(pool, result)
268
269 // transfer swap result to recipient then receive input tokens from swap callback
270 if zeroForOne {
271 // receive token0 from swap callback
272 // send token1 to recipient (output)
273 if result.Amount1.IsNeg() {
274 i.safeTransfer(0, rlm, pool, recipient, token1Path, result.Amount1.Abs(), false)
275 }
276 i.safeSwapCallback(0, rlm, pool, token0Path, result.Amount0, result.Amount1, zeroForOne, swapCallback)
277 } else {
278 // receive token1 from swap callback
279 // send token0 to recipient (output)
280 if result.Amount0.IsNeg() {
281 i.safeTransfer(0, rlm, pool, recipient, token0Path, result.Amount0.Abs(), true)
282 }
283 i.safeSwapCallback(0, rlm, pool, token1Path, result.Amount1, result.Amount0, zeroForOne, swapCallback)
284 }
285
286 lastObservation, err := lastObservation(pool.ObservationState())
287 if err != nil {
288 panic(err)
289 }
290
291 token0Amount := result.Amount0.ToString()
292 token1Amount := result.Amount1.ToString()
293
294 previousRealm := rlm.Previous()
295
296 chain.Emit(
297 "Swap",
298 "prevAddr", previousRealm.Address().String(),
299 "prevRealm", previousRealm.PkgPath(),
300 "poolPath", pool.PoolPath(),
301 "zeroForOne", utils.FormatBool(zeroForOne),
302 "requestAmount", amountSpecified,
303 "sqrtPriceLimitX96", sqrtPriceLimitX96,
304 "recipient", recipient.String(),
305 "token0Amount", token0Amount,
306 "token1Amount", token1Amount,
307 "protocolFee0", utils.FormatInt(pool.ProtocolFeesToken0()),
308 "protocolFee1", utils.FormatInt(pool.ProtocolFeesToken1()),
309 "sqrtPriceX96", pool.Slot0SqrtPriceX96().ToString(),
310 "exactIn", strconv.FormatBool(comp.ExactInput),
311 "currentTick", strconv.FormatInt(int64(pool.Slot0Tick()), 10),
312 "liquidity", pool.Liquidity().ToString(),
313 "feeGrowthGlobal0X128", pool.FeeGrowthGlobal0X128().ToString(),
314 "feeGrowthGlobal1X128", pool.FeeGrowthGlobal1X128().ToString(),
315 "balanceToken0", utils.FormatInt(pool.BalanceToken0()),
316 "balanceToken1", utils.FormatInt(pool.BalanceToken1()),
317 "tickCumulative", utils.FormatInt(lastObservation.TickCumulative()),
318 "secondsPerLiquidityCumulativeX128", lastObservation.SecondsPerLiquidityCumulativeX128(),
319 "observationTimestamp", utils.FormatInt(lastObservation.BlockTimestamp()),
320 )
321
322 return token0Amount, token1Amount
323}
324
325// DrySwap simulates a swap without modifying pool state.
326// Returns amount0, amount1 and a success boolean.
327// Returns false if pool has no liquidity or computation fails.
328func (i *poolV1) DrySwap(
329 token0Path string,
330 token1Path string,
331 fee uint32,
332 zeroForOne bool,
333 amountSpecified string,
334 sqrtPriceLimitX96 string,
335) (string, string, bool) {
336 amounts := i256.MustFromDecimal(amountSpecified)
337 if amounts.IsZero() {
338 return "0", "0", false
339 }
340
341 pool := i.mustGetPoolBy(token0Path, token1Path, fee)
342 poolSnapshot := pool.Clone()
343
344 // no liquidity -> simulation fails
345 if poolSnapshot.Liquidity().IsZero() {
346 return "0", "0", false
347 }
348
349 slot0Start := poolSnapshot.Slot0()
350 sqrtPriceLimit := u256.MustFromDecimal(sqrtPriceLimitX96)
351 validatePriceLimits(slot0Start, zeroForOne, sqrtPriceLimit)
352
353 feeGrowthGlobalX128 := getFeeGrowthGlobal(poolSnapshot, zeroForOne)
354 feeProtocol := getFeeProtocol(slot0Start, zeroForOne)
355 cache := newSwapCache(feeProtocol, poolSnapshot.Liquidity().Clone(), time.Now().Unix())
356 state := newSwapState(amounts, feeGrowthGlobalX128, cache.liquidityStart, slot0Start)
357
358 comp := SwapComputation{
359 AmountSpecified: amounts,
360 SqrtPriceLimitX96: sqrtPriceLimit,
361 ZeroForOne: zeroForOne,
362 ExactInput: amounts.Gt(zeroI256),
363 InitialState: state,
364 Cache: cache,
365 }
366
367 result, err := i.computeSwap(poolSnapshot, comp, nil)
368 if err != nil {
369 return "0", "0", false
370 }
371
372 if zeroForOne {
373 if poolSnapshot.BalanceToken1() < gnsmath.SafeConvertToInt64(result.Amount1.Abs()) {
374 return "0", "0", false
375 }
376 } else {
377 if poolSnapshot.BalanceToken0() < gnsmath.SafeConvertToInt64(result.Amount0.Abs()) {
378 return "0", "0", false
379 }
380 }
381
382 return result.Amount0.ToString(), result.Amount1.ToString(), true
383}
384
385// tickCrossHookFn is invoked after an initialized tick is crossed. Swap uses it
386// for externally visible side effects; DrySwap passes nil.
387type tickCrossHookFn func(pool *pl.Pool, tickId int32, zeroForOne bool, timestamp int64)
388
389// computeSwap performs the core swap computation without modifying pool state.
390// The computation continues until either:
391// - The entire amount is consumed (amountSpecifiedRemaining = 0)
392// - The price limit is reached (sqrtPriceX96 = sqrtPriceLimitX96)
393//
394// Important: This function is critical for AMM price discovery. It iterates through
395// tick ranges, calculating swap amounts and fees for each liquidity segment.
396// Returns an error if the computation fails at any step.
397//
398// The optional `onTickCross` callback is invoked when an initialized tick is
399// crossed; Swap supplies a hook that performs a cross-realm call into the
400// configured tick-cross hook, while DrySwap passes nil.
401func (i *poolV1) computeSwap(pool *pl.Pool, comp SwapComputation, onTickCross tickCrossHookFn) (*SwapResult, error) {
402 state := comp.InitialState
403 var err error
404
405 // Compute swap steps until completion
406 for shouldContinueSwap(state, comp.SqrtPriceLimitX96) {
407 state, err = i.computeSwapStep(state, pool, comp.ZeroForOne, comp.SqrtPriceLimitX96, comp.ExactInput, comp.Cache, onTickCross)
408 if err != nil {
409 return nil, err
410 }
411 }
412
413 // Calculate final amounts
414 amount0 := state.amountCalculated
415 amount1 := i256.Zero().Sub(comp.AmountSpecified, state.amountSpecifiedRemaining)
416 if comp.ZeroForOne == comp.ExactInput {
417 amount0, amount1 = amount1, amount0
418 }
419
420 // Prepare result
421 result := &SwapResult{
422 Amount0: amount0,
423 Amount1: amount1,
424 NewSqrtPrice: state.sqrtPriceX96,
425 NewTick: state.tick,
426 NewLiquidity: state.liquidity,
427 NewProtocolFeeToken0: pool.ProtocolFeesToken0(),
428 NewProtocolFeeToken1: pool.ProtocolFeesToken1(),
429 FeeGrowthGlobal0X128: pool.FeeGrowthGlobal0X128(),
430 FeeGrowthGlobal1X128: pool.FeeGrowthGlobal1X128(),
431 }
432
433 // Update protocol fees if necessary
434 if comp.ZeroForOne {
435 if state.protocolFee.Gt(zero) {
436 result.NewProtocolFeeToken0 = gnsmath.SafeAddInt64(result.NewProtocolFeeToken0, gnsmath.SafeConvertToInt64(state.protocolFee))
437 }
438 result.FeeGrowthGlobal0X128 = state.feeGrowthGlobalX128.Clone()
439 } else {
440 if state.protocolFee.Gt(zero) {
441 result.NewProtocolFeeToken1 = gnsmath.SafeAddInt64(result.NewProtocolFeeToken1, gnsmath.SafeConvertToInt64(state.protocolFee))
442 }
443 result.FeeGrowthGlobal1X128 = state.feeGrowthGlobalX128.Clone()
444 }
445
446 return result, nil
447}
448
449// applySwapResult updates pool state with computed results.
450// All state changes are applied at once to maintain consistency
451func applySwapResult(pool *pl.Pool, result *SwapResult) {
452 slot0 := pool.Slot0()
453 slot0.SetSqrtPriceX96(result.NewSqrtPrice)
454 slot0.SetTick(result.NewTick)
455 pool.SetSlot0(slot0)
456
457 pool.SetLiquidity(result.NewLiquidity)
458 pool.SetProtocolFeesToken0(result.NewProtocolFeeToken0)
459 pool.SetProtocolFeesToken1(result.NewProtocolFeeToken1)
460 pool.SetFeeGrowthGlobal0X128(result.FeeGrowthGlobal0X128)
461 pool.SetFeeGrowthGlobal1X128(result.FeeGrowthGlobal1X128)
462}
463
464// validatePriceLimits ensures the provided price limit is valid for the swap direction
465// The function enforces that:
466// For zeroForOne (selling token0):
467// - Price limit must be below current price
468// - Price limit must be above MIN_SQRT_RATIO
469//
470// For !zeroForOne (selling token1):
471// - Price limit must be above current price
472// - Price limit must be below MAX_SQRT_RATIO
473func validatePriceLimits(slot0 pl.Slot0, zeroForOne bool, sqrtPriceLimitX96 *u256.Uint) {
474 if zeroForOne {
475 cond1 := sqrtPriceLimitX96.Lt(slot0.SqrtPriceX96())
476 cond2 := sqrtPriceLimitX96.Gt(consts.MinSqrtRatio())
477 if !(cond1 && cond2) {
478 panic(newErrorWithDetail(
479 errPriceOutOfRange,
480 ufmt.Sprintf("sqrtPriceLimitX96(%s) < slot0Start.sqrtPriceX96(%s) && sqrtPriceLimitX96(%s) > MIN_SQRT_RATIO(%s)",
481 sqrtPriceLimitX96.ToString(),
482 slot0.SqrtPriceX96().ToString(),
483 sqrtPriceLimitX96.ToString(),
484 MIN_SQRT_RATIO),
485 ))
486 }
487 } else {
488 cond1 := sqrtPriceLimitX96.Gt(slot0.SqrtPriceX96())
489 cond2 := sqrtPriceLimitX96.Lt(consts.MaxSqrtRatio())
490 if !(cond1 && cond2) {
491 panic(newErrorWithDetail(
492 errPriceOutOfRange,
493 ufmt.Sprintf("sqrtPriceLimitX96(%s) > slot0Start.sqrtPriceX96(%s) && sqrtPriceLimitX96(%s) < MAX_SQRT_RATIO(%s)",
494 sqrtPriceLimitX96.ToString(),
495 slot0.SqrtPriceX96().ToString(),
496 sqrtPriceLimitX96.ToString(),
497 MAX_SQRT_RATIO),
498 ))
499 }
500 }
501}
502
503// getFeeProtocol returns the appropriate fee protocol based on zero for one.
504// When zeroForOne is true, we want the lower 4 bits (% 16).
505// Otherwise, we want the upper 4 bits (/ 16).
506func getFeeProtocol(slot0 pl.Slot0, zeroForOne bool) uint8 {
507 shift := uint8(0)
508 if !zeroForOne {
509 shift = 4
510 }
511 return (slot0.FeeProtocol() >> shift) & uint8(0xF)
512}
513
514// getFeeGrowthGlobal returns the appropriate fee growth global based on zero for one.
515func getFeeGrowthGlobal(pool *pl.Pool, zeroForOne bool) *u256.Uint {
516 if zeroForOne {
517 return pool.FeeGrowthGlobal0X128().Clone()
518 }
519 return pool.FeeGrowthGlobal1X128().Clone()
520}
521
522// shouldContinueSwap checks if swap should continue based on remaining amount and price limit.
523func shouldContinueSwap(state SwapState, sqrtPriceLimitX96 *u256.Uint) bool {
524 return !state.amountSpecifiedRemaining.IsZero() && !state.sqrtPriceX96.Eq(sqrtPriceLimitX96)
525}
526
527// computeSwapStep executes a single step of swap and returns new state
528func (i *poolV1) computeSwapStep(
529 state SwapState,
530 pool *pl.Pool,
531 zeroForOne bool,
532 sqrtPriceLimitX96 *u256.Uint,
533 exactInput bool,
534 cache *SwapCache,
535 onTickCross tickCrossHookFn,
536) (SwapState, error) {
537 step := computeSwapStepInit(state, pool, zeroForOne)
538
539 // determining the price target for this step
540 sqrtRatioTargetX96 := computeTargetSqrtRatio(step, sqrtPriceLimitX96, zeroForOne).Clone()
541
542 // computing the amounts to be swapped at this step
543 var (
544 newState SwapState
545 err error
546 )
547
548 newState, step = computeAmounts(state, sqrtRatioTargetX96, pool, step)
549 newState, err = updateAmounts(step, newState, exactInput)
550 if err != nil {
551 return state, err
552 }
553
554 // if the protocol fee is on, calculate how much is owed,
555 // decrement fee amount, and increment protocol fee
556 if cache.feeProtocol > 0 {
557 newState, step, err = updateFeeProtocol(step, cache.feeProtocol, newState)
558 if err != nil {
559 return state, err
560 }
561 }
562
563 // update global fee tracker
564 if newState.liquidity.Gt(u256.Zero()) {
565 update := u256.MulDiv(step.feeAmount, fixedPointQ128, newState.liquidity)
566 feeGrowthGlobalX128 := u256.Zero().Add(newState.feeGrowthGlobalX128, update)
567 newState.setFeeGrowthGlobalX128(feeGrowthGlobalX128)
568 }
569
570 // handling tick transitions
571 if newState.sqrtPriceX96.Eq(step.sqrtPriceNextX96) {
572 newState = i.tickTransition(step, zeroForOne, newState, pool, cache, onTickCross)
573 } else if newState.sqrtPriceX96.Neq(step.sqrtPriceStartX96) {
574 newState.setTick(gnsmath.TickMathGetTickAtSqrtRatio(newState.sqrtPriceX96))
575 }
576
577 return newState, nil
578}
579
580// updateFeeProtocol calculates and updates protocol fees for the current step.
581func updateFeeProtocol(step StepComputations, feeProtocol uint8, state SwapState) (SwapState, StepComputations, error) {
582 delta := u256.Zero().Div(step.feeAmount, u256.NewUint(uint64(feeProtocol)))
583
584 newFeeAmount, overflow := u256.Zero().SubOverflow(step.feeAmount, delta)
585 if overflow {
586 return state, step, errors.New(errUnderflow)
587 }
588
589 step.feeAmount = newFeeAmount
590
591 newProtocolFee, overflow := u256.Zero().AddOverflow(state.protocolFee, delta)
592 if overflow {
593 return state, step, errors.New(errOverflow)
594 }
595 state.protocolFee = newProtocolFee
596
597 return state, step, nil
598}
599
600// computeSwapStepInit initializes the computation for a single swap step.
601func computeSwapStepInit(state SwapState, pool *pl.Pool, zeroForOne bool) StepComputations {
602 var step StepComputations
603 step.sqrtPriceStartX96 = state.sqrtPriceX96
604 tickNext, initialized := tickBitmapNextInitializedTickWithInOneWord(
605 pool,
606 state.tick,
607 pool.TickSpacing(),
608 zeroForOne,
609 )
610
611 step.tickNext = tickNext
612 step.initialized = initialized
613
614 // prevent overshoot the min/max tick
615 step.clampTickNext()
616 // get the price for the next tick
617 step.sqrtPriceNextX96 = gnsmath.TickMathGetSqrtRatioAtTick(step.tickNext)
618 return step
619}
620
621// computeTargetSqrtRatio determines the target sqrt price for the current swap step.
622func computeTargetSqrtRatio(step StepComputations, sqrtPriceLimitX96 *u256.Uint, zeroForOne bool) *u256.Uint {
623 if shouldUsePriceLimit(step.sqrtPriceNextX96, sqrtPriceLimitX96, zeroForOne) {
624 return sqrtPriceLimitX96
625 }
626 return step.sqrtPriceNextX96
627}
628
629// shouldUsePriceLimit returns true if the price limit should be used instead of the next tick price
630func shouldUsePriceLimit(sqrtPriceNext, sqrtPriceLimit *u256.Uint, zeroForOne bool) bool {
631 if zeroForOne {
632 return sqrtPriceNext.Lt(sqrtPriceLimit)
633 }
634 return sqrtPriceNext.Gt(sqrtPriceLimit)
635}
636
637// computeAmounts calculates the input and output amounts for the current swap step.
638func computeAmounts(state SwapState, sqrtRatioTargetX96 *u256.Uint, pool *pl.Pool, step StepComputations) (SwapState, StepComputations) {
639 sqrtPriceX96, amountIn, amountOut, feeAmount := gnsmath.SwapMathComputeSwapStep(
640 state.sqrtPriceX96,
641 sqrtRatioTargetX96,
642 state.liquidity,
643 state.amountSpecifiedRemaining,
644 uint64(pool.Fee()),
645 )
646
647 step.amountIn = amountIn
648 step.amountOut = amountOut
649 step.feeAmount = feeAmount
650
651 state.setSqrtPriceX96(sqrtPriceX96)
652
653 return state, step
654}
655
656// updateAmounts calculates new remaining and calculated amounts based on the swap step.
657// For exact input swaps:
658// - Decrements remaining input amount by (amountIn + feeAmount)
659// - Decrements calculated amount by amountOut
660//
661// For exact output swaps:
662// - Increments remaining output amount by amountOut
663// - Increments calculated amount by (amountIn + feeAmount)
664func updateAmounts(step StepComputations, state SwapState, exactInput bool) (SwapState, error) {
665 amountInWithFeeU256 := u256.Zero().Add(step.amountIn, step.feeAmount)
666 if amountInWithFeeU256.Gt(maxInt256) {
667 return state, errors.New(errOverflow)
668 }
669
670 amountInWithFee := i256.FromUint256(amountInWithFeeU256)
671 if step.amountOut.Gt(maxInt256) {
672 return state, errors.New(errOverflow)
673 }
674
675 var (
676 amountSpecifiedRemaining *i256.Int
677 amountCalculated *i256.Int
678 overflow bool
679 )
680
681 if exactInput {
682 amountSpecifiedRemaining, overflow = i256.Zero().SubOverflow(state.amountSpecifiedRemaining, amountInWithFee)
683 if overflow {
684 return state, errors.New(errUnderflow)
685 }
686 amountCalculated, overflow = i256.Zero().SubOverflow(state.amountCalculated, i256.FromUint256(step.amountOut))
687 if overflow {
688 return state, errors.New(errUnderflow)
689 }
690 } else {
691 amountSpecifiedRemaining, overflow = i256.Zero().AddOverflow(state.amountSpecifiedRemaining, i256.FromUint256(step.amountOut))
692 if overflow {
693 return state, errors.New(errOverflow)
694 }
695 amountCalculated, overflow = i256.Zero().AddOverflow(state.amountCalculated, amountInWithFee)
696 if overflow {
697 return state, errors.New(errOverflow)
698 }
699 }
700
701 // If an overflowed value is stored in state, it may cause problems in the next step
702 if amountCalculated.Gt(maxInt64) || amountSpecifiedRemaining.Gt(maxInt64) {
703 return state, errors.New(errOverflow)
704 }
705
706 // If an underflowed value is stored in state, it may cause problems in the next step
707 if amountCalculated.Lt(minInt64) || amountSpecifiedRemaining.Lt(minInt64) {
708 return state, errors.New(errUnderflow)
709 }
710
711 state.amountSpecifiedRemaining = amountSpecifiedRemaining
712 state.amountCalculated = amountCalculated
713
714 return state, nil
715}
716
717// tickTransition handles the transition between price ticks during a swap
718func (i *poolV1) tickTransition(step StepComputations, zeroForOne bool, state SwapState, pool *pl.Pool, cache *SwapCache, onTickCross tickCrossHookFn) SwapState {
719 // ensure existing state to keep immutability
720 newState := state
721
722 if step.initialized {
723 // Compute oracle values on first initialized tick cross
724 if !cache.computedLatestObservation {
725 observationState := pool.ObservationState()
726 if observationState != nil {
727 tickCumulative, secondsPerLiquidityStr, err := observeSingle(
728 observationState,
729 cache.blockTimestamp,
730 0,
731 state.tick,
732 observationState.Index(),
733 cache.liquidityStart,
734 observationState.Cardinality(),
735 )
736 if err == nil {
737 cache.tickCumulative = tickCumulative
738 cache.secondsPerLiquidityCumulativeX128 = u256.MustFromDecimal(secondsPerLiquidityStr)
739 cache.computedLatestObservation = true
740 }
741 }
742 }
743
744 if cache.secondsPerLiquidityCumulativeX128 == nil {
745 cache.secondsPerLiquidityCumulativeX128 = u256.Zero()
746 }
747
748 fee0, fee1 := u256.Zero(), u256.Zero()
749
750 if zeroForOne {
751 fee0 = state.feeGrowthGlobalX128
752 fee1 = pool.FeeGrowthGlobal1X128()
753 } else {
754 fee0 = pool.FeeGrowthGlobal0X128()
755 fee1 = state.feeGrowthGlobalX128
756 }
757
758 liquidityNet := tickCross(
759 pool,
760 step.tickNext,
761 fee0,
762 fee1,
763 cache.secondsPerLiquidityCumulativeX128,
764 cache.tickCumulative,
765 cache.blockTimestamp,
766 )
767
768 if zeroForOne {
769 liquidityNet = i256.Zero().Neg(liquidityNet)
770 }
771
772 newState.liquidity = gnsmath.LiquidityMathAddDelta(state.liquidity, liquidityNet)
773
774 if onTickCross != nil {
775 onTickCross(pool, step.tickNext, zeroForOne, cache.blockTimestamp)
776 }
777 }
778
779 newState.tick = step.tickNext
780 if zeroForOne {
781 newState.tick = step.tickNext - 1
782 }
783
784 return newState
785}