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