From e0f996ec757a4f419e3ed5eec8ddb0b7b43af8c5 Mon Sep 17 00:00:00 2001 From: Pano Skylakis <49353965+pano-skylakis@users.noreply.github.com> Date: Thu, 9 Nov 2023 18:07:23 +1300 Subject: [PATCH] [TP-1717] Add test cases for secondary fee native-input swaps (#1140) Co-authored-by: Pano Skylakis --- ...ange.getUnsignedSwapTxFromAmountIn.test.ts | 906 +++++++++--------- 1 file changed, 469 insertions(+), 437 deletions(-) diff --git a/packages/internal/dex/sdk/src/exchange.getUnsignedSwapTxFromAmountIn.test.ts b/packages/internal/dex/sdk/src/exchange.getUnsignedSwapTxFromAmountIn.test.ts index 946685de1a..f9c85eb5b6 100644 --- a/packages/internal/dex/sdk/src/exchange.getUnsignedSwapTxFromAmountIn.test.ts +++ b/packages/internal/dex/sdk/src/exchange.getUnsignedSwapTxFromAmountIn.test.ts @@ -106,7 +106,223 @@ describe('getUnsignedSwapTxFromAmountIn', () => { }); }); - describe('with a native token in', () => { + describe('with a single pool without fees and default slippage tolerance', () => { + it('generates valid swap calldata', async () => { + const params = setupSwapTxTest(); + + mockRouterImplementation(params); + + const exchange = new Exchange(TEST_DEX_CONFIGURATION); + + const { swap } = await exchange.getUnsignedSwapTxFromAmountIn( + params.fromAddress, + params.inputToken, + params.outputToken, + newAmountFromString('100', USDC_TEST_TOKEN).value, + ); + + expectToBeDefined(swap.transaction.data); + + const data = swap.transaction.data.toString(); + + const { swapParams } = decodeMulticallExactInputSingleWithoutFees(data); + expectInstanceOf(BigNumber, swapParams.amountIn); + + expect(swapParams.tokenIn).toBe(params.inputToken); // input token + expect(swapParams.tokenOut).toBe(params.outputToken); // output token + expect(swapParams.fee).toBe(10000); // fee + expect(swapParams.recipient).toBe(params.fromAddress); // recipient + expect(swap.transaction.to).toBe(TEST_ROUTER_ADDRESS); // to address + expect(swap.transaction.from).toBe(params.fromAddress); // from address + expect(swap.transaction.value).toBe('0x00'); // refers to 0ETH + expect(formatTokenAmount(swapParams.amountIn, USDC_TEST_TOKEN)).toBe('100.0'); // amount in + expect(formatEther(swapParams.amountOutMinimum)).toBe('999.000999000999000999'); // min amount out (includes slippage) + expect(swapParams.sqrtPriceLimitX96.toString()).toBe('0'); // sqrtPriceX96Limit + }); + + it('returns the gas estimate for the swap', async () => { + const params = setupSwapTxTest(); + + mockRouterImplementation(params); + + const exchange = new Exchange(TEST_DEX_CONFIGURATION); + + const tx = await exchange.getUnsignedSwapTxFromAmountIn( + params.fromAddress, + params.inputToken, + params.outputToken, + newAmountFromString('100', USDC_TEST_TOKEN).value, + ); + + expectToBeDefined(tx.swap.gasFeeEstimate); + + expect(tx.swap.gasFeeEstimate.value).toEqual(TEST_TRANSACTION_GAS_USAGE.mul(TEST_GAS_PRICE)); + expect(tx.swap.gasFeeEstimate.token.chainId).toEqual(NATIVE_TEST_TOKEN.chainId); + expect(tx.swap.gasFeeEstimate.token.address).toEqual(''); // Default configuration is a native token for gas and not an ERC20 + expect(tx.swap.gasFeeEstimate.token.decimals).toEqual(NATIVE_TEST_TOKEN.decimals); + expect(tx.swap.gasFeeEstimate.token.symbol).toEqual(NATIVE_TEST_TOKEN.symbol); + expect(tx.swap.gasFeeEstimate.token.name).toEqual(NATIVE_TEST_TOKEN.name); + }); + + it('returns valid quote', async () => { + const params = setupSwapTxTest(); + + mockRouterImplementation(params); + + const exchange = new Exchange(TEST_DEX_CONFIGURATION); + + const { quote } = await exchange.getUnsignedSwapTxFromAmountIn( + params.fromAddress, + params.inputToken, + params.outputToken, + newAmountFromString('100', USDC_TEST_TOKEN).value, + ); + + expect(quote).not.toBe(undefined); + expect(quote.amount.token.address).toEqual(params.outputToken); + expect(quote.slippage).toBe(0.1); + expect(formatAmount(quote.amount)).toEqual('1000.0'); + expect(quote.amountWithMaxSlippage.token.address).toEqual(params.outputToken); + expect(formatAmount(quote.amountWithMaxSlippage)).toEqual('999.000999000999000999'); // includes slippage + }); + }); + + describe('with a single pool without fees and high slippage tolerance', () => { + it('generates valid calldata', async () => { + const params = setupSwapTxTest(); + mockRouterImplementation(params); + + const exchange = new Exchange(TEST_DEX_CONFIGURATION); + + const { swap } = await exchange.getUnsignedSwapTxFromAmountIn( + params.fromAddress, + params.inputToken, + params.outputToken, + newAmountFromString('100', USDC_TEST_TOKEN).value, + HIGHER_SLIPPAGE, + ); + + expectToBeDefined(swap.transaction.data); + + const data = swap.transaction.data.toString(); + + const { swapParams } = decodeMulticallExactInputSingleWithoutFees(data); + expectInstanceOf(BigNumber, swapParams.amountIn); + + expect(swapParams.tokenIn).toBe(params.inputToken); // input token + expect(swapParams.tokenOut).toBe(params.outputToken); // output token + expect(swapParams.fee).toBe(10000); // fee + expect(swapParams.recipient).toBe(params.fromAddress); // recipient + expect(swap.transaction.to).toBe(TEST_ROUTER_ADDRESS); // to address + expect(swap.transaction.from).toBe(params.fromAddress); // from address + expect(swap.transaction.value).toBe('0x00'); // refers to 0ETH + expect(formatTokenAmount(swapParams.amountIn, USDC_TEST_TOKEN)).toBe('100.0'); // amount in + expect(formatEther(swapParams.amountOutMinimum)).toBe('998.003992015968063872'); // min amount out (includes 0.2% slippage) + expect(swapParams.sqrtPriceLimitX96.toString()).toBe('0'); // sqrtPriceX96Limit + }); + + it('returns valid quote', async () => { + const params = setupSwapTxTest(); + mockRouterImplementation(params); + + const exchange = new Exchange(TEST_DEX_CONFIGURATION); + + const { quote } = await exchange.getUnsignedSwapTxFromAmountIn( + params.fromAddress, + params.inputToken, + params.outputToken, + newAmountFromString('100', USDC_TEST_TOKEN).value, + HIGHER_SLIPPAGE, + ); + + expect(quote.amount.token.address).toEqual(params.outputToken); + expect(quote.slippage).toBe(0.2); + expect(formatAmount(quote.amount)).toEqual('1000.0'); + expect(quote.amountWithMaxSlippage.token.address).toEqual(params.outputToken); + expect(formatAmount(quote.amountWithMaxSlippage)).toEqual('998.003992015968063872'); // includes 0.2% slippage + }); + }); + + describe('with a single pool and secondary fees', () => { + it('generates valid swap calldata', async () => { + const params = setupSwapTxTest(); + const findOptimalRouteMock = mockRouterImplementation(params); + + const secondaryFees: SecondaryFee[] = [ + { recipient: TEST_FEE_RECIPIENT, basisPoints: 100 }, // 1% Fee + ]; + const exchange = new Exchange({ ...TEST_DEX_CONFIGURATION, secondaryFees }); + + const { swap, quote } = await exchange.getUnsignedSwapTxFromAmountIn( + params.fromAddress, + params.inputToken, + params.outputToken, + newAmountFromString('100', USDC_TEST_TOKEN).value, + 3, // 3% Slippage + ); + + expectToBeDefined(swap.transaction.data); + + expect(formatAmount(quote.amountWithMaxSlippage)).toEqual('961.165048543689320388'); // userQuoteRes.amountOutMinimum = swapReq.amountOutMinimum + + const ourQuoteReqAmountIn = findOptimalRouteMock.mock.calls[0][0]; + expect(formatAmount(ourQuoteReqAmountIn)).toEqual('99.0'); // ourQuoteReq.amountIn = the amount specified less the fee + + const data = swap.transaction.data.toString(); + + const { swapParams, secondaryFeeParams } = decodeMulticallExactInputSingleWithFees(data); + expectInstanceOf(BigNumber, swapParams.amountIn); + + expect(secondaryFeeParams[0].recipient).toBe(TEST_FEE_RECIPIENT); + expect(secondaryFeeParams[0].basisPoints.toString()).toBe('100'); + + expect(swapParams.tokenIn).toBe(params.inputToken); + expect(swapParams.tokenOut).toBe(params.outputToken); + expect(swapParams.fee).toBe(10000); + expect(swapParams.recipient).toBe(params.fromAddress); + expect(formatTokenAmount(swapParams.amountIn, USDC_TEST_TOKEN)).toBe('100.0'); // swap.amountIn = userQuoteReq.amountIn + expect(formatEther(swapParams.amountOutMinimum)).toBe('961.165048543689320388'); // swap.amountOutMinimum = ourQuoteRes.amountOut - slippage + expect(swapParams.sqrtPriceLimitX96.toString()).toBe('0'); + + expect(swap.transaction.to).toBe(TEST_SECONDARY_FEE_ADDRESS); + expect(swap.transaction.from).toBe(params.fromAddress); + expect(swap.transaction.value).toBe('0x00'); + }); + }); + + describe('with a single pool and secondary fees and a native token in', () => { + it('should include the user-specified amount as the value of the transaction', async () => { + mockRouterImplementation({ + pools: [createPool(nativeTokenService.wrappedToken, FUN_TEST_TOKEN)], + }); + + const secondaryFees: SecondaryFee[] = [ + { recipient: TEST_FEE_RECIPIENT, basisPoints: 100 }, // 1% Fee + ]; + const exchange = new Exchange({ ...TEST_DEX_CONFIGURATION, secondaryFees }); + + const { swap } = await exchange.getUnsignedSwapTxFromAmountIn( + TEST_FROM_ADDRESS, + 'native', + FUN_TEST_TOKEN.address, + newAmountFromString('100', FUN_TEST_TOKEN).value, + ); + + expectToBeDefined(swap.transaction.data); + expectToBeDefined(swap.transaction.value); + const data = swap.transaction.data.toString(); + + const { swapParams } = decodeMulticallExactInputSingleWithFees(data); + expectInstanceOf(BigNumber, swapParams.amountIn); + + expect(swapParams.tokenIn).toBe(WIMX_TEST_TOKEN.address); // should be the wrapped native token + expect(swapParams.tokenOut).toBe(FUN_TEST_TOKEN.address); + expect(swap.transaction.value).toBe('0x056bc75e2d63100000'); // should be a hex + expect(formatTokenAmount(swapParams.amountIn, WIMX_TEST_TOKEN)).toBe('100.0'); // amount in + }); + }); + + describe('with a single pool and a native token in', () => { it('should include the user-specified amount as the value of the transaction', async () => { mockRouterImplementation({ pools: [createPool(nativeTokenService.wrappedToken, FUN_TEST_TOKEN)], @@ -312,7 +528,7 @@ describe('getUnsignedSwapTxFromAmountIn', () => { }); }); - describe('with multiple pools and a native token out', () => { + describe('with multiple pools', () => { describe('with a native token out', () => { it('should specify the Router contract as the recipient of the swap function call', async () => { mockRouterImplementation({ @@ -343,144 +559,6 @@ describe('getUnsignedSwapTxFromAmountIn', () => { }); }); - describe('When the swap transaction requires approval', () => { - it('should include the unsigned approval transaction', async () => { - const params = setupSwapTxTest(); - mockRouterImplementation(params); - const erc20ContractInterface = ERC20__factory.createInterface(); - - const exchange = new Exchange(TEST_DEX_CONFIGURATION); - - const amountIn = addAmount(APPROVED_AMOUNT, newAmountFromString('1', USDC_TEST_TOKEN)); - const tx = await exchange.getUnsignedSwapTxFromAmountIn( - params.fromAddress, - params.inputToken, - params.outputToken, - amountIn.value, - ); - - expectToBeDefined(tx.approval?.transaction.data); - - const decodedResults = erc20ContractInterface.decodeFunctionData('approve', tx.approval.transaction.data); - expect(decodedResults[0]).toEqual(TEST_ROUTER_ADDRESS); - // we have already approved 1000000000000000000 but this is not enough, so we expect to approve the full amount - expect(decodedResults[1].toString()).toEqual(amountIn.value.toString()); - expect(tx.approval.transaction.to).toEqual(params.inputToken); - expect(tx.approval.transaction.from).toEqual(params.fromAddress); - expect(tx.approval.transaction.value).toEqual(0); // we do not want to send any ETH - }); - - it('should include the gas estimate for the approval transaction', async () => { - const params = setupSwapTxTest(); - mockRouterImplementation(params); - - const exchange = new Exchange(TEST_DEX_CONFIGURATION); - - const amountIn = addAmount(APPROVED_AMOUNT, newAmountFromString('1', USDC_TEST_TOKEN)); - const tx = await exchange.getUnsignedSwapTxFromAmountIn( - params.fromAddress, - params.inputToken, - params.outputToken, - amountIn.value, - ); - - expectToBeDefined(tx.approval?.gasFeeEstimate); - expect(tx.approval.gasFeeEstimate.value).toEqual(TEST_GAS_PRICE.mul(APPROVE_GAS_ESTIMATE)); - expect(tx.approval.gasFeeEstimate.token.chainId).toEqual(NATIVE_TEST_TOKEN.chainId); - expect(tx.approval.gasFeeEstimate.token.address).toEqual(''); - expect(tx.approval.gasFeeEstimate.token.decimals).toEqual(NATIVE_TEST_TOKEN.decimals); - expect(tx.approval.gasFeeEstimate.token.symbol).toEqual(NATIVE_TEST_TOKEN.symbol); - expect(tx.approval.gasFeeEstimate.token.name).toEqual(NATIVE_TEST_TOKEN.name); - }); - }); - - describe('When the swap transaction does not require approval', () => { - it('should not include the unsigned approval transaction', async () => { - const params = setupSwapTxTest(); - mockRouterImplementation(params); - - const exchange = new Exchange(TEST_DEX_CONFIGURATION); - - // Set the amountIn to be the same as the APPROVED_AMOUNT - const tx = await exchange.getUnsignedSwapTxFromAmountIn( - params.fromAddress, - params.inputToken, - params.outputToken, - APPROVED_AMOUNT.value, - ); - - // we have already approved 1000000000000000000, so we don't expect to approve anything - expect(tx.approval).toBe(null); - }); - }); - - describe('When no route found', () => { - it('throws NoRoutesAvailableError', async () => { - const params = setupSwapTxTest(); - - (Router as unknown as jest.Mock).mockImplementationOnce(() => ({ - findOptimalRoute: jest.fn().mockRejectedValue(new NoRoutesAvailableError()), - })); - - const exchange = new Exchange(TEST_DEX_CONFIGURATION); - await expect( - exchange.getUnsignedSwapTxFromAmountIn( - params.fromAddress, - params.inputToken, - params.outputToken, - newAmountFromString('100', USDC_TEST_TOKEN).value, - ), - ).rejects.toThrow(new NoRoutesAvailableError()); - }); - }); - - describe('Swap with single pool and secondary fees', () => { - it('generates valid swap calldata', async () => { - const params = setupSwapTxTest(); - const findOptimalRouteMock = mockRouterImplementation(params); - - const secondaryFees: SecondaryFee[] = [ - { recipient: TEST_FEE_RECIPIENT, basisPoints: 100 }, // 1% Fee - ]; - const exchange = new Exchange({ ...TEST_DEX_CONFIGURATION, secondaryFees }); - - const { swap, quote } = await exchange.getUnsignedSwapTxFromAmountIn( - params.fromAddress, - params.inputToken, - params.outputToken, - newAmountFromString('100', USDC_TEST_TOKEN).value, - 3, // 3% Slippage - ); - - expectToBeDefined(swap.transaction.data); - - expect(formatAmount(quote.amountWithMaxSlippage)).toEqual('961.165048543689320388'); // userQuoteRes.amountOutMinimum = swapReq.amountOutMinimum - - const ourQuoteReqAmountIn = findOptimalRouteMock.mock.calls[0][0]; - expect(formatAmount(ourQuoteReqAmountIn)).toEqual('99.0'); // ourQuoteReq.amountIn = the amount specified less the fee - - const data = swap.transaction.data.toString(); - - const { swapParams, secondaryFeeParams } = decodeMulticallExactInputSingleWithFees(data); - expectInstanceOf(BigNumber, swapParams.amountIn); - - expect(secondaryFeeParams[0].recipient).toBe(TEST_FEE_RECIPIENT); - expect(secondaryFeeParams[0].basisPoints.toString()).toBe('100'); - - expect(swapParams.tokenIn).toBe(params.inputToken); - expect(swapParams.tokenOut).toBe(params.outputToken); - expect(swapParams.fee).toBe(10000); - expect(swapParams.recipient).toBe(params.fromAddress); - expect(formatTokenAmount(swapParams.amountIn, USDC_TEST_TOKEN)).toBe('100.0'); // swap.amountIn = userQuoteReq.amountIn - expect(formatEther(swapParams.amountOutMinimum)).toBe('961.165048543689320388'); // swap.amountOutMinimum = ourQuoteRes.amountOut - slippage - expect(swapParams.sqrtPriceLimitX96.toString()).toBe('0'); - - expect(swap.transaction.to).toBe(TEST_SECONDARY_FEE_ADDRESS); - expect(swap.transaction.from).toBe(params.fromAddress); - expect(swap.transaction.value).toBe('0x00'); - }); - }); - describe('Swap with multiple pools and secondary fees', () => { it('generates valid swap calldata', async () => { const params = setupSwapTxTest({ multiPoolSwap: true }); @@ -554,9 +632,45 @@ describe('getUnsignedSwapTxFromAmountIn', () => { ]); }); }); + }); + + describe('Swap with secondary fees and paused secondary fee contract', () => { + it('should use the default router contract with no fees applied to the swap', async () => { + erc20Contract = (Contract as unknown as jest.Mock).mockImplementation(() => ({ + allowance: jest.fn().mockResolvedValue(APPROVED_AMOUNT.value), + estimateGas: { approve: jest.fn().mockResolvedValue(APPROVE_GAS_ESTIMATE) }, + paused: jest.fn().mockResolvedValue(true), + })); + + const params = setupSwapTxTest(); + mockRouterImplementation(params); + + const secondaryFees: SecondaryFee[] = [ + { recipient: TEST_FEE_RECIPIENT, basisPoints: 100 }, // 1% Fee + ]; + const exchange = new Exchange({ ...TEST_DEX_CONFIGURATION, secondaryFees }); + + const { swap, quote } = await exchange.getUnsignedSwapTxFromAmountIn( + params.fromAddress, + params.inputToken, + params.outputToken, + newAmountFromString('100', USDC_TEST_TOKEN).value, + ); + + expectToBeDefined(swap.transaction.data); + + expect(formatAmount(quote.amountWithMaxSlippage)).toEqual('999.000999000999000999'); // min amount out (includes slippage) + expect(quote.fees.length).toBe(0); // expect no fees to be applied + + const data = swap.transaction.data.toString(); + const { swapParams } = decodeMulticallExactInputSingleWithoutFees(data); + + expect(formatEther(swapParams.amountOutMinimum)).toBe(formatEther(quote.amountWithMaxSlippage.value)); + expect(swap.transaction.to).toBe(TEST_ROUTER_ADDRESS); // expect the default router contract to be used + }); - describe('Swap with secondary fees and paused secondary fee contract', () => { - it('should use the default router contract with no fees applied to the swap', async () => { + describe('when the secondary fee contract is unpaused after a swap request', () => { + it('should apply secondary fees to a subsequent swap request', async () => { erc20Contract = (Contract as unknown as jest.Mock).mockImplementation(() => ({ allowance: jest.fn().mockResolvedValue(APPROVED_AMOUNT.value), estimateGas: { approve: jest.fn().mockResolvedValue(APPROVE_GAS_ESTIMATE) }, @@ -571,373 +685,291 @@ describe('getUnsignedSwapTxFromAmountIn', () => { ]; const exchange = new Exchange({ ...TEST_DEX_CONFIGURATION, secondaryFees }); + await exchange.getUnsignedSwapTxFromAmountIn( + params.fromAddress, + params.inputToken, + params.outputToken, + newAmountFromString('100', USDC_TEST_TOKEN).value, + 3, // 3% Slippage + ); + + // Unpause the secondary fee contract + erc20Contract = (Contract as unknown as jest.Mock).mockImplementation(() => ({ + allowance: jest.fn().mockResolvedValue(APPROVED_AMOUNT.value), + estimateGas: { approve: jest.fn().mockResolvedValue(APPROVE_GAS_ESTIMATE) }, + paused: jest.fn().mockResolvedValue(false), + })); + const { swap, quote } = await exchange.getUnsignedSwapTxFromAmountIn( params.fromAddress, params.inputToken, params.outputToken, newAmountFromString('100', USDC_TEST_TOKEN).value, + 3, // 3% Slippage ); expectToBeDefined(swap.transaction.data); - expect(formatAmount(quote.amountWithMaxSlippage)).toEqual('999.000999000999000999'); // min amount out (includes slippage) - expect(quote.fees.length).toBe(0); // expect no fees to be applied + expect(formatAmount(quote.amountWithMaxSlippage)).toEqual('961.165048543689320388'); // min amount out (includes slippage) + expect(quote.fees.length).toBe(1); // expect no fees to be applied const data = swap.transaction.data.toString(); - const { swapParams } = decodeMulticallExactInputSingleWithoutFees(data); + const { swapParams } = decodeMulticallExactInputSingleWithFees(data); expect(formatEther(swapParams.amountOutMinimum)).toBe(formatEther(quote.amountWithMaxSlippage.value)); - expect(swap.transaction.to).toBe(TEST_ROUTER_ADDRESS); // expect the default router contract to be used - }); - - describe('when the secondary fee contract is unpaused after a swap request', () => { - it('should apply secondary fees to a subsequent swap request', async () => { - erc20Contract = (Contract as unknown as jest.Mock).mockImplementation(() => ({ - allowance: jest.fn().mockResolvedValue(APPROVED_AMOUNT.value), - estimateGas: { approve: jest.fn().mockResolvedValue(APPROVE_GAS_ESTIMATE) }, - paused: jest.fn().mockResolvedValue(true), - })); - - const params = setupSwapTxTest(); - mockRouterImplementation(params); - - const secondaryFees: SecondaryFee[] = [ - { recipient: TEST_FEE_RECIPIENT, basisPoints: 100 }, // 1% Fee - ]; - const exchange = new Exchange({ ...TEST_DEX_CONFIGURATION, secondaryFees }); - - await exchange.getUnsignedSwapTxFromAmountIn( - params.fromAddress, - params.inputToken, - params.outputToken, - newAmountFromString('100', USDC_TEST_TOKEN).value, - 3, // 3% Slippage - ); - - // Unpause the secondary fee contract - erc20Contract = (Contract as unknown as jest.Mock).mockImplementation(() => ({ - allowance: jest.fn().mockResolvedValue(APPROVED_AMOUNT.value), - estimateGas: { approve: jest.fn().mockResolvedValue(APPROVE_GAS_ESTIMATE) }, - paused: jest.fn().mockResolvedValue(false), - })); - - const { swap, quote } = await exchange.getUnsignedSwapTxFromAmountIn( - params.fromAddress, - params.inputToken, - params.outputToken, - newAmountFromString('100', USDC_TEST_TOKEN).value, - 3, // 3% Slippage - ); - - expectToBeDefined(swap.transaction.data); - - expect(formatAmount(quote.amountWithMaxSlippage)).toEqual('961.165048543689320388'); // min amount out (includes slippage) - expect(quote.fees.length).toBe(1); // expect no fees to be applied - - const data = swap.transaction.data.toString(); - const { swapParams } = decodeMulticallExactInputSingleWithFees(data); - - expect(formatEther(swapParams.amountOutMinimum)).toBe(formatEther(quote.amountWithMaxSlippage.value)); - expect(swap.transaction.to).toBe(TEST_SECONDARY_FEE_ADDRESS); // expect the secondary fee contract to be used - }); + expect(swap.transaction.to).toBe(TEST_SECONDARY_FEE_ADDRESS); // expect the secondary fee contract to be used }); }); + }); - describe('Swap with single pool without fees and default slippage tolerance', () => { - it('generates valid swap calldata', async () => { - const params = setupSwapTxTest(); - - mockRouterImplementation(params); + describe('When the swap transaction requires approval', () => { + it('should include the unsigned approval transaction', async () => { + const params = setupSwapTxTest(); + mockRouterImplementation(params); + const erc20ContractInterface = ERC20__factory.createInterface(); - const exchange = new Exchange(TEST_DEX_CONFIGURATION); - - const { swap } = await exchange.getUnsignedSwapTxFromAmountIn( - params.fromAddress, - params.inputToken, - params.outputToken, - newAmountFromString('100', USDC_TEST_TOKEN).value, - ); + const exchange = new Exchange(TEST_DEX_CONFIGURATION); - expectToBeDefined(swap.transaction.data); + const amountIn = addAmount(APPROVED_AMOUNT, newAmountFromString('1', USDC_TEST_TOKEN)); + const tx = await exchange.getUnsignedSwapTxFromAmountIn( + params.fromAddress, + params.inputToken, + params.outputToken, + amountIn.value, + ); - const data = swap.transaction.data.toString(); + expectToBeDefined(tx.approval?.transaction.data); - const { swapParams } = decodeMulticallExactInputSingleWithoutFees(data); - expectInstanceOf(BigNumber, swapParams.amountIn); + const decodedResults = erc20ContractInterface.decodeFunctionData('approve', tx.approval.transaction.data); + expect(decodedResults[0]).toEqual(TEST_ROUTER_ADDRESS); + // we have already approved 1000000000000000000 but this is not enough, so we expect to approve the full amount + expect(decodedResults[1].toString()).toEqual(amountIn.value.toString()); + expect(tx.approval.transaction.to).toEqual(params.inputToken); + expect(tx.approval.transaction.from).toEqual(params.fromAddress); + expect(tx.approval.transaction.value).toEqual(0); // we do not want to send any ETH + }); - expect(swapParams.tokenIn).toBe(params.inputToken); // input token - expect(swapParams.tokenOut).toBe(params.outputToken); // output token - expect(swapParams.fee).toBe(10000); // fee - expect(swapParams.recipient).toBe(params.fromAddress); // recipient - expect(swap.transaction.to).toBe(TEST_ROUTER_ADDRESS); // to address - expect(swap.transaction.from).toBe(params.fromAddress); // from address - expect(swap.transaction.value).toBe('0x00'); // refers to 0ETH - expect(formatTokenAmount(swapParams.amountIn, USDC_TEST_TOKEN)).toBe('100.0'); // amount in - expect(formatEther(swapParams.amountOutMinimum)).toBe('999.000999000999000999'); // min amount out (includes slippage) - expect(swapParams.sqrtPriceLimitX96.toString()).toBe('0'); // sqrtPriceX96Limit - }); + it('should include the gas estimate for the approval transaction', async () => { + const params = setupSwapTxTest(); + mockRouterImplementation(params); - it('returns the gas estimate for the swap', async () => { - const params = setupSwapTxTest(); + const exchange = new Exchange(TEST_DEX_CONFIGURATION); - mockRouterImplementation(params); + const amountIn = addAmount(APPROVED_AMOUNT, newAmountFromString('1', USDC_TEST_TOKEN)); + const tx = await exchange.getUnsignedSwapTxFromAmountIn( + params.fromAddress, + params.inputToken, + params.outputToken, + amountIn.value, + ); - const exchange = new Exchange(TEST_DEX_CONFIGURATION); + expectToBeDefined(tx.approval?.gasFeeEstimate); + expect(tx.approval.gasFeeEstimate.value).toEqual(TEST_GAS_PRICE.mul(APPROVE_GAS_ESTIMATE)); + expect(tx.approval.gasFeeEstimate.token.chainId).toEqual(NATIVE_TEST_TOKEN.chainId); + expect(tx.approval.gasFeeEstimate.token.address).toEqual(''); + expect(tx.approval.gasFeeEstimate.token.decimals).toEqual(NATIVE_TEST_TOKEN.decimals); + expect(tx.approval.gasFeeEstimate.token.symbol).toEqual(NATIVE_TEST_TOKEN.symbol); + expect(tx.approval.gasFeeEstimate.token.name).toEqual(NATIVE_TEST_TOKEN.name); + }); + }); - const tx = await exchange.getUnsignedSwapTxFromAmountIn( - params.fromAddress, - params.inputToken, - params.outputToken, - newAmountFromString('100', USDC_TEST_TOKEN).value, - ); + describe('When the swap transaction does not require approval', () => { + it('should not include the unsigned approval transaction', async () => { + const params = setupSwapTxTest(); + mockRouterImplementation(params); - expectToBeDefined(tx.swap.gasFeeEstimate); + const exchange = new Exchange(TEST_DEX_CONFIGURATION); - expect(tx.swap.gasFeeEstimate.value).toEqual(TEST_TRANSACTION_GAS_USAGE.mul(TEST_GAS_PRICE)); - expect(tx.swap.gasFeeEstimate.token.chainId).toEqual(NATIVE_TEST_TOKEN.chainId); - expect(tx.swap.gasFeeEstimate.token.address).toEqual(''); // Default configuration is a native token for gas and not an ERC20 - expect(tx.swap.gasFeeEstimate.token.decimals).toEqual(NATIVE_TEST_TOKEN.decimals); - expect(tx.swap.gasFeeEstimate.token.symbol).toEqual(NATIVE_TEST_TOKEN.symbol); - expect(tx.swap.gasFeeEstimate.token.name).toEqual(NATIVE_TEST_TOKEN.name); - }); + // Set the amountIn to be the same as the APPROVED_AMOUNT + const tx = await exchange.getUnsignedSwapTxFromAmountIn( + params.fromAddress, + params.inputToken, + params.outputToken, + APPROVED_AMOUNT.value, + ); - it('returns valid quote', async () => { - const params = setupSwapTxTest(); + // we have already approved 1000000000000000000, so we don't expect to approve anything + expect(tx.approval).toBe(null); + }); + }); - mockRouterImplementation(params); + describe('When no route found', () => { + it('throws NoRoutesAvailableError', async () => { + const params = setupSwapTxTest(); - const exchange = new Exchange(TEST_DEX_CONFIGURATION); + (Router as unknown as jest.Mock).mockImplementationOnce(() => ({ + findOptimalRoute: jest.fn().mockRejectedValue(new NoRoutesAvailableError()), + })); - const { quote } = await exchange.getUnsignedSwapTxFromAmountIn( + const exchange = new Exchange(TEST_DEX_CONFIGURATION); + await expect( + exchange.getUnsignedSwapTxFromAmountIn( params.fromAddress, params.inputToken, params.outputToken, newAmountFromString('100', USDC_TEST_TOKEN).value, - ); - - expect(quote).not.toBe(undefined); - expect(quote.amount.token.address).toEqual(params.outputToken); - expect(quote.slippage).toBe(0.1); - expect(formatAmount(quote.amount)).toEqual('1000.0'); - expect(quote.amountWithMaxSlippage.token.address).toEqual(params.outputToken); - expect(formatAmount(quote.amountWithMaxSlippage)).toEqual('999.000999000999000999'); // includes slippage - }); + ), + ).rejects.toThrow(new NoRoutesAvailableError()); }); + }); - describe('Swap with single pool without fees and high slippage tolerance', () => { - it('generates valid calldata', async () => { - const params = setupSwapTxTest(); - mockRouterImplementation(params); + describe('Pass in zero address', () => { + it('throws InvalidAddressError', async () => { + const params = setupSwapTxTest(); - const exchange = new Exchange(TEST_DEX_CONFIGURATION); + const exchange = new Exchange(TEST_DEX_CONFIGURATION); - const { swap } = await exchange.getUnsignedSwapTxFromAmountIn( - params.fromAddress, + const invalidAddress = constants.AddressZero; + + await expect( + exchange.getUnsignedSwapTxFromAmountIn( + invalidAddress, params.inputToken, params.outputToken, newAmountFromString('100', USDC_TEST_TOKEN).value, HIGHER_SLIPPAGE, - ); + ), + ).rejects.toThrow(new InvalidAddressError('Error: invalid from address')); - expectToBeDefined(swap.transaction.data); - - const data = swap.transaction.data.toString(); - - const { swapParams } = decodeMulticallExactInputSingleWithoutFees(data); - expectInstanceOf(BigNumber, swapParams.amountIn); - - expect(swapParams.tokenIn).toBe(params.inputToken); // input token - expect(swapParams.tokenOut).toBe(params.outputToken); // output token - expect(swapParams.fee).toBe(10000); // fee - expect(swapParams.recipient).toBe(params.fromAddress); // recipient - expect(swap.transaction.to).toBe(TEST_ROUTER_ADDRESS); // to address - expect(swap.transaction.from).toBe(params.fromAddress); // from address - expect(swap.transaction.value).toBe('0x00'); // refers to 0ETH - expect(formatTokenAmount(swapParams.amountIn, USDC_TEST_TOKEN)).toBe('100.0'); // amount in - expect(formatEther(swapParams.amountOutMinimum)).toBe('998.003992015968063872'); // min amount out (includes 0.2% slippage) - expect(swapParams.sqrtPriceLimitX96.toString()).toBe('0'); // sqrtPriceX96Limit - }); - - it('returns valid quote', async () => { - const params = setupSwapTxTest(); - mockRouterImplementation(params); - - const exchange = new Exchange(TEST_DEX_CONFIGURATION); - - const { quote } = await exchange.getUnsignedSwapTxFromAmountIn( + await expect( + exchange.getUnsignedSwapTxFromAmountIn( params.fromAddress, - params.inputToken, + invalidAddress, params.outputToken, newAmountFromString('100', USDC_TEST_TOKEN).value, HIGHER_SLIPPAGE, - ); + ), + ).rejects.toThrow(new InvalidAddressError('Error: invalid token in address')); - expect(quote.amount.token.address).toEqual(params.outputToken); - expect(quote.slippage).toBe(0.2); - expect(formatAmount(quote.amount)).toEqual('1000.0'); - expect(quote.amountWithMaxSlippage.token.address).toEqual(params.outputToken); - expect(formatAmount(quote.amountWithMaxSlippage)).toEqual('998.003992015968063872'); // includes 0.2% slippage - }); + await expect( + exchange.getUnsignedSwapTxFromAmountIn( + params.fromAddress, + params.inputToken, + invalidAddress, + newAmountFromString('100', USDC_TEST_TOKEN).value, + HIGHER_SLIPPAGE, + ), + ).rejects.toThrow(new InvalidAddressError('Error: invalid token out address')); }); + }); - describe('Pass in zero address', () => { - it('throws InvalidAddressError', async () => { - const params = setupSwapTxTest(); + describe('Pass in invalid addresses', () => { + it('throws InvalidAddressError', async () => { + const params = setupSwapTxTest(); - const exchange = new Exchange(TEST_DEX_CONFIGURATION); + const exchange = new Exchange(TEST_DEX_CONFIGURATION); - const invalidAddress = constants.AddressZero; - - await expect( - exchange.getUnsignedSwapTxFromAmountIn( - invalidAddress, - params.inputToken, - params.outputToken, - newAmountFromString('100', USDC_TEST_TOKEN).value, - HIGHER_SLIPPAGE, - ), - ).rejects.toThrow(new InvalidAddressError('Error: invalid from address')); - - await expect( - exchange.getUnsignedSwapTxFromAmountIn( - params.fromAddress, - invalidAddress, - params.outputToken, - newAmountFromString('100', USDC_TEST_TOKEN).value, - HIGHER_SLIPPAGE, - ), - ).rejects.toThrow(new InvalidAddressError('Error: invalid token in address')); - - await expect( - exchange.getUnsignedSwapTxFromAmountIn( - params.fromAddress, - params.inputToken, - invalidAddress, - newAmountFromString('100', USDC_TEST_TOKEN).value, - HIGHER_SLIPPAGE, - ), - ).rejects.toThrow(new InvalidAddressError('Error: invalid token out address')); - }); - }); + const invalidAddress = '0x0123abcdef'; - describe('Pass in invalid addresses', () => { - it('throws InvalidAddressError', async () => { - const params = setupSwapTxTest(); + await expect( + exchange.getUnsignedSwapTxFromAmountIn( + invalidAddress, + params.inputToken, + params.outputToken, + newAmountFromString('100', USDC_TEST_TOKEN).value, + HIGHER_SLIPPAGE, + ), + ).rejects.toThrow(new InvalidAddressError('Error: invalid from address')); - const exchange = new Exchange(TEST_DEX_CONFIGURATION); + await expect( + exchange.getUnsignedSwapTxFromAmountIn( + params.fromAddress, + invalidAddress, + params.outputToken, + newAmountFromString('100', USDC_TEST_TOKEN).value, + HIGHER_SLIPPAGE, + ), + ).rejects.toThrow(new InvalidAddressError('Error: invalid token in address')); - const invalidAddress = '0x0123abcdef'; - - await expect( - exchange.getUnsignedSwapTxFromAmountIn( - invalidAddress, - params.inputToken, - params.outputToken, - newAmountFromString('100', USDC_TEST_TOKEN).value, - HIGHER_SLIPPAGE, - ), - ).rejects.toThrow(new InvalidAddressError('Error: invalid from address')); - - await expect( - exchange.getUnsignedSwapTxFromAmountIn( - params.fromAddress, - invalidAddress, - params.outputToken, - newAmountFromString('100', USDC_TEST_TOKEN).value, - HIGHER_SLIPPAGE, - ), - ).rejects.toThrow(new InvalidAddressError('Error: invalid token in address')); - - await expect( - exchange.getUnsignedSwapTxFromAmountIn( - params.fromAddress, - params.inputToken, - invalidAddress, - newAmountFromString('100', USDC_TEST_TOKEN).value, - HIGHER_SLIPPAGE, - ), - ).rejects.toThrow(new InvalidAddressError('Error: invalid token out address')); - }); + await expect( + exchange.getUnsignedSwapTxFromAmountIn( + params.fromAddress, + params.inputToken, + invalidAddress, + newAmountFromString('100', USDC_TEST_TOKEN).value, + HIGHER_SLIPPAGE, + ), + ).rejects.toThrow(new InvalidAddressError('Error: invalid token out address')); }); + }); - describe('Pass in maxHops > 10', () => { - it('throws InvalidMaxHopsError', async () => { - const params = setupSwapTxTest(); - mockRouterImplementation(params); + describe('Pass in maxHops > 10', () => { + it('throws InvalidMaxHopsError', async () => { + const params = setupSwapTxTest(); + mockRouterImplementation(params); - const exchange = new Exchange(TEST_DEX_CONFIGURATION); + const exchange = new Exchange(TEST_DEX_CONFIGURATION); - await expect( - exchange.getUnsignedSwapTxFromAmountIn( - params.fromAddress, - params.inputToken, - params.outputToken, - newAmountFromString('100', USDC_TEST_TOKEN).value, - HIGHER_SLIPPAGE, - 11, - ), - ).rejects.toThrow(new InvalidMaxHopsError('Error: max hops must be less than or equal to 10')); - }); + await expect( + exchange.getUnsignedSwapTxFromAmountIn( + params.fromAddress, + params.inputToken, + params.outputToken, + newAmountFromString('100', USDC_TEST_TOKEN).value, + HIGHER_SLIPPAGE, + 11, + ), + ).rejects.toThrow(new InvalidMaxHopsError('Error: max hops must be less than or equal to 10')); }); + }); - describe('Pass in maxHops < 1', () => { - it('throws InvalidMaxHopsError', async () => { - const params = setupSwapTxTest(); - mockRouterImplementation(params); + describe('Pass in maxHops < 1', () => { + it('throws InvalidMaxHopsError', async () => { + const params = setupSwapTxTest(); + mockRouterImplementation(params); - const exchange = new Exchange(TEST_DEX_CONFIGURATION); + const exchange = new Exchange(TEST_DEX_CONFIGURATION); - await expect( - exchange.getUnsignedSwapTxFromAmountIn( - params.fromAddress, - params.inputToken, - params.outputToken, - newAmountFromString('100', USDC_TEST_TOKEN).value, - HIGHER_SLIPPAGE, - 0, - ), - ).rejects.toThrow(new InvalidMaxHopsError('Error: max hops must be greater than or equal to 1')); - }); + await expect( + exchange.getUnsignedSwapTxFromAmountIn( + params.fromAddress, + params.inputToken, + params.outputToken, + newAmountFromString('100', USDC_TEST_TOKEN).value, + HIGHER_SLIPPAGE, + 0, + ), + ).rejects.toThrow(new InvalidMaxHopsError('Error: max hops must be greater than or equal to 1')); }); + }); - describe('With slippage greater than 50', () => { - it('throws InvalidSlippageError', async () => { - const params = setupSwapTxTest(); - mockRouterImplementation(params); + describe('With slippage greater than 50', () => { + it('throws InvalidSlippageError', async () => { + const params = setupSwapTxTest(); + mockRouterImplementation(params); - const exchange = new Exchange(TEST_DEX_CONFIGURATION); + const exchange = new Exchange(TEST_DEX_CONFIGURATION); - await expect( - exchange.getUnsignedSwapTxFromAmountIn( - params.fromAddress, - params.inputToken, - params.outputToken, - newAmountFromString('100', USDC_TEST_TOKEN).value, - 100, - 2, - ), - ).rejects.toThrow(new InvalidSlippageError('Error: slippage percent must be less than or equal to 50')); - }); + await expect( + exchange.getUnsignedSwapTxFromAmountIn( + params.fromAddress, + params.inputToken, + params.outputToken, + newAmountFromString('100', USDC_TEST_TOKEN).value, + 100, + 2, + ), + ).rejects.toThrow(new InvalidSlippageError('Error: slippage percent must be less than or equal to 50')); }); + }); - describe('With slippage less than 0', () => { - it('throws InvalidSlippageError', async () => { - const params = setupSwapTxTest(); - mockRouterImplementation(params); + describe('With slippage less than 0', () => { + it('throws InvalidSlippageError', async () => { + const params = setupSwapTxTest(); + mockRouterImplementation(params); - const exchange = new Exchange(TEST_DEX_CONFIGURATION); + const exchange = new Exchange(TEST_DEX_CONFIGURATION); - await expect( - exchange.getUnsignedSwapTxFromAmountIn( - params.fromAddress, - params.inputToken, - params.outputToken, - newAmountFromString('100', USDC_TEST_TOKEN).value, - -5, - 2, - ), - ).rejects.toThrow(new InvalidSlippageError('Error: slippage percent must be greater than or equal to 0')); - }); + await expect( + exchange.getUnsignedSwapTxFromAmountIn( + params.fromAddress, + params.inputToken, + params.outputToken, + newAmountFromString('100', USDC_TEST_TOKEN).value, + -5, + 2, + ), + ).rejects.toThrow(new InvalidSlippageError('Error: slippage percent must be greater than or equal to 0')); }); }); });