Skip to content

Commit 314c6e8

Browse files
ewilzlint-action
andauthored
encode refundETH on swaps with very high slippage with ETH input token. (Uniswap#28)
* add option to refundETH when encoding a swap * Fix code style issues with Prettier * add refundETH for high slippage ETH trades * Fix code style issues with Prettier * mismatched paren * unit test * address PR comments Co-authored-by: Lint Action <[email protected]>
1 parent 4cff8c6 commit 314c6e8

File tree

2 files changed

+91
-16
lines changed

2 files changed

+91
-16
lines changed

src/swapRouter.test.ts

+59-4
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe('SwapRouter', () => {
2727
const liquidity = 1_000_000
2828

2929
// v3
30-
const makePool = (token0: Token, token1: Token) => {
30+
const makePool = (token0: Token, token1: Token, liquidity: number) => {
3131
return new Pool(token0, token1, feeAmount, sqrtRatioX96, liquidity, TickMath.getTickAtSqrtRatio(sqrtRatioX96), [
3232
{
3333
index: nearestUsableTick(TickMath.MIN_TICK, TICK_SPACINGS[feeAmount]),
@@ -50,10 +50,10 @@ describe('SwapRouter', () => {
5050
return new Pair(amount0, amount1)
5151
}
5252

53-
const pool_0_1 = makePool(token0, token1)
53+
const pool_0_1 = makePool(token0, token1, liquidity)
5454
const pair_0_1 = makePair(token0, token1, liquidity)
5555

56-
const pool_1_WETH = makePool(token1, WETH)
56+
const pool_1_WETH = makePool(token1, WETH, liquidity)
5757
const pair_1_WETH = makePair(token1, WETH, liquidity)
5858

5959
const slippageTolerance = new Percent(1, 100)
@@ -465,6 +465,60 @@ describe('SwapRouter', () => {
465465
})
466466
})
467467
})
468+
469+
describe('high price impact with ETH input to result in refundETH being appended to calldata', () => {
470+
const expectedCalldata =
471+
'0x5ae401dc000000000000000000000000000000000000000000000000000000000000007b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000000e4472b43f300000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000062000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000412210e8a00000000000000000000000000000000000000000000000000000000'
472+
const amountIn = CurrencyAmount.fromRawAmount(ETHER, JSBI.BigInt(100))
473+
const pool_1_WETH_slippage = makePool(token1, WETH, 100)
474+
const REFUND_ETH_FUNCTION_SIG = /12210e8a/
475+
476+
const v2Trade = V2Trade.exactIn(new V2Route([pair_1_WETH], ETHER, token1), amountIn)
477+
const v3Trade = V3Trade.fromRoute(
478+
new V3Route([pool_1_WETH_slippage], ETHER, token1),
479+
amountIn,
480+
TradeType.EXACT_INPUT
481+
)
482+
483+
it('array of trades', async () => {
484+
const trades = [v2Trade, await v3Trade]
485+
const { calldata, value } = SwapRouter.swapCallParameters(trades, {
486+
slippageTolerance,
487+
recipient,
488+
deadlineOrPreviousBlockhash: deadline,
489+
})
490+
expect(calldata).toEqual(expectedCalldata)
491+
expect(calldata).toMatch(REFUND_ETH_FUNCTION_SIG)
492+
expect(value).toBe('0xc8')
493+
})
494+
})
495+
496+
describe('high price impact with ERCO20 input does not result in refundETH call', () => {
497+
const expectedCalldata =
498+
'0x5ae401dc000000000000000000000000000000000000000000000000000000000000007b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000e4472b43f3000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000620000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e404e45aaf0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
499+
const amountIn = CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(100))
500+
const pool_1_WETH_slippage = makePool(token1, WETH, 100)
501+
const REFUND_ETH_FUNCTION_SIG = /12210e8a/
502+
503+
const v2Trade = V2Trade.exactIn(new V2Route([pair_1_WETH], token1, WETH), amountIn)
504+
const v3Trade = V3Trade.fromRoute(
505+
new V3Route([pool_1_WETH_slippage], token1, WETH),
506+
amountIn,
507+
TradeType.EXACT_INPUT
508+
)
509+
510+
it('array of trades', async () => {
511+
const trades = [v2Trade, await v3Trade]
512+
const { calldata, value } = SwapRouter.swapCallParameters(trades, {
513+
slippageTolerance,
514+
recipient,
515+
deadlineOrPreviousBlockhash: deadline,
516+
})
517+
expect(calldata).toEqual(expectedCalldata)
518+
expect(calldata).not.toMatch(REFUND_ETH_FUNCTION_SIG)
519+
expect(value).toBe('0x00')
520+
})
521+
})
468522
})
469523

470524
describe('ETH output', () => {
@@ -710,7 +764,7 @@ describe('SwapRouter', () => {
710764
const expectedCalldata =
711765
'0xac9650d80000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000003800000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000048000000000000000000000000000000000000000000000000000000000000005c000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000104472b43f30000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000006100000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000124b858183f0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000005f00000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000001000bb80000000000000000000000000000000000000002000bb8c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044f2d5d56b000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000032c7eb000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044f2d5d56b0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000032c8ad00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010411ed56c90000000000000000000000000000000000000000000000000000000000000001000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000bb8ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc4000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e90a182f00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e90a182f000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
712766

713-
const pool_0_WETH = makePool(token0, WETH)
767+
const pool_0_WETH = makePool(token0, WETH, liquidity)
714768
const amountIn = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100))
715769
const v2Trade = V2Trade.exactIn(new V2Route([pair_0_1, pair_1_WETH], token0, WETH), amountIn)
716770
const v3Trade = V3Trade.fromRoute(
@@ -907,6 +961,7 @@ describe('SwapRouter', () => {
907961
describe('when input is native', () => {
908962
const expectedCalldata =
909963
'0xac9650d8000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000002a00000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000036000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000004a0000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044f2d5d56b0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000032c8a50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000241c58db4f000000000000000000000000000000000000000000000000000000000032c8ad000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024571ac8b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024571ac8b000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a4f100b2050000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024496169970000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e90a182f0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
964+
910965
const ETH = Ether.onChain(1)
911966
const amountIn = CurrencyAmount.fromRawAmount(ETH, JSBI.BigInt(10))
912967
const v3Trade = V3Trade.fromRoute(new V3Route([pool_1_WETH], ETH, token1), amountIn, TradeType.EXACT_INPUT)

src/swapRouter.ts

+32-12
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { MulticallExtended, Validation } from './multicallExtended'
2424
import { PaymentsExtended } from './paymentsExtended'
2525

2626
const ZERO = JSBI.BigInt(0)
27+
const REFUND_ETH_PRICE_IMPACT_THRESHOLD = new Percent(JSBI.BigInt(50), JSBI.BigInt(100))
2728

2829
/**
2930
* Options for producing the arguments to send calls to the router.
@@ -62,6 +63,12 @@ export interface SwapAndAddOptions extends SwapOptions {
6263
outputTokenPermit?: PermitOptions
6364
}
6465

66+
type AnyTradeType =
67+
| Trade<Currency, Currency, TradeType>
68+
| V2Trade<Currency, Currency, TradeType>
69+
| V3Trade<Currency, Currency, TradeType>
70+
| (V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType>)[]
71+
6572
/**
6673
* Represents the Uniswap V2 + V3 SwapRouter02, and has static methods for helping execute trades.
6774
*/
@@ -176,11 +183,7 @@ export abstract class SwapRouter {
176183
}
177184

178185
private static encodeSwaps(
179-
trades:
180-
| Trade<Currency, Currency, TradeType>
181-
| V2Trade<Currency, Currency, TradeType>
182-
| V3Trade<Currency, Currency, TradeType>
183-
| (V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType>)[],
186+
trades: AnyTradeType,
184187
options: SwapOptions,
185188
isSwapAndAdd?: boolean
186189
): {
@@ -357,8 +360,9 @@ export abstract class SwapRouter {
357360
}
358361
}
359362

360-
// must refund when paying in ETH, but with an uncertain input amount
361-
if (inputIsNative && sampleTrade.tradeType === TradeType.EXACT_OUTPUT) {
363+
// must refund when paying in ETH: either with an uncertain input amount OR if there's a chance of a partial fill.
364+
// unlike ERC20's, the full ETH value must be sent in the transaction, so the rest must be refunded.
365+
if (inputIsNative && (sampleTrade.tradeType === TradeType.EXACT_OUTPUT || SwapRouter.riskOfPartialFill(trades))) {
362366
calldatas.push(Payments.encodeRefundETH())
363367
}
364368

@@ -374,11 +378,7 @@ export abstract class SwapRouter {
374378
* @param options options for the call parameters
375379
*/
376380
public static swapAndAddCallParameters(
377-
trades:
378-
| Trade<Currency, Currency, TradeType>
379-
| V2Trade<Currency, Currency, TradeType>
380-
| V3Trade<Currency, Currency, TradeType>
381-
| (V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType>)[],
381+
trades: AnyTradeType,
382382
options: SwapAndAddOptions,
383383
position: Position,
384384
addLiquidityOptions: CondensedAddLiquidityOptions,
@@ -469,6 +469,26 @@ export abstract class SwapRouter {
469469
}
470470
}
471471

472+
// if price impact is very high, there's a chance of hitting max/min prices resulting in a partial fill of the swap
473+
private static riskOfPartialFill(trades: AnyTradeType): boolean {
474+
if (Array.isArray(trades)) {
475+
return trades.some((trade) => {
476+
return SwapRouter.v3TradeWithHighPriceImpact(trade)
477+
})
478+
} else {
479+
return SwapRouter.v3TradeWithHighPriceImpact(trades)
480+
}
481+
}
482+
483+
private static v3TradeWithHighPriceImpact(
484+
trade:
485+
| Trade<Currency, Currency, TradeType>
486+
| V2Trade<Currency, Currency, TradeType>
487+
| V3Trade<Currency, Currency, TradeType>
488+
): boolean {
489+
return !(trade instanceof V2Trade) && trade.priceImpact.greaterThan(REFUND_ETH_PRICE_IMPACT_THRESHOLD)
490+
}
491+
472492
private static getPositionAmounts(
473493
position: Position,
474494
zeroForOne: boolean

0 commit comments

Comments
 (0)