跳转至

17.1 Uniswap V2

Uniswap 是一个构建在以太坊区块链上的去中心化自动流动性协议。它通过一套不可升级的智能合约实现了一个恒定乘积公式,从而无需依赖中心化中介即可实现无缝的代币兑换。

每个 Uniswap 智能合约(被称为一个“交易对”)管理着一个由两种 ERC-20 代币储备组成的流动性池。流动性提供者(LPs)可以向池中按等值贡献这两种代币,以换取代表其在池中资产相应份额的流动性池代币。

Uniswap 的基本机制由以下不变量公式决定:

x∗y = k

其中 x 和 y 是池中两种代币的储备量,k 是恒定乘积。该公式确保了任何交易都必须维持这个不变量。由该公式创建的曲线会以较差的汇率惩罚较大的交易,从而保持池内的平衡。

Uniswap 对每笔交易收取 0.30% 的费用,这笔费用会被添加回储备金中,使得 k 值随时间推移而略有增加。由于池内的代币价格只能通过交易来改变,因此池中价格与外部市场之间的任何差异都会产生套利机会。套利者确保了 Uniswap 的价格能紧密跟踪全球市场价格。

对于终端用户而言,Uniswap 提供了简单直观的体验。用户选择一个输入代币和一个输出代币,指定输入金额,协议便会计算出他们将收到的输出金额。一旦确认,兑换就会执行,输出的代币会立即发送到用户的钱包中。

Uniswap V2 自动做市模型

  • 交易者 (Trader) -> 执行兑换 (Execute Swap)
  • 输入 (Input): 3 Token A + 0.30% 手续费
  • 输出 (Output): 1 Token B
  • 执行兑换 -> Uniswap 交易对 (Uniswap Pair)
  • 信息: 交易改变了储备金的平衡,从而产生新的价格。
  • 价格曲线: 由 x*y=k 定义
  • 储备金 (Reserves):
  • 池子开始状态 (Pool Start): 1200 Token A / 400 Token B = 价格 3
  • 池子结束状态 (Pool End): 1203.03 / 399 = 价格 3.01
  • 结果: -> 新价格 (Next price)

本系统将模拟一个涉及两个 Uniswap 式流动性池的套利场景:

  • 池 A: 持有 X1 DAI 和 Y1 ETH。
  • 池 B: 持有 X2 DAI 和 Y2 ETH。
  • 兑换手续费: 0.30% (0.003)。

每个池子都维持其自身的恒定乘积不变量:

  • X1∗Y1=K1
  • X2∗Y2=K2

每个池中的 ETH/DAI 价格由以下方式决定:

  • 池 A: Y1 / X1
  • 池 B: Y2 / X2

两个池子之间的价格差异会引入套利机会。

1. 池状态管理

  • 定义一个包含 DAI 和 ETH 储备金的 Pool 数据结构。
  • 实现以下操作:
  • add_liquidity()
  • remove_liquidity()
  • swap(input_token, input_amount)

2. 套利计算引擎

  • 识别池 A 和池 B 之间何时存在套利机会。
  • 计算用于套利的最优 ETH 金额。
  • 估算执行套利所能获得的利润(以 ETH 计)。
  • 将该引擎设计为以非阻塞模式(异步或多线程)运行。

3. 测试用例

  • 模拟一个用户在池 A 中用 100 DAI 兑换 ETH 的场景。
  • 验证池状态更新和输出金额的正确性。
  • 模拟套利的检测与执行过程。

4. 基准测试

  • 实现一个测试来对非阻塞执行进行性能基准测试。
#ifndef POOL_H
#define POOL_H

#include <string>

enum class Token { DAI, ETH };

struct PoolState {
  double dai;
  double eth;
};

class Pool {
 public:
  Pool(double initial_dai, double initial_eth);

  void add_liquidity(double dai_amount, double eth_amount);
  void remove_liquidity(double dai_amount, double eth_amount);

  double swap(Token input_token, double input_amount);

  PoolState get_state() const;
  double get_price(Token token) const;
  void print_state() const;

 private:
  PoolState state;
  static constexpr double SWAP_FEE = 0.003;
};

#endif  // POOL_H
#include <iomanip>
#include <iostream>
#include <stdexcept>

#include "Pool.h"

Pool::Pool(double initial_dai, double initial_eth)
    : state{initial_dai, initial_eth} {}

void Pool::add_liquidity(double dai_amount, double eth_amount) {
  if (dai_amount < 0 || eth_amount < 0) {
    throw std::invalid_argument("Liquidity amounts cannot be negative.");
  }
  state.dai += dai_amount;
  state.eth += eth_amount;
}

void Pool::remove_liquidity(double dai_amount, double eth_amount) {
  if (dai_amount < 0 || eth_amount < 0) {
    throw std::invalid_argument("Liquidity amounts cannot be negative.");
  }
  if (dai_amount > state.dai || eth_amount > state.eth) {
    throw std::runtime_error("Insufficient liquidity to remove.");
  }
  state.dai -= dai_amount;
  state.eth -= eth_amount;
}

double Pool::swap(Token input_token, double input_amount) {
  if (input_amount <= 0) {
    throw std::invalid_argument("Input amount must be positive.");
  }

  const double k = state.dai * state.eth;
  if (k == 0) {
    throw std::runtime_error("Swap failed: pool has no liquidity.");
  }

  const double input_after_fee = input_amount * (1.0 - SWAP_FEE);
  double output_amount = 0.0;

  if (input_token == Token::DAI) {
    // Calculate the output amount
    output_amount =
        (state.eth * input_after_fee) / (state.dai + input_after_fee);

    if (output_amount > state.eth) {
      throw std::runtime_error(
          "Swap failed: insufficient ETH liquidity for this trade size.");
    }

    // Use the full input amount when updating reserves
    state.dai += input_amount;
    state.eth -= output_amount;

  } else if (input_token == Token::ETH) {
    // Calculate the output amount
    output_amount =
        (state.dai * input_after_fee) / (state.eth + input_after_fee);

    if (output_amount > state.dai) {
      throw std::runtime_error(
          "Swap failed: insufficient DAI liquidity for this trade size.");
    }

    // Use the full input amount when updating reserves
    state.eth += input_amount;
    state.dai -= output_amount;
  }

  if (output_amount < 0) return 0;
  return output_amount;
}

PoolState Pool::get_state() const { return state; }

double Pool::get_price(Token token) const {
  if (token == Token::ETH) {
    return (state.eth > 0) ? state.dai / state.eth : 0.0;
  }
  return (state.dai > 0) ? state.eth / state.dai : 0.0;
}

void Pool::print_state() const {
  std::cout << std::fixed << std::setprecision(4) << "Pool(DAI: " << state.dai
            << ", ETH: " << state.eth
            << ", Price ETH/DAI: " << get_price(Token::ETH) << ")";
}
#ifndef ARBITRAGE_ENGINE_H
#define ARBITRAGE_ENGINE_H

#include <future>
#include <string>
#include <vector>

#include "Pool.h"

struct ArbitrageOpportunity {
  bool profitable;
  double profit_eth;
  double optimal_eth_amount;
  std::vector<std::string> path;
  std::string description;
};

class ArbitrageEngine {
 public:
  // Implement non-blocking/async execution using std::future
  std::future<ArbitrageOpportunity> find_arbitrage_async(Pool& pool_a,
                                                         Pool& pool_b);

  static ArbitrageOpportunity calculate_arbitrage(const PoolState& state_a,
                                                  const PoolState& state_b);
};

#endif  // ARBITRAGE_ENGINE_H
#include <cmath>
#include <functional>
#include <iostream>
#include <thread>

#include "ArbitrageEngine.h"

// Helper function to calculate the profit for a given input amount.
// This is our profit function, f(E), representing the final PnL of the entire
// arbitrage path.
double calculate_profit_for_input(double eth_input,
                                  const PoolState& high_price_pool,
                                  const PoolState& low_price_pool) {
  if (eth_input <= 0) {
    return 0.0;
  }

  constexpr double gamma = 1.0 - 0.003;
  const double y1 = high_price_pool.eth;
  const double x1 = high_price_pool.dai;
  const double y2 = low_price_pool.eth;
  const double x2 = low_price_pool.dai;

  // Step 1: Swap ETH for DAI in the high-price pool
  double dai_out_numerator = x1 * eth_input * gamma;
  double dai_out_denominator = y1 + eth_input * gamma;
  if (dai_out_denominator <= 0) return 0;
  double dai_out = dai_out_numerator / dai_out_denominator;

  // Step 2: Swap DAI for ETH in the low-price pool
  double eth_out_final_numerator = y2 * dai_out * gamma;
  double eth_out_final_denominator = x2 + dai_out * gamma;
  if (eth_out_final_denominator <= 0) return 0;
  double eth_out_final = eth_out_final_numerator / eth_out_final_denominator;

  return eth_out_final - eth_input;
}

// Helper function to calculate the price difference between the two pools after
// a trade.
double get_price_difference_after_trade(double eth_input,
                                        const PoolState& high_price_pool,
                                        const PoolState& low_price_pool) {
  if (eth_input <= 0) {
    return (high_price_pool.dai / high_price_pool.eth) -
           (low_price_pool.dai / low_price_pool.eth);
  }

  constexpr double gamma = 1.0 - 0.003;

  // Simulate the trade in the high-price pool
  double dai_out = (high_price_pool.dai * eth_input * gamma) /
                   (high_price_pool.eth + eth_input * gamma);
  double final_high_eth = high_price_pool.eth + eth_input;
  double final_high_dai = high_price_pool.dai - dai_out;
  double final_high_price =
      (final_high_eth > 0) ? final_high_dai / final_high_eth : 0;

  // Simulate the trade in the low-price pool
  double eth_out_final = (low_price_pool.eth * dai_out * gamma) /
                         (low_price_pool.dai + dai_out * gamma);
  double final_low_dai = low_price_pool.dai + dai_out;
  double final_low_eth = low_price_pool.eth - eth_out_final;
  double final_low_price =
      (final_low_eth > 0) ? final_low_dai / final_low_eth : 0;

  return final_high_price - final_low_price;
}

ArbitrageOpportunity ArbitrageEngine::calculate_arbitrage(
    const PoolState& state_a, const PoolState& state_b) {
  double price_a = (state_a.eth > 0) ? state_a.dai / state_a.eth : 0.0;
  double price_b = (state_b.eth > 0) ? state_b.dai / state_b.eth : 0.0;

  if (std::abs(price_a - price_b) < 1e-6) {
    return {false};
  }

  const PoolState* high_price_pool_state =
      (price_a > price_b) ? &state_a : &state_b;
  const PoolState* low_price_pool_state =
      (price_a > price_b) ? &state_b : &state_a;

  // Use the Bisection Method (a numerical root-finding algorithm) to find
  // the trade amount that makes the prices equal. This ensures that the prices
  // converge after the trade.
  double low = 0;
  // Set a reasonable upper bound for the search, e.g., the ETH reserves of the
  // high-price pool.
  double high = high_price_pool_state->eth;

  // Check if the endpoints of the search interval have opposite signs, a
  // prerequisite for the bisection method.
  if (get_price_difference_after_trade(low, *high_price_pool_state,
                                       *low_price_pool_state) *
          get_price_difference_after_trade(high, *high_price_pool_state,
                                           *low_price_pool_state) >=
      0) {
    // If they have the same sign, the optimal solution may be outside the
    // current search range or non-existent.
    return {false};
  }

  // Iterate 100 times to achieve sufficient precision.
  for (int i = 0; i < 100; ++i) {
    double mid = (low + high) / 2.0;
    if (get_price_difference_after_trade(mid, *high_price_pool_state,
                                         *low_price_pool_state) > 0) {
      low = mid;
    } else {
      high = mid;
    }
  }

  double optimal_eth_amount = (low + high) / 2.0;
  double profit = calculate_profit_for_input(
      optimal_eth_amount, *high_price_pool_state, *low_price_pool_state);

  if (profit > 1e-9) {
    std::string high_price_pool_name =
        (price_a > price_b) ? "Pool A" : "Pool B";
    std::string low_price_pool_name = (price_a > price_b) ? "Pool B" : "Pool A";
    return {true,
            profit,
            optimal_eth_amount,
            {"ETH -> " + high_price_pool_name, "DAI -> " + low_price_pool_name},
            "ETH price is higher in " + high_price_pool_name};
  }

  return {false};
}

std::future<ArbitrageOpportunity> ArbitrageEngine::find_arbitrage_async(
    Pool& pool_a, Pool& pool_b) {
  return std::async(std::launch::async, [=] {
    return calculate_arbitrage(pool_a.get_state(), pool_b.get_state());
  });
}
#include <cassert>
#include <chrono>
#include <iostream>
#include <thread>
#include <vector>

#include "ArbitrageEngine.h"
#include "Pool.h"

void test_user_swap() {
  std::cout << "\n--- Running Test: User Swap ---" << std::endl;
  Pool pool_a(1200.0, 400.0);
  std::cout << "Initial State: ";
  pool_a.print_state();
  std::cout << std::endl;

  double input_dai = 100.0;
  double eth_received = pool_a.swap(Token::DAI, input_dai);

  std::cout << "Swapped " << input_dai << " DAI for " << eth_received << " ETH."
            << std::endl;
  std::cout << "Final State: ";
  pool_a.print_state();
  std::cout << std::endl;

  // Update assertion to match the correct final reserves
  assert(eth_received > 0);
  assert(std::abs(pool_a.get_state().dai - (1200.0 + 100.0)) <
         1e-9);  // The reserve increases by the full input amount
  assert(std::abs(pool_a.get_state().eth - (400.0 - eth_received)) < 1e-9);
  std::cout << "User Swap Test Passed.\n" << std::endl;
}

void test_arbitrage_simulation() {
  std::cout << "--- Running Test: Arbitrage Simulation ---" << std::endl;
  Pool pool_a(4500.0, 1.0);  // ETH Price: 4500 DAI
  Pool pool_b(4000.0, 1.0);  // ETH Price: 4000 DAI

  std::cout << "Initial States:" << std::endl;
  std::cout << "Pool A: ";
  pool_a.print_state();
  std::cout << std::endl;
  std::cout << "Pool B: ";
  pool_b.print_state();
  std::cout << std::endl;

  ArbitrageEngine engine;
  auto future_opp = engine.find_arbitrage_async(pool_a, pool_b);
  ArbitrageOpportunity opportunity = future_opp.get();

  assert(opportunity.profitable);

  std::cout << "\nArbitrage Opportunity Found!" << std::endl;
  std::cout << "Description: " << opportunity.description << std::endl;
  std::cout << "Optimal ETH to use: " << opportunity.optimal_eth_amount
            << std::endl;
  std::cout << "Estimated Profit (ETH): " << opportunity.profit_eth
            << std::endl;
  std::cout << "Path: " << opportunity.path[0] << " -> " << opportunity.path[1]
            << std::endl;

  std::cout << "\nExecuting arbitrage..." << std::endl;
  double dai_from_a = pool_a.swap(Token::ETH, opportunity.optimal_eth_amount);
  pool_b.swap(Token::DAI, dai_from_a);

  std::cout << "\nFinal Pool States After Arbitrage:" << std::endl;
  std::cout << "Pool A: ";
  pool_a.print_state();
  std::cout << std::endl;
  std::cout << "Pool B: ";
  pool_b.print_state();
  std::cout << std::endl;

  double final_price_a = pool_a.get_price(Token::ETH);
  double final_price_b = pool_b.get_price(Token::ETH);
  assert(std::abs(final_price_a - final_price_b) < 1.0);
  std::cout << "Arbitrage Simulation Test Passed.\n" << std::endl;
}

void benchmark_execution() {
  std::cout << "--- Running Benchmark: Async vs Sync ---" << std::endl;
  ArbitrageEngine engine;
  int num_tasks = 500;  // Increase the number of tasks to better observe
                        // multithreading effects

  // Async/multithreaded execution
  auto start_async = std::chrono::high_resolution_clock::now();
  std::vector<std::future<ArbitrageOpportunity>> futures;
  for (int i = 0; i < num_tasks; ++i) {
    // Create pool objects inside the loop to ensure each task has independent
    // data
    Pool p_a(4500.0 + i, 1.0), p_b(4000.0, 1.0);
    futures.push_back(engine.find_arbitrage_async(p_a, p_b));
  }
  for (auto& f : futures) {
    f.get();
  }
  auto end_async = std::chrono::high_resolution_clock::now();
  std::chrono::duration<double, std::milli> async_duration =
      end_async - start_async;
  std::cout << "Async execution for " << num_tasks
            << " tasks took: " << async_duration.count() << " ms" << std::endl;

  // Synchronous execution
  auto start_sync = std::chrono::high_resolution_clock::now();
  for (int i = 0; i < num_tasks; ++i) {
    Pool p_a(4500.0 + i, 1.0), p_b(4000.0, 1.0);
    // Directly call the calculation function to simulate synchronous blocking
    // behavior
    ArbitrageEngine::calculate_arbitrage(p_a.get_state(), p_b.get_state());

    // Simulate blocking/time-consuming operations in a synchronous loop
    // std::this_thread::sleep_for(std::chrono::microseconds(200));
  }
  auto end_sync = std::chrono::high_resolution_clock::now();
  std::chrono::duration<double, std::milli> sync_duration =
      end_sync - start_sync;
  std::cout << "Sync execution for " << num_tasks
            << " tasks took: " << sync_duration.count() << " ms" << std::endl;

  std::cout << "\nNote: Async performance gain depends on core count and task "
               "complexity."
            << std::endl;
  std::cout
      << "On multi-core systems, async should be faster for CPU-bound tasks."
      << std::endl;
}

int main() {
  test_user_swap();
  test_arbitrage_simulation();
  benchmark_execution();
  return 0;
}
cmake_minimum_required(VERSION 3.10) project(UniswapArbitrage)

    set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON)

        add_executable(arbitrage_sim main.cpp Pool.cpp ArbitrageEngine.cpp)

            if (UNIX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread") endif()