UNPKG

@uniswap/universal-router

Version:

Smart contracts for Universal Router

441 lines (389 loc) 15.9 kB
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 { abi as TOKEN_ABI } from '../../artifacts/solmate/src/tokens/ERC20.sol/ERC20.json' import { resetFork, WETH, DAI, USDC, PERMIT2 } from './shared/mainnetForkHelpers' import { ADDRESS_THIS, ALICE_ADDRESS, DEADLINE, ETH_ADDRESS, MAX_UINT, MAX_UINT160, MSG_SENDER, ONE_PERCENT_BIPS, SOURCE_MSG_SENDER, SOURCE_ROUTER, } from './shared/constants' import { expandTo18DecimalsBN, expandTo6DecimalsBN } from './shared/helpers' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import deployUniversalRouter from './shared/deployUniversalRouter' import { RoutePlanner, CommandType } from './shared/planner' import hre from 'hardhat' import { executeRouter } from './shared/executeRouter' import { getPermitSignature, PermitSingle } from './shared/protocolHelpers/permit2' import { ADDRESS_ZERO } from '@uniswap/v3-sdk' const { ethers } = hre describe('Uniswap V2 Tests:', () => { let alice: SignerWithAddress let bob: SignerWithAddress let router: UniversalRouter let permit2: IPermit2 let daiContract: Contract let wethContract: Contract let usdcContract: Contract let planner: RoutePlanner const amountIn: BigNumber = expandTo18DecimalsBN(5) beforeEach(async () => { await resetFork() await hre.network.provider.request({ method: 'hardhat_impersonateAccount', params: [ALICE_ADDRESS], }) alice = await ethers.getSigner(ALICE_ADDRESS) bob = (await ethers.getSigners())[1] daiContract = new ethers.Contract(DAI.address, TOKEN_ABI, bob) 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(bob.address)) as UniversalRouter planner = new RoutePlanner() // 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)) // Bob max-approves the permit2 contract to access his DAI and WETH await daiContract.connect(bob).approve(permit2.address, MAX_UINT) await wethContract.connect(bob).approve(permit2.address, MAX_UINT) await usdcContract.connect(bob).approve(permit2.address, MAX_UINT) // for these tests Bob gives the router max approval on permit2 await permit2.approve(DAI.address, router.address, MAX_UINT160, DEADLINE) await permit2.approve(WETH.address, router.address, MAX_UINT160, DEADLINE) }) describe('Trade on Uniswap with Permit2, giving approval every time', () => { let permit: PermitSingle beforeEach(async () => { // cancel the permit on DAI await permit2.approve(DAI.address, router.address, 0, 0) }) it('Permit2 can silently fail', async () => { const amountInDAI = expandTo18DecimalsBN(100) // bob signs a permit to allow the router to access his DAI permit = { details: { token: DAI.address, amount: amountInDAI, expiration: 0, // expiration of 0 is block.timestamp nonce: 0, // this is his first trade }, spender: router.address, sigDeadline: DEADLINE, } const sig = await getPermitSignature(permit, bob, permit2) // 1) permit the router to access funds, not allowing revert planner.addCommand(CommandType.PERMIT2_PERMIT, [permit, sig]) // 2) permit the router to access funds again, allowing revert planner.addCommand(CommandType.PERMIT2_PERMIT, [permit, sig], true) let nonce = (await permit2.allowance(bob.address, DAI.address, router.address)).nonce expect(nonce).to.eq(0) await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) nonce = (await permit2.allowance(bob.address, DAI.address, router.address)).nonce expect(nonce).to.eq(1) }) it('V2 exactIn, permiting the exact amount', async () => { const amountInDAI = expandTo18DecimalsBN(100) const minAmountOutWETH = expandTo18DecimalsBN(0.02) // second bob signs a permit to allow the router to access his DAI permit = { details: { token: DAI.address, amount: amountInDAI, expiration: 0, // expiration of 0 is block.timestamp nonce: 0, // this is his first trade }, spender: router.address, sigDeadline: DEADLINE, } const sig = await getPermitSignature(permit, bob, permit2) // 1) permit the router to access funds, 2) withdraw the funds into the pair, 3) trade planner.addCommand(CommandType.PERMIT2_PERMIT, [permit, sig]) planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ MSG_SENDER, amountInDAI, minAmountOutWETH, [DAI.address, WETH.address], SOURCE_MSG_SENDER, ]) const { wethBalanceBefore, wethBalanceAfter, daiBalanceAfter, daiBalanceBefore } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) expect(wethBalanceAfter.sub(wethBalanceBefore)).to.be.gte(minAmountOutWETH) expect(daiBalanceBefore.sub(daiBalanceAfter)).to.be.eq(amountInDAI) }) it('V2 exactOut, permiting the maxAmountIn', async () => { const maxAmountInDAI = expandTo18DecimalsBN(4000) const amountOutWETH = expandTo18DecimalsBN(1) // second bob signs a permit to allow the router to access his DAI permit = { details: { token: DAI.address, amount: maxAmountInDAI, expiration: 0, // expiration of 0 is block.timestamp nonce: 0, // this is his first trade }, spender: router.address, sigDeadline: DEADLINE, } const sig = await getPermitSignature(permit, bob, permit2) // 1) permit the router to access funds, 2) trade - the transfer happens within the trade for exactOut planner.addCommand(CommandType.PERMIT2_PERMIT, [permit, sig]) planner.addCommand(CommandType.V2_SWAP_EXACT_OUT, [ MSG_SENDER, amountOutWETH, maxAmountInDAI, [DAI.address, WETH.address], SOURCE_MSG_SENDER, ]) const { wethBalanceBefore, wethBalanceAfter, daiBalanceAfter, daiBalanceBefore } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) expect(wethBalanceAfter.sub(wethBalanceBefore)).to.be.eq(amountOutWETH) expect(daiBalanceBefore.sub(daiBalanceAfter)).to.be.lte(maxAmountInDAI) }) it('V2 exactIn, swapping more than max_uint160 should revert', async () => { const max_uint = BigNumber.from(MAX_UINT160) const minAmountOutWETH = expandTo18DecimalsBN(0.03) // second bob signs a permit to allow the router to access his DAI permit = { details: { token: DAI.address, amount: max_uint, expiration: 0, // expiration of 0 is block.timestamp nonce: 0, // this is his first trade }, spender: router.address, sigDeadline: DEADLINE, } const sig = await getPermitSignature(permit, bob, permit2) // 1) permit the router to access funds, 2) withdraw the funds into the pair, 3) trade planner.addCommand(CommandType.PERMIT2_PERMIT, [permit, sig]) planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ MSG_SENDER, BigNumber.from(MAX_UINT160).add(1), minAmountOutWETH, [DAI.address, WETH.address], SOURCE_MSG_SENDER, ]) const testCustomErrors = await (await ethers.getContractFactory('TestCustomErrors')).deploy() await expect( executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) ).to.be.revertedWithCustomError(testCustomErrors, 'UnsafeCast') }) }) describe('ERC20 --> ERC20', () => { it('completes a V2 exactIn swap', async () => { const minAmountOut = expandTo18DecimalsBN(0.0001) planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ MSG_SENDER, amountIn, minAmountOut, [DAI.address, WETH.address], SOURCE_MSG_SENDER, ]) const { wethBalanceBefore, wethBalanceAfter } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) expect(wethBalanceAfter.sub(wethBalanceBefore)).to.be.gt(minAmountOut) }) it('completes a V2 exactOut swap', async () => { const amountOut = expandTo18DecimalsBN(1) planner.addCommand(CommandType.V2_SWAP_EXACT_OUT, [ MSG_SENDER, amountOut, expandTo18DecimalsBN(10000), [WETH.address, DAI.address], SOURCE_MSG_SENDER, ]) planner.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, 0]) const { daiBalanceBefore, daiBalanceAfter } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) expect(daiBalanceAfter.sub(daiBalanceBefore)).to.be.gt(amountOut) }) it('exactIn trade, where an output fee is taken', async () => { // back to the router so someone can take a fee planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ ADDRESS_THIS, amountIn, 1, [DAI.address, WETH.address], SOURCE_MSG_SENDER, ]) planner.addCommand(CommandType.PAY_PORTION, [WETH.address, alice.address, ONE_PERCENT_BIPS]) planner.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, 1]) const { commands, inputs } = planner const wethBalanceBeforeAlice = await wethContract.balanceOf(alice.address) const wethBalanceBeforeBob = await wethContract.balanceOf(bob.address) await router.connect(bob)['execute(bytes,bytes[],uint256)'](commands, inputs, DEADLINE) const wethBalanceAfterAlice = await wethContract.balanceOf(alice.address) const wethBalanceAfterBob = await wethContract.balanceOf(bob.address) const aliceFee = wethBalanceAfterAlice.sub(wethBalanceBeforeAlice) const bobEarnings = wethBalanceAfterBob.sub(wethBalanceBeforeBob) expect(bobEarnings).to.be.gt(0) expect(aliceFee).to.be.gt(0) // total fee is 1% of bob's output expect(aliceFee.add(bobEarnings).mul(ONE_PERCENT_BIPS).div(10_000)).to.eq(aliceFee) }) it('completes a V2 exactIn swap with longer path', async () => { const minAmountOut = expandTo18DecimalsBN(0.0001) planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ MSG_SENDER, amountIn, minAmountOut, [DAI.address, USDC.address, WETH.address], SOURCE_MSG_SENDER, ]) const { wethBalanceBefore, wethBalanceAfter } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) expect(wethBalanceAfter.sub(wethBalanceBefore)).to.be.gt(minAmountOut) }) }) describe('ERC20 --> ETH', () => { it('completes a V2 exactIn swap', async () => { planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ ADDRESS_THIS, amountIn, 1, [DAI.address, WETH.address], SOURCE_MSG_SENDER, ]) planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, 0]) const { gasSpent, ethBalanceBefore, ethBalanceAfter, v2SwapEventArgs } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) const { amount1Out: wethTraded } = v2SwapEventArgs! expect(ethBalanceAfter.sub(ethBalanceBefore)).to.eq(wethTraded.sub(gasSpent)) }) it('completes a V2 exactOut swap', async () => { const amountOut = expandTo18DecimalsBN(1) planner.addCommand(CommandType.V2_SWAP_EXACT_OUT, [ ADDRESS_THIS, amountOut, expandTo18DecimalsBN(10000), [DAI.address, WETH.address], SOURCE_MSG_SENDER, ]) planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, amountOut]) planner.addCommand(CommandType.SWEEP, [DAI.address, MSG_SENDER, 0]) const { gasSpent, ethBalanceBefore, ethBalanceAfter, v2SwapEventArgs } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) const { amount1Out: wethTraded } = v2SwapEventArgs! expect(ethBalanceAfter.sub(ethBalanceBefore)).to.eq(amountOut.sub(gasSpent)) expect(wethTraded).to.eq(amountOut) }) it('completes a V2 exactOut swap, with ETH fee', async () => { const amountOut = expandTo18DecimalsBN(1) const totalPortion = amountOut.mul(ONE_PERCENT_BIPS).div(10000) const actualAmountOut = amountOut.sub(totalPortion) planner.addCommand(CommandType.V2_SWAP_EXACT_OUT, [ ADDRESS_THIS, amountOut, expandTo18DecimalsBN(10000), [DAI.address, WETH.address], SOURCE_MSG_SENDER, ]) planner.addCommand(CommandType.UNWRAP_WETH, [ADDRESS_THIS, amountOut]) planner.addCommand(CommandType.PAY_PORTION, [ETH_ADDRESS, alice.address, ONE_PERCENT_BIPS]) planner.addCommand(CommandType.SWEEP, [ETH_ADDRESS, MSG_SENDER, 0]) const { commands, inputs } = planner await expect( router.connect(bob)['execute(bytes,bytes[],uint256)'](commands, inputs, DEADLINE) ).to.changeEtherBalances([alice, bob], [totalPortion, actualAmountOut]) }) }) describe('ETH --> ERC20', () => { it('completes a V2 exactIn swap', async () => { const minAmountOut = expandTo18DecimalsBN(0.001) const pairAddress = Pair.getAddress(DAI, WETH) planner.addCommand(CommandType.WRAP_ETH, [pairAddress, amountIn]) // amountIn of 0 because the weth is already in the pair planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ MSG_SENDER, 0, minAmountOut, [WETH.address, DAI.address], SOURCE_MSG_SENDER, ]) const { daiBalanceBefore, daiBalanceAfter, v2SwapEventArgs } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract, amountIn ) const { amount0Out: daiTraded } = v2SwapEventArgs! expect(daiBalanceAfter.sub(daiBalanceBefore)).to.be.gt(minAmountOut) expect(daiBalanceAfter.sub(daiBalanceBefore)).to.equal(daiTraded) }) it('completes a V2 exactOut swap', async () => { const amountOut = expandTo18DecimalsBN(100) const value = expandTo18DecimalsBN(1) planner.addCommand(CommandType.WRAP_ETH, [ADDRESS_THIS, value]) planner.addCommand(CommandType.V2_SWAP_EXACT_OUT, [ MSG_SENDER, amountOut, expandTo18DecimalsBN(1), [WETH.address, DAI.address], SOURCE_ROUTER, ]) planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, 0]) const { ethBalanceBefore, ethBalanceAfter, daiBalanceBefore, daiBalanceAfter, v2SwapEventArgs, gasSpent } = await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract, value) const { amount0Out: daiTraded, amount1In: wethTraded } = v2SwapEventArgs! expect(daiBalanceAfter.sub(daiBalanceBefore)).gt(amountOut) // rounding expect(daiBalanceAfter.sub(daiBalanceBefore)).eq(daiTraded) expect(ethBalanceBefore.sub(ethBalanceAfter)).to.eq(wethTraded.add(gasSpent)) }) }) })