diff --git a/package.json b/package.json index 0d8297852..48411ccfe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@paraswap/dex-lib", - "version": "4.0.5", + "version": "4.0.6", "main": "build/index.js", "types": "build/index.d.ts", "repository": "https://github.com/paraswap/paraswap-dex-lib", diff --git a/src/abi/fluid-dex/fluid-dex.abi.json b/src/abi/fluid-dex/fluid-dex.abi.json index 14ada197a..cdd5e0f1a 100644 --- a/src/abi/fluid-dex/fluid-dex.abi.json +++ b/src/abi/fluid-dex/fluid-dex.abi.json @@ -1,9 +1,119 @@ [ + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "dexId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "liquidity", + "type": "address" + }, + { + "internalType": "address", + "name": "factory", + "type": "address" + }, + { + "components": [ + { + "internalType": "address", + "name": "shift", + "type": "address" + }, + { + "internalType": "address", + "name": "admin", + "type": "address" + }, + { + "internalType": "address", + "name": "colOperations", + "type": "address" + }, + { + "internalType": "address", + "name": "debtOperations", + "type": "address" + }, + { + "internalType": "address", + "name": "perfectOperationsAndSwapOut", + "type": "address" + } + ], + "internalType": "struct Structs.Implementations", + "name": "implementations", + "type": "tuple" + }, + { + "internalType": "address", + "name": "deployerContract", + "type": "address" + }, + { + "internalType": "address", + "name": "token0", + "type": "address" + }, + { + "internalType": "address", + "name": "token1", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "supplyToken0Slot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "borrowToken0Slot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "supplyToken1Slot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "borrowToken1Slot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "exchangePriceToken0Slot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "exchangePriceToken1Slot", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "oracleMapping", + "type": "uint256" + } + ], + "internalType": "struct Structs.ConstantViews", + "name": "constantViews_", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, { "inputs": [ { "internalType": "uint256", - "name": "errorId", + "name": "errorId_", "type": "uint256" } ], @@ -14,7 +124,18 @@ "inputs": [ { "internalType": "uint256", - "name": "shares", + "name": "errorId", + "type": "uint256" + } + ], + "name": "FluidDexFactoryError", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "shares_", "type": "uint256" } ], @@ -87,7 +208,7 @@ "type": "uint256" } ], - "internalType": "struct IFluidDexT1.PricesAndExchangePrice", + "internalType": "struct Structs.PricesAndExchangePrice", "name": "pex_", "type": "tuple" } @@ -117,6 +238,346 @@ "name": "FluidDexSwapResult", "type": "error" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "errorId_", + "type": "uint256" + } + ], + "name": "FluidLiquidityCalcsError", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "errorId_", + "type": "uint256" + } + ], + "name": "FluidSafeTransferError", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "int256", + "name": "routing", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amtOut", + "type": "uint256" + } + ], + "name": "LogArbitrage", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "LogBorrowDebtLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token0Amt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token1Amt", + "type": "uint256" + } + ], + "name": "LogBorrowPerfectDebtLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "LogDepositColLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token0Amt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token1Amt", + "type": "uint256" + } + ], + "name": "LogDepositPerfectColLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + ], + "name": "LogPauseSwapAndArbitrage", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + ], + "name": "LogUnpauseSwapAndArbitrage", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token0Amt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token1Amt", + "type": "uint256" + } + ], + "name": "LogPaybackDebtInOneToken", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "LogPaybackDebtLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token0Amt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token1Amt", + "type": "uint256" + } + ], + "name": "LogPaybackPerfectDebtLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token0Amt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token1Amt", + "type": "uint256" + } + ], + "name": "LogWithdrawColInOneToken", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "LogWithdrawColLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token0Amt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "token1Amt", + "type": "uint256" + } + ], + "name": "LogWithdrawPerfectColLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "swap0to1", + "type": "bool" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "Swap", + "type": "event" + }, + { + "stateMutability": "payable", + "type": "fallback" + }, { "inputs": [], "name": "DEX_ID", @@ -148,9 +609,9 @@ "type": "uint256" }, { - "internalType": "bool", - "name": "estimate_", - "type": "bool" + "internalType": "address", + "name": "to_", + "type": "address" } ], "name": "borrow", @@ -182,9 +643,9 @@ "type": "uint256" }, { - "internalType": "bool", - "name": "estimate_", - "type": "bool" + "internalType": "address", + "name": "to_", + "type": "address" } ], "name": "borrowPerfect", @@ -248,11 +709,11 @@ }, { "internalType": "address", - "name": "perfectOperationsAndOracle", + "name": "perfectOperationsAndSwapOut", "type": "address" } ], - "internalType": "struct IFluidDexT1.Implementations", + "internalType": "struct Structs.Implementations", "name": "implementations", "type": "tuple" }, @@ -307,7 +768,7 @@ "type": "uint256" } ], - "internalType": "struct IFluidDexT1.ConstantViews", + "internalType": "struct Structs.ConstantViews", "name": "constantsView_", "type": "tuple" } @@ -342,7 +803,7 @@ "type": "uint256" } ], - "internalType": "struct IFluidDexT1.ConstantViews2", + "internalType": "struct Structs.ConstantViews2", "name": "constantsView2_", "type": "tuple" } @@ -476,7 +937,7 @@ "type": "uint256" } ], - "internalType": "struct IFluidDexT1.CollateralReserves", + "internalType": "struct Structs.CollateralReserves", "name": "c_", "type": "tuple" } @@ -547,7 +1008,7 @@ "type": "uint256" } ], - "internalType": "struct IFluidDexT1.DebtReserves", + "internalType": "struct Structs.DebtReserves", "name": "d_", "type": "tuple" } @@ -562,6 +1023,29 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "token_", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount_", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data_", + "type": "bytes" + } + ], + "name": "liquidityCallback", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -605,7 +1089,7 @@ "type": "uint256" } ], - "internalType": "struct IFluidDexT1.Oracle[]", + "internalType": "struct Structs.Oracle[]", "name": "twaps_", "type": "tuple[]" }, @@ -778,6 +1262,40 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bool", + "name": "swap0to1_", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "amountIn_", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin_", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to_", + "type": "address" + } + ], + "name": "swapInWithCallback", + "outputs": [ + { + "internalType": "uint256", + "name": "amountOut_", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { @@ -812,6 +1330,40 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bool", + "name": "swap0to1_", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "amountOut_", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountInMax_", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to_", + "type": "address" + } + ], + "name": "swapOutWithCallback", + "outputs": [ + { + "internalType": "uint256", + "name": "amountIn_", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { @@ -830,9 +1382,9 @@ "type": "uint256" }, { - "internalType": "bool", - "name": "estimate_", - "type": "bool" + "internalType": "address", + "name": "to_", + "type": "address" } ], "name": "withdraw", @@ -864,9 +1416,9 @@ "type": "uint256" }, { - "internalType": "bool", - "name": "estimate_", - "type": "bool" + "internalType": "address", + "name": "to_", + "type": "address" } ], "name": "withdrawPerfect", @@ -903,9 +1455,9 @@ "type": "uint256" }, { - "internalType": "bool", - "name": "estimate_", - "type": "bool" + "internalType": "address", + "name": "to_", + "type": "address" } ], "name": "withdrawPerfectInOneToken", @@ -918,5 +1470,9 @@ ], "stateMutability": "nonpayable", "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" } ] diff --git a/src/dex/fluid-dex/constants.ts b/src/dex/fluid-dex/constants.ts new file mode 100644 index 000000000..da6d99534 --- /dev/null +++ b/src/dex/fluid-dex/constants.ts @@ -0,0 +1 @@ +export const MIN_SWAP_LIQUIDITY = 10n ** 4n; diff --git a/src/dex/fluid-dex/fluid-dex-e2e.test.ts b/src/dex/fluid-dex/fluid-dex-e2e.test.ts index a6873d07d..333aed44d 100644 --- a/src/dex/fluid-dex/fluid-dex-e2e.test.ts +++ b/src/dex/fluid-dex/fluid-dex-e2e.test.ts @@ -118,6 +118,23 @@ describe('FluidDex E2E', () => { describe('Mainnet', () => { const network = Network.MAINNET; + describe('ETH -> INST', () => { + const tokenASymbol: string = 'ETH'; + const tokenBSymbol: string = 'INST'; + + const tokenAAmount: string = '100000000000000'; + const tokenBAmount: string = '100000000000000'; + + testForNetwork( + network, + dexKey, + tokenASymbol, + tokenBSymbol, + tokenAAmount, + tokenBAmount, + ); + }); + describe('ETH -> wstETH', () => { const tokenASymbol: string = 'wstETH'; const tokenBSymbol: string = 'ETH'; diff --git a/src/dex/fluid-dex/fluid-dex-events.test.ts b/src/dex/fluid-dex/fluid-dex-events.test.ts index 19d6a89d7..f5bad501d 100644 --- a/src/dex/fluid-dex/fluid-dex-events.test.ts +++ b/src/dex/fluid-dex/fluid-dex-events.test.ts @@ -10,14 +10,16 @@ import { FluidDexLiquidityProxyState } from './types'; import { FluidDexConfig } from './config'; import { FluidDexLiquidityProxy } from './fluid-dex-liquidity-proxy'; import { FluidDexFactory } from './fluid-dex-factory'; +import { FluidDexEventPool } from './fluid-dex-pool'; +import { StatefulEventSubscriber } from '../../stateful-event-subscriber'; jest.setTimeout(50 * 1000); -async function fetchLiquidityProxyState( - liquidityProxy: FluidDexLiquidityProxy, +async function fetchState( + statefulEventSubscriber: StatefulEventSubscriber, blockNumber: number, -): Promise { - return liquidityProxy.generateState(blockNumber); +): Promise { + return statefulEventSubscriber.generateState(blockNumber); } // eventName -> blockNumbers @@ -64,7 +66,7 @@ describe('FluidDex EventPool Mainnet', function () { liquidityProxy, liquidityProxy.addressesSubscribed, (_blockNumber: number) => - fetchLiquidityProxyState(liquidityProxy, _blockNumber), + fetchState(liquidityProxy, _blockNumber), blockNumber, `${dexKey}_${poolAddress}`, dexHelper.provider, @@ -126,4 +128,52 @@ describe('FluidDex EventPool Mainnet', function () { }, ); }); + + describe('Pool events', () => { + let dexPool: FluidDexEventPool; + + const eventsToTest: Record = { + '0x8710039D5de6840EdE452A85672B32270a709aE2': { + LogPauseSwapAndArbitrage: [21337128], + }, + '0x2886a01a0645390872a9eb99dae1283664b0c524': { + LogPauseSwapAndArbitrage: [21374547], + }, + }; + + Object.entries(eventsToTest).forEach( + ([poolAddress, events]: [string, EventMappings]) => { + describe(`Events for ${poolAddress}`, () => { + beforeEach(() => { + dexPool = new FluidDexEventPool( + dexKey, + poolAddress, + network, + dexHelper, + logger, + ); + }); + Object.entries(events).forEach( + ([eventName, blockNumbers]: [string, number[]]) => { + describe(`${eventName}`, () => { + blockNumbers.forEach((blockNumber: number) => { + it(`State after ${blockNumber}`, async function () { + await testEventSubscriber( + dexPool, + dexPool.addressesSubscribed, + (_blockNumber: number) => + fetchState(dexPool, _blockNumber), + blockNumber, + `${dexKey}_${poolAddress}`, + dexHelper.provider, + ); + }); + }); + }); + }, + ); + }); + }, + ); + }); }); diff --git a/src/dex/fluid-dex/fluid-dex-liquidity-proxy.ts b/src/dex/fluid-dex/fluid-dex-liquidity-proxy.ts index ae61daa84..f6f7b890b 100644 --- a/src/dex/fluid-dex/fluid-dex-liquidity-proxy.ts +++ b/src/dex/fluid-dex/fluid-dex-liquidity-proxy.ts @@ -28,7 +28,7 @@ export class FluidDexLiquidityProxy extends StatefulEventSubscriber { + handlers: { + [event: string]: ( + event: any, + state: DeepReadonly, + log: Readonly, + ) => DeepReadonly | null; + } = {}; + + logDecoder: (log: Log) => any; + addressesSubscribed: Address[]; + protected poolIface = new Interface(FluidDexPoolABI); + + constructor( + readonly parentName: string, + readonly poolAddress: string, + protected network: number, + protected dexHelper: IDexHelper, + logger: Logger, + ) { + super(parentName, 'pool', dexHelper, logger); + + this.logDecoder = (log: Log) => this.poolIface.parseLog(log); + this.addressesSubscribed = [poolAddress]; + + // Add handlers + this.handlers['LogPauseSwapAndArbitrage'] = + this.handleLogPauseSwapAndArbitrage.bind(this); + this.handlers['LogUnpauseSwapAndArbitrage'] = + this.handleLogUnpauseSwapAndArbitrage.bind(this); + } + + /** + * The function is called every time any of the subscribed + * addresses release log. The function accepts the current + * state, updates the state according to the log, and returns + * the updated state. + * @param state - Current state of event subscriber + * @param log - Log released by one of the subscribed addresses + * @returns Updates state of the event subscriber after the log + */ + async processLog( + state: DeepReadonly, + log: Readonly, + ): Promise | null> { + try { + let event; + try { + event = this.logDecoder(log); + } catch (e) { + return null; + } + + if (event.name in this.handlers) { + return this.handlers[event.name](event, state, log); + } + } catch (e) { + catchParseLogError(e, this.logger); + } + + return null; + } + + handleLogPauseSwapAndArbitrage(): PoolState { + return { isSwapAndArbitragePaused: true }; + } + + handleLogUnpauseSwapAndArbitrage(): PoolState { + return { isSwapAndArbitragePaused: false }; + } + + async getStateOrGenerate( + blockNumber: number, + readonly: boolean = false, + ): Promise> { + let state = this.getState(blockNumber); + if (!state) { + state = await this.generateState(blockNumber); + if (!readonly) this.setState(state, blockNumber); + } + return state; + } + + /** + * The function generates state using on-chain calls. This + * function is called to regenerate state if the event based + * system fails to fetch events and the local state is no + * more correct. + * @param blockNumber - Blocknumber for which the state should + * should be generated + * @returns state of the event subscriber at blocknumber + */ + async generateState(blockNumber: number): Promise> { + const multicallData = [ + { + target: this.addressesSubscribed[0], + callData: this.poolIface.encodeFunctionData('readFromStorage', [ + hexZeroPad(hexlify(1), 32), + ]), + decodeFunction: uint256ToBigInt, + }, + ]; + + const storageResults = await this.dexHelper.multiWrapper.tryAggregate< + bigint | DecodedStateMultiCallResultWithRelativeBitmaps + >( + false, + multicallData, + blockNumber, + this.dexHelper.multiWrapper.defaultBatchSize, + false, + ); + + const isSwapAndArbitragePaused = + BigInt(storageResults[0].returnData.toString()) >> 255n === 1n; + + return { isSwapAndArbitragePaused }; + } +} diff --git a/src/dex/fluid-dex/fluid-dex.ts b/src/dex/fluid-dex/fluid-dex.ts index e8ea87ad6..fa3284ea4 100644 --- a/src/dex/fluid-dex/fluid-dex.ts +++ b/src/dex/fluid-dex/fluid-dex.ts @@ -34,6 +34,8 @@ import { generalDecoder } from '../../lib/decoders'; import { BigNumber } from 'ethers'; import { sqrt } from './utils'; import { FluidDexLiquidityProxy } from './fluid-dex-liquidity-proxy'; +import { FluidDexEventPool } from './fluid-dex-pool'; +import { MIN_SWAP_LIQUIDITY } from './constants'; export class FluidDex extends SimpleExchange implements IDex { readonly hasConstantPriceLargeAmounts = false; @@ -46,14 +48,14 @@ export class FluidDex extends SimpleExchange implements IDex { pools: FluidDexPool[] = []; + eventPools: FluidDexEventPool[] = []; + readonly factory: FluidDexFactory; readonly liquidityProxy: FluidDexLiquidityProxy; readonly fluidDexPoolIface: Interface; - FEE_100_PERCENT = BigInt(1000000); - constructor( readonly network: Network, readonly dexKey: string, @@ -110,6 +112,20 @@ export class FluidDex extends SimpleExchange implements IDex { await this.factory.initialize(blockNumber); this.pools = await this.fetchFluidDexPools(blockNumber); + this.eventPools = await Promise.all( + this.pools.map(async pool => { + const eventPool = new FluidDexEventPool( + this.dexKey, + pool.address, + this.network, + this.dexHelper, + this.logger, + ); + await eventPool.initialize(blockNumber); + return eventPool; + }), + ); + await this.liquidityProxy.initialize(blockNumber); } @@ -147,29 +163,24 @@ export class FluidDex extends SimpleExchange implements IDex { side: SwapSide, blockNumber: number, ): Promise { - const pool = this.getPoolByTokenPair(srcToken.address, destToken.address); - return pool ? [pool.id] : []; + const pools = this.getPoolsByTokenPair(srcToken.address, destToken.address); + return pools.map(pool => pool.id); } - getPoolByTokenPair( - srcToken: Address, - destToken: Address, - ): FluidDexPool | null { + getPoolsByTokenPair(srcToken: Address, destToken: Address): FluidDexPool[] { const srcAddress = srcToken.toLowerCase(); const destAddress = destToken.toLowerCase(); // A pair must have 2 different tokens. - if (srcAddress === destAddress) return null; + if (srcAddress === destAddress) return []; - for (const pool of this.pools) { - if ( + const pools = this.pools.filter( + pool => (srcAddress === pool.token0 && destAddress === pool.token1) || - (srcAddress === pool.token1 && destAddress === pool.token0) - ) { - return pool; - } - } - return null; + (srcAddress === pool.token1 && destAddress === pool.token0), + ); + + return pools; } // Returns pool prices for amounts. @@ -187,61 +198,93 @@ export class FluidDex extends SimpleExchange implements IDex { try { if (srcToken.address.toLowerCase() === destToken.address.toLowerCase()) return null; - // Get the pool to use. - const pool = this.getPoolByTokenPair(srcToken.address, destToken.address); - if (!pool) return null; - // Make sure the pool meets the optional limitPools filter. - if (limitPools && !limitPools.includes(pool.id)) return null; + + // Get the pools to use. + let pools = this.getPoolsByTokenPair(srcToken.address, destToken.address); + + if (limitPools) { + pools = pools.filter(pool => limitPools.includes(pool.id)); + } + + if (!pools.length) return null; const liquidityProxyState = await this.liquidityProxy.getStateOrGenerate( blockNumber, ); - const currentPoolReserves = liquidityProxyState.poolsReserves.find( - poolReserve => - poolReserve.pool.toLowerCase() === pool.address.toLowerCase(), - ); - if (!currentPoolReserves) { - return null; - } - const prices = amounts.map(amount => { - if (side === SwapSide.SELL) { - return this.swapIn( - srcToken.address.toLowerCase() === pool.token0.toLowerCase(), - amount, - currentPoolReserves.collateralReserves, - currentPoolReserves.debtReserves, - srcToken.decimals, - destToken.decimals, - BigInt(currentPoolReserves.fee), - currentPoolReserves.dexLimits, - Math.floor(Date.now() / 1000), + const poolsPrices = await Promise.all( + pools.map(async pool => { + const currentPoolReserves = liquidityProxyState.poolsReserves.find( + poolReserve => + poolReserve.pool.toLowerCase() === pool.address.toLowerCase(), ); - } else { - return this.swapOut( - srcToken.address.toLowerCase() === pool.token0.toLowerCase(), - amount, - currentPoolReserves.collateralReserves, - currentPoolReserves.debtReserves, - srcToken.decimals, - destToken.decimals, - BigInt(currentPoolReserves.fee), - currentPoolReserves.dexLimits, - Math.floor(Date.now() / 1000), + + const eventPool = this.eventPools.find( + eventPool => + eventPool.poolAddress.toLowerCase() === + pool.address.toLowerCase(), ); - } - }); - return [ - { - prices: prices, - unit: getBigIntPow(destToken.decimals), - data: {}, - exchange: this.dexKey, - poolIdentifier: pool.id, - gasCost: FLUID_DEX_GAS_COST, - poolAddresses: [pool.address], - }, - ]; + + if (!eventPool) { + this.logger.warn( + `${this.dexKey}-${this.network}: Event pool ${pool.address} was not found...`, + ); + return null; + } + + const state = await eventPool.getStateOrGenerate(blockNumber); + + if (!currentPoolReserves || state.isSwapAndArbitragePaused === true) { + return null; + } + + const prices = amounts.map(amount => { + if (side === SwapSide.SELL) { + return this.swapIn( + srcToken.address.toLowerCase() === pool.token0.toLowerCase(), + amount, + currentPoolReserves.collateralReserves, + currentPoolReserves.debtReserves, + srcToken.decimals, + destToken.decimals, + BigInt(currentPoolReserves.fee), + currentPoolReserves.dexLimits, + Math.floor(Date.now() / 1000), + ); + } else { + return this.swapOut( + srcToken.address.toLowerCase() === pool.token0.toLowerCase(), + amount, + currentPoolReserves.collateralReserves, + currentPoolReserves.debtReserves, + srcToken.decimals, + destToken.decimals, + BigInt(currentPoolReserves.fee), + currentPoolReserves.dexLimits, + Math.floor(Date.now() / 1000), + ); + } + }); + + return { + prices: prices, + unit: getBigIntPow(destToken.decimals), + data: { + poolId: pool.id, + }, + exchange: this.dexKey, + poolIdentifier: pool.id, + gasCost: FLUID_DEX_GAS_COST, + poolAddresses: [pool.address], + }; + }), + ); + + const notNullResults = poolsPrices.filter( + res => res !== null, + ) as ExchangePrices; + + return notNullResults; } catch (e) { this.logger.error( `Error_getPricesVolume ${srcToken.address || srcToken.symbol}, ${ @@ -272,10 +315,9 @@ export class FluidDex extends SimpleExchange implements IDex { ): AdapterExchangeParam { // Encode here the payload for adapter const payload = ''; - const pool = this.getPoolByTokenPair(srcToken, destToken); return { - targetExchange: pool!.address, + targetExchange: '0x', payload, networkFee: '0', }; @@ -313,7 +355,11 @@ export class FluidDex extends SimpleExchange implements IDex { side === SwapSide.SELL ? 'amountOut_' : 'amountIn_', ); - const pool = this.getPoolByTokenPair(srcToken, destToken); + const pool = this.pools.find(pool => pool.id === data.poolId); + if (!pool) + throw new Error( + `${this.dexKey}-${this.network}: Pool with id: ${data.poolId} was not found`, + ); if (side === SwapSide.SELL) { if (pool!.token0.toLowerCase() !== srcToken.toLowerCase()) { @@ -584,11 +630,84 @@ export class FluidDex extends SimpleExchange implements IDex { if (priceDiff > maxAllowedDiff) { return 0n; } + + if (amountInCollateral > 0) { + let reservesRatioValid = swap0To1 + ? this.verifyToken1Reserves( + colReserveIn + amountInCollateral, + colReserveOut - amountOutCollateral, + oldPrice, + ) + : this.verifyToken0Reserves( + colReserveOut - amountOutCollateral, + colReserveIn + amountInCollateral, + oldPrice, + ); + if (!reservesRatioValid) { + return 0n; + } + } + + if (amountInDebt > 0) { + let reservesRatioValid = swap0To1 + ? this.verifyToken1Reserves( + debtReserveIn + amountInDebt, + debtReserveOut - amountOutDebt, + oldPrice, + ) + : this.verifyToken0Reserves( + debtReserveOut - amountOutDebt, + debtReserveIn + amountInDebt, + oldPrice, + ); + if (!reservesRatioValid) { + return 0n; + } + } + const totalAmountOut = amountOutCollateral + amountOutDebt; return totalAmountOut; } + /** + * Checks if token0 reserves are sufficient compared to token1 reserves. + * This helps prevent edge cases and ensures high precision in calculations. + * @param {number} token0Reserves - The reserves of token0. + * @param {number} token1Reserves - The reserves of token1. + * @param {number} price - The current price used for calculation. + * @returns {boolean} - Returns false if token0 reserves are too low, true otherwise. + */ + protected verifyToken0Reserves( + token0Reserves: bigint, + token1Reserves: bigint, + price: bigint, + ): boolean { + return ( + token0Reserves >= + (token1Reserves * 10n ** 27n) / (price * MIN_SWAP_LIQUIDITY) + ); + } + + /** + * Checks if token1 reserves are sufficient compared to token0 reserves. + * This helps prevent edge cases and ensures high precision in calculations. + * @param {number} token0Reserves - The reserves of token0. + * @param {number} token1Reserves - The reserves of token1. + * @param {number} price - The current price used for calculation. + * @returns {boolean} - Returns false if token1 reserves are too low, true otherwise. + */ + protected verifyToken1Reserves( + token0Reserves: bigint, + token1Reserves: bigint, + price: bigint, + ): boolean { + return ( + token1Reserves >= + (token0Reserves * price) / (10n ** 27n * MIN_SWAP_LIQUIDITY) + ); + } + /** * Calculates the currently available swappable amount for a token limit considering expansion since last syncTime. * @param syncTime - timestamp in seconds when the limits were synced @@ -765,7 +884,7 @@ export class FluidDex extends SimpleExchange implements IDex { syncTime, ); - if (amountIn == 2n ** 256n - 1n) { + if (amountIn === 2n ** 256n - 1n) { return amountIn; } const ans = (amountIn * BigInt(10 ** inDecimals)) / BigInt(10 ** 12); @@ -985,6 +1104,39 @@ export class FluidDex extends SimpleExchange implements IDex { return 2n ** 256n - 1n; } + if (amountInCollateral > 0) { + let reservesRatioValid = swap0to1 + ? this.verifyToken1Reserves( + colReserveIn + amountInCollateral, + colReserveOut - amountOutCollateral, + oldPrice, + ) + : this.verifyToken0Reserves( + colReserveOut - amountOutCollateral, + colReserveIn + amountInCollateral, + oldPrice, + ); + if (!reservesRatioValid) { + return 0n; + } + } + if (amountInDebt > 0) { + let reservesRatioValid = swap0to1 + ? this.verifyToken1Reserves( + debtReserveIn + amountInDebt, + debtReserveOut - amountOutDebt, + oldPrice, + ) + : this.verifyToken0Reserves( + debtReserveOut - amountOutDebt, + debtReserveIn + amountInDebt, + oldPrice, + ); + if (!reservesRatioValid) { + return 0n; + } + } + const totalAmountIn = amountInCollateral + amountInDebt; return totalAmountIn; diff --git a/src/dex/fluid-dex/types.ts b/src/dex/fluid-dex/types.ts index cffe433ce..0fb982e07 100644 --- a/src/dex/fluid-dex/types.ts +++ b/src/dex/fluid-dex/types.ts @@ -72,7 +72,9 @@ export interface PoolWithReserves { debtReserves: DebtReserves; } -export type FluidDexData = {}; +export type FluidDexData = { + poolId: string; +}; // Each pool has a contract address and token pairs. export type FluidDexPool = { diff --git a/tests/constants-e2e.ts b/tests/constants-e2e.ts index de23167f1..01be43ba8 100644 --- a/tests/constants-e2e.ts +++ b/tests/constants-e2e.ts @@ -131,6 +131,10 @@ export const Tokens: { addBalance: balancesFn, addAllowance: allowedFn, }, + INST: { + address: '0x6f40d4A6237C257fff2dB00FA0510DeEECd303eb', + decimals: 18, + }, aEthUSDC: { address: '0x98c23e9d8f34fefb1b7bd6a91b7ff122f4e16f5c', decimals: 6, @@ -1691,6 +1695,7 @@ export const Holders: { SKY: '0x0ddda327A6614130CCb20bc0097313A282176A01', MKR: '0xe9aAA7A9DDc0877626C1779AbC29993aD89A6c1f', ETHx: '0xFCC1A2c71F01B7f58Ed538a6B4AAa5A0724eB5A6', + INST: '0xe2Dd506477D4792A7E811D2E93D44CeBa82c668B', // Idle tokens AA_wstETH: '0xd7C1b48877A7dFA7D51cf1144c89C0A3F134F935', 'AA_idle_cpPOR-USDC': '0x085c8eaccA6911fE60aE3f8FbAe5F3012E3A05Ec',