diff --git a/src/dex/index.ts b/src/dex/index.ts index f623e50e6..19b06bed8 100644 --- a/src/dex/index.ts +++ b/src/dex/index.ts @@ -81,6 +81,7 @@ import { EtherFi } from './etherfi'; import { Spark } from './spark/spark'; import { VelodromeSlipstream } from './uniswap-v3/forks/velodrome-slipstream/velodrome-slipstream'; import { AaveV3Stata } from './aave-v3-stata/aave-v3-stata'; +import { SquadswapV3 } from './squadswap-v3/squadswap-v3'; const LegacyDexes = [ CurveV2, @@ -157,6 +158,7 @@ const Dexes = [ PharaohV1, Spark, AaveV3Stata, + SquadswapV3, ]; export type LegacyDexConstructor = new (dexHelper: IDexHelper) => IDexTxBuilder< diff --git a/src/dex/squadswap-v3/config.ts b/src/dex/squadswap-v3/config.ts new file mode 100644 index 000000000..d17737704 --- /dev/null +++ b/src/dex/squadswap-v3/config.ts @@ -0,0 +1,34 @@ +import { DexParams } from '../uniswap-v3/types'; +import { DexConfigMap, AdapterMappings } from '../../types'; +import { Network, SwapSide } from '../../constants'; + +const SQUAD_SUPPORTED_FEES = [10000n, 2500n, 500n, 100n]; + +export const SquadswapV3Config: DexConfigMap = { + SquadswapV3: { + [Network.BSC]: { + factory: '0x009c4ef7C0e0Dd6bd1ea28417c01Ea16341367c3', + deployer: '0x38e09D9444B41CFda398DD31eb2713Ca5c3B75eA', + quoter: '0x81Da0D4e1157391a22a656ad84AAb9b2716F21e0', + router: '0xAf4b332ddBa499B6116235a095CEE2f2030BCBC0', + supportedFees: SQUAD_SUPPORTED_FEES, + stateMulticall: '0x9DAd2ED7ADc6eaacf81589Cd043579c9684E5C81', + uniswapMulticall: '0xac1cE734566f390A94b00eb9bf561c2625BF44ea', + chunksCount: 10, + initRetryFrequency: 30, + initHash: + '0xf08a35894b6b71b07d95a23022375630f6cee63a27d724c703617c17c4fc387d', + subgraphURL: + 'https://api.studio.thegraph.com/query/59394/test-pcs-uni/v0.0.8', + }, + }, +}; + +export const Adapters: Record = { + // TODO: add adapters for each chain + // This is an example to copy + [Network.BSC]: { + [SwapSide.SELL]: [{ name: 'BscAdapter02', index: 4 }], + [SwapSide.BUY]: [{ name: 'BscBuyAdapter', index: 5 }], + }, +}; diff --git a/src/dex/squadswap-v3/constants.ts b/src/dex/squadswap-v3/constants.ts new file mode 100644 index 000000000..b28bc7144 --- /dev/null +++ b/src/dex/squadswap-v3/constants.ts @@ -0,0 +1,36 @@ +export const SQUADSWAPV3_TICK_GAS_COST = 24_000; // Ceiled +export const SQUADSWAPV3_TICK_BASE_OVERHEAD = 75_000; +export const SQUADSWAPV3_POOL_SEARCH_OVERHEAD = 10_000; + +// This is used for price calculation. If out of scope, return 0n +export const TICK_BITMAP_TO_USE = 4n; + +// This is used to check if the state is still valid. +export const TICK_BITMAP_BUFFER = 8n; + +export const MAX_PRICING_COMPUTATION_STEPS_ALLOWED = 128; + +export const SQUADSWAPV3_SUBGRAPH_URL = + 'https://api.studio.thegraph.com/query/59394/test-pcs-uni/v0.0.8'; + +export const SQUADSWAPV3_EFFICIENCY_FACTOR = 3; + +export const ZERO_TICK_INFO = { + liquidityGross: 0n, + liquidityNet: 0n, + tickCumulativeOutside: 0n, + secondsPerLiquidityOutsideX128: 0n, + secondsOutside: 0n, + initialized: false, +}; + +export const ZERO_ORACLE_OBSERVATION = { + blockTimestamp: 0n, + tickCumulative: 0n, + secondsPerLiquidityCumulativeX128: 0n, + initialized: false, +}; + +export const OUT_OF_RANGE_ERROR_POSTFIX = `INVALID_TICK_BIT_MAP_RANGES`; + +export const DEFAULT_POOL_INIT_CODE_HASH = `0xf08a35894b6b71b07d95a23022375630f6cee63a27d724c703617c17c4fc387d`; diff --git a/src/dex/squadswap-v3/contract-math/BitMath.ts b/src/dex/squadswap-v3/contract-math/BitMath.ts new file mode 100644 index 000000000..d7a2e6d9a --- /dev/null +++ b/src/dex/squadswap-v3/contract-math/BitMath.ts @@ -0,0 +1,90 @@ +import { + BI_MAX_UINT128, + BI_MAX_UINT16, + BI_MAX_UINT32, + BI_MAX_UINT64, + BI_MAX_UINT8, +} from '../../../bigint-constants'; +import { _require } from '../../../utils'; + +export class BitMath { + static mostSignificantBit(x: bigint): bigint { + _require(x > 0, '', { x }, 'x > 0'); + let r = 0n; + + if (x >= 0x100000000000000000000000000000000n) { + x >>= 128n; + r += 128n; + } + if (x >= 0x10000000000000000n) { + x >>= 64n; + r += 64n; + } + if (x >= 0x100000000n) { + x >>= 32n; + r += 32n; + } + if (x >= 0x10000n) { + x >>= 16n; + r += 16n; + } + if (x >= 0x100n) { + x >>= 8n; + r += 8n; + } + if (x >= 0x10n) { + x >>= 4n; + r += 4n; + } + if (x >= 0x4n) { + x >>= 2n; + r += 2n; + } + if (x >= 0x2n) r += 1n; + + return r; + } + + static leastSignificantBit(x: bigint): bigint { + _require(x > 0, '', { x }, 'x > 0'); + + let r = 255n; + if ((x & BI_MAX_UINT128) > 0n) { + r -= 128n; + } else { + x >>= 128n; + } + if ((x & BI_MAX_UINT64) > 0n) { + r -= 64n; + } else { + x >>= 64n; + } + if ((x & BI_MAX_UINT32) > 0n) { + r -= 32n; + } else { + x >>= 32n; + } + if ((x & BI_MAX_UINT16) > 0n) { + r -= 16n; + } else { + x >>= 16n; + } + if ((x & BI_MAX_UINT8) > 0n) { + r -= 8n; + } else { + x >>= 8n; + } + if ((x & 0xfn) > 0n) { + r -= 4n; + } else { + x >>= 4n; + } + if ((x & 0x3n) > 0n) { + r -= 2n; + } else { + x >>= 2n; + } + if ((x & 0x1n) > 0n) r -= 1n; + return r; + } +} diff --git a/src/dex/squadswap-v3/contract-math/FixedPoint128.ts b/src/dex/squadswap-v3/contract-math/FixedPoint128.ts new file mode 100644 index 000000000..2058307bd --- /dev/null +++ b/src/dex/squadswap-v3/contract-math/FixedPoint128.ts @@ -0,0 +1,3 @@ +export class FixedPoint128 { + static readonly Q128 = 0x100000000000000000000000000000000n; +} diff --git a/src/dex/squadswap-v3/contract-math/FixedPoint96.ts b/src/dex/squadswap-v3/contract-math/FixedPoint96.ts new file mode 100644 index 000000000..1a551dcb9 --- /dev/null +++ b/src/dex/squadswap-v3/contract-math/FixedPoint96.ts @@ -0,0 +1,4 @@ +export class FixedPoint96 { + static readonly RESOLUTION = 96n; + static readonly Q96 = 0x1000000000000000000000000n; +} diff --git a/src/dex/squadswap-v3/contract-math/FullMath.ts b/src/dex/squadswap-v3/contract-math/FullMath.ts new file mode 100644 index 000000000..7c6a3bdc3 --- /dev/null +++ b/src/dex/squadswap-v3/contract-math/FullMath.ts @@ -0,0 +1,30 @@ +import { BI_MAX_UINT256 } from '../../../bigint-constants'; +import { _require } from '../../../utils'; + +export class FullMath { + static mulDiv(a: bigint, b: bigint, denominator: bigint) { + const result = (a * b) / denominator; + + _require( + result <= BI_MAX_UINT256, + '', + { result, BI_MAX_UINT: BI_MAX_UINT256 }, + 'result <= BI_MAX_UINT', + ); + + return result; + } + + static mulDivRoundingUp(a: bigint, b: bigint, denominator: bigint) { + const result = (a * b + denominator - 1n) / denominator; + + _require( + result <= BI_MAX_UINT256, + '', + { result, BI_MAX_UINT: BI_MAX_UINT256 }, + 'result <= BI_MAX_UINT', + ); + + return result; + } +} diff --git a/src/dex/squadswap-v3/contract-math/LiquidityMath.ts b/src/dex/squadswap-v3/contract-math/LiquidityMath.ts new file mode 100644 index 000000000..a495e55cc --- /dev/null +++ b/src/dex/squadswap-v3/contract-math/LiquidityMath.ts @@ -0,0 +1,17 @@ +import { _require } from '../../../utils'; + +export class LiquidityMath { + static addDelta(x: bigint, y: bigint): bigint { + let z; + if (y < 0) { + const _y = BigInt.asUintN(128, -y); + z = x - _y; + _require(z < x, 'LS', { z, x, y, _y }, 'z < x'); + } else { + const _y = BigInt.asUintN(128, y); + z = x + _y; + _require(z >= x, 'LA', { z, x, y, _y }, 'z >= x'); + } + return z; + } +} diff --git a/src/dex/squadswap-v3/contract-math/Oracle.ts b/src/dex/squadswap-v3/contract-math/Oracle.ts new file mode 100644 index 000000000..059e84e28 --- /dev/null +++ b/src/dex/squadswap-v3/contract-math/Oracle.ts @@ -0,0 +1,226 @@ +import { + OracleObservation, + OracleObservationCandidates, + PoolState, +} from '../../uniswap-v3/types'; +import { _require } from '../../../utils'; +import { DeepReadonly } from 'ts-essentials'; +import { ZERO_ORACLE_OBSERVATION } from '../constants'; + +function replaceUndefinedObservationWithZero(state: PoolState, index: number) { + if (state.observations[index] === undefined) { + state.observations[index] = { ...ZERO_ORACLE_OBSERVATION }; + } +} + +export class Oracle { + static transform( + state: DeepReadonly, + last: OracleObservation, + blockTimestamp: bigint, + tick: bigint, + liquidity: bigint, + ): OracleObservation { + const delta = blockTimestamp - last.blockTimestamp; + return { + blockTimestamp: state.blockTimestamp, + tickCumulative: last.tickCumulative + BigInt.asIntN(56, tick) * delta, + secondsPerLiquidityCumulativeX128: + last.secondsPerLiquidityCumulativeX128 + + (BigInt.asUintN(160, delta) << 128n) / + (liquidity > 0n ? liquidity : 1n), + initialized: true, + }; + } + + static write( + state: PoolState, + index: number, + blockTimestamp: bigint, + tick: bigint, + liquidity: bigint, + cardinality: number, + cardinalityNext: number, + ): [number, number] { + const last = state.observations[index]; + + if (last.blockTimestamp == state.blockTimestamp) + return [index, cardinality]; + + let indexUpdated = 0; + let cardinalityUpdated = 0; + + if (cardinalityNext > cardinality && index == cardinality - 1) { + cardinalityUpdated = cardinalityNext; + } else { + cardinalityUpdated = cardinality; + } + + indexUpdated = (index + 1) % cardinalityUpdated; + + state.observations[indexUpdated] = Oracle.transform( + state, + last, + blockTimestamp, + tick, + liquidity, + ); + if (indexUpdated !== index) { + delete state.observations[index]; + } + return [indexUpdated, cardinalityUpdated]; + } + + static lte(time: bigint, a: bigint, b: bigint): boolean { + if (a <= time && b <= time) return a <= b; + + const aAdjusted = a > time ? a : a + 2n ** 32n; + const bAdjusted = b > time ? b : b + 2n ** 32n; + return aAdjusted <= bAdjusted; + } + + static binarySearch( + state: DeepReadonly, + time: bigint, + target: bigint, + index: number, + cardinality: number, + ): OracleObservationCandidates { + let l = (index + 1) % cardinality; + let r = l + cardinality - 1; + let i; + + let beforeOrAt; + let atOrAfter; + while (true) { + i = (l + r) / 2; + + beforeOrAt = state.observations[i % cardinality]; + + // we've landed on an uninitialized tick, keep searching higher (more recently) + if (!beforeOrAt.initialized) { + l = i + 1; + continue; + } + + atOrAfter = state.observations[(i + 1) % cardinality]; + + const targetAtOrAfter = Oracle.lte( + time, + beforeOrAt.blockTimestamp, + target, + ); + + // check if we've found the answer! + if (targetAtOrAfter && Oracle.lte(time, target, atOrAfter.blockTimestamp)) + break; + + if (!targetAtOrAfter) r = i - 1; + else l = i + 1; + } + return { beforeOrAt, atOrAfter }; + } + + static getSurroundingObservations( + state: DeepReadonly, + time: bigint, + target: bigint, + tick: bigint, + index: number, + liquidity: bigint, + cardinality: number, + ): OracleObservationCandidates { + let beforeOrAt = state.observations[index]; + + if (Oracle.lte(time, beforeOrAt.blockTimestamp, target)) { + if (beforeOrAt.blockTimestamp === target) { + return { beforeOrAt, atOrAfter: beforeOrAt }; + } else { + return { + beforeOrAt, + atOrAfter: Oracle.transform( + state, + beforeOrAt, + target, + tick, + liquidity, + ), + }; + } + } + + beforeOrAt = state.observations[(index + 1) % cardinality]; + if (!beforeOrAt.initialized) beforeOrAt = state.observations[0]; + + _require( + Oracle.lte(time, beforeOrAt.blockTimestamp, target), + 'OLD', + { time, beforeOrAtBlockTimestamp: beforeOrAt.blockTimestamp, target }, + 'Oracle.lte(time, beforeOrAt.blockTimestamp, target)', + ); + + return Oracle.binarySearch(state, time, target, index, cardinality); + } + + static observeSingle( + state: DeepReadonly, + time: bigint, + secondsAgo: bigint, + tick: bigint, + index: number, + liquidity: bigint, + cardinality: number, + ): [bigint, bigint] { + if (secondsAgo == 0n) { + let last = state.observations[index]; + if (last.blockTimestamp != time) + last = Oracle.transform(state, last, time, tick, liquidity); + return [last.tickCumulative, last.secondsPerLiquidityCumulativeX128]; + } + + const target = time - secondsAgo; + + const { beforeOrAt, atOrAfter } = Oracle.getSurroundingObservations( + state, + time, + target, + tick, + index, + liquidity, + cardinality, + ); + + if (target === beforeOrAt.blockTimestamp) { + return [ + beforeOrAt.tickCumulative, + beforeOrAt.secondsPerLiquidityCumulativeX128, + ]; + } else if (target === atOrAfter.blockTimestamp) { + return [ + atOrAfter.tickCumulative, + atOrAfter.secondsPerLiquidityCumulativeX128, + ]; + } else { + const observationTimeDelta = + atOrAfter.blockTimestamp - beforeOrAt.blockTimestamp; + const targetDelta = target - beforeOrAt.blockTimestamp; + return [ + beforeOrAt.tickCumulative + + ((atOrAfter.tickCumulative - beforeOrAt.tickCumulative) / + observationTimeDelta) * + targetDelta, + beforeOrAt.secondsPerLiquidityCumulativeX128 + + BigInt.asUintN( + 160, + (BigInt.asUintN( + 256, + atOrAfter.secondsPerLiquidityCumulativeX128 - + beforeOrAt.secondsPerLiquidityCumulativeX128, + ) * + targetDelta) / + observationTimeDelta, + ), + ]; + } + } +} diff --git a/src/dex/squadswap-v3/contract-math/SqrtPriceMath.ts b/src/dex/squadswap-v3/contract-math/SqrtPriceMath.ts new file mode 100644 index 000000000..31b801d73 --- /dev/null +++ b/src/dex/squadswap-v3/contract-math/SqrtPriceMath.ts @@ -0,0 +1,226 @@ +import { BI_MAX_UINT160 } from '../../../bigint-constants'; +import { FixedPoint96 } from './FixedPoint96'; +import { FullMath } from './FullMath'; +import { UnsafeMath } from './UnsafeMath'; +import { _require } from '../../../utils'; + +export class SqrtPriceMath { + static getNextSqrtPriceFromAmount0RoundingUp( + sqrtPX96: bigint, + liquidity: bigint, + amount: bigint, + add: boolean, + ): bigint { + if (amount === 0n) return sqrtPX96; + const numerator1 = + BigInt.asUintN(256, liquidity) << FixedPoint96.RESOLUTION; + + const product = amount * sqrtPX96; + if (add) { + if (product / amount === sqrtPX96) { + const denominator = numerator1 + product; + if (denominator >= numerator1) { + return BigInt.asUintN( + 160, + FullMath.mulDivRoundingUp(numerator1, sqrtPX96, denominator), + ); + } + } + return BigInt.asUintN( + 160, + UnsafeMath.divRoundingUp(numerator1, numerator1 / sqrtPX96 + amount), + ); + } else { + _require( + product / amount === sqrtPX96 && numerator1 > product, + '', + { product, amount, sqrtPX96, numerator1 }, + 'product / amount === sqrtPX96 && numerator1 > product', + ); + const denominator = numerator1 - product; + return BigInt.asUintN( + 160, + FullMath.mulDivRoundingUp(numerator1, sqrtPX96, denominator), + ); + } + } + + static getNextSqrtPriceFromAmount1RoundingDown( + sqrtPX96: bigint, + liquidity: bigint, + amount: bigint, + add: boolean, + ): bigint { + if (add) { + const quotient = + amount <= BI_MAX_UINT160 + ? (amount << FixedPoint96.RESOLUTION) / liquidity + : FullMath.mulDiv(amount, FixedPoint96.Q96, liquidity); + return BigInt.asUintN(160, BigInt.asUintN(256, sqrtPX96) + quotient); + } else { + const quotient = + amount <= BI_MAX_UINT160 + ? UnsafeMath.divRoundingUp( + amount << FixedPoint96.RESOLUTION, + liquidity, + ) + : FullMath.mulDivRoundingUp(amount, FixedPoint96.Q96, liquidity); + + _require( + sqrtPX96 > quotient, + '', + { sqrtPX96, quotient }, + 'sqrtPX96 > quotient', + ); + return BigInt.asUintN(160, sqrtPX96 - quotient); + } + } + + static getNextSqrtPriceFromInput( + sqrtPX96: bigint, + liquidity: bigint, + amountIn: bigint, + zeroForOne: boolean, + ): bigint { + _require(sqrtPX96 > 0n, '', { sqrtPX96 }, 'sqrtPX96 > 0n'); + _require(liquidity > 0n, '', { liquidity }, 'liquidity > 0n'); + + return zeroForOne + ? SqrtPriceMath.getNextSqrtPriceFromAmount0RoundingUp( + sqrtPX96, + liquidity, + amountIn, + true, + ) + : SqrtPriceMath.getNextSqrtPriceFromAmount1RoundingDown( + sqrtPX96, + liquidity, + amountIn, + true, + ); + } + + static getNextSqrtPriceFromOutput( + sqrtPX96: bigint, + liquidity: bigint, + amountOut: bigint, + zeroForOne: boolean, + ): bigint { + _require(sqrtPX96 > 0n, '', { sqrtPX96 }, 'sqrtPX96 > 0n'); + _require(liquidity > 0n, '', { liquidity }, 'liquidity > 0n'); + + return zeroForOne + ? SqrtPriceMath.getNextSqrtPriceFromAmount1RoundingDown( + sqrtPX96, + liquidity, + amountOut, + false, + ) + : SqrtPriceMath.getNextSqrtPriceFromAmount0RoundingUp( + sqrtPX96, + liquidity, + amountOut, + false, + ); + } + + static getAmount0Delta( + sqrtRatioAX96: bigint, + sqrtRatioBX96: bigint, + liquidity: bigint, + roundUp: boolean, + ) { + if (sqrtRatioAX96 > sqrtRatioBX96) { + [sqrtRatioAX96, sqrtRatioBX96] = [sqrtRatioBX96, sqrtRatioAX96]; + } + + const numerator1 = + BigInt.asUintN(256, liquidity) << FixedPoint96.RESOLUTION; + const numerator2 = sqrtRatioBX96 - sqrtRatioAX96; + + _require(sqrtRatioAX96 > 0, '', { sqrtRatioAX96 }, 'sqrtRatioAX96 > 0'); + + return roundUp + ? UnsafeMath.divRoundingUp( + FullMath.mulDivRoundingUp(numerator1, numerator2, sqrtRatioBX96), + sqrtRatioAX96, + ) + : FullMath.mulDiv(numerator1, numerator2, sqrtRatioBX96) / sqrtRatioAX96; + } + + static getAmount1Delta( + sqrtRatioAX96: bigint, + sqrtRatioBX96: bigint, + liquidity: bigint, + roundUp: boolean, + ) { + if (sqrtRatioAX96 > sqrtRatioBX96) + [sqrtRatioAX96, sqrtRatioBX96] = [sqrtRatioBX96, sqrtRatioAX96]; + + return roundUp + ? FullMath.mulDivRoundingUp( + liquidity, + sqrtRatioBX96 - sqrtRatioAX96, + FixedPoint96.Q96, + ) + : FullMath.mulDiv( + liquidity, + sqrtRatioBX96 - sqrtRatioAX96, + FixedPoint96.Q96, + ); + } + + // Overloaded with different argument numbers + static _getAmount0DeltaO( + sqrtRatioAX96: bigint, + sqrtRatioBX96: bigint, + liquidity: bigint, + ) { + return liquidity < 0 + ? -BigInt.asIntN( + 256, + SqrtPriceMath.getAmount0Delta( + sqrtRatioAX96, + sqrtRatioBX96, + BigInt.asUintN(128, -liquidity), + false, + ), + ) + : BigInt.asIntN( + 256, + SqrtPriceMath.getAmount0Delta( + sqrtRatioAX96, + sqrtRatioBX96, + BigInt.asUintN(128, liquidity), + true, + ), + ); + } + + // Overloaded with different argument numbers + static _getAmount1DeltaO( + sqrtRatioAX96: bigint, + sqrtRatioBX96: bigint, + liquidity: bigint, + ) { + return liquidity < 0 + ? -BigInt.asIntN( + 256, + SqrtPriceMath.getAmount1Delta( + sqrtRatioAX96, + sqrtRatioBX96, + BigInt.asUintN(128, -liquidity), + false, + ), + ) + : BigInt.asIntN( + 256, + SqrtPriceMath.getAmount1Delta( + sqrtRatioAX96, + sqrtRatioBX96, + BigInt.asUintN(128, liquidity), + true, + ), + ); + } +} diff --git a/src/dex/squadswap-v3/contract-math/SwapMath.ts b/src/dex/squadswap-v3/contract-math/SwapMath.ts new file mode 100644 index 000000000..3f19cf8cb --- /dev/null +++ b/src/dex/squadswap-v3/contract-math/SwapMath.ts @@ -0,0 +1,139 @@ +import { BI_POWS } from '../../../bigint-constants'; +import { FullMath } from './FullMath'; +import { SqrtPriceMath } from './SqrtPriceMath'; + +export class SwapMath { + static computeSwapStep( + sqrtRatioCurrentX96: bigint, + sqrtRatioTargetX96: bigint, + liquidity: bigint, + amountRemaining: bigint, + feePips: bigint, + ): { + sqrtRatioNextX96: bigint; + amountIn: bigint; + amountOut: bigint; + feeAmount: bigint; + } { + const zeroForOne = sqrtRatioCurrentX96 >= sqrtRatioTargetX96; + const exactIn = amountRemaining >= 0n; + + let sqrtRatioNextX96 = 0n; + let amountIn = 0n; + let amountOut = 0n; + let feeAmount = 0n; + + if (exactIn) { + const amountRemainingLessFee = FullMath.mulDiv( + BigInt.asUintN(256, amountRemaining), + BI_POWS[6] - feePips, + BI_POWS[6], + ); + amountIn = zeroForOne + ? SqrtPriceMath.getAmount0Delta( + sqrtRatioTargetX96, + sqrtRatioCurrentX96, + liquidity, + true, + ) + : SqrtPriceMath.getAmount1Delta( + sqrtRatioCurrentX96, + sqrtRatioTargetX96, + liquidity, + true, + ); + if (amountRemainingLessFee >= amountIn) + sqrtRatioNextX96 = sqrtRatioTargetX96; + else + sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromInput( + sqrtRatioCurrentX96, + liquidity, + amountRemainingLessFee, + zeroForOne, + ); + } else { + amountOut = zeroForOne + ? SqrtPriceMath.getAmount1Delta( + sqrtRatioTargetX96, + sqrtRatioCurrentX96, + liquidity, + false, + ) + : SqrtPriceMath.getAmount0Delta( + sqrtRatioCurrentX96, + sqrtRatioTargetX96, + liquidity, + false, + ); + if (BigInt.asUintN(256, -amountRemaining) >= amountOut) + sqrtRatioNextX96 = sqrtRatioTargetX96; + else + sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromOutput( + sqrtRatioCurrentX96, + liquidity, + BigInt.asUintN(256, -amountRemaining), + zeroForOne, + ); + } + + const max = sqrtRatioTargetX96 == sqrtRatioNextX96; + + if (zeroForOne) { + amountIn = + max && exactIn + ? amountIn + : SqrtPriceMath.getAmount0Delta( + sqrtRatioNextX96, + sqrtRatioCurrentX96, + liquidity, + true, + ); + amountOut = + max && !exactIn + ? amountOut + : SqrtPriceMath.getAmount1Delta( + sqrtRatioNextX96, + sqrtRatioCurrentX96, + liquidity, + false, + ); + } else { + amountIn = + max && exactIn + ? amountIn + : SqrtPriceMath.getAmount1Delta( + sqrtRatioCurrentX96, + sqrtRatioNextX96, + liquidity, + true, + ); + amountOut = + max && !exactIn + ? amountOut + : SqrtPriceMath.getAmount0Delta( + sqrtRatioCurrentX96, + sqrtRatioNextX96, + liquidity, + false, + ); + } + + // cap the output amount to not exceed the remaining output amount + if (!exactIn && amountOut > BigInt.asUintN(256, -amountRemaining)) { + amountOut = BigInt.asUintN(256, -amountRemaining); + } + + if (exactIn && sqrtRatioNextX96 != sqrtRatioTargetX96) { + // we didn't reach the target, so take the remainder of the maximum input as fee + feeAmount = BigInt.asUintN(256, amountRemaining) - amountIn; + } else { + feeAmount = FullMath.mulDivRoundingUp( + amountIn, + feePips, + BI_POWS[6] - feePips, + ); + } + + return { sqrtRatioNextX96, amountIn, amountOut, feeAmount }; + } +} diff --git a/src/dex/squadswap-v3/contract-math/Tick.ts b/src/dex/squadswap-v3/contract-math/Tick.ts new file mode 100644 index 000000000..660f2e79d --- /dev/null +++ b/src/dex/squadswap-v3/contract-math/Tick.ts @@ -0,0 +1,82 @@ +import { LiquidityMath } from './LiquidityMath'; +import { _require } from '../../../utils'; +import { NumberAsString } from '@paraswap/core'; +import { ZERO_TICK_INFO } from '../constants'; +import { PoolState, TickInfo } from '../../uniswap-v3/types'; + +export class Tick { + static update( + state: PoolState, + tick: bigint, + tickCurrent: bigint, + liquidityDelta: bigint, + secondsPerLiquidityCumulativeX128: bigint, + tickCumulative: bigint, + time: bigint, + upper: boolean, + maxLiquidity: bigint, + ): boolean { + let info = state.ticks[Number(tick)]; + + if (info === undefined) { + info = { ...ZERO_TICK_INFO }; + state.ticks[Number(tick)] = info; + } + + const liquidityGrossBefore = info.liquidityGross; + const liquidityGrossAfter = LiquidityMath.addDelta( + liquidityGrossBefore, + liquidityDelta, + ); + + _require( + liquidityGrossAfter <= maxLiquidity, + 'LO', + { liquidityGrossAfter, maxLiquidity }, + 'liquidityGrossAfter <= maxLiquidity', + ); + + const flipped = (liquidityGrossAfter == 0n) != (liquidityGrossBefore == 0n); + + if (liquidityGrossBefore == 0n) { + if (tick <= tickCurrent) { + info.secondsPerLiquidityOutsideX128 = secondsPerLiquidityCumulativeX128; + info.tickCumulativeOutside = tickCumulative; + info.secondsOutside = time; + } + info.initialized = true; + } + + info.liquidityGross = liquidityGrossAfter; + + info.liquidityNet = upper + ? BigInt.asIntN( + 128, + BigInt.asIntN(256, info.liquidityNet) - liquidityDelta, + ) + : BigInt.asIntN( + 128, + BigInt.asIntN(256, info.liquidityNet) + liquidityDelta, + ); + return flipped; + } + + static clear(state: PoolState, tick: bigint) { + delete state.ticks[Number(tick)]; + } + + static cross( + ticks: Record, + tick: bigint, + secondsPerLiquidityCumulativeX128: bigint, + tickCumulative: bigint, + time: bigint, + ): bigint { + const info = ticks[Number(tick)]; + info.secondsPerLiquidityOutsideX128 = + secondsPerLiquidityCumulativeX128 - info.secondsPerLiquidityOutsideX128; + info.tickCumulativeOutside = tickCumulative - info.tickCumulativeOutside; + info.secondsOutside = time - info.secondsOutside; + return info.liquidityNet; + } +} diff --git a/src/dex/squadswap-v3/contract-math/TickBitMap.ts b/src/dex/squadswap-v3/contract-math/TickBitMap.ts new file mode 100644 index 000000000..42d213783 --- /dev/null +++ b/src/dex/squadswap-v3/contract-math/TickBitMap.ts @@ -0,0 +1,128 @@ +import { BI_MAX_UINT8 } from '../../../bigint-constants'; +import { BitMath } from './BitMath'; +import { _require } from '../../../utils'; +import { DeepReadonly } from 'ts-essentials'; +import { + OUT_OF_RANGE_ERROR_POSTFIX, + TICK_BITMAP_BUFFER, + TICK_BITMAP_TO_USE, +} from '../constants'; +import { PoolState } from '../../uniswap-v3/types'; + +function isWordPosOut( + wordPos: bigint, + startTickBitmap: bigint, + // For pricing we use wider range to check price impact. If function called from event + // it must always be within buffer + isPriceQuery: boolean, +) { + let lowerTickBitmapLimit; + let upperTickBitmapLimit; + + if (isPriceQuery) { + lowerTickBitmapLimit = + startTickBitmap - (TICK_BITMAP_BUFFER + TICK_BITMAP_TO_USE); + upperTickBitmapLimit = + startTickBitmap + (TICK_BITMAP_BUFFER + TICK_BITMAP_TO_USE); + } else { + lowerTickBitmapLimit = startTickBitmap - TICK_BITMAP_BUFFER; + upperTickBitmapLimit = startTickBitmap + TICK_BITMAP_BUFFER; + } + + _require( + wordPos >= lowerTickBitmapLimit && wordPos <= upperTickBitmapLimit, + `wordPos is out of safe state tickBitmap request range: ${OUT_OF_RANGE_ERROR_POSTFIX}`, + { wordPos }, + `wordPos >= LOWER_TICK_REQUEST_LIMIT && wordPos <= UPPER_TICK_REQUEST_LIMIT`, + ); +} + +export class TickBitMap { + static position(tick: bigint): [bigint, bigint] { + return [BigInt.asIntN(16, tick >> 8n), BigInt.asUintN(8, tick % 256n)]; + } + + static flipTick(state: PoolState, tick: bigint, tickSpacing: bigint) { + _require( + tick % tickSpacing === 0n, + '', + { tick, tickSpacing }, + 'tick % tickSpacing == 0n,', + ); + const [wordPos, bitPos] = TickBitMap.position(tick / tickSpacing); + const mask = 1n << bitPos; + + // flipTick is used only in _updatePosition which is always state changing event + // Therefore it is never used in price query + isWordPosOut(wordPos, state.startTickBitmap, false); + + const stringWordPos = wordPos.toString(); + if (state.tickBitmap[stringWordPos] === undefined) { + state.tickBitmap[stringWordPos] = 0n; + } + + state.tickBitmap[stringWordPos] ^= mask; + } + + static nextInitializedTickWithinOneWord( + state: DeepReadonly, + tick: bigint, + tickSpacing: bigint, + lte: boolean, + isPriceQuery: boolean, + ): [bigint, boolean] { + let compressed = tick / tickSpacing; + if (tick < 0n && tick % tickSpacing != 0n) compressed--; + + let next = 0n; + let initialized = false; + + if (lte) { + const [wordPos, bitPos] = TickBitMap.position(compressed); + const mask = (1n << bitPos) - 1n + (1n << bitPos); + + isWordPosOut(wordPos, state.startTickBitmap, isPriceQuery); + let tickBitmapValue = state.tickBitmap[wordPos.toString()]; + tickBitmapValue = tickBitmapValue === undefined ? 0n : tickBitmapValue; + + const masked = tickBitmapValue & mask; + + initialized = masked != 0n; + next = initialized + ? (compressed - + BigInt.asIntN(24, bitPos - BitMath.mostSignificantBit(masked))) * + tickSpacing + : (compressed - BigInt.asIntN(24, bitPos)) * tickSpacing; + } else { + // start from the word of the next tick, since the current tick state doesn't matter + const [wordPos, bitPos] = TickBitMap.position(compressed + 1n); + const mask = ~((1n << bitPos) - 1n); + + isWordPosOut(wordPos, state.startTickBitmap, isPriceQuery); + let tickBitmapValue = state.tickBitmap[wordPos.toString()]; + tickBitmapValue = tickBitmapValue === undefined ? 0n : tickBitmapValue; + + const masked = tickBitmapValue & mask; + + initialized = masked != 0n; + next = initialized + ? (compressed + + 1n + + BigInt.asIntN(24, BitMath.leastSignificantBit(masked) - bitPos)) * + tickSpacing + : (compressed + 1n + BigInt.asIntN(24, BI_MAX_UINT8 - bitPos)) * + tickSpacing; + } + + return [next, initialized]; + } + + static _putZeroIfUndefined( + state: PoolState, + tickBitmapValue: bigint | undefined, + wordPos: bigint, + isPriceQuery: boolean = false, + ): bigint { + return tickBitmapValue === undefined ? 0n : tickBitmapValue; + } +} diff --git a/src/dex/squadswap-v3/contract-math/TickMath.ts b/src/dex/squadswap-v3/contract-math/TickMath.ts new file mode 100644 index 000000000..5515ce15c --- /dev/null +++ b/src/dex/squadswap-v3/contract-math/TickMath.ts @@ -0,0 +1,211 @@ +import { gt } from 'lodash'; +import { BI_MAX_UINT256 } from '../../../bigint-constants'; +import { _gt } from './utils'; +import { _require } from '../../../utils'; + +export class TickMath { + static readonly MIN_TICK = -887272n; + static readonly MAX_TICK = -TickMath.MIN_TICK; + static readonly MIN_SQRT_RATIO = 4295128739n; + static readonly MAX_SQRT_RATIO = + 1461446703485210103287273052203988822378723970342n; + + static getSqrtRatioAtTick(tick: bigint): bigint { + const absTick = + tick < 0n + ? BigInt.asUintN(256, -BigInt.asIntN(256, tick)) + : BigInt.asUintN(256, BigInt.asIntN(256, tick)); + _require( + absTick <= BigInt.asUintN(256, TickMath.MAX_TICK), + 'T', + { absTick }, + 'absTick <= BigInt.asUintN(256, TickMath.MAX_TICK)', + ); + + let ratio = + (absTick & 0x1n) !== 0n + ? 0xfffcb933bd6fad37aa2d162d1a594001n + : 0x100000000000000000000000000000000n; + if ((absTick & 0x2n) !== 0n) + ratio = (ratio * 0xfff97272373d413259a46990580e213an) >> 128n; + if ((absTick & 0x4n) !== 0n) + ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdccn) >> 128n; + if ((absTick & 0x8n) !== 0n) + ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0n) >> 128n; + if ((absTick & 0x10n) !== 0n) + ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644n) >> 128n; + if ((absTick & 0x20n) !== 0n) + ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0n) >> 128n; + if ((absTick & 0x40n) !== 0n) + ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861n) >> 128n; + if ((absTick & 0x80n) !== 0n) + ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053n) >> 128n; + if ((absTick & 0x100n) !== 0n) + ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4n) >> 128n; + if ((absTick & 0x200n) !== 0n) + ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54n) >> 128n; + if ((absTick & 0x400n) !== 0n) + ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3n) >> 128n; + if ((absTick & 0x800n) !== 0n) + ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9n) >> 128n; + if ((absTick & 0x1000n) !== 0n) + ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825n) >> 128n; + if ((absTick & 0x2000n) !== 0n) + ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5n) >> 128n; + if ((absTick & 0x4000n) !== 0n) + ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7n) >> 128n; + if ((absTick & 0x8000n) !== 0n) + ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6n) >> 128n; + if ((absTick & 0x10000n) !== 0n) + ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9n) >> 128n; + if ((absTick & 0x20000n) !== 0n) + ratio = (ratio * 0x5d6af8dedb81196699c329225ee604n) >> 128n; + if ((absTick & 0x40000n) !== 0n) + ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98n) >> 128n; + if ((absTick & 0x80000n) !== 0n) + ratio = (ratio * 0x48a170391f7dc42444e8fa2n) >> 128n; + + if (tick > 0) ratio = BI_MAX_UINT256 / ratio; + return BigInt.asUintN( + 160, + (ratio >> 32n) + (ratio % (1n << 32n) == 0n ? 0n : 1n), + ); + } + + static getTickAtSqrtRatio(sqrtPriceX96: bigint): bigint { + _require( + sqrtPriceX96 >= TickMath.MIN_SQRT_RATIO && + sqrtPriceX96 < TickMath.MAX_SQRT_RATIO, + 'R', + { sqrtPriceX96 }, + 'sqrtPriceX96 >= TickMath.MIN_SQRT_RATIO && sqrtPriceX96 < TickMath.MAX_SQRT_RATIO', + ); + + let ratio = BigInt.asUintN(256, sqrtPriceX96) << 32n; + + let r = ratio; + let msb = 0n; + + let f = _gt(r, 0xffffffffffffffffffffffffffffffffn) << 7n; + msb = msb | f; + r = r >> f; + + f = _gt(r, 0xffffffffffffffffn) << 6n; + msb = msb | f; + r = r >> f; + + f = _gt(r, 0xffffffffn) << 5n; + msb = msb | f; + r = r >> f; + + f = _gt(r, 0xffffn) << 4n; + msb = msb | f; + r = r >> f; + + f = _gt(r, 0xffn) << 3n; + msb = msb | f; + r = r >> f; + + f = _gt(r, 0xfn) << 2n; + msb = msb | f; + r = r >> f; + + f = _gt(r, 0x3n) << 1n; + msb = msb | f; + r = r >> f; + + f = _gt(r, 0x1n); + msb = msb | f; + + if (msb >= 128n) r = ratio >> (msb - 127n); + else r = ratio << (127n - msb); + + let log_2 = (BigInt.asIntN(256, msb) - 128n) << 64n; + + r = (r * r) >> 127n; + f = r >> 128n; + log_2 = log_2 | (f << 63n); + r = r >> f; + + r = (r * r) >> 127n; + f = r >> 128n; + log_2 = log_2 | (f << 62n); + r = r >> f; + + r = (r * r) >> 127n; + f = r >> 128n; + log_2 = log_2 | (f << 61n); + r = r >> f; + + r = (r * r) >> 127n; + f = r >> 128n; + log_2 = log_2 | (f << 60n); + r = r >> f; + + r = (r * r) >> 127n; + f = r >> 128n; + log_2 = log_2 | (f << 59n); + r = r >> f; + + r = (r * r) >> 127n; + f = r >> 128n; + log_2 = log_2 | (f << 58n); + r = r >> f; + + r = (r * r) >> 127n; + f = r >> 128n; + log_2 = log_2 | (f << 57n); + r = r >> f; + + r = (r * r) >> 127n; + f = r >> 128n; + log_2 = log_2 | (f << 56n); + r = r >> f; + + r = (r * r) >> 127n; + f = r >> 128n; + log_2 = log_2 | (f << 55n); + r = r >> f; + + r = (r * r) >> 127n; + f = r >> 128n; + log_2 = log_2 | (f << 54n); + r = r >> f; + + r = (r * r) >> 127n; + f = r >> 128n; + log_2 = log_2 | (f << 53n); + r = r >> f; + + r = (r * r) >> 127n; + f = r >> 128n; + log_2 = log_2 | (f << 52n); + r = r >> f; + + r = (r * r) >> 127n; + f = r >> 128n; + log_2 = log_2 | (f << 51n); + r = r >> f; + + r = (r * r) >> 127n; + f = r >> 128n; + log_2 = log_2 | (f << 50n); + + const log_sqrt10001 = log_2 * 255738958999603826347141n; // 128.128 number + + const tickLow = BigInt.asIntN( + 24, + (log_sqrt10001 - 3402992956809132418596140100660247210n) >> 128n, + ); + const tickHi = BigInt.asIntN( + 24, + (log_sqrt10001 + 291339464771989622907027621153398088495n) >> 128n, + ); + + return tickLow === tickHi + ? tickLow + : TickMath.getSqrtRatioAtTick(tickHi) <= sqrtPriceX96 + ? tickHi + : tickLow; + } +} diff --git a/src/dex/squadswap-v3/contract-math/UnsafeMath.ts b/src/dex/squadswap-v3/contract-math/UnsafeMath.ts new file mode 100644 index 000000000..aebd7c579 --- /dev/null +++ b/src/dex/squadswap-v3/contract-math/UnsafeMath.ts @@ -0,0 +1,5 @@ +export class UnsafeMath { + static divRoundingUp(x: bigint, y: bigint) { + return (x + y - 1n) / y; + } +} diff --git a/src/dex/squadswap-v3/contract-math/pancakeswap-v3-math.ts b/src/dex/squadswap-v3/contract-math/pancakeswap-v3-math.ts new file mode 100644 index 000000000..0d64e976e --- /dev/null +++ b/src/dex/squadswap-v3/contract-math/pancakeswap-v3-math.ts @@ -0,0 +1,687 @@ +import _ from 'lodash'; +import { + OutputResult, + PoolState, + Slot0, + TickInfo, +} from '../../uniswap-v3/types'; +import { LiquidityMath } from './LiquidityMath'; +import { Oracle } from './Oracle'; +import { SqrtPriceMath } from './SqrtPriceMath'; +import { SwapMath } from './SwapMath'; +import { Tick } from './Tick'; +import { TickBitMap } from './TickBitMap'; +import { TickMath } from './TickMath'; +import { _require } from '../../../utils'; +import { DeepReadonly } from 'ts-essentials'; +import { NumberAsString, SwapSide } from '@paraswap/core'; +import { BI_MAX_INT } from '../../../bigint-constants'; +import { + MAX_PRICING_COMPUTATION_STEPS_ALLOWED, + OUT_OF_RANGE_ERROR_POSTFIX, +} from '../constants'; +import { FullMath } from './FullMath'; +import { FixedPoint128 } from './FixedPoint128'; + +type ModifyPositionParams = { + tickLower: bigint; + tickUpper: bigint; + liquidityDelta: bigint; +}; + +type PriceComputationState = { + amountSpecifiedRemaining: bigint; + amountCalculated: bigint; + sqrtPriceX96: bigint; + tick: bigint; + protocolFee: bigint; + liquidity: bigint; + isFirstCycleState: boolean; +}; + +type PriceComputationCache = { + liquidityStart: bigint; + blockTimestamp: bigint; + feeProtocol: bigint; + secondsPerLiquidityCumulativeX128: bigint; + tickCumulative: bigint; + computedLatestObservation: boolean; + tickCount: number; +}; + +const PROTOCOL_FEE_SP = 65536n; + +const PROTOCOL_FEE_DENOMINATOR = 10000n; + +function _updatePriceComputationObjects< + T extends PriceComputationState | PriceComputationCache, +>(toUpdate: T, updateBy: T) { + for (const k of Object.keys(updateBy) as (keyof T)[]) { + toUpdate[k] = updateBy[k]; + } +} + +function _priceComputationCycles( + poolState: DeepReadonly, + ticksCopy: Record, + slot0Start: Slot0, + state: PriceComputationState, + cache: PriceComputationCache, + sqrtPriceLimitX96: bigint, + zeroForOne: boolean, + exactInput: boolean, +): [ + // result + PriceComputationState, + // Latest calculated full cycle state we can use for bigger amounts + { + latestFullCycleState: PriceComputationState; + latestFullCycleCache: PriceComputationCache; + }, +] { + const latestFullCycleState: PriceComputationState = { ...state }; + + if (cache.tickCount == 0) { + cache.tickCount = 1; + } + const latestFullCycleCache: PriceComputationCache = { ...cache }; + + // We save tick before any change. Later we use this to restore + // state before last step + let lastTicksCopy: { index: number; tick: TickInfo } | undefined; + + let i = 0; + for ( + ; + state.amountSpecifiedRemaining !== 0n && + state.sqrtPriceX96 !== sqrtPriceLimitX96; + ++i + ) { + if ( + latestFullCycleCache.tickCount + i > + MAX_PRICING_COMPUTATION_STEPS_ALLOWED + ) { + state.amountSpecifiedRemaining = 0n; + state.amountCalculated = 0n; + break; + } + + const step = { + sqrtPriceStartX96: 0n, + tickNext: 0n, + initialized: false, + sqrtPriceNextX96: 0n, + amountIn: 0n, + amountOut: 0n, + feeAmount: 0n, + }; + + step.sqrtPriceStartX96 = state.sqrtPriceX96; + + try { + [step.tickNext, step.initialized] = + TickBitMap.nextInitializedTickWithinOneWord( + poolState, + state.tick, + poolState.tickSpacing, + zeroForOne, + true, + ); + } catch (e) { + if ( + e instanceof Error && + e.message.endsWith(OUT_OF_RANGE_ERROR_POSTFIX) + ) { + state.amountSpecifiedRemaining = 0n; + state.amountCalculated = 0n; + break; + } + throw e; + } + + if (step.tickNext < TickMath.MIN_TICK) { + step.tickNext = TickMath.MIN_TICK; + } else if (step.tickNext > TickMath.MAX_TICK) { + step.tickNext = TickMath.MAX_TICK; + } + + step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext); + + const swapStepResult = SwapMath.computeSwapStep( + state.sqrtPriceX96, + ( + zeroForOne + ? step.sqrtPriceNextX96 < sqrtPriceLimitX96 + : step.sqrtPriceNextX96 > sqrtPriceLimitX96 + ) + ? sqrtPriceLimitX96 + : step.sqrtPriceNextX96, + state.liquidity, + state.amountSpecifiedRemaining, + poolState.fee, + ); + + state.sqrtPriceX96 = swapStepResult.sqrtRatioNextX96; + step.amountIn = swapStepResult.amountIn; + step.amountOut = swapStepResult.amountOut; + step.feeAmount = swapStepResult.feeAmount; + + if (exactInput) { + state.amountSpecifiedRemaining -= step.amountIn + step.feeAmount; + state.amountCalculated = state.amountCalculated - step.amountOut; + } else { + state.amountSpecifiedRemaining += step.amountOut; + state.amountCalculated = + state.amountCalculated + step.amountIn + step.feeAmount; + } + + if (cache.feeProtocol > 0n) { + const delta = + (step.feeAmount * cache.feeProtocol) / PROTOCOL_FEE_DENOMINATOR; + step.feeAmount -= delta; + state.protocolFee += delta; + } + + if (state.sqrtPriceX96 === step.sqrtPriceNextX96) { + if (step.initialized) { + if (!cache.computedLatestObservation) { + [cache.tickCumulative, cache.secondsPerLiquidityCumulativeX128] = + Oracle.observeSingle( + poolState, + cache.blockTimestamp, + 0n, + slot0Start.tick, + slot0Start.observationIndex, + cache.liquidityStart, + slot0Start.observationCardinality, + ); + cache.computedLatestObservation = true; + } + + if (state.amountSpecifiedRemaining === 0n) { + const castTickNext = Number(step.tickNext); + lastTicksCopy = { + index: castTickNext, + tick: { ...ticksCopy[castTickNext] }, + }; + } + + let liquidityNet = Tick.cross( + ticksCopy, + step.tickNext, + cache.secondsPerLiquidityCumulativeX128, + cache.tickCumulative, + cache.blockTimestamp, + ); + if (zeroForOne) liquidityNet = -liquidityNet; + + state.liquidity = LiquidityMath.addDelta(state.liquidity, liquidityNet); + } + + state.tick = zeroForOne ? step.tickNext - 1n : step.tickNext; + } else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) { + state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96); + } + + if (state.amountSpecifiedRemaining !== 0n) { + _updatePriceComputationObjects(latestFullCycleState, state); + _updatePriceComputationObjects(latestFullCycleCache, cache); + // If it last cycle, check if ticks were changed and then restore previous state + // for next calculations + } else if (lastTicksCopy !== undefined) { + ticksCopy[lastTicksCopy.index] = lastTicksCopy.tick; + } + } + + if (i > 1) { + latestFullCycleCache.tickCount += i - 1; + } + + if (state.amountSpecifiedRemaining !== 0n) { + state.amountSpecifiedRemaining = 0n; + state.amountCalculated = 0n; + } + + return [state, { latestFullCycleState, latestFullCycleCache }]; +} + +class PancakeswapV3Math { + queryOutputs( + poolState: DeepReadonly, + // Amounts must increase + amounts: bigint[], + zeroForOne: boolean, + side: SwapSide, + ): OutputResult { + const slot0Start = poolState.slot0; + + const isSell = side === SwapSide.SELL; + + // While calculating, ticks are changing, so to not change the actual state, + // we use copy + const ticksCopy = _.cloneDeep(poolState.ticks); + + const sqrtPriceLimitX96 = zeroForOne + ? TickMath.MIN_SQRT_RATIO + 1n + : TickMath.MAX_SQRT_RATIO - 1n; + + const cache: PriceComputationCache = { + liquidityStart: poolState.liquidity, + blockTimestamp: this._blockTimestamp(poolState), + feeProtocol: zeroForOne + ? slot0Start.feeProtocol % PROTOCOL_FEE_SP + : slot0Start.feeProtocol >> 16n, + secondsPerLiquidityCumulativeX128: 0n, + tickCumulative: 0n, + computedLatestObservation: false, + tickCount: 0, // what is this + }; + + const state: PriceComputationState = { + // Will be overwritten later + amountSpecifiedRemaining: 0n, + amountCalculated: 0n, + sqrtPriceX96: slot0Start.sqrtPriceX96, + tick: slot0Start.tick, + protocolFee: 0n, + liquidity: cache.liquidityStart, + isFirstCycleState: true, + }; + + let isOutOfRange = false; + let previousAmount = 0n; + + const outputs = new Array(amounts.length); + const tickCounts = new Array(amounts.length); + for (const [i, amount] of amounts.entries()) { + if (amount === 0n) { + outputs[i] = 0n; + tickCounts[i] = 0; + continue; + } + + const amountSpecified = isSell + ? BigInt.asIntN(256, amount) + : -BigInt.asIntN(256, amount); + + if (state.isFirstCycleState) { + // Set first non zero amount + state.amountSpecifiedRemaining = amountSpecified; + state.isFirstCycleState = false; + } else { + state.amountSpecifiedRemaining = + amountSpecified - (previousAmount - state.amountSpecifiedRemaining); + } + + const exactInput = amountSpecified > 0n; + + _require( + zeroForOne + ? sqrtPriceLimitX96 < slot0Start.sqrtPriceX96 && + sqrtPriceLimitX96 > TickMath.MIN_SQRT_RATIO + : sqrtPriceLimitX96 > slot0Start.sqrtPriceX96 && + sqrtPriceLimitX96 < TickMath.MAX_SQRT_RATIO, + 'SPL', + { zeroForOne, sqrtPriceLimitX96, slot0Start }, + 'zeroForOne ? sqrtPriceLimitX96 < slot0Start.sqrtPriceX96 && sqrtPriceLimitX96 > TickMath.MIN_SQRT_RATIO : sqrtPriceLimitX96 > slot0Start.sqrtPriceX96 && sqrtPriceLimitX96 < TickMath.MAX_SQRT_RATIO', + ); + + if (!isOutOfRange) { + const [finalState, { latestFullCycleState, latestFullCycleCache }] = + _priceComputationCycles( + poolState, + ticksCopy, + slot0Start, + state, + cache, + sqrtPriceLimitX96, + zeroForOne, + exactInput, + ); + if ( + finalState.amountSpecifiedRemaining === 0n && + finalState.amountCalculated === 0n + ) { + isOutOfRange = true; + outputs[i] = 0n; + tickCounts[i] = 0; + continue; + } + + // We use it on next step to correct state.amountSpecifiedRemaining + previousAmount = amountSpecified; + + // First extract calculated values + const [amount0, amount1] = + zeroForOne === exactInput + ? [ + amountSpecified - finalState.amountSpecifiedRemaining, + finalState.amountCalculated, + ] + : [ + finalState.amountCalculated, + amountSpecified - finalState.amountSpecifiedRemaining, + ]; + + // Update for next amount + _updatePriceComputationObjects(state, latestFullCycleState); + _updatePriceComputationObjects(cache, latestFullCycleCache); + + if (isSell) { + outputs[i] = BigInt.asUintN(256, -(zeroForOne ? amount1 : amount0)); + tickCounts[i] = latestFullCycleCache.tickCount; + continue; + } else { + outputs[i] = zeroForOne + ? BigInt.asUintN(256, amount0) + : BigInt.asUintN(256, amount1); + tickCounts[i] = latestFullCycleCache.tickCount; + continue; + } + } else { + outputs[i] = 0n; + tickCounts[i] = 0; + } + } + + return { + outputs, + tickCounts, + }; + } + + swapFromEvent( + poolState: PoolState, + newSqrtPriceX96: bigint, + newTick: bigint, + newLiquidity: bigint, + zeroForOne: boolean, + ): void { + const slot0Start = poolState.slot0; + + const cache = { + liquidityStart: poolState.liquidity, + blockTimestamp: this._blockTimestamp(poolState), + feeProtocol: zeroForOne + ? slot0Start.feeProtocol % PROTOCOL_FEE_SP + : slot0Start.feeProtocol >> 16n, + secondsPerLiquidityCumulativeX128: 0n, + tickCumulative: 0n, + computedLatestObservation: false, + }; + + const state = { + // Because I don't have the exact amount user used, set this number to MAX_NUMBER to proceed + // with calculations. I think it is not a problem since in loop I don't rely on this value + amountSpecifiedRemaining: BI_MAX_INT, + amountCalculated: 0n, + sqrtPriceX96: slot0Start.sqrtPriceX96, + tick: slot0Start.tick, + protocolFee: 0n, + liquidity: cache.liquidityStart, + }; + + // Because I didn't have all variables, adapted loop stop with state.tick !== newTick + // condition. This cycle need only to calculate Tick.cross() function values + // It means that we are interested in cycling only if state.tick !== newTick + // When they become equivalent, we proceed with state updating part as normal + // And if assumptions regarding this cycle are correct, we don't need to process + // the last cycle when state.tick === newTick + while (state.tick !== newTick && state.sqrtPriceX96 !== newSqrtPriceX96) { + const step = { + sqrtPriceStartX96: 0n, + tickNext: 0n, + initialized: false, + sqrtPriceNextX96: 0n, + amountIn: 0n, + amountOut: 0n, + feeAmount: 0n, + }; + + step.sqrtPriceStartX96 = state.sqrtPriceX96; + + [step.tickNext, step.initialized] = + TickBitMap.nextInitializedTickWithinOneWord( + poolState, + state.tick, + poolState.tickSpacing, + zeroForOne, + false, + ); + + if (step.tickNext < TickMath.MIN_TICK) { + step.tickNext = TickMath.MIN_TICK; + } else if (step.tickNext > TickMath.MAX_TICK) { + step.tickNext = TickMath.MAX_TICK; + } + + step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext); + + const swapStepResult = SwapMath.computeSwapStep( + state.sqrtPriceX96, + ( + zeroForOne + ? step.sqrtPriceNextX96 < newSqrtPriceX96 + : step.sqrtPriceNextX96 > newSqrtPriceX96 + ) + ? newSqrtPriceX96 + : step.sqrtPriceNextX96, + state.liquidity, + state.amountSpecifiedRemaining, + poolState.fee, + ); + + state.sqrtPriceX96 = swapStepResult.sqrtRatioNextX96; + + if (cache.feeProtocol > 0) { + const delta = + (step.feeAmount * cache.feeProtocol) / PROTOCOL_FEE_DENOMINATOR; + step.feeAmount -= delta; + state.protocolFee += delta; + } + + if (state.sqrtPriceX96 == step.sqrtPriceNextX96) { + if (step.initialized) { + if (!cache.computedLatestObservation) { + [cache.tickCumulative, cache.secondsPerLiquidityCumulativeX128] = + Oracle.observeSingle( + poolState, + cache.blockTimestamp, + 0n, + slot0Start.tick, + slot0Start.observationIndex, + cache.liquidityStart, + slot0Start.observationCardinality, + ); + cache.computedLatestObservation = true; + } + + let liquidityNet = Tick.cross( + poolState.ticks, + step.tickNext, + cache.secondsPerLiquidityCumulativeX128, + cache.tickCumulative, + cache.blockTimestamp, + ); + + if (zeroForOne) liquidityNet = -liquidityNet; + + state.liquidity = LiquidityMath.addDelta( + state.liquidity, + liquidityNet, + ); + } + + state.tick = zeroForOne ? step.tickNext - 1n : step.tickNext; + } else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) { + state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96); + } + } + + if (slot0Start.tick !== newTick) { + const [observationIndex, observationCardinality] = Oracle.write( + poolState, + slot0Start.observationIndex, + this._blockTimestamp(poolState), + slot0Start.tick, + poolState.liquidity, + slot0Start.observationCardinality, + slot0Start.observationCardinalityNext, + ); + + [ + poolState.slot0.sqrtPriceX96, + poolState.slot0.tick, + poolState.slot0.observationIndex, + poolState.slot0.observationCardinality, + ] = [newSqrtPriceX96, newTick, observationIndex, observationCardinality]; + } else { + poolState.slot0.sqrtPriceX96 = newSqrtPriceX96; + } + + if (poolState.liquidity !== newLiquidity) + poolState.liquidity = newLiquidity; + } + + _modifyPosition( + state: PoolState, + params: ModifyPositionParams, + ): [bigint, bigint] { + const _slot0 = state.slot0; + + this._updatePosition( + state, + params.tickLower, + params.tickUpper, + params.liquidityDelta, + _slot0.tick, + ); + + let amount0 = 0n; + let amount1 = 0n; + if (params.liquidityDelta !== 0n) { + if (_slot0.tick < params.tickLower) { + amount0 = SqrtPriceMath._getAmount0DeltaO( + TickMath.getSqrtRatioAtTick(params.tickLower), + TickMath.getSqrtRatioAtTick(params.tickUpper), + params.liquidityDelta, + ); + } else if (_slot0.tick < params.tickUpper) { + const liquidityBefore = state.liquidity; + + [state.slot0.observationIndex, state.slot0.observationCardinality] = + Oracle.write( + state, + _slot0.observationIndex, + this._blockTimestamp(state), + _slot0.tick, + liquidityBefore, + _slot0.observationCardinality, + _slot0.observationCardinalityNext, + ); + + amount0 = SqrtPriceMath._getAmount0DeltaO( + _slot0.sqrtPriceX96, + TickMath.getSqrtRatioAtTick(params.tickUpper), + params.liquidityDelta, + ); + amount1 = SqrtPriceMath._getAmount1DeltaO( + TickMath.getSqrtRatioAtTick(params.tickLower), + _slot0.sqrtPriceX96, + params.liquidityDelta, + ); + + state.liquidity = LiquidityMath.addDelta( + liquidityBefore, + params.liquidityDelta, + ); + } else { + amount1 = SqrtPriceMath._getAmount1DeltaO( + TickMath.getSqrtRatioAtTick(params.tickLower), + TickMath.getSqrtRatioAtTick(params.tickUpper), + params.liquidityDelta, + ); + } + } + return [amount0, amount1]; + } + + private _isTickToProcess(state: PoolState, tick: bigint): boolean { + return tick >= state.lowestKnownTick && tick <= state.highestKnownTick; + } + + private _updatePosition( + state: PoolState, + tickLower: bigint, + tickUpper: bigint, + liquidityDelta: bigint, + tick: bigint, + ): void { + // if we need to update the ticks, do it + let flippedLower = false; + let flippedUpper = false; + if (liquidityDelta !== 0n) { + const time = this._blockTimestamp(state); + const [tickCumulative, secondsPerLiquidityCumulativeX128] = + Oracle.observeSingle( + state, + time, + 0n, + state.slot0.tick, + state.slot0.observationIndex, + state.liquidity, + state.slot0.observationCardinality, + ); + + if (this._isTickToProcess(state, tickLower)) { + flippedLower = Tick.update( + state, + tickLower, + tick, + liquidityDelta, + secondsPerLiquidityCumulativeX128, + tickCumulative, + time, + false, + state.maxLiquidityPerTick, + ); + } + if (this._isTickToProcess(state, tickUpper)) { + flippedUpper = Tick.update( + state, + tickUpper, + tick, + liquidityDelta, + secondsPerLiquidityCumulativeX128, + tickCumulative, + time, + true, + state.maxLiquidityPerTick, + ); + } + + if (flippedLower) { + TickBitMap.flipTick(state, tickLower, state.tickSpacing); + } + if (flippedUpper) { + TickBitMap.flipTick(state, tickUpper, state.tickSpacing); + } + } + + // clear any tick data that is no longer needed + if (liquidityDelta < 0n) { + if (flippedLower) { + Tick.clear(state, tickLower); + } + if (flippedUpper) { + Tick.clear(state, tickUpper); + } + } + } + + private _blockTimestamp(state: DeepReadonly) { + return BigInt.asUintN(32, state.blockTimestamp); + } +} + +export const pancakeswapV3Math = new PancakeswapV3Math(); diff --git a/src/dex/squadswap-v3/contract-math/utils.ts b/src/dex/squadswap-v3/contract-math/utils.ts new file mode 100644 index 000000000..0f4caff99 --- /dev/null +++ b/src/dex/squadswap-v3/contract-math/utils.ts @@ -0,0 +1,11 @@ +export function _mulmod(x: bigint, y: bigint, m: bigint): bigint { + return m === 0n ? 0n : (x * y) % m; +} + +export function _lt(x: bigint, y: bigint) { + return x < y ? 1n : 0n; +} + +export function _gt(x: bigint, y: bigint) { + return x > y ? 1n : 0n; +} diff --git a/src/dex/squadswap-v3/squadswap-v3-e2e.test.ts b/src/dex/squadswap-v3/squadswap-v3-e2e.test.ts new file mode 100644 index 000000000..36bd3a5b6 --- /dev/null +++ b/src/dex/squadswap-v3/squadswap-v3-e2e.test.ts @@ -0,0 +1,199 @@ +import dotenv from 'dotenv'; +dotenv.config(); + +import { testE2E } from '../../../tests/utils-e2e'; +import { + Tokens, + Holders, + NativeTokenSymbols, +} from '../../../tests/constants-e2e'; +import { Network, ContractMethod, SwapSide } from '../../constants'; +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { generateConfig } from '../../config'; + +function testForNetwork( + network: Network, + dexKey: string, + tokenASymbol: string, + tokenBSymbol: string, + tokenAAmount: string, + tokenBAmount: string, + nativeTokenAmount: string, + slippage?: number | undefined, +) { + const provider = new StaticJsonRpcProvider( + generateConfig(network).privateHttpProvider, + network, + ); + const tokens = Tokens[network]; + const holders = Holders[network]; + const nativeTokenSymbol = NativeTokenSymbols[network]; + + const sideToContractMethods = new Map([ + [ + SwapSide.SELL, + [ + ContractMethod.simpleSwap, + ContractMethod.multiSwap, + ContractMethod.megaSwap, + ], + ], + [SwapSide.BUY, [ContractMethod.simpleBuy, ContractMethod.buy]], + ]); + + describe(`${network}`, () => { + sideToContractMethods.forEach((contractMethods, side) => + describe(`${side}`, () => { + contractMethods.forEach((contractMethod: ContractMethod) => { + describe(`${contractMethod}`, () => { + it(`${nativeTokenSymbol} -> ${tokenASymbol}`, async () => { + await testE2E( + tokens[nativeTokenSymbol], + tokens[tokenASymbol], + holders[nativeTokenSymbol], + side === SwapSide.SELL ? nativeTokenAmount : tokenAAmount, + side, + dexKey, + contractMethod, + network, + provider, + undefined, + undefined, + undefined, + slippage, + ); + }); + it(`${tokenASymbol} -> ${nativeTokenSymbol}`, async () => { + await testE2E( + tokens[tokenASymbol], + tokens[nativeTokenSymbol], + holders[tokenASymbol], + side === SwapSide.SELL ? tokenAAmount : nativeTokenAmount, + side, + dexKey, + contractMethod, + network, + provider, + undefined, + undefined, + undefined, + slippage, + ); + }); + it(`${tokenASymbol} -> ${tokenBSymbol}`, async () => { + await testE2E( + tokens[tokenASymbol], + tokens[tokenBSymbol], + holders[tokenASymbol], + side === SwapSide.SELL ? tokenAAmount : tokenBAmount, + side, + dexKey, + contractMethod, + network, + provider, + undefined, + undefined, + undefined, + slippage, + ); + }); + }); + }); + }), + ); + }); +} + +describe('SquadswapV3 E2E', () => { + const dexKey = 'SquadswapV3'; + + describe('SquadswapV3 BSC', () => { + const network = Network.BSC; + const tokens = Tokens[network]; + const holders = Holders[network]; + const provider = new StaticJsonRpcProvider( + generateConfig(network).privateHttpProvider, + network, + ); + + const sideToContractMethods = new Map([ + [ + SwapSide.SELL, + [ + ContractMethod.simpleSwap, + ContractMethod.multiSwap, + ContractMethod.megaSwap, + ], + ], + [SwapSide.BUY, [ContractMethod.simpleBuy, ContractMethod.buy]], + ]); + + const pairs: { name: string; sellAmount: string; buyAmount: string }[][] = [ + [ + { + name: NativeTokenSymbols[network], + sellAmount: '100000000000000000000', + buyAmount: '100000000000000000000', + }, + { + name: 'USDT', + sellAmount: '100000000000000000000', + buyAmount: '10000000000000', + }, + ], + [ + { + name: 'WBNB', + sellAmount: '1000000000000000000', + buyAmount: '10000000000000000000', + }, + { + name: 'USDT', + sellAmount: '10000000000000000000', + buyAmount: '1000000000000000000', + }, + ], + ]; + + sideToContractMethods.forEach((contractMethods, side) => + describe(`${side}`, () => { + contractMethods.forEach((contractMethod: ContractMethod) => { + pairs.forEach(pair => { + describe(`${contractMethod}`, () => { + it(`${pair[0].name} -> ${pair[1].name}`, async () => { + await testE2E( + tokens[pair[0].name], + tokens[pair[1].name], + holders[pair[0].name], + side === SwapSide.SELL + ? pair[0].sellAmount + : pair[0].buyAmount, + side, + dexKey, + contractMethod, + network, + provider, + ); + }); + it(`${pair[1].name} -> ${pair[0].name}`, async () => { + await testE2E( + tokens[pair[1].name], + tokens[pair[0].name], + holders[pair[1].name], + side === SwapSide.SELL + ? pair[1].sellAmount + : pair[1].buyAmount, + side, + dexKey, + contractMethod, + network, + provider, + ); + }); + }); + }); + }); + }), + ); + }); +}); diff --git a/src/dex/squadswap-v3/squadswap-v3-events.test.ts b/src/dex/squadswap-v3/squadswap-v3-events.test.ts new file mode 100644 index 000000000..4e0ac1c89 --- /dev/null +++ b/src/dex/squadswap-v3/squadswap-v3-events.test.ts @@ -0,0 +1,495 @@ +/* eslint-disable no-console */ +import dotenv from 'dotenv'; +dotenv.config(); + +import { Interface, Result } from '@ethersproject/abi'; +import { DummyDexHelper } from '../../dex-helper/index'; +import { Network, SwapSide } from '../../constants'; +import { BI_POWS } from '../../bigint-constants'; +import { checkPoolPrices, checkPoolsLiquidity } from '../../../tests/utils'; +import { Tokens } from '../../../tests/constants-e2e'; +import PancakeswapV3QuoterABI from '../../abi/pancakeswap-v3/PancakeswapV3Quoter.abi.json'; +import { Address } from '@paraswap/core'; +import { SquadswapV3 } from './squadswap-v3'; +import * as net from 'net'; + +const networks = [Network.BSC]; + +const dexKey = 'SquadswapV3'; + +const quoterIface = new Interface(PancakeswapV3QuoterABI); + +const testingData: Partial<{ [key in Network]: any }> = { + [Network.BSC]: { + tokenA: Tokens[Network.BSC]['bBTC'], + tokenASymbol: 'bBTC', + stableSellAmounts: [ + 0n, + 10_000n * BI_POWS[6], + 20_000n * BI_POWS[6], + 30_000n * BI_POWS[6], + ], + stableBuyAmounts: [0n, 1n * BI_POWS[6], 2n * BI_POWS[6], 3n * BI_POWS[6]], + regularSellAmounts: [ + 0n, + 1n * BI_POWS[18], + 2n * BI_POWS[18], + 3n * BI_POWS[18], + ], + regularBuyAmounts: [ + 0n, + 1n * BI_POWS[18], + 2n * BI_POWS[18], + 3n * BI_POWS[18], + ], + }, +}; + +describe('SquadswapV3', function () { + describe('BSC', () => { + describe('WBNB -> USDT', () => { + it('getPoolIdentifiers and getPricesVolume SELL', async function () { + const network = Network.BSC; + const dexHelper = new DummyDexHelper(network); + const blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + const pancakeswapV3 = new SquadswapV3(network, dexKey, dexHelper); + + const WBNB = Tokens[network]['WBNB']; + const USDT = Tokens[network]['USDT']; + + const amounts = [0n, 1n * BI_POWS[18], 2n * BI_POWS[18]]; + + const pools = await pancakeswapV3.getPoolIdentifiers( + WBNB, + USDT, + SwapSide.SELL, + blockNumber, + ); + console.log(`WBNB <> USDT Pool Identifiers: `, pools); + + expect(pools.length).toBeGreaterThan(0); + + const poolPrices = await pancakeswapV3.getPricesVolume( + WBNB, + USDT, + amounts, + SwapSide.SELL, + blockNumber, + pools, + ); + console.log(`WBNB <> USDT Pool Prices: `, poolPrices); + + expect(poolPrices).not.toBeNull(); + checkPoolPrices( + poolPrices!.filter(pp => pp.unit !== 0n), + amounts, + SwapSide.SELL, + dexKey, + ); + + // Check if onchain pricing equals to calculated ones + let falseChecksCounter = 0; + await Promise.all( + poolPrices!.map(async price => { + const fee = + pancakeswapV3.eventPools[price.poolIdentifier!]!.feeCode; + const res = await checkOnChainPricing( + pancakeswapV3, + dexHelper, + 'quoteExactInputSingle', + blockNumber, + price.prices, + WBNB.address, + USDT.address, + fee, + amounts, + ); + if (res === false) falseChecksCounter++; + }), + ); + expect(falseChecksCounter).toBeLessThan(poolPrices!.length); + }); + }); + }); + + networks.forEach(network => + describe(network, function () { + let blockNumber: number; + let pancakeswapV3: SquadswapV3; + const testData = testingData[network]; + if (testData) { + const { + stableA, + stableASymbol, + stableB, + stableBSymbol, + stableSellAmounts, + stableBuyAmounts, + tokenA, + tokenASymbol, + tokenB, + tokenBSymbol, + regularSellAmounts, + regularBuyAmounts, + } = testData; + const dexHelper = new DummyDexHelper(network); + + beforeEach(async () => { + blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + pancakeswapV3 = new SquadswapV3(network, dexKey, dexHelper); + }); + + describe('Stable pairs', function () { + it('getPoolIdentifiers and getPricesVolume SELL stable pairs', async function () { + const pools = await pancakeswapV3.getPoolIdentifiers( + stableA, + stableB, + SwapSide.SELL, + blockNumber, + ); + console.log( + `${stableASymbol} <> ${stableBSymbol} Pool Identifiers: `, + pools, + ); + + expect(pools.length).toBeGreaterThan(0); + + const poolPrices = await pancakeswapV3.getPricesVolume( + stableA, + stableB, + stableSellAmounts, + SwapSide.SELL, + blockNumber, + pools, + ); + console.log( + `${stableASymbol} <> ${stableBSymbol} Pool Prices: `, + poolPrices, + ); + + expect(poolPrices).not.toBeNull(); + checkPoolPrices( + poolPrices!.filter(pp => pp.unit !== 0n), + stableSellAmounts, + SwapSide.SELL, + dexKey, + ); + + // Check if onchain pricing equals to calculated ones + let falseChecksCounter = 0; + await Promise.all( + poolPrices!.map(async price => { + const fee = + pancakeswapV3.eventPools[price.poolIdentifier!]!.feeCode; + const res = await checkOnChainPricing( + pancakeswapV3, + dexHelper, + 'quoteExactInputSingle', + blockNumber, + price.prices, + stableA.address, + stableB.address, + fee, + stableSellAmounts, + ); + if (res === false) falseChecksCounter++; + }), + ); + expect(falseChecksCounter).toBeLessThan(poolPrices!.length); + }); + + it('getPoolIdentifiers and getPricesVolume BUY stable pairs', async function () { + const pools = await pancakeswapV3.getPoolIdentifiers( + stableA, + stableB, + SwapSide.BUY, + blockNumber, + ); + console.log( + `${stableASymbol} <> ${stableBSymbol} Pool Identifiers: `, + pools, + ); + + expect(pools.length).toBeGreaterThan(0); + + const poolPrices = await pancakeswapV3.getPricesVolume( + stableA, + stableB, + stableBuyAmounts, + SwapSide.BUY, + blockNumber, + pools, + ); + console.log( + `${stableASymbol} <> ${stableBSymbol} Pool Prices: `, + poolPrices, + ); + + expect(poolPrices).not.toBeNull(); + checkPoolPrices( + poolPrices!.filter(pp => pp.unit !== 0n), + stableBuyAmounts, + SwapSide.BUY, + dexKey, + ); + + // Check if onchain pricing equals to calculated ones + let falseChecksCounter = 0; + await Promise.all( + poolPrices!.map(async price => { + const fee = + pancakeswapV3.eventPools[price.poolIdentifier!]!.feeCode; + const res = await checkOnChainPricing( + pancakeswapV3, + dexHelper, + 'quoteExactOutputSingle', + blockNumber, + price.prices, + stableA.address, + stableB.address, + fee, + stableBuyAmounts, + ); + if (res === false) falseChecksCounter++; + }), + ); + expect(falseChecksCounter).toBeLessThan(poolPrices!.length); + }); + + it('getTopPoolsForToken', async function () { + const poolLiquidity = await pancakeswapV3.getTopPoolsForToken( + stableA.address, + 10, + ); + console.log(`${stableASymbol} Top Pools:`, poolLiquidity); + + if (!pancakeswapV3.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity(poolLiquidity, stableA.address, dexKey); + } + }); + }); + + describe('Regular pairs', function () { + it('getPoolIdentifiers and getPricesVolume SELL', async function () { + const pools = await pancakeswapV3.getPoolIdentifiers( + tokenA, + tokenB, + SwapSide.SELL, + blockNumber, + ); + console.log( + `${tokenASymbol} <> ${tokenBSymbol} Pool Identifiers: `, + pools, + ); + + expect(pools.length).toBeGreaterThan(0); + + const poolPrices = await pancakeswapV3.getPricesVolume( + tokenA, + tokenB, + regularSellAmounts, + SwapSide.SELL, + blockNumber, + pools, + ); + console.log( + `${tokenASymbol} <> ${tokenBSymbol} Pool Prices: `, + poolPrices, + ); + + expect(poolPrices).not.toBeNull(); + checkPoolPrices( + poolPrices!, + regularSellAmounts, + SwapSide.SELL, + dexKey, + ); + + let falseChecksCounter = 0; + await Promise.all( + poolPrices!.map(async price => { + const fee = + pancakeswapV3.eventPools[price.poolIdentifier!]!.feeCode; + const res = await checkOnChainPricing( + pancakeswapV3, + dexHelper, + 'quoteExactInputSingle', + blockNumber, + price.prices, + tokenA.address, + tokenB.address, + fee, + regularSellAmounts, + ); + if (res === false) falseChecksCounter++; + }), + ); + + expect(falseChecksCounter).toBeLessThan(poolPrices!.length); + }); + + it('getPoolIdentifiers and getPricesVolume BUY', async function () { + const pools = await pancakeswapV3.getPoolIdentifiers( + tokenA, + tokenB, + SwapSide.BUY, + blockNumber, + ); + console.log( + `${stableASymbol} <> ${stableBSymbol} Pool Identifiers: `, + pools, + ); + + expect(pools.length).toBeGreaterThan(0); + + const poolPrices = await pancakeswapV3.getPricesVolume( + tokenA, + tokenB, + regularBuyAmounts, + SwapSide.BUY, + blockNumber, + pools, + ); + console.log( + `${tokenASymbol} <> ${tokenBSymbol} Pool Prices: `, + poolPrices, + ); + + expect(poolPrices).not.toBeNull(); + checkPoolPrices( + poolPrices!, + regularBuyAmounts, + SwapSide.BUY, + dexKey, + ); + + // Check if onchain pricing equals to calculated ones + let falseChecksCounter = 0; + await Promise.all( + poolPrices!.map(async price => { + const fee = + pancakeswapV3.eventPools[price.poolIdentifier!]!.feeCode; + const res = await checkOnChainPricing( + pancakeswapV3, + dexHelper, + 'quoteExactOutputSingle', + blockNumber, + price.prices, + tokenA.address, + tokenB.address, + fee, + regularBuyAmounts, + ); + if (res === false) falseChecksCounter++; + }), + ); + expect(falseChecksCounter).toBeLessThan(poolPrices!.length); + }); + + it('getTopPoolsForToken', async function () { + const poolLiquidity = await pancakeswapV3.getTopPoolsForToken( + tokenA.address, + 10, + ); + console.log(`${stableASymbol} Top Pools:`, poolLiquidity); + + if (!pancakeswapV3.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity(poolLiquidity, stableA.address, dexKey); + } + }); + }); + } + }), + ); +}); + +function getReaderCalldata( + exchangeAddress: string, + readerIface: Interface, + amounts: bigint[], + funcName: string, + tokenIn: Address, + tokenOut: Address, + fee: bigint, +) { + return amounts.map(amount => ({ + target: exchangeAddress, + callData: readerIface.encodeFunctionData(funcName, [ + [tokenIn, tokenOut, amount, fee, 0n], + ]), + })); +} + +function decodeReaderResult(results: any, readerIface: any, funcName: any) { + return results.map((result: any) => { + const parsed = readerIface.decodeFunctionResult(funcName, result); + return parsed[0]._hex.toString(); // BigInt'i string'e dönüştürüyoruz + }); +} + +async function checkOnChainPricing( + pancakeswapV3: SquadswapV3, + dexHelper: DummyDexHelper, + funcName: string, + blockNumber: number, + prices: bigint[], + tokenIn: Address, + tokenOut: Address, + fee: bigint, + _amounts: bigint[], +) { + // Quoter address + const exchangeAddress = '0x81Da0D4e1157391a22a656ad84AAb9b2716F21e0'; + const readerIface = quoterIface; + + const sum = prices.reduce((acc, curr) => (acc += curr), 0n); + + if (sum === 0n) { + console.log( + `Prices were not calculated for tokenIn=${tokenIn}, tokenOut=${tokenOut}, fee=${fee.toString()}. Most likely price impact is too big for requested amount`, + ); + return false; + } + + const readerCallData = getReaderCalldata( + exchangeAddress, + readerIface, + _amounts.slice(1), + funcName, + tokenIn, + tokenOut, + fee, + ); + + let readerResult; + try { + readerResult = ( + await dexHelper.multiContract.methods + .aggregate(readerCallData) + .call({}, blockNumber) + ).returnData; + } catch (e) { + console.log( + `Can not fetch on-chain pricing for fee ${fee}. It happens for low liquidity pools`, + ); + return false; + } + + const expectedPrices = [0n].concat( + decodeReaderResult(readerResult, readerIface, funcName), + ); + + let firstZeroIndex = prices.slice(1).indexOf(0n); + + // we skipped first, so add +1 on result + firstZeroIndex = firstZeroIndex === -1 ? prices.length : firstZeroIndex; + + console.log('PRICE: ', prices); + console.log('ON-chain prices: ', prices); + + // Compare only the ones for which we were able to calculate prices + expect(prices.slice(0, firstZeroIndex)).toEqual( + expectedPrices.slice(0, firstZeroIndex), + ); + return true; +} diff --git a/src/dex/squadswap-v3/squadswap-v3-factory.ts b/src/dex/squadswap-v3/squadswap-v3-factory.ts new file mode 100644 index 000000000..5ae3bf3e5 --- /dev/null +++ b/src/dex/squadswap-v3/squadswap-v3-factory.ts @@ -0,0 +1,80 @@ +import { Interface } from '@ethersproject/abi'; +import { DeepReadonly } from 'ts-essentials'; +import FactoryABI from '../../abi/pancakeswap-v3/PancakeswapV3Factory.abi.json'; +import { IDexHelper } from '../../dex-helper/idex-helper'; +import { StatefulEventSubscriber } from '../../stateful-event-subscriber'; +import { Address, Log, Logger } from '../../types'; +import { LogDescription } from 'ethers/lib/utils'; +import { FactoryState } from '../uniswap-v3/types'; + +export type OnPoolCreatedCallback = ({ + token0, + token1, + fee, +}: { + token0: string; + token1: string; + fee: bigint; +}) => Promise; + +/* + * "Stateless" event subscriber in order to capture "PoolCreated" event on new pools created. + * State is present, but it's a placeholder to actually make the events reach handlers (if there's no previous state - `processBlockLogs` is not called) + */ +export class SquadswapV3Factory extends StatefulEventSubscriber { + handlers: { + [event: string]: (event: any) => Promise; + } = {}; + + logDecoder: (log: Log) => any; + + public readonly factoryIface = new Interface(FactoryABI); + + constructor( + readonly dexHelper: IDexHelper, + parentName: string, + protected readonly factoryAddress: Address, + logger: Logger, + protected readonly onPoolCreated: OnPoolCreatedCallback, + mapKey: string = '', + ) { + super( + parentName, + `${parentName} Factory`, + dexHelper, + logger, + false, + mapKey, + ); + + this.addressesSubscribed = [factoryAddress]; + + this.logDecoder = (log: Log) => this.factoryIface.parseLog(log); + + this.handlers['PoolCreated'] = this.handleNewPool.bind(this); + } + + generateState(): FactoryState { + return {}; + } + + protected async processLog( + _: DeepReadonly, + log: Readonly, + ): Promise { + const event = this.logDecoder(log); + if (event.name in this.handlers) { + await this.handlers[event.name](event); + } + + return {}; + } + + async handleNewPool(event: LogDescription) { + const token0 = event.args.token0.toLowerCase(); + const token1 = event.args.token1.toLowerCase(); + const fee = event.args.fee; + + await this.onPoolCreated({ token0, token1, fee }); + } +} diff --git a/src/dex/squadswap-v3/squadswap-v3-integration.test.ts b/src/dex/squadswap-v3/squadswap-v3-integration.test.ts new file mode 100644 index 000000000..0b9dc2411 --- /dev/null +++ b/src/dex/squadswap-v3/squadswap-v3-integration.test.ts @@ -0,0 +1,501 @@ +/* eslint-disable no-console */ +import dotenv from 'dotenv'; +dotenv.config(); + +import { Interface, Result } from '@ethersproject/abi'; +import { DummyDexHelper } from '../../dex-helper/index'; +import { Network, SwapSide } from '../../constants'; +import { BI_POWS } from '../../bigint-constants'; +import { checkPoolPrices, checkPoolsLiquidity } from '../../../tests/utils'; +import { Tokens } from '../../../tests/constants-e2e'; +import PancakeswapV3QuoterABI from '../../abi/pancakeswap-v3/PancakeswapV3Quoter.abi.json'; +import { Address } from '@paraswap/core'; +import { SquadswapV3 } from './squadswap-v3'; +import * as net from 'net'; + +const networks = [Network.BSC]; + +const dexKey = 'SquadswapV3'; + +const quoterIface = new Interface(PancakeswapV3QuoterABI); + +const testingData: Partial<{ [key in Network]: any }> = { + [Network.BSC]: { + tokenA: Tokens[Network.BSC]['bBTC'], + tokenASymbol: 'bBTC', + stableA: Tokens[Network.BSC]['USDC'], + stableASymbol: 'USDC', + tokenB: Tokens[Network.BSC]['WBNB'], + tokenBSymbol: 'WBNB', + stableB: Tokens[Network.BSC]['USDT'], + stableBSymbol: 'USDT', + stableSellAmounts: [ + 0n, + 10_000n * BI_POWS[6], + 20_000n * BI_POWS[6], + 30_000n * BI_POWS[6], + ], + stableBuyAmounts: [0n, 1n * BI_POWS[6], 2n * BI_POWS[6], 3n * BI_POWS[6]], + regularSellAmounts: [ + 0n, + 1n * BI_POWS[18], + 2n * BI_POWS[18], + 3n * BI_POWS[18], + ], + regularBuyAmounts: [ + 0n, + 1n * BI_POWS[18], + 2n * BI_POWS[18], + 3n * BI_POWS[18], + ], + }, +}; + +describe('SquadswapV3', function () { + describe('BSC', () => { + describe('WBNB -> USDT', () => { + it('getPoolIdentifiers and getPricesVolume SELL', async function () { + const network = Network.BSC; + const dexHelper = new DummyDexHelper(network); + const blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + const pancakeswapV3 = new SquadswapV3(network, dexKey, dexHelper); + + const WBNB = Tokens[network]['WBNB']; + const USDT = Tokens[network]['USDT']; + + const amounts = [0n, 1n * BI_POWS[18], 2n * BI_POWS[18]]; + + const pools = await pancakeswapV3.getPoolIdentifiers( + WBNB, + USDT, + SwapSide.SELL, + blockNumber, + ); + console.log(`WBNB <> USDT Pool Identifiers: `, pools); + + expect(pools.length).toBeGreaterThan(0); + + const poolPrices = await pancakeswapV3.getPricesVolume( + WBNB, + USDT, + amounts, + SwapSide.SELL, + blockNumber, + pools, + ); + console.log(`WBNB <> USDT Pool Prices: `, poolPrices); + + expect(poolPrices).not.toBeNull(); + checkPoolPrices( + poolPrices!.filter(pp => pp.unit !== 0n), + amounts, + SwapSide.SELL, + dexKey, + ); + + // Check if onchain pricing equals to calculated ones + let falseChecksCounter = 0; + await Promise.all( + poolPrices!.map(async price => { + const fee = + pancakeswapV3.eventPools[price.poolIdentifier!]!.feeCode; + const res = await checkOnChainPricing( + pancakeswapV3, + dexHelper, + 'quoteExactInputSingle', + blockNumber, + price.prices, + WBNB.address, + USDT.address, + fee, + amounts, + ); + if (res === false) falseChecksCounter++; + }), + ); + expect(falseChecksCounter).toBeLessThan(poolPrices!.length); + }); + }); + }); + + networks.forEach(network => + describe(network, function () { + let blockNumber: number; + let pancakeswapV3: SquadswapV3; + const testData = testingData[network]; + if (testData) { + const { + stableA, + stableASymbol, + stableB, + stableBSymbol, + stableSellAmounts, + stableBuyAmounts, + tokenA, + tokenASymbol, + tokenB, + tokenBSymbol, + regularSellAmounts, + regularBuyAmounts, + } = testData; + const dexHelper = new DummyDexHelper(network); + + beforeEach(async () => { + blockNumber = await dexHelper.web3Provider.eth.getBlockNumber(); + pancakeswapV3 = new SquadswapV3(network, dexKey, dexHelper); + }); + + describe('Stable pairs', function () { + it('getPoolIdentifiers and getPricesVolume SELL stable pairs', async function () { + const pools = await pancakeswapV3.getPoolIdentifiers( + stableA, + stableB, + SwapSide.SELL, + blockNumber, + ); + console.log( + `${stableASymbol} <> ${stableBSymbol} Pool Identifiers: `, + pools, + ); + + expect(pools.length).toBeGreaterThan(0); + + const poolPrices = await pancakeswapV3.getPricesVolume( + stableA, + stableB, + stableSellAmounts, + SwapSide.SELL, + blockNumber, + pools, + ); + console.log( + `${stableASymbol} <> ${stableBSymbol} Pool Prices: `, + poolPrices, + ); + + expect(poolPrices).not.toBeNull(); + checkPoolPrices( + poolPrices!.filter(pp => pp.unit !== 0n), + stableSellAmounts, + SwapSide.SELL, + dexKey, + ); + + // Check if onchain pricing equals to calculated ones + let falseChecksCounter = 0; + await Promise.all( + poolPrices!.map(async price => { + const fee = + pancakeswapV3.eventPools[price.poolIdentifier!]!.feeCode; + const res = await checkOnChainPricing( + pancakeswapV3, + dexHelper, + 'quoteExactInputSingle', + blockNumber, + price.prices, + stableA.address, + stableB.address, + fee, + stableSellAmounts, + ); + if (res === false) falseChecksCounter++; + }), + ); + expect(falseChecksCounter).toBeLessThan(poolPrices!.length); + }); + + it('getPoolIdentifiers and getPricesVolume BUY stable pairs', async function () { + const pools = await pancakeswapV3.getPoolIdentifiers( + stableA, + stableB, + SwapSide.BUY, + blockNumber, + ); + console.log( + `${stableASymbol} <> ${stableBSymbol} Pool Identifiers: `, + pools, + ); + + expect(pools.length).toBeGreaterThan(0); + + const poolPrices = await pancakeswapV3.getPricesVolume( + stableA, + stableB, + stableBuyAmounts, + SwapSide.BUY, + blockNumber, + pools, + ); + console.log( + `${stableASymbol} <> ${stableBSymbol} Pool Prices: `, + poolPrices, + ); + + expect(poolPrices).not.toBeNull(); + checkPoolPrices( + poolPrices!.filter(pp => pp.unit !== 0n), + stableBuyAmounts, + SwapSide.BUY, + dexKey, + ); + + // Check if onchain pricing equals to calculated ones + let falseChecksCounter = 0; + await Promise.all( + poolPrices!.map(async price => { + const fee = + pancakeswapV3.eventPools[price.poolIdentifier!]!.feeCode; + const res = await checkOnChainPricing( + pancakeswapV3, + dexHelper, + 'quoteExactOutputSingle', + blockNumber, + price.prices, + stableA.address, + stableB.address, + fee, + stableBuyAmounts, + ); + if (res === false) falseChecksCounter++; + }), + ); + expect(falseChecksCounter).toBeLessThan(poolPrices!.length); + }); + + it('getTopPoolsForToken', async function () { + const poolLiquidity = await pancakeswapV3.getTopPoolsForToken( + stableA.address, + 10, + ); + console.log(`${stableASymbol} Top Pools:`, poolLiquidity); + + if (!pancakeswapV3.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity(poolLiquidity, stableA.address, dexKey); + } + }); + }); + + describe('Regular pairs', function () { + it('getPoolIdentifiers and getPricesVolume SELL', async function () { + const pools = await pancakeswapV3.getPoolIdentifiers( + tokenA, + tokenB, + SwapSide.SELL, + blockNumber, + ); + console.log( + `${tokenASymbol} <> ${tokenBSymbol} Pool Identifiers: `, + pools, + ); + + expect(pools.length).toBeGreaterThan(0); + + const poolPrices = await pancakeswapV3.getPricesVolume( + tokenA, + tokenB, + regularSellAmounts, + SwapSide.SELL, + blockNumber, + pools, + ); + console.log( + `${tokenASymbol} <> ${tokenBSymbol} Pool Prices: `, + poolPrices, + ); + + expect(poolPrices).not.toBeNull(); + checkPoolPrices( + poolPrices!, + regularSellAmounts, + SwapSide.SELL, + dexKey, + ); + + let falseChecksCounter = 0; + await Promise.all( + poolPrices!.map(async price => { + const fee = + pancakeswapV3.eventPools[price.poolIdentifier!]!.feeCode; + const res = await checkOnChainPricing( + pancakeswapV3, + dexHelper, + 'quoteExactInputSingle', + blockNumber, + price.prices, + tokenA.address, + tokenB.address, + fee, + regularSellAmounts, + ); + if (res === false) falseChecksCounter++; + }), + ); + + expect(falseChecksCounter).toBeLessThan(poolPrices!.length); + }); + + it('getPoolIdentifiers and getPricesVolume BUY', async function () { + const pools = await pancakeswapV3.getPoolIdentifiers( + tokenA, + tokenB, + SwapSide.BUY, + blockNumber, + ); + console.log( + `${stableASymbol} <> ${stableBSymbol} Pool Identifiers: `, + pools, + ); + + expect(pools.length).toBeGreaterThan(0); + + const poolPrices = await pancakeswapV3.getPricesVolume( + tokenA, + tokenB, + regularBuyAmounts, + SwapSide.BUY, + blockNumber, + pools, + ); + console.log( + `${tokenASymbol} <> ${tokenBSymbol} Pool Prices: `, + poolPrices, + ); + + expect(poolPrices).not.toBeNull(); + checkPoolPrices( + poolPrices!, + regularBuyAmounts, + SwapSide.BUY, + dexKey, + ); + + // Check if onchain pricing equals to calculated ones + let falseChecksCounter = 0; + await Promise.all( + poolPrices!.map(async price => { + const fee = + pancakeswapV3.eventPools[price.poolIdentifier!]!.feeCode; + const res = await checkOnChainPricing( + pancakeswapV3, + dexHelper, + 'quoteExactOutputSingle', + blockNumber, + price.prices, + tokenA.address, + tokenB.address, + fee, + regularBuyAmounts, + ); + if (res === false) falseChecksCounter++; + }), + ); + expect(falseChecksCounter).toBeLessThan(poolPrices!.length); + }); + + it('getTopPoolsForToken', async function () { + const poolLiquidity = await pancakeswapV3.getTopPoolsForToken( + tokenA.address, + 10, + ); + console.log(`${stableASymbol} Top Pools:`, poolLiquidity); + + if (!pancakeswapV3.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity(poolLiquidity, stableA.address, dexKey); + } + }); + }); + } + }), + ); +}); + +function getReaderCalldata( + exchangeAddress: string, + readerIface: Interface, + amounts: bigint[], + funcName: string, + tokenIn: Address, + tokenOut: Address, + fee: bigint, +) { + return amounts.map(amount => ({ + target: exchangeAddress, + callData: readerIface.encodeFunctionData(funcName, [ + [tokenIn, tokenOut, amount, fee, 0n], + ]), + })); +} + +function decodeReaderResult(results: any, readerIface: any, funcName: any) { + return results.map((result: any) => { + const parsed = readerIface.decodeFunctionResult(funcName, result); + return parsed[0]._hex.toString(); // BigInt'i string'e dönüştürüyoruz + }); +} + +async function checkOnChainPricing( + SquadswapV3: SquadswapV3, + dexHelper: DummyDexHelper, + funcName: string, + blockNumber: number, + prices: bigint[], + tokenIn: Address, + tokenOut: Address, + fee: bigint, + _amounts: bigint[], +) { + // Quoter address + const exchangeAddress = '0x81Da0D4e1157391a22a656ad84AAb9b2716F21e0'; + const readerIface = quoterIface; + + const sum = prices.reduce((acc, curr) => (acc += curr), 0n); + + if (sum === 0n) { + console.log( + `Prices were not calculated for tokenIn=${tokenIn}, tokenOut=${tokenOut}, fee=${fee.toString()}. Most likely price impact is too big for requested amount`, + ); + return false; + } + + const readerCallData = getReaderCalldata( + exchangeAddress, + readerIface, + _amounts.slice(1), + funcName, + tokenIn, + tokenOut, + fee, + ); + + let readerResult; + try { + readerResult = ( + await dexHelper.multiContract.methods + .aggregate(readerCallData) + .call({}, blockNumber) + ).returnData; + } catch (e) { + console.log( + `Can not fetch on-chain pricing for fee ${fee}. It happens for low liquidity pools`, + ); + return false; + } + + const expectedPrices = [0n].concat( + decodeReaderResult(readerResult, readerIface, funcName), + ); + + let firstZeroIndex = prices.slice(1).indexOf(0n); + + // we skipped first, so add +1 on result + firstZeroIndex = firstZeroIndex === -1 ? prices.length : firstZeroIndex; + + console.log('PRICE: ', prices); + console.log('ON-chain prices: ', prices); + + // Compare only the ones for which we were able to calculate prices + expect(prices.slice(0, firstZeroIndex)).toEqual( + expectedPrices.slice(0, firstZeroIndex), + ); + return true; +} diff --git a/src/dex/squadswap-v3/squadswap-v3-pool.ts b/src/dex/squadswap-v3/squadswap-v3-pool.ts new file mode 100644 index 000000000..232b936fe --- /dev/null +++ b/src/dex/squadswap-v3/squadswap-v3-pool.ts @@ -0,0 +1,527 @@ +import _ from 'lodash'; +import { Contract } from 'web3-eth-contract'; +import { Interface } from '@ethersproject/abi'; +import { ethers } from 'ethers'; +import { assert, DeepReadonly } from 'ts-essentials'; +import { Log, Logger, BlockHeader, Address } from '../../types'; +import { + InitializeStateOptions, + StatefulEventSubscriber, +} from '../../stateful-event-subscriber'; +import { IDexHelper } from '../../dex-helper/idex-helper'; +import PancakeswapV3PoolABI from '../../abi/pancakeswap-v3/PancakeswapV3Pool.abi.json'; +import { bigIntify, catchParseLogError, isSampled } from '../../utils'; +import { pancakeswapV3Math } from './contract-math/pancakeswap-v3-math'; +import { MultiCallParams } from '../../lib/multi-wrapper'; +import { + OUT_OF_RANGE_ERROR_POSTFIX, + TICK_BITMAP_BUFFER, + TICK_BITMAP_TO_USE, +} from './constants'; +import { TickBitMap } from './contract-math/TickBitMap'; +import { uint256ToBigInt } from '../../lib/decoders'; +import { + DecodedStateMultiCallResultWithRelativeBitmaps, + PoolState, +} from '../uniswap-v3/types'; +import { decodeStateMultiCallResultWithRelativeBitmaps } from './utils'; +import { + _reduceTickBitmap, + _reduceTicks, +} from '../uniswap-v3/contract-math/utils'; + +export class SquadswapV3EventPool extends StatefulEventSubscriber { + handlers: { + [event: string]: ( + event: any, + pool: PoolState, + log: Log, + blockHeader: Readonly, + ) => PoolState; + } = {}; + + logDecoder: (log: Log) => any; + + readonly token0: Address; + + readonly token1: Address; + + private _poolAddress?: Address; + + private _stateRequestCallData?: MultiCallParams< + bigint | DecodedStateMultiCallResultWithRelativeBitmaps + >[]; + + public readonly poolIface = new Interface(PancakeswapV3PoolABI); + + public initFailed = false; + public initRetryAttemptCount = 0; + + public readonly feeCodeAsString; + + constructor( + readonly dexHelper: IDexHelper, + parentName: string, + readonly stateMultiContract: Contract, + readonly erc20Interface: Interface, + protected readonly factoryAddress: Address, + public readonly feeCode: bigint, + token0: Address, + token1: Address, + logger: Logger, + mapKey: string = '', + readonly poolInitCodeHash: string, + readonly poolDeployer?: string, + ) { + super( + parentName, + `${token0}_${token1}_${feeCode}`, + dexHelper, + logger, + true, + mapKey, + ); + this.feeCodeAsString = feeCode.toString(); + this.token0 = token0.toLowerCase(); + this.token1 = token1.toLowerCase(); + this.logDecoder = (log: Log) => this.poolIface.parseLog(log); + this.addressesSubscribed = new Array
(1); + + // Add handlers + this.handlers['Swap'] = this.handleSwapEvent.bind(this); + this.handlers['Burn'] = this.handleBurnEvent.bind(this); + this.handlers['Mint'] = this.handleMintEvent.bind(this); + this.handlers['SetFeeProtocol'] = this.handleSetFeeProtocolEvent.bind(this); + this.handlers['IncreaseObservationCardinalityNext'] = + this.handleIncreaseObservationCardinalityNextEvent.bind(this); + + // Wen need them to keep balance of the pool up to date + this.handlers['Collect'] = this.handleCollectEvent.bind(this); + // Almost the same as Collect, but for pool owners + this.handlers['CollectProtocol'] = this.handleCollectEvent.bind(this); + this.handlers['Flash'] = this.handleFlashEvent.bind(this); + } + + get poolAddress() { + if (this._poolAddress === undefined) { + this._poolAddress = this._computePoolAddress( + this.token0, + this.token1, + this.feeCode, + ); + } + return this._poolAddress; + } + + set poolAddress(address: Address) { + this._poolAddress = address.toLowerCase(); + } + + async initialize( + blockNumber: number, + options?: InitializeStateOptions, + ) { + await super.initialize(blockNumber, options); + } + + protected getPoolIdentifierData() { + return { + token0: this.token0, + token1: this.token1, + fee: this.feeCode, + }; + } + + protected async processBlockLogs( + state: DeepReadonly, + logs: Readonly[], + blockHeader: Readonly, + ): Promise | null> { + const newState = await super.processBlockLogs(state, logs, blockHeader); + if (newState && !newState.isValid) { + return await this.generateState(blockHeader.number); + } + return newState; + } + + protected processLog( + state: DeepReadonly, + log: Readonly, + blockHeader: Readonly, + ): DeepReadonly | null { + try { + const event = this.logDecoder(log); + + const uniswapV3EventLoggingSampleRate = + this.dexHelper.config.data.uniswapV3EventLoggingSampleRate; + if ( + !this.dexHelper.config.isSlave && + uniswapV3EventLoggingSampleRate && + isSampled(uniswapV3EventLoggingSampleRate) + ) { + this.logger.info( + `event=${event.name} - block=${ + blockHeader.number + }. Log sampled at rate ${uniswapV3EventLoggingSampleRate * 100}%`, + ); + } + + if (event.name in this.handlers) { + // Because we have observations in array which is mutable by nature, there is a + // ts compile error: https://stackoverflow.com/questions/53412934/disable-allowing-assigning-readonly-types-to-non-readonly-types + // And there is no good workaround, so turn off the type checker for this line + const _state = _.cloneDeep(state) as PoolState; + try { + return this.handlers[event.name](event, _state, log, blockHeader); + } catch (e) { + if ( + e instanceof Error && + e.message.endsWith(OUT_OF_RANGE_ERROR_POSTFIX) + ) { + this.logger.warn( + `${this.parentName}: Pool ${this.poolAddress} on ${ + this.dexHelper.config.data.network + } is out of TickBitmap requested range. Re-query the state. ${JSON.stringify( + event, + )}`, + e, + ); + } else { + this.logger.error( + `${this.parentName}: Pool ${this.poolAddress}, ` + + `network=${this.dexHelper.config.data.network}: Unexpected ` + + `error while handling event on blockNumber=${blockHeader.number}, ` + + `blockHash=${blockHeader.hash} and parentHash=${ + blockHeader.parentHash + } for UniswapV3, ${JSON.stringify(event)}`, + e, + ); + } + _state.isValid = false; + return _state; + } + } + } catch (e) { + catchParseLogError(e, this.logger); + } + return null; // ignore unrecognized event + } + + private _getStateRequestCallData() { + if (!this._stateRequestCallData) { + const callData: MultiCallParams< + bigint | DecodedStateMultiCallResultWithRelativeBitmaps + >[] = [ + { + target: this.token0, + callData: this.erc20Interface.encodeFunctionData('balanceOf', [ + this.poolAddress, + ]), + decodeFunction: uint256ToBigInt, + }, + { + target: this.token1, + callData: this.erc20Interface.encodeFunctionData('balanceOf', [ + this.poolAddress, + ]), + decodeFunction: uint256ToBigInt, + }, + { + target: this.stateMultiContract.options.address, + callData: this.stateMultiContract.methods + .getFullStateWithRelativeBitmaps( + this.factoryAddress, + this.token0, + this.token1, + this.feeCode, + this.getBitmapRangeToRequest(), + this.getBitmapRangeToRequest(), + ) + .encodeABI(), + decodeFunction: decodeStateMultiCallResultWithRelativeBitmaps, + }, + ]; + this._stateRequestCallData = callData; + } + return this._stateRequestCallData; + } + + getBitmapRangeToRequest() { + return TICK_BITMAP_TO_USE + TICK_BITMAP_BUFFER; + } + + async generateState(blockNumber: number): Promise> { + const callData = this._getStateRequestCallData(); + + const [resBalance0, resBalance1, resState] = + await this.dexHelper.multiWrapper.tryAggregate< + bigint | DecodedStateMultiCallResultWithRelativeBitmaps + >( + false, + callData, + blockNumber, + this.dexHelper.multiWrapper.defaultBatchSize, + false, + ); + + // Quite ugly solution, but this is the one that fits to current flow. + // I think UniswapV3 callbacks subscriptions are complexified for no reason. + // Need to be revisited later + assert(resState.success, 'Pool does not exist'); + + const [balance0, balance1, _state] = [ + resBalance0.returnData, + resBalance1.returnData, + resState.returnData, + ] as [bigint, bigint, DecodedStateMultiCallResultWithRelativeBitmaps]; + + const tickBitmap = {}; + const ticks = {}; + + _reduceTickBitmap(tickBitmap, _state.tickBitmap); + _reduceTicks(ticks, _state.ticks); + + const observations = { + [_state.slot0.observationIndex]: { + blockTimestamp: bigIntify(_state.observation.blockTimestamp), + tickCumulative: bigIntify(_state.observation.tickCumulative), + secondsPerLiquidityCumulativeX128: bigIntify( + _state.observation.secondsPerLiquidityCumulativeX128, + ), + initialized: _state.observation.initialized, + }, + }; + + const currentTick = bigIntify(_state.slot0.tick); + const tickSpacing = bigIntify(_state.tickSpacing); + + const startTickBitmap = TickBitMap.position(currentTick / tickSpacing)[0]; + const requestedRange = this.getBitmapRangeToRequest(); + + return { + pool: _state.pool, + blockTimestamp: bigIntify(_state.blockTimestamp), + slot0: { + sqrtPriceX96: bigIntify(_state.slot0.sqrtPriceX96), + tick: currentTick, + observationIndex: +_state.slot0.observationIndex, + observationCardinality: +_state.slot0.observationCardinality, + observationCardinalityNext: +_state.slot0.observationCardinalityNext, + feeProtocol: bigIntify(_state.slot0.feeProtocol), + }, + liquidity: bigIntify(_state.liquidity), + fee: this.feeCode, + tickSpacing, + maxLiquidityPerTick: bigIntify(_state.maxLiquidityPerTick), + tickBitmap, + ticks, + observations, + isValid: true, + startTickBitmap, + lowestKnownTick: + (BigInt.asIntN(24, startTickBitmap - requestedRange) << 8n) * + tickSpacing, + highestKnownTick: + ((BigInt.asIntN(24, startTickBitmap + requestedRange) << 8n) + + BigInt.asIntN(24, 255n)) * + tickSpacing, + balance0, + balance1, + }; + } + + handleSwapEvent( + event: any, + pool: PoolState, + log: Log, + blockHeader: BlockHeader, + ) { + const newSqrtPriceX96 = bigIntify(event.args.sqrtPriceX96); + const amount0 = bigIntify(event.args.amount0); + const amount1 = bigIntify(event.args.amount1); + const newTick = bigIntify(event.args.tick); + const newLiquidity = bigIntify(event.args.liquidity); + // const protocolFeesToken0 = bigIntify(event.arg.protocolFeesToken0); + // const protocolFeesToken1 = bigIntify(event.arg.protocolFeesToken1); + + pool.blockTimestamp = bigIntify(blockHeader.timestamp); + + if (amount0 <= 0n && amount1 <= 0n) { + this.logger.error( + `${this.parentName}: amount0 <= 0n && amount1 <= 0n for ` + + `${this.poolAddress} and ${blockHeader.number}. Check why it happened`, + ); + pool.isValid = false; + return pool; + } else { + const zeroForOne = amount0 > 0n; + + pancakeswapV3Math.swapFromEvent( + pool, + newSqrtPriceX96, + newTick, + newLiquidity, + zeroForOne, + ); + + if (zeroForOne) { + if (amount1 < 0n) { + pool.balance1 -= BigInt.asUintN(256, -amount1); + } else { + this.logger.error( + `In swapEvent for pool ${pool.pool} received incorrect values ${zeroForOne} and ${amount1}`, + ); + pool.isValid = false; + } + // This is not correct fully, because pool may get more tokens then it needs, but + // it is not accounted in internal state, it should be good enough + pool.balance0 += BigInt.asUintN(256, amount0); + } else { + if (amount0 < 0n) { + pool.balance0 -= BigInt.asUintN(256, -amount0); + } else { + this.logger.error( + `In swapEvent for pool ${pool.pool} received incorrect values ${zeroForOne} and ${amount0}`, + ); + pool.isValid = false; + } + pool.balance1 += BigInt.asUintN(256, amount1); + } + + return pool; + } + } + + handleBurnEvent( + event: any, + pool: PoolState, + log: Log, + blockHeader: BlockHeader, + ) { + const amount = bigIntify(event.args.amount); + const tickLower = bigIntify(event.args.tickLower); + const tickUpper = bigIntify(event.args.tickUpper); + pool.blockTimestamp = bigIntify(blockHeader.timestamp); + + pancakeswapV3Math._modifyPosition(pool, { + tickLower, + tickUpper, + liquidityDelta: -BigInt.asIntN(128, BigInt.asIntN(256, amount)), + }); + + // From this transaction I conclude that there is no balance change from + // Burn event: https://dashboard.tenderly.co/tx/mainnet/0xfccf5341147ac3ad0e66452273d12dfc3219e81f8fb369a6cdecfb24b9b9d078/logs + // And it aligns with UniswapV3 doc: + // https://github.com/Uniswap/v3-core/blob/05c10bf6d547d6121622ac51c457f93775e1df09/contracts/interfaces/pool/IUniswapV3PoolActions.sol#L59 + // It just updates positions and tokensOwed which may be requested calling collect + // So, we don't need to update pool.balances0 and pool.balances1 here + + return pool; + } + + handleMintEvent( + event: any, + pool: PoolState, + log: Log, + blockHeader: BlockHeader, + ) { + const amount = bigIntify(event.args.amount); + const tickLower = bigIntify(event.args.tickLower); + const tickUpper = bigIntify(event.args.tickUpper); + const amount0 = bigIntify(event.args.amount0); + const amount1 = bigIntify(event.args.amount1); + pool.blockTimestamp = bigIntify(blockHeader.timestamp); + + pancakeswapV3Math._modifyPosition(pool, { + tickLower, + tickUpper, + liquidityDelta: amount, + }); + + pool.balance0 += amount0; + pool.balance1 += amount1; + + return pool; + } + + handleSetFeeProtocolEvent( + event: any, + pool: PoolState, + log: Log, + blockHeader: BlockHeader, + ) { + const feeProtocol0 = bigIntify(event.args.feeProtocol0New); + const feeProtocol1 = bigIntify(event.args.feeProtocol1New); + pool.slot0.feeProtocol = feeProtocol0 + (feeProtocol1 << 16n); + pool.blockTimestamp = bigIntify(blockHeader.timestamp); + + return pool; + } + + handleCollectEvent( + event: any, + pool: PoolState, + log: Log, + blockHeader: BlockHeader, + ) { + const amount0 = bigIntify(event.args.amount0); + const amount1 = bigIntify(event.args.amount1); + pool.balance0 -= amount0; + pool.balance1 -= amount1; + pool.blockTimestamp = bigIntify(blockHeader.timestamp); + + return pool; + } + + handleFlashEvent( + event: any, + pool: PoolState, + log: Log, + blockHeader: BlockHeader, + ) { + const paid0 = bigIntify(event.args.paid0); + const paid1 = bigIntify(event.args.paid1); + + pool.balance0 += paid0; + pool.balance1 += paid1; + pool.blockTimestamp = bigIntify(blockHeader.timestamp); + + return pool; + } + + handleIncreaseObservationCardinalityNextEvent( + event: any, + pool: PoolState, + log: Log, + blockHeader: BlockHeader, + ) { + pool.slot0.observationCardinalityNext = parseInt( + event.args.observationCardinalityNextNew, + 10, + ); + pool.blockTimestamp = bigIntify(blockHeader.timestamp); + return pool; + } + + private _computePoolAddress( + token0: Address, + token1: Address, + fee: bigint, + ): Address { + // https://github.com/Uniswap/v3-periphery/blob/main/contracts/libraries/PoolAddress.sol + if (token0 > token1) [token0, token1] = [token1, token0]; + + const encodedKey = ethers.utils.keccak256( + ethers.utils.defaultAbiCoder.encode( + ['address', 'address', 'uint24'], + [token0, token1, BigInt.asUintN(24, fee)], + ), + ); + + return ethers.utils.getCreate2Address( + this.poolDeployer ? this.poolDeployer : this.factoryAddress, + encodedKey, + this.poolInitCodeHash, + ); + } +} diff --git a/src/dex/squadswap-v3/squadswap-v3.ts b/src/dex/squadswap-v3/squadswap-v3.ts new file mode 100644 index 000000000..cf45348fc --- /dev/null +++ b/src/dex/squadswap-v3/squadswap-v3.ts @@ -0,0 +1,1141 @@ +import { defaultAbiCoder, Interface } from '@ethersproject/abi'; +import _ from 'lodash'; +import { pack } from '@ethersproject/solidity'; +import { + Token, + Address, + ExchangePrices, + AdapterExchangeParam, + SimpleExchangeParam, + PoolLiquidity, + Logger, + NumberAsString, + PoolPrices, + DexExchangeParam, +} from '../../types'; +import { SwapSide, Network, CACHE_PREFIX } from '../../constants'; +import * as CALLDATA_GAS_COST from '../../calldata-gas-cost'; +import { + getBigIntPow, + getDexKeysWithNetwork, + interpolate, + isTruthy, +} from '../../utils'; +import { IDex } from '../../dex/idex'; +import { IDexHelper } from '../../dex-helper/idex-helper'; +import { + DexParams, + OutputResult, + PoolState, + UniswapV3Data, + UniswapV3Functions, + UniswapV3SimpleSwapParams, +} from '../uniswap-v3/types'; +import { + getLocalDeadlineAsFriendlyPlaceholder, + SimpleExchange, +} from '../simple-exchange'; +import { SquadswapV3Config, Adapters } from './config'; +import { SquadswapV3EventPool } from './squadswap-v3-pool'; +import PancakeswapV3RouterABI from '../../abi/pancakeswap-v3/PancakeswapV3Router.abi.json'; +import PancakeswapV3QuoterABI from '../../abi/pancakeswap-v3/PancakeswapV3Quoter.abi.json'; +import UniswapV3MultiABI from '../../abi/uniswap-v3/UniswapMulti.abi.json'; +import PancakeswapV3StateMulticallABI from '../../abi/pancakeswap-v3/PancakeV3StateMulticall.abi.json'; +import { + SQUADSWAPV3_EFFICIENCY_FACTOR, + SQUADSWAPV3_TICK_BASE_OVERHEAD, + SQUADSWAPV3_POOL_SEARCH_OVERHEAD, + SQUADSWAPV3_TICK_GAS_COST, +} from './constants'; +import { DeepReadonly } from 'ts-essentials'; +import { pancakeswapV3Math } from './contract-math/pancakeswap-v3-math'; +import { Contract } from 'web3-eth-contract'; +import { AbiItem } from 'web3-utils'; +import { BalanceRequest, getBalances } from '../../lib/tokens/balancer-fetcher'; +import { + AssetType, + DEFAULT_ID_ERC20, + DEFAULT_ID_ERC20_AS_STRING, +} from '../../lib/tokens/types'; +import { + OnPoolCreatedCallback, + SquadswapV3Factory, +} from './squadswap-v3-factory'; +import { extractReturnAmountPosition } from '../../executor/utils'; + +type PoolPairsInfo = { + token0: Address; + token1: Address; + fee: string; +}; + +const PoolsRegistryHashKey = `${CACHE_PREFIX}_poolsRegistry`; + +const PANCAKESWAPV3_CLEAN_NOT_EXISTING_POOL_TTL_MS = 3 * 24 * 60 * 60 * 1000; // 3 days +const PANCAKESWAPV3_CLEAN_NOT_EXISTING_POOL_INTERVAL_MS = 24 * 60 * 60 * 1000; // Once in a day +const PANCAKESWAPV3_QUOTE_GASLIMIT = 200_000; + +export class SquadswapV3 extends SimpleExchange implements IDex { + private readonly factory: SquadswapV3Factory; + readonly isFeeOnTransferSupported: boolean = false; + readonly eventPools: Record = {}; + + readonly hasConstantPriceLargeAmounts = false; + readonly needWrapNative = true; + + intervalTask?: NodeJS.Timeout; + + public static dexKeysWithNetwork: { key: string; networks: Network[] }[] = + getDexKeysWithNetwork(_.pick(SquadswapV3Config, ['SquadswapV3'])); + + logger: Logger; + + private uniswapMulti: Contract; + private stateMultiContract: Contract; + + private notExistingPoolSetKey: string; + + constructor( + protected network: Network, + dexKey: string, + protected dexHelper: IDexHelper, + protected adapters = Adapters[network] || {}, + readonly routerIface = new Interface(PancakeswapV3RouterABI), + readonly quoterIface = new Interface(PancakeswapV3QuoterABI), + protected config = SquadswapV3Config['SquadswapV3'][network], // protected poolsToPreload = PoolsToPreload[dexKey][network] || [], + ) { + super(dexHelper, dexKey); + this.logger = dexHelper.getLogger(dexKey + '-' + network); + this.uniswapMulti = new this.dexHelper.web3Provider.eth.Contract( + UniswapV3MultiABI as AbiItem[], + this.config.uniswapMulticall, + ); + this.stateMultiContract = new this.dexHelper.web3Provider.eth.Contract( + PancakeswapV3StateMulticallABI as AbiItem[], + this.config.stateMulticall, + ); + // To receive revert reasons + this.dexHelper.web3Provider.eth.handleRevert = false; + + // Normalise once all config addresses and use across all scenarios + this.config = this._toLowerForAllConfigAddresses(); + + this.notExistingPoolSetKey = + `${CACHE_PREFIX}_${network}_${dexKey}_not_existings_pool_set`.toLowerCase(); + + this.factory = new SquadswapV3Factory( + dexHelper, + dexKey, + this.config.factory, + this.logger, + this.onPoolCreatedDeleteFromNonExistingSet, + ); + } + + get supportedFees() { + return this.config.supportedFees; + } + + getAdapters(side: SwapSide): { name: string; index: number }[] | null { + return this.adapters[side] ? this.adapters[side] : null; + } + + getPoolIdentifier(srcAddress: Address, destAddress: Address, fee: bigint) { + const tokenAddresses = this._sortTokens(srcAddress, destAddress).join('_'); + return `${this.dexKey}_${tokenAddresses}_${fee}`; + } + + async initializePricing(blockNumber: number) { + // Init listening to new pools creation + await this.factory.initialize(blockNumber); + + if (!this.dexHelper.config.isSlave) { + const cleanExpiredNotExistingPoolsKeys = async () => { + const maxTimestamp = + Date.now() - PANCAKESWAPV3_CLEAN_NOT_EXISTING_POOL_TTL_MS; + await this.dexHelper.cache.zremrangebyscore( + this.notExistingPoolSetKey, + 0, + maxTimestamp, + ); + }; + + void cleanExpiredNotExistingPoolsKeys(); + + this.intervalTask = setInterval( + cleanExpiredNotExistingPoolsKeys.bind(this), + PANCAKESWAPV3_CLEAN_NOT_EXISTING_POOL_INTERVAL_MS, + ); + } + } + + /* + * When a non existing pool is queried, it's blacklisted for an arbitrary long period in order to prevent issuing too many rpc calls + * Once the pool is created, it gets immediately flagged + */ + onPoolCreatedDeleteFromNonExistingSet: OnPoolCreatedCallback = async ({ + token0, + token1, + fee, + }) => { + const logPrefix = '[onPoolCreatedDeleteFromNonExistingSet]'; + const [_token0, _token1] = this._sortTokens(token0, token1); + const poolKey = `${_token0}_${_token1}_${fee}`; + + // consider doing it only from master pool for less calls to distant cache + + // delete entry locally to let local instance discover the pool + delete this.eventPools[this.getPoolIdentifier(_token0, _token1, fee)]; + + try { + this.logger.info( + `${logPrefix} delete pool from not existing set=${this.notExistingPoolSetKey}; key=${poolKey}`, + ); + // delete pool record from set + const result = await this.dexHelper.cache.zrem( + this.notExistingPoolSetKey, + [poolKey], + ); + this.logger.info( + `${logPrefix} delete pool from not existing set=${this.notExistingPoolSetKey}; key=${poolKey}; result: ${result}`, + ); + } catch (error) { + this.logger.error( + `${logPrefix} ERROR: failed to delete pool from set: set=${this.notExistingPoolSetKey}; key=${poolKey}`, + error, + ); + } + }; + + async getPool( + srcAddress: Address, + destAddress: Address, + fee: bigint, + blockNumber: number, + ): Promise { + let pool = this.eventPools[ + this.getPoolIdentifier(srcAddress, destAddress, fee) + ] as SquadswapV3EventPool | null | undefined; + + if (pool === null) return null; + + if (pool) { + if (!pool.initFailed) { + return pool; + } else { + // if init failed then prefer to early return pool with empty state to fallback to rpc call + if ( + ++pool.initRetryAttemptCount % this.config.initRetryFrequency !== + 0 + ) { + return pool; + } + // else pursue with re-try initialization + } + } + + const [token0, token1] = this._sortTokens(srcAddress, destAddress); + + const key = `${token0}_${token1}_${fee}`.toLowerCase(); + + // no need to run this logic on retry initialisation scenario + if (!pool) { + const notExistingPoolScore = await this.dexHelper.cache.zscore( + this.notExistingPoolSetKey, + key, + ); + + const poolDoesNotExist = notExistingPoolScore !== null; + + if (poolDoesNotExist) { + this.eventPools[this.getPoolIdentifier(srcAddress, destAddress, fee)] = + null; + return null; + } + } + + this.logger.trace(`starting to listen to new pool: ${key}`); + pool = + pool || + new SquadswapV3EventPool( + this.dexHelper, + this.dexKey, + this.stateMultiContract, + this.erc20Interface, + this.config.factory, + fee, + token0, + token1, + this.logger, + this.cacheStateKey, + this.config.initHash, + this.config.deployer, + ); + + try { + await pool.initialize(blockNumber, { + initCallback: (state: DeepReadonly) => { + //really hacky, we need to push poolAddress so that we subscribeToLogs in StatefulEventSubscriber + pool!.addressesSubscribed[0] = state.pool; + pool!.poolAddress = state.pool; + pool!.initFailed = false; + pool!.initRetryAttemptCount = 0; + }, + }); + } catch (e) { + if (e instanceof Error && e.message.endsWith('Pool does not exist')) { + // no need to await we want the set to have the pool key but it's not blocking + this.dexHelper.cache.zadd( + this.notExistingPoolSetKey, + [Date.now(), key], + 'NX', + ); + + // Pool does not exist for this feeCode, so we can set it to null + // to prevent more requests for this pool + pool = null; + this.logger.trace( + `${this.dexHelper}: Pool: srcAddress=${srcAddress}, destAddress=${destAddress}, fee=${fee} not found`, + e, + ); + } else { + // on unkown error mark as failed and increase retryCount for retry init strategy + // note: state would be null by default which allows to fallback + this.logger.warn( + `${this.dexKey}: Can not generate pool state for srcAddress=${srcAddress}, destAddress=${destAddress}, fee=${fee} pool fallback to rpc and retry every ${this.config.initRetryFrequency} times, initRetryAttemptCount=${pool.initRetryAttemptCount}`, + e, + ); + pool.initFailed = true; + } + } + + if (pool !== null) { + const allEventPools = Object.values(this.eventPools); + // if pool was created, delete pool record from non existing set + this.dexHelper.cache + .zrem(this.notExistingPoolSetKey, [key]) + .catch(() => {}); + this.logger.info( + `starting to listen to new non-null pool: ${key}. Already following ${allEventPools + // Not that I like this reduce, but since it is done only on initialization, expect this to be ok + .reduce( + (acc, curr) => (curr !== null ? ++acc : acc), + 0, + )} non-null pools or ${allEventPools.length} total pools`, + ); + } + + this.eventPools[this.getPoolIdentifier(srcAddress, destAddress, fee)] = + pool; + return pool; + } + + async addMasterPool(poolKey: string, blockNumber: number): Promise { + const _pairs = await this.dexHelper.cache.hget( + PoolsRegistryHashKey, + `${this.cacheStateKey}_${poolKey}`, + ); + if (!_pairs) { + this.logger.warn( + `did not find poolConfig in for key ${PoolsRegistryHashKey} ${this.cacheStateKey}_${poolKey}`, + ); + return false; + } + + const poolInfo: PoolPairsInfo = JSON.parse(_pairs); + + const pool = await this.getPool( + poolInfo.token0, + poolInfo.token1, + BigInt(poolInfo.fee), + blockNumber, + ); + + if (!pool) { + return false; + } + + return true; + } + + async getPoolIdentifiers( + srcToken: Token, + destToken: Token, + side: SwapSide, + blockNumber: number, + ): Promise { + const _srcToken = this.dexHelper.config.wrapETH(srcToken); + const _destToken = this.dexHelper.config.wrapETH(destToken); + + const [_srcAddress, _destAddress] = this._getLoweredAddresses( + _srcToken, + _destToken, + ); + + if (_srcAddress === _destAddress) return []; + + const pools = ( + await Promise.all( + this.supportedFees.map(async fee => + this.getPool(_srcAddress, _destAddress, fee, blockNumber), + ), + ) + ).filter(pool => pool); + + if (pools.length === 0) return []; + + return pools.map(pool => + this.getPoolIdentifier(_srcAddress, _destAddress, pool!.feeCode), + ); + } + + async getPricingFromRpc( + from: Token, + to: Token, + amounts: bigint[], + side: SwapSide, + pools: SquadswapV3EventPool[], + ): Promise | null> { + if (pools.length === 0) { + return null; + } + this.logger.warn(`fallback to rpc for ${pools.length} pool(s)`); + + const requests = pools.map( + pool => ({ + owner: pool.poolAddress, + asset: side == SwapSide.SELL ? from.address : to.address, + assetType: AssetType.ERC20, + ids: [ + { + id: DEFAULT_ID_ERC20, + spenders: [], + }, + ], + }), + [], + ); + + const balances = await getBalances(this.dexHelper.multiWrapper, requests); + + pools = pools.filter((pool, index) => { + const balance = balances[index].amounts[DEFAULT_ID_ERC20_AS_STRING]; + if (balance >= amounts[amounts.length - 1]) { + return true; + } + this.logger.warn( + `[${this.network}][${pool.parentName}] have no balance ${pool.poolAddress} ${from.address} ${to.address}. (Balance: ${balance})`, + ); + return false; + }); + + pools.forEach(pool => { + this.logger.warn( + `[${this.network}][${pool.parentName}] fallback to rpc for ${pool.name}`, + ); + }); + + const unitVolume = getBigIntPow( + (side === SwapSide.SELL ? from : to).decimals, + ); + + const chunks = amounts.length - 1; + + const _width = Math.floor(chunks / this.config.chunksCount); + + const _amounts = [unitVolume].concat( + Array.from(Array(this.config.chunksCount).keys()).map( + i => amounts[(i + 1) * _width], + ), + ); + + const calldata = pools.map(pool => + _amounts.map(_amount => ({ + target: this.config.quoter, + gasLimit: PANCAKESWAPV3_QUOTE_GASLIMIT, + callData: + side === SwapSide.SELL + ? this.quoterIface.encodeFunctionData('quoteExactInputSingle', [ + [ + from.address, + to.address, + _amount.toString(), + pool.feeCodeAsString, + 0, //sqrtPriceLimitX96 + ], + ]) + : this.quoterIface.encodeFunctionData('quoteExactOutputSingle', [ + [ + from.address, + to.address, + _amount.toString(), + pool.feeCodeAsString, + 0, //sqrtPriceLimitX96 + ], + ]), + })), + ); + + const data = await this.uniswapMulti.methods + .multicall(calldata.flat()) + .call(); + + const decode = (j: number): bigint => { + if (!data.returnData[j].success) { + return 0n; + } + const decoded = defaultAbiCoder.decode( + ['uint256'], + data.returnData[j].returnData, + ); + return BigInt(decoded[0].toString()); + }; + + let i = 0; + const result = pools.map(pool => { + const _rates = _amounts.map(() => decode(i++)); + const unit: bigint = _rates[0]; + + const prices = interpolate( + _amounts.slice(1), + _rates.slice(1), + amounts, + side, + ); + + return { + prices, + unit, + data: { + path: [ + { + tokenIn: from.address, + tokenOut: to.address, + fee: pool.feeCodeAsString, + }, + ], + }, + poolIdentifier: this.getPoolIdentifier( + pool.token0, + pool.token1, + pool.feeCode, + ), + exchange: this.dexKey, + gasCost: prices.map(p => (p === 0n ? 0 : PANCAKESWAPV3_QUOTE_GASLIMIT)), + poolAddresses: [pool.poolAddress], + }; + }); + + return result; + } + + async getPricesVolume( + srcToken: Token, + destToken: Token, + amounts: bigint[], + side: SwapSide, + blockNumber: number, + limitPools?: string[], + ): Promise> { + try { + const _srcToken = this.dexHelper.config.wrapETH(srcToken); + const _destToken = this.dexHelper.config.wrapETH(destToken); + + const [_srcAddress, _destAddress] = this._getLoweredAddresses( + _srcToken, + _destToken, + ); + + if (_srcAddress === _destAddress) return null; + + let selectedPools: SquadswapV3EventPool[] = []; + + if (!limitPools) { + selectedPools = ( + await Promise.all( + this.supportedFees.map(async fee => { + const locallyFoundPool = + this.eventPools[ + this.getPoolIdentifier(_srcAddress, _destAddress, fee) + ]; + if (locallyFoundPool) return locallyFoundPool; + + const newlyFetchedPool = await this.getPool( + _srcAddress, + _destAddress, + fee, + blockNumber, + ); + return newlyFetchedPool; + }), + ) + ).filter(isTruthy); + } else { + const pairIdentifierWithoutFee = this.getPoolIdentifier( + _srcAddress, + _destAddress, + 0n, + // Trim from 0 fee postfix, so it become comparable + ).slice(0, -1); + + const poolIdentifiers = limitPools.filter(identifier => + identifier.startsWith(pairIdentifierWithoutFee), + ); + + selectedPools = ( + await Promise.all( + poolIdentifiers.map(async identifier => { + let locallyFoundPool = this.eventPools[identifier]; + if (locallyFoundPool) return locallyFoundPool; + + const [, srcAddress, destAddress, fee] = identifier.split('_'); + const newlyFetchedPool = await this.getPool( + srcAddress, + destAddress, + BigInt(fee), + blockNumber, + ); + return newlyFetchedPool; + }), + ) + ).filter(isTruthy); + } + + if (selectedPools.length === 0) return null; + + const poolsToUse = selectedPools.reduce( + (acc, pool) => { + let state = pool.getState(blockNumber); + if (state === null) { + this.logger.trace( + `${this.dexKey}: State === null. Fallback to rpc ${pool.name}`, + ); + acc.poolWithoutState.push(pool); + } else { + acc.poolWithState.push(pool); + } + return acc; + }, + { + poolWithState: [] as SquadswapV3EventPool[], + poolWithoutState: [] as SquadswapV3EventPool[], + }, + ); + + const rpcResultsPromise = this.getPricingFromRpc( + _srcToken, + _destToken, + amounts, + side, + this.network === Network.ZKEVM ? [] : poolsToUse.poolWithoutState, + ); + + const states = poolsToUse.poolWithState.map( + p => p.getState(blockNumber)!, + ); + + const unitAmount = getBigIntPow( + side == SwapSide.SELL ? _srcToken.decimals : _destToken.decimals, + ); + + const _amounts = [...amounts.slice(1)]; + + const [token0] = this._sortTokens(_srcAddress, _destAddress); + + const zeroForOne = token0 === _srcAddress ? true : false; + + const result = await Promise.all( + poolsToUse.poolWithState.map(async (pool, i) => { + const state = states[i]; + + if (state.liquidity <= 0n) { + if (state.liquidity < 0) { + this.logger.error( + `${this.dexKey}-${this.network}: ${pool.poolAddress} pool has negative liquidity: ${state.liquidity}. Find with key: ${pool.mapKey}`, + ); + } + this.logger.trace(`pool have 0 liquidity`); + return null; + } + + const balanceDestToken = + _destAddress === pool.token0 ? state.balance0 : state.balance1; + + const unitResult = this._getOutputs( + state, + [unitAmount], + zeroForOne, + side, + balanceDestToken, + ); + const pricesResult = this._getOutputs( + state, + _amounts, + zeroForOne, + side, + balanceDestToken, + ); + + if (!pricesResult) { + this.logger.debug('Prices or unit is not calculated'); + return null; + } + + const prices = [0n, ...pricesResult.outputs]; + const gasCost = [ + 0, + ...pricesResult.outputs.map((p, index) => { + if (p == 0n) { + return 0; + } else { + return ( + SQUADSWAPV3_POOL_SEARCH_OVERHEAD + + SQUADSWAPV3_TICK_BASE_OVERHEAD + + pricesResult.tickCounts[index] * SQUADSWAPV3_TICK_GAS_COST + ); + } + }), + ]; + return { + unit: unitResult?.outputs[0] || 0n, + prices, + data: { + path: [ + { + tokenIn: _srcAddress, + tokenOut: _destAddress, + fee: pool.feeCode.toString(), + }, + ], + }, + poolIdentifier: this.getPoolIdentifier( + pool.token0, + pool.token1, + pool.feeCode, + ), + exchange: this.dexKey, + gasCost: gasCost, + poolAddresses: [pool.poolAddress], + }; + }), + ); + const rpcResults = await rpcResultsPromise; + + const notNullResult = result.filter( + res => res !== null, + ) as ExchangePrices; + + if (rpcResults) { + rpcResults.forEach(r => { + if (r) { + notNullResult.push(r); + } + }); + } + + return notNullResult; + } catch (e) { + this.logger.error( + `Error_getPricesVolume ${srcToken.symbol || srcToken.address}, ${ + destToken.symbol || destToken.address + }, ${side}:`, + e, + ); + return null; + } + } + + getAdapterParam( + srcToken: string, + destToken: string, + srcAmount: string, + destAmount: string, + data: UniswapV3Data, + side: SwapSide, + ): AdapterExchangeParam { + const { path: rawPath } = data; + const path = this._encodePath(rawPath, side); + + const payload = this.abiCoder.encodeParameter( + { + ParentStruct: { + path: 'bytes', + deadline: 'uint256', + }, + }, + { + path, + deadline: getLocalDeadlineAsFriendlyPlaceholder(), // FIXME: more gas efficient to pass block.timestamp in adapter + }, + ); + + return { + targetExchange: this.config.router, + payload, + networkFee: '0', + }; + } + + getCalldataGasCost(poolPrices: PoolPrices): number | number[] { + const gasCost = + CALLDATA_GAS_COST.DEX_OVERHEAD + + CALLDATA_GAS_COST.LENGTH_SMALL + + // ParentStruct header + CALLDATA_GAS_COST.OFFSET_SMALL + + // ParentStruct -> path header + CALLDATA_GAS_COST.OFFSET_SMALL + + // ParentStruct -> deadline + CALLDATA_GAS_COST.TIMESTAMP + + // ParentStruct -> path (20+3+20 = 43 = 32+11 bytes) + CALLDATA_GAS_COST.LENGTH_SMALL + + CALLDATA_GAS_COST.FULL_WORD + + CALLDATA_GAS_COST.wordNonZeroBytes(11); + const arr = new Array(poolPrices.prices.length); + poolPrices.prices.forEach((p, index) => { + if (p == 0n) { + arr[index] = 0; + } else { + arr[index] = gasCost; + } + }); + return arr; + } + + async getSimpleParam( + srcToken: string, + destToken: string, + srcAmount: string, + destAmount: string, + data: UniswapV3Data, + side: SwapSide, + ): Promise { + const swapFunction = + side === SwapSide.SELL + ? UniswapV3Functions.exactInput + : UniswapV3Functions.exactOutput; + + const path = this._encodePath(data.path, side); + const swapFunctionParams: UniswapV3SimpleSwapParams = + side === SwapSide.SELL + ? { + recipient: this.augustusAddress, + deadline: getLocalDeadlineAsFriendlyPlaceholder(), + amountIn: srcAmount, + amountOutMinimum: destAmount, + path, + } + : { + recipient: this.augustusAddress, + deadline: getLocalDeadlineAsFriendlyPlaceholder(), + amountOut: destAmount, + amountInMaximum: srcAmount, + path, + }; + const swapData = this.routerIface.encodeFunctionData(swapFunction, [ + swapFunctionParams, + ]); + + return this.buildSimpleParamWithoutWETHConversion( + srcToken, + srcAmount, + destToken, + destAmount, + swapData, + this.config.router, + ); + } + + getDexParam( + srcToken: Address, + destToken: Address, + srcAmount: NumberAsString, + destAmount: NumberAsString, + recipient: Address, + data: UniswapV3Data, + side: SwapSide, + ): DexExchangeParam { + const swapFunction = + side === SwapSide.SELL + ? UniswapV3Functions.exactInput + : UniswapV3Functions.exactOutput; + + const path = this._encodePath(data.path, side); + + const swapFunctionParams: UniswapV3SimpleSwapParams = + side === SwapSide.SELL + ? { + recipient, + deadline: getLocalDeadlineAsFriendlyPlaceholder(), + amountIn: srcAmount, + amountOutMinimum: destAmount, + path, + } + : { + recipient, + deadline: getLocalDeadlineAsFriendlyPlaceholder(), + amountOut: destAmount, + amountInMaximum: srcAmount, + path, + }; + + const exchangeData = this.routerIface.encodeFunctionData(swapFunction, [ + swapFunctionParams, + ]); + + return { + needWrapNative: this.needWrapNative, + dexFuncHasRecipient: true, + exchangeData, + targetExchange: this.config.router, + returnAmountPos: + side === SwapSide.SELL + ? extractReturnAmountPosition( + this.routerIface, + swapFunction, + 'amountOut', + ) + : undefined, + }; + } + + async getTopPoolsForToken( + tokenAddress: Address, + limit: number, + ): Promise { + if (!this.config.subgraphURL) return []; + + const _tokenAddress = tokenAddress.toLowerCase(); + + const res = await this._querySubgraph( + `query ($token: Bytes!, $count: Int) { + pools0: pools(first: $count, orderBy: totalValueLockedUSD, orderDirection: desc, where: {token0: $token}) { + id + token0 { + id + decimals + } + token1 { + id + decimals + } + totalValueLockedUSD + } + pools1: pools(first: $count, orderBy: totalValueLockedUSD, orderDirection: desc, where: {token1: $token}) { + id + token0 { + id + decimals + } + token1 { + id + decimals + } + totalValueLockedUSD + } + }`, + { + token: _tokenAddress, + count: limit, + }, + ); + + if (!(res && res.pools0 && res.pools1)) { + this.logger.error( + `Error_${this.dexKey}_Subgraph: couldn't fetch the pools from the subgraph`, + ); + return []; + } + + const pools0 = _.map(res.pools0, pool => ({ + exchange: this.dexKey, + address: pool.id.toLowerCase(), + connectorTokens: [ + { + address: pool.token1.id.toLowerCase(), + decimals: parseInt(pool.token1.decimals), + }, + ], + liquidityUSD: + parseFloat(pool.totalValueLockedUSD) * SQUADSWAPV3_EFFICIENCY_FACTOR, + })); + + const pools1 = _.map(res.pools1, pool => ({ + exchange: this.dexKey, + address: pool.id.toLowerCase(), + connectorTokens: [ + { + address: pool.token0.id.toLowerCase(), + decimals: parseInt(pool.token0.decimals), + }, + ], + liquidityUSD: + parseFloat(pool.totalValueLockedUSD) * SQUADSWAPV3_EFFICIENCY_FACTOR, + })); + + const pools = _.slice( + _.sortBy(_.concat(pools0, pools1), [pool => -1 * pool.liquidityUSD]), + 0, + limit, + ); + return pools; + } + + private async _getPoolsFromIdentifiers( + poolIdentifiers: string[], + blockNumber: number, + ): Promise { + const pools = await Promise.all( + poolIdentifiers.map(async identifier => { + const [, srcAddress, destAddress, fee] = identifier.split('_'); + return this.getPool(srcAddress, destAddress, BigInt(fee), blockNumber); + }), + ); + return pools.filter(pool => pool) as SquadswapV3EventPool[]; + } + + private _getLoweredAddresses(srcToken: Token, destToken: Token) { + return [srcToken.address.toLowerCase(), destToken.address.toLowerCase()]; + } + + private _sortTokens(srcAddress: Address, destAddress: Address) { + return [srcAddress, destAddress].sort((a, b) => (a < b ? -1 : 1)); + } + + private _toLowerForAllConfigAddresses() { + // If new config property will be added, the TS will throw compile error + const newConfig: DexParams = { + router: this.config.router.toLowerCase(), + quoter: this.config.quoter.toLowerCase(), + factory: this.config.factory.toLowerCase(), + supportedFees: this.config.supportedFees, + stateMulticall: this.config.stateMulticall.toLowerCase(), + chunksCount: this.config.chunksCount, + initRetryFrequency: this.config.initRetryFrequency, + uniswapMulticall: this.config.uniswapMulticall, + deployer: this.config.deployer?.toLowerCase(), + initHash: this.config.initHash, + subgraphURL: this.config.subgraphURL, + }; + return newConfig; + } + + private _getOutputs( + state: DeepReadonly, + amounts: bigint[], + zeroForOne: boolean, + side: SwapSide, + destTokenBalance: bigint, + ): OutputResult | null { + try { + const outputsResult = pancakeswapV3Math.queryOutputs( + state, + amounts, + zeroForOne, + side, + ); + + if (side === SwapSide.SELL) { + if (outputsResult.outputs[0] > destTokenBalance) { + return null; + } + + for (let i = 0; i < outputsResult.outputs.length; i++) { + if (outputsResult.outputs[i] > destTokenBalance) { + outputsResult.outputs[i] = 0n; + outputsResult.tickCounts[i] = 0; + } + } + } else { + if (amounts[0] > destTokenBalance) { + return null; + } + + // This may be improved by first checking outputs and requesting outputs + // only for amounts that makes more sense, but I don't think this is really + // important now + for (let i = 0; i < amounts.length; i++) { + if (amounts[i] > destTokenBalance) { + outputsResult.outputs[i] = 0n; + outputsResult.tickCounts[i] = 0; + } + } + } + + return outputsResult; + } catch (e) { + this.logger.debug( + `${this.dexKey}: received error in _getOutputs while calculating outputs`, + e, + ); + return null; + } + } + + private async _querySubgraph( + query: string, + variables: Object, + timeout = 30000, + ) { + if (!this.config.subgraphURL) return []; + + try { + const res = await this.dexHelper.httpRequest.querySubgraph( + this.config.subgraphURL, + { query, variables }, + { timeout }, + ); + return res.data; + } catch (e) { + this.logger.error(`${this.dexKey}: can not query subgraph: `, e); + return {}; + } + } + + private _encodePath( + path: { + tokenIn: Address; + tokenOut: Address; + fee: NumberAsString; + }[], + side: SwapSide, + ): string { + if (path.length === 0) { + this.logger.error( + `${this.dexKey}: Received invalid path=${path} for side=${side} to encode`, + ); + return '0x'; + } + + const { _path, types } = path.reduce( + ( + { _path, types }: { _path: string[]; types: string[] }, + curr, + index, + ): { _path: string[]; types: string[] } => { + if (index === 0) { + return { + types: ['address', 'uint24', 'address'], + _path: [curr.tokenIn, curr.fee, curr.tokenOut], + }; + } else { + return { + types: [...types, 'uint24', 'address'], + _path: [..._path, curr.fee, curr.tokenOut], + }; + } + }, + { _path: [], types: [] }, + ); + + return side === SwapSide.BUY + ? pack(types.reverse(), _path.reverse()) + : pack(types, _path); + } + + releaseResources() { + if (this.intervalTask !== undefined) { + clearInterval(this.intervalTask); + this.intervalTask = undefined; + } + } +} diff --git a/src/dex/squadswap-v3/types.ts b/src/dex/squadswap-v3/types.ts new file mode 100644 index 000000000..edeb4162d --- /dev/null +++ b/src/dex/squadswap-v3/types.ts @@ -0,0 +1,23 @@ +import { Address } from '../../types'; + +export type PoolState = { + // TODO: poolState is the state of event + // subscriber. This should be the minimum + // set of parameters required to compute + // pool prices. Complete me! +}; + +export type SquadswapV3Data = { + // TODO: SquadswapV3Data is the dex data that is + // returned by the API that can be used for + // tx building. The data structure should be minimal. + // Complete me! + exchange: Address; +}; + +export type DexParams = { + factory: any; + // TODO: DexParams is set of parameters the can + // be used to initiate a DEX fork. + // Complete me! +}; diff --git a/src/dex/squadswap-v3/utils.ts b/src/dex/squadswap-v3/utils.ts new file mode 100644 index 000000000..25b208ee3 --- /dev/null +++ b/src/dex/squadswap-v3/utils.ts @@ -0,0 +1,65 @@ +import { BytesLike, ethers } from 'ethers'; +import { assert } from 'ts-essentials'; +import { extractSuccessAndValue } from '../../lib/decoders'; +import { MultiResult } from '../../lib/multi-wrapper'; +import { DecodedStateMultiCallResultWithRelativeBitmaps } from '../uniswap-v3/types'; + +export function decodeStateMultiCallResultWithRelativeBitmaps( + result: MultiResult | BytesLike, +): DecodedStateMultiCallResultWithRelativeBitmaps { + const [isSuccess, toDecode] = extractSuccessAndValue(result); + + assert( + isSuccess && toDecode !== '0x', + `decodeStateMultiCallResultWithRelativeBitmaps failed to get decodable result: ${result}`, + ); + + const decoded = ethers.utils.defaultAbiCoder.decode( + [ + // I don't want to pass here any interface, so I just use it in ethers format + ` + tuple( + address pool, + uint256 blockTimestamp, + tuple( + uint160 sqrtPriceX96, + int24 tick, + uint16 observationIndex, + uint16 observationCardinality, + uint16 observationCardinalityNext, + uint32 feeProtocol, + bool unlocked, + ) slot0, + uint128 liquidity, + int24 tickSpacing, + uint128 maxLiquidityPerTick, + tuple( + uint32 blockTimestamp, + int56 tickCumulative, + uint160 secondsPerLiquidityCumulativeX128, + bool initialized, + ) observation, + tuple( + int16 index, + uint256 value, + )[] tickBitmap, + tuple( + int24 index, + tuple( + uint128 liquidityGross, + int128 liquidityNet, + int56 tickCumulativeOutside, + uint160 secondsPerLiquidityOutsideX128, + uint32 secondsOutside, + bool initialized, + ) value, + )[] ticks + ) + `, + ], + toDecode, + )[0]; + // This conversion is not precise, because when we decode, we have more values + // But I typed only the ones that are used later + return decoded as DecodedStateMultiCallResultWithRelativeBitmaps; +}