diff --git a/test/e2e/UniswapTrade.t.sol b/test/e2e/UniswapTrade.t.sol new file mode 100644 index 00000000..ab71c6f2 --- /dev/null +++ b/test/e2e/UniswapTrade.t.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.8; + +import {Vm} from "forge-std/Vm.sol"; + +import {IERC20} from "src/contracts/interfaces/IERC20.sol"; + +import {GPv2Interaction} from "src/contracts/libraries/GPv2Interaction.sol"; +import {GPv2Order} from "src/contracts/libraries/GPv2Order.sol"; +import {GPv2Signing} from "src/contracts/mixins/GPv2Signing.sol"; + +import {SettlementEncoder} from "../libraries/encoders/SettlementEncoder.sol"; +import {Registry, TokenRegistry} from "../libraries/encoders/TokenRegistry.sol"; +import {Helper, IERC20Mintable} from "./Helper.sol"; + +using SettlementEncoder for SettlementEncoder.State; +using TokenRegistry for TokenRegistry.State; +using TokenRegistry for Registry; + +interface IUniswapV2Factory { + function createPair(address, address) external returns (address); +} + +interface IUniswapV2Pair { + function token0() external view returns (address); + function mint(address) external; + function swap(uint256, uint256, address, bytes calldata) external; + function getReserves() external view returns (uint256, uint256); +} + +contract UniswapTradeTest is Helper(true) { + IERC20Mintable dai; + IERC20Mintable wETH; + + IUniswapV2Factory factory; + IUniswapV2Pair uniswapPair; + + bool isWethToken0; + + function setUp() public override { + super.setUp(); + + dai = deployMintableErc20("dai", "dai"); + wETH = deployMintableErc20("wETH", "wETH"); + + factory = IUniswapV2Factory(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f); + uniswapPair = IUniswapV2Pair(factory.createPair(address(wETH), address(dai))); + + isWethToken0 = uniswapPair.token0() == address(wETH); + } + + // Settles the following batch: + // + // /----(1. SELL 1 wETH for dai if p(wETH) >= 500)-----\ + // | | + // | v + // [dai]<---(Uniswap Pair 1000 wETH / 600.000 dai)--->[wETH] + // ^ | + // | | + // \----(2. BUY 0.5 wETH for dai if p(wETH) <= 600)----/ + function test_should_two_overlapping_orders_and_trade_surplus_with_uniswap() external { + uint256 wethReserve = 1000 ether; + uint256 daiReserve = 600000 ether; + wETH.mint(address(uniswapPair), wethReserve); + dai.mint(address(uniswapPair), daiReserve); + uniswapPair.mint(address(this)); + + // The current batch has a sell order selling 1 wETH and a buy order buying + // 0.5 wETH. This means there is exactly a surplus 0.5 wETH that needs to be + // sold to Uniswap. Uniswap is governed by a balancing equation which can be + // used to compute the exact buy amount for selling the 0.5 wETH and we can + // use to build our the settlement with a smart contract interaction. + // ``` + // (reservewETH + inwETH * 0.997) * (reservedai - outdai) = reservewETH * reservedai + // outdai = (reservedai * inwETH * 0.997) / (reservewETH + inwETH * 0.997) + // = (reservedai * inwETH * 997) / (reservewETH * 1000 + inwETH * 997) + // ``` + uint256 uniswapWethInAmount = 0.5 ether; + uint256 uniswapDaiOutAmount = + daiReserve * uniswapWethInAmount * 997 / ((wethReserve * 1000) + (uniswapWethInAmount * 997)); + + Vm.Wallet memory trader1 = vm.createWallet("trader1"); + Vm.Wallet memory trader2 = vm.createWallet("trader2"); + + // mint some weth + wETH.mint(trader1.addr, 1.001 ether); + vm.prank(trader1.addr); + wETH.approve(vaultRelayer, type(uint256).max); + + // place order to sell 1 wETH for min 500 dai + encoder.signEncodeTrade( + vm, + trader1, + GPv2Order.Data({ + kind: GPv2Order.KIND_SELL, + partiallyFillable: false, + sellToken: wETH, + buyToken: dai, + sellAmount: 1 ether, + buyAmount: 500 ether, + feeAmount: 0.001 ether, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20, + receiver: GPv2Order.RECEIVER_SAME_AS_OWNER + }), + domainSeparator, + GPv2Signing.Scheme.Eip712, + 0 + ); + + // mint some dai + dai.mint(trader2.addr, 300.3 ether); + vm.prank(trader2.addr); + dai.approve(vaultRelayer, type(uint256).max); + + // place order to buy 0.5 wETH for max 300 dai + encoder.signEncodeTrade( + vm, + trader2, + GPv2Order.Data({ + kind: GPv2Order.KIND_BUY, + partiallyFillable: false, + sellToken: dai, + buyToken: wETH, + sellAmount: 300 ether, + buyAmount: 0.5 ether, + feeAmount: 0.3 ether, + validTo: 0xffffffff, + appData: bytes32(uint256(1)), + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20, + receiver: GPv2Order.RECEIVER_SAME_AS_OWNER + }), + domainSeparator, + GPv2Signing.Scheme.Eip712, + 0 + ); + + // interaction to swap the remainder on uniswap + encoder.addInteraction( + GPv2Interaction.Data({ + target: address(wETH), + value: 0, + callData: abi.encodeCall(IERC20.transfer, (address(uniswapPair), uniswapWethInAmount)) + }), + SettlementEncoder.InteractionStage.INTRA + ); + (uint256 amount0Out, uint256 amount1Out) = + isWethToken0 ? (uint256(0), uniswapDaiOutAmount) : (uniswapDaiOutAmount, uint256(0)); + encoder.addInteraction( + GPv2Interaction.Data({ + target: address(uniswapPair), + value: 0, + callData: abi.encodeCall(IUniswapV2Pair.swap, (amount0Out, amount1Out, address(settlement), hex"")) + }), + SettlementEncoder.InteractionStage.INTRA + ); + + // set token prices + IERC20[] memory tokens = new IERC20[](2); + tokens[0] = wETH; + tokens[1] = dai; + uint256[] memory prices = new uint256[](2); + prices[0] = uniswapDaiOutAmount; + prices[1] = uniswapWethInAmount; + encoder.tokenRegistry.tokenRegistry().setPrices(tokens, prices); + + SettlementEncoder.EncodedSettlement memory encodedSettlement = encoder.encode(settlement); + + vm.prank(solver); + settle(encodedSettlement); + + assertEq(wETH.balanceOf(address(settlement)), 0.001 ether, "weth fees not as expected"); + assertEq(dai.balanceOf(address(settlement)), 0.3 ether, "dai fees not as expected"); + + assertEq(wETH.balanceOf(trader1.addr), 0, "not all weth sold"); + assertEq(dai.balanceOf(trader1.addr), uniswapDaiOutAmount * 2, "dai received not as expected"); + + assertEq(wETH.balanceOf(trader2.addr), 0.5 ether, "weth bought not correct amount"); + assertEq(dai.balanceOf(trader2.addr), 300.3 ether - (uniswapDaiOutAmount + 0.3 ether)); + + uint256 finalWethReserve; + uint256 finalDaiReserve; + + { + (uint256 token0Reserve, uint256 token1Reserve) = uniswapPair.getReserves(); + (finalWethReserve, finalDaiReserve) = + isWethToken0 ? (token0Reserve, token1Reserve) : (token1Reserve, token0Reserve); + } + assertEq(finalWethReserve, wethReserve + uniswapWethInAmount, "weth reserve not as expected"); + assertEq(finalDaiReserve, daiReserve - uniswapDaiOutAmount, "dai reserve not as expected"); + } +} diff --git a/test/e2e/uniswapTrade.test.ts b/test/e2e/uniswapTrade.test.ts deleted file mode 100644 index e7080af8..00000000 --- a/test/e2e/uniswapTrade.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -import ERC20 from "@openzeppelin/contracts/build/contracts/ERC20PresetMinterPauser.json"; -import UniswapV2Factory from "@uniswap/v2-core/build/UniswapV2Factory.json"; -import UniswapV2Pair from "@uniswap/v2-core/build/UniswapV2Pair.json"; -import { expect } from "chai"; -import { Contract, Wallet } from "ethers"; -import { ethers, waffle } from "hardhat"; - -import { - OrderKind, - SettlementEncoder, - SigningScheme, - TypedDataDomain, - domain, -} from "../../src/ts"; - -import { deployTestContracts } from "./fixture"; - -describe("E2E: Should Trade Surplus With Uniswap", () => { - let deployer: Wallet; - let solver: Wallet; - let pooler: Wallet; - let traders: Wallet[]; - - let settlement: Contract; - let vaultRelayer: Contract; - let domainSeparator: TypedDataDomain; - - let weth: Contract; - let usdt: Contract; - let uniswapPair: Contract; - let isWethToken0: boolean; - - beforeEach(async () => { - const deployment = await deployTestContracts(); - - ({ - deployer, - settlement, - vaultRelayer, - wallets: [solver, pooler, ...traders], - } = deployment); - - const { authenticator, manager } = deployment; - await authenticator.connect(manager).addSolver(solver.address); - - const { chainId } = await ethers.provider.getNetwork(); - domainSeparator = domain(chainId, settlement.address); - - weth = await waffle.deployContract(deployer, ERC20, ["WETH", 18]); - usdt = await waffle.deployContract(deployer, ERC20, ["USDT", 6]); - - const uniswapFactory = await waffle.deployContract( - deployer, - UniswapV2Factory, - [deployer.address], - ); - await uniswapFactory.createPair(weth.address, usdt.address); - uniswapPair = new Contract( - await uniswapFactory.getPair(weth.address, usdt.address), - UniswapV2Pair.abi, - deployer, - ); - - // NOTE: Which token ends up as token 0 or token 1 depends on the addresses - // of the WETH and USDT token which can change depending on which order the - // tests are run. Because of this, check the Uniswap pair to see which token - // ended up on which index. - isWethToken0 = (await uniswapPair.token0()) === weth.address; - }); - - it("should settle two overlapping orders and trade surplus with Uniswap", async () => { - // Settles the following batch: - // - // /----(1. SELL 1 WETH for USDT if p(WETH) >= 500)-----\ - // | | - // | v - // [USDT]<---(Uniswap Pair 1000 WETH / 600.000 USDT)--->[WETH] - // ^ | - // | | - // \----(2. BUY 0.5 WETH for USDT if p(WETH) <= 600)----/ - - const uniswapWethReserve = ethers.utils.parseEther("1000.0"); - const uniswapUsdtReserve = ethers.utils.parseUnits("600000.0", 6); - await weth.mint(uniswapPair.address, uniswapWethReserve); - await usdt.mint(uniswapPair.address, uniswapUsdtReserve); - await uniswapPair.mint(pooler.address); - - // The current batch has a sell order selling 1 WETH and a buy order buying - // 0.5 WETH. This means there is exactly a surplus 0.5 WETH that needs to be - // sold to Uniswap. Uniswap is governed by a balancing equation which can be - // used to compute the exact buy amount for selling the 0.5 WETH and we can - // use to build our the settlement with a smart contract interaction. - // ``` - // (reserveWETH + inWETH * 0.997) * (reserveUSDT - outUSDT) = reserveWETH * reserveUSDT - // outUSDT = (reserveUSDT * inWETH * 0.997) / (reserveWETH + inWETH * 0.997) - // = (reserveUSDT * inWETH * 997) / (reserveWETH * 1000 + inWETH * 997) - // ``` - const uniswapWethInAmount = ethers.utils.parseEther("0.5"); - const uniswapUsdtOutAmount = uniswapUsdtReserve - .mul(uniswapWethInAmount) - .mul(997) - .div(uniswapWethReserve.mul(1000).add(uniswapWethInAmount.mul(997))); - - const encoder = new SettlementEncoder(domainSeparator); - - await weth.mint(traders[0].address, ethers.utils.parseEther("1.001")); - await weth - .connect(traders[0]) - .approve(vaultRelayer.address, ethers.constants.MaxUint256); - await encoder.signEncodeTrade( - { - kind: OrderKind.SELL, - partiallyFillable: false, - sellToken: weth.address, - buyToken: usdt.address, - sellAmount: ethers.utils.parseEther("1.0"), - buyAmount: ethers.utils.parseUnits("500.0", 6), - feeAmount: ethers.utils.parseEther("0.001"), - validTo: 0xffffffff, - appData: 1, - }, - traders[0], - SigningScheme.EIP712, - ); - - await usdt.mint(traders[1].address, ethers.utils.parseUnits("300.3", 6)); - await usdt - .connect(traders[1]) - .approve(vaultRelayer.address, ethers.constants.MaxUint256); - await encoder.signEncodeTrade( - { - kind: OrderKind.BUY, - partiallyFillable: false, - buyToken: weth.address, - sellToken: usdt.address, - buyAmount: ethers.utils.parseEther("0.5"), - sellAmount: ethers.utils.parseUnits("300.0", 6), - feeAmount: ethers.utils.parseUnits("0.3", 6), - validTo: 0xffffffff, - appData: 2, - }, - traders[1], - SigningScheme.EIP712, - ); - - encoder.encodeInteraction({ - target: weth.address, - callData: weth.interface.encodeFunctionData("transfer", [ - uniswapPair.address, - uniswapWethInAmount, - ]), - }); - - const [amount0Out, amount1Out] = isWethToken0 - ? [0, uniswapUsdtOutAmount] - : [uniswapUsdtOutAmount, 0]; - encoder.encodeInteraction({ - target: uniswapPair.address, - callData: uniswapPair.interface.encodeFunctionData("swap", [ - amount0Out, - amount1Out, - settlement.address, - "0x", - ]), - }); - - await settlement.connect(solver).settle( - ...encoder.encodedSettlement({ - [weth.address]: uniswapUsdtOutAmount, - [usdt.address]: uniswapWethInAmount, - }), - ); - - expect(await weth.balanceOf(settlement.address)).to.deep.equal( - ethers.utils.parseEther("0.001"), - ); - expect(await usdt.balanceOf(settlement.address)).to.deep.equal( - ethers.utils.parseUnits("0.3", 6), - ); - - expect(await weth.balanceOf(traders[0].address)).to.deep.equal( - ethers.constants.Zero, - ); - expect(await usdt.balanceOf(traders[0].address)).to.deep.equal( - uniswapUsdtOutAmount.mul(2), - ); - - expect(await weth.balanceOf(traders[1].address)).to.deep.equal( - ethers.utils.parseEther("0.5"), - ); - expect(await usdt.balanceOf(traders[1].address)).to.deep.equal( - ethers.utils - .parseUnits("300.3", 6) - .sub(uniswapUsdtOutAmount.add(ethers.utils.parseUnits("0.3", 6))), - ); - - const [token0Reserve, token1Reserve] = await uniswapPair.getReserves(); - const [finalWethReserve, finalUsdtReserve] = isWethToken0 - ? [token0Reserve, token1Reserve] - : [token1Reserve, token0Reserve]; - expect([finalWethReserve, finalUsdtReserve]).to.deep.equal([ - uniswapWethReserve.add(uniswapWethInAmount), - uniswapUsdtReserve.sub(uniswapUsdtOutAmount), - ]); - }); -});