Files
dex/pkg/lx/swap.go

661 lines
22 KiB
Go

// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package lx
import (
"fmt"
"math/big"
)
var bigMaxSwapFee = big.NewInt(MaxSwapFee)
// Swap-level errors.
var (
ErrMaxFeeExactOut = fmt.Errorf("cannot use max swap fee with exact output")
)
// GetSqrtPriceTarget returns the price target for the next swap step.
// Matches V4 SwapMath.getSqrtPriceTarget.
//
// zeroForOne == true: max(sqrtPriceNextX96, sqrtPriceLimitX96)
// zeroForOne == false: min(sqrtPriceNextX96, sqrtPriceLimitX96)
func GetSqrtPriceTarget(zeroForOne bool, sqrtPriceNextX96, sqrtPriceLimitX96 *big.Int) *big.Int {
if zeroForOne {
if sqrtPriceNextX96.Cmp(sqrtPriceLimitX96) >= 0 {
return new(big.Int).Set(sqrtPriceNextX96)
}
return new(big.Int).Set(sqrtPriceLimitX96)
}
if sqrtPriceNextX96.Cmp(sqrtPriceLimitX96) <= 0 {
return new(big.Int).Set(sqrtPriceNextX96)
}
return new(big.Int).Set(sqrtPriceLimitX96)
}
// ComputeSwapStep computes the result of swapping within a single tick range.
// Matches V4 SwapMath.computeSwapStep exactly.
//
// amountRemaining is SIGNED per V4 convention:
//
// negative = exact input (user specifies how much to spend)
// positive = exact output (user specifies how much to receive)
//
// Returns sqrtPriceNextX96, amountIn, amountOut, feeAmount, err.
// On error, all numeric returns are nil.
func ComputeSwapStep(
sqrtPriceCurrentX96, sqrtPriceTargetX96, liquidity, amountRemaining *big.Int,
feePips uint32,
) (sqrtPriceNextX96, amountIn, amountOut, feeAmount *big.Int, err error) {
zeroForOne := sqrtPriceCurrentX96.Cmp(sqrtPriceTargetX96) >= 0
exactIn := amountRemaining.Sign() < 0
feePipsBig := big.NewInt(int64(feePips))
feeComplement := new(big.Int).Sub(bigMaxSwapFee, feePipsBig) // MAX_SWAP_FEE - feePips
// Guard: exact output with 100% fee is a division by zero.
if !exactIn && feePips >= MaxSwapFee {
return nil, nil, nil, nil, ErrMaxFeeExactOut
}
if exactIn {
// |amountRemaining|
absRemaining := new(big.Int).Neg(amountRemaining)
// amountRemainingLessFee = MulDiv(|amountRemaining|, MAX_SWAP_FEE - feePips, MAX_SWAP_FEE)
amountRemainingLessFee, mErr := MulDiv(absRemaining, feeComplement, bigMaxSwapFee)
if mErr != nil {
return nil, nil, nil, nil, fmt.Errorf("ComputeSwapStep fee calc: %w", mErr)
}
if zeroForOne {
amountIn, err = GetAmount0Delta(sqrtPriceTargetX96, sqrtPriceCurrentX96, liquidity, true)
} else {
amountIn, err = GetAmount1Delta(sqrtPriceCurrentX96, sqrtPriceTargetX96, liquidity, true)
}
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("ComputeSwapStep amountIn delta: %w", err)
}
if amountRemainingLessFee.Cmp(amountIn) >= 0 {
// We can reach the target price.
sqrtPriceNextX96 = new(big.Int).Set(sqrtPriceTargetX96)
if feePips == MaxSwapFee {
feeAmount = new(big.Int).Set(amountIn)
} else {
feeAmount, err = MulDivRoundingUp(amountIn, feePipsBig, feeComplement)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("ComputeSwapStep fee rounding: %w", err)
}
}
} else {
// Exhaust the remaining amount before reaching target.
amountIn = amountRemainingLessFee
sqrtPriceNextX96, err = GetNextSqrtPriceFromInput(
sqrtPriceCurrentX96, liquidity, amountRemainingLessFee, zeroForOne,
)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("ComputeSwapStep next price from input: %w", err)
}
// Fee = remainder of the max input not used for the swap.
feeAmount = new(big.Int).Sub(absRemaining, amountIn)
}
if zeroForOne {
amountOut, err = GetAmount1Delta(sqrtPriceNextX96, sqrtPriceCurrentX96, liquidity, false)
} else {
amountOut, err = GetAmount0Delta(sqrtPriceCurrentX96, sqrtPriceNextX96, liquidity, false)
}
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("ComputeSwapStep amountOut delta: %w", err)
}
} else {
// exactOut: amountRemaining >= 0
if zeroForOne {
amountOut, err = GetAmount1Delta(sqrtPriceTargetX96, sqrtPriceCurrentX96, liquidity, false)
} else {
amountOut, err = GetAmount0Delta(sqrtPriceCurrentX96, sqrtPriceTargetX96, liquidity, false)
}
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("ComputeSwapStep amountOut target delta: %w", err)
}
if amountRemaining.Cmp(amountOut) >= 0 {
// We can reach the target price.
sqrtPriceNextX96 = new(big.Int).Set(sqrtPriceTargetX96)
} else {
// Cap the output and compute the resulting price.
amountOut = new(big.Int).Set(amountRemaining)
sqrtPriceNextX96, err = GetNextSqrtPriceFromOutput(
sqrtPriceCurrentX96, liquidity, amountOut, zeroForOne,
)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("ComputeSwapStep next price from output: %w", err)
}
}
if zeroForOne {
amountIn, err = GetAmount0Delta(sqrtPriceNextX96, sqrtPriceCurrentX96, liquidity, true)
} else {
amountIn, err = GetAmount1Delta(sqrtPriceCurrentX96, sqrtPriceNextX96, liquidity, true)
}
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("ComputeSwapStep amountIn exact out delta: %w", err)
}
// feeComplement is guaranteed non-zero here (MaxSwapFee check above).
feeAmount, err = MulDivRoundingUp(amountIn, feePipsBig, feeComplement)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("ComputeSwapStep exact out fee: %w", err)
}
}
return
}
// EngineCL implements Engine using in-process Go math for
// Uniswap V4-style concentrated liquidity. This IS the engine --
// all V4 math (swap loop, tick crossing, fee growth, position
// tracking) lives here.
type EngineCL struct{}
// NewEngine creates a new concentrated liquidity engine.
func NewEngine() *EngineCL {
return &EngineCL{}
}
// Verify interface compliance at compile time.
var _ Engine = (*EngineCL)(nil)
// Initialize computes the initial tick from sqrtPriceX96.
func (e *EngineCL) Initialize(sqrtPriceX96 *big.Int) (int24, error) {
tick, err := GetTickAtSqrtPrice(sqrtPriceX96)
if err != nil {
return 0, err
}
return tick, nil
}
// Swap executes the V4 tick-crossing swap loop.
// Mutates pool state in place (sqrtPriceX96, tick, liquidity, feeGrowth).
func (e *EngineCL) Swap(pool *PoolState, params SwapParams) (BalanceDelta, error) {
zeroForOne := params.ZeroForOne
if params.AmountSpecified.Sign() == 0 {
return ZeroBalanceDelta(), nil
}
// Validate price limit
if zeroForOne {
if params.SqrtPriceLimitX96.Cmp(pool.SqrtPriceX96) >= 0 {
return ZeroBalanceDelta(), fmt.Errorf("%w: zeroForOne limit %s >= current %s",
ErrPriceLimitReached, params.SqrtPriceLimitX96, pool.SqrtPriceX96)
}
if params.SqrtPriceLimitX96.Cmp(MinSqrtRatio) <= 0 {
return ZeroBalanceDelta(), fmt.Errorf("%w: limit below MIN_SQRT_RATIO", ErrInvalidSqrtPrice)
}
} else {
if params.SqrtPriceLimitX96.Cmp(pool.SqrtPriceX96) <= 0 {
return ZeroBalanceDelta(), fmt.Errorf("%w: oneForZero limit %s <= current %s",
ErrPriceLimitReached, params.SqrtPriceLimitX96, pool.SqrtPriceX96)
}
if params.SqrtPriceLimitX96.Cmp(MaxSqrtRatio) >= 0 {
return ZeroBalanceDelta(), fmt.Errorf("%w: limit above MAX_SQRT_RATIO", ErrInvalidSqrtPrice)
}
}
// 100% fee makes exact output impossible
swapFee := pool.LPFee
if swapFee >= MaxSwapFee && params.AmountSpecified.Sign() > 0 {
return ZeroBalanceDelta(), fmt.Errorf("invalid fee for exact output")
}
amountSpecifiedRemaining := new(big.Int).Set(params.AmountSpecified)
amountCalculated := big.NewInt(0)
sqrtPriceX96 := new(big.Int).Set(pool.SqrtPriceX96)
tick := pool.Tick
liquidity := new(big.Int).Set(pool.Liquidity)
var feeGrowthGlobalX128 *big.Int
if zeroForOne {
feeGrowthGlobalX128 = new(big.Int).Set(pool.FeeGrowth0X128)
} else {
feeGrowthGlobalX128 = new(big.Int).Set(pool.FeeGrowth1X128)
}
// MAIN SWAP LOOP
for amountSpecifiedRemaining.Sign() != 0 && sqrtPriceX96.Cmp(params.SqrtPriceLimitX96) != 0 {
sqrtPriceStartX96 := new(big.Int).Set(sqrtPriceX96)
tickNext, initialized := pool.TickBitmap.NextInitializedTickWithinOneWord(
tick, pool.TickSpacing, zeroForOne,
)
if tickNext < MinTick {
tickNext = MinTick
}
if tickNext > MaxTick {
tickNext = MaxTick
}
sqrtPriceNextX96, err := GetSqrtPriceAtTick(tickNext)
if err != nil {
return ZeroBalanceDelta(), fmt.Errorf("GetSqrtPriceAtTick(%d): %w", tickNext, err)
}
sqrtPriceTarget := GetSqrtPriceTarget(zeroForOne, sqrtPriceNextX96, params.SqrtPriceLimitX96)
var stepAmountIn, stepAmountOut, stepFeeAmount *big.Int
sqrtPriceX96, stepAmountIn, stepAmountOut, stepFeeAmount, err = ComputeSwapStep(
sqrtPriceX96, sqrtPriceTarget, liquidity, amountSpecifiedRemaining, swapFee,
)
if err != nil {
return ZeroBalanceDelta(), fmt.Errorf("ComputeSwapStep at tick %d: %w", tick, err)
}
if params.AmountSpecified.Sign() > 0 {
// exactOutput
amountSpecifiedRemaining.Sub(amountSpecifiedRemaining, stepAmountOut)
consumed := new(big.Int).Add(stepAmountIn, stepFeeAmount)
amountCalculated.Sub(amountCalculated, consumed)
} else {
// exactInput
consumed := new(big.Int).Add(stepAmountIn, stepFeeAmount)
amountSpecifiedRemaining.Add(amountSpecifiedRemaining, consumed)
amountCalculated.Add(amountCalculated, stepAmountOut)
}
if liquidity.Sign() > 0 {
feeGrowthDelta, fgErr := SimpleMulDiv(stepFeeAmount, Q128, liquidity)
if fgErr != nil {
return ZeroBalanceDelta(), fmt.Errorf("fee growth calc: %w", fgErr)
}
feeGrowthGlobalX128.Add(feeGrowthGlobalX128, feeGrowthDelta)
}
if sqrtPriceX96.Cmp(sqrtPriceNextX96) == 0 {
if initialized {
var fg0, fg1 *big.Int
if zeroForOne {
fg0 = feeGrowthGlobalX128
fg1 = pool.FeeGrowth1X128
} else {
fg0 = pool.FeeGrowth0X128
fg1 = feeGrowthGlobalX128
}
liquidityNet := crossTick(pool, tickNext, fg0, fg1)
if zeroForOne {
liquidityNet.Neg(liquidityNet)
}
var addErr error
liquidity, addErr = AddDelta(liquidity, liquidityNet)
if addErr != nil {
return ZeroBalanceDelta(), fmt.Errorf("liquidity overflow crossing tick %d: %w", tickNext, addErr)
}
}
if zeroForOne {
tick = tickNext - 1
} else {
tick = tickNext
}
} else if sqrtPriceX96.Cmp(sqrtPriceStartX96) != 0 {
var tErr error
tick, tErr = GetTickAtSqrtPrice(sqrtPriceX96)
if tErr != nil {
return ZeroBalanceDelta(), fmt.Errorf("swap tick lookup: %w", tErr)
}
}
}
pool.SqrtPriceX96 = sqrtPriceX96
pool.Tick = tick
pool.Liquidity = liquidity
if zeroForOne {
pool.FeeGrowth0X128 = feeGrowthGlobalX128
} else {
pool.FeeGrowth1X128 = feeGrowthGlobalX128
}
amountUsed := new(big.Int).Sub(params.AmountSpecified, amountSpecifiedRemaining)
if zeroForOne != (params.AmountSpecified.Sign() < 0) {
return NewBalanceDelta(amountCalculated, amountUsed), nil
}
return NewBalanceDelta(amountUsed, amountCalculated), nil
}
// ModifyLiquidity implements V4 concentrated liquidity add/remove.
func (e *EngineCL) ModifyLiquidity(
pool *PoolState, owner Address, params ModifyLiquidityParams,
) (BalanceDelta, BalanceDelta, error) {
liquidityDelta := params.LiquidityDelta
tickLower := params.TickLower
tickUpper := params.TickUpper
if tickLower >= tickUpper {
return ZeroBalanceDelta(), ZeroBalanceDelta(), ErrInvalidTickRange
}
if tickLower < MinTick || tickUpper > MaxTick {
return ZeroBalanceDelta(), ZeroBalanceDelta(), ErrTickOutOfRange
}
if tickLower%pool.TickSpacing != 0 || tickUpper%pool.TickSpacing != 0 {
return ZeroBalanceDelta(), ZeroBalanceDelta(), ErrTickMisaligned
}
var flippedLower, flippedUpper bool
var grossAfterLower, grossAfterUpper *big.Int
if liquidityDelta.Sign() != 0 {
var uErr error
flippedLower, grossAfterLower, uErr = updateTick(pool, tickLower, liquidityDelta, false)
if uErr != nil {
return ZeroBalanceDelta(), ZeroBalanceDelta(), uErr
}
flippedUpper, grossAfterUpper, uErr = updateTick(pool, tickUpper, liquidityDelta, true)
if uErr != nil {
return ZeroBalanceDelta(), ZeroBalanceDelta(), uErr
}
if liquidityDelta.Sign() >= 0 {
maxLiqPerTick := tickSpacingToMaxLiquidityPerTick(pool.TickSpacing)
if grossAfterLower.Cmp(maxLiqPerTick) > 0 {
return ZeroBalanceDelta(), ZeroBalanceDelta(),
fmt.Errorf("%w: tick %d liquidity overflow", ErrInsufficientLiquidity, tickLower)
}
if grossAfterUpper.Cmp(maxLiqPerTick) > 0 {
return ZeroBalanceDelta(), ZeroBalanceDelta(),
fmt.Errorf("%w: tick %d liquidity overflow", ErrInsufficientLiquidity, tickUpper)
}
}
if flippedLower {
if fErr := pool.TickBitmap.FlipTick(tickLower, pool.TickSpacing); fErr != nil {
return ZeroBalanceDelta(), ZeroBalanceDelta(), fmt.Errorf("flip lower tick: %w", fErr)
}
}
if flippedUpper {
if fErr := pool.TickBitmap.FlipTick(tickUpper, pool.TickSpacing); fErr != nil {
return ZeroBalanceDelta(), ZeroBalanceDelta(), fmt.Errorf("flip upper tick: %w", fErr)
}
}
}
feeGrowthInside0, feeGrowthInside1 := getFeeGrowthInside(pool, tickLower, tickUpper)
feeDelta, fErr := updatePosition(pool, owner, params, feeGrowthInside0, feeGrowthInside1)
if fErr != nil {
return ZeroBalanceDelta(), ZeroBalanceDelta(), fmt.Errorf("update position: %w", fErr)
}
if liquidityDelta.Sign() < 0 {
if flippedLower {
clearTick(pool, tickLower)
}
if flippedUpper {
clearTick(pool, tickUpper)
}
}
var delta BalanceDelta
if liquidityDelta.Sign() != 0 {
currentTick := pool.Tick
if currentTick < tickLower {
sqrtLower, sErr := GetSqrtPriceAtTick(tickLower)
if sErr != nil {
return ZeroBalanceDelta(), ZeroBalanceDelta(), fmt.Errorf("sqrt price lower: %w", sErr)
}
sqrtUpper, sErr := GetSqrtPriceAtTick(tickUpper)
if sErr != nil {
return ZeroBalanceDelta(), ZeroBalanceDelta(), fmt.Errorf("sqrt price upper: %w", sErr)
}
amount0, err := GetAmount0DeltaSigned(sqrtLower, sqrtUpper, liquidityDelta)
if err != nil {
return ZeroBalanceDelta(), ZeroBalanceDelta(),
fmt.Errorf("amount0 delta (below range): %w", err)
}
delta = NewBalanceDelta(amount0, big.NewInt(0))
} else if currentTick < tickUpper {
sqrtUpper, sErr := GetSqrtPriceAtTick(tickUpper)
if sErr != nil {
return ZeroBalanceDelta(), ZeroBalanceDelta(), fmt.Errorf("sqrt price upper: %w", sErr)
}
sqrtLower, sErr := GetSqrtPriceAtTick(tickLower)
if sErr != nil {
return ZeroBalanceDelta(), ZeroBalanceDelta(), fmt.Errorf("sqrt price lower: %w", sErr)
}
amount0, err := GetAmount0DeltaSigned(pool.SqrtPriceX96, sqrtUpper, liquidityDelta)
if err != nil {
return ZeroBalanceDelta(), ZeroBalanceDelta(),
fmt.Errorf("amount0 delta (in range): %w", err)
}
amount1, err := GetAmount1DeltaSigned(sqrtLower, pool.SqrtPriceX96, liquidityDelta)
if err != nil {
return ZeroBalanceDelta(), ZeroBalanceDelta(),
fmt.Errorf("amount1 delta (in range): %w", err)
}
delta = NewBalanceDelta(amount0, amount1)
newLiq, lErr := AddDelta(pool.Liquidity, liquidityDelta)
if lErr != nil {
return ZeroBalanceDelta(), ZeroBalanceDelta(),
fmt.Errorf("%w: %v", ErrInsufficientLiquidity, lErr)
}
pool.Liquidity = newLiq
} else {
sqrtLower, sErr := GetSqrtPriceAtTick(tickLower)
if sErr != nil {
return ZeroBalanceDelta(), ZeroBalanceDelta(), fmt.Errorf("sqrt price lower: %w", sErr)
}
sqrtUpper, sErr := GetSqrtPriceAtTick(tickUpper)
if sErr != nil {
return ZeroBalanceDelta(), ZeroBalanceDelta(), fmt.Errorf("sqrt price upper: %w", sErr)
}
amount1, err := GetAmount1DeltaSigned(sqrtLower, sqrtUpper, liquidityDelta)
if err != nil {
return ZeroBalanceDelta(), ZeroBalanceDelta(),
fmt.Errorf("amount1 delta (above range): %w", err)
}
delta = NewBalanceDelta(big.NewInt(0), amount1)
}
}
return delta, feeDelta, nil
}
// Donate distributes tokens to LPs via fee growth updates.
func (e *EngineCL) Donate(pool *PoolState, amount0, amount1 *big.Int) (BalanceDelta, error) {
if pool.Liquidity == nil || pool.Liquidity.Sign() <= 0 {
return ZeroBalanceDelta(), ErrNoLiquidity
}
if amount0 != nil && amount0.Sign() > 0 {
fg, err := SimpleMulDiv(amount0, Q128, pool.Liquidity)
if err != nil {
return ZeroBalanceDelta(), fmt.Errorf("donate fee growth0: %w", err)
}
pool.FeeGrowth0X128 = new(big.Int).Add(pool.FeeGrowth0X128, fg)
}
if amount1 != nil && amount1.Sign() > 0 {
fg, err := SimpleMulDiv(amount1, Q128, pool.Liquidity)
if err != nil {
return ZeroBalanceDelta(), fmt.Errorf("donate fee growth1: %w", err)
}
pool.FeeGrowth1X128 = new(big.Int).Add(pool.FeeGrowth1X128, fg)
}
return NewBalanceDelta(amount0, amount1), nil
}
// Quote estimates swap output without mutating state.
func (e *EngineCL) Quote(pool *Pool, amountIn *big.Int, zeroForOne bool) *big.Int {
if pool.Liquidity.Sign() == 0 || amountIn.Sign() <= 0 {
return big.NewInt(0)
}
// Use ComputeSwapStep for a single step (no tick crossing).
// V4 exactInput convention: amountRemaining is negative.
negAmountIn := new(big.Int).Neg(amountIn)
// Use the boundary sqrt price as target (MIN for zeroForOne, MAX for oneForZero).
var target *big.Int
if zeroForOne {
target = new(big.Int).Add(MinSqrtRatio, big.NewInt(1))
} else {
target = new(big.Int).Sub(MaxSqrtRatio, big.NewInt(1))
}
_, _, amountOut, _, err := ComputeSwapStep(
pool.SqrtPriceX96, target, pool.Liquidity, negAmountIn, 0, // fee=0 for estimation
)
if err != nil {
return big.NewInt(0)
}
return amountOut
}
// =========================================================================
// V4 Tick Helpers (engine-internal)
// =========================================================================
func updateTick(
pool *PoolState, tick int32, liquidityDelta *big.Int, upper bool,
) (flipped bool, liquidityGrossAfter *big.Int, err error) {
info := pool.getOrCreateTick(tick)
liquidityGrossBefore := new(big.Int).Set(info.LiquidityGross)
liquidityGrossAfter, err = AddDelta(liquidityGrossBefore, liquidityDelta)
if err != nil {
return false, nil, fmt.Errorf("tick %d liquidity: %w", tick, err)
}
flipped = (liquidityGrossAfter.Sign() == 0) != (liquidityGrossBefore.Sign() == 0)
if liquidityGrossBefore.Sign() == 0 {
if tick <= pool.Tick {
info.FeeGrowthOutside0X128 = new(big.Int).Set(pool.FeeGrowth0X128)
info.FeeGrowthOutside1X128 = new(big.Int).Set(pool.FeeGrowth1X128)
}
}
info.LiquidityGross = liquidityGrossAfter
if upper {
info.LiquidityNet = new(big.Int).Sub(info.LiquidityNet, liquidityDelta)
} else {
info.LiquidityNet = new(big.Int).Add(info.LiquidityNet, liquidityDelta)
}
return
}
func crossTick(
pool *PoolState, tick int32, feeGrowthGlobal0X128, feeGrowthGlobal1X128 *big.Int,
) *big.Int {
info := pool.getOrCreateTick(tick)
info.FeeGrowthOutside0X128 = new(big.Int).Sub(feeGrowthGlobal0X128, info.FeeGrowthOutside0X128)
info.FeeGrowthOutside1X128 = new(big.Int).Sub(feeGrowthGlobal1X128, info.FeeGrowthOutside1X128)
return new(big.Int).Set(info.LiquidityNet)
}
func clearTick(pool *PoolState, tick int32) {
delete(pool.Ticks, tick)
}
// wrapUint256 replicates Solidity uint256 wrapping: result mod 2^256.
var uint256Modulus = new(big.Int).Lsh(big.NewInt(1), 256) // 2^256
func wrapUint256(v *big.Int) *big.Int {
v.Mod(v, uint256Modulus)
return v
}
func getFeeGrowthInside(
pool *PoolState, tickLower, tickUpper int32,
) (feeGrowthInside0X128, feeGrowthInside1X128 *big.Int) {
lower := pool.getOrCreateTick(tickLower)
upper := pool.getOrCreateTick(tickUpper)
tickCurrent := pool.Tick
if tickCurrent < tickLower {
feeGrowthInside0X128 = wrapUint256(new(big.Int).Sub(lower.FeeGrowthOutside0X128, upper.FeeGrowthOutside0X128))
feeGrowthInside1X128 = wrapUint256(new(big.Int).Sub(lower.FeeGrowthOutside1X128, upper.FeeGrowthOutside1X128))
} else if tickCurrent >= tickUpper {
feeGrowthInside0X128 = wrapUint256(new(big.Int).Sub(upper.FeeGrowthOutside0X128, lower.FeeGrowthOutside0X128))
feeGrowthInside1X128 = wrapUint256(new(big.Int).Sub(upper.FeeGrowthOutside1X128, lower.FeeGrowthOutside1X128))
} else {
feeGrowthInside0X128 = wrapUint256(new(big.Int).Sub(
wrapUint256(new(big.Int).Sub(pool.FeeGrowth0X128, lower.FeeGrowthOutside0X128)),
upper.FeeGrowthOutside0X128,
))
feeGrowthInside1X128 = wrapUint256(new(big.Int).Sub(
wrapUint256(new(big.Int).Sub(pool.FeeGrowth1X128, lower.FeeGrowthOutside1X128)),
upper.FeeGrowthOutside1X128,
))
}
return
}
func updatePosition(
pool *PoolState, owner Address, params ModifyLiquidityParams,
feeGrowthInside0X128, feeGrowthInside1X128 *big.Int,
) (BalanceDelta, error) {
posKey := LPPositionKey(owner, params.TickLower, params.TickUpper, params.Salt)
pos, ok := pool.Positions[posKey]
if !ok {
pos = &LPPosition{
Owner: owner, TickLower: params.TickLower, TickUpper: params.TickUpper,
Liquidity: big.NewInt(0),
FeeGrowthInside0LastX128: big.NewInt(0), FeeGrowthInside1LastX128: big.NewInt(0),
TokensOwed0: big.NewInt(0), TokensOwed1: big.NewInt(0),
}
pool.Positions[posKey] = pos
}
var feesOwed0, feesOwed1 *big.Int
if pos.Liquidity.Sign() > 0 {
var err error
feesOwed0, err = SimpleMulDiv(wrapUint256(new(big.Int).Sub(feeGrowthInside0X128, pos.FeeGrowthInside0LastX128)), pos.Liquidity, Q128)
if err != nil {
return ZeroBalanceDelta(), fmt.Errorf("fee0 calc: %w", err)
}
feesOwed1, err = SimpleMulDiv(wrapUint256(new(big.Int).Sub(feeGrowthInside1X128, pos.FeeGrowthInside1LastX128)), pos.Liquidity, Q128)
if err != nil {
return ZeroBalanceDelta(), fmt.Errorf("fee1 calc: %w", err)
}
} else {
feesOwed0 = big.NewInt(0)
feesOwed1 = big.NewInt(0)
}
pos.FeeGrowthInside0LastX128 = new(big.Int).Set(feeGrowthInside0X128)
pos.FeeGrowthInside1LastX128 = new(big.Int).Set(feeGrowthInside1X128)
if params.LiquidityDelta.Sign() != 0 {
newLiq := new(big.Int).Add(pos.Liquidity, params.LiquidityDelta)
if newLiq.Sign() < 0 {
newLiq = big.NewInt(0)
}
pos.Liquidity = newLiq
}
pos.Owner = owner
pos.TickLower = params.TickLower
pos.TickUpper = params.TickUpper
return NewBalanceDelta(feesOwed0, feesOwed1), nil
}
func tickSpacingToMaxLiquidityPerTick(tickSpacing int32) *big.Int {
minTick := MinTick / tickSpacing
if MinTick%tickSpacing != 0 {
minTick--
}
maxTick := MaxTick / tickSpacing
numTicks := maxTick - minTick + 1
return new(big.Int).Div(uint128Max, big.NewInt(int64(numTicks)))
}