diff --git a/src/entities/protocols/uniswap.ts b/src/entities/protocols/uniswap.ts index e93dffe6..f7262e4d 100644 --- a/src/entities/protocols/uniswap.ts +++ b/src/entities/protocols/uniswap.ts @@ -20,12 +20,18 @@ import { Currency, TradeType, CurrencyAmount, Percent } from '@uniswap/sdk-core' import { Command, RouterTradeType, TradeConfig } from '../Command' import { SENDER_AS_RECIPIENT, ROUTER_AS_RECIPIENT, CONTRACT_BALANCE } from '../../utils/constants' import { encodeFeeBips } from '../../utils/numbers' -import { BigNumber } from 'ethers' +import { BigNumber, BigNumberish } from 'ethers' + +export type FlatFeeOptions = { + amount: BigNumberish + recipient: string +} // the existing router permit object doesn't include enough data for permit2 // so we extend swap options with the permit2 permit export type SwapOptions = Omit & { inputTokenPermit?: Permit2Permit + flatFee?: FlatFeeOptions } const REFUND_ETH_PRICE_IMPACT_THRESHOLD = new Percent(50, 100) @@ -40,7 +46,9 @@ interface Swap { // also translates trade objects from previous (v2, v3) SDKs export class UniswapTrade implements Command { readonly tradeType: RouterTradeType = RouterTradeType.UniswapTrade - constructor(public trade: RouterTrade, public options: SwapOptions) {} + constructor(public trade: RouterTrade, public options: SwapOptions) { + if (!!options.fee && !!options.flatFee) throw new Error('Only one fee option permitted') + } encode(planner: RoutePlanner, _config: TradeConfig): void { let payerIsUser = true @@ -66,7 +74,7 @@ export class UniswapTrade implements Command { this.trade.tradeType === TradeType.EXACT_INPUT && this.trade.routes.length > 2 const outputIsNative = this.trade.outputAmount.currency.isNative const inputIsNative = this.trade.inputAmount.currency.isNative - const routerMustCustody = performAggregatedSlippageCheck || outputIsNative || !!this.options.fee + const routerMustCustody = performAggregatedSlippageCheck || outputIsNative || hasFeeOption(this.options) for (const swap of this.trade.swaps) { switch (swap.route.protocol) { @@ -107,6 +115,25 @@ export class UniswapTrade implements Command { } } + // If there is a flat fee, that absolute amount is sent to the fee recipient + // In the case where ETH is the output currency, the fee is taken in WETH (for gas reasons) + if (!!this.options.flatFee) { + const feeAmount = this.options.flatFee.amount + if (minimumAmountOut.lt(feeAmount)) throw new Error('Flat fee amount greater than minimumAmountOut') + + planner.addCommand(CommandType.TRANSFER, [ + this.trade.outputAmount.currency.wrapped.address, + this.options.flatFee.recipient, + feeAmount, + ]) + + // If the trade is exact output, and a fee was taken, we must adjust the amount out to be the amount after the fee + // Otherwise we continue as expected with the trade's normal expected output + if (this.trade.tradeType === TradeType.EXACT_OUTPUT) { + minimumAmountOut = minimumAmountOut.sub(feeAmount) + } + } + // The remaining tokens that need to be sent to the user after the fee is taken will be caught // by this if-else clause. if (outputIsNative) { @@ -289,3 +316,7 @@ function addMixedSwap( function riskOfPartialFill(trade: RouterTrade): boolean { return trade.priceImpact.greaterThan(REFUND_ETH_PRICE_IMPACT_THRESHOLD) } + +function hasFeeOption(swapOptions: SwapOptions): boolean { + return !!swapOptions.fee || !!swapOptions.flatFee +} diff --git a/test/forge/SwapERC20CallParameters.t.sol b/test/forge/SwapERC20CallParameters.t.sol index 1f8f6c39..bf9ca41a 100644 --- a/test/forge/SwapERC20CallParameters.t.sol +++ b/test/forge/SwapERC20CallParameters.t.sol @@ -244,6 +244,46 @@ contract SwapERC20CallParametersTest is Test, Interop, DeployRouter { assertEq(address(router).balance, 0); } + function testV2ExactOutputSingleNativeInputWithFlatFee() public { + MethodParameters memory params = readFixture(json, "._UNISWAP_V2_ETH_FOR_1000_USDC_WITH_FLAT_FEE"); + + uint256 outputAmount = 1000 * ONE_USDC; + uint256 feeAmount = 50 * ONE_USDC; + + assertEq(from.balance, BALANCE); + assertEq(USDC.balanceOf(RECIPIENT), 0); + assertEq(USDC.balanceOf(FEE_RECIPIENT), 0); + + (bool success,) = address(router).call{value: params.value}(params.data); + require(success, "call failed"); + assertLe(from.balance, BALANCE - params.value); + assertEq(USDC.balanceOf(RECIPIENT), outputAmount); + assertEq(USDC.balanceOf(FEE_RECIPIENT), feeAmount); + assertEq(WETH.balanceOf(address(router)), 0); + assertEq(address(router).balance, 0); + } + + function testV2ExactOutputSingleNativeOutputWithFlatFee() public { + MethodParameters memory params = readFixture(json, "._UNISWAP_V2_USCD_FOR_10_ETH_WITH_FLAT_FEE"); + + deal(address(USDC), from, BALANCE); + USDC.approve(address(permit2), BALANCE); + permit2.approve(address(USDC), address(router), uint160(BALANCE), uint48(block.timestamp + 1000)); + + uint256 outputAmount = 10 ether; + uint256 feeAmount = 5 ether; + + assertEq(WETH.balanceOf(FEE_RECIPIENT), 0); + uint256 recipientBalanceBefore = RECIPIENT.balance; + + (bool success,) = address(router).call{value: params.value}(params.data); + require(success, "call failed"); + assertGt(RECIPIENT.balance - recipientBalanceBefore, outputAmount); // tiny imprecision with exactOut + assertEq(WETH.balanceOf(FEE_RECIPIENT), feeAmount); + assertEq(WETH.balanceOf(address(router)), 0); + assertEq(address(router).balance, 0); + } + function testV2ExactOutputSingleERC20() public { MethodParameters memory params = readFixture(json, "._UNISWAP_V2_USDC_FOR_1_ETH"); @@ -292,6 +332,26 @@ contract SwapERC20CallParametersTest is Test, Interop, DeployRouter { assertGt(totalOut, 1000 * ONE_USDC); } + function testV3ExactInputSingleNativeWithFlatFee() public { + MethodParameters memory params = readFixture(json, "._UNISWAP_V3_1_ETH_FOR_USDC_WITH_FLAT_FEE"); + + assertEq(from.balance, BALANCE); + assertEq(USDC.balanceOf(RECIPIENT), 0); + assertEq(USDC.balanceOf(FEE_RECIPIENT), 0); + + (bool success,) = address(router).call{value: params.value}(params.data); + require(success, "call failed"); + assertLe(from.balance, BALANCE - params.value); + + uint256 recipientBalance = USDC.balanceOf(RECIPIENT); + uint256 feeRecipientBalance = USDC.balanceOf(FEE_RECIPIENT); + uint256 totalOut = recipientBalance + feeRecipientBalance; + uint256 expectedFee = 50 * ONE_USDC; + assertEq(feeRecipientBalance, expectedFee); + assertEq(recipientBalance, totalOut - expectedFee); + assertGt(totalOut, 1000 * ONE_USDC); + } + function testV3ExactInputSingleERC20() public { MethodParameters memory params = readFixture(json, "._UNISWAP_V3_1000_USDC_FOR_ETH"); diff --git a/test/forge/interop.json b/test/forge/interop.json index 243c8df2..592fc43e 100644 --- a/test/forge/interop.json +++ b/test/forge/interop.json @@ -200,15 +200,15 @@ "value": "0" }, "_UNISWAP_V2_1_ETH_FOR_USDC_WITH_FEE": { - "calldata": "0x24856bc30000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000040b080604000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000041c2a8d600000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb00000000000000000000000000000000000000000000000000000000000001f40000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000000000000000000000000000000000000000000000000000003e78ed32", + "calldata": "0x24856bc30000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000040b080604000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000041c2a8d600000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb00000000000000000000000000000000000000000000000000000000000001f40000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000000000000000000000000000000000000041c2a8d6", "value": "1000000000000000000" }, "_UNISWAP_V2_1_ETH_FOR_USDC_2_HOP_WITH_FEE": { - "calldata": "0x24856bc30000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000040b080604000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000003b871aa9530194fb7600000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000600000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000000600000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000000000000000000000000000388d2620dba7e72217", + "calldata": "0x24856bc30000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000040b080604000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000003b871aa9530194fb7600000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000600000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000000600000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa00000000000000000000000000000000000000000000003b871aa9530194fb76", "value": "1000000000000000000" }, "_UNISWAP_V2_1000_USDC_FOR_ETH_WITH_WETH_FEE": { - "calldata": "0x24856bc300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000308060c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000a5532d103970df400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb00000000000000000000000000000000000000000000000000000000000001f40000000000000000000000000000000000000000000000000000000000000040000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa00000000000000000000000000000000000000000000000009d0f0469035e6db", + "calldata": "0x24856bc300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000308060c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000a5532d103970df400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb00000000000000000000000000000000000000000000000000000000000001f40000000000000000000000000000000000000000000000000000000000000040000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000000000000000000000000000000a5532d103970df4", "value": "0" }, "_UNISWAP_V2_ETH_FOR_1000_USDC_WITH_FEE": { @@ -216,15 +216,31 @@ "value": "954092494227022159" }, "_UNISWAP_V3_1_ETH_FOR_USDC_WITH_FEE": { - "calldata": "0x24856bc30000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000040b000604000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000041d544a300000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb00000000000000000000000000000000000000000000000000000000000001f40000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000000000000000000000000000000000000000000000000000003e8a9acf", + "calldata": "0x24856bc30000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000040b000604000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000041d544a300000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb00000000000000000000000000000000000000000000000000000000000001f40000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000000000000000000000000000000000000041d544a3", "value": "1000000000000000000" }, "_UNISWAP_V3_1000_USDC_FOR_ETH_WITH_WETH_FEE": { - "calldata": "0x24856bc300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000300060c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000a52678c95398ddb00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb8c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb00000000000000000000000000000000000000000000000000000000000001f40000000000000000000000000000000000000000000000000000000000000040000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa00000000000000000000000000000000000000000000000009ce48c58dc379f7", + "calldata": "0x24856bc300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000300060c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000a52678c95398ddb00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb8c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb00000000000000000000000000000000000000000000000000000000000001f40000000000000000000000000000000000000000000000000000000000000040000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000000000000000000000000000000a52678c95398ddb", "value": "0" }, "_UNISWAP_V3_DAI_FOR_1_ETH_2_HOP_WITH_WETH_FEE": { "calldata": "0x24856bc300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000301060c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000e9bb2d80e8435e500000000000000000000000000000000000000000000004cc45359a454a11b4300000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f46b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb00000000000000000000000000000000000000000000000000000000000001f40000000000000000000000000000000000000000000000000000000000000040000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000000000000000000000000000000de0b6b3a7640000", "value": "0" + }, + "_UNISWAP_V2_ETH_FOR_1000_USDC_WITH_FLAT_FEE": { + "calldata": "0x24856bc30000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000050b0905040c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000002a00000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000c9b7472d7bf686600000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000003e95ba800000000000000000000000000000000000000000000000000c9b7472d7bf686600000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb0000000000000000000000000000000000000000000000000000000002faf0800000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000000000000000000000000000000000000000000000", + "value": "908447786440026214" + }, + "_UNISWAP_V3_1_ETH_FOR_USDC_WITH_FLAT_FEE": { + "calldata": "0x24856bc30000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000040b000504000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000044f7ce0000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb0000000000000000000000000000000000000000000000000000000002faf0800000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000000000000000000000000000000000000044f7ce00", + "value": "1000000000000000000" + }, + "_UNISWAP_V2_USCD_FOR_1_ETH_WITH_FLAT_FEE": { + "calldata": "0x24856bc300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000309050c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000d02ab486cedc0000000000000000000000000000000000000000000000000000000000047ab1042900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb0000000000000000000000000000000000000000000000004563918244f400000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000000000000000000000000000008ac7230489e80000", + "value": "0" + }, + "_UNISWAP_V2_USCD_FOR_10_ETH_WITH_FLAT_FEE": { + "calldata": "0x24856bc300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000309050c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000d02ab486cedc0000000000000000000000000000000000000000000000000000000000047ab1042900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb0000000000000000000000000000000000000000000000004563918244f400000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000000000000000000000000000008ac7230489e80000", + "value": "0" } } diff --git a/test/uniswapTrades.test.ts b/test/uniswapTrades.test.ts index 06292d7e..d4037234 100644 --- a/test/uniswapTrades.test.ts +++ b/test/uniswapTrades.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai' import JSBI from 'jsbi' import { BigNumber, utils, Wallet } from 'ethers' import { expandTo18Decimals } from '../src/utils/numbers' -import { SwapRouter, UniswapTrade } from '../src' +import { SwapRouter, UniswapTrade, FlatFeeOptions } from '../src' import { MixedRouteTrade, MixedRouteSDK } from '@uniswap/router-sdk' import { Trade as V2Trade, Pair, Route as RouteV2 } from '@uniswap/v2-sdk' import { Trade as V3Trade, Route as RouteV3, Pool, FeeOptions } from '@uniswap/v3-sdk' @@ -251,6 +251,40 @@ describe('Uniswap', () => { expect(methodParameters.value).to.eq(methodParametersV2.value) }) + it('encodes a single exactOutput ETH->USDC swap, with a flat fee', async () => { + const outputUSDC = utils.parseUnits('1050', 6).toString() + const trade = new V2Trade( + new RouteV2([WETH_USDC_V2], ETHER, USDC), + CurrencyAmount.fromRawAmount(USDC, outputUSDC), + TradeType.EXACT_OUTPUT + ) + const feeOptions: FlatFeeOptions = { amount: utils.parseUnits('50', 6), recipient: TEST_FEE_RECIPIENT_ADDRESS } + const opts = swapOptions({ flatFee: feeOptions }) + const methodParameters = SwapRouter.swapERC20CallParameters(buildTrade([trade]), opts) + const methodParametersV2 = SwapRouter.swapCallParameters(new UniswapTrade(buildTrade([trade]), opts)) + registerFixture('_UNISWAP_V2_ETH_FOR_1000_USDC_WITH_FLAT_FEE', methodParametersV2) + expect(hexToDecimalString(methodParameters.value)).to.not.equal('0') + expect(methodParameters.calldata).to.eq(methodParametersV2.calldata) + expect(methodParameters.value).to.eq(methodParametersV2.value) + }) + + it('encodes a single exactOutput USDC->ETH swap, with a flat fee', async () => { + const outputUSDC = utils.parseUnits('15', 18).toString() + const trade = new V2Trade( + new RouteV2([WETH_USDC_V2], USDC, ETHER), + CurrencyAmount.fromRawAmount(ETHER, outputUSDC), + TradeType.EXACT_OUTPUT + ) + const feeOptions: FlatFeeOptions = { amount: utils.parseUnits('5', 18), recipient: TEST_FEE_RECIPIENT_ADDRESS } + const opts = swapOptions({ flatFee: feeOptions }) + const methodParameters = SwapRouter.swapERC20CallParameters(buildTrade([trade]), opts) + const methodParametersV2 = SwapRouter.swapCallParameters(new UniswapTrade(buildTrade([trade]), opts)) + registerFixture('_UNISWAP_V2_USCD_FOR_10_ETH_WITH_FLAT_FEE', methodParametersV2) + expect(hexToDecimalString(methodParameters.value)).to.equal('0') + expect(methodParameters.calldata).to.eq(methodParametersV2.calldata) + expect(methodParameters.value).to.eq(methodParametersV2.value) + }) + it('encodes a single exactOutput USDC->ETH swap', async () => { const outputETH = utils.parseEther('1').toString() const trade = new V2Trade( @@ -302,6 +336,23 @@ describe('Uniswap', () => { expect(methodParameters.value).to.eq(methodParametersV2.value) }) + it('encodes a single exactInput ETH->USDC swap, with a flat fee', async () => { + const inputEther = utils.parseEther('1').toString() + const trade = await V3Trade.fromRoute( + new RouteV3([WETH_USDC_V3], ETHER, USDC), + CurrencyAmount.fromRawAmount(ETHER, inputEther), + TradeType.EXACT_INPUT + ) + const feeOptions: FlatFeeOptions = { amount: utils.parseUnits('50', 6), recipient: TEST_FEE_RECIPIENT_ADDRESS } + const opts = swapOptions({ flatFee: feeOptions }) + const methodParameters = SwapRouter.swapERC20CallParameters(buildTrade([trade]), opts) + const methodParametersV2 = SwapRouter.swapCallParameters(new UniswapTrade(buildTrade([trade]), opts)) + registerFixture('_UNISWAP_V3_1_ETH_FOR_USDC_WITH_FLAT_FEE', methodParametersV2) + expect(hexToDecimalString(methodParameters.value)).to.eq(inputEther) + expect(methodParameters.calldata).to.eq(methodParametersV2.calldata) + expect(methodParameters.value).to.eq(methodParametersV2.value) + }) + it('encodes a single exactInput USDC->ETH swap', async () => { const inputUSDC = utils.parseUnits('1000', 6).toString() const trade = await V3Trade.fromRoute( @@ -595,4 +646,35 @@ describe('Uniswap', () => { expect(methodParameters.value).to.eq(methodParametersV2.value) }) }) + + describe('fees', () => { + it('throws if instantiated with a proportional fee and a flat fee', async () => { + const outputUSDC = utils.parseUnits('1050', 6).toString() + const trade = new V2Trade( + new RouteV2([WETH_USDC_V2], ETHER, USDC), + CurrencyAmount.fromRawAmount(USDC, outputUSDC), + TradeType.EXACT_OUTPUT + ) + const proportionalFee: FeeOptions = { fee: new Percent(5, 100), recipient: TEST_FEE_RECIPIENT_ADDRESS } + const feeOptions: FlatFeeOptions = { amount: utils.parseUnits('50', 6), recipient: TEST_FEE_RECIPIENT_ADDRESS } + const opts = swapOptions({ fee: proportionalFee, flatFee: feeOptions }) + expect(function () { + SwapRouter.swapCallParameters(new UniswapTrade(buildTrade([trade]), opts)) + }).to.throw('Only one fee option permitted') + }) + + it('throws if flat fee amount is larger than minimumAmountOut', async () => { + const inputEther = utils.parseEther('1').toString() + const trade = await V3Trade.fromRoute( + new RouteV3([WETH_USDC_V3], ETHER, USDC), + CurrencyAmount.fromRawAmount(ETHER, inputEther), + TradeType.EXACT_INPUT + ) + const feeOptions: FlatFeeOptions = { amount: utils.parseUnits('5000', 6), recipient: TEST_FEE_RECIPIENT_ADDRESS } + const opts = swapOptions({ flatFee: feeOptions }) + expect(function () { + SwapRouter.swapCallParameters(new UniswapTrade(buildTrade([trade]), opts)) + }).to.throw('Flat fee amount greater than minimumAmountOut') + }) + }) })