Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SOR - Assert behavior for tokens with 0 decimals #1498

Merged
merged 4 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/slow-eggs-compare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'backend': patch
---

SOR - Assert behavior for tokens with 0 decimals
6 changes: 3 additions & 3 deletions modules/sor/balancer-sor.integration.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
Expand Down
159 changes: 159 additions & 0 deletions modules/sor/sorV2/lib/poolsV2/balancer-v2-sor.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// yarn vitest balancer-v2-sor.integration.test.ts

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 } from '../../../../../test/anvil/anvil-global-setup';
import { prismaPoolDynamicDataFactory, prismaPoolFactory, prismaPoolTokenFactory } from '../../../../../test/factories';
import { createTestClient, Hex, http, TestClient } from 'viem';
import { gnosis } from 'viem/chains';
import { PrismaPoolAndHookWithDynamic } from '../../../../../prisma/prisma-types';

/**
* 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: gnosis,
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', () => {
let prismaWeightedPool: PrismaPoolAndHookWithDynamic;
let tIn: Token;
let tOut: Token;

beforeAll(async () => {
// setup mock pool data
const wxDAI = prismaPoolTokenFactory.build({
address: '0xe91d153e0b41518a2ce8dd3d7944fa863463a97d',
balance: '2110.269380198644452506',
weight: '0.5',
});
const MPS = prismaPoolTokenFactory.build({
address: '0xfa57aa7beed63d03aaf85ffd1753f5f6242588fb',
balance: '356',
weight: '0.5',
token: {
decimals: 0,
},
});
prismaWeightedPool = prismaPoolFactory.build({
id: '0x4bcf6b48906fa0f68bea1fc255869a41241d4851000200000000000000000021',
address: '0x4bcf6b48906fa0f68bea1fc255869a41241d4851',
type: 'WEIGHTED',
protocolVersion,
tokens: [wxDAI, MPS],
dynamicData: prismaPoolDynamicDataFactory.build({
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 () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the issue description this is the test which ensures there is no issue in the sdk.

// get SOR paths
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,
});

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;
expect(returnAmountQuery.amount).toEqual(returnAmountSOR.amount);
});
});
});
12 changes: 11 additions & 1 deletion test/anvil/anvil-global-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ type NetworkSetup = {
forkBlockNumber: bigint;
};

type NetworksWithFork = Extract<keyof typeof ChainId, 'MAINNET' | 'POLYGON' | 'FANTOM' | 'SEPOLIA' | 'OPTIMISM'>;
type NetworksWithFork = Extract<
keyof typeof ChainId,
'MAINNET' | 'POLYGON' | 'FANTOM' | 'SEPOLIA' | 'OPTIMISM' | 'GNOSIS_CHAIN'
>;

const ANVIL_PORTS: Record<NetworksWithFork, number> = {
//Ports separated by 100 to avoid port collision when running tests in parallel
Expand All @@ -21,6 +24,7 @@ const ANVIL_PORTS: Record<NetworksWithFork, number> = {
FANTOM: 8845,
SEPOLIA: 8945,
OPTIMISM: 9045,
GNOSIS_CHAIN: 9145,
};

export const ANVIL_NETWORKS: Record<NetworksWithFork, NetworkSetup> = {
Expand Down Expand Up @@ -57,6 +61,12 @@ export const ANVIL_NETWORKS: Record<NetworksWithFork, NetworkSetup> = {
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 {
Expand Down
3 changes: 2 additions & 1 deletion test/factories/prismaToken.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ZERO_ADDRESS } from '@balancer/sdk';
export const prismaPoolTokenFactory = Factory.define<PrismaPoolTokenWithDynamicData>(({ sequence, params }) => {
const tokenAddress = params?.address || createRandomAddress();
const poolId = params?.poolId || createRandomAddress();
const decimals = params?.token?.decimals ?? 18;
return {
id: poolId + '-' + tokenAddress,
address: tokenAddress,
Expand All @@ -16,7 +17,7 @@ export const prismaPoolTokenFactory = Factory.define<PrismaPoolTokenWithDynamicD
nestedPoolId: null,
priceRateProvider: ZERO_ADDRESS,
exemptFromProtocolYieldFee: false,
token: prismaTokenFactory.build({ address: tokenAddress }),
token: prismaTokenFactory.build({ address: tokenAddress, decimals }),
balance: '10.000000000000000000',
balanceUSD: 10,
weight: '0.5',
Expand Down
5 changes: 5 additions & 0 deletions test/vitest-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { stopAnvilForks } from './anvil/anvil-global-setup';

afterAll(async () => {
await stopAnvilForks();
});
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"resolveJsonModule": true,
"outDir": "./dist",
"skipLibCheck": true,
"sourceMap": true
"sourceMap": true,
"types": ["vitest/globals"],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes working with tests easier.

},
"exclude": ["node_modules", "debug", "**/*.spec.ts", "**/*.test.ts"]
}
1 change: 1 addition & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ export default defineConfig({
],
testTimeout: 120_000,
hookTimeout: 120_000,
setupFiles: ['/test/vitest-setup.ts'],
},
});
Loading