{"content":{"title":"写一个uniswap价差套利的合约","body":"# 1. 价差套利\r\n所谓的价差套利，本质上就是低买高卖。举个例子，假设sushiswap里的ETH价值1500USDT，uniswap里的ETH价值1600USDT，那么我们可以以1500USDT在sushiswap里买入1ETH，然后在uniswap里卖出获得1600USDT，假设手续费是5U，那么最终获利95USDT。这里有个问题，那就是我们必须先用自己的1500USDT去买ETH，假设我们没有这么多钱怎么办呢？uniswap给我们提供了类似flash loan的flash swap功能，可以让我们无成本进行套利。\r\n\r\n# 2. UniswapV2里的flash swap原理\r\n在UniswapV2里，真正执行swap的过程是发生在UniswapV2Pair里的，下面让我们看看UniswapV2Pair的swap函数：\r\n```solidity\r\nfunction swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {\r\n        require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');\r\n        (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings\r\n        require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');\r\n\r\n        uint balance0;\r\n        uint balance1;\r\n        { // scope for _token{0,1}, avoids stack too deep errors\r\n        address _token0 = token0;\r\n        address _token1 = token1;\r\n        require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');\r\n        if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens\r\n        if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens\r\n        if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);\r\n        balance0 = IERC20(_token0).balanceOf(address(this));\r\n        balance1 = IERC20(_token1).balanceOf(address(this));\r\n        }\r\n        uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;\r\n        uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;\r\n        require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');\r\n        { // scope for reserve{0,1}Adjusted, avoids stack too deep errors\r\n        uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));\r\n        uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));\r\n        require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');\r\n        }\r\n\r\n        _update(balance0, balance1, _reserve0, _reserve1);\r\n        emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);\r\n    }\r\n```\r\n我们知道，一次swap，其实是用一种token去换另一种token，你把你要卖掉的token转给pair，pair给你转你想要的token。swap函数有四个参数，其中`amount0Out` 和 `amount1Out` 是指你想要获得的那个token的数量。比如你想卖token1获得100e18个token0，你就需要传入`amount0Out = 100e18` 以及 `amount1Out = 0`，这时候pair会先把100e18的token0转给你，也就是下面这行代码：\r\n```solidity\r\nif (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens\r\n```\r\n然后判断data是否有值，如果有值，就说明是一次flash swap，它会把调用者指定的to地址当成一个实现了IUniswapV2Callee接口的合约，然后调用这个合约的uniswapV2Call方法：\r\n```solidity\r\n if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);\r\n```\r\n因为是先把token0转给to地址，然后再调用uniswapV2Call，这样就相当于to地址的那个合约先问pair借了一笔钱，接下来只要在uniswapV2Call方法把一定的token1转到pair，并且满足K值公式，那么整个swap就是合法的。\r\n\r\n回到第一节的那个例子，我们如何进行套利呢？这里我们不需要第一步用1500USDT去买1ETH，我们可以直接找到sushiswap的ETH-USDT的pair，然后amount0Out传入1e18，to传入一个你的合约地址，这时候pair就会把1个ETH转到你的合约地址，然后你需要在uniswapV2Call方法里使用uniswap把1ETH换成1600USDT，然后把1500USDT转回sushiswap的ETH-USDT的pair里就可以了，剩下的USDT就会留在你的合约中，成为你的利润。\r\n\r\n# 3 UniswapV2里的flash swap实现\r\n```solidity\r\nfunction startArbitrageFromV2( \r\n        address pool,\r\n        uint256 amountBorrowed)external onlyOwner{\r\n        IUniswapV3Pool v3Pool = IUniswapV3Pool(pool);\r\n        address token0 = v3Pool.token0();\r\n        address token1 = v3Pool.token1();\r\n        address pairAddress =\r\n            IUniswapV2Factory(v2Factory).getPair(token0, token1);\r\n        require(pairAddress != address(0), \"This pool does not exist\");\r\n        bytes memory data = abi.encode(\r\n            pool,\r\n            amountBorrowed\r\n        );\r\n        // borrow token0 from V2\r\n        IUniswapV2Pair(pairAddress).swap(\r\n            amountBorrowed,\r\n            0,\r\n            address(this), \r\n            data\r\n        );\r\n    }\r\n```\r\n首先我们写一个startArbitrageFromV2函数，这个函数的入参是一个pool地址和amountBorrowed。pool地址是uniswapV3中的ETH-USDT交易对地址，是用来卖出ETH的，amountBorrowed就是我们要从sushiswap里ETH-USDT的pair借出的ETH的数量1e18。然后我们拿到pool里的token0和token1，也就是ETH和USDT，然后通过IUniswapV2Factory获得sushiswap里的ETH-USDT交易对，判断pair不为0地址，然后将pool和amountBorrowed编码进data，最后调用sushiswap里ETH-USDT的pair的swap方法，amount0Out传入的是1e18,amount1Out传入的是0,to传的是address(this)。\r\n\r\n接下来我们实现uniswapV2Call方法：\r\n```solidity\r\nfunction uniswapV2Call(\r\n        address _sender,\r\n        uint256 _amount0,\r\n        uint256 _amount1,\r\n        bytes calldata _data\r\n    ) external {\r\n        address token0 = IUniswapV2Pair(msg.sender).token0();\r\n        address token1 = IUniswapV2Pair(msg.sender).token1();\r\n        require(\r\n            msg.sender == UniswapV2Library.pairFor(v2Factory, token0, token1),\r\n            \"not from swap pair\"\r\n        );\r\n\r\n        (address pool,,)=abi.decode(_data,(address,uint256,uint256));\r\n        IUniswapV3Pool v3Pool = IUniswapV3Pool(pool);\r\n        uint24 fee = v3Pool.fee();\r\n\r\n        require(_amount0 > 0,\"amount invalid\");\r\n\r\n\r\n        //V3 sell token0 get token1\r\n        address[] memory path = new address[](2);\r\n        path[0] = token0;\r\n        path[1] = token1;\r\n        IERC20(token0).approve(address(v3Router), _amount0);\r\n\r\n       \r\n        uint256 amountShouldReturned =\r\n            UniswapV2Library.getAmountsIn(v2Factory, _amount0, path)[0];\r\n        \r\n        ISwapRouter.ExactInputSingleParams memory params = ISwapRouter\r\n            .ExactInputSingleParams({\r\n                tokenIn: path[0],\r\n                tokenOut: path[1],\r\n                fee: fee,\r\n                recipient: address(this),\r\n                deadline: block.timestamp+deadline,\r\n                amountIn: _amount0,\r\n                amountOutMinimum: amountShouldReturned,\r\n                sqrtPriceLimitX96: 0\r\n            });\r\n        uint256 amountReceived = v3Router.exactInputSingle(params);\r\n        //return back token1 to V2\r\n        IERC20(token1).transfer(msg.sender, amountShouldReturned);\r\n        emit Logs(amountReceived - amountShouldReturned);\r\n    }\r\n```\r\n由于这个方法是pair调用的，因此msg.sender是个UniswapV2Pair，通过拿到token0和token1，还原出pair，和msg.sender进行比较。然后解析出pool，拿到pool的fee，为下面swap做准备。这时候这个合约已经有了ETH，数量就是_amount0，这时候就可以使用UniswapV2Library计算出应该还回去多少USDT，amountShouldReturned计算的值就是这个数据。接下来就是调用UniswapV3的exactInputSingle方法，将ETH全部转成成USDT，amountReceived就是得到的USDT的数量，然后把amountShouldReturned数量的USDT转给msg.sender，也就是转给pair。如果一切顺利，这个合约就留下了（amountReceived - amountShouldReturned）数量的USDT作为利润。\r\n\r\n# 4 总结\r\n本文实现了一个从sushiswap到uniswapV3的价差套利合约，理论上来说价差套利可以是V2->V2、V2->V3、V3->V2、V3->V3，具体步骤都差不多，都是先找到有价差的两个pair，然后调用价格低的那个pair的swap，然后callback到你的合约，你的合约把你得到的token0去价格高的那个pair卖掉换成token1，然后一部分token1转回价格低的pair，留下的token1就是利润。当然，这只是理论情况，实际情况下这种套利空间几乎不存在，一般来说都是CEX的币价先变，然后产生套利空间驱动套利者将DEX的价格修正，正常来说套利者不可能只修正uniswap而不去修正sushiswap，因此DEX之间的套利空间就变得很小。\r\n\r\n# 5 代码\r\n代码中除了实现了V2->V3的套利，还实现了V3->V2的套利，具体代码见startArbitrageFromV3和uniswapV3SwapCallback。\r\n**注意：这部分代码没有严格测试过，如果想使用，请自担风险！**\r\n```solidity\r\n//SPDX-License-Identifier: MIT\r\npragma solidity >=0.7.5;\r\npragma abicoder v2;\r\nimport \"./UniswapV2Library.sol\";\r\nimport \"./interfaces/IUniswapV2Router02.sol\";\r\nimport \"./interfaces/IUniswapV2Pair.sol\";\r\nimport \"./interfaces/IUniswapV2Factory.sol\";\r\nimport \"./interfaces/IERC20.sol\";\r\nimport \"./uniswap/v3-core/interfaces/IUniswapV3Pool.sol\";\r\nimport \"./uniswap/v3-periphery/interfaces/ISwapRouter.sol\";\r\nimport \"./uniswap/v3-periphery/libraries/PoolAddress.sol\";\r\nimport \"@openzeppelin/contracts/access/Ownable.sol\";\r\n\r\n\r\ncontract ArbitrageSwapWithFlashLoaner is Ownable{\r\n    \r\n    uint256 constant deadline = 100;\r\n    uint160 internal constant MIN_SQRT_RATIO = 4295128739;\r\n    uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342;\r\n    IUniswapV2Router02 public v2Router;\r\n    address public v2Factory;\r\n    ISwapRouter public v3Router;\r\n    address public v3Factory;\r\n\r\n    event Logs(uint256 amount);\r\n\r\n    constructor(\r\n        address _v2Factory,\r\n        address _v3Factory,\r\n        address _v2Router,\r\n        address _v3Router\r\n    ) public {\r\n        v2Factory = _v2Factory;\r\n        v3Factory = _v3Factory;\r\n        v2Router = IUniswapV2Router02(_v2Router);\r\n        v3Router = ISwapRouter(_v3Router);\r\n    }\r\n\r\n    function startArbitrageFromV2( \r\n        address pool,\r\n        uint256 amountBorrowed)external onlyOwner{\r\n        IUniswapV3Pool v3Pool = IUniswapV3Pool(pool);\r\n        address token0 = v3Pool.token0();\r\n        address token1 = v3Pool.token1();\r\n        address pairAddress =\r\n            IUniswapV2Factory(v2Factory).getPair(token0, token1);\r\n        require(pairAddress != address(0), \"This pool does not exist\");\r\n        bytes memory data = abi.encode(\r\n            pool,\r\n            amountBorrowed\r\n        );\r\n        // borrow token0 from V2\r\n        IUniswapV2Pair(pairAddress).swap(\r\n            amountBorrowed,\r\n            0,\r\n            address(this), \r\n            data\r\n        );\r\n    }\r\n\r\n    function uniswapV2Call(\r\n        address _sender,\r\n        uint256 _amount0,\r\n        uint256 _amount1,\r\n        bytes calldata _data\r\n    ) external {\r\n        address token0 = IUniswapV2Pair(msg.sender).token0();\r\n        address token1 = IUniswapV2Pair(msg.sender).token1();\r\n        require(\r\n            msg.sender == UniswapV2Library.pairFor(v2Factory, token0, token1),\r\n            \"not from swap pair\"\r\n        );\r\n\r\n        (address pool,,)=abi.decode(_data,(address,uint256,uint256));\r\n        IUniswapV3Pool v3Pool = IUniswapV3Pool(pool);\r\n        uint24 fee = v3Pool.fee();\r\n\r\n        require(_amount0 > 0,\"amount invalid\");\r\n\r\n\r\n        //V3 sell token0 get token1\r\n        address[] memory path = new address[](2);\r\n        path[0] = token0;\r\n        path[1] = token1;\r\n        IERC20(token0).approve(address(v3Router), _amount0);\r\n\r\n       \r\n        uint256 amountShouldReturned =\r\n            UniswapV2Library.getAmountsIn(v2Factory, _amount0, path)[0];\r\n        \r\n        ISwapRouter.ExactInputSingleParams memory params = ISwapRouter\r\n            .ExactInputSingleParams({\r\n                tokenIn: path[0],\r\n                tokenOut: path[1],\r\n                fee: fee,\r\n                recipient: address(this),\r\n                deadline: block.timestamp+deadline,\r\n                amountIn: _amount0,\r\n                amountOutMinimum: amountShouldReturned,\r\n                sqrtPriceLimitX96: 0\r\n            });\r\n        uint256 amountReceived = v3Router.exactInputSingle(params);\r\n        //return back token1 to V2\r\n        IERC20(token1).transfer(msg.sender, amountShouldReturned);\r\n        emit Logs(amountReceived - amountShouldReturned);\r\n    }\r\n\r\n    function startArbitrageFromV3(\r\n        address pool,\r\n        uint256 amountBorrowed) external onlyOwner{\r\n        IUniswapV3Pool v3Pool = IUniswapV3Pool(pool);\r\n        bytes memory data = abi.encode(\r\n            pool,\r\n            amountBorrowed\r\n        );\r\n        //borrow token0 from v3\r\n        IUniswapV3Pool(v3Pool).swap(\r\n            address(this),\r\n            false,\r\n            int(amountBorrowed),\r\n            MAX_SQRT_RATIO-1,\r\n            data\r\n        );\r\n    }\r\n\r\n    function uniswapV3SwapCallback(\r\n        int amount0,\r\n        int amount1,\r\n        bytes calldata data\r\n    ) external{\r\n        IUniswapV3Pool v3Pool = IUniswapV3Pool(msg.sender);\r\n        address token0 = v3Pool.token0();\r\n        address token1 = v3Pool.token1();\r\n        uint24  fee    = v3Pool.fee();\r\n        require(msg.sender ==PoolAddress.computeAddress(v3Factory,PoolAddress.getPoolKey(token0, token1, fee)), \"not authorized\");\r\n        uint256 amountRequired = uint256(amount1);\r\n        uint256 amountToken0 = uint256(-amount0);\r\n\r\n        //v2 sell token0 and get token1\r\n        address[] memory path = new address[](2);\r\n        path[0] = token0;\r\n        path[1] = token1;\r\n        IERC20(token0).approve(address(v2Router), amountToken0);\r\n        uint256 amountReceived = v2Router.swapExactTokensForTokens(\r\n            amountToken0,\r\n            amountRequired,\r\n            path,\r\n            address(this),\r\n            block.timestamp+deadline\r\n        )[1];\r\n        // return token1 back to pool\r\n        IERC20(token1).transfer(msg.sender, amountRequired);\r\n        // (amountReceived - amountRequired) token0 will be profit\r\n        emit Logs(amountReceived - amountRequired);\r\n    }\r\n\r\n    function withdrawBalance(address token,uint256 amount) external onlyOwner {\r\n        require(amount > 0, \"amount==0\");\r\n        if(token==address(0x0)){\r\n            msg.sender.transfer(amount);\r\n        }else{\r\n            IERC20(token).transfer(msg.sender, amount);\r\n        }\r\n    }\r\n\r\n    function setV2Router(address _v2Router,address _v2Factory) external onlyOwner {\r\n        v2Router = IUniswapV2Router02(_v2Router);\r\n        v2Factory = _v2Factory;\r\n    }\r\n\r\n    function setV3Router(address _v3Router,address _v3Factory) external onlyOwner {\r\n        v3Router = ISwapRouter(_v3Router);\r\n        v3Factory = _v3Factory;\r\n    }\r\n}\r\n\r\n```"},"author":{"user":"https://learnblockchain.cn/people/2894","address":"0xBDF9255fD9A205a5Cd64DDa55E5Bc609975A6968"},"history":null,"timestamp":1675408709,"version":1}