写一个uniswap价差套利的合约

  • biakia
  • 更新于 2023-02-03 15:18
  • 阅读 6662

价差套利所谓的价差套利,本质上就是低买高卖。举个例子,假设sushiswap里的ETH价值1500USDT,uniswap里的ETH价值1600USDT,那么我们可以以1500USDT在sushiswap里买入1ETH,然后在uniswap里卖出获得1600USDT

1. 价差套利

所谓的价差套利,本质上就是低买高卖。举个例子,假设sushiswap里的ETH价值1500USDT,uniswap里的ETH价值1600USDT,那么我们可以以1500USDT在sushiswap里买入1ETH,然后在uniswap里卖出获得1600USDT,假设手续费是5U,那么最终获利95USDT。这里有个问题,那就是我们必须先用自己的1500USDT去买ETH,假设我们没有这么多钱怎么办呢?uniswap给我们提供了类似flash loan的flash swap功能,可以让我们无成本进行套利。

2. UniswapV2里的flash swap原理

在UniswapV2里,真正执行swap的过程是发生在UniswapV2Pair里的,下面让我们看看UniswapV2Pair的swap函数:

function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
        require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
        (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
        require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');

        uint balance0;
        uint balance1;
        { // scope for _token{0,1}, avoids stack too deep errors
        address _token0 = token0;
        address _token1 = token1;
        require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
        if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
        if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
        if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
        balance0 = IERC20(_token0).balanceOf(address(this));
        balance1 = IERC20(_token1).balanceOf(address(this));
        }
        uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
        uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
        require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
        { // scope for reserve{0,1}Adjusted, avoids stack too deep errors
        uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
        uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
        require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
        }

        _update(balance0, balance1, _reserve0, _reserve1);
        emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
    }

我们知道,一次swap,其实是用一种token去换另一种token,你把你要卖掉的token转给pair,pair给你转你想要的token。swap函数有四个参数,其中amount0Outamount1Out 是指你想要获得的那个token的数量。比如你想卖token1获得100e18个token0,你就需要传入amount0Out = 100e18 以及 amount1Out = 0,这时候pair会先把100e18的token0转给你,也就是下面这行代码:

if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens

然后判断data是否有值,如果有值,就说明是一次flash swap,它会把调用者指定的to地址当成一个实现了IUniswapV2Callee接口的合约,然后调用这个合约的uniswapV2Call方法:

 if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);

因为是先把token0转给to地址,然后再调用uniswapV2Call,这样就相当于to地址的那个合约先问pair借了一笔钱,接下来只要在uniswapV2Call方法把一定的token1转到pair,并且满足K值公式,那么整个swap就是合法的。

回到第一节的那个例子,我们如何进行套利呢?这里我们不需要第一步用1500USDT去买1ETH,我们可以直接找到sushiswap的ETH-USDT的pair,然后amount0Out传入1e18,to传入一个你的合约地址,这时候pair就会把1个ETH转到你的合约地址,然后你需要在uniswapV2Call方法里使用uniswap把1ETH换成1600USDT,然后把1500USDT转回sushiswap的ETH-USDT的pair里就可以了,剩下的USDT就会留在你的合约中,成为你的利润。

3 UniswapV2里的flash swap实现

function startArbitrageFromV2( 
        address pool,
        uint256 amountBorrowed)external onlyOwner{
        IUniswapV3Pool v3Pool = IUniswapV3Pool(pool);
        address token0 = v3Pool.token0();
        address token1 = v3Pool.token1();
        address pairAddress =
            IUniswapV2Factory(v2Factory).getPair(token0, token1);
        require(pairAddress != address(0), "This pool does not exist");
        bytes memory data = abi.encode(
            pool,
            amountBorrowed
        );
        // borrow token0 from V2
        IUniswapV2Pair(pairAddress).swap(
            amountBorrowed,
            0,
            address(this), 
            data
        );
    }

首先我们写一个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)。

接下来我们实现uniswapV2Call方法:

function uniswapV2Call(
        address _sender,
        uint256 _amount0,
        uint256 _amount1,
        bytes calldata _data
    ) external {
        address token0 = IUniswapV2Pair(msg.sender).token0();
        address token1 = IUniswapV2Pair(msg.sender).token1();
        require(
            msg.sender == UniswapV2Library.pairFor(v2Factory, token0, token1),
            "not from swap pair"
        );

        (address pool,,)=abi.decode(_data,(address,uint256,uint256));
        IUniswapV3Pool v3Pool = IUniswapV3Pool(pool);
        uint24 fee = v3Pool.fee();

        require(_amount0 > 0,"amount invalid");

        //V3 sell token0 get token1
        address[] memory path = new address[](2);
        path[0] = token0;
        path[1] = token1;
        IERC20(token0).approve(address(v3Router), _amount0);

        uint256 amountShouldReturned =
            UniswapV2Library.getAmountsIn(v2Factory, _amount0, path)[0];

        ISwapRouter.ExactInputSingleParams memory params = ISwapRouter
            .ExactInputSingleParams({
                tokenIn: path[0],
                tokenOut: path[1],
                fee: fee,
                recipient: address(this),
                deadline: block.timestamp+deadline,
                amountIn: _amount0,
                amountOutMinimum: amountShouldReturned,
                sqrtPriceLimitX96: 0
            });
        uint256 amountReceived = v3Router.exactInputSingle(params);
        //return back token1 to V2
        IERC20(token1).transfer(msg.sender, amountShouldReturned);
        emit Logs(amountReceived - amountShouldReturned);
    }

由于这个方法是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作为利润。

4 总结

本文实现了一个从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之间的套利空间就变得很小。

5 代码

代码中除了实现了V2->V3的套利,还实现了V3->V2的套利,具体代码见startArbitrageFromV3和uniswapV3SwapCallback。 注意:这部分代码没有严格测试过,如果想使用,请自担风险!

//SPDX-License-Identifier: MIT
pragma solidity >=0.7.5;
pragma abicoder v2;
import "./UniswapV2Library.sol";
import "./interfaces/IUniswapV2Router02.sol";
import "./interfaces/IUniswapV2Pair.sol";
import "./interfaces/IUniswapV2Factory.sol";
import "./interfaces/IERC20.sol";
import "./uniswap/v3-core/interfaces/IUniswapV3Pool.sol";
import "./uniswap/v3-periphery/interfaces/ISwapRouter.sol";
import "./uniswap/v3-periphery/libraries/PoolAddress.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract ArbitrageSwapWithFlashLoaner is Ownable{

    uint256 constant deadline = 100;
    uint160 internal constant MIN_SQRT_RATIO = 4295128739;
    uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342;
    IUniswapV2Router02 public v2Router;
    address public v2Factory;
    ISwapRouter public v3Router;
    address public v3Factory;

    event Logs(uint256 amount);

    constructor(
        address _v2Factory,
        address _v3Factory,
        address _v2Router,
        address _v3Router
    ) public {
        v2Factory = _v2Factory;
        v3Factory = _v3Factory;
        v2Router = IUniswapV2Router02(_v2Router);
        v3Router = ISwapRouter(_v3Router);
    }

    function startArbitrageFromV2( 
        address pool,
        uint256 amountBorrowed)external onlyOwner{
        IUniswapV3Pool v3Pool = IUniswapV3Pool(pool);
        address token0 = v3Pool.token0();
        address token1 = v3Pool.token1();
        address pairAddress =
            IUniswapV2Factory(v2Factory).getPair(token0, token1);
        require(pairAddress != address(0), "This pool does not exist");
        bytes memory data = abi.encode(
            pool,
            amountBorrowed
        );
        // borrow token0 from V2
        IUniswapV2Pair(pairAddress).swap(
            amountBorrowed,
            0,
            address(this), 
            data
        );
    }

    function uniswapV2Call(
        address _sender,
        uint256 _amount0,
        uint256 _amount1,
        bytes calldata _data
    ) external {
        address token0 = IUniswapV2Pair(msg.sender).token0();
        address token1 = IUniswapV2Pair(msg.sender).token1();
        require(
            msg.sender == UniswapV2Library.pairFor(v2Factory, token0, token1),
            "not from swap pair"
        );

        (address pool,,)=abi.decode(_data,(address,uint256,uint256));
        IUniswapV3Pool v3Pool = IUniswapV3Pool(pool);
        uint24 fee = v3Pool.fee();

        require(_amount0 > 0,"amount invalid");

        //V3 sell token0 get token1
        address[] memory path = new address[](2);
        path[0] = token0;
        path[1] = token1;
        IERC20(token0).approve(address(v3Router), _amount0);

        uint256 amountShouldReturned =
            UniswapV2Library.getAmountsIn(v2Factory, _amount0, path)[0];

        ISwapRouter.ExactInputSingleParams memory params = ISwapRouter
            .ExactInputSingleParams({
                tokenIn: path[0],
                tokenOut: path[1],
                fee: fee,
                recipient: address(this),
                deadline: block.timestamp+deadline,
                amountIn: _amount0,
                amountOutMinimum: amountShouldReturned,
                sqrtPriceLimitX96: 0
            });
        uint256 amountReceived = v3Router.exactInputSingle(params);
        //return back token1 to V2
        IERC20(token1).transfer(msg.sender, amountShouldReturned);
        emit Logs(amountReceived - amountShouldReturned);
    }

    function startArbitrageFromV3(
        address pool,
        uint256 amountBorrowed) external onlyOwner{
        IUniswapV3Pool v3Pool = IUniswapV3Pool(pool);
        bytes memory data = abi.encode(
            pool,
            amountBorrowed
        );
        //borrow token0 from v3
        IUniswapV3Pool(v3Pool).swap(
            address(this),
            false,
            int(amountBorrowed),
            MAX_SQRT_RATIO-1,
            data
        );
    }

    function uniswapV3SwapCallback(
        int amount0,
        int amount1,
        bytes calldata data
    ) external{
        IUniswapV3Pool v3Pool = IUniswapV3Pool(msg.sender);
        address token0 = v3Pool.token0();
        address token1 = v3Pool.token1();
        uint24  fee    = v3Pool.fee();
        require(msg.sender ==PoolAddress.computeAddress(v3Factory,PoolAddress.getPoolKey(token0, token1, fee)), "not authorized");
        uint256 amountRequired = uint256(amount1);
        uint256 amountToken0 = uint256(-amount0);

        //v2 sell token0 and get token1
        address[] memory path = new address[](2);
        path[0] = token0;
        path[1] = token1;
        IERC20(token0).approve(address(v2Router), amountToken0);
        uint256 amountReceived = v2Router.swapExactTokensForTokens(
            amountToken0,
            amountRequired,
            path,
            address(this),
            block.timestamp+deadline
        )[1];
        // return token1 back to pool
        IERC20(token1).transfer(msg.sender, amountRequired);
        // (amountReceived - amountRequired) token0 will be profit
        emit Logs(amountReceived - amountRequired);
    }

    function withdrawBalance(address token,uint256 amount) external onlyOwner {
        require(amount > 0, "amount==0");
        if(token==address(0x0)){
            msg.sender.transfer(amount);
        }else{
            IERC20(token).transfer(msg.sender, amount);
        }
    }

    function setV2Router(address _v2Router,address _v2Factory) external onlyOwner {
        v2Router = IUniswapV2Router02(_v2Router);
        v2Factory = _v2Factory;
    }

    function setV3Router(address _v3Router,address _v3Factory) external onlyOwner {
        v3Router = ISwapRouter(_v3Router);
        v3Factory = _v3Factory;
    }
}
点赞 8
收藏 19
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
biakia
biakia
0x2464...d1BB
江湖只有他的大名,没有他的介绍。