Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,32 @@ const result = calculator.computeFixedAllocation(
// result.token0Reserve, result.token1Reserve are the two amounts to use
```

### `computeSingleSidedRange(spotPrice, reserveForToken, reserve)`

Builds a **single-sided** price range from the spot price and a single reserve: only one token is deposited (the opposite reserve is zero) and the spot price sits exactly on one bound. Which bound the spot sits on follows from the deposited token, in sqrt(token1/token0) terms:

- depositing **token0**: spot = **min** bound;
- depositing **token1**: spot = **max** bound.

The opposite bound is derived from the other token's `maxAvailableLiquidity`: it is the price at which the deposited reserve has fully converted into exactly that amount. This is the range-order relation `sqrtPmin * sqrtPmax = amountGt / amountLt` (in 1e18 fixed-point) — the geometric mean of the bounds is the average execution price across the range. Consequently the opposite token's `maxAvailableLiquidity` must exceed the spot-equivalent value of the deposit, otherwise the range would be empty or inverted and the method throws.

Returns a `PriceAllocationRange` (`minPrice`, `spotPrice`, `maxPrice` as `Price` objects), ready for the allocation methods.

**Use case**: “I only have 1,000,000 USDC and want to end up with at most 800 WETH; give me the range that does that starting from the current spot.”

```ts
// calculator tokens: USDC (maxAvailableLiquidity 1_000_000e6), WETH (maxAvailableLiquidity 800e18)
const range = calculator.computeSingleSidedRange(
spotPrice, // Price: current spot (e.g. 2500 USDC per WETH), sits on one bound
usdcAddress, // token being deposited
parseUnits('1000000', 6),
)
// range.minPrice === spotPrice (token0 deposit)
// range.maxPrice — derived far bound: 625 USDC per WETH here, so the
// average execution across the range is sqrt(2500 * 625) = 1250 USDC per WETH
// and the 1,000,000 USDC fully converts into exactly 800 WETH at the far bound
```

### `computeSpotPrice(token0Balance, token1Balance, scaledPriceBounds)`

Takes **raw** amounts of token0 and token1 (pool order: lower address = token0) and a **`ScaledPriceBounds`** range. Converts the bounds the same way as allocation methods, then computes the **implied** spot `sqrt(P_spot)` in **1e18** fixed-point (same units as `ConcentratedLiquidityInfo.sqrtPriceSpot`).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1

import { describe, expect, it } from 'vitest'
import { Address } from '@1inch/sdk-core'
import { UINT_256_MAX } from '@1inch/byte-utils'
import { ConcentrateLiquidityCalculator } from './concentrate-liquidity-calculator'
import { Price } from '../price'
import type { PricePair, PriceToken } from '../price/types'
import {
computeBalances,
computeLiquidityFromAmounts,
} from '../concentrate-liquidity-math/concentrate-liquidity-math'

const USDC = new Address('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48')
const WETH = new Address('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')
/** Extra token for negative tests (not USDC/WETH). */
const DAI = new Address('0x6B175474E89094C44Da98b954EedeAC495271d0F')

const USDC_TOKEN: PriceToken = { address: USDC, decimals: 6n }
const WETH_TOKEN: PriceToken = { address: WETH, decimals: 18n }
const DAI_TOKEN: PriceToken = { address: DAI, decimals: 18n }

const pairUsdcQuoteWethBase: PricePair = { quoteToken: USDC_TOKEN, baseToken: WETH_TOKEN }

const maxUsdc = 1_000_000n * 10n ** 6n
const maxWeth = 800n * 10n ** 18n

/** USDC has the lower address, so it is token0 and WETH is token1. */
const calculator = ConcentrateLiquidityCalculator.new({
tokenA: { address: USDC, decimals: 6n, maxAvailableLiquidity: maxUsdc },
tokenB: { address: WETH, decimals: 18n, maxAvailableLiquidity: maxWeth },
})

describe('ConcentrateLiquidityCalculator', () => {
describe('computeSingleSidedRange', () => {
const spot = Price.fromHuman('2500', pairUsdcQuoteWethBase)

describe('token0 (USDC) deposit', () => {
/** 1,000,000 USDC at 2500 is worth 400 WETH; target is maxWeth = 800 WETH. */
const reserve = 1_000_000n * 10n ** 6n

it('should place spot at the min bound and derive the max bound', () => {
const range = calculator.computeSingleSidedRange(spot, USDC, reserve)

expect(range.minPrice).toBe(spot)
expect(range.spotPrice).toBe(spot)
/**
* Range-order relation: sqrtPmax = targetGt * 1e18^2 / (reserve * sqrtPspot).
* Converting 1,000,000 USDC into 800 WETH means an average execution of
* 1250 USDC/WETH = sqrt(2500 * 625), so the far bound is 625 USDC per WETH.
*/
expect(range.maxPrice.toSqrt()).toBe(2n * spot.toSqrt())
expect(range.maxPrice.toHuman(USDC)).toBe('625')
})

it('should fully convert the reserve into token1 maxAvailableLiquidity at the far bound', () => {
const range = calculator.computeSingleSidedRange(spot, USDC, reserve)

const { targetL } = computeLiquidityFromAmounts(
reserve,
UINT_256_MAX,
range.spotPrice.toSqrt(),
range.minPrice.toSqrt(),
range.maxPrice.toSqrt(),
)
/** Same liquidity, spot moved to the far bound: all token0 sold for token1. */
const atFarBound = computeBalances(
targetL,
range.maxPrice.toSqrt(),
range.minPrice.toSqrt(),
range.maxPrice.toSqrt(),
)

expect(atFarBound.bLt).toBe(0n)
expect(atFarBound.bGt).toBe(maxWeth)
})

it('should produce a range accepted by computeFixedAllocation', () => {
const range = calculator.computeSingleSidedRange(spot, USDC, reserve)

const allocation = calculator.computeFixedAllocation(range, USDC, reserve)

/** Only token0 is required; the fixed side may lose a few wei to integer math. */
expect(allocation.token1Reserve).toBe(0n)
expect(allocation.token0Reserve).toBeLessThanOrEqual(reserve)
expect(allocation.token0Reserve).toBeGreaterThan((reserve * 999_999n) / 1_000_000n)
})
})

describe('token1 (WETH) deposit', () => {
/** 200 WETH at 2500 is worth 500,000 USDC; target is maxUsdc = 1,000,000 USDC. */
const reserve = 200n * 10n ** 18n

it('should place spot at the max bound and derive the min bound', () => {
const range = calculator.computeSingleSidedRange(spot, WETH, reserve)

expect(range.maxPrice).toBe(spot)
expect(range.spotPrice).toBe(spot)
/**
* Range-order relation: sqrtPmin = reserve * 1e18^2 / (targetLt * sqrtPspot).
* Converting 200 WETH into 1,000,000 USDC means an average execution of
* 5000 USDC/WETH = sqrt(2500 * 10000), so the far bound is 10000 USDC per WETH.
*/
expect(range.minPrice.toSqrt()).toBe(spot.toSqrt() / 2n)
expect(range.minPrice.toHuman(USDC)).toBe('10000')
})

it('should fully convert the reserve into token0 maxAvailableLiquidity at the far bound', () => {
const range = calculator.computeSingleSidedRange(spot, WETH, reserve)

const { targetL } = computeLiquidityFromAmounts(
UINT_256_MAX,
reserve,
range.spotPrice.toSqrt(),
range.minPrice.toSqrt(),
range.maxPrice.toSqrt(),
)
/** Same liquidity, spot moved to the far bound: all token1 sold for token0. */
const atFarBound = computeBalances(
targetL,
range.minPrice.toSqrt(),
range.minPrice.toSqrt(),
range.maxPrice.toSqrt(),
)

expect(atFarBound.bLt).toBe(maxUsdc)
expect(atFarBound.bGt).toBe(0n)
})

it('should produce a range accepted by computeFixedAllocation', () => {
const range = calculator.computeSingleSidedRange(spot, WETH, reserve)

const allocation = calculator.computeFixedAllocation(range, WETH, reserve)

expect(allocation.token0Reserve).toBe(0n)
expect(allocation.token1Reserve).toBeLessThanOrEqual(reserve)
expect(allocation.token1Reserve).toBeGreaterThan((reserve * 999_999n) / 1_000_000n)
})
})

describe('validation', () => {
it('should throw when the reserve is zero', () => {
expect(() => calculator.computeSingleSidedRange(spot, USDC, 0n)).toThrow(
'reserve must be positive',
)
})

it('should throw when the reserve token is not in the pair', () => {
expect(() => calculator.computeSingleSidedRange(spot, DAI, 1n)).toThrow(
'reserve should be in some pair token',
)
})

it('should throw when the spot price is for a different pair', () => {
const pairUsdcQuoteDaiBase: PricePair = { quoteToken: USDC_TOKEN, baseToken: DAI_TOKEN }
const foreignSpot = Price.fromHuman('1', pairUsdcQuoteDaiBase)

expect(() => calculator.computeSingleSidedRange(foreignSpot, USDC, 1n)).toThrow(
'prices should be for the calculator token pair',
)
})

it('should throw when token1 maxAvailableLiquidity does not exceed the deposit spot value', () => {
/** 1,000,000 USDC at 2500 is worth exactly 400 WETH — no room for a range. */
const smallTarget = ConcentrateLiquidityCalculator.new({
tokenA: { address: USDC, decimals: 6n, maxAvailableLiquidity: maxUsdc },
tokenB: { address: WETH, decimals: 18n, maxAvailableLiquidity: 400n * 10n ** 18n },
})

expect(() =>
smallTarget.computeSingleSidedRange(spot, USDC, 1_000_000n * 10n ** 6n),
).toThrow('token1 maxAvailableLiquidity should exceed the spot value of the deposit')
})

it('should throw when token0 maxAvailableLiquidity does not exceed the deposit spot value', () => {
/** 400 WETH at 2500 is worth exactly 1,000,000 USDC — no room for a range. */
expect(() => calculator.computeSingleSidedRange(spot, WETH, 400n * 10n ** 18n)).toThrow(
'token0 maxAvailableLiquidity should exceed the spot value of the deposit',
)
})
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import type {
PriceAllocationRange,
PriceBounds,
} from './types'
import { Price } from '../price'
import { ONE_E18 } from '../concentrate-grow-liquidity-2d-args'
import {
computeLiquidityAndPrice,
computeLiquidityFromAmounts,
Expand Down Expand Up @@ -84,6 +86,77 @@ export class ConcentrateLiquidityCalculator {
}
}

/**
* Build a single-sided price range from a spot price and a single reserve.
*
* The spot price sits exactly on one bound (the position holds only the provided
* token, the opposite reserve is zero):
* - depositing token0: spot = min bound (in sqrt(token1/token0) terms);
* - depositing token1: spot = max bound.
*
* The opposite bound is derived from the other token's `maxAvailableLiquidity`:
* it is the price at which the deposited reserve has fully converted into exactly
* that amount. This follows from the range-order relation
* `sqrtPmin * sqrtPmax / 1e18^2 = amountGt / amountLt` (the geometric mean of the
* bounds is the average execution price across the range), so the opposite token's
* `maxAvailableLiquidity` must exceed the spot-equivalent value of the deposit.
*
* @param spotPrice Current spot price; sits exactly on one bound of the range
* @param reserveForToken Token being deposited (must be one of the pair tokens)
* @param reserve Raw amount of `reserveForToken` to deposit (must be positive)
* @returns Price range with the spot on one bound and the derived opposite bound
*/
computeSingleSidedRange(
spotPrice: Price,
reserveForToken: Address,
reserve: bigint,
): PriceAllocationRange {
assert(reserve > 0n, 'reserve must be positive')
assert(
spotPrice.token0.address.equal(this.token0.address) &&
spotPrice.token1.address.equal(this.token1.address),
'prices should be for the calculator token pair',
)
assert(
reserveForToken.equal(this.token0.address) || reserveForToken.equal(this.token1.address),
'reserve should be in some pair token',
)

const isReserveLt = reserveForToken.equal(this.token0.address)
const sqrtPspot = spotPrice.toSqrt()
const pair = { tokenA: spotPrice.token0, tokenB: spotPrice.token1 }

if (isReserveLt) {
const targetGt = this.token1.maxAvailableLiquidity
const sqrtPmax = (targetGt * ONE_E18 * ONE_E18) / (reserve * sqrtPspot)
assert(
sqrtPmax > sqrtPspot,
'token1 maxAvailableLiquidity should exceed the spot value of the deposit',
)

return {
minPrice: spotPrice,
spotPrice,
maxPrice: Price.fromSqrt(sqrtPmax, pair),
}
}

const targetLt = this.token0.maxAvailableLiquidity
assert(targetLt > 0n, 'token0 maxAvailableLiquidity must be positive')

const sqrtPmin = (reserve * ONE_E18 * ONE_E18) / (targetLt * sqrtPspot)
assert(
sqrtPmin > 0n && sqrtPmin < sqrtPspot,
'token0 maxAvailableLiquidity should exceed the spot value of the deposit',
)

return {
minPrice: Price.fromSqrt(sqrtPmin, pair),
spotPrice,
maxPrice: spotPrice,
}
}

computeSpotPrice(reserves: ConcentratedLiquidityInfo, bounds: PriceBounds): bigint {
assert(bounds.maxPrice.gte(bounds.minPrice), 'maxPrice should be >= minPrice')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export { PriceRange } from './price-range'
export type { PriceRangeJSON } from './price-range'
export { TokenReserve } from './token-reserve'
export type { TokenReserveArgs, TokenReserveJSON } from './token-reserve'
export { ConcentrateLiquidityCalculator } from './concentrate-liquidity-calculator/concentrate-liquidity-calculator'
export type {
ConcentrateTokenInfo,
ConcentrateLiquidityCalculatorArgs,
Expand Down
Loading