diff --git a/.changeset/quick-teachers-tease.md b/.changeset/quick-teachers-tease.md new file mode 100644 index 00000000..cd6eed3f --- /dev/null +++ b/.changeset/quick-teachers-tease.md @@ -0,0 +1,5 @@ +--- +"@balancer/sdk": minor +--- + +Adds buffer/boosted pool support to swaps. diff --git a/src/entities/swap/paths/pathHelpers.ts b/src/entities/swap/paths/pathHelpers.ts index 7309c4ad..54a77101 100644 --- a/src/entities/swap/paths/pathHelpers.ts +++ b/src/entities/swap/paths/pathHelpers.ts @@ -34,6 +34,9 @@ export function validatePaths(paths: Path[]) { if (paths.length === 0) throw new Error('Invalid swap: must contain at least 1 path.'); + validateBufferVersion(paths); + validateBufferLength(paths); + const protocolVersion = paths[0].protocolVersion; if (!paths.every((p) => p.protocolVersion === protocolVersion)) throw new Error( @@ -56,3 +59,25 @@ export function validatePaths(paths: Path[]) { ); } } + +function validateBufferVersion(paths: Path[]) { + if ( + !paths.every((p) => { + return p.isBuffer ? p.protocolVersion === 3 : true; + }) + ) { + throw new Error('Unsupported swap: buffers not supported in V2.'); + } +} + +function validateBufferLength(paths: Path[]) { + if ( + !paths.every((p) => { + return p.isBuffer ? p.isBuffer.length === p.pools.length : true; + }) + ) { + throw new Error( + 'Unsupported swap: buffers and pools must have same length.', + ); + } +} diff --git a/src/entities/swap/paths/pathWithAmount.ts b/src/entities/swap/paths/pathWithAmount.ts index 25b5dd29..a1386f66 100644 --- a/src/entities/swap/paths/pathWithAmount.ts +++ b/src/entities/swap/paths/pathWithAmount.ts @@ -5,6 +5,7 @@ import { TokenApi } from './types'; export class PathWithAmount { public readonly pools: Address[]; + public readonly isBuffer: boolean[]; public readonly tokens: TokenApi[]; public readonly outputAmount: TokenAmount; public readonly inputAmount: TokenAmount; @@ -15,6 +16,7 @@ export class PathWithAmount { pools: Address[], inputAmountRaw: bigint, outputAmountRaw: bigint, + isBuffer: boolean[] | undefined, ) { if (pools.length === 0 || tokens.length < 2) { throw new Error( @@ -38,6 +40,9 @@ export class PathWithAmount { tokens[tokens.length - 1].decimals, ); this.pools = pools; + this.isBuffer = isBuffer + ? isBuffer + : new Array(this.pools.length).fill(false); this.tokens = tokens; this.inputAmount = TokenAmount.fromRawAmount(tokenIn, inputAmountRaw); this.outputAmount = TokenAmount.fromRawAmount( diff --git a/src/entities/swap/paths/types.ts b/src/entities/swap/paths/types.ts index 1633254c..9437ac09 100644 --- a/src/entities/swap/paths/types.ts +++ b/src/entities/swap/paths/types.ts @@ -5,6 +5,7 @@ export type TokenApi = Omit; export type Path = { pools: Address[] | Hex[]; + isBuffer?: boolean[]; tokens: TokenApi[]; outputAmountRaw: bigint; inputAmountRaw: bigint; diff --git a/src/entities/swap/swaps/v2/index.ts b/src/entities/swap/swaps/v2/index.ts index 856d90ad..a2c5695b 100644 --- a/src/entities/swap/swaps/v2/index.ts +++ b/src/entities/swap/swaps/v2/index.ts @@ -55,6 +55,7 @@ export class SwapV2 implements SwapBase { p.pools.map((pool) => pool.toLowerCase() as Address), p.inputAmountRaw, p.outputAmountRaw, + undefined, ), ); diff --git a/src/entities/swap/swaps/v3/index.ts b/src/entities/swap/swaps/v3/index.ts index 3e5853f9..5573a455 100644 --- a/src/entities/swap/swaps/v3/index.ts +++ b/src/entities/swap/swaps/v3/index.ts @@ -55,6 +55,7 @@ export class SwapV3 implements SwapBase { p.pools, p.inputAmountRaw, p.outputAmountRaw, + p.isBuffer, ), ); @@ -563,7 +564,7 @@ export class SwapV3 implements SwapBase { return { pool: pool, tokenOut: p.tokens[i + 1].address, - isBuffer: false, + isBuffer: p.isBuffer[i], }; }), }; @@ -579,7 +580,7 @@ export class SwapV3 implements SwapBase { return { pool: pool, tokenOut: p.tokens[i + 1].address, - isBuffer: false, + isBuffer: p.isBuffer[i], }; }), }; diff --git a/test/entities/swaps/swap.test.ts b/test/entities/swaps/swap.test.ts index dd4b29fb..594f9f60 100644 --- a/test/entities/swaps/swap.test.ts +++ b/test/entities/swaps/swap.test.ts @@ -110,6 +110,73 @@ describe('Swap', () => { 'Unsupported swap: all paths must start/end with same token.', ); }); + describe('buffers', () => { + const pathWithBuffers: Path = { + protocolVersion: 3, + tokens: [ + { + address: + '0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8', + decimals: 18, + }, // DAI + { + address: + '0x8a88124522dbbf1e56352ba3de1d9f78c143751e', + decimals: 18, + }, // stataToken + { + address: + '0xde46e43f46ff74a23a65ebb0580cbe3dfe684a17', + decimals: 6, + }, // stataToken + { + address: + '0xFF34B3d4Aee8ddCd6F9AFFFB6Fe49bD371b8a357', + decimals: 6, + }, // USDC + ], + pools: [ + '0x8a88124522dbbf1e56352ba3de1d9f78c143751e', // buffer + '0x90a46864cb1f042060554592038367e9c97e17f3', + '0xde46e43f46ff74a23a65ebb0580cbe3dfe684a17', // buffer + ], + isBuffer: [true, false, true], + inputAmountRaw: 1000000000000000000n, + outputAmountRaw: 1000000n, + }; + test('should throw if buffers used for V2', () => { + expect(() => { + new Swap({ + chainId, + paths: [ + { + ...pathWithBuffers, + protocolVersion: 2, + }, + ], + swapKind: SwapKind.GivenIn, + }); + }).toThrowError( + 'Unsupported swap: buffers not supported in V2.', + ); + }); + test('should throw if buffers not same length as pools', () => { + expect(() => { + new Swap({ + chainId, + paths: [ + { + ...pathWithBuffers, + isBuffer: [true, false], + }, + ], + swapKind: SwapKind.GivenIn, + }); + }).toThrowError( + 'Unsupported swap: buffers and pools must have same length.', + ); + }); + }); }); }); diff --git a/test/entities/swaps/v2/swapV2.integration.test.ts b/test/entities/swaps/v2/swapV2.integration.test.ts index fe18d60b..d900943a 100644 --- a/test/entities/swaps/v2/swapV2.integration.test.ts +++ b/test/entities/swaps/v2/swapV2.integration.test.ts @@ -137,28 +137,28 @@ describe('SwapV2', () => { ...swapParams, swapKind: SwapKind.GivenIn, }); - await assertSwapExactIn( - vault, + await assertSwapExactIn({ + contractToCall: vault, client, rpcUrl, chainId, swap, wethIsEth, - ); + }); }); test('GivenOut', async () => { const swap = new Swap({ ...swapParams, swapKind: SwapKind.GivenOut, }); - await assertSwapExactOut( - vault, + await assertSwapExactOut({ + contractToCall: vault, client, rpcUrl, chainId, swap, wethIsEth, - ); + }); }); }); describe('wethIsEth: true', () => { @@ -170,14 +170,14 @@ describe('SwapV2', () => { paths: [pathBalWeth], swapKind: SwapKind.GivenIn, }); - await assertSwapExactIn( - vault, + await assertSwapExactIn({ + contractToCall: vault, client, rpcUrl, chainId, swap, wethIsEth, - ); + }); }); test('GivenOut', async () => { const swap = new Swap({ @@ -185,14 +185,14 @@ describe('SwapV2', () => { paths: [pathBalWeth], swapKind: SwapKind.GivenOut, }); - await assertSwapExactOut( - vault, + await assertSwapExactOut({ + contractToCall: vault, client, rpcUrl, chainId, swap, wethIsEth, - ); + }); }); }); describe('eth in', () => { @@ -206,14 +206,14 @@ describe('SwapV2', () => { paths: [pathWethBal], swapKind: SwapKind.GivenIn, }); - await assertSwapExactIn( - vault, + await assertSwapExactIn({ + contractToCall: vault, client, rpcUrl, chainId, swap, wethIsEth, - ); + }); }); test('GivenOut', async () => { const pathWethBal = { @@ -225,14 +225,14 @@ describe('SwapV2', () => { paths: [pathWethBal], swapKind: SwapKind.GivenOut, }); - await assertSwapExactOut( - vault, + await assertSwapExactOut({ + contractToCall: vault, client, rpcUrl, chainId, swap, wethIsEth, - ); + }); }); }); }); diff --git a/test/entities/swaps/v3/swapV3.integration.test.ts b/test/entities/swaps/v3/swapV3.integration.test.ts index dca8e1ef..a160ba3e 100644 --- a/test/entities/swaps/v3/swapV3.integration.test.ts +++ b/test/entities/swaps/v3/swapV3.integration.test.ts @@ -51,6 +51,8 @@ const WETH = TOKENS[chainId].WETH; const USDC = TOKENS[chainId].USDC_AAVE; const DAI = TOKENS[chainId].DAI_AAVE; const USDC_DAI_BPT = POOLS[chainId].MOCK_USDC_DAI_POOL; +const stataUSDC = TOKENS[chainId].stataUSDC; +const stataDAI = TOKENS[chainId].stataDAI; describe('SwapV3', () => { let client: Client & PublicActions & TestActions & WalletActions; @@ -179,7 +181,7 @@ describe('SwapV3', () => { testAddress, tokens, [WETH.slot, BAL.slot, USDC.slot] as number[], - [parseEther('100'), parseEther('100'), 100000000n], + [parseEther('100'), parseEther('100'), 100000000000n], ); await approveSpenderOnTokens( @@ -360,14 +362,14 @@ describe('SwapV3', () => { paths: [pathBalWeth], swapKind: SwapKind.GivenIn, }); - await assertSwapExactIn( - BALANCER_ROUTER[chainId], + await assertSwapExactIn({ + contractToCall: BALANCER_ROUTER[chainId], client, rpcUrl, chainId, swap, - false, - ); + wethIsEth: false, + }); }); test('GivenOut', async () => { const swap = new Swap({ @@ -375,14 +377,14 @@ describe('SwapV3', () => { paths: [pathBalWeth], swapKind: SwapKind.GivenOut, }); - await assertSwapExactOut( - BALANCER_ROUTER[chainId], + await assertSwapExactOut({ + contractToCall: BALANCER_ROUTER[chainId], client, rpcUrl, chainId, swap, - false, - ); + wethIsEth: false, + }); }); }); describe('wethIsEth: true', () => { @@ -393,14 +395,14 @@ describe('SwapV3', () => { paths: [pathBalWeth], swapKind: SwapKind.GivenIn, }); - await assertSwapExactIn( - BALANCER_ROUTER[chainId], + await assertSwapExactIn({ + contractToCall: BALANCER_ROUTER[chainId], client, rpcUrl, chainId, swap, - true, - ); + wethIsEth: true, + }); }); test('GivenOut', async () => { const swap = new Swap({ @@ -408,14 +410,14 @@ describe('SwapV3', () => { paths: [pathBalWeth], swapKind: SwapKind.GivenOut, }); - await assertSwapExactOut( - BALANCER_ROUTER[chainId], + await assertSwapExactOut({ + contractToCall: BALANCER_ROUTER[chainId], client, rpcUrl, chainId, swap, - true, - ); + wethIsEth: true, + }); }); }); describe('eth in', () => { @@ -429,14 +431,14 @@ describe('SwapV3', () => { paths: [pathWethBal], swapKind: SwapKind.GivenIn, }); - await assertSwapExactIn( - BALANCER_ROUTER[chainId], + await assertSwapExactIn({ + contractToCall: BALANCER_ROUTER[chainId], client, rpcUrl, chainId, swap, - true, - ); + wethIsEth: true, + }); }); test('GivenOut', async () => { const pathWethBal = { @@ -448,14 +450,14 @@ describe('SwapV3', () => { paths: [pathWethBal], swapKind: SwapKind.GivenOut, }); - await assertSwapExactOut( - BALANCER_ROUTER[chainId], + await assertSwapExactOut({ + contractToCall: BALANCER_ROUTER[chainId], client, rpcUrl, chainId, swap, - true, - ); + wethIsEth: true, + }); }); }); }); @@ -468,64 +470,66 @@ describe('SwapV3', () => { describe('wethIsEth: false', () => { const wethIsEth = false; test('GivenIn', async () => { - await assertSwapExactIn( - BALANCER_BATCH_ROUTER[chainId], + await assertSwapExactIn({ + contractToCall: BALANCER_BATCH_ROUTER[chainId], client, rpcUrl, chainId, - new Swap({ + swap: new Swap({ chainId, paths: [pathMultiSwap], swapKind: SwapKind.GivenIn, }), wethIsEth, - ); + }); }); test('GivenOut', async () => { - await assertSwapExactOut( - BALANCER_BATCH_ROUTER[chainId], + await assertSwapExactOut({ + contractToCall: BALANCER_BATCH_ROUTER[chainId], client, rpcUrl, chainId, - new Swap({ + swap: new Swap({ chainId, paths: [pathMultiSwap], swapKind: SwapKind.GivenOut, }), wethIsEth, - ); + }); }); }); describe('wethIsEth: true', () => { const wethIsEth = true; describe('eth in', async () => { test('GivenIn', async () => { - await assertSwapExactIn( - BALANCER_BATCH_ROUTER[chainId], + await assertSwapExactIn({ + contractToCall: + BALANCER_BATCH_ROUTER[chainId], client, rpcUrl, chainId, - new Swap({ + swap: new Swap({ chainId, paths: [pathMultiSwap], swapKind: SwapKind.GivenIn, }), wethIsEth, - ); + }); }); test('GivenOut', async () => { - await assertSwapExactOut( - BALANCER_BATCH_ROUTER[chainId], + await assertSwapExactOut({ + contractToCall: + BALANCER_BATCH_ROUTER[chainId], client, rpcUrl, chainId, - new Swap({ + swap: new Swap({ chainId, paths: [pathMultiSwap], swapKind: SwapKind.GivenOut, }), wethIsEth, - ); + }); }); }); describe('eth out', () => { @@ -535,14 +539,15 @@ describe('SwapV3', () => { paths: [pathUsdcWethMulti], swapKind: SwapKind.GivenIn, }); - await assertSwapExactIn( - BALANCER_BATCH_ROUTER[chainId], + await assertSwapExactIn({ + contractToCall: + BALANCER_BATCH_ROUTER[chainId], client, rpcUrl, chainId, swap, wethIsEth, - ); + }); }); test('GivenOut', async () => { const swap = new Swap({ @@ -550,14 +555,15 @@ describe('SwapV3', () => { paths: [pathUsdcWethMulti], swapKind: SwapKind.GivenOut, }); - await assertSwapExactOut( - BALANCER_BATCH_ROUTER[chainId], + await assertSwapExactOut({ + contractToCall: + BALANCER_BATCH_ROUTER[chainId], client, rpcUrl, chainId, swap, wethIsEth, - ); + }); }); }); }); @@ -570,14 +576,14 @@ describe('SwapV3', () => { paths: [pathMultiSwap, pathWithExit], swapKind: SwapKind.GivenIn, }); - await assertSwapExactIn( - BALANCER_BATCH_ROUTER[chainId], + await assertSwapExactIn({ + contractToCall: BALANCER_BATCH_ROUTER[chainId], client, rpcUrl, chainId, swap, - false, - ); + wethIsEth: false, + }); }); test('GivenOut', async () => { const swap = new Swap({ @@ -585,14 +591,14 @@ describe('SwapV3', () => { paths: [pathMultiSwap, pathWithExit], swapKind: SwapKind.GivenOut, }); - await assertSwapExactOut( - BALANCER_BATCH_ROUTER[chainId], + await assertSwapExactOut({ + contractToCall: BALANCER_BATCH_ROUTER[chainId], client, rpcUrl, chainId, swap, - false, - ); + wethIsEth: false, + }); }); }); describe('wethIsEth: true', () => { @@ -603,14 +609,15 @@ describe('SwapV3', () => { paths: [pathMultiSwap, pathWithExit], swapKind: SwapKind.GivenIn, }); - await assertSwapExactIn( - BALANCER_BATCH_ROUTER[chainId], + await assertSwapExactIn({ + contractToCall: + BALANCER_BATCH_ROUTER[chainId], client, rpcUrl, chainId, swap, - true, - ); + wethIsEth: true, + }); }); test('GivenOut', async () => { const swap = new Swap({ @@ -618,14 +625,15 @@ describe('SwapV3', () => { paths: [pathMultiSwap, pathWithExit], swapKind: SwapKind.GivenOut, }); - await assertSwapExactOut( - BALANCER_BATCH_ROUTER[chainId], + await assertSwapExactOut({ + contractToCall: + BALANCER_BATCH_ROUTER[chainId], client, rpcUrl, chainId, swap, - true, - ); + wethIsEth: true, + }); }); }); describe('eth out', () => { @@ -638,14 +646,15 @@ describe('SwapV3', () => { ], swapKind: SwapKind.GivenIn, }); - await assertSwapExactIn( - BALANCER_BATCH_ROUTER[chainId], + await assertSwapExactIn({ + contractToCall: + BALANCER_BATCH_ROUTER[chainId], client, rpcUrl, chainId, swap, - true, - ); + wethIsEth: true, + }); }); test('GivenOut', async () => { const swap = new Swap({ @@ -656,14 +665,15 @@ describe('SwapV3', () => { ], swapKind: SwapKind.GivenOut, }); - await assertSwapExactOut( - BALANCER_BATCH_ROUTER[chainId], + await assertSwapExactOut({ + contractToCall: + BALANCER_BATCH_ROUTER[chainId], client, rpcUrl, chainId, swap, - true, - ); + wethIsEth: true, + }); }); }); }); @@ -683,15 +693,15 @@ describe('SwapV3', () => { paths: [pathBalWeth], swapKind: SwapKind.GivenIn, }); - await assertSwapExactIn( - BALANCER_ROUTER[chainId], + await assertSwapExactIn({ + contractToCall: BALANCER_ROUTER[chainId], client, rpcUrl, chainId, swap, - false, + wethIsEth: false, usePermit2Signatures, - ); + }); }); test('GivenOut', async () => { const swap = new Swap({ @@ -699,15 +709,15 @@ describe('SwapV3', () => { paths: [pathBalWeth], swapKind: SwapKind.GivenOut, }); - await assertSwapExactOut( - BALANCER_ROUTER[chainId], + await assertSwapExactOut({ + contractToCall: BALANCER_ROUTER[chainId], client, rpcUrl, chainId, swap, - false, + wethIsEth: false, usePermit2Signatures, - ); + }); }); }); describe('wethIsEth: true', () => { @@ -718,15 +728,15 @@ describe('SwapV3', () => { swapKind: SwapKind.GivenIn, }); await expect(() => - assertSwapExactIn( - BALANCER_ROUTER[chainId], + assertSwapExactIn({ + contractToCall: BALANCER_ROUTER[chainId], client, rpcUrl, chainId, swap, - true, + wethIsEth: true, usePermit2Signatures, - ), + }), ).rejects.toThrowError(buildCallWithPermit2ETHError); }); }); @@ -739,53 +749,54 @@ describe('SwapV3', () => { describe('wethIsEth: false', () => { const wethIsEth = false; test('GivenIn', async () => { - await assertSwapExactIn( - BALANCER_BATCH_ROUTER[chainId], + await assertSwapExactIn({ + contractToCall: BALANCER_BATCH_ROUTER[chainId], client, rpcUrl, chainId, - new Swap({ + swap: new Swap({ chainId, paths: [pathMultiSwap], swapKind: SwapKind.GivenIn, }), wethIsEth, usePermit2Signatures, - ); + }); }); test('GivenOut', async () => { - await assertSwapExactOut( - BALANCER_BATCH_ROUTER[chainId], + await assertSwapExactOut({ + contractToCall: BALANCER_BATCH_ROUTER[chainId], client, rpcUrl, chainId, - new Swap({ + swap: new Swap({ chainId, paths: [pathMultiSwap], swapKind: SwapKind.GivenOut, }), wethIsEth, usePermit2Signatures, - ); + }); }); }); describe('wethIsEth: true', () => { const wethIsEth = true; test('should throw', async () => { await expect(() => - assertSwapExactIn( - BALANCER_BATCH_ROUTER[chainId], + assertSwapExactIn({ + contractToCall: + BALANCER_BATCH_ROUTER[chainId], client, rpcUrl, chainId, - new Swap({ + swap: new Swap({ chainId, paths: [pathMultiSwap], swapKind: SwapKind.GivenIn, }), wethIsEth, usePermit2Signatures, - ), + }), ).rejects.toThrowError( buildCallWithPermit2ETHError, ); @@ -800,15 +811,15 @@ describe('SwapV3', () => { paths: [pathMultiSwap, pathWithExit], swapKind: SwapKind.GivenIn, }); - await assertSwapExactIn( - BALANCER_BATCH_ROUTER[chainId], + await assertSwapExactIn({ + contractToCall: BALANCER_BATCH_ROUTER[chainId], client, rpcUrl, chainId, swap, - false, + wethIsEth: false, usePermit2Signatures, - ); + }); }); test('GivenOut', async () => { const swap = new Swap({ @@ -816,15 +827,15 @@ describe('SwapV3', () => { paths: [pathMultiSwap, pathWithExit], swapKind: SwapKind.GivenOut, }); - await assertSwapExactOut( - BALANCER_BATCH_ROUTER[chainId], + await assertSwapExactOut({ + contractToCall: BALANCER_BATCH_ROUTER[chainId], client, rpcUrl, chainId, swap, - false, + wethIsEth: false, usePermit2Signatures, - ); + }); }); }); describe('wethIsEth: true', () => { @@ -835,15 +846,16 @@ describe('SwapV3', () => { swapKind: SwapKind.GivenIn, }); await expect(() => - assertSwapExactIn( - BALANCER_BATCH_ROUTER[chainId], + assertSwapExactIn({ + contractToCall: + BALANCER_BATCH_ROUTER[chainId], client, rpcUrl, chainId, swap, - true, + wethIsEth: true, usePermit2Signatures, - ), + }), ).rejects.toThrowError( buildCallWithPermit2ETHError, ); @@ -853,4 +865,163 @@ describe('SwapV3', () => { }); }); }); + + describe('boosted', () => { + describe('multi-hop swap', () => { + // USDC[wrap]aUSDC[swap]aDAI[unwrap]DAI + const pathWithBuffers = { + protocolVersion: 3, + tokens: [ + { + address: USDC.address, + decimals: USDC.decimals, + }, + { + address: stataUSDC.address, + decimals: stataUSDC.decimals, + }, + { + address: stataDAI.address, + decimals: stataDAI.decimals, + }, + { + address: DAI.address, + decimals: DAI.decimals, + }, + ], + pools: [ + stataUSDC.address, + POOLS[chainId].MOCK_BOOSTED_POOL.id, + stataDAI.address, + ], + isBuffer: [true, false, true], + }; + + describe('query method should return correct updated', () => { + test('GivenIn', async () => { + const swap = new Swap({ + chainId, + paths: [ + { + ...pathWithBuffers, + inputAmountRaw: 100000000n, + outputAmountRaw: 0n, + } as Path, + ], + swapKind: SwapKind.GivenIn, + }); + + const expected = (await swap.query( + rpcUrl, + )) as ExactInQueryOutput; + + const daiToken = new Token( + chainId, + DAI.address, + DAI.decimals, + ); + expect(expected.swapKind).to.eq(SwapKind.GivenIn); + expect(expected.pathAmounts).to.deep.eq([ + 98985649641876909761n, + ]); + expect(expected.expectedAmountOut.token).to.deep.eq( + daiToken, + ); + expect(expected.expectedAmountOut.amount).to.eq( + 98985649641876909761n, + ); + }); + test('GivenOut', async () => { + const swap = new Swap({ + chainId, + paths: [ + { + ...pathWithBuffers, + inputAmountRaw: 0n, + outputAmountRaw: 100000000000000000000n, + } as Path, + ], + swapKind: SwapKind.GivenOut, + }); + + const expected = (await swap.query( + rpcUrl, + )) as ExactOutQueryOutput; + + const usdcToken = new Token( + chainId, + USDC.address, + USDC.decimals, + ); + expect(expected.swapKind).to.eq(SwapKind.GivenOut); + expect(expected.pathAmounts).to.deep.eq([101014646n]); + expect(expected.expectedAmountIn.token).to.deep.eq( + usdcToken, + ); + expect(expected.expectedAmountIn.amount).to.eq(101014646n); + }); + }); + describe('swap should be executed correctly', () => { + beforeEach(async () => { + await approveTokens( + client, + testAddress, + tokens, + protocolVersion, + ); + }); + test('GivenIn', async () => { + const swap = new Swap({ + chainId, + paths: [ + { + ...pathWithBuffers, + inputAmountRaw: 100000000n, + outputAmountRaw: 0n, + } as Path, + ], + swapKind: SwapKind.GivenIn, + }); + // Buffers can have a small difference due to rates so we don't check for 100% match between result and query + await assertSwapExactIn({ + contractToCall: BALANCER_BATCH_ROUTER[chainId], + client, + rpcUrl, + chainId, + swap, + wethIsEth: false, + outputTest: { + testExactOutAmount: false, + percentage: 0.001, + }, + }); + }); + test('GivenOut', async () => { + const swap = new Swap({ + chainId, + paths: [ + { + ...pathWithBuffers, + inputAmountRaw: 0n, + outputAmountRaw: 100000000000000000000n, + } as Path, + ], + swapKind: SwapKind.GivenOut, + }); + await assertSwapExactOut({ + contractToCall: BALANCER_BATCH_ROUTER[chainId], + client, + rpcUrl, + chainId, + swap, + wethIsEth: false, + inputTest: { + testExactInAmount: false, + percentage: 0.001, + }, + }); + }); + }); + }); + }); }); diff --git a/test/entities/swaps/v3/swapV3.test.ts b/test/entities/swaps/v3/swapV3.test.ts index 69ede96e..023769b6 100644 --- a/test/entities/swaps/v3/swapV3.test.ts +++ b/test/entities/swaps/v3/swapV3.test.ts @@ -10,28 +10,28 @@ import { Path, TokenApi } from '@/entities/swap/paths/types'; import { TOKENS } from 'test/lib/utils/addresses'; +const tokens: TokenApi[] = [ + { + address: TOKENS[1].WETH.address, + decimals: TOKENS[1].WETH.decimals, + }, + { + address: TOKENS[1].DAI.address, + decimals: TOKENS[1].DAI.decimals, + }, + { + address: TOKENS[1].USDC.address, + decimals: TOKENS[1].USDC.decimals, + }, + { + address: TOKENS[1].USDT.address, + decimals: TOKENS[1].USDT.decimals, + }, +]; + describe('SwapV3', () => { - describe('getSwaps', () => { + describe('getSwaps - non-boosted', () => { describe('batchSwap', () => { - const tokens: TokenApi[] = [ - { - address: TOKENS[1].WETH.address, - decimals: TOKENS[1].WETH.decimals, - }, - { - address: TOKENS[1].DAI.address, - decimals: TOKENS[1].DAI.decimals, - }, - { - address: TOKENS[1].USDC.address, - decimals: TOKENS[1].USDC.decimals, - }, - { - address: TOKENS[1].USDT.address, - decimals: TOKENS[1].USDT.decimals, - }, - ]; - const path1hop: Path = { protocolVersion: 3, tokens: [tokens[0], tokens[3]], @@ -139,6 +139,123 @@ describe('SwapV3', () => { ], }; + expect(swap.swaps).to.deep.eq([ + expected1hop, + expected3hops, + ]); + }); + }); + }); + }); + describe('getSwaps - boosted', () => { + describe('batchSwap', () => { + const path1hop: Path = { + protocolVersion: 3, + tokens: [tokens[0], tokens[3]], + pools: [tokens[0].address], + isBuffer: [true], + inputAmountRaw: 2000000000000000000n, + outputAmountRaw: 2000000n, + }; + const path3hops: Path = { + protocolVersion: 3, + tokens, + pools: [ + tokens[0].address, + '0xcb444e90d8198415266c6a2724b7900fb12fc56e000000000000000000000011', + tokens[3].address, + ], + isBuffer: [true, false, true], + inputAmountRaw: 1000000000000000000n, + outputAmountRaw: 1000000n, + }; + + describe('GivenIn', () => { + test('swaps should be created correctly', () => { + const swap = new SwapV3({ + chainId: ChainId.MAINNET, + paths: [path1hop, path3hops], + swapKind: SwapKind.GivenIn, + }); + const expected1hop: SwapPathExactAmountIn = { + tokenIn: path1hop.tokens[0].address, + exactAmountIn: path1hop.inputAmountRaw, + steps: [ + { + pool: path1hop.pools[0], + tokenOut: path1hop.tokens[1].address, + isBuffer: true, + }, + ], + }; + const expected3hops: SwapPathExactAmountIn = { + tokenIn: path3hops.tokens[0].address, + exactAmountIn: path3hops.inputAmountRaw, + steps: [ + { + pool: path3hops.pools[0], + tokenOut: path3hops.tokens[1].address, + isBuffer: true, + }, + { + pool: path3hops.pools[1], + tokenOut: path3hops.tokens[2].address, + isBuffer: false, + }, + { + pool: path3hops.pools[2], + tokenOut: path3hops.tokens[3].address, + isBuffer: true, + }, + ], + }; + + expect(swap.swaps).to.deep.eq([ + expected1hop, + expected3hops, + ]); + }); + }); + describe('GivenOut', () => { + test('swaps should be created correctly', () => { + const swap = new SwapV3({ + chainId: ChainId.MAINNET, + paths: [path1hop, path3hops], + swapKind: SwapKind.GivenOut, + }); + const expected1hop: SwapPathExactAmountOut = { + tokenIn: path1hop.tokens[0].address, + exactAmountOut: path1hop.outputAmountRaw, + steps: [ + { + pool: path1hop.pools[0], + tokenOut: path1hop.tokens[1].address, + isBuffer: true, + }, + ], + }; + const expected3hops: SwapPathExactAmountOut = { + tokenIn: path3hops.tokens[0].address, + exactAmountOut: path3hops.outputAmountRaw, + steps: [ + { + pool: path3hops.pools[0], + tokenOut: path3hops.tokens[1].address, + isBuffer: true, + }, + { + pool: path3hops.pools[1], + tokenOut: path3hops.tokens[2].address, + isBuffer: false, + }, + { + pool: path3hops.pools[2], + tokenOut: path3hops.tokens[3].address, + isBuffer: true, + }, + ], + }; + expect(swap.swaps).to.deep.eq([ expected1hop, expected3hops, diff --git a/test/lib/utils/addresses.ts b/test/lib/utils/addresses.ts index 53d3d7d1..196ed77c 100644 --- a/test/lib/utils/addresses.ts +++ b/test/lib/utils/addresses.ts @@ -122,7 +122,17 @@ export const TOKENS: Record> = { slot: 0, }, DAI_AAVE: { - address: '0xff34b3d4aee8ddcd6f9afffb6fe49bd371b8a357', + address: '0xFF34B3d4Aee8ddCd6F9AFFFB6Fe49bD371b8a357', + decimals: 18, + slot: 0, + }, + stataUSDC: { + address: '0x8a88124522dbbf1e56352ba3de1d9f78c143751e', + decimals: 6, + slot: 0, + }, + stataDAI: { + address: '0xde46e43f46ff74a23a65ebb0580cbe3dfe684a17', decimals: 18, slot: 0, }, @@ -285,5 +295,12 @@ export const POOLS: Record> = { decimals: 18, slot: 0, }, + MOCK_BOOSTED_POOL: { + address: '0x302b75a27e5e157f93c679dd7a25fdfcdbc1473c', + id: '0x302b75a27e5e157f93c679dd7a25fdfcdbc1473c', + type: PoolType.Stable, + decimals: 18, + slot: 0, + }, }, }; diff --git a/test/lib/utils/swapHelpers.ts b/test/lib/utils/swapHelpers.ts index 80ea538f..25db95d1 100644 --- a/test/lib/utils/swapHelpers.ts +++ b/test/lib/utils/swapHelpers.ts @@ -19,15 +19,46 @@ import { } from '../../../src'; import { sendTransactionGetBalances } from '../../lib/utils/helper'; -export async function assertSwapExactIn( - contractToCall: Address, - client: Client & PublicActions & TestActions & WalletActions, - rpcUrl: string, - chainId: ChainId, - swap: Swap, - wethIsEth: boolean, +// Helper function to check if two BigInts are within a given percentage +function areBigIntsWithinPercent( + value1: bigint, + value2: bigint, + percent: number, +): boolean { + if (percent < 0) { + throw new Error('Percent must be non-negative'); + } + const difference = value1 > value2 ? value1 - value2 : value2 - value1; + const percentFactor = BigInt(Math.floor(percent * 1e8)); + const tolerance = (value2 * percentFactor) / BigInt(1e10); + return difference <= tolerance; +} + +export async function assertSwapExactIn({ + contractToCall, + client, + rpcUrl, + chainId, + swap, + wethIsEth, usePermit2Signatures = false, -) { + outputTest = { + testExactOutAmount: true, + percentage: 0, + }, +}: { + contractToCall: Address; + client: Client & PublicActions & TestActions & WalletActions; + rpcUrl: string; + chainId: ChainId; + swap: Swap; + wethIsEth: boolean; + usePermit2Signatures?: boolean; + outputTest?: { + testExactOutAmount: boolean; + percentage: number; + }; +}) { const testAddress = (await client.getAddresses())[0]; const slippage = Slippage.fromPercentage('0.1'); const deadline = 999999999999999999n; @@ -108,22 +139,52 @@ export async function assertSwapExactIn( expectedTokenOutDelta = 0n; } - expect(balanceDeltas).to.deep.eq([ - expectedEthDelta, - expectedTokenInDelta, - expectedTokenOutDelta, - ]); + if (outputTest.testExactOutAmount) + expect(balanceDeltas).to.deep.eq([ + expectedEthDelta, + expectedTokenInDelta, + expectedTokenOutDelta, + ]); + else { + // Here we check that output diff is within an acceptable tolerance. + // !!! This should only be used in the case of buffers as all other cases can be equal + expect(balanceDeltas[0]).to.eq(expectedEthDelta); + expect(balanceDeltas[1]).to.eq(expectedTokenInDelta); + expect( + areBigIntsWithinPercent( + balanceDeltas[2], + expectedTokenOutDelta, + outputTest.percentage, + ), + ).toBe(true); + } } -export async function assertSwapExactOut( - contractToCall: Address, - client: Client & PublicActions & TestActions & WalletActions, - rpcUrl: string, - chainId: ChainId, - swap: Swap, - wethIsEth: boolean, +export async function assertSwapExactOut({ + contractToCall, + client, + rpcUrl, + chainId, + swap, + wethIsEth, usePermit2Signatures = false, -) { + inputTest = { + testExactInAmount: true, + percentage: 0, + }, +}: { + contractToCall: Address; + client: Client & PublicActions & TestActions & WalletActions; + rpcUrl: string; + chainId: ChainId; + swap: Swap; + wethIsEth: boolean; + usePermit2Signatures?: boolean; + inputTest?: { + testExactInAmount: boolean; + percentage: number; + }; +}) { const testAddress = (await client.getAddresses())[0]; const slippage = Slippage.fromPercentage('0.1'); const deadline = 999999999999999999n; @@ -207,9 +268,23 @@ export async function assertSwapExactOut( expectedTokenOutDelta = 0n; } - expect(balanceDeltas).to.deep.eq([ - expectedEthDelta, - expectedTokenInDelta, - expectedTokenOutDelta, - ]); + if (inputTest.testExactInAmount) + expect(balanceDeltas).to.deep.eq([ + expectedEthDelta, + expectedTokenInDelta, + expectedTokenOutDelta, + ]); + else { + // Here we check that output diff is within an acceptable tolerance. + // !!! This should only be used in the case of buffers as all other cases can be equal + expect(balanceDeltas[0]).to.eq(expectedEthDelta); + expect(balanceDeltas[2]).to.eq(expectedTokenOutDelta); + expect( + areBigIntsWithinPercent( + balanceDeltas[1], + expectedTokenInDelta, + inputTest.percentage, + ), + ).toBe(true); + } }