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