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