From 548694345677556822df7cb989dbf342f376e1a8 Mon Sep 17 00:00:00 2001 From: Alice <34962750+hensha256@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:50:27 +0100 Subject: [PATCH] Add v4 to receive (#376) * add v4 to receive function * linting * PR comments * fix and snapshots --- contracts/UniversalRouter.sol | 2 +- contracts/base/Dispatcher.sol | 6 +- test/integration-tests/UniswapMixed.test.ts | 104 +++++++++++++++++- .../__snapshots__/Payments.gas.test.ts.snap | 4 +- .../__snapshots__/Uniswap.gas.test.ts.snap | 18 +-- .../UniversalRouter.gas.test.ts.snap | 2 +- 6 files changed, 114 insertions(+), 22 deletions(-) diff --git a/contracts/UniversalRouter.sol b/contracts/UniversalRouter.sol index 8fb5e726..9c7ab92a 100644 --- a/contracts/UniversalRouter.sol +++ b/contracts/UniversalRouter.sol @@ -66,6 +66,6 @@ contract UniversalRouter is IUniversalRouter, Dispatcher { /// @notice To receive ETH from WETH receive() external payable { - if (msg.sender != address(WETH9)) revert InvalidEthSender(); + if (msg.sender != address(WETH9) && msg.sender != address(poolManager)) revert InvalidEthSender(); } } diff --git a/contracts/base/Dispatcher.sol b/contracts/base/Dispatcher.sol index 3c019303..30a8c61e 100644 --- a/contracts/base/Dispatcher.sol +++ b/contracts/base/Dispatcher.sol @@ -177,12 +177,12 @@ abstract contract Dispatcher is Payments, V2SwapRouter, V3SwapRouter, V4SwapRout } else if (command == Commands.WRAP_ETH) { // equivalent: abi.decode(inputs, (address, uint256)) address recipient; - uint256 amountMin; + uint256 amount; assembly { recipient := calldataload(inputs.offset) - amountMin := calldataload(add(inputs.offset, 0x20)) + amount := calldataload(add(inputs.offset, 0x20)) } - Payments.wrapETH(map(recipient), amountMin); + Payments.wrapETH(map(recipient), amount); } else if (command == Commands.UNWRAP_WETH) { // equivalent: abi.decode(inputs, (address, uint256)) address recipient; diff --git a/test/integration-tests/UniswapMixed.test.ts b/test/integration-tests/UniswapMixed.test.ts index e6e899f8..0c7e6681 100644 --- a/test/integration-tests/UniswapMixed.test.ts +++ b/test/integration-tests/UniswapMixed.test.ts @@ -2,7 +2,7 @@ import type { Contract } from '@ethersproject/contracts' import { Pair } from '@uniswap/v2-sdk' import { expect } from './shared/expect' import { BigNumber } from 'ethers' -import { IPermit2, UniversalRouter } from '../../typechain' +import { IPermit2, PoolManager, PositionManager, UniversalRouter } from '../../typechain' import { abi as TOKEN_ABI } from '../../artifacts/solmate/src/tokens/ERC20.sol/ERC20.json' import { resetFork, WETH, DAI, USDC, USDT, PERMIT2 } from './shared/mainnetForkHelpers' import { @@ -10,9 +10,11 @@ import { ALICE_ADDRESS, CONTRACT_BALANCE, DEADLINE, + ETH_ADDRESS, MAX_UINT, MAX_UINT160, MSG_SENDER, + OPEN_DELTA, SOURCE_MSG_SENDER, SOURCE_ROUTER, } from './shared/constants' @@ -24,9 +26,19 @@ import hre from 'hardhat' import { getPermitBatchSignature } from './shared/protocolHelpers/permit2' import { encodePathExactInput, encodePathExactOutput } from './shared/swapRouter02Helpers' import { executeRouter } from './shared/executeRouter' +import { Actions, V4Planner } from './shared/v4Planner' +import { + addLiquidityToV4Pool, + DAI_USDC, + deployV4PoolManager, + encodeMultihopExactInPath, + ETH_USDC, + initializeV4Pool, + USDC_WETH, +} from './shared/v4Helpers' const { ethers } = hre -describe('Uniswap V2 and V3 Tests:', () => { +describe('Uniswap V2, V3, and V4 Tests:', () => { let alice: SignerWithAddress let bob: SignerWithAddress let router: UniversalRouter @@ -35,6 +47,12 @@ describe('Uniswap V2 and V3 Tests:', () => { let wethContract: Contract let usdcContract: Contract let planner: RoutePlanner + let v4Planner: V4Planner + let v4PoolManager: PoolManager + let v4PositionManager: PositionManager + + // current market ETH price at block + const USD_ETH_PRICE = 3820 beforeEach(async () => { await resetFork() @@ -48,13 +66,20 @@ describe('Uniswap V2 and V3 Tests:', () => { wethContract = new ethers.Contract(WETH.address, TOKEN_ABI, bob) usdcContract = new ethers.Contract(USDC.address, TOKEN_ABI, bob) permit2 = PERMIT2.connect(bob) as IPermit2 - router = (await deployUniversalRouter()) as UniversalRouter + + v4PoolManager = (await deployV4PoolManager()).connect(bob) as PoolManager + router = (await deployUniversalRouter(v4PoolManager.address)).connect(bob) as UniversalRouter + + v4PositionManager = (await ethers.getContractAt('PositionManager', await router.V4_POSITION_MANAGER())).connect( + bob + ) as PositionManager planner = new RoutePlanner() + v4Planner = new V4Planner() // alice gives bob some tokens - await daiContract.connect(alice).transfer(bob.address, expandTo18DecimalsBN(100000)) - await wethContract.connect(alice).transfer(bob.address, expandTo18DecimalsBN(100)) - await usdcContract.connect(alice).transfer(bob.address, expandTo6DecimalsBN(100000)) + await daiContract.connect(alice).transfer(bob.address, expandTo18DecimalsBN(1000000)) + await wethContract.connect(alice).transfer(bob.address, expandTo18DecimalsBN(1000)) + await usdcContract.connect(alice).transfer(bob.address, expandTo6DecimalsBN(50000000)) // Bob max-approves the permit2 contract to access his DAI and WETH await daiContract.connect(bob).approve(permit2.address, MAX_UINT) @@ -65,6 +90,21 @@ describe('Uniswap V2 and V3 Tests:', () => { await permit2.approve(DAI.address, router.address, MAX_UINT160, DEADLINE) await permit2.approve(WETH.address, router.address, MAX_UINT160, DEADLINE) await permit2.approve(USDC.address, router.address, MAX_UINT160, DEADLINE) + + // for setting up pools, bob gives position manager approval on permit2 + await permit2.approve(DAI.address, v4PositionManager.address, MAX_UINT160, DEADLINE) + await permit2.approve(WETH.address, v4PositionManager.address, MAX_UINT160, DEADLINE) + await permit2.approve(USDC.address, v4PositionManager.address, MAX_UINT160, DEADLINE) + + // bob initializes 3 v4 pools + await initializeV4Pool(v4PoolManager, USDC_WETH.poolKey, USDC_WETH.price) + await initializeV4Pool(v4PoolManager, DAI_USDC.poolKey, DAI_USDC.price) + await initializeV4Pool(v4PoolManager, ETH_USDC.poolKey, ETH_USDC.price) + + // bob adds liquidity to the pools + await addLiquidityToV4Pool(v4PositionManager, USDC_WETH, expandTo18DecimalsBN(2).toString(), bob) + await addLiquidityToV4Pool(v4PositionManager, DAI_USDC, expandTo18DecimalsBN(400).toString(), bob) + await addLiquidityToV4Pool(v4PositionManager, ETH_USDC, expandTo18DecimalsBN(0.1).toString(), bob) }) describe('Interleaving routes', () => { @@ -516,6 +556,58 @@ describe('Uniswap V2 and V3 Tests:', () => { expect(ethBalanceAfter.sub(ethBalanceBefore)).to.eq(fullAmountOut.sub(gasSpent)) }) + it('ERC20 --> ERC20 split V4 and V4 different routes, with wrap, aggregate slippage', async () => { + // route 1: DAI -> USDC -> WETH + // route 2: DAI -> USDC -> ETH, then router wraps ETH -> WETH + const route1 = [DAI_USDC.poolKey, USDC_WETH.poolKey] + const route2 = [DAI_USDC.poolKey, ETH_USDC.poolKey] + const v4AmountIn1 = expandTo18DecimalsBN(100) + const v4AmountIn2 = expandTo18DecimalsBN(150) + const aggregateMinOut = expandTo18DecimalsBN(250 / Math.floor(USD_ETH_PRICE * 1.01)) + + let currencyIn = daiContract.address + // add first split to v4 planner + v4Planner.addAction(Actions.SWAP_EXACT_IN, [ + { + currencyIn, + path: encodeMultihopExactInPath(route1, currencyIn), + amountIn: v4AmountIn1, + amountOutMinimum: 0, + }, + ]) + // add second split to v4 planner + v4Planner.addAction(Actions.SWAP_EXACT_IN, [ + { + currencyIn, + path: encodeMultihopExactInPath(route2, currencyIn), + amountIn: v4AmountIn2, + amountOutMinimum: 0, + }, + ]) + // settle all DAI with no limit + v4Planner.addAction(Actions.SETTLE_ALL, [currencyIn, v4AmountIn1.add(v4AmountIn2)]) + // take all the WETH and all the ETH into the router + v4Planner.addAction(Actions.TAKE, [WETH.address, ADDRESS_THIS, OPEN_DELTA]) + v4Planner.addAction(Actions.TAKE, [ETH_ADDRESS, ADDRESS_THIS, OPEN_DELTA]) + + planner.addCommand(CommandType.V4_SWAP, [v4Planner.actions, v4Planner.params]) + // wrap all the ETH into WETH + planner.addCommand(CommandType.WRAP_ETH, [ADDRESS_THIS, CONTRACT_BALANCE]) + // now we can send the WETH to the user, with aggregate slippage check + planner.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, aggregateMinOut]) + + const { daiBalanceBefore, daiBalanceAfter, wethBalanceBefore, wethBalanceAfter } = await executeRouter( + planner, + bob, + router, + wethContract, + daiContract, + usdcContract + ) + expect(wethBalanceAfter.sub(wethBalanceBefore)).to.be.gte(aggregateMinOut) + expect(daiBalanceBefore.sub(daiBalanceAfter)).to.be.eq(v4AmountIn1.add(v4AmountIn2)) + }) + describe('Batch reverts', () => { let subplan: RoutePlanner const planOneTokens = [DAI.address, WETH.address] diff --git a/test/integration-tests/gas-tests/__snapshots__/Payments.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/Payments.gas.test.ts.snap index 69211e4e..530bbc8e 100644 --- a/test/integration-tests/gas-tests/__snapshots__/Payments.gas.test.ts.snap +++ b/test/integration-tests/gas-tests/__snapshots__/Payments.gas.test.ts.snap @@ -31,14 +31,14 @@ Object { exports[`Payments Gas Tests Individual Command Tests gas: UNWRAP_WETH 1`] = ` Object { "calldataByteLength": 324, - "gasUsed": 44600, + "gasUsed": 44620, } `; exports[`Payments Gas Tests Individual Command Tests gas: UNWRAP_WETH_WITH_FEE 1`] = ` Object { "calldataByteLength": 644, - "gasUsed": 51039, + "gasUsed": 51059, } `; diff --git a/test/integration-tests/gas-tests/__snapshots__/Uniswap.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/Uniswap.gas.test.ts.snap index 7f0fb7f7..46117bb8 100644 --- a/test/integration-tests/gas-tests/__snapshots__/Uniswap.gas.test.ts.snap +++ b/test/integration-tests/gas-tests/__snapshots__/Uniswap.gas.test.ts.snap @@ -87,14 +87,14 @@ Object { exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ETH split V2 and V3, exactOut, one hop 1`] = ` Object { "calldataByteLength": 964, - "gasUsed": 192316, + "gasUsed": 192336, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ETH split V2 and V3, one hop 1`] = ` Object { "calldataByteLength": 964, - "gasUsed": 184980, + "gasUsed": 185000, } `; @@ -206,21 +206,21 @@ Object { exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ETH gas: exactIn, one trade, one hop 1`] = ` Object { "calldataByteLength": 644, - "gasUsed": 123179, + "gasUsed": 123199, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ETH gas: exactOut, one trade, one hop 1`] = ` Object { "calldataByteLength": 804, - "gasUsed": 128023, + "gasUsed": 128043, } `; exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ERC20 --> ETH gas: exactOut, with ETH fee 1`] = ` Object { "calldataByteLength": 964, - "gasUsed": 136047, + "gasUsed": 136067, } `; @@ -234,7 +234,7 @@ Object { exports[`Uniswap Gas Tests Trade on UniswapV2 with Universal Router. ETH --> ERC20 gas: exactOut, one trade, one hop 1`] = ` Object { "calldataByteLength": 772, - "gasUsed": 125212, + "gasUsed": 125232, } `; @@ -325,14 +325,14 @@ Object { exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ETH gas: exactIn swap 1`] = ` Object { "calldataByteLength": 644, - "gasUsed": 121969, + "gasUsed": 121989, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ETH gas: exactOut swap 1`] = ` Object { "calldataByteLength": 644, - "gasUsed": 129476, + "gasUsed": 129496, } `; @@ -346,6 +346,6 @@ Object { exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ETH --> ERC20 gas: exactOut swap 1`] = ` Object { "calldataByteLength": 772, - "gasUsed": 124663, + "gasUsed": 124683, } `; diff --git a/test/integration-tests/gas-tests/__snapshots__/UniversalRouter.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/UniversalRouter.gas.test.ts.snap index 9773ab3e..f843f61a 100644 --- a/test/integration-tests/gas-tests/__snapshots__/UniversalRouter.gas.test.ts.snap +++ b/test/integration-tests/gas-tests/__snapshots__/UniversalRouter.gas.test.ts.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`UniversalRouter Gas Tests gas: bytecode size 1`] = `18715`; +exports[`UniversalRouter Gas Tests gas: bytecode size 1`] = `18786`;