diff --git a/packages/sushi/src/router/data-fetcher.ts b/packages/sushi/src/router/data-fetcher.ts index 764273d996..19aa82f957 100644 --- a/packages/sushi/src/router/data-fetcher.ts +++ b/packages/sushi/src/router/data-fetcher.ts @@ -2,6 +2,7 @@ import { http, PublicClient, createPublicClient } from 'viem' import { ChainId, TestnetChainId } from '../chain/index.js' import { publicClientConfig } from '../config/index.js' import { Type } from '../currency/index.js' +import { AerodromeSlipstreamProvider } from './liquidity-providers/AerodromeSlipstream.js' import { AlienBaseV2Provider } from './liquidity-providers/AlienBaseV2.js' import { AlienBaseV3Provider } from './liquidity-providers/AlienBaseV3.js' import { ApeSwapProvider } from './liquidity-providers/ApeSwap.js' @@ -82,6 +83,7 @@ import { UniswapV2Provider } from './liquidity-providers/UniswapV2.js' import { UniswapV3Provider } from './liquidity-providers/UniswapV3.js' import { VVSFlawlessProvider } from './liquidity-providers/VVSFlawless.js' import { VVSStandardProvider } from './liquidity-providers/VVSStandard.js' +import { VelodromeSlipstreamProvider } from './liquidity-providers/VelodromeSlipstream.js' import { WagmiProvider } from './liquidity-providers/Wagmi.js' import { WigoswapProvider } from './liquidity-providers/Wigoswap.js' import { ZebraV2Provider } from './liquidity-providers/ZebraV2.js' @@ -183,6 +185,7 @@ export class DataFetcher { // concrete providers this.providers = [new NativeWrapProvider(this.chainId, this.web3Client)] ;[ + AerodromeSlipstreamProvider, AlienBaseV2Provider, AlienBaseV3Provider, ApeSwapProvider, @@ -254,6 +257,7 @@ export class DataFetcher { UbeSwapProvider, UniswapV2Provider, UniswapV3Provider, + VelodromeSlipstreamProvider, VVSStandardProvider, VVSFlawlessProvider, WagmiProvider, diff --git a/packages/sushi/src/router/liquidity-providers/AerodromeSlipstream.ts b/packages/sushi/src/router/liquidity-providers/AerodromeSlipstream.ts new file mode 100644 index 0000000000..476820f0fa --- /dev/null +++ b/packages/sushi/src/router/liquidity-providers/AerodromeSlipstream.ts @@ -0,0 +1,35 @@ +import { PublicClient } from 'viem' +import { ChainId } from '../../chain/index.js' +import { LiquidityProviders } from './LiquidityProvider.js' +import { VelodromeSlipstreamBaseProvider } from './VelodromeSlipstreamBase.js' + +export class AerodromeSlipstreamProvider extends VelodromeSlipstreamBaseProvider { + constructor(chainId: ChainId, web3Client: PublicClient) { + const factory = { + [ChainId.BASE]: '0x5e7BB104d84c7CB9B682AaC2F3d509f5F406809A', + } as const + const customSwapFeeModule = { + [ChainId.BASE]: '0xF4171B0953b52Fa55462E4d76ecA1845Db69af00', + } as const + const poolImplementation = { + [ChainId.BASE]: '0xeC8E5342B19977B4eF8892e02D8DAEcfa1315831', + } as const + const tickLens = { + [ChainId.BASE]: '0x3e1116ea5034f5d73a7b530071709d54a4109f5f', + } as const + super( + chainId, + web3Client, + factory, + tickLens, + poolImplementation, + customSwapFeeModule, + ) + } + getType(): LiquidityProviders { + return LiquidityProviders.AerodromeSlipstream + } + getPoolProviderName(): string { + return 'AerodromeSlipstream' + } +} diff --git a/packages/sushi/src/router/liquidity-providers/LiquidityProvider.ts b/packages/sushi/src/router/liquidity-providers/LiquidityProvider.ts index b8806439d0..2600d58807 100644 --- a/packages/sushi/src/router/liquidity-providers/LiquidityProvider.ts +++ b/packages/sushi/src/router/liquidity-providers/LiquidityProvider.ts @@ -83,6 +83,8 @@ export enum LiquidityProviders { EddyFinance = 'EddyFinance', MMFinance = 'MMFinance', VVSFlawless = 'VVSFlawless', + AerodromeSlipstream = 'AerodromeSlipstream', + VelodromeSlipstream = 'VelodromeSlipstream', } export abstract class LiquidityProvider { @@ -236,4 +238,6 @@ export const UniV3LiquidityProviders: LiquidityProviders[] = [ LiquidityProviders.Scribe, LiquidityProviders.Horizon, LiquidityProviders.VVSFlawless, + LiquidityProviders.AerodromeSlipstream, + LiquidityProviders.VelodromeSlipstream, ] diff --git a/packages/sushi/src/router/liquidity-providers/UniswapV3Base.ts b/packages/sushi/src/router/liquidity-providers/UniswapV3Base.ts index ddd02d2427..041d9c6534 100644 --- a/packages/sushi/src/router/liquidity-providers/UniswapV3Base.ts +++ b/packages/sushi/src/router/liquidity-providers/UniswapV3Base.ts @@ -485,8 +485,7 @@ export abstract class UniswapV3BaseProvider extends LiquidityProvider { } getStaticPools(t1: Token, t2: Token): StaticPoolUniV3[] { - const fees = Object.values(this.FEE) - const feeList = fees.splice(fees.length / 2) as number[] + const feeList = Object.values(this.FEE).filter((v) => typeof v === 'number') const currencyCombinations = getCurrencyCombinations(this.chainId, t1, t2) const allCurrencyCombinationsWithAllFees: [Type, Type, number][] = @@ -570,8 +569,7 @@ export abstract class UniswapV3BaseProvider extends LiquidityProvider { } async ensureFeeAndTicks(): Promise { - const fees = Object.values(this.FEE) - const feeList = fees.splice(fees.length / 2) as number[] + const feeList = Object.values(this.FEE).filter((v) => typeof v === 'number') const results = (await this.client.multicall({ multicallAddress: this.client.chain?.contracts?.multicall3 ?.address as Address, diff --git a/packages/sushi/src/router/liquidity-providers/VelodromeSlipstream.ts b/packages/sushi/src/router/liquidity-providers/VelodromeSlipstream.ts new file mode 100644 index 0000000000..92ed77e554 --- /dev/null +++ b/packages/sushi/src/router/liquidity-providers/VelodromeSlipstream.ts @@ -0,0 +1,35 @@ +import { PublicClient } from 'viem' +import { ChainId } from '../../chain/index.js' +import { LiquidityProviders } from './LiquidityProvider.js' +import { VelodromeSlipstreamBaseProvider } from './VelodromeSlipstreamBase.js' + +export class VelodromeSlipstreamProvider extends VelodromeSlipstreamBaseProvider { + constructor(chainId: ChainId, web3Client: PublicClient) { + const factory = { + [ChainId.OPTIMISM]: '0xCc0bDDB707055e04e497aB22a59c2aF4391cd12F', + } as const + const customSwapFeeModule = { + [ChainId.OPTIMISM]: '0x7361E9079920fb75496E9764A2665d8ee5049D5f', + } as const + const poolImplementation = { + [ChainId.OPTIMISM]: '0xc28aD28853A547556780BEBF7847628501A3bCbb', + } as const + const tickLens = { + [ChainId.OPTIMISM]: '0x49C6FDCb3D5b2CecD8baff66c8e94b9B261ad925', + } as const + super( + chainId, + web3Client, + factory, + tickLens, + poolImplementation, + customSwapFeeModule, + ) + } + getType(): LiquidityProviders { + return LiquidityProviders.VelodromeSlipstream + } + getPoolProviderName(): string { + return 'VelodromeSlipstream' + } +} diff --git a/packages/sushi/src/router/liquidity-providers/VelodromeSlipstreamBase.ts b/packages/sushi/src/router/liquidity-providers/VelodromeSlipstreamBase.ts new file mode 100644 index 0000000000..80f61834ad --- /dev/null +++ b/packages/sushi/src/router/liquidity-providers/VelodromeSlipstreamBase.ts @@ -0,0 +1,333 @@ +import { + Address, + Hex, + PublicClient, + encodeAbiParameters, + getAddress, + keccak256, + parseAbi, + parseAbiParameters, +} from 'viem' +import { ChainId } from '../../chain/index.js' +import { Token } from '../../currency/index.js' +import { DataFetcherOptions } from '../data-fetcher.js' +import { getCurrencyCombinations } from '../get-currency-combinations.js' +import { + NUMBER_OF_SURROUNDING_TICKS, + PoolFilter, + StaticPoolUniV3, + UniswapV3BaseProvider, + V3Pool, + bitmapIndex, +} from './UniswapV3Base.js' + +export const ZERO_FEE_INDICATOR = 420 + +export interface SlipstreamPool extends StaticPoolUniV3 { + tickSpacing: number +} + +const feeAbi = [ + { + inputs: [], + name: 'fee', + outputs: [{ internalType: 'uint24', name: '', type: 'uint24' }], + stateMutability: 'view', + type: 'function', + }, +] as const + +const slot0Abi = [ + { + inputs: [], + name: 'slot0', + outputs: [ + { internalType: 'uint160', name: 'sqrtPriceX96', type: 'uint160' }, + { internalType: 'int24', name: 'tick', type: 'int24' }, + { internalType: 'uint16', name: 'observationIndex', type: 'uint16' }, + { + internalType: 'uint16', + name: 'observationCardinality', + type: 'uint16', + }, + { + internalType: 'uint16', + name: 'observationCardinalityNext', + type: 'uint16', + }, + { internalType: 'bool', name: 'unlocked', type: 'bool' }, + ], + stateMutability: 'view', + type: 'function', + }, +] as const + +const SlipstreamABI = parseAbi([ + 'function tickSpacingToFee(int24) view returns (uint24)', + 'function poolImplementation() view returns (address)', + 'function tickSpacings() view returns (int24[])', + 'function swapFeeModule() view returns (address)', +]) + +export abstract class VelodromeSlipstreamBaseProvider extends UniswapV3BaseProvider { + override TICK_SPACINGS: Record = {} + didFetchTickSpacing = false + + readonly BASE_FEE = 100 + DEFAULT_TICK_SPACINGS = [1, 50, 100, 200, 2000] as const + tickSpacings: number[] = [...this.DEFAULT_TICK_SPACINGS] + + poolImplementation: Record = {} + customSwapFeeModule: Record = {} + + constructor( + chainId: ChainId, + web3Client: PublicClient, + factory: Record, + tickLens: Record, + poolImplementation: Record, + customSwapFeeModule: Record, + isTest = false, + ) { + super( + chainId, + web3Client, + factory, + { [chainId]: `0x${'0'.repeat(64)}` }, + tickLens, + isTest, + ) + this.poolImplementation = poolImplementation + this.customSwapFeeModule = customSwapFeeModule + if (!(chainId in this.poolImplementation)) { + throw new Error( + `${this.getType()} cannot be instantiated for chainid ${chainId}, no poolImplementation address`, + ) + } + if (!(chainId in this.customSwapFeeModule)) { + throw new Error( + `${this.getType()} cannot be instantiated for chainid ${chainId}, no customSwapFeeModule address`, + ) + } + } + + override async fetchPoolData( + t0: Token, + t1: Token, + excludePools?: Set | PoolFilter, + options?: DataFetcherOptions, + ): Promise { + if (!this.didFetchTickSpacing) { + const result = await this.client + .readContract({ + address: this.factory[this.chainId as keyof typeof this.factory]!, + blockNumber: options?.blockNumber, + abi: SlipstreamABI, + functionName: 'tickSpacings', + }) + .catch((e) => { + console.warn( + `${this.getLogPrefix()} - INIT: multicall failed, message: ${ + e.message + }`, + ) + return undefined + }) + if (result) this.didFetchTickSpacing = true + this.tickSpacings = (result ?? this.DEFAULT_TICK_SPACINGS) as number[] + } + + let staticPools = this.getStaticPools(t0, t1) + if (excludePools) + staticPools = staticPools.filter((p) => !excludePools.has(p.address)) + + const slot0 = await this.client + .multicall({ + multicallAddress: this.client.chain?.contracts?.multicall3?.address!, + allowFailure: true, + blockNumber: options?.blockNumber, + contracts: staticPools.map((pool) => ({ + address: pool.address, + chainId: this.chainId, + abi: slot0Abi, + functionName: 'slot0', + })), + }) + .catch((e) => { + console.warn( + `${this.getLogPrefix()} - INIT: multicall failed, message: ${ + e.message + }`, + ) + return undefined + }) + + const poolFees = await this.client + .multicall({ + multicallAddress: this.client.chain?.contracts?.multicall3?.address!, + allowFailure: true, + blockNumber: options?.blockNumber, + contracts: staticPools.map( + (pool) => + ({ + address: pool.address, + chainId: this.chainId, + abi: feeAbi, + functionName: 'fee', + }) as const, + ), + }) + .catch((e) => { + console.warn( + `${this.getLogPrefix()} - INIT: multicall failed, message: ${ + e.message + }`, + ) + return undefined + }) + + const existingPools: V3Pool[] = [] + + staticPools.forEach((pool, i) => { + if (slot0 === undefined || !slot0[i]) return + const sqrtPriceX96 = slot0[i]!.result?.[0] // price + const tick = slot0[i]!.result?.[1] // tick + if (!sqrtPriceX96 || sqrtPriceX96 === 0n || typeof tick !== 'number') + return + const fee = poolFees?.[i]?.result // fee + if (!fee) return + const activeTick = Math.floor(tick / pool.tickSpacing) * pool.tickSpacing + if (typeof activeTick !== 'number') return + this.TICK_SPACINGS[pool.address.toLowerCase()] = pool.tickSpacing + existingPools.push({ + ...pool, + fee, + sqrtPriceX96, + activeTick, + }) + }) + + return existingPools + } + + override getIndexes(existingPools: V3Pool[]): [number[], number[]] { + const minIndexes = existingPools.map((pool) => + bitmapIndex( + pool.activeTick - NUMBER_OF_SURROUNDING_TICKS, + this.TICK_SPACINGS[pool.address.toLowerCase()]!, + ), + ) + const maxIndexes = existingPools.map((pool) => + bitmapIndex( + pool.activeTick + NUMBER_OF_SURROUNDING_TICKS, + this.TICK_SPACINGS[pool.address.toLowerCase()]!, + ), + ) + return [minIndexes, maxIndexes] + } + + override handleTickBoundries( + i: number, + pool: V3Pool, + poolTicks: { + index: number + DLiquidity: bigint + }[], + minIndexes: number[], + maxIndexes: number[], + ) { + const lowerUnknownTick = + minIndexes[i]! * this.TICK_SPACINGS[pool.address.toLowerCase()]! * 256 - + this.TICK_SPACINGS[pool.address.toLowerCase()]! + console.assert( + poolTicks.length === 0 || lowerUnknownTick < poolTicks[0]!.index, + 'Error 236: unexpected min tick index', + ) + poolTicks.unshift({ + index: lowerUnknownTick, + DLiquidity: 0n, + }) + const upperUnknownTick = + (maxIndexes[i]! + 1) * + this.TICK_SPACINGS[pool.address.toLowerCase()]! * + 256 + console.assert( + poolTicks[poolTicks.length - 1]!.index < upperUnknownTick, + 'Error 244: unexpected max tick index', + ) + poolTicks.push({ + index: upperUnknownTick, + DLiquidity: 0n, + }) + } + + override getStaticPools(t1: Token, t2: Token): SlipstreamPool[] { + const allCombinations = getCurrencyCombinations(this.chainId, t1, t2) + const currencyCombinations: [Token, Token, number][] = [] + allCombinations.forEach(([currencyA, currencyB]) => { + if (currencyA && currencyB) { + const tokenA = currencyA.wrapped + const tokenB = currencyB.wrapped + if (tokenA.equals(tokenB)) return + const tokens = tokenA.sortsBefore(tokenB) + ? [tokenA, tokenB] + : [tokenB, tokenA] + currencyCombinations.push( + ...this.tickSpacings.map( + (t) => [...tokens, t] as [Token, Token, number], + ), + ) + } + }) + return currencyCombinations.map(([currencyA, currencyB, tickSpacing]) => ({ + address: this.getSlipstreamPoolAddress( + this.factory[this.chainId as keyof typeof this.factory]!, + currencyA.wrapped, + currencyB.wrapped, + tickSpacing, + ), + token0: currencyA, + token1: currencyB, + fee: this.BASE_FEE, + tickSpacing, + })) + } + + // algebra doesnt have the fee/ticks setup the same way univ3 has + override async ensureFeeAndTicks(): Promise { + return true + } + + getSlipstreamPoolAddress( + factory: Address, + tokenA: Token, + tokenB: Token, + tickSpacing: number, + ): Address { + const [token0, token1] = tokenA.sortsBefore(tokenB) + ? [tokenA, tokenB] + : [tokenB, tokenA] + const constructorArgumentsEncoded = encodeAbiParameters( + parseAbiParameters('address, address, int24'), + [token0.address, token1.address, tickSpacing], + ) + const initCode = + `0x3d602d80600a3d3981f3363d3d373d3d3d363d73${this.poolImplementation[ + this.chainId as keyof typeof this.poolImplementation + ]!.replace('0x', '')}5af43d82803e903d91602b57fd5bf3` as Hex + const initCodeHash = keccak256(initCode) + + const create2Inputs = [ + '0xff', + factory, + // salt + keccak256(constructorArgumentsEncoded), + // init code hash + initCodeHash, + ] + const sanitizedInputs = `0x${create2Inputs + .map((i) => i.slice(2)) + .join('')}` as Hex + return getAddress(`0x${keccak256(sanitizedInputs).slice(-40)}`) + } +}