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;
}