Kind Realm (renderable)
Name v1
Namespace gnoswap / pool
Exported functions 3
Module gno.land/r/gnoswap/pool/v1
gno 0.9 swap.gno gno
⧉
1 package pool 2 3 import ( 4 "chain" 5 "errors" 6 "strconv" 7 "time" 8 GetPoolPath (token0Path string, token1Path string, fee uint32) string
NewPoolV1 (store interface {GetFeeAmountTickSpacing func() map[uint32]int32; GetObservations func() *gno.land/p/nt/bptree/v0.BPTree; GetPendingProtocolFee func(string) int64; GetPendingProtocolFees func() map[string]int64; GetPoolCreationFee func() int64; GetPools func() *gno.land/p/nt/bptree/v0.BPTree; GetSlot0FeeProtocol func() uint8; GetSwapEndHook func() func(.uverse.realm, string) .uverse.error; GetSwapStartHook func() func(.uverse.realm, string, int64); GetTickCrossHook func() func(.uverse.realm, string, int32, bool, int64); GetUnlocked func() bool; GetWithdrawalFeeBPS func() uint64; HasFeeAmountTickSpacing func() bool; HasObservations func() bool; HasPendingProtocolFees func() bool; HasPoolCreationFee func() bool; HasPools func() bool; HasSlot0FeeProtocol func() bool; HasSwapEndHook func() bool; HasSwapStartHook func() bool; HasTickCrossHook func() bool; HasUnlocked func() bool; HasWithdrawalFeeBPS func() bool; RemovePendingProtocolFee func(int, .uverse.realm, string) .uverse.error; SetFeeAmountTickSpacing func(int, .uverse.realm, map[uint32]int32) .uverse.error; SetObservations func(int, .uverse.realm, *gno.land/p/nt/bptree/v0.BPTree) .uverse.error; SetPendingProtocolFee func(int, .uverse.realm, string, int64) .uverse.error; SetPendingProtocolFees func(int, .uverse.realm, map[string]int64) .uverse.error; SetPoolCreationFee func(int, .uverse.realm, int64) .uverse.error; SetPools func(int, .uverse.realm, *gno.land/p/nt/bptree/v0.BPTree) .uverse.error; SetSlot0FeeProtocol func(int, .uverse.realm, uint8) .uverse.error; SetSwapEndHook func(int, .uverse.realm, func(.uverse.realm, string) .uverse.error) .uverse.error; SetSwapStartHook func(int, .uverse.realm, func(.uverse.realm, string, int64)) .uverse.error; SetTickCrossHook func(int, .uverse.realm, func(.uverse.realm, string, int32, bool, int64)) .uverse.error; SetUnlocked func(int, .uverse.realm, bool) .uverse.error; SetWithdrawalFeeBPS func(int, .uverse.realm, uint64) .uverse.error}) interface {Burn func(int, .uverse.realm, string, string, uint32, int32, int32, string, .uverse.address) (string, string); Collect func(int, .uverse.realm, string, string, uint32, .uverse.address, int32, int32, string, string) (string, string); CollectProtocol func(int, .uverse.realm, string, string, uint32, .uverse.address, string, string) (string, string); CollectSwapFee func(int, .uverse.realm, string, string, uint32, .uverse.address, int32, int32, string, string) (string, string, string, string); CreatePool func(int, .uverse.realm, string, string, uint32, string); DrySwap func(string, string, uint32, bool, string, string) (string, string, .uverse.error); ExistsPoolPath func(string) bool; GetBalanceToken0 func(string) (int64, .uverse.error); GetBalanceToken1 func(string) (int64, .uverse.error); GetFee func(string) (uint32, .uverse.error); GetFeeAmountTickSpacing func(uint32) (int32, .uverse.error); GetFeeAmountTickSpacings func() map[uint32]int32; GetFeeGrowthGlobal0X128 func(string) (*gno.land/p/gnoswap/uint256/v1.Uint, .uverse.error); GetFeeGrowthGlobal1X128 func(string) (*gno.land/p/gnoswap/uint256/v1.Uint, .uverse.error); GetFeeGrowthGlobalX128 func(string) (*gno.land/p/gnoswap/uint256/v1.Uint, *gno.land/p/gnoswap/uint256/v1.Uint, .uverse.error); GetInitializedTicksInRange func(string, int32, int32) ([]int32, .uverse.error); GetLiquidity func(string) (*gno.land/p/gnoswap/uint256/v1.Uint, .uverse.error); GetObservationAt func(string, uint16) (gno.land/r/gnoswap/pool.Observation, .uverse.error); GetPendingProtocolFees func() map[string]int64; GetPoolCreationFee func() int64; GetPoolPositions func(string) *gno.land/p/nt/bptree/rotree/v0.ReadOnlyTree; GetPools func() *gno.land/p/nt/bptree/rotree/v0.ReadOnlyTree; GetPositionFeeGrowthInside0LastX128 func(string, string) (string, .uverse.error); GetPositionFeeGrowthInside1LastX128 func(string, string) (string, .uverse.error); GetPositionFeeGrowthInsideLastX128 func(string, string) (string, string, .uverse.error); GetPositionLiquidity func(string, string) (string, .uverse.error); GetPositionTokensOwed0 func(string, string) (int64, .uverse.error); GetPositionTokensOwed1 func(string, string) (int64, .uverse.error); GetProtocolFeesToken0 func(string) (int64, .uverse.error); GetProtocolFeesToken1 func(string) (int64, .uverse.error); GetSlot0 func(string) gno.land/r/gnoswap/pool.Slot0; GetSlot0FeeProtocol func(string) (uint8, .uverse.error); GetSlot0SqrtPriceX96 func(string) (*gno.land/p/gnoswap/uint256/v1.Uint, .uverse.error); GetSlot0Tick func(string) (int32, .uverse.error); GetSlot0Unlocked func(string) (bool, .uverse.error); GetTickBitmaps func(string, int16) (string, .uverse.error); GetTickCumulativeOutside func(string, int32) (int64, .uverse.error); GetTickFeeGrowthOutside0X128 func(string, int32) (string, .uverse.error); GetTickFeeGrowthOutside1X128 func(string, int32) (string, .uverse.error); GetTickFeeGrowthOutsideX128 func(string, int32) (string, string, .uverse.error); GetTickInfo func(string, int32) (gno.land/r/gnoswap/pool.TickInfo, .uverse.error); GetTickInitialized func(string, int32) (bool, .uverse.error); GetTickLiquidityGross func(string, int32) (string, .uverse.error); GetTickLiquidityNet func(string, int32) (string, .uverse.error); GetTickSecondsOutside func(string, int32) (uint32, .uverse.error); GetTickSecondsPerLiquidityOutsideX128 func(string, int32) (string, .uverse.error); GetTickSpacing func(string) (int32, .uverse.error); GetToken0Path func(string) (string, .uverse.error); GetToken1Path func(string) (string, .uverse.error); GetWithdrawalFee func() uint64; IncreaseObservationCardinalityNext func(int, .uverse.realm, string, string, uint32, uint16); Mint func(int, .uverse.realm, string, string, uint32, int32, int32, string, .uverse.address) (string, string); Observe func(string, []uint32) ([]int64, []string, .uverse.error); OracleConsult func(string, uint32) (int32, *gno.land/p/gnoswap/uint256/v1.Uint, .uverse.error); Render func(string) string; SetFeeProtocol func(int, .uverse.realm, uint8, uint8); SetPoolCreationFee func(int, .uverse.realm, int64); SetSwapEndHook func(int, .uverse.realm, func(.uverse.realm, string) .uverse.error); SetSwapStartHook func(int, .uverse.realm, func(.uverse.realm, string, int64)); SetTickCrossHook func(int, .uverse.realm, func(.uverse.realm, string, int32, bool, int64)); SetWithdrawalFee func(int, .uverse.realm, uint64); SnapshotCumulativesInside func(string, int32, int32) (int64, *gno.land/p/gnoswap/uint256/v1.Uint, uint32, .uverse.error); Swap func(int, .uverse.realm, string, string, uint32, .uverse.address, bool, string, string, func(.uverse.realm, int64, int64, *gno.land/r/gnoswap/pool.CallbackMarker) .uverse.error) (string, string)}
NewTickEventInfo (tickID int32, tickInfo struct{liquidityGross string; liquidityNet string; feeGrowthOutside0X128 string; feeGrowthOutside1X128 string; tickCumulativeOutside int64; secondsPerLiquidityOutsideX128 string; secondsOutside uint32; initialized bool}) *gno.land/r/gnoswap/pool/v1.tickEventInfo
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
Render unavailable
rate limit exceeded — the render query is served by the Gnoland archive RPC.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.
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.
23 var (
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.
48 func (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.
79 func (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.
110 func (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.
129 type 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.
142 type 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.
186 func (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.
382 func (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.
451 type 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.
465 func (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
515 func 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
537 func 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).
570 func 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.
579 func 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.
587 func 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
592 func (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.
649 func 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.
669 func 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.
690 func 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
698 func 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.
706 func 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)
732 func 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
786 func (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 }
856