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..974e8a506 100644
--- a/src/dex/fluid-dex/fluid-dex-events.test.ts
+++ b/src/dex/fluid-dex/fluid-dex-events.test.ts
@@ -10,6 +10,7 @@ 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';
jest.setTimeout(50 * 1000);
@@ -126,4 +127,15 @@ describe('FluidDex EventPool Mainnet', function () {
},
);
});
+
+ describe('Pool events', () => {
+ let dexPool: FluidDexEventPool;
+
+ const eventsToTest: Record
= {
+ '0x8710039D5de6840EdE452A85672B32270a709aE2': {
+ LogPauseSwapAndArbitrage: [21337128],
+ LogUnpauseSwapAndArbitrage: [],
+ },
+ };
+ });
});
diff --git a/src/dex/fluid-dex/fluid-dex-liquidity-proxy.ts b/src/dex/fluid-dex/fluid-dex-liquidity-proxy.ts
index ae61daa84..036eb43d1 100644
--- a/src/dex/fluid-dex/fluid-dex-liquidity-proxy.ts
+++ b/src/dex/fluid-dex/fluid-dex-liquidity-proxy.ts
@@ -5,6 +5,7 @@ import { bigIntify, catchParseLogError } from '../../utils';
import { StatefulEventSubscriber } from '../../stateful-event-subscriber';
import { IDexHelper } from '../../dex-helper/idex-helper';
import ResolverABI from '../../abi/fluid-dex/resolver.abi.json';
+import FluidDexPoolABI from '../../abi/fluid-dex/fluid-dex.abi.json';
import LiquidityABI from '../../abi/fluid-dex/liquidityUserModule.abi.json';
import {
CommonAddresses,
@@ -13,7 +14,13 @@ import {
PoolReserveResponse,
} from './types';
import { Address } from '../../types';
-import { Contract } from 'ethers';
+import { Contract, ethers } from 'ethers';
+import { uint256ToBigInt } from '../../lib/decoders';
+import { DecodedStateMultiCallResultWithRelativeBitmaps } from '../uniswap-v3/types';
+
+const {
+ utils: { hexlify, hexZeroPad },
+} = ethers;
export class FluidDexLiquidityProxy extends StatefulEventSubscriber {
handlers: {
@@ -28,7 +35,9 @@ export class FluidDexLiquidityProxy extends StatefulEventSubscriber ({
+ // target: pool.pool,
+ // 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 poolsReserves = convertedResult.poolsReserves.map(
+ // (poolReserve, index) => {
+ // const isSwapAndArbitragePaused =
+ // BigInt(storageResults[index].returnData.toString()) >> BigInt(255) ===
+ // BigInt(1);
+ // return { ...poolReserve, isSwapAndArbitragePaused };
+ // },
+ // );
+ //
this.logger.info(`${this.parentName}: ${this.name}: generating state...`);
return convertedResult;
diff --git a/src/dex/fluid-dex/fluid-dex-pool.ts b/src/dex/fluid-dex/fluid-dex-pool.ts
new file mode 100644
index 000000000..2fbfa64c7
--- /dev/null
+++ b/src/dex/fluid-dex/fluid-dex-pool.ts
@@ -0,0 +1,138 @@
+import { StatefulEventSubscriber } from '../../stateful-event-subscriber';
+import { DeepReadonly } from 'ts-essentials';
+import { Address, Log, Logger } from '../../types';
+import { IDexHelper } from '../../dex-helper';
+import { Interface } from '@ethersproject/abi';
+import FluidDexPoolABI from '../../abi/fluid-dex/fluid-dex.abi.json';
+import { catchParseLogError } from '../../utils';
+import { ethers } from 'ethers';
+import { uint256ToBigInt } from '../../lib/decoders';
+import { DecodedStateMultiCallResultWithRelativeBitmaps } from '../uniswap-v3/types';
+
+const {
+ utils: { hexlify, hexZeroPad },
+} = ethers;
+
+type PoolState = {
+ isSwapAndArbitragePaused: boolean;
+};
+
+export class FluidDexEventPool 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() {
+ return { isSwapAndArbitragePaused: true };
+ }
+
+ handleLogUnpauseSwapAndArbitrage() {
+ 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()) >> BigInt(255) ===
+ BigInt(1);
+
+ return { isSwapAndArbitragePaused };
+ }
+}
diff --git a/src/dex/fluid-dex/fluid-dex.ts b/src/dex/fluid-dex/fluid-dex.ts
index e8ea87ad6..a15894031 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,99 @@ 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;
+
+ // Get the pools to use.
+ const pools = this.getPoolsByTokenPair(
+ srcToken.address,
+ destToken.address,
+ );
+
+ if (!pools.length) return null;
+ const poolIds = pools.map(pool => pool.id);
// Make sure the pool meets the optional limitPools filter.
- if (limitPools && !limitPools.includes(pool.id)) return null;
+ if (
+ limitPools &&
+ !limitPools.every(limitPool => poolIds.includes(limitPool))
+ )
+ 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 +321,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 +361,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 +636,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 +890,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 +1110,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 a924a7192..bd2c3934e 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,
@@ -1681,6 +1685,7 @@ export const Holders: {
SKY: '0x0ddda327A6614130CCb20bc0097313A282176A01',
MKR: '0xe9aAA7A9DDc0877626C1779AbC29993aD89A6c1f',
ETHx: '0xFCC1A2c71F01B7f58Ed538a6B4AAa5A0724eB5A6',
+ INST: '0xe2Dd506477D4792A7E811D2E93D44CeBa82c668B',
// Idle tokens
AA_wstETH: '0xd7C1b48877A7dFA7D51cf1144c89C0A3F134F935',
'AA_idle_cpPOR-USDC': '0x085c8eaccA6911fE60aE3f8FbAe5F3012E3A05Ec',