From 2cf910f33afe9f6763bcd68026215fefef7f6d1b Mon Sep 17 00:00:00 2001 From: Bruno Eidam Guerios Date: Wed, 15 Jan 2025 17:17:33 -0300 Subject: [PATCH 1/4] SOR - Assert behavior for tokens with 0 decimals --- .changeset/slow-eggs-compare.md | 5 + modules/sor/balancer-sor.integration.test.ts | 4 +- .../balancer-v2-sor.integration.test.ts | 117 ++++++++++++++++++ test/anvil/anvil-global-setup.ts | 12 +- test/factories/prismaToken.factory.ts | 3 +- test/vitest-setup.ts | 5 + vitest.config.ts | 1 + 7 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 .changeset/slow-eggs-compare.md create mode 100644 modules/sor/sorV2/lib/poolsV2/balancer-v2-sor.integration.test.ts create mode 100644 test/vitest-setup.ts diff --git a/.changeset/slow-eggs-compare.md b/.changeset/slow-eggs-compare.md new file mode 100644 index 000000000..4b901235c --- /dev/null +++ b/.changeset/slow-eggs-compare.md @@ -0,0 +1,5 @@ +--- +'backend': patch +--- + +SOR - Assert behavior for tokens with 0 decimals diff --git a/modules/sor/balancer-sor.integration.test.ts b/modules/sor/balancer-sor.integration.test.ts index 0178a8cf3..63788f2df 100644 --- a/modules/sor/balancer-sor.integration.test.ts +++ b/modules/sor/balancer-sor.integration.test.ts @@ -88,8 +88,8 @@ describe('Balancer SOR Integration Tests', () => { }); // get SOR paths - const tIn = new Token(parseFloat(chainToIdMap['SEPOLIA']), BAL.address as Address, 18); - const tOut = new Token(parseFloat(chainToIdMap['SEPOLIA']), WETH.address as Address, 18); + const tIn = new Token(parseFloat(chainToIdMap['SEPOLIA']), BAL.address as Address, BAL.token.decimals); + const tOut = new Token(parseFloat(chainToIdMap['SEPOLIA']), WETH.address as Address, WETH.token.decimals); const amountIn = BigInt(0.1e18); paths = (await sorGetPathsWithPools( tIn, diff --git a/modules/sor/sorV2/lib/poolsV2/balancer-v2-sor.integration.test.ts b/modules/sor/sorV2/lib/poolsV2/balancer-v2-sor.integration.test.ts new file mode 100644 index 000000000..e9fb4b349 --- /dev/null +++ b/modules/sor/sorV2/lib/poolsV2/balancer-v2-sor.integration.test.ts @@ -0,0 +1,117 @@ +// yarn vitest balancer-v2-sor.integration.test.ts + +import { ExactInQueryOutput, Swap, SwapKind, Token, Address, Path } from '@balancer/sdk'; + +import { PathWithAmount } from '../path'; +import { sorGetPathsWithPools } from '../static'; +import { getOutputAmount } from '../utils/helpers'; +import { chainToChainId as chainToIdMap } from '../../../../network/chain-id-to-chain'; + +import { ANVIL_NETWORKS, startFork, stopAnvilForks } from '../../../../../test/anvil/anvil-global-setup'; +import { prismaPoolDynamicDataFactory, prismaPoolFactory, prismaPoolTokenFactory } from '../../../../../test/factories'; +import { createTestClient, Hex, http, TestClient } from 'viem'; +import { sepolia } from 'viem/chains'; + +/** + * Test Data: + * + * In order to properly compare SOR quotes vs SDK queries, we need to setup test data from a specific blockNumber. + * Although the API does not provide that functionality, we can use subgraph to achieve it. + * These tests run against [BalancerV2 subgraph](https://thegraph.com/explorer/subgraphs/C4ayEZP2yTXRAB8vSaTrgN4m9anTe9Mdm2ViyiAuV9TV?view=Query) + * TODO: improve test data setup by creating a script that fetches all necessary data automatically for a given blockNumber. + */ + +const protocolVersion = 2; + +describe('Balancer V2 SOR Integration Tests', () => { + let rpcUrl: string; + let paths: PathWithAmount[]; + let sdkSwap: Swap; + let snapshot: Hex; + let client: TestClient; + + beforeAll(async () => { + // start fork to run queries against + ({ rpcUrl } = await startFork(ANVIL_NETWORKS.GNOSIS_CHAIN)); + client = createTestClient({ + mode: 'anvil', + chain: sepolia, + transport: http(rpcUrl), + }); + snapshot = await client.snapshot(); + }); + + beforeEach(async () => { + await client.revert({ + id: snapshot, + }); + snapshot = await client.snapshot(); + }); + + describe('Weighted Pool Path - Token with 0 decimals', () => { + beforeAll(async () => { + // setup mock pool data + const wxDAI = prismaPoolTokenFactory.build({ + address: '0xe91d153e0b41518a2ce8dd3d7944fa863463a97d', + balance: '1818.053229398448550484', + weight: '0.5', + }); + const MPS = prismaPoolTokenFactory.build({ + address: '0xfa57aa7beed63d03aaf85ffd1753f5f6242588fb', + balance: '437', + weight: '0.5', + token: { + decimals: 0, + }, + }); + const prismaWeightedPool = prismaPoolFactory.build({ + id: '0x4bcf6b48906fa0f68bea1fc255869a41241d4851000200000000000000000021', + address: '0x4bcf6b48906fa0f68bea1fc255869a41241d4851', + type: 'WEIGHTED', + protocolVersion, + tokens: [wxDAI, MPS], + dynamicData: prismaPoolDynamicDataFactory.build({ + totalShares: '1551.518323760171904919', + swapFee: '0.03', + }), + chain: 'GNOSIS', + }); + + // get SOR paths + const tIn = new Token(parseFloat(chainToIdMap['GNOSIS']), wxDAI.address as Address, wxDAI.token.decimals); + const tOut = new Token(parseFloat(chainToIdMap['GNOSIS']), MPS.address as Address, MPS.token.decimals); + const amountIn = BigInt(1e18); + paths = (await sorGetPathsWithPools( + tIn, + tOut, + SwapKind.GivenIn, + amountIn, + [prismaWeightedPool], + protocolVersion, + )) as PathWithAmount[]; + + // build SDK swap from SOR paths + sdkSwap = new Swap({ + chainId: parseFloat(chainToIdMap['GNOSIS']), + paths: paths.map((path) => ({ + protocolVersion, + inputAmountRaw: path.inputAmount.amount, + outputAmountRaw: path.outputAmount.amount, + tokens: path.tokens.map((token) => ({ + address: token.address, + decimals: token.decimals, + })), + pools: path.pools.map((pool) => pool.id), + })), + swapKind: SwapKind.GivenIn, + }); + }); + + test('SOR quote should match swap query', async () => { + const returnAmountSOR = getOutputAmount(paths); + const queryOutput = await sdkSwap.query(rpcUrl); + const returnAmountQuery = (queryOutput as ExactInQueryOutput).expectedAmountOut; + expect(returnAmountQuery.amount).toEqual(returnAmountSOR.amount); + }); + }); +}); diff --git a/test/anvil/anvil-global-setup.ts b/test/anvil/anvil-global-setup.ts index 6b6a4222a..8e9be4781 100644 --- a/test/anvil/anvil-global-setup.ts +++ b/test/anvil/anvil-global-setup.ts @@ -12,7 +12,10 @@ type NetworkSetup = { forkBlockNumber: bigint; }; -type NetworksWithFork = Extract; +type NetworksWithFork = Extract< + keyof typeof ChainId, + 'MAINNET' | 'POLYGON' | 'FANTOM' | 'SEPOLIA' | 'OPTIMISM' | 'GNOSIS_CHAIN' +>; const ANVIL_PORTS: Record = { //Ports separated by 100 to avoid port collision when running tests in parallel @@ -21,6 +24,7 @@ const ANVIL_PORTS: Record = { FANTOM: 8845, SEPOLIA: 8945, OPTIMISM: 9045, + GNOSIS_CHAIN: 9145, }; export const ANVIL_NETWORKS: Record = { @@ -57,6 +61,12 @@ export const ANVIL_NETWORKS: Record = { port: ANVIL_PORTS.OPTIMISM, forkBlockNumber: 117374265n, }, + GNOSIS_CHAIN: { + rpcEnv: 'GNOSIS_CHAIN_RPC_URL', + fallBackRpc: 'https://rpc.ankr.com/gnosis', + port: ANVIL_PORTS.GNOSIS_CHAIN, + forkBlockNumber: 35214423n, + }, }; function getAnvilOptions(network: NetworkSetup, blockNumber?: bigint): CreateAnvilOptions { diff --git a/test/factories/prismaToken.factory.ts b/test/factories/prismaToken.factory.ts index 2156ef6fe..5034e3f95 100644 --- a/test/factories/prismaToken.factory.ts +++ b/test/factories/prismaToken.factory.ts @@ -7,6 +7,7 @@ import { ZERO_ADDRESS } from '@balancer/sdk'; export const prismaPoolTokenFactory = Factory.define(({ sequence, params }) => { const tokenAddress = params?.address || createRandomAddress(); const poolId = params?.poolId || createRandomAddress(); + const decimals = params?.token?.decimals ?? 18; return { id: poolId + '-' + tokenAddress, address: tokenAddress, @@ -16,7 +17,7 @@ export const prismaPoolTokenFactory = Factory.define { + await stopAnvilForks(); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 4b4d90428..2bea66f6e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -12,5 +12,6 @@ export default defineConfig({ ], testTimeout: 120_000, hookTimeout: 120_000, + setupFiles: ['/test/vitest-setup.ts'], }, }); From a9b607dcf1f0f48c63c76f463b2315333929457d Mon Sep 17 00:00:00 2001 From: Bruno Eidam Guerios Date: Wed, 15 Jan 2025 17:32:07 -0300 Subject: [PATCH 2/4] Attempt to fix build issue on CI --- test/vitest-setup.ts | 5 ----- vitest-setup.ts | 5 +++++ vitest.config.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) delete mode 100644 test/vitest-setup.ts create mode 100644 vitest-setup.ts diff --git a/test/vitest-setup.ts b/test/vitest-setup.ts deleted file mode 100644 index 37f2b0aad..000000000 --- a/test/vitest-setup.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { stopAnvilForks } from './anvil/anvil-global-setup'; - -afterAll(async () => { - await stopAnvilForks(); -}); diff --git a/vitest-setup.ts b/vitest-setup.ts new file mode 100644 index 000000000..28407e633 --- /dev/null +++ b/vitest-setup.ts @@ -0,0 +1,5 @@ +import { stopAnvilForks } from 'test/anvil/anvil-global-setup'; + +afterAll(async () => { + await stopAnvilForks(); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 2bea66f6e..4200f4c00 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -12,6 +12,6 @@ export default defineConfig({ ], testTimeout: 120_000, hookTimeout: 120_000, - setupFiles: ['/test/vitest-setup.ts'], + setupFiles: ['./vitest-setup.ts'], }, }); From 5b2ffcbcc0d3fa25c2ebabbe3bf0612de51321cc Mon Sep 17 00:00:00 2001 From: Bruno Eidam Guerios Date: Wed, 15 Jan 2025 17:50:37 -0300 Subject: [PATCH 3/4] Another attempt to fix build on CI --- test/vitest-setup.ts | 5 +++++ tsconfig.json | 3 ++- vitest-setup.ts | 5 ----- vitest.config.ts | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 test/vitest-setup.ts delete mode 100644 vitest-setup.ts diff --git a/test/vitest-setup.ts b/test/vitest-setup.ts new file mode 100644 index 000000000..37f2b0aad --- /dev/null +++ b/test/vitest-setup.ts @@ -0,0 +1,5 @@ +import { stopAnvilForks } from './anvil/anvil-global-setup'; + +afterAll(async () => { + await stopAnvilForks(); +}); diff --git a/tsconfig.json b/tsconfig.json index b67a4b630..4f356f3f4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,8 @@ "resolveJsonModule": true, "outDir": "./dist", "skipLibCheck": true, - "sourceMap": true + "sourceMap": true, + "types": ["vitest/globals"], }, "exclude": ["node_modules", "debug", "**/*.spec.ts", "**/*.test.ts"] } diff --git a/vitest-setup.ts b/vitest-setup.ts deleted file mode 100644 index 28407e633..000000000 --- a/vitest-setup.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { stopAnvilForks } from 'test/anvil/anvil-global-setup'; - -afterAll(async () => { - await stopAnvilForks(); -}); diff --git a/vitest.config.ts b/vitest.config.ts index 4200f4c00..2bea66f6e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -12,6 +12,6 @@ export default defineConfig({ ], testTimeout: 120_000, hookTimeout: 120_000, - setupFiles: ['./vitest-setup.ts'], + setupFiles: ['/test/vitest-setup.ts'], }, }); From 4821240d06d790e629242c5e2c4d77a0fd250a8f Mon Sep 17 00:00:00 2001 From: Bruno Eidam Guerios Date: Thu, 16 Jan 2025 10:16:58 -0300 Subject: [PATCH 4/4] Add extra integration test with return amount > 0 --- modules/sor/balancer-sor.integration.test.ts | 2 +- .../balancer-v2-sor.integration.test.ts | 62 ++++++++++++++++--- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/modules/sor/balancer-sor.integration.test.ts b/modules/sor/balancer-sor.integration.test.ts index 63788f2df..a9bab5dc0 100644 --- a/modules/sor/balancer-sor.integration.test.ts +++ b/modules/sor/balancer-sor.integration.test.ts @@ -1,6 +1,6 @@ // yarn vitest balancer-sor.integration.test.ts -import { ExactInQueryOutput, Swap, SwapKind, Token, Address, Path } from '@balancer/sdk'; +import { ExactInQueryOutput, Swap, SwapKind, Token, Address, Path, ExactOutQueryOutput } from '@balancer/sdk'; import { PathWithAmount } from './sorV2/lib/path'; import { sorGetPathsWithPools } from './sorV2/lib/static'; diff --git a/modules/sor/sorV2/lib/poolsV2/balancer-v2-sor.integration.test.ts b/modules/sor/sorV2/lib/poolsV2/balancer-v2-sor.integration.test.ts index e9fb4b349..c6d982e80 100644 --- a/modules/sor/sorV2/lib/poolsV2/balancer-v2-sor.integration.test.ts +++ b/modules/sor/sorV2/lib/poolsV2/balancer-v2-sor.integration.test.ts @@ -1,16 +1,17 @@ // yarn vitest balancer-v2-sor.integration.test.ts -import { ExactInQueryOutput, Swap, SwapKind, Token, Address, Path } from '@balancer/sdk'; +import { ExactInQueryOutput, Swap, SwapKind, Token, Address } from '@balancer/sdk'; import { PathWithAmount } from '../path'; import { sorGetPathsWithPools } from '../static'; import { getOutputAmount } from '../utils/helpers'; import { chainToChainId as chainToIdMap } from '../../../../network/chain-id-to-chain'; -import { ANVIL_NETWORKS, startFork, stopAnvilForks } from '../../../../../test/anvil/anvil-global-setup'; +import { ANVIL_NETWORKS, startFork } from '../../../../../test/anvil/anvil-global-setup'; import { prismaPoolDynamicDataFactory, prismaPoolFactory, prismaPoolTokenFactory } from '../../../../../test/factories'; import { createTestClient, Hex, http, TestClient } from 'viem'; -import { sepolia } from 'viem/chains'; +import { gnosis } from 'viem/chains'; +import { PrismaPoolAndHookWithDynamic } from '../../../../../prisma/prisma-types'; /** * Test Data: @@ -35,7 +36,7 @@ describe('Balancer V2 SOR Integration Tests', () => { ({ rpcUrl } = await startFork(ANVIL_NETWORKS.GNOSIS_CHAIN)); client = createTestClient({ mode: 'anvil', - chain: sepolia, + chain: gnosis, transport: http(rpcUrl), }); snapshot = await client.snapshot(); @@ -49,38 +50,46 @@ describe('Balancer V2 SOR Integration Tests', () => { }); describe('Weighted Pool Path - Token with 0 decimals', () => { + let prismaWeightedPool: PrismaPoolAndHookWithDynamic; + let tIn: Token; + let tOut: Token; + beforeAll(async () => { // setup mock pool data const wxDAI = prismaPoolTokenFactory.build({ address: '0xe91d153e0b41518a2ce8dd3d7944fa863463a97d', - balance: '1818.053229398448550484', + balance: '2110.269380198644452506', weight: '0.5', }); const MPS = prismaPoolTokenFactory.build({ address: '0xfa57aa7beed63d03aaf85ffd1753f5f6242588fb', - balance: '437', + balance: '356', weight: '0.5', token: { decimals: 0, }, }); - const prismaWeightedPool = prismaPoolFactory.build({ + prismaWeightedPool = prismaPoolFactory.build({ id: '0x4bcf6b48906fa0f68bea1fc255869a41241d4851000200000000000000000021', address: '0x4bcf6b48906fa0f68bea1fc255869a41241d4851', type: 'WEIGHTED', protocolVersion, tokens: [wxDAI, MPS], dynamicData: prismaPoolDynamicDataFactory.build({ - totalShares: '1551.518323760171904919', + totalShares: '1584.613732317989225757', swapFee: '0.03', }), chain: 'GNOSIS', }); + tIn = new Token(parseFloat(chainToIdMap['GNOSIS']), wxDAI.address as Address, wxDAI.token.decimals); + tOut = new Token(parseFloat(chainToIdMap['GNOSIS']), MPS.address as Address, MPS.token.decimals); + }); + + test('SOR quote should match swap query - below min', async () => { // get SOR paths - const tIn = new Token(parseFloat(chainToIdMap['GNOSIS']), wxDAI.address as Address, wxDAI.token.decimals); - const tOut = new Token(parseFloat(chainToIdMap['GNOSIS']), MPS.address as Address, MPS.token.decimals); const amountIn = BigInt(1e18); + paths = (await sorGetPathsWithPools( tIn, tOut, @@ -105,9 +114,42 @@ describe('Balancer V2 SOR Integration Tests', () => { })), swapKind: SwapKind.GivenIn, }); + + const returnAmountSOR = getOutputAmount(paths); + const queryOutput = await sdkSwap.query(rpcUrl); + const returnAmountQuery = (queryOutput as ExactInQueryOutput).expectedAmountOut; + expect(returnAmountQuery.amount).toEqual(returnAmountSOR.amount); }); test('SOR quote should match swap query', async () => { + // get SOR paths + const amountIn = BigInt(10e18); + + paths = (await sorGetPathsWithPools( + tIn, + tOut, + SwapKind.GivenIn, + amountIn, + [prismaWeightedPool], + protocolVersion, + )) as PathWithAmount[]; + + // build SDK swap from SOR paths + sdkSwap = new Swap({ + chainId: parseFloat(chainToIdMap['GNOSIS']), + paths: paths.map((path) => ({ + protocolVersion, + inputAmountRaw: path.inputAmount.amount, + outputAmountRaw: path.outputAmount.amount, + tokens: path.tokens.map((token) => ({ + address: token.address, + decimals: token.decimals, + })), + pools: path.pools.map((pool) => pool.id), + })), + swapKind: SwapKind.GivenIn, + }); + const returnAmountSOR = getOutputAmount(paths); const queryOutput = await sdkSwap.query(rpcUrl); const returnAmountQuery = (queryOutput as ExactInQueryOutput).expectedAmountOut;