diff --git a/scripts/dex-integration.ts b/scripts/dex-integration.ts index e3487e690..dfe2a31da 100644 --- a/scripts/dex-integration.ts +++ b/scripts/dex-integration.ts @@ -116,7 +116,7 @@ function testIntegration(argv: IOptions) { process.env.NODE_ENV = 'test'; } - require('../node_modules/jest-cli/build/cli').run( + require('../node_modules/jest-cli/build/run').run( `src\/dex\/${dexNameParam}\/.+\.test\.ts`, ); } diff --git a/src/dex/index.ts b/src/dex/index.ts index f32c7d800..3092d5c4c 100644 --- a/src/dex/index.ts +++ b/src/dex/index.ts @@ -70,6 +70,7 @@ import { SpiritSwapV3 } from './quickswap/spiritswap-v3'; import { TraderJoeV21 } from './trader-joe-v2.1'; import { PancakeswapV3 } from './pancakeswap-v3/pancakeswap-v3'; import { Algebra } from './algebra/algebra'; +import { Morphex } from './morphex/morphex'; const LegacyDexes = [ CurveV2, @@ -137,6 +138,7 @@ const Dexes = [ MaverickV1, Camelot, SwaapV2, + Morphex, ]; export type LegacyDexConstructor = new (dexHelper: IDexHelper) => IDexTxBuilder< diff --git a/src/dex/morphex/config.ts b/src/dex/morphex/config.ts new file mode 100644 index 000000000..7d576f158 --- /dev/null +++ b/src/dex/morphex/config.ts @@ -0,0 +1,31 @@ +import { DexParams } from '../gmx/types'; +import { DexConfigMap } from '../../types'; +import { Network, SwapSide } from '../../constants'; + +export const MorphexConfig: DexConfigMap = { + Morphex: { + [Network.FANTOM]: { + vault: '0x245cD6d33578de9aF75a3C0c636c726b1A8cbdAa', + reader: '0xcA47b9b612a152ece991F31d8D3547D73BaF2Ecc', + priceFeed: '0x7a451DE877CbB6551AACa671d0458B6f9dF1e29A', + fastPriceFeed: '0x7f54C35A38D89fcf5Fe516206E6628745ed38CC7', + fastPriceEvents: '0xDc7C389be5da32e326A261dC0126feCa7AE04d79', + usdg: '0xe135c7BFfda932b5B862Da442cF4CbC4d43DC3Ad', + }, + }, +}; + +export const Adapters: { + [chainId: number]: { + [side: string]: { name: string; index: number }[] | null; + }; +} = { + [Network.FANTOM]: { + [SwapSide.SELL]: [ + { + name: 'FantomAdapter01', + index: 6, // TODO: it's for aavev3, but there is no Morphex adapter + }, + ], + }, +}; diff --git a/src/dex/morphex/morphex-e2e.test.ts b/src/dex/morphex/morphex-e2e.test.ts new file mode 100644 index 000000000..47d04c20a --- /dev/null +++ b/src/dex/morphex/morphex-e2e.test.ts @@ -0,0 +1,91 @@ +import dotenv from 'dotenv'; +dotenv.config(); + +import { testE2E } from '../../../tests/utils-e2e'; +import { + Tokens, + Holders, + NativeTokenSymbols, +} from '../../../tests/constants-e2e'; +import { Network, ContractMethod, SwapSide } from '../../constants'; +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { generateConfig } from '../../config'; + +describe('Morphex E2E', () => { + const dexKey = 'Morphex'; + + describe('Morphex Fantom', () => { + const network = Network.FANTOM; + const tokens = Tokens[network]; + const holders = Holders[network]; + const provider = new StaticJsonRpcProvider( + generateConfig(network).privateHttpProvider, + network, + ); + + const tokenASymbol: string = 'axlUSDC'; + const tokenBSymbol: string = 'lzUSDC'; + const nativeTokenSymbol = NativeTokenSymbols[network]; + + const tokenAAmount: string = '500000000'; // 500 Axelar USDC + const tokenBAmount: string = '500000000'; // 500 Layer Zero USDC + const nativeTokenAmount = '100000000000000000000'; // 100 FTM + + const sideToContractMethods = new Map([ + [ + SwapSide.SELL, + [ + ContractMethod.simpleSwap, + // ContractMethod.multiSwap, + // ContractMethod.megaSwap, + ], + ], + ]); + + sideToContractMethods.forEach((contractMethods, side) => + contractMethods.forEach((contractMethod: ContractMethod) => { + describe(`${contractMethod}`, () => { + it(nativeTokenSymbol + ' -> TOKEN', async () => { + await testE2E( + tokens[nativeTokenSymbol], + tokens[tokenASymbol], + holders[nativeTokenSymbol], + side === SwapSide.SELL ? nativeTokenAmount : tokenAAmount, + side, + dexKey, + contractMethod, + network, + provider, + ); + }); + it('TOKEN -> ' + nativeTokenSymbol, async () => { + await testE2E( + tokens[tokenASymbol], + tokens[nativeTokenSymbol], + holders[tokenASymbol], + side === SwapSide.SELL ? tokenAAmount : nativeTokenAmount, + side, + dexKey, + contractMethod, + network, + provider, + ); + }); + it('TOKEN -> TOKEN', async () => { + await testE2E( + tokens[tokenASymbol], + tokens[tokenBSymbol], + holders[tokenASymbol], + side === SwapSide.SELL ? tokenAAmount : tokenBAmount, + side, + dexKey, + contractMethod, + network, + provider, + ); + }); + }); + }), + ); + }); +}); diff --git a/src/dex/morphex/morphex-events.test.ts b/src/dex/morphex/morphex-events.test.ts new file mode 100644 index 000000000..e488738af --- /dev/null +++ b/src/dex/morphex/morphex-events.test.ts @@ -0,0 +1,92 @@ +import dotenv from 'dotenv'; +dotenv.config(); + +import { GMXEventPool } from '../gmx/pool'; +import { MorphexConfig } from './config'; +import { Network } from '../../constants'; +import { DummyDexHelper } from '../../dex-helper/index'; +import { testEventSubscriber } from '../../../tests/utils-events'; +import { PoolState } from '../gmx/types'; + +jest.setTimeout(50 * 1000); +const dexKey = 'Morphex'; +const network = Network.FANTOM; +const params = MorphexConfig[dexKey][network]; + +async function fetchPoolState( + gmxPool: GMXEventPool, + blockNumber: number, +): Promise { + return gmxPool.generateState(blockNumber); +} + +// timestamp can't be compared exactly as the event released +// doesn't have the timestamp. It is safe to consider the +// timestamp as the blockTime as the max deviation is bounded +// on the contract +const stateWithoutTimestamp = (state: PoolState) => ({ + ...state, + secondaryPrices: { + prices: state.secondaryPrices.prices, + // timestamp (this is removed) + }, +}); + +function compareState(state: PoolState, expectedState: PoolState) { + expect(stateWithoutTimestamp(state)).toEqual( + stateWithoutTimestamp(expectedState), + ); +} + +describe('Morphex Event', function () { + const blockNumbers: { [eventName: string]: number[] } = { + IncreaseUsdgAmount: [ + 67247602, 67247565, 67247561, 67247508, 67247393, 67247305, 67247303, + 67247302, 67247230, 67247220, 67247218, 67247216, 67247215, 67247145, + 67247059, 67247026, 67246788, 67246731, + ], + DecreaseUsdgAmount: [ + 67247778, 67247602, 67247565, 67247561, 67247508, 67247393, 67247305, + 67247303, 67247302, 67247230, 67247220, 67247218, 67247216, 67247215, + 67247145, 67247059, 67247026, 67246788, + ], + Transfer: [ + 67087282, 67087063, 67087039, 67068002, 67052880, 67052806, 67052801, + ], + PriceUpdate: [67248035, 67247977, 67247907, 67247897, 67247893], + }; + + describe('MorphexEventPool', function () { + Object.keys(blockNumbers).forEach((event: string) => { + blockNumbers[event].forEach((blockNumber: number) => { + it(`Should return the correct state after the ${blockNumber}:${event}`, async function () { + const dexHelper = new DummyDexHelper(network); + const logger = dexHelper.getLogger(dexKey); + + const config = await GMXEventPool.getConfig( + params, + blockNumber, + dexHelper.multiContract, + ); + const gmxPool = new GMXEventPool( + dexKey, + network, + dexHelper, + logger, + config, + ); + + await testEventSubscriber( + gmxPool, + gmxPool.addressesSubscribed, + (_blockNumber: number) => fetchPoolState(gmxPool, _blockNumber), + blockNumber, + `${dexKey}_${params.vault}`, + dexHelper.provider, + compareState, + ); + }); + }); + }); + }); +}); diff --git a/src/dex/morphex/morphex-integration.test.ts b/src/dex/morphex/morphex-integration.test.ts new file mode 100644 index 000000000..6300b74f6 --- /dev/null +++ b/src/dex/morphex/morphex-integration.test.ts @@ -0,0 +1,113 @@ +import dotenv from 'dotenv'; +dotenv.config(); + +import { Interface } from '@ethersproject/abi'; +import { DummyDexHelper } from '../../dex-helper/index'; +import { Network, SwapSide } from '../../constants'; +import { Morphex } from './morphex'; +import { MorphexConfig } from './config'; +import { + checkPoolPrices, + checkPoolsLiquidity, + checkConstantPoolPrices, +} from '../../../tests/utils'; +import { Tokens } from '../../../tests/constants-e2e'; +import ReaderABI from '../../abi/gmx/reader.json'; + +const network = Network.FANTOM; +const TokenASymbol = 'axlUSDC'; +const TokenA = Tokens[network][TokenASymbol]; + +const TokenBSymbol = 'WFTM'; +const TokenB = Tokens[network][TokenBSymbol]; + +const amounts = [ + 0n, + 1000000000n, + 2000000000n, + 3000000000n, + 4000000000n, + 5000000000n, +]; + +const dexKey = 'Morphex'; +const params = MorphexConfig[dexKey][network]; +const readerInterface = new Interface(ReaderABI); +const readerAddress = params.reader; + +describe('Morphex', function () { + it('getPoolIdentifiers and getPricesVolume SELL', async function () { + const dexHelper = new DummyDexHelper(network); + const blocknumber = await dexHelper.web3Provider.eth.getBlockNumber(); + const gmx = new Morphex(network, dexKey, dexHelper); + + await gmx.initializePricing(blocknumber); + + const pools = await gmx.getPoolIdentifiers( + TokenA, + TokenB, + SwapSide.SELL, + blocknumber, + ); + console.log(`${TokenASymbol} <> ${TokenBSymbol} Pool Identifiers: `, pools); + + expect(pools.length).toBeGreaterThan(0); + + const poolPrices = await gmx.getPricesVolume( + TokenA, + TokenB, + amounts, + SwapSide.SELL, + blocknumber, + pools, + ); + console.log(`${TokenASymbol} <> ${TokenBSymbol} Pool Prices: `, poolPrices); + + expect(poolPrices).not.toBeNull(); + if (gmx.hasConstantPriceLargeAmounts) { + checkConstantPoolPrices(poolPrices!, amounts, dexKey); + } else { + checkPoolPrices(poolPrices!, amounts, SwapSide.SELL, dexKey); + } + + // Do on chain pricing based on reader to compare + const readerCallData = amounts.map(a => ({ + target: readerAddress, + callData: readerInterface.encodeFunctionData('getAmountOut', [ + params.vault, + TokenA.address, + TokenB.address, + a.toString(), + ]), + })); + + const readerResult = ( + await dexHelper.multiContract.methods + .aggregate(readerCallData) + .call({}, blocknumber) + ).returnData; + const expectedPrices = readerResult.map((p: any) => + BigInt( + readerInterface.decodeFunctionResult('getAmountOut', p)[0].toString(), + ), + ); + + expect(poolPrices![0].prices).toEqual(expectedPrices); + }); + + it('getTopPoolsForToken', async function () { + const dexHelper = new DummyDexHelper(network); + const gmx = new Morphex(network, dexKey, dexHelper); + + await gmx.updatePoolState(); + const poolLiquidity = await gmx.getTopPoolsForToken(TokenA.address, 10); + console.log( + `${TokenASymbol} Top Pools:`, + JSON.stringify(poolLiquidity, null, 2), + ); + + if (!gmx.hasConstantPriceLargeAmounts) { + checkPoolsLiquidity(poolLiquidity, TokenA.address, dexKey); + } + }); +}); diff --git a/src/dex/morphex/morphex.ts b/src/dex/morphex/morphex.ts new file mode 100644 index 000000000..41596a87a --- /dev/null +++ b/src/dex/morphex/morphex.ts @@ -0,0 +1,22 @@ +import { Network } from '../../constants'; +import { IDexHelper } from '../../dex-helper'; +import { DexParams } from '../gmx/types'; +import { Adapters, MorphexConfig } from './config'; +import { GMX } from '../gmx/gmx'; +import { getDexKeysWithNetwork } from '../../utils'; + +export class Morphex extends GMX { + public static dexKeysWithNetwork: { key: string; networks: Network[] }[] = + getDexKeysWithNetwork(MorphexConfig); + + constructor( + protected network: Network, + dexKey: string, + protected dexHelper: IDexHelper, + protected adapters = Adapters[network], + protected params: DexParams = MorphexConfig[dexKey][network], + ) { + super(network, dexKey, dexHelper, adapters, params); + this.logger = dexHelper.getLogger(dexKey); + } +} diff --git a/tests/constants-e2e.ts b/tests/constants-e2e.ts index cf0284848..a161a1f61 100644 --- a/tests/constants-e2e.ts +++ b/tests/constants-e2e.ts @@ -496,6 +496,14 @@ export const Tokens: { address: '0xe578C856933D8e1082740bf7661e379Aa2A30b26', decimals: 6, }, + axlUSDC: { + address: '0x1B6382DBDEa11d97f24495C9A90b7c88469134a4', + decimals: 6, + }, + lzUSDC: { + address: '0x28a92dde19D9989F39A49905d7C9C2FAc7799bDf', + decimals: 6, + }, }, [Network.BSC]: { POPS: { @@ -964,6 +972,8 @@ export const Holders: { ETH: '0xf48883940b4056801de30f12b934dcea90133ee6', GUSDC: '0x894d774a293f8aa3d23d67815d4cadb5319c1094', GDAI: '0x0e2ed73f9c1409e2b36fe6c46e60d4557b7c2ac0', + axlUSDC: '0xccf932cd565c21d2e516c8ff3a4f244eea27e09a', + lzUSDC: ' 0xd30442beee8269bfb3829c401c62b38d2ea5bdb4', }, [Network.BSC]: { DAI: '0xf68a4b64162906eff0ff6ae34e2bb1cd42fef62d',