Uniswap v2-Flash Swap-Twap-Arbitrage

jerichou
2025-08-10 / 0 评论 / 3 阅读 / 正在检测是否收录...

一、Flash Swap

在 Uniswap 的 Flash Swap(闪电兑换) 机制中,要求交易后储备金满足 reserve_after >= reserve_before 的核心目的是为了确保 流动性提供者(LP)的本金安全系统的偿付能力。以下是具体分析:

Flash Swap 的基本流程

Flash Swap 允许用户 无需预先支付代币 即可临时借出资金,但需在同一笔交易内完成还款。其关键步骤如下:

  1. 借出代币:用户从交易对中借出代币(如 amount0Out > 0amount1Out > 0)。
  2. 执行回调:用户合约实现 uniswapV2Call,在回调中完成任意操作(如套利、还款)。
  3. 还款验证:合约检查还款后的储备金是否满足 reserve_after >= reserve_before
    me5ike3u.png
    me5ikqgn.png
    me5il4vi.png
// Uniswap/v2-core/contracts/UniswapV2Pair.sol
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);
    }

当参数传递data时将开启 falsh swap

分叉主网测试 flash swap
// test/UniswapV2FlashSwap.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {IUniswapV2Pair} from "../src/interfaces/uniswap-v2/IUniswapV2Pair.sol";
import {IERC20} from "../src/interfaces/IERC20.sol";

error InvalidToken();

contract UniswapV2FlashSwap {
    IUniswapV2Pair private immutable pair;
    address private immutable token0;
    address private immutable token1;

    constructor(address _pair) {
        pair = IUniswapV2Pair(_pair);
        token0 = pair.token0();
        token1 = pair.token1();
    }

    function flashSwap(address token, uint256 amount) external {
        if (token != token0 && token != token1) {
            revert InvalidToken();
        }

        // 1. Determine amount0Out and amount1Out
        uint256 amount0Out = token == token0 ? amount : 0;
        uint256 amount1Out = token == token1 ? amount : 0;

        // 2. Encode token and msg.sender as bytes
        bytes memory data = abi.encode(token, msg.sender);

        // 3. Call pair.swap
        pair.swap(amount0Out, amount1Out, address(this), data);
    }

    // Uniswap V2 callback
    function uniswapV2Call(address sender, uint256 amount0, uint256 amount1, bytes calldata data) external {
        require(msg.sender == address(pair), "!pair");
        require(sender == address(this), "!sender");
        // 1. Require msg.sender is pair contract
        // 2. Require sender is this contract
        // Alice -> FlashSwap ---- to = FlashSwap ----> UniswapV2Pair
        //                    <-- sender = FlashSwap --
        // Eve ------------ to = FlashSwap -----------> UniswapV2Pair
        //          FlashSwap <-- sender = Eve --------

        // 3. Decode token and caller from data
        (address token, address caller) = abi.decode(data, (address, address));
        // 4. Determine amount borrowed (only one of them is > 0)
        uint256 amount = token == token0 ? amount0 : amount1;

        // 5. Calculate flash swap fee and amount to repay
        // fee = borrowed amount * 3 / 997 + 1 to round up
        uint256 fee = (amount * 3) / 997 + 1;
        uint256 amountToRepay = amount + fee;

        // 6. Get flash swap fee from caller
        IERC20(token).transferFrom(caller, address(this), fee);

        // 7. Repay Uniswap V2 pair
        IERC20(token).transfer(address(pair), amountToRepay);
    }
}
// test/UniswapV2FlashSwap.test.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {Test, console2} from "forge-std/Test.sol";
import {IERC20} from "../src/interfaces/IERC20.sol";
import {IUniswapV2Router02} from "../src/interfaces/uniswap-v2/IUniswapV2Router02.sol";
import {DAI, UNISWAP_V2_ROUTER_02, UNISWAP_V2_PAIR_DAI_WETH} from "../src/Constants.sol";
import {UniswapV2FlashSwap} from "./UniswapV2FlashSwap.sol";

contract UniswapV2FlashSwapTest is Test {
    IERC20 private constant dai = IERC20(DAI);

    IUniswapV2Router02 private constant router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02);
    UniswapV2FlashSwap private flashSwap;

    address private constant user = address(100);

    function setUp() public {
        flashSwap = new UniswapV2FlashSwap(UNISWAP_V2_PAIR_DAI_WETH);

        deal(DAI, user, 10000 * 1e18);
        vm.prank(user);
        dai.approve(address(flashSwap), type(uint256).max);
        // user -> flashSwap.flashSwap
        //         -> pair.swap
        //            -> flashSwap.uniswapV2Call
        //               -> token.transferFrom(user, flashSwap, fee)
    }

    function test_flashSwap() public {
        uint256 dai0 = dai.balanceOf(UNISWAP_V2_PAIR_DAI_WETH);
        vm.prank(user);
        flashSwap.flashSwap(DAI, 1e6 * 1e18);
        uint256 dai1 = dai.balanceOf(UNISWAP_V2_PAIR_DAI_WETH);

        console2.log("DAI fee", dai1 - dai0);
        assertGe(dai1, dai0, "DAI balance of pair");
    }
}

二、Twap

Uniswap V2 作为价格预言机存在被攻击的风险,主要源于其链上价格延迟性流动性池的可操纵性。攻击者常通过 借贷协议(Lending Protocol) 或其他杠杆手段放大攻击效果。以下是具体分析:


1. Uniswap V2 预言机的核心问题

(1) 价格延迟机制

Uniswap V2 的预言机基于 累计价格(Price Cumulative),通过记录每个区块第一笔交易的价格时间积分(TWAP)来平滑价格波动。但存在两个关键弱点:

  • 时间窗口依赖:短期价格波动仍可能被操纵(尤其在低流动性池中)。
  • 区块间隔影响:价格更新频率取决于区块生成速度(如以太坊约12秒/块)。
(2) 流动性池的脆弱性
  • 低流动性池:小资金即可大幅改变价格。
  • 瞬时价格依赖:借贷协议可能直接读取瞬时价格(而非 TWAP),加剧风险。

2. 典型攻击流程(结合借贷协议)

以下是攻击者通过 操纵价格 → 欺骗借贷协议 → 套利获利 的常见步骤:

步骤1:操纵 Uniswap 价格
  • 攻击手段

    1. 通过闪电贷借入大量代币(如 USDC)。
    2. 在 Uniswap 低流动性池中大幅买入/卖出目标代币(如 ETH),扭曲瞬时价格。
    • 例如:用 100 万 USDC 买入 ETH,将池子价格从 1 ETH = 2000 USDC 推高到 1 ETH = 3000 USDC。
    1. 借贷协议读取被操纵后的高价,认为抵押物价值虚高。
步骤2:欺骗借贷协议
  • 攻击操作

    1. 以扭曲后的 ETH 价格作为抵押,借出超额其他资产(如 DAI)。
    • 例如:按 1 ETH = 3000 USDC 的虚假价格抵押 100 ETH,借出 200,000 DAI(实际 ETH 仅值 2000 USDC)。
    1. 恢复 Uniswap 价格(通过反向交易或等待 TWAP 平滑)。
步骤3:套利与获利
  • 最终获利

    • 通过操纵价格虚增抵押物价值 → 借出超过抵押物实际价值的资金 → 放弃抵押物赚取差价
    • 借贷协议出现坏账,损失由其他用户承担。

3. 真实案例:Harvest Finance 攻击(2020)

  • 攻击细节
    1. 攻击者通过闪电贷借入大量 USDC。
    2. 操纵 USDC/fUSDT 池的价格,导致 Harvest Finance 的质押合约高估 fUSDT 价值。
    3. 存入少量 fUSDT 提取超额收益,获利约 2400 万美元。
  • 根本原因:协议直接依赖 Uniswap 的瞬时价格,未使用 TWAP 或交叉验证。

4. 防御措施

(1) 协议层面的改进
  • 使用 TWAP 而非瞬时价格

    例如:Chainlink 或 Uniswap V3 的预言机,需至少 10 分钟的时间窗口平滑价格。

  • 多预言机交叉验证

    结合 Chainlink、Uniswap TWAP 和中心化交易所价格,降低单点故障风险。

  • 流动性门槛

    仅允许高流动性池(如 TVL > 100 万美元)作为价格源。

(2) Uniswap V2 的局限性
  • V2 的设计缺陷

    累计价格仍可能被短时大额交易扭曲,尤其在低流动性池中。

  • 升级到 V3

    Uniswap V3 提供更精细的预言机(如区间 TWAP),抗操纵性更强。

me5iluo8.png
me5im8tx.png
me5imjwe.png

// Uniswap/v2-core/contracts/UniswapV2Pair.sol
function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
        require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
        uint32 blockTimestamp = uint32(block.timestamp % 2**32);
        uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
        if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
            // * never overflows, and + overflow is desired
            price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
            price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
        }
        reserve0 = uint112(balance0);
        reserve1 = uint112(balance1);
        blockTimestampLast = blockTimestamp;
        emit Sync(reserve0, reserve1);
    }

这段代码负责 更新累计价格(price0CumulativeLast/price1CumulativeLast,用于实现 Uniswap V2 的 TWAP 预言机。其核心逻辑是:

  • 计算自上次更新后经过的时间(timeElapsed)。
  • 根据当前储备金比例(reserve1/reserve0reserve0/reserve1)计算价格,并加权累加到历史数据中。
price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
  • UQ112x112.encode(_reserve1).uqdiv(_reserve0)

    • _reserve1 编码为 UQ112x112) 定点数(112位整数 + 112位小数)。相当于左移 112 位
    • 执行定点数除法 reserve1reserve0reserve0reserve1,得到 token1 相对于 token0 的价格。
  • \* timeElapsed
    将价格按时间加权累加。例如:

    • 若 1 ETH = 2000 USDT(即 USDTETH=2000ETHUSDT=2000),且 timeElapsed = 30 秒,则累加 2000×30=60,0002000×30=60,000 到 price0CumulativeLast
  • 价格部分占用 224 位,时间部分占用 32 位,刚好是 256 位所以乘法不会溢出
  • 加法溢出无害性

    • TWAP 计算依赖差值
      外部合约(如预言机)计算 TWAP 时,仅需两个时间点的累计价格差:

      TWAP=priceCumulative(t2)−priceCumulative(t1)t2−t1TWAP=t2−t1priceCumulative(t2)−priceCumulative(t1)

      • 即使 priceCumulative 溢出(回绕到 0),差值在模运算下仍正确
      • 例如:

        • priceCumulative(t1)=2256−1000priceCumulative(t1)=2256−1000
        • priceCumulative(t2)=500priceCumulative(t2)=500(溢出后)
        • 实际差值:500−(2256−1000)≡1500mod  2256500−(2256−1000)≡1500mod2256(与未溢出时一致)。
fork 主网测试
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {Test, console2} from "forge-std/Test.sol";
import {IERC20} from "../src/interfaces/IERC20.sol";
import {IUniswapV2Router02} from "../src/interfaces/uniswap-v2/IUniswapV2Router02.sol";
import {IUniswapV2Pair} from "../src/interfaces/uniswap-v2/IUniswapV2Pair.sol";
import {DAI, WETH, UNISWAP_V2_ROUTER_02, UNISWAP_V2_PAIR_DAI_WETH} from "../src/Constants.sol";
import {UniswapV2Twap} from "./UniswapV2Twap.sol";

contract UniswapV2TwapTest is Test {
    IERC20 private constant weth = IERC20(WETH);
    IUniswapV2Pair private constant pair = IUniswapV2Pair(UNISWAP_V2_PAIR_DAI_WETH);
    IUniswapV2Router02 private constant router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02);

    uint256 private constant MIN_WAIT = 300;

    UniswapV2Twap private twap;

    function setUp() public {
        twap = new UniswapV2Twap(address(pair));
    }

    function getSpot() internal view returns (uint256) {
        (uint112 reserve0, uint112 reserve1,) = pair.getReserves();
        // DAI / WETH
        return uint256(reserve0) * 1e18 / uint256(reserve1);
    }

    function swap() internal {
        deal(WETH, address(this), 100 * 1e18);
        weth.approve(address(router), type(uint256).max);

        address[] memory path = new address[](2);
        path[0] = WETH;
        path[1] = DAI;

        // Input token amount and all subsequent output token amounts
        router.swapExactTokensForTokens({
            amountIn: 100 * 1e18,
            amountOutMin: 1,
            path: path,
            to: address(this),
            deadline: block.timestamp
        });
    }

    function test_twap_same_price() public {
        skip(MIN_WAIT + 1);
        twap.update();

        uint256 twap0 = twap.consult(WETH, 1e18);

        skip(MIN_WAIT + 1);
        twap.update();

        uint256 twap1 = twap.consult(WETH, 1e18);

        assertApproxEqAbs(twap0, twap1, 1, "ETH TWAP");
    }

    function test_twap_close_to_last_spot() public {
        // Update TWAP
        skip(MIN_WAIT + 1);
        twap.update();

        // Get TWAP
        uint256 twap0 = twap.consult(WETH, 1e18);

        // Swap
        swap();
        uint256 spot = getSpot();
        console2.log("ETH spot price", spot);

        // Update TWAP
        skip(MIN_WAIT + 1);
        twap.update();

        // Get TWAP
        uint256 twap1 = twap.consult(WETH, 1e18);

        console2.log("twap0", twap0);
        console2.log("twap1", twap1);

        // Check TWAP is close to last spot
        assertLt(twap1, twap0, "twap1 >= twap0");
        assertGe(twap1, spot, "twap1 < spot");
    }
}
// SPDX-License-Identifier: MIT
pragma solidity >= 0.4 < 0.9;

import {IUniswapV2Pair} from "../src/interfaces/uniswap-v2/IUniswapV2Pair.sol";
import {FixedPoint} from "../src/uniswap-v2/FixedPoint.sol";

// Modified from https://github.com/Uniswap/v2-periphery/blob/master/contracts/examples/ExampleOracleSimple.sol
// Do not use this contract in production
contract UniswapV2Twap {
    using FixedPoint for *;

    // Minimum wait time in seconds before the function update can be called again
    // TWAP of time > MIN_WAIT
    uint256 private constant MIN_WAIT = 300;

    IUniswapV2Pair public immutable pair;
    address public immutable token0;
    address public immutable token1;

    // Cumulative prices are uq112x112 price * seconds
    uint256 public price0CumulativeLast;
    uint256 public price1CumulativeLast;
    // Last timestamp the cumulative prices were updated
    uint32 public updatedAt;

    // TWAP of token0 and token1
    // range: [0, 2**112 - 1]
    // resolution: 1 / 2**112
    // TWAP of token0 in terms of token1
    FixedPoint.uq112x112 public price0Avg;
    // TWAP of token1 in terms of token0
    FixedPoint.uq112x112 public price1Avg;

    // Exercise 1
    constructor(address _pair) {
        // 1. Set pair contract from constructor input
        pair = IUniswapV2Pair(_pair);
        // 2. Set token0 and token1 from pair contract
        token0 = pair.token0();
        token1 = pair.token1();
        // 3. Store price0CumulativeLast and price1CumulativeLast from pair contract
        (price0CumulativeLast, price1CumulativeLast) = (pair.price0CumulativeLast(), pair.price1CumulativeLast());

        // 4. Call pair.getReserve to get last timestamp the reserves were updated
        //    and store it into the state variable updatedAt
        (,, updatedAt) = pair.getReserves();
    }

    // Exercise 2
    // Calculates cumulative prices up to current timestamp
    function _getCurrentCumulativePrices() internal view returns (uint256 price0Cumulative, uint256 price1Cumulative) {
        // 1. Get latest cumulative prices from the pair contract
        (price0Cumulative, price1Cumulative) = (pair.price0CumulativeLast(), pair.price1CumulativeLast());

        // If current block timestamp > last timestamp reserves were updated,
        // calculate cumulative prices until current time.
        // Otherwise return latest cumulative prices retrieved from the pair contract.

        // 2. Get reserves and last timestamp the reserves were updated from
        //    the pair contract
        (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast) = pair.getReserves();

        // 3. Cast block.timestamp to uint32
        uint32 blockTimestamp = uint32(block.timestamp);
        if (blockTimestampLast != blockTimestamp) {
            // 4. Calculate elapsed time
            uint32 dt = blockTimestamp - blockTimestampLast;

            // Addition overflow is desired
            unchecked {
                // 5. Add spot price * elapsed time to cumulative prices.
                //    - Use FixedPoint.fraction to calculate spot price.
                //    - FixedPoint.fraction returns UQ112x112, so cast it into uint256.
                //    - Multiply spot price by time elapsed
                price0Cumulative += uint256(FixedPoint.fraction(reserve1, reserve0)._x) * dt;
                price1Cumulative += uint256(FixedPoint.fraction(reserve0, reserve1)._x) * dt;
            }
        }
    }

    // Exercise 3
    // Updates cumulative prices
    function update() external {
        // 1. Cast block.timestamp to uint32
        uint32 blockTimestamp = uint32(block.timestamp);
        // 2. Calculate elapsed time since last time cumulative prices were
        //    updated in this contract
        uint32 dt = blockTimestamp - updatedAt;
        // 3. Require time elapsed >= MIN_WAIT
        require(dt >= MIN_WAIT, "UniswapV2Twap: MIN_WAIT");
        // 4. Call the internal function _getCurrentCumulativePrices to get
        //    current cumulative prices
        (uint256 price0Cumulative, uint256 price1Cumulative) = _getCurrentCumulativePrices();

        // Overflow is desired, casting never truncates
        // https://docs.uniswap.org/contracts/v2/guides/smart-contract-integration/building-an-oracle
        // Subtracting between two cumulative price values will result in
        // a number that fits within the range of uint256 as long as the
        // observations are made for periods of max 2^32 seconds, or ~136 years
        unchecked {
            // 5. Calculate TWAP price0Avg and price1Avg
            //    - TWAP = (current cumulative price - last cumulative price) / dt
            //    - Cast TWAP into uint224 and then into FixedPoint.uq112x112
            price0Avg = FixedPoint.uq112x112(uint224((price0Cumulative - price0CumulativeLast) / dt));
            price1Avg = FixedPoint.uq112x112(uint224((price1Cumulative - price1CumulativeLast) / dt));
        }

        // 6. Update state variables price0CumulativeLast, price1CumulativeLast and updatedAt
        (price0CumulativeLast, price1CumulativeLast) = (price0Cumulative, price1Cumulative);
        updatedAt = blockTimestamp;
    }

    // Exercise 4
    // Returns the amount out corresponding to the amount in for a given token
    function consult(address tokenIn, uint256 amountIn) external view returns (uint256 amountOut) {
        // 1. Require tokenIn is either token0 or token1
        require(tokenIn == token0 || tokenIn == token1, "UniswapV2Twap: INVALID_TOKEN");
        // 2. Calculate amountOut
        //    - amountOut = TWAP of tokenIn * amountIn
        //    - Use FixePoint.mul to multiply TWAP of tokenIn with amountIn
        //    - FixedPoint.mul returns uq144x112, use FixedPoint.decode144 to return uint144
        if (tokenIn == token0) {
            // Example
            //   token0 = WETH
            //   token1 = USDC
            //   price0Avg = avg price of WETH in terms of USDC = 2000 USDC / 1 WETH
            //   tokenIn = WETH
            //   amountIn = 2
            //   amountOut = price0Avg * amountIn = 4000 USDC
            amountOut = FixedPoint.mul(price0Avg, amountIn).decode144();
        } else {
            amountOut = FixedPoint.mul(price1Avg, amountIn).decode144();
        }
    }
}

三、Arb

在 Uniswap V2 中,套利(Arbitrage) 是指利用不同交易所或同一交易所内的价格差异,通过低买高卖赚取无风险利润的行为。Uniswap V2 作为一个去中心化交易所(DEX),其价格完全由流动性池(Liquidity Pool)的算法决定,因此套利者在维持市场价格与全球市场价格一致方面扮演了关键角色。
me5inli0.png

分叉主网测试套利
  • 策略1:使用本金直接套利。
  • 策略 2:借出 tokenIn → 兑换 → 反向兑换 → 归还 tokenIn
// UniswapV2Arb1.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {IUniswapV2Pair} from "../src/interfaces/uniswap-v2/IUniswapV2Pair.sol";
import {IUniswapV2Router02} from "../src/interfaces/uniswap-v2/IUniswapV2Router02.sol";
import {IERC20} from "../src/interfaces/IERC20.sol";

contract UniswapV2Arb1 {
    error UniswapV2Arb1__InsufficientProfit();

    struct SwapParams {
        // Router to execute first swap - tokenIn for tokenOut
        address router0;
        // Router to execute second swap - tokenOut for tokenIn
        address router1;
        // Token in of first swap
        address tokenIn;
        // Token out of first swap
        address tokenOut;
        // Amount in for the first swap
        uint256 amountIn;
        // Revert the arbitrage if profit is less than this minimum
        uint256 minProfit;
    }

    function _swap(SwapParams memory params) internal returns (uint256 amountOut) {
        IERC20(params.tokenIn).approve(params.router0, params.amountIn);

        address[] memory path = new address[](2);
        path[0] = params.tokenIn;
        path[1] = params.tokenOut;
        uint256[] memory amounts = IUniswapV2Router02(params.router0).swapExactTokensForTokens({
            amountIn: params.amountIn,
            amountOutMin: 0,
            path: path,
            to: address(this),
            deadline: block.timestamp
        });

        IERC20(params.tokenOut).approve(params.router1, amounts[1]);
        path[0] = params.tokenOut;
        path[1] = params.tokenIn;
        amounts = IUniswapV2Router02(params.router1).swapExactTokensForTokens({
            amountIn: amounts[1],
            amountOutMin: 1,
            path: path,
            to: address(this),
            deadline: block.timestamp
        });

        amountOut = amounts[1];
    }
    // - Execute an arbitrage between router0 and router1
    // - Pull tokenIn from msg.sender
    // - Send amountIn + profit back to msg.sender

    function swap(SwapParams calldata params) external {
        IERC20(params.tokenIn).transferFrom(msg.sender, address(this), params.amountIn);
        uint256 amountOut = _swap(params);
        if (amountOut - params.amountIn < params.minProfit) {
            revert UniswapV2Arb1__InsufficientProfit();
        }
        IERC20(params.tokenIn).transfer(msg.sender, amountOut);
    }

    // - Execute an arbitrage between router0 and router1 using flash swap
    // - Borrow tokenIn with flash swap from pair
    // - Send profit back to msg.sender
    /**
     * @param pair Address of pair contract to flash swap and borrow tokenIn
     * @param isToken0 True if token to borrow is token0 of pair
     * @param params Swap parameters
     */
    function flashSwap(address pair, bool isToken0, SwapParams calldata params) external {
        IUniswapV2Pair uniswap_pair = IUniswapV2Pair(pair);

        // 1. Determine amount0Out and amount1Out
        uint256 amount0Out = isToken0 ? params.amountIn : 0;
        uint256 amount1Out = isToken0 ? 0 : params.amountIn;

        // 2. Encode pair and msg.sender、params as bytes
        bytes memory data = abi.encode(msg.sender, pair, params);

        // 3. Call pair.swap
        uniswap_pair.swap(amount0Out, amount1Out, address(this), data);
    }

    function uniswapV2Call(address sender, uint256 amount0Out, uint256 amount1Out, bytes calldata data) external {
        require(sender == address(this), "!sender");

        (address caller, address pair, SwapParams memory params) = abi.decode(data, (address, address, SwapParams));
        require(msg.sender == pair, "!pair");

        uint256 amountOut = _swap(params);

        // Calculate flash swap fee and amount to repay
        // fee = borrowed amount * 3 / 997 + 1 to round up
        uint256 fee = (params.amountIn * 3) / 997 + 1;
        uint256 amountToRepay = params.amountIn + fee;

        //
        uint256 profit = amountOut - amountToRepay;
        if (profit < params.minProfit) {
            revert UniswapV2Arb1__InsufficientProfit();
        }

        IERC20(params.tokenIn).transfer(address(pair), amountToRepay);
        IERC20(params.tokenIn).transfer(caller, profit);
    }
}
//UniswapV2Arb1.test.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {Test, console2} from "forge-std/Test.sol";
import {IERC20} from "../src/interfaces/IERC20.sol";
import {IWETH} from "../src/interfaces/IWETH.sol";
import {IUniswapV2Router02} from "../src/interfaces/uniswap-v2/IUniswapV2Router02.sol";
import {
    DAI,
    WETH,
    UNISWAP_V2_ROUTER_02,
    SUSHISWAP_V2_ROUTER_02,
    UNISWAP_V2_PAIR_DAI_WETH,
    UNISWAP_V2_PAIR_DAI_MKR
} from "../src/Constants.sol";
import {UniswapV2Arb1} from "./UniswapV2Arb1.sol";

// Test arbitrage between Uniswap and Sushiswap
// Buy WETH on Uniswap, sell on Sushiswap.
// For flashSwap, borrow DAI from DAI/MKR pair
contract UniswapV2Arb1Test is Test {
    IUniswapV2Router02 private constant uni_router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02);
    IUniswapV2Router02 private constant sushi_router = IUniswapV2Router02(SUSHISWAP_V2_ROUTER_02);
    IERC20 private constant dai = IERC20(DAI);
    IWETH private constant weth = IWETH(WETH);
    address constant user = address(11);

    UniswapV2Arb1 private arb;

    function setUp() public {
        arb = new UniswapV2Arb1();

        // Setup - WETH cheaper on Uniswap than Sushiswap
        deal(address(this), 100 * 1e18);

        weth.deposit{value: 100 * 1e18}();
        weth.approve(address(uni_router), type(uint256).max);

        address[] memory path = new address[](2);
        path[0] = WETH;
        path[1] = DAI;

        uni_router.swapExactTokensForTokens({
            amountIn: 100 * 1e18,
            amountOutMin: 1,
            path: path,
            to: user,
            deadline: block.timestamp
        });

        // Setup - user has DAI, approves arb to spend DAI
        deal(DAI, user, 10000 * 1e18);
        vm.prank(user);
        dai.approve(address(arb), type(uint256).max);
    }

    function test_swap() public {
        uint256 bal0 = dai.balanceOf(user);
        vm.prank(user);
        arb.swap(
            UniswapV2Arb1.SwapParams({
                router0: UNISWAP_V2_ROUTER_02,
                router1: SUSHISWAP_V2_ROUTER_02,
                tokenIn: DAI,
                tokenOut: WETH,
                amountIn: 10000 * 1e18,
                minProfit: 1
            })
        );
        uint256 bal1 = dai.balanceOf(user);

        assertGe(bal1, bal0, "no profit");
        assertEq(dai.balanceOf(address(arb)), 0, "DAI balance of arb != 0");
        console2.log("profit", bal1 - bal0);
    }

    function test_flashSwap() public {
        uint256 bal0 = dai.balanceOf(user);
        vm.prank(user);
        arb.flashSwap(
            UNISWAP_V2_PAIR_DAI_MKR,
            true,
            UniswapV2Arb1.SwapParams({
                router0: UNISWAP_V2_ROUTER_02,
                router1: SUSHISWAP_V2_ROUTER_02,
                tokenIn: DAI,
                tokenOut: WETH,
                amountIn: 60 * 1e18,
                minProfit: 1
            })
        );
        uint256 bal1 = dai.balanceOf(user);

        assertGe(bal1, bal0, "no profit");
        assertEq(dai.balanceOf(address(arb)), 0, "DAI balance of arb != 0");
        console2.log("profit", bal1 - bal0);
    }
}
  • 策略3:借出 tokenOut → 卖出 → 归还 tokenIn
// UniswapV2Arb2.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {IUniswapV2Pair} from "../../../src/interfaces/uniswap-v2/IUniswapV2Pair.sol";
import {IERC20} from "../../../src/interfaces/IERC20.sol";

error InsufficientProfit();

contract UniswapV2Arb2 {
    struct FlashSwapData {
        // Caller of flashSwap (msg.sender inside flashSwap)
        address caller;
        // Pair to flash swap from
        address pair0;
        // Pair to swap from
        address pair1;
        // True if flash swap is token0 in and token1 out
        bool isZeroForOne;
        // Amount in to repay flash swap
        uint256 amountIn;
        // Amount to borrow from flash swap
        uint256 amountOut;
        // Revert if profit is less than this minimum
        uint256 minProfit;
    }

    // Exercise 1
    // - Flash swap to borrow tokenOut
    /**
     * @param pair0 Pair contract to flash swap
     * @param pair1 Pair contract to swap
     * @param isZeroForOne True if flash swap is token0 in and token1 out
     * @param amountIn Amount in to repay flash swap
     * @param minProfit Minimum profit that this arbitrage must make
     */
    function flashSwap(address pair0, address pair1, bool isZeroForOne, uint256 amountIn, uint256 minProfit) external {
        // Write your code here
        // Don’t change any other code
        (uint112 reserve0, uint112 reserve1,) = IUniswapV2Pair(pair0).getReserves();

        // Hint - use getAmountOut to calculate amountOut to borrow
        uint256 amountOut =
            isZeroForOne ? getAmountOut(amountIn, reserve0, reserve1) : getAmountOut(amountIn, reserve1, reserve0);

        bytes memory data = abi.encode(
            FlashSwapData({
                caller: msg.sender,
                pair0: pair0,
                pair1: pair1,
                isZeroForOne: isZeroForOne,
                amountIn: amountIn,
                amountOut: amountOut,
                minProfit: minProfit
            })
        );

        IUniswapV2Pair(pair0).swap({
            amount0Out: isZeroForOne ? 0 : amountOut,
            amount1Out: isZeroForOne ? amountOut : 0,
            to: address(this),
            data: data
        });
    }

    function uniswapV2Call(address sender, uint256 amount0Out, uint256 amount1Out, bytes calldata data) external {
        // Write your code here
        // Don’t change any other code

        // NOTE - anyone can call

        FlashSwapData memory params = abi.decode(data, (FlashSwapData));

        // Token in and token out from flash swap
        address token0 = IUniswapV2Pair(params.pair0).token0();
        address token1 = IUniswapV2Pair(params.pair0).token1();
        (address tokenIn, address tokenOut) = params.isZeroForOne ? (token0, token1) : (token1, token0);

        (uint112 reserve0, uint112 reserve1,) = IUniswapV2Pair(params.pair1).getReserves();

        uint256 amountOut = params.isZeroForOne
            ? getAmountOut(params.amountOut, reserve1, reserve0)
            : getAmountOut(params.amountOut, reserve0, reserve1);

        IERC20(tokenOut).transfer(params.pair1, params.amountOut);

        IUniswapV2Pair(params.pair1).swap({
            amount0Out: params.isZeroForOne ? amountOut : 0,
            amount1Out: params.isZeroForOne ? 0 : amountOut,
            to: address(this),
            data: ""
        });

        // NOTE - no need to calculate flash swap fee
        IERC20(tokenIn).transfer(params.pair0, params.amountIn);

        uint256 profit = amountOut - params.amountIn;
        if (profit < params.minProfit) {
            revert InsufficientProfit();
        }
        IERC20(tokenIn).transfer(params.caller, profit);
    }

    function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut)
        internal
        pure
        returns (uint256 amountOut)
    {
        uint256 amountInWithFee = amountIn * 997;
        uint256 numerator = amountInWithFee * reserveOut;
        uint256 denominator = reserveIn * 1000 + amountInWithFee;
        amountOut = numerator / denominator;
    }
}
// UniswapV2Arb2.test.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {Test, console2} from "forge-std/Test.sol";
import {IERC20} from "../src/interfaces/IERC20.sol";
import {IWETH} from "../src/interfaces/IWETH.sol";
import {IUniswapV2Router02} from "../src/interfaces/uniswap-v2/IUniswapV2Router02.sol";
import {
    DAI,
    WETH,
    UNISWAP_V2_ROUTER_02,
    UNISWAP_V2_PAIR_DAI_WETH,
    SUSHISWAP_V2_ROUTER_02,
    SUSHISWAP_V2_PAIR_DAI_WETH
} from "../src/Constants.sol";
import {UniswapV2Arb2} from "./UniswapV2Arb2.sol";

// Test arbitrage between Uniswap and Sushiswap
// Buy WETH on Uniswap, sell on Sushiswap.
// For flashSwap, borrow DAI from DAI/MKR pair
contract UniswapV2Arb2Test is Test {
    IUniswapV2Router02 private constant uni_router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02);
    IUniswapV2Router02 private constant sushi_router = IUniswapV2Router02(SUSHISWAP_V2_ROUTER_02);
    IERC20 private constant dai = IERC20(DAI);
    IWETH private constant weth = IWETH(WETH);
    address constant user = address(11);

    UniswapV2Arb2 private arb;

    function setUp() public {
        arb = new UniswapV2Arb2();

        // Setup - WETH cheaper on Uniswap than Sushiswap
        deal(address(this), 100 * 1e18);

        weth.deposit{value: 100 * 1e18}();
        weth.approve(address(uni_router), type(uint256).max);

        address[] memory path = new address[](2);
        path[0] = WETH;
        path[1] = DAI;

        uni_router.swapExactTokensForTokens({
            amountIn: 10 * 1e18,
            amountOutMin: 1,
            path: path,
            to: user,
            deadline: block.timestamp
        });
    }

    function test_flashSwap() public {
        uint256 bal0 = dai.balanceOf(user);
        vm.prank(user);
        arb.flashSwap(UNISWAP_V2_PAIR_DAI_WETH, SUSHISWAP_V2_PAIR_DAI_WETH, true, 10000 * 1e18, 1);
        uint256 bal1 = dai.balanceOf(user);

        assertGe(bal1, bal0, "no profit");
        assertEq(dai.balanceOf(address(arb)), 0, "DAI balance of arb != 0");
        console2.log("profit", bal1 - bal0);
    }
}
1

评论 (0)

取消