v1 source realm
package v1 implements GnoSwap's concentrated liquidity pools based on Uniswap V3. It manages liquidity positions, exe...
View source
Pool
Concentrated liquidity AMM pools with tick-based pricing.
Overview
Pool contracts implement Uniswap V3-style concentrated liquidity, allowing LPs to provide liquidity within custom price ranges for maximum capital efficiency.
Configuration
- Pool Creation Fee: 100 GNS (default)
- Protocol Fee: Disabled (0) or a denominator of 4-10, routing 1/4 to 1/10 of swap fees to the protocol
- Withdrawal Fee: 1% on fee-bearing collection (configurable)
- Fee Tiers: 0.01%, 0.05%, 0.3%, 1%
- Tick Spacing: Auto-set by fee tier
- Max Liquidity Per Tick: Depends on tick spacing; use
GetMaxLiquidityPerTickrather than2^128 - 1
Core Concepts
Concentrated Liquidity
Liquidity providers concentrate capital within custom price ranges instead of 0-∞. This allows LPs to allocate capital where it's most likely to generate fees - near the current price for volatile pairs, or within tight ranges for stable pairs. Capital efficiency can improve by orders of magnitude depending on range selection and pair volatility. For more details, check out GnoSwap Docs.
Tick System
- Price space divided into discrete ticks (0.01% apart)
- Each tick represents ~0.01% price change
- Positions defined by upper/lower tick boundaries
- Liquidity activated only when price in range
Key Functions
CreatePool
Deploys a new trading pair.
- Requires 100 GNS creation fee by default
- Valid fee tier required
- Accepts either token path order and canonicalizes token0/token1
- If paths are reversed, the initial square-root price is inverted
- Initial
sqrtPriceX96must be in[MIN_SQRT_RATIO, MAX_SQRT_RATIO) - Does not compare the initial price with an oracle or external market price
Mint
Adds liquidity to position (called by Position contract).
- Calculates token amounts from liquidity
- Updates tick bitmap
- Transfers tokens from owner
- Returns actual amounts used
Burn
Removes liquidity without collecting tokens.
- Pool-level operation: burn first, then collect owed tokens
- Calculates owed principal
- Updates position state
Collect
Pays tokens owed by a pool position without a withdrawal fee. This fee-free
path is normally used for principal after Burn.
- Transfers the requested portion of
tokensOwed - Updates
tokensOwed
CollectSwapFee
Pays accrued swap fees through the fee-bearing collection path.
- Applies the configured withdrawal fee
- Returns gross collected amounts and the fee withheld
Position.DecreaseLiquidityandPosition.CollectFeeinvoke the appropriate pool paths internally
Swap
Core swap execution (called by Router).
- Iterates through ticks
- Updates price and liquidity
- Calculates fees
- Maintains TWAP oracle
Swap Callback
The Swap function uses a callback pattern for token transfers, following the Uniswap V3 flash swap design.
Callback Signature:
1func swapCallback(cur realm, amount0Delta, amount1Delta int64, _ *pool.CallbackMarker) error
Delta Convention:
| Delta | Meaning |
|---|---|
Positive (> 0) |
Amount the pool must RECEIVE (input token) |
Negative (< 0) |
Amount the pool has SENT (output token) |
Swap Direction Examples:
For zeroForOne = true (token0 → token1):
amount0Delta > 0: Pool receives token0 (input)amount1Delta < 0: Pool sends token1 (output)
For zeroForOne = false (token1 → token0):
amount0Delta < 0: Pool sends token0 (output)amount1Delta > 0: Pool receives token1 (input)
Callback Implementation Example:
1func swapCallback(cur realm, amount0Delta, amount1Delta int64, _ *pool.CallbackMarker) error {
2 caller := cur.Previous().Address()
3 poolAddr := chain.PackageAddress("gno.land/r/gnoswap/pool")
4
5 // Security check: ensure this callback is invoked by the legitimate pool
6 if caller != poolAddr {
7 return errors.New("unauthorized caller")
8 }
9
10 if amount0Delta > 0 {
11 // Transfer token0 to pool
12 common.SafeGRC20Transfer(0, cur, token0Path, poolAddr, amount0Delta)
13 }
14 if amount1Delta > 0 {
15 // Transfer token1 to pool
16 common.SafeGRC20Transfer(0, cur, token1Path, poolAddr, amount1Delta)
17 }
18 return nil
19}
Important Notes:
- A custom callback should verify that the caller is the legitimate pool.
- In the router flow, the supplied closure performs that pool-origin check
before calling
router.SwapCallback; the Router implementation then checks that its caller is Router v1. - The callback MUST transfer at least the positive delta amount to the pool.
- Return
nilon success, or an error to revert the swap. - Pool validates the balance increase after callback execution.
Technical Details
Price Math
Q96 Format: Prices stored as sqrtPriceX96 = sqrt(price) * 2^96
Price 1:1 → sqrtPriceX96 = 79228162514264337593543950336
Price 1:4 → sqrtPriceX96 = 39614081257132168796771975168
Price 100:1 → sqrtPriceX96 = 792281625142643375935439503360
Tick to Price: price = 1.0001^tick
tick 0 = price 1
tick 6932 = price ~2
tick -6932 = price ~0.5
Range Liquidity:
Liquidity is calculated from the token required by the current price:
- Below the range (
current < lower): token0 only - In the range (
lower <= current < upper): both token0 and token1 - Above the range (
current >= upper): token1 only
The integer formulas use the square-root prices and round in the direction
required by the mint or burn operation; there is no single amount formula
that applies to all three cases.
Impermanent Loss:
- Narrow range: Higher fees, higher IL
- Wide range: Lower fees, lower IL
- Stable pairs: ±0.1% ranges optimal
- Volatile pairs: ±10%+ ranges recommended
Fee Mechanics
Swap Fees:
- Charged on input amount
- Accumulates as feeGrowthGlobal
- Distributed pro-rata to in-range liquidity
Fee Calculation:
fees = feeGrowthInside * liquidity
feeGrowthInside = feeGrowthGlobal - feeGrowthOutside
Protocol fees:
0disables protocol fee collection4through10are denominators:4routes 25% and10routes 10% of swap fees to the protocol- Governance-managed configuration applies to the pool set, not an independent percentage selected on each pool
Security
Reentrancy Protection
- The live guard is the pool-wide
Unlockedkey in the pool KV store, managed bypool/v1/lock.gno.Slot0.unlockedis a separate stored field and is not the guard;GetSlot0Unlockedreports that field, not the live lock. - The lock is not swap-specific.
CreatePool,Mint,Burn,Collect,CollectSwapFee,CollectProtocol,SetFeeProtocol,SetWithdrawalFee,SetPoolCreationFee,IncreaseObservationCardinalityNext,SetSwapStartHook,SetSwapEndHook,SetTickCrossHook,Swap, and the read-onlyDrySwapall assert that the pool is unlocked before doing any work. - The unlocked assertion is read-only and runs before the access checks, so a call that aborts on authorization leaves no persisted lock behind.
- Settlement order is operation-specific rather than uniformly
checks-effects-interactions.
Swapsettles optimistically through the callback and verifies the resulting balance increase afterwards, whileMintpulls tokens before its final pool save. Review the specific path rather than assuming every write precedes every external call.
Price Manipulation
- TWAP oracle provides time-weighted observations for monitoring; it is not an automatic initial-price guard
- Large swaps limited by liquidity
- Slippage protection required
Pool Creation Griefing
Issue: CreatePool validates the fee tier, token canonicalization, and
square-root price bounds, but does not compare the initial price with an
oracle or external market price. A pool can therefore be created at an
economically inappropriate extreme price.
Impact:
- Pool may be temporarily unusable
- No rational LP may provide liquidity at a distorted price
- Price cannot self-correct without liquidity
Recovery Mechanism: Recovery requires coordinated liquidity provision and swaps to move the price toward a desired market rate, followed by liquidity removal. The protocol does not perform this correction automatically, and profitability depends on market conditions, fees, and slippage.
Example Recovery Sequence:
This pseudocode assumes the integrating realm function has a current cur token.
// Illustrative sequence; the caller must compose and execute these operations
1. position.Mint(cross(cur), ..., fullRange, largeAmount, ...) // Add liquidity
2. router.ExactInSwapRoute(cross(cur), ..., targetRoute, ...) // Fix price via arbitrage
3. position.DecreaseLiquidity(cross(cur), positionId, ...) // Remove liquidity and collect principal
4. position.CollectFee(cross(cur), positionId) // Collect any remaining fees
Prevention:
- 100 GNS creation fee provides deterrent
- Consider implementing price oracle validation for high-value pairs
- Monitor pool creation events for suspicious activity
Rounding
- Integer math rounds directionally for the input/output invariant; not every division rounds down
- Minimum liquidity enforced
- Full precision for amounts
package v1 implements GnoSwap's concentrated liquidity pools based on Uniswap V3. It manages liquidity positions, executes swaps, and maintains pool state including price, liquidity, and fee calculations.
The pool contract is the core of the GnoSwap AMM, supporting: - Concentrated liquidity within custom price ranges - Multiple fee tiers (0.01%, 0.05%, 0.3%, 1%) - Single-tick and cross-tick swaps - Protocol fee collection - Tick bitmap optimization for gas efficiency
6
const MIN_SQRT_RATIO, MAX_SQRT_RATIO
const MaxBpsValue, ZeroBps
const MAX_LIQUIDITY_PER_TICK_SPACING_1, MAX_LIQUIDITY_PER_TICK_SPACING_10, MAX_LIQUIDITY_PER_TICK_SPACING_60, MAX_LIQUIDITY_PER_TICK_SPACING_200, MIN_TICK, MAX_TICK
1const (
2 MAX_LIQUIDITY_PER_TICK_SPACING_1 = "191757530477355301479181766273477"
3 MAX_LIQUIDITY_PER_TICK_SPACING_10 = "1917569901783203986719870431555990"
4 MAX_LIQUIDITY_PER_TICK_SPACING_60 = "11505743598341114571880798222544994"
5 MAX_LIQUIDITY_PER_TICK_SPACING_200 = "38350317471085141830651933667504588"
6 MIN_TICK int32 = -887272
7 MAX_TICK int32 = 887272
8)const MAX_UINT64, MAX_INT64, MAX_INT128, MIN_INT128, MAX_UINT128, MAX_INT256, INT64_MIN, INT64_MAX, Q96_RESOLUTION, Q128_RESOLUTION, Q64, Q96, Q128
1const (
2 MAX_UINT64 string = "18446744073709551615"
3 MAX_INT64 string = "9223372036854775807"
4 MAX_INT128 string = "170141183460469231731687303715884105727"
5 MIN_INT128 string = "-170141183460469231731687303715884105728"
6 MAX_UINT128 string = "340282366920938463463374607431768211455"
7 MAX_INT256 string = "57896044618658097711785492504343953926634992332820282019728792003956564819967"
8
9 INT64_MIN int64 = -9223372036854775808
10 INT64_MAX int64 = 9223372036854775807
11
12 Q96_RESOLUTION uint = 96
13 Q128_RESOLUTION uint = 128
14
15 Q64 string = "18446744073709551616" // 2 ** 64
16 Q96 string = "79228162514264337593543950336" // 2 ** 96
17 Q128 string = "340282366920938463463374607431768211456" // 2 ** 128
18)const GNS_TOKEN_KEY
3
func GetPoolPath
ActionGetPoolPath generates a unique pool path string based on the token paths and fee tier. Parameters:
- token0Path: first token contract path; pool paths canonicalize token order.
- token1Path: second token contract path; pool paths canonicalize token order.
- fee: fee tier encoded in the pool identifier.
Returns:
- poolPath: canonical token0:token1:fee pool identifier.
func NewPoolV1
ActionNewPoolV1 constructs the version-one pool implementation around shared persistent storage.
Parameters:
- store: pool-domain storage implementation used by the returned API.
Returns:
- implementation: IPool backed by store.
func NewTickEventInfo
ActionNewTickEventInfo creates event data for one tick and its persisted state.
Parameters:
- tickID: tick index represented by the event.
- tickInfo: tick state whose fields are serialized by ToString.
Returns:
- *tickEventInfo: event information value retaining the supplied tick index and state.
6
type ModifyPositionParams
struct 1type ModifyPositionParams struct {
2 // owner is the address that owns the position
3 owner address
4
5 tickLower int32 // lower tick of the position
6 tickUpper int32 // upper tick of the position
7
8 // liquidityDelta represents the change in liquidity
9 // Positive for minting, negative for burning
10 liquidityDelta *i256.Int
11}ModifyPositionParams repersents the parameters for modifying a liquidity position. This structure is used internally both `Mint` and `Burn` operation to manage the liquidity positions.
type StepComputations
struct1type StepComputations struct {
2 sqrtPriceStartX96 *u256.Uint // price at the beginning of the step
3 tickNext int32 // next tick to swap to from the current tick in the swap direction
4 initialized bool // whether tickNext is initialized
5 sqrtPriceNextX96 *u256.Uint // sqrt(price) for the next tick (token1/token0) Q96
6 amountIn *u256.Uint // how much being swapped in this step
7 amountOut *u256.Uint // how much is being swapped out in this step
8 feeAmount *u256.Uint // how much fee is being paid in this step
9}StepComputations holds intermediate values used during a single step of a swap. Each step represents movement from the current tick to the next initialized tick or the target price, whichever comes first.
type SwapCache
struct 1type SwapCache struct {
2 feeProtocol uint8 // protocol fee for the input token
3 liquidityStart *u256.Uint // liquidity at the beginning of the swap
4 blockTimestamp int64 // current block timestamp
5 tickCumulative int64 // current tick accumulator value
6 secondsPerLiquidityCumulativeX128 *u256.Uint // current seconds per liquidity accumulator
7 computedLatestObservation bool // whether we've computed the above accumulators
8 slot0Start pl.Slot0 // immutable Slot0 snapshot captured at swap start
9 readOnly bool // quote without tick accounting or hooks
10}SwapCache holds immutable swap-start inputs and oracle cumulatives that are populated lazily when the swap first crosses an initialized tick.
type SwapComputation
structSwapComputation encapsulates the pure computation logic for swaps.
type SwapResult
structSwapResult encapsulates all state changes from a swap. It ensures atomic state transitions that can be applied at once.
type SwapState
struct1type SwapState struct {
2 amountSpecifiedRemaining *i256.Int // amount remaining to be swapped in/out of the input/output token
3 amountCalculated *i256.Int // amount already swapped out/in of the output/input token
4 sqrtPriceX96 *u256.Uint // current sqrt(price)
5 tick int32 // tick associated with the current sqrt(price)
6 feeGrowthGlobalX128 *u256.Uint // global fee growth of the input token
7 protocolFee *u256.Uint // amount of input token paid as protocol fee
8 liquidity *u256.Uint // current liquidity in range
9}SwapState tracks the changing values during a swap. This type helps manage the state transitions that occur as the swap progresses across different price ranges.
23
- chain stdlib
- errors stdlib
- gno.land/p/gnoswap/consts/v1 package
- gno.land/p/gnoswap/gnsmath/v1 package
- gno.land/p/gnoswap/int256/v1 package
- gno.land/p/gnoswap/rbac/v1 package
- gno.land/p/gnoswap/uint256/v1 package
- gno.land/p/gnoswap/utils/v1 package
- gno.land/p/moul/md/v0 package
- gno.land/p/moul/mdtable/v0 package
- gno.land/p/nt/bptree/rotree/v0 package
- gno.land/p/nt/ufmt/v0 package
- gno.land/r/gnoswap/access/v1 realm
- gno.land/r/gnoswap/common realm
- gno.land/r/gnoswap/emission realm
- gno.land/r/gnoswap/gns realm
- gno.land/r/gnoswap/halt/v1 realm
- gno.land/r/gnoswap/pool realm
- gno.land/r/gnoswap/protocol_fee realm
- gno.land/r/gnoswap/rbac/v1 realm
- strconv stdlib
- strings stdlib
- time stdlib