跳转至

uniswap-v3

Uniswap-v2的主要特点是“流动性均匀分布”。作为流动性提供者(LP),提供的资金会被均匀地分布在从 0 到无穷大的所有可能的价格区间上。

一个交易池里是 DAI 和 USDC 这两种价值约等于 1 美元的稳定币。在 v2 中,提供的流动性不仅分布在 $ 0.99 到 $1.01 这个最常发生交易的价格范围,也同样被分配到了 $ 0.1 和 $10,000 这种几乎不可能出现的价格上。这导致了巨大的资本效率低下,因为大部分资金永远不会被用到。

Uniswap-v3 最革命性的改变就是引入了集中流动性。

这个新机制允许流动性提供者(LP)将他们的资金集中在自己选定的一个有限的价格区间内。

还用 DAI/USDC 的例子,在 v3 中,可以选择只将资金投入到 $ 0.99 到 $1.01 这个价格区间。

特性 Uniswap-v2 Uniswap-v3
流动性分布 均匀分布 资金被分散在 (0, ∞) 的整个价格曲线上。 集中流动性 LP 可以自定义资金生效的价格区间。
资本效率 低 大量资金在无效价格区间闲置。 高 资金被高效利用,在相同资金量下可获得更高回报。
LP 代币 ERC-20 代币 流动性份额是同质化的,可以轻松与其他人的份额互换。 NFT (非同质化代币) 每个 LP 的头寸(价格区间、资金量)都是独一无二的,因此用 NFT 表示。
交易手续费 单一费率 所有交易池都是固定的 0.3% 手续费 。 多级费率 提供了多个费率等级(例如 0.05%, 0.30%, 1.00%),LP 可以根据不同代币对的波动性选择加入不同费率的池子。
价格预言机 相对简单,时间加权平均价格(TWAP)容易被操纵。 更先进、更难被操纵的 TWAP 预言机,提供了更可靠的价格数据。
#ifndef UTILS_H
#define UTILS_H

#include <algorithm>
#include <cmath>
#include <limits>

namespace V3Math {
inline double tick_to_sqrt_price(int tick) {
  return std::pow(1.0001, static_cast<double>(tick) / 2.0);
}

inline int sqrt_price_to_tick(double sqrt_price) {
  if (sqrt_price <= 0 || !std::isfinite(sqrt_price)) {
    return std::numeric_limits<int>::min();
  }

  return static_cast<int>(
      std::floor(std::log(sqrt_price * sqrt_price) / std::log(1.0001)));
}

// Calculate the quantity changes of the two tokens.
inline double get_amount0_delta(double sqrt_price_a, double sqrt_price_b,
                                double liquidity) {
  if (sqrt_price_a > sqrt_price_b) std::swap(sqrt_price_a, sqrt_price_b);
  if (sqrt_price_a <= 0) return 0;
  return liquidity * (sqrt_price_b - sqrt_price_a) /
         (sqrt_price_a * sqrt_price_b);
}

inline double get_amount1_delta(double sqrt_price_a, double sqrt_price_b,
                                double liquidity) {
  if (sqrt_price_a > sqrt_price_b) std::swap(sqrt_price_a, sqrt_price_b);
  return liquidity * (sqrt_price_b - sqrt_price_a);
}

// Calculate the next price based on the input.
inline double get_next_sqrt_price_from_amount0_in(double sqrt_price,
                                                  double liquidity,
                                                  double amount_in) {
  if (liquidity == 0) return sqrt_price;
  // P' = 1 / (1/P + amount_in/L)
  return 1.0 / (1.0 / sqrt_price + amount_in / liquidity);
}

inline double get_next_sqrt_price_from_amount1_in(double sqrt_price,
                                                  double liquidity,
                                                  double amount_in) {
  if (liquidity == 0) return sqrt_price;
  return sqrt_price + (amount_in / liquidity);
}
}  // namespace V3Math

#endif  // UTILS_H
#ifndef POOL_H
#define POOL_H

#include <map>
#include <string>

enum class Token { DAI, ETH };  // DAI is token1, ETH is token0

struct PoolState {
  int current_tick;
  double current_liquidity;
  double sqrt_price;
  // Map of tick index to the net change in liquidity at that tick
  std::map<int, double> liquidity_net;
};

class Pool {
 public:
  Pool(int initial_tick);

  void add_liquidity(int lower_tick, int upper_tick, double amount);
  double swap(Token input_token, double input_amount);

  std::pair<double, PoolState> simulate_swap(Token input_token,
                                             double input_amount) const;

  const PoolState& get_state() const;
  double get_price() const;  // Price of ETH in terms of DAI
  void print_state() const;

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

  void update_liquidity_and_tick(int target_tick);
};

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

#include "Pool.h"
#include "utils.h"

Pool::Pool(int initial_tick) {
  state.current_tick = initial_tick;
  state.sqrt_price = V3Math::tick_to_sqrt_price(initial_tick);
  state.current_liquidity = 0;
}

void Pool::add_liquidity(int lower_tick, int upper_tick, double amount) {
  if (amount <= 0) throw std::invalid_argument("Liquidity must be positive.");
  if (lower_tick >= upper_tick)
    throw std::invalid_argument("lower_tick must be less than upper_tick.");
  state.liquidity_net[lower_tick] += amount;
  state.liquidity_net[upper_tick] -= amount;

  if (state.current_tick >= lower_tick && state.current_tick < upper_tick) {
    state.current_liquidity += amount;
  }
}

double Pool::swap(Token input_token, double input_amount) {
  auto result = simulate_swap(input_token, input_amount);
  this->state = result.second;
  return result.first;
}

std::pair<double, PoolState> Pool::simulate_swap(Token input_token,
                                                 double input_amount) const {
  PoolState temp_state = this->state;
  if (input_amount <= 0) {
    return {0.0, temp_state};
  }

  double amount_remaining = input_amount * (1.0 - SWAP_FEE);
  double output_amount = 0;

  bool is_token0_in = (input_token == Token::ETH);

  while (amount_remaining > 1e-12) {
    double sqrt_price_start = temp_state.sqrt_price;

    int next_tick;
    if (is_token0_in) {
      auto it = temp_state.liquidity_net.lower_bound(temp_state.current_tick);
      next_tick = (it == temp_state.liquidity_net.begin())
                      ? std::numeric_limits<int>::min() + 1
                      : (--it)->first;
    } else {
      auto it = temp_state.liquidity_net.upper_bound(temp_state.current_tick);
      next_tick = (it == temp_state.liquidity_net.end())
                      ? std::numeric_limits<int>::max() - 1
                      : it->first;
    }

    double sqrt_price_next_tick = V3Math::tick_to_sqrt_price(next_tick);

    double amount_in_to_reach_next = std::numeric_limits<double>::max();
    if (temp_state.current_liquidity > 0) {
      if (is_token0_in) {
        amount_in_to_reach_next =
            V3Math::get_amount0_delta(sqrt_price_next_tick, sqrt_price_start,
                                      temp_state.current_liquidity);
      } else {
        amount_in_to_reach_next =
            V3Math::get_amount1_delta(sqrt_price_start, sqrt_price_next_tick,
                                      temp_state.current_liquidity);
      }
    }

    double amount_to_swap = std::min(amount_remaining, amount_in_to_reach_next);

    if (temp_state.current_liquidity > 0 && amount_to_swap > 0) {
      double sqrt_price_target;
      if (is_token0_in) {
        sqrt_price_target = V3Math::get_next_sqrt_price_from_amount0_in(
            sqrt_price_start, temp_state.current_liquidity, amount_to_swap);
        output_amount += V3Math::get_amount1_delta(
            sqrt_price_target, sqrt_price_start, temp_state.current_liquidity);
        temp_state.sqrt_price = sqrt_price_target;
      } else {
        sqrt_price_target = V3Math::get_next_sqrt_price_from_amount1_in(
            sqrt_price_start, temp_state.current_liquidity, amount_to_swap);
        output_amount += V3Math::get_amount0_delta(
            sqrt_price_start, sqrt_price_target, temp_state.current_liquidity);
        temp_state.sqrt_price = sqrt_price_target;
      }
      amount_remaining -= amount_to_swap;
    } else {
      // No liquidity in the current range, so we can't process any more of the
      // swap.
      amount_remaining = 0;
    }

    bool crossed_tick =
        (std::abs(amount_to_swap - amount_in_to_reach_next) < 1e-9);

    if (crossed_tick) {
      temp_state.current_tick = next_tick;
      if (temp_state.liquidity_net.count(next_tick)) {
        temp_state.current_liquidity += (is_token0_in ? -1.0 : 1.0) *
                                        temp_state.liquidity_net.at(next_tick);
      }
    } else {
      temp_state.current_tick =
          V3Math::sqrt_price_to_tick(temp_state.sqrt_price);
    }

    if (std::abs(temp_state.sqrt_price - sqrt_price_start) < 1e-12) {
      break;
    }
  }
  return {output_amount, temp_state};
}

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

double Pool::get_price() const { return state.sqrt_price * state.sqrt_price; }

void Pool::print_state() const {
  std::cout << std::fixed << std::setprecision(4)
            << "Pool(Tick: " << state.current_tick
            << ", Liquidity: " << state.current_liquidity
            << ", Price ETH/DAI: " << get_price() << ")";
}
#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:
  std::future<ArbitrageOpportunity> find_arbitrage_async(const Pool& pool_a,
                                                         const Pool& pool_b);

  static ArbitrageOpportunity calculate_arbitrage(const Pool& pool_a,
                                                  const Pool& pool_b);

 private:
  // Brent's method for optimization
  static double brent_maximize(const std::function<double(double)>& f, double a,
                               double b, double tol);
};

#endif  // ARBITRAGE_ENGINE_H
#include <algorithm>
#include <cmath>
#include <iostream>

#include "ArbitrageEngine.h"

ArbitrageOpportunity ArbitrageEngine::calculate_arbitrage(const Pool& pool_a,
                                                          const Pool& pool_b) {
  double price_a = pool_a.get_price();
  double price_b = pool_b.get_price();

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

  const Pool* high_price_pool = (price_a > price_b) ? &pool_a : &pool_b;
  const Pool* low_price_pool = (price_a > price_b) ? &pool_b : &pool_a;

  auto profit_func = [&](double eth_input) {
    if (eth_input <= 0) return 0.0;
    auto dai_out_sim = high_price_pool->simulate_swap(Token::ETH, eth_input);
    if (dai_out_sim.first <= 0) return -1.0;  // Indicate failure
    auto eth_back_sim =
        low_price_pool->simulate_swap(Token::DAI, dai_out_sim.first);
    return eth_back_sim.first - eth_input;
  };

  // We search in a reasonable range, e.g., 0 to 1000 ETH
  double optimal_eth_amount = brent_maximize(profit_func, 0, 1000.0, 1e-7);
  double max_profit = profit_func(optimal_eth_amount);

  if (max_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,
            max_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(
    const Pool& pool_a, const Pool& pool_b) {
  return std::async(std::launch::async, &ArbitrageEngine::calculate_arbitrage,
                    std::ref(pool_a), std::ref(pool_b));
}

// A standard and robust implementation of Brent's method for maximization.
double ArbitrageEngine::brent_maximize(
    const std::function<double(double)>& func, double lower_bound,
    double upper_bound, double tolerance) {
  auto f = [&](double x) {
    return -func(x);
  };  // We minimize the negative of the function.

  const double golden_ratio = (3.0 - std::sqrt(5.0)) / 2.0;
  double x, w, v, u;
  double fx, fw, fv, fu;
  double d = 0.0, e = 0.0;

  v = w = x = lower_bound + golden_ratio * (upper_bound - lower_bound);
  fv = fw = fx = f(x);

  for (int i = 0; i < 100; ++i) {
    double midpoint = 0.5 * (lower_bound + upper_bound);
    double tol1 = tolerance * std::abs(x) + 1.0e-9;
    double tol2 = 2.0 * tol1;

    if (std::abs(x - midpoint) <= (tol2 - 0.5 * (upper_bound - lower_bound))) {
      return x;  // Found maximum
    }

    if (std::abs(e) > tol1) {
      double r = (x - w) * (fx - fv);
      double q = (x - v) * (fx - fw);
      double p = (x - v) * q - (x - w) * r;
      q = 2.0 * (q - r);
      if (q > 0.0) p = -p;
      q = std::abs(q);
      double etemp = e;
      e = d;
      if (std::abs(p) >= std::abs(0.5 * q * etemp) ||
          p <= q * (lower_bound - x) || p >= q * (upper_bound - x)) {
        d = golden_ratio *
            (e = (x >= midpoint ? lower_bound - x : upper_bound - x));
      } else {
        d = p / q;
        u = x + d;
        if (u - lower_bound < tol2 || upper_bound - u < tol2) {
          d = (x < midpoint) ? tol1 : -tol1;
        }
      }
    } else {
      d = golden_ratio *
          (e = (x >= midpoint ? lower_bound - x : upper_bound - x));
    }

    u = x + (std::abs(d) >= tol1 ? d : (d > 0 ? tol1 : -tol1));
    fu = f(u);

    if (fu <= fx) {
      if (u >= x)
        lower_bound = x;
      else
        upper_bound = x;
      v = w;
      fv = fw;
      w = x;
      fw = fx;
      x = u;
      fx = fu;
    } else {
      if (u < x)
        lower_bound = u;
      else
        upper_bound = u;
      if (fu <= fw || w == x) {
        v = w;
        fv = fw;
        w = u;
        fw = fu;
      } else if (fu <= fv || v == x || v == w) {
        v = u;
        fv = fu;
      }
    }
  }
  return x;
}
#include <cassert>
#include <chrono>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <thread>
#include <vector>

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

void test_user_swap_v3() {
  std::cout << "\n--- Running Test: User Swap V3 (Full Implementation) ---"
            << std::endl;
  // Tick for a price of ~$4000/ETH
  int initial_tick = V3Math::sqrt_price_to_tick(std::sqrt(4000.0));
  Pool pool_a(initial_tick);
  pool_a.add_liquidity(initial_tick - 200, initial_tick + 200,
                       100.0);  // Provide 100 liquidity units

  std::cout << "Initial State: ";
  pool_a.print_state();
  std::cout << std::endl;

  double input_dai = 10000.0;  // Swap $10,000
  double eth_received = pool_a.swap(Token::DAI, input_dai);

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

  assert(eth_received > 0);
  assert(pool_a.get_state().current_tick >
         initial_tick);  // Price should increase
  std::cout << "User Swap Test V3 Passed.\n" << std::endl;
}

void test_arbitrage_simulation_v3() {
  std::cout
      << "--- Running Test: Arbitrage Simulation V3 (Full Implementation) ---"
      << std::endl;
  // Pool A: Price ~ $4100
  int tick_a = V3Math::sqrt_price_to_tick(std::sqrt(4100.0));
  Pool pool_a(tick_a);
  pool_a.add_liquidity(tick_a - 500, tick_a + 500, 500.0);

  // Pool B: Price ~ $4000
  int tick_b = V3Math::sqrt_price_to_tick(std::sqrt(4000.0));
  Pool pool_b(tick_b);
  pool_b.add_liquidity(tick_b - 500, tick_b + 500, 500.0);

  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: " << std::setprecision(8)
            << opportunity.optimal_eth_amount << std::endl;
  std::cout << "Estimated Profit (ETH): " << std::setprecision(8)
            << 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;

  // Corrected Assertion: Check for relative convergence (e.g., within 1%)
  double price_a_final = pool_a.get_price();
  double price_b_final = pool_b.get_price();
  assert(std::abs(price_a_final - price_b_final) < (price_a_final * 0.01));
  std::cout << "Arbitrage Simulation Test V3 Passed.\n" << std::endl;
}

void benchmark_execution_v3() {
  std::cout << "--- Running Benchmark: Async vs Sync V3 ---" << std::endl;
  ArbitrageEngine engine;
  int num_tasks = 20;

  int base_tick = V3Math::sqrt_price_to_tick(std::sqrt(4000.0));

  auto start_async = std::chrono::high_resolution_clock::now();
  std::vector<std::future<ArbitrageOpportunity>> futures;
  for (int i = 0; i < num_tasks; ++i) {
    Pool p_a(base_tick + i * 5), p_b(base_tick);
    p_a.add_liquidity(base_tick - 500, base_tick + 500 + i * 5, 500.0);
    p_b.add_liquidity(base_tick - 500, base_tick + 500, 500.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;

  auto start_sync = std::chrono::high_resolution_clock::now();
  for (int i = 0; i < num_tasks; ++i) {
    Pool p_a(base_tick + i * 5), p_b(base_tick);
    p_a.add_liquidity(base_tick - 500, base_tick + 500 + i * 5, 500.0);
    p_b.add_liquidity(base_tick - 500, base_tick + 500, 500.0);
    ArbitrageEngine::calculate_arbitrage(p_a, p_b);
  }
  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;
}

int main() {
  test_user_swap_v3();
  test_arbitrage_simulation_v3();
  benchmark_execution_v3();
  return 0;
}