17.1 Uniswap V2¶
Uniswap 是一个构建在以太坊区块链上的去中心化自动流动性协议。它通过一套不可升级的智能合约实现了一个恒定乘积公式,从而无需依赖中心化中介即可实现无缝的代币兑换。
每个 Uniswap 智能合约(被称为一个“交易对”)管理着一个由两种 ERC-20 代币储备组成的流动性池。流动性提供者(LPs)可以向池中按等值贡献这两种代币,以换取代表其在池中资产相应份额的流动性池代币。
Uniswap 的基本机制由以下不变量公式决定:
其中 x 和 y 是池中两种代币的储备量,k 是恒定乘积。该公式确保了任何交易都必须维持这个不变量。由该公式创建的曲线会以较差的汇率惩罚较大的交易,从而保持池内的平衡。
Uniswap 对每笔交易收取 0.30% 的费用,这笔费用会被添加回储备金中,使得 k 值随时间推移而略有增加。由于池内的代币价格只能通过交易来改变,因此池中价格与外部市场之间的任何差异都会产生套利机会。套利者确保了 Uniswap 的价格能紧密跟踪全球市场价格。
对于终端用户而言,Uniswap 提供了简单直观的体验。用户选择一个输入代币和一个输出代币,指定输入金额,协议便会计算出他们将收到的输出金额。一旦确认,兑换就会执行,输出的代币会立即发送到用户的钱包中。

- 交易者 (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;
}