diff --git a/package.json b/package.json index f78a3ce99..33ccd9c76 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@paraswap/dex-lib", - "version": "2.28.0", + "version": "2.28.8", "main": "build/index.js", "types": "build/index.d.ts", "repository": "https://github.com/paraswap/paraswap-dex-lib", diff --git a/src/config.ts b/src/config.ts index 2c146b271..bef159a5b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -217,7 +217,7 @@ const baseConfigs: { [network: number]: BaseConfig } = { adapterAddresses: { AvalancheAdapter01: '0x745Ec73855CeC7249E5fF4c9DD81cc65b4D297a9', AvalancheAdapter02: '0xDCf4EE5B700e2a5Fec458e06B763A4a3E3004494', - AvalancheBuyAdapter: '0xeBF40A40CA3D4310Bf53048F48e860656e1D7C81', + AvalancheBuyAdapter: '0x7ebbDBB57d2ab59079423cf8337cf8805e225bB1', }, uniswapV2ExchangeRouterAddress: '0x53e693c6C7FFC4446c53B205Cf513105Bf140D7b', diff --git a/src/dex/algebra/algebra-e2e.test.ts b/src/dex/algebra/algebra-e2e.test.ts index e732b564c..d945141d1 100644 --- a/src/dex/algebra/algebra-e2e.test.ts +++ b/src/dex/algebra/algebra-e2e.test.ts @@ -180,6 +180,26 @@ describe('Algebra', () => { nativeTokenAmount, ); }); + + describe('Optimism', () => { + const network = Network.OPTIMISM; + const tokenASymbol: string = 'USDC'; + const tokenBSymbol: string = 'USDT'; + + const tokenAAmount: string = '100000000'; + const tokenBAmount: string = '50000'; + const nativeTokenAmount = '100000000000000'; + + testForNetwork( + network, + dexKey, + tokenASymbol, + tokenBSymbol, + tokenAAmount, + tokenBAmount, + nativeTokenAmount, + ) + }); }); describe('CamelotV3', () => { diff --git a/src/dex/algebra/algebra-events.test.ts b/src/dex/algebra/algebra-events.test.ts index c6bfa5614..bba66f5d3 100644 --- a/src/dex/algebra/algebra-events.test.ts +++ b/src/dex/algebra/algebra-events.test.ts @@ -7,12 +7,13 @@ import { AlgebraConfig } from './config'; import { Network } from '../../constants'; import { DummyDexHelper } from '../../dex-helper/index'; import { testEventSubscriber } from '../../../tests/utils-events'; -import { PoolState } from './types'; +import { PoolStateV1_1, PoolState_v1_9 } from './types'; import { Interface } from '@ethersproject/abi'; import ERC20ABI from '../../abi/erc20.json'; import StateMulticallABI from '../../abi/algebra/AlgebraStateMulticall.abi.json'; import { AbiItem } from 'web3-utils'; -import { AlgebraEventPool } from './algebra-pool'; +import { AlgebraEventPoolV1_1 } from './algebra-pool-v1_1'; +import { AlgebraEventPoolV1_9 } from './algebra-pool-v1_9'; jest.setTimeout(300 * 1000); const dexKey = 'QuickSwapV3'; @@ -20,10 +21,10 @@ const network = Network.POLYGON; const config = AlgebraConfig[dexKey][network]; async function fetchPoolStateFromContract( - algebraPool: AlgebraEventPool, + algebraPool: AlgebraEventPoolV1_1 | AlgebraEventPoolV1_9, blockNumber: number, poolAddress: string, -): Promise { +): Promise { const message = `Algebra: ${poolAddress} blockNumber ${blockNumber}`; console.log(`Fetching state ${message}`); // Be careful to not request state prior to contract deployment @@ -31,11 +32,76 @@ async function fetchPoolStateFromContract( // We had that mechanism, but removed it with this commit // You can restore it, but better just to find block after state multicall // deployment - const state = algebraPool.generateState(blockNumber); + const state = await algebraPool.generateState(blockNumber); console.log(`Done ${message}`); return state; } +// To make this test to pass, you need to increase till 1500: TICK_BITMAP_BUFFER=1500 +describe('CamelotV3 Event Edge Case', function () { + const poolAddress = '0xaB72b23F347d41e8E993176ecb7CF3b842FBAC8C'; + const token0 = '0x6bb7a17acc227fd1f6781d1eedeae01b42047ee0'; + const token1 = '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8'; + const dexKey = 'CamelotV3'; + const network = Network.ARBITRUM; + const config = AlgebraConfig[dexKey][network]; + + const blockNumbers: { [eventName: string]: number[] } = { + // topic0 - 0x0c396cd989a39f4459b5fa1aed6a9a8dcdbc45908acfd67e028cd568da98982c + ['Burn']: [119409575], + // topic0 - 0xbdbdb71d7860376ba52b25a5028beea23581364a40522f6bcfb86bb1f2dca633 + ['Collect']: [119409575], + }; + + describe('AlgebraEventPool', function () { + Object.keys(blockNumbers).forEach((event: string) => { + blockNumbers[event].forEach((blockNumber: number) => { + it(`${event}:${blockNumber} - should return correct state`, async function () { + const dexHelper = new DummyDexHelper(network); + // await dexHelper.init(); + + const logger = dexHelper.getLogger(dexKey); + + const algebraPool = new AlgebraEventPoolV1_9( + dexHelper, + dexKey, + new dexHelper.web3Provider.eth.Contract( + StateMulticallABI as AbiItem[], + config.algebraStateMulticall, + ), + new Interface(ERC20ABI), + config.factory, + token0, + token1, + logger, + undefined, + config.initHash, + config.deployer, + ); + + // It is done in generateState. But here have to make it manually + algebraPool.poolAddress = poolAddress.toLowerCase(); + algebraPool.addressesSubscribed[0] = poolAddress; + + await testEventSubscriber( + algebraPool as any, + algebraPool.addressesSubscribed, + (_blockNumber: number) => + fetchPoolStateFromContract( + algebraPool, + _blockNumber, + poolAddress, + ), + blockNumber, + `${dexKey}_${poolAddress}`, + dexHelper.provider, + ); + }); + }); + }); + }); +}); + describe('Algebra Event', function () { const poolAddress = '0x5b41eedcfc8e0ae47493d4945aa1ae4fe05430ff'; const token0 = '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270'; @@ -79,7 +145,7 @@ describe('Algebra Event', function () { const logger = dexHelper.getLogger(dexKey); - const algebraPool = new AlgebraEventPool( + const algebraPool = new AlgebraEventPoolV1_1( dexHelper, dexKey, new dexHelper.web3Provider.eth.Contract( @@ -101,7 +167,7 @@ describe('Algebra Event', function () { algebraPool.addressesSubscribed[0] = poolAddress; await testEventSubscriber( - algebraPool, + algebraPool as any, algebraPool.addressesSubscribed, (_blockNumber: number) => fetchPoolStateFromContract( diff --git a/src/dex/algebra/algebra-integration.test.ts b/src/dex/algebra/algebra-integration.test.ts index c059c9060..97017d03a 100644 --- a/src/dex/algebra/algebra-integration.test.ts +++ b/src/dex/algebra/algebra-integration.test.ts @@ -166,8 +166,9 @@ describe('Algebra', function () { const tokens = Tokens[network]; - const srcTokenSymbol = 'USDC'; - const destTokenSymbol = 'WMATIC'; + const srcTokenSymbol = 'WMATIC'; + const destTokenSymbol = 'DAI'; + // const destTokenSymbol = 'USDC'; const amountsForSell = [ 0n, diff --git a/src/dex/algebra/algebra-pool-v1_1.ts b/src/dex/algebra/algebra-pool-v1_1.ts index fea7cd5f3..2b9746de5 100644 --- a/src/dex/algebra/algebra-pool-v1_1.ts +++ b/src/dex/algebra/algebra-pool-v1_1.ts @@ -26,13 +26,13 @@ import { uint256ToBigInt } from '../../lib/decoders'; import { MultiCallParams } from '../../lib/multi-wrapper'; import { decodeStateMultiCallResultWithRelativeBitmapsV1_1 } from './utils'; import { AlgebraMath } from './lib/AlgebraMath'; -import { TickBitMap } from '../uniswap-v3/contract-math/TickBitMap'; import { _reduceTickBitmap, _reduceTicks, } from '../uniswap-v3/contract-math/utils'; import { Constants } from './lib/Constants'; import { Network } from '../../constants'; +import { TickTable } from './lib/TickTable'; export class AlgebraEventPoolV1_1 extends StatefulEventSubscriber { handlers: { @@ -364,7 +364,7 @@ export class AlgebraEventPoolV1_1 extends StatefulEventSubscriber tick: bigIntify(_state.globalState.tick), }; const currentTick = globalState.tick; - const startTickBitmap = TickBitMap.position( + const startTickBitmap = TickTable.position( BigInt(currentTick) / Constants.TICK_SPACING, )[0]; @@ -381,6 +381,7 @@ export class AlgebraEventPoolV1_1 extends StatefulEventSubscriber isValid: true, balance0, balance1, + areTicksCompressed: true, }; } diff --git a/src/dex/algebra/algebra-pool-v1_9.ts b/src/dex/algebra/algebra-pool-v1_9.ts index 9b30bfcd8..f740680a9 100644 --- a/src/dex/algebra/algebra-pool-v1_9.ts +++ b/src/dex/algebra/algebra-pool-v1_9.ts @@ -26,13 +26,13 @@ import { uint256ToBigInt } from '../../lib/decoders'; import { MultiCallParams } from '../../lib/multi-wrapper'; import { decodeStateMultiCallResultWithRelativeBitmapsV1_9 } from './utils'; import { AlgebraMath } from './lib/AlgebraMath'; -import { TickBitMap } from '../uniswap-v3/contract-math/TickBitMap'; import { _reduceTickBitmap, _reduceTicks, } from '../uniswap-v3/contract-math/utils'; import { Constants } from './lib/Constants'; import { Network } from '../../constants'; +import { TickTable } from './lib/TickTable'; export class AlgebraEventPoolV1_9 extends StatefulEventSubscriber { handlers: { @@ -293,9 +293,7 @@ export class AlgebraEventPoolV1_9 extends StatefulEventSubscriber> 8n, - ); + const currentBitmapIndex = int16(BigInt(tick) >> 8n); const buffer = this.getBitmapRangeToRequest(); const startBitMapIndex = currentBitmapIndex - buffer; @@ -366,9 +364,7 @@ export class AlgebraEventPoolV1_9 extends StatefulEventSubscriber { readonly isFeeOnTransferSupported: boolean = false; @@ -507,6 +506,11 @@ export class Algebra extends SimpleExchange implements IDex { const zeroForOne = token0 === _srcAddress ? true : false; 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; } diff --git a/src/dex/algebra/config.ts b/src/dex/algebra/config.ts index 54aaab370..f8f942067 100644 --- a/src/dex/algebra/config.ts +++ b/src/dex/algebra/config.ts @@ -51,6 +51,21 @@ export const AlgebraConfig: DexConfigMap = { deployer: '0x24e85f5f94c6017d2d87b434394e87df4e4d56e3', version: 'v1.1', }, + [Network.OPTIMISM]: { + factory: '0x0C8f7b0cb986b31c67D994fb5c224592A03A4AfD', + router: '0xEDB4E3E3bB11255fF14C2762C6A6A28F1D3A36f2', + quoter: '0xf4211E7709D2294Cd10799E41623006dFB0D66aF', + initHash: + '0xbce37a54eab2fcd71913a0d40723e04238970e7fc1159bfd58ad5b79531697e7', + chunksCount: 10, + initRetryFrequency: 10, + algebraStateMulticall: '0x30F6B9b6485ff0B67E881f5ac80D3F1c70A4B23d', + subgraphURL: + 'https://api.thegraph.com/subgraphs/name/iliaazhel/zyberswap-info-optimism-pp', + uniswapMulticall: '0x1F98415757620B543A52E61c46B32eB19261F984', + deployer: '0xc0d4323426c709e8d04b5b130e7f059523464a91', + version: 'v1.1', + }, }, CamelotV3: { [Network.ARBITRUM]: { @@ -61,7 +76,7 @@ export const AlgebraConfig: DexConfigMap = { '0x6c1bebd370ba84753516bc1393c0d0a6c645856da55f5393ac8ab3d6dbc861d3', chunksCount: 10, initRetryFrequency: 10, - algebraStateMulticall: '0x541FeaEcB21a4cb0fBFCF90C5bae47BaDF747edE', + algebraStateMulticall: '0x2cB568442a102dF518b3D37CBD0d2884523C940B', subgraphURL: 'https://api.thegraph.com/subgraphs/name/camelotlabs/camelot-amm-v3', uniswapMulticall: '0x1F98415757620B543A52E61c46B32eB19261F984', @@ -85,4 +100,8 @@ export const Adapters: Record = { [SwapSide.SELL]: [{ name: 'ArbitrumAdapter01', index: 3 }], [SwapSide.BUY]: [{ name: 'ArbitrumBuyAdapter', index: 2 }], }, + [Network.OPTIMISM]: { + [SwapSide.SELL]: [{ name: 'OptimismAdapter01', index: 3 }], + [SwapSide.BUY]: [{ name: 'OptimismBuyAdapter', index: 2 }], + }, }; diff --git a/src/dex/algebra/lib/AlgebraMath.ts b/src/dex/algebra/lib/AlgebraMath.ts index 53892bb5b..af9af67d1 100644 --- a/src/dex/algebra/lib/AlgebraMath.ts +++ b/src/dex/algebra/lib/AlgebraMath.ts @@ -3,7 +3,6 @@ import { PoolStateV1_1, PoolState_v1_9 } from '../types'; import { NumberAsString, SwapSide } from '@paraswap/core'; import { OutputResult, TickInfo } from '../../uniswap-v3/types'; import { Tick } from '../../uniswap-v3/contract-math/Tick'; -import { TickBitMap } from '../../uniswap-v3/contract-math/TickBitMap'; import { SqrtPriceMath } from '../../uniswap-v3/contract-math/SqrtPriceMath'; import { TickMath } from '../../uniswap-v3/contract-math/TickMath'; import { LiquidityMath } from '../../uniswap-v3/contract-math/LiquidityMath'; @@ -21,6 +20,8 @@ import { MAX_PRICING_COMPUTATION_STEPS_ALLOWED, OUT_OF_RANGE_ERROR_POSTFIX, } from '../../uniswap-v3/constants'; +import { TickManager } from './TickManager'; +import { TickTable } from './TickTable'; type UpdatePositionCache = { price: bigint; @@ -112,12 +113,12 @@ function _priceComputationCycles( try { [step.tickNext, step.initialized] = - TickBitMap.nextInitializedTickWithinOneWord( + TickTable.nextInitializedTickWithinOneWord( poolState, state.tick, - poolState.tickSpacing, zeroForOne, true, + poolState.areTicksCompressed ? poolState.tickSpacing : undefined, ); } catch (e) { if ( @@ -378,9 +379,9 @@ class AlgebraMathClass { currentTick: bigint, currentPrice: bigint, ) { - let amount0; - let amount1; - let globalLiquidityDelta; + let amount0 = 0n; + let amount1 = 0n; + let globalLiquidityDelta = 0n; // If current tick is less than the provided bottom one then only the token0 has to be provided if (currentTick < bottomTick) { amount0 = SqrtPriceMath._getAmount0DeltaO( @@ -434,7 +435,7 @@ class AlgebraMathClass { const time = this._blockTimestamp(state); if ( - Tick.update( + TickManager.update( state, bottomTick, cache.tick, @@ -447,10 +448,14 @@ class AlgebraMathClass { ) ) { toggledBottom = true; - TickBitMap.flipTick(state, bottomTick, state.tickSpacing); + TickTable.toggleTick( + state, + bottomTick, + state.areTicksCompressed ? state.tickSpacing : undefined, + ); } if ( - Tick.update( + TickManager.update( state, topTick, cache.tick, @@ -463,13 +468,17 @@ class AlgebraMathClass { ) ) { toggledTop = true; - TickBitMap.flipTick(state, topTick, state.tickSpacing); + TickTable.toggleTick( + state, + topTick, + state.areTicksCompressed ? state.tickSpacing : undefined, + ); } } // skip fee && position related stuffs - // same as UniwapV3Pool.sol line 327 -> if (params.liquidityDelta != 0) { + // same as UniswapV3Pool.sol line 327 -> if (params.liquidityDelta != 0) { if (liquidityDelta !== 0n) { // if liquidityDelta is negative and the tick was toggled, it means that it should not be initialized anymore, so we delete it if (liquidityDelta < 0) { @@ -510,7 +519,7 @@ class AlgebraMathClass { let cache: SwapCalculationCache = { amountCalculated: 0n, - amountRequiredInitial: BI_MAX_INT, // similarly to waht we did for uniswap + amountRequiredInitial: BI_MAX_INT, // similarly to what we did for uniswap communityFee: 0n, exactInput: false, fee: 0n, @@ -563,19 +572,18 @@ class AlgebraMathClass { stepSqrtPrice: 0n, tickCount: 0, }; - let i = 0; // swap until there is remaining input or output tokens or we reach the price limit while (true) { step.stepSqrtPrice = currentPrice; //equivalent of tickTable.nextTickInTheSameRow(currentTick, zeroToOne); [step.nextTick, step.initialized] = - TickBitMap.nextInitializedTickWithinOneWord( + TickTable.nextInitializedTickWithinOneWord( poolState, currentTick, - poolState.tickSpacing, zeroToOne, false, + poolState.areTicksCompressed ? poolState.tickSpacing : undefined, ); step.nextTickPrice = TickMath.getSqrtRatioAtTick(step.nextTick); diff --git a/src/dex/algebra/lib/TickManager.ts b/src/dex/algebra/lib/TickManager.ts new file mode 100644 index 000000000..b9ae526cf --- /dev/null +++ b/src/dex/algebra/lib/TickManager.ts @@ -0,0 +1,91 @@ +import { IAlgebraPoolState } from '../types'; +import { LiquidityMath } from '../../uniswap-v3/contract-math/LiquidityMath'; +import { _require } from '../../../utils'; +import { NumberAsString } from '@paraswap/core'; +import { ZERO_TICK_INFO } from '../../uniswap-v3/constants'; +import { TickInfo } from '../../uniswap-v3/types'; + +export class TickManager { + static update( + state: Pick, + 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; + } + + // uint128 liquidityTotalBefore = data.liquidityTotal; + // uint128 liquidityTotalAfter = LiquidityMath.addDelta(liquidityTotalBefore, liquidityDelta); + // require(liquidityTotalAfter < Constants.MAX_LIQUIDITY_PER_TICK + 1, 'LO'); + const liquidityGrossBefore = info.liquidityGross; + const liquidityGrossAfter = LiquidityMath.addDelta( + liquidityGrossBefore, + liquidityDelta, + ); + _require( + liquidityGrossAfter <= maxLiquidity, + 'LO', + { liquidityGrossAfter, maxLiquidity }, + 'liquidityGrossAfter <= maxLiquidity', + ); + + // int128 liquidityDeltaBefore = data.liquidityDelta; + const liquidityNetBefore = info.liquidityNet; + + // // when the lower (upper) tick is crossed left to right (right to left), liquidity must be added (removed) + // data.liquidityDelta = upper + // ? int256(liquidityDeltaBefore).sub(liquidityDelta).toInt128() + // : int256(liquidityDeltaBefore).add(liquidityDelta).toInt128(); + // + // data.liquidityTotal = liquidityTotalAfter; + info.liquidityNet = upper + ? BigInt.asIntN( + 128, + BigInt.asIntN(256, liquidityNetBefore) - liquidityDelta, + ) + : BigInt.asIntN( + 128, + BigInt.asIntN(256, liquidityNetBefore) + liquidityDelta, + ); + + // flipped = (liquidityTotalAfter == 0); + // if (liquidityTotalBefore == 0) { + // flipped = !flipped; + // // by convention, we assume that all growth before a tick was initialized happened _below_ the tick + // if (tick <= currentTick) { + // data.outerFeeGrowth0Token = totalFeeGrowth0Token; + // data.outerFeeGrowth1Token = totalFeeGrowth1Token; + // data.outerSecondsPerLiquidity = secondsPerLiquidityCumulative; + // data.outerTickCumulative = tickCumulative; + // data.outerSecondsSpent = time; + // } + // data.initialized = true; + // } + + let flipped = liquidityGrossAfter === 0n; + if (liquidityGrossBefore === 0n) { + flipped = !flipped; + if (tick <= tickCurrent) { + info.secondsPerLiquidityOutsideX128 = secondsPerLiquidityCumulativeX128; + info.tickCumulativeOutside = tickCumulative; + info.secondsOutside = time; + } + info.initialized = true; + } + + // data.liquidityTotal = liquidityTotalAfter; + info.liquidityGross = liquidityGrossAfter; + + return flipped; + } +} diff --git a/src/dex/algebra/lib/TickTable.ts b/src/dex/algebra/lib/TickTable.ts new file mode 100644 index 000000000..c90dd50c9 --- /dev/null +++ b/src/dex/algebra/lib/TickTable.ts @@ -0,0 +1,216 @@ +import { IAlgebraPoolState } from '../types'; +import { _require } from '../../../utils'; +import { DeepReadonly } from 'ts-essentials'; +import { + OUT_OF_RANGE_ERROR_POSTFIX, + TICK_BITMAP_BUFFER, + TICK_BITMAP_TO_USE, +} from '../../uniswap-v3/constants'; +import { TickMath } from '../../uniswap-v3/contract-math/TickMath'; +import { Yul } from './yul-helper'; + +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 TickTable { + static position(tick: bigint) { + return [ + // wordPos + BigInt.asIntN(16, tick >> 8n), + // bitPos + BigInt.asUintN(8, tick & BigInt(0xff)), + ]; + } + + static toggleTick( + state: Pick, + tick: bigint, + tickSpacing?: bigint, + ) { + if (tickSpacing !== undefined) { + tick /= tickSpacing; + } + const [rowNumber, bitNumber] = TickTable.position(tick); + const mask = 1n << bitNumber; + + // toggleTick is used only in _updatePosition which is always state changing event + // Therefore it is never used in price query + isWordPosOut(rowNumber, state.startTickBitmap, false); + + const stringWordPos = rowNumber.toString(); + if (state.tickBitmap[stringWordPos] === undefined) { + state.tickBitmap[stringWordPos] = 0n; + } + + state.tickBitmap[stringWordPos] ^= mask; + if (state.tickBitmap[stringWordPos] === 0n) { + delete state.tickBitmap[stringWordPos]; + } + } + + static nextInitializedTickWithinOneWord( + state: DeepReadonly< + Pick + >, + tick: bigint, + lte: boolean, + isPriceQuery: boolean, + tickSpacing?: bigint, + ): [bigint, boolean] { + if (tickSpacing !== undefined) { + tick = Yul.sub( + Yul.sdiv(tick, tickSpacing), + Yul.and( + Yul.slt(tick, 0n), + Yul.not(Yul.iszero(Yul.smod(tick, tickSpacing))), + ), + ); + } + const [rowNumber, bitNumber] = TickTable.position(tick); + isWordPosOut(rowNumber, state.startTickBitmap, isPriceQuery); + let tickBitmapValue = state.tickBitmap[rowNumber.toString()]; + tickBitmapValue = tickBitmapValue === undefined ? 0n : tickBitmapValue; + if (lte) { + const _row = tickBitmapValue << (255n - bitNumber); + if (_row != 0n) { + tick -= BigInt.asIntN(24, 255n - TickTable.getMostSignificantBit(_row)); + return [TickTable.boundTick(tick, tickSpacing), true]; + } else { + tick -= BigInt.asIntN(24, bitNumber); + return [TickTable.boundTick(tick, tickSpacing), false]; + } + } else { + tick += 1n; + const _row = tickBitmapValue >> bitNumber; + if (_row !== 0n) { + tick += BigInt.asIntN( + 24, + TickTable.getSingleSignificantBit(-_row & _row), + ); + return [TickTable.boundTick(tick, tickSpacing), true]; + } else { + tick += BigInt.asIntN(24, 255n - bitNumber); + return [TickTable.boundTick(tick, tickSpacing), false]; + } + } + } + + static getSingleSignificantBit(word: bigint): bigint { + let singleBitPos = 0n; + singleBitPos = Yul.iszero( + word && + BigInt( + '0x5555555555555555555555555555555555555555555555555555555555555555', + ), + ); + singleBitPos = + singleBitPos || + Yul.iszero( + word && + BigInt( + '0x00000000000000000000000000000000ffffffffffffffffffffffffffffffff', + ), + ) << 7n; + singleBitPos = + singleBitPos || + Yul.iszero( + word && + BigInt( + '0x0000000000000000ffffffffffffffff0000000000000000ffffffffffffffff', + ), + ) << 6n; + singleBitPos = + singleBitPos || + Yul.iszero( + word && + BigInt( + '0x00000000ffffffff00000000ffffffff00000000ffffffff00000000ffffffff', + ), + ) << 5n; + singleBitPos = + singleBitPos || + Yul.iszero( + word && + BigInt( + '0x0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff', + ), + ) << 4n; + singleBitPos = + singleBitPos || + Yul.iszero( + word && + BigInt( + '0x00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff', + ), + ) << 3n; + singleBitPos = + singleBitPos || + Yul.iszero( + word && + BigInt( + '0x0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f', + ), + ) << 2n; + singleBitPos = + singleBitPos || + Yul.iszero( + word && + BigInt( + '0x3333333333333333333333333333333333333333333333333333333333333333', + ), + ) << 1n; + + return BigInt.asUintN(8, singleBitPos); + } + + static getMostSignificantBit(word: bigint): bigint { + word = word || word >> 1n; + word = word || word >> 2n; + word = word || word >> 4n; + word = word || word >> 8n; + word = word || word >> 16n; + word = word || word >> 32n; + word = word || word >> 64n; + word = word || word >> 128n; + word = word - (word >> 1n); + return TickTable.getSingleSignificantBit(BigInt.asUintN(256, word)); + } + + static boundTick(tick: bigint, tickSpacing?: bigint): bigint { + if (tickSpacing !== undefined) { + tick *= tickSpacing; + } + let boundedTick = tick; + if (boundedTick < TickMath.MIN_TICK) { + boundedTick = TickMath.MIN_TICK; + } else if (boundedTick > TickMath.MAX_TICK) { + boundedTick = TickMath.MAX_TICK; + } + return boundedTick; + } +} diff --git a/src/dex/algebra/lib/yul-helper.ts b/src/dex/algebra/lib/yul-helper.ts new file mode 100644 index 000000000..ec5ae91e2 --- /dev/null +++ b/src/dex/algebra/lib/yul-helper.ts @@ -0,0 +1,29 @@ +export class Yul { + static sub(x: bigint, y: bigint): bigint { + return x - y; + } + + static sdiv(x: bigint, y: bigint): bigint { + return y === 0n ? 0n : x / y; + } + + static and(x: bigint, y: bigint): bigint { + return x & y; + } + + static slt(x: bigint, y: bigint): bigint { + return x < y ? 1n : 0n; + } + + static not(x: bigint): bigint { + return ~x; + } + + static iszero(x: bigint): bigint { + return x === 0n ? 1n : 0n; + } + + static smod(x: bigint, y: bigint): bigint { + return y === 0n ? 0n : x % y; + } +} diff --git a/src/dex/algebra/types.ts b/src/dex/algebra/types.ts index a7bee6493..98a8c1d33 100644 --- a/src/dex/algebra/types.ts +++ b/src/dex/algebra/types.ts @@ -23,6 +23,7 @@ export type PoolStateV1_1 = { startTickBitmap: bigint; balance0: bigint; balance1: bigint; + areTicksCompressed: boolean; }; type GlobalState_v1_9 = { @@ -47,6 +48,7 @@ export type PoolState_v1_9 = { startTickBitmap: bigint; balance0: bigint; balance1: bigint; + areTicksCompressed: boolean; }; export type AlgebraData = { @@ -72,6 +74,8 @@ export type DexParams = { forceRPC?: boolean; }; +export type IAlgebraPoolState = PoolStateV1_1 | PoolState_v1_9; + export type TickBitMapMappingsWithBigNumber = { index: number; value: BigNumber; diff --git a/src/dex/balancer-v2/balancer-v2-e2e.test.ts b/src/dex/balancer-v2/balancer-v2-e2e.test.ts index cef5c2cf1..1aadf7669 100644 --- a/src/dex/balancer-v2/balancer-v2-e2e.test.ts +++ b/src/dex/balancer-v2/balancer-v2-e2e.test.ts @@ -1007,6 +1007,183 @@ describe('BalancerV2 E2E', () => { }); }); + describe('BalancerV2 Avalanche', () => { + const dexKey = 'BalancerV2'; + const network = Network.AVALANCHE; + const tokens = Tokens[Network.AVALANCHE]; + const holders = Holders[Network.AVALANCHE]; + const provider = new StaticJsonRpcProvider( + generateConfig(network).privateHttpProvider, + network, + ); + + describe('ComposableStable', () => { + const sideToContractMethods = new Map([ + [ + SwapSide.SELL, + [ + ContractMethod.simpleSwap, + ContractMethod.multiSwap, + ContractMethod.megaSwap, + ], + ], + ]); + + const pairs: { name: string; sellAmount: string; buyAmount: string }[][] = + [ + [ + { + name: 'sAVAX', + sellAmount: '20000000000000', + buyAmount: '200000000', + }, + { + name: 'AVAX', + sellAmount: '200000000', + buyAmount: '2000000000000000', + }, + ], + [ + { + name: 'USDT', + sellAmount: '20000000', + buyAmount: '200000000', + }, + { + name: 'USDC', + sellAmount: '200000000', + buyAmount: '2000000000', + }, + ] + ]; + + 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, + ); + }); + }); + }); + }); + }), + ); + }); + + describe('Weighted Pool', () => { + 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: 'BETS', + sellAmount: '20000000000000', + buyAmount: '200000000', + }, + { + name: 'sAVAX', + sellAmount: '200000000', + buyAmount: '2000000000000000', + }, + ], + [ + { + name: 'HATCHY', + sellAmount: '20000000000000', + buyAmount: '200000000', + }, + { + name: 'AVAX', + sellAmount: '200000000', + buyAmount: '2000000000000000', + }, + ] + ]; + + 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, + ); + }); + }); + }); + }); + }), + ); + }); + + }); + describe('BeetsFi OPTIMISM', () => { const dexKey = 'BeetsFi'; const network = Network.OPTIMISM; @@ -1259,4 +1436,5 @@ describe('BalancerV2 E2E', () => { }); }); }); + }); diff --git a/src/dex/balancer-v2/balancer-v2.ts b/src/dex/balancer-v2/balancer-v2.ts index ac1c3f443..f71274414 100644 --- a/src/dex/balancer-v2/balancer-v2.ts +++ b/src/dex/balancer-v2/balancer-v2.ts @@ -826,6 +826,8 @@ export class BalancerV2 prices: resOut.prices, data: { poolId: pool.id, + tokenIn: _from.address.toLowerCase(), + tokenOut: _to.address.toLowerCase(), }, poolAddresses: [poolAddress], exchange: this.dexKey, diff --git a/src/dex/balancer-v2/config.ts b/src/dex/balancer-v2/config.ts index 9352193a6..cbb0b3a77 100644 --- a/src/dex/balancer-v2/config.ts +++ b/src/dex/balancer-v2/config.ts @@ -19,6 +19,11 @@ export const BalancerConfig: DexConfigMap = { 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-arbitrum-v2', vaultAddress: '0xBA12222222228d8Ba445958a75a0704d566BF2C8', }, + [Network.AVALANCHE]: { + subgraphURL: + 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-avalanche-v2', + vaultAddress: '0xBA12222222228d8Ba445958a75a0704d566BF2C8', + } }, BeetsFi: { [Network.FANTOM]: { @@ -63,4 +68,8 @@ export const Adapters: Record = { [SwapSide.SELL]: [{ name: 'OptimismAdapter01', index: 4 }], [SwapSide.BUY]: [{ name: 'OptimismBuyAdapter', index: 5 }], }, + [Network.AVALANCHE]: { + [SwapSide.SELL]: [{ name: 'AvalancheAdapter01', index: 8 }], + [SwapSide.BUY]: [{ name: 'AvalancheBuyAdapter', index: 7 }], + } }; diff --git a/src/dex/balancer-v2/optimizer.ts b/src/dex/balancer-v2/optimizer.ts index 818c2f7f8..201b7a19f 100644 --- a/src/dex/balancer-v2/optimizer.ts +++ b/src/dex/balancer-v2/optimizer.ts @@ -1,70 +1,83 @@ -import { UnoptimizedRate, OptimalSwapExchange } from '../../types'; -import { BalancerSwapV2 } from './types'; +import _ from 'lodash'; +import { UnoptimizedRate } from '../../types'; import { SwapSide } from '../../constants'; import { BalancerConfig } from './config'; - -const MAX_UINT256 = - '115792089237316195423570985008687907853269984665640564039457584007913129639935'; - -const AllBalancerV2Forks = Object.keys(BalancerConfig); +import { OptimalSwap } from '@paraswap/core'; export function balancerV2Merge(or: UnoptimizedRate): UnoptimizedRate { - const fixSwap = ( - rawSwap: OptimalSwapExchange[], - side: SwapSide, - ): OptimalSwapExchange[] => { - const newBalancers: { [key: string]: OptimalSwapExchange } = {}; - let optimizedSwap = new Array>(); - rawSwap.forEach((s: OptimalSwapExchange) => { - const exchangeKey = s.exchange.toLowerCase(); - if (AllBalancerV2Forks.some(d => d.toLowerCase() === exchangeKey)) { - if (!(exchangeKey in newBalancers)) { - newBalancers[exchangeKey] = { - exchange: s.exchange, - srcAmount: '0', - destAmount: '0', - percent: 0, - poolAddresses: [], - data: { - swaps: new Array(), - gasUSD: '0', - }, - }; - } - newBalancers[exchangeKey].srcAmount = ( - BigInt(newBalancers[exchangeKey].srcAmount) + BigInt(s.srcAmount) + const balancerForksList = Object.keys(BalancerConfig).map(b => + b.toLowerCase(), + ); + const fixSwap = (rawRate: OptimalSwap[], side: SwapSide): OptimalSwap[] => { + let lastExchange: false | OptimalSwap = false; + let optimizedRate = new Array(); + rawRate.forEach((s: OptimalSwap) => { + if ( + s.swapExchanges.length !== 1 || + !balancerForksList.includes(s.swapExchanges[0].exchange.toLowerCase()) + ) { + lastExchange = false; + optimizedRate.push(s); + } else if ( + lastExchange && + lastExchange.swapExchanges[0].exchange.toLowerCase() === + s.swapExchanges[0].exchange.toLowerCase() && + _.last( + lastExchange.swapExchanges[0].data.swaps, + )!.tokenOut.toLowerCase() === + s.swapExchanges[0].data.tokenIn.toLowerCase() + ) { + const [lastExchangeSwap] = lastExchange.swapExchanges; + const [currentSwap] = s.swapExchanges; + lastExchangeSwap.srcAmount = ( + BigInt(lastExchangeSwap.srcAmount) + BigInt(currentSwap.srcAmount) ).toString(); - newBalancers[exchangeKey].destAmount = ( - BigInt(newBalancers[exchangeKey].destAmount) + BigInt(s.destAmount) + lastExchangeSwap.destAmount = ( + BigInt(lastExchangeSwap.destAmount) + BigInt(currentSwap.destAmount) ).toString(); - newBalancers[exchangeKey].percent += s.percent; - newBalancers[exchangeKey].data.exchangeProxy = s.data.exchangeProxy; - newBalancers[exchangeKey].data.gasUSD = ( - parseFloat(newBalancers[exchangeKey].data.gasUSD) + - parseFloat(s.data.gasUSD) + lastExchangeSwap.percent += currentSwap.percent; + lastExchangeSwap.data.gasUSD = ( + parseFloat(lastExchangeSwap.data.gasUSD) + + parseFloat(currentSwap.data.gasUSD) ).toFixed(6); - newBalancers[exchangeKey].data.swaps.push({ - poolId: s.data.poolId, - amount: side === SwapSide.SELL ? s.srcAmount : s.destAmount, + lastExchangeSwap.data.swaps.push({ + poolId: currentSwap.data.poolId, + amount: + side === SwapSide.SELL + ? currentSwap.srcAmount + : currentSwap.destAmount, + tokenIn: currentSwap.data.tokenIn, + tokenOut: currentSwap.data.tokenOut, }); - newBalancers[exchangeKey].poolAddresses!.push(s.poolAddresses![0]); + lastExchangeSwap.poolAddresses!.push(currentSwap.poolAddresses![0]); } else { - optimizedSwap.push(s); + lastExchange = _.cloneDeep(s); + lastExchange.swapExchanges[0].data = {}; + lastExchange.swapExchanges[0].data.gasUSD = + s.swapExchanges[0].data.gasUSD; + lastExchange.swapExchanges[0].data.swaps = [ + { + poolId: s.swapExchanges[0].data.poolId, + amount: + side === SwapSide.SELL + ? s.swapExchanges[0].srcAmount + : s.swapExchanges[0].destAmount, + tokenIn: s.swapExchanges[0].data.tokenIn, + tokenOut: s.swapExchanges[0].data.tokenOut, + }, + ]; + optimizedRate.push(lastExchange); } }); - optimizedSwap = optimizedSwap.concat(Object.values(newBalancers)); - return optimizedSwap; + return optimizedRate; }; or.bestRoute = or.bestRoute.map(r => ({ ...r, - swaps: r.swaps.map(s => ({ - ...s, - swapExchanges: fixSwap(s.swapExchanges, or.side), - })), + swaps: fixSwap(r.swaps, or.side), })); return or; } diff --git a/src/dex/balancer-v2/types.ts b/src/dex/balancer-v2/types.ts index f1ef3194c..33dd35bfe 100644 --- a/src/dex/balancer-v2/types.ts +++ b/src/dex/balancer-v2/types.ts @@ -82,6 +82,8 @@ export interface SubgraphPoolBase { export type BalancerSwapV2 = { poolId: string; amount: string; + tokenIn: string; + tokenOut: string; }; export type OptimizedBalancerV2Data = { @@ -139,6 +141,8 @@ export type BalancerV2DirectParam = [ export type BalancerV2Data = { poolId: string; + tokenIn: string; + tokenOut: string; }; export type DexParams = { diff --git a/src/dex/curve-v1-factory/price-handlers/functions/calc_token_amount.ts b/src/dex/curve-v1-factory/price-handlers/functions/calc_token_amount.ts index d8c37134c..cb83a1dfe 100644 --- a/src/dex/curve-v1-factory/price-handlers/functions/calc_token_amount.ts +++ b/src/dex/curve-v1-factory/price-handlers/functions/calc_token_amount.ts @@ -35,6 +35,38 @@ const customPlain3CoinThree: calc_token_amount = ( return (diff * token_amount) / D0; }; +const factoryPlain2Basic: calc_token_amount = ( + self: IPoolContext, + state: PoolState, + amounts: bigint[], + is_deposit: boolean, +) => { + const { N_COINS } = self.constants; + const amp = state.A; + const balances = [...state.balances]; + const D0 = self.get_D(self, balances, amp); + for (const i of _.range(N_COINS)) { + if (is_deposit) balances[i] += amounts[i]; + else balances[i] -= amounts[i]; + } + const D1 = self.get_D(self, balances, amp); + + if (state.totalSupply === undefined) { + throw new Error( + `${self.IMPLEMENTATION_NAME} customPlain3CoinThree: totalSupply is not provided`, + ); + } + + const token_amount = state.totalSupply; + let diff = 0n; + if (is_deposit) { + diff = D1 - D0; + } else { + diff = D0 - D1; + } + return (diff * token_amount) / D0; +}; + const customAvalanche3CoinLending: calc_token_amount = ( self: IPoolContext, state: PoolState, @@ -114,7 +146,7 @@ const implementations: Record = { [ImplementationNames.FACTORY_META_USD_BALANCES_FRAX_USDC]: notImplemented, [ImplementationNames.FACTORY_PLAIN_2_BALANCES]: notImplemented, - [ImplementationNames.FACTORY_PLAIN_2_BASIC]: notImplemented, + [ImplementationNames.FACTORY_PLAIN_2_BASIC]: factoryPlain2Basic, [ImplementationNames.FACTORY_PLAIN_2_ETH]: notImplemented, [ImplementationNames.FACTORY_PLAIN_2_OPTIMIZED]: notImplemented, diff --git a/src/dex/pancakeswap-v3/config.ts b/src/dex/pancakeswap-v3/config.ts index b4ca340ff..43636ee06 100644 --- a/src/dex/pancakeswap-v3/config.ts +++ b/src/dex/pancakeswap-v3/config.ts @@ -38,6 +38,21 @@ export const PancakeswapV3Config: DexConfigMap = { subgraphURL: 'https://api.thegraph.com/subgraphs/name/pancakeswap/exchange-v3-bsc', }, + [Network.ARBITRUM]: { + factory: '0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865', + deployer: '0x41ff9AA7e16B8B1a8a8dc4f0eFacd93D02d071c9', + quoter: '0xB048Bbc1Ee6b733FFfCFb9e9CeF7375518e25997', + router: '0x1b81D678ffb9C0263b24A97847620C99d213eB14', + supportedFees: PANCAKE_SUPPORTED_FEES, + stateMulticall: '0xF8498aCeD3aFa417653415B8e32BAE9d764FBFf5', + uniswapMulticall: '0x1F98415757620B543A52E61c46B32eB19261F984', + chunksCount: 10, + initRetryFrequency: 30, + initHash: + '0x6ce8eb472fa82df5469c6ab6d485f17c3ad13c8cd7af59b3d4a8026c5ce0f7e2', + subgraphURL: + 'https://api.studio.thegraph.com/query/45376/exchange-v3-arbitrum/version/latest', + }, }, }; @@ -50,4 +65,8 @@ export const Adapters: Record = { [SwapSide.SELL]: [{ name: 'BscAdapter02', index: 4 }], [SwapSide.BUY]: [{ name: 'BscBuyAdapter', index: 5 }], }, + [Network.ARBITRUM]: { + [SwapSide.SELL]: [{ name: 'ArbitrumAdapter01', index: 3 }], + [SwapSide.BUY]: [{ name: 'ArbitrumBuyAdapter', index: 2 }], + }, }; diff --git a/src/dex/pancakeswap-v3/pancakeswap-v3-e2e.test.ts b/src/dex/pancakeswap-v3/pancakeswap-v3-e2e.test.ts index b9e03e693..9be62cd7e 100644 --- a/src/dex/pancakeswap-v3/pancakeswap-v3-e2e.test.ts +++ b/src/dex/pancakeswap-v3/pancakeswap-v3-e2e.test.ts @@ -229,4 +229,82 @@ describe('PancakeswapV3 E2E', () => { }), ); }); + + describe('PancakeswapV3 Arbitrum', () => { + const network = Network.ARBITRUM; + 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: '2687649500000000', + buyAmount: '5000000', + }, + { + name: 'USDC', + sellAmount: '5000000', + buyAmount: '1000000000000000', + }, + ], + ]; + + 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/pancakeswap-v3/pancakeswap-v3.ts b/src/dex/pancakeswap-v3/pancakeswap-v3.ts index c38f5089a..41d2de86e 100644 --- a/src/dex/pancakeswap-v3/pancakeswap-v3.ts +++ b/src/dex/pancakeswap-v3/pancakeswap-v3.ts @@ -596,6 +596,11 @@ export class PancakeswapV3 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; } @@ -618,7 +623,7 @@ export class PancakeswapV3 balanceDestToken, ); - if (!unitResult || !pricesResult) { + if (!pricesResult) { this.logger.debug('Prices or unit is not calculated'); return null; } @@ -639,7 +644,7 @@ export class PancakeswapV3 }), ]; return { - unit: unitResult.outputs[0], + unit: unitResult?.outputs[0] || 0n, prices, data: { path: [ diff --git a/src/dex/uniswap-v2/config.ts b/src/dex/uniswap-v2/config.ts index 4c4399048..34fb93bc6 100644 --- a/src/dex/uniswap-v2/config.ts +++ b/src/dex/uniswap-v2/config.ts @@ -386,6 +386,15 @@ export const UniswapV2Config: DexConfigMap = { poolGasCost: 90 * 1000, feeCode: 25, }, + [Network.ARBITRUM]: { + subgraphURL: + 'https://api.studio.thegraph.com/query/45376/exchange-v2-arbitrum/version/latest', + factoryAddress: '0x02a84c1b3BBD7401a5f7fa98a384EBC70bB5749E', + initCode: + '0x57224589c67f3f30a6b0d7a1b54cf3153ab84563bc609ef41dfb34f8b2974d2d', + poolGasCost: 90 * 1000, + feeCode: 25, + }, }, PaintSwap: { [Network.FANTOM]: { diff --git a/src/dex/uniswap-v2/uniswap-v2-e2e-arbitrum.test.ts b/src/dex/uniswap-v2/uniswap-v2-e2e-arbitrum.test.ts index 2ae9450b2..5768af377 100644 --- a/src/dex/uniswap-v2/uniswap-v2-e2e-arbitrum.test.ts +++ b/src/dex/uniswap-v2/uniswap-v2-e2e-arbitrum.test.ts @@ -359,4 +359,80 @@ describe('UniswapV2 E2E Arbitrum', () => { }), ); }); + + describe('PancakeSwapV2', () => { + const dexKey = 'PancakeSwapV2'; + + 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: 'ETH', sellAmount: '505000000000000', buyAmount: '940617' }, + { name: 'USDC', sellAmount: '940617', buyAmount: '505000000000000' }, + ], + [ + { + name: 'ETH', + sellAmount: '631955000000000', + buyAmount: '1000000000000000000', + }, + { + name: 'ARB', + sellAmount: '1000000000000000000', + buyAmount: '631955000000000', + }, + ], + ]; + + 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/uniswap-v3/contract-math/TickBitMap.ts b/src/dex/uniswap-v3/contract-math/TickBitMap.ts index e2fb5c4b2..405cfa70e 100644 --- a/src/dex/uniswap-v3/contract-math/TickBitMap.ts +++ b/src/dex/uniswap-v3/contract-math/TickBitMap.ts @@ -120,13 +120,4 @@ export class TickBitMap { 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/uniswap-v3/contract-math/utils.ts b/src/dex/uniswap-v3/contract-math/utils.ts index 3506c445c..65d97736d 100644 --- a/src/dex/uniswap-v3/contract-math/utils.ts +++ b/src/dex/uniswap-v3/contract-math/utils.ts @@ -38,16 +38,18 @@ export function _reduceTicks( ) { return ticksToReduce.reduce>((acc, curr) => { const { index, value } = curr; - acc[index] = { - liquidityGross: bigIntify(value.liquidityGross), - liquidityNet: bigIntify(value.liquidityNet), - tickCumulativeOutside: bigIntify(value.tickCumulativeOutside), - secondsPerLiquidityOutsideX128: bigIntify( - value.secondsPerLiquidityOutsideX128, - ), - secondsOutside: bigIntify(value.secondsOutside), - initialized: value.initialized, - }; + if (value.initialized) { + acc[index] = { + liquidityGross: bigIntify(value.liquidityGross), + liquidityNet: bigIntify(value.liquidityNet), + tickCumulativeOutside: bigIntify(value.tickCumulativeOutside), + secondsPerLiquidityOutsideX128: bigIntify( + value.secondsPerLiquidityOutsideX128, + ), + secondsOutside: bigIntify(value.secondsOutside), + initialized: value.initialized, + }; + } return acc; }, ticks); } diff --git a/src/dex/uniswap-v3/uniswap-v3.ts b/src/dex/uniswap-v3/uniswap-v3.ts index 347fad846..486f29cae 100644 --- a/src/dex/uniswap-v3/uniswap-v3.ts +++ b/src/dex/uniswap-v3/uniswap-v3.ts @@ -276,7 +276,7 @@ export class UniswapV3 e, ); } else { - // on unkown error mark as failed and increase retryCount for retry init strategy + // on unknown 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}`, @@ -620,6 +620,11 @@ export class UniswapV3 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; } diff --git a/tests/constants-e2e.ts b/tests/constants-e2e.ts index 7ec3c0673..004aa98b0 100644 --- a/tests/constants-e2e.ts +++ b/tests/constants-e2e.ts @@ -698,6 +698,14 @@ export const Tokens: { address: '0xCFc37A6AB183dd4aED08C204D1c2773c0b1BDf46', decimals: 18, }, + BETS: { + address: '0x94025780a1ab58868d9b2dbbb775f44b32e8e6e5', + decimals: 18, + }, + HATCHY: { + address: '0x502580fc390606b47fc3b741d6d49909383c28a9', + decimals: 18, + }, }, [Network.ARBITRUM]: { DAI: { @@ -715,10 +723,14 @@ export const Tokens: { addAllowance: _allowancesFn, }, ETH: { address: ETHER_ADDRESS, decimals: 18 }, - USDC: { + USDCe: { address: '0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8', decimals: 6, }, + USDC: { + address: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + decimals: 6, + }, OHM: { address: '0xf0cb2dc0db5e6c66b9a70ac27b06b878da017028', decimals: 9, @@ -773,6 +785,14 @@ export const Tokens: { address: '0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f', decimals: 8, }, + USDCe: { + address: '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', + decimals: 6, + }, + LEX: { + address: '0x6bB7A17AcC227fd1F6781D1EEDEAE01B42047eE0', + decimals: 18, + }, }, [Network.OPTIMISM]: { DAI: { @@ -965,6 +985,8 @@ export const Holders: { avWAVAX: '0xc5ed2333f8a2C351fCA35E5EBAdb2A82F5d254C3', WAVAX: '0xbbff2a8ec8d702e61faaccf7cf705968bb6a5bab', sAVAX: '0xC73DF1e68FC203F6E4b6270240D6f82A850e8D38', + BETS: '0x8cc2284c90d05578633418f9cde104f402375a65', + HATCHY: '0x14ec295ec8def851ec6e2959df872dd24e422631', USDCe: '0x3a2434c698f8d79af1f5a9e43013157ca8b11a66', USDC: '0xbf14db80d9275fb721383a77c00ae180fc40ae98', USDTe: '0x84d34f4f83a87596cd3fb6887cff8f17bf5a7b83', @@ -995,7 +1017,8 @@ export const Holders: { ETH: '0xF977814e90dA44bFA03b6295A0616a897441aceC', DAI: '0x07d7f291e731a41d3f0ea4f1ae5b6d920ffb3fe0', WETH: '0xc31e54c7a869b9fcbecc14363cf510d1c41fa443', - USDC: '0x62383739d68dd0f844103db8dfb05a7eded5bbe6', + USDCe: '0x62383739d68dd0f844103db8dfb05a7eded5bbe6', + USDC: '0xa843392198862f98d17e3aa1421b08f2c2020cff', OHM: '0xebce5f29ff5ca9aa330ebdf7ec6b5f474bff271e', USDT: '0x62383739d68dd0f844103db8dfb05a7eded5bbe6', POPS: '0x4b78b52e7de4d8b7d367297cb8a87c1875a9d591',