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