简介

v1版本,基于恒定乘积做市商模型( x × y = k x\times y = k x×y=k
v2版本,包含CorePeriphery

  • Factory 管生(创建池子)
  • Pair 管养(存钱、算价、交易)
  • Router 管用(让用户方便地操作)

其支持预言机机制,TWAP(Time-Weighted Average Price)时间加权平均价格
v3版本

  • 增加集中流动性
  • 费率分层:支持0.05%/0.30%/1.00%
  • 非同质化流动性
  • 高级预言机:优化了 TWAP 的计算方式,支持更高效的价格查询

增强关系式
L = x y P = y x L = Δ y Δ P \begin{align*}L &= \sqrt {x y} \\ \sqrt{P}&= \sqrt {\frac{y}{x}}\\ L&= \frac{\Delta y}{\Delta \sqrt{P}} \end{align*} LP L=xy =xy =ΔP Δy

v4 版本
单例类PoolManager管理所有的代币对池,支持Hooks

V2

Core核心

主要有

  • UniswapV2Factory
  • UniswapV2Pair

UniswapV2Pair

比较使用 k b e f o r e ≤ k a f t e r k_{before} \le k_{after} kbeforekafter
v2交易对,其主要成员有
reserve0/reserve1:表示交易对的两种代币的储备金量
totalSupply:表示所有用户的LP代币总数量
factory:表示创建交易对的工厂合约
token0/token1:表示交易对的两种代币合约地址
price0CumulativeLast/price1CumulativeLast:表示TWAP预言机的核心变量,是对“边际价格 × 持续时间”进行连续积分,其计算公式为 p r i c e 0 C u m u l a t i v e L a s t = ∑ r e s e r v e 1 ∗ 2 112 r e s e r v e 0 ∗ Δ t price0CumulativeLast=\sum{\frac{reserve1 * 2^{112}} {reserve0}} * \Delta t price0CumulativeLast=reserve0reserve12112Δt
p r i c e 1 C u m u l a t i v e L a s t = ∑ r e s e r v e 0 ∗ 2 112 r e s e r v e 1 ∗ Δ t price1CumulativeLast =\sum{\frac{reserve0 * 2^{112}}{reserve1}} * \Delta t price1CumulativeLast=reserve1reserve02112Δt
其中 Δ t = b l o c k T i m e s t a m p L a s t − b l o c k T i m e s t a m p L a s t l a s t \Delta t = blockTimestampLast - blockTimestampLast_{last} Δt=blockTimestampLastblockTimestampLastlast
blockTimestampLast:表示上次计算连续积分时的区块时间戳对 2 32 2^{32} 232的模
关键操作

名称 说明
mint 增加流动性
burn 减少流动性
swap 实现资产交换

mint:对于初次增加流动性,其流动性计算公式为 a m o u n t 0 ∗ a m o u n t 1 − 10 3 \sqrt{amount0 * amount1} - 10^3 amount0amount1 103追加情况时计算公式为 min ⁡ { a m o u n t 0 r e s e r v e 0 ∗ t o t a l S u p p l y , a m o u n t 1 r e s e r v e 1 ∗ t o t a l S u p p l y } \min \{ \frac{amount0}{reserve0} *totalSupply, \frac{amount1}{reserve1} * totalSupply \} min{reserve0amount0totalSupply,reserve1amount1totalSupply} 其中amount0/amount1为存入的交易对代币值
如果设置了要收协议费,即factoryfeeTo()接收地址不为0,协议费计算公式为 k − k l a s t 5 k + k l a s t ∗ t o t a l S u p p l y \frac{k - k_{last}}{5k + k_{last}} * totalSupply 5k+klastkklasttotalSupply 其中 k = r e s e r v e 0 ∗ r e s e r v e 1 k=\sqrt{reserve0 * reserve1} k=reserve0reserve1 , k l a s t k_{last} klast表示上一次计算的 k k k
swap:由Router来调用,其传的参数amount0Outamount1Out其中一个必定为0,采用了“乐观转账(Optimistic Transfer)”模式,主要承担三件职责

  • 资产交割:根据输入的参数,将代币从流动性池(Pair)转移至目标地址(to)
  • 数学验证:在转账后,通过余额反推输入金额,并验证扣除 0.3% 手续费后的乘积是否不小于交易前的乘积
  • 状态同步:更新储备金余额和时间戳,为 TWAP(时间加权平均价格)预言机提供数据。

交换时需要满足 x × y ≥ k x \times y \ge k x×yk。闪电贷是通过swap来实现的,uniswapV2Call 中实现偿还贷款及费用

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

Periphery外围

主要包含

  • UniswapV2Migrator
  • UniswapV2Router01(已废弃)
  • UniswapV2Router02

UniswapV2Router02

UniswapV2Factory

UniswapV2Router02

通过amountIn计算amoutOut公式为 r e s e r v e 0 ⋅ r e s e r v e 1 = ( r e s e r v e 0 + 0.997 ⋅ a m o u n t I n ) ⋅ ( r e s e r v e 1 − a m o u n t O u t ) reserve0 \cdot reserve1 = (reserve0 + 0.997 \cdot amountIn) \cdot (reserve1 - amountOut) reserve0reserve1=(reserve0+0.997amountIn)(reserve1amountOut)
a m o u t O u t = 0.997 ⋅ r e s e r v e 1 ⋅ a m o u n t I n r e s e r v e 0 + 0.997 ⋅ a m o u n t I n amoutOut = \frac{0.997 \cdot reserve1 \cdot amountIn}{reserve0 + 0.997 \cdot amountIn} amoutOut=reserve0+0.997amountIn0.997reserve1amountIn

函数 说明
swapExactTokensForTokens path中的第一个为输入token,即根据一个amoutIn,批量计算amoutOut
swapTokensForExactTokens path中的最后一个为输出token,即根据一个amoutOut批量计算amoutIn

UniswapV2Library

函数 说明
getAmountOut 根据amountIn计算amountOut
getAmountIn 根据amoutOut计算amoutIn
getAmountsOut 批量计算amountOut
getAmountsIn 批量计算amoutIn
getReserves 返回代币对的储备量
quote 计算LP代币的价值

V3

core核心

主要有

  • UniswapV3Factory
  • UniswapV3Pool

UniswapV3Factory

«interface»

IUniswapV3Factory

«interface»

IUniswapV3Pool

UniswapV3PoolDeployer

UniswapV3Pool

P ( i ) = 1.0001 i × 2 96 \sqrt{P(i)} = \sqrt{1.0001^i} \times 2^{96} P(i) =1.0001i ×296,其中 i i i表示tick的索引
UniswapV3Pool的主要成员有

成员 说明
slot0 池子的“仪表盘”
ticks 记录每个 Tick 边界上流动性变化的账本
tickBitmap 这是一张位图(Bitmap),用于快速寻找“下一个有流动性的 Tick” ,以256为一组
positions 非同质化的仓位账本,是以地址+下界+上界作为key。position即头寸,即一个区间相关的信息
struct Slot0 {
        // the current price
        uint160 sqrtPriceX96;
        // the current tick
        int24 tick;
        // the most-recently updated index of the observations array
        uint16 observationIndex;
        // the current maximum number of observations that are being stored
        uint16 observationCardinality;
        // the next maximum number of observations to store, triggered in observations.write
        uint16 observationCardinalityNext;
        // the current protocol fee as a percentage of the swap fee taken on withdrawal
        // represented as an integer denominator (1/x)%
        uint8 feeProtocol;
        // whether the pool is locked
        bool unlocked;
    }

mapping(int24 => Tick.Info) public override ticks;
mapping(int16 => uint256) public override tickBitmap;
mapping(bytes32 => Position.Info) public override positions;
//Tick.Info
struct Info {
        // the total position liquidity that references this tick
        uint128 liquidityGross;
        // amount of net liquidity added (subtracted) when tick is crossed from left to right (right to left),
        int128 liquidityNet;
        // fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick)
        // only has relative meaning, not absolute — the value depends on when the tick is initialized
        uint256 feeGrowthOutside0X128;
        uint256 feeGrowthOutside1X128;
        // the cumulative tick value on the other side of the tick
        int56 tickCumulativeOutside;
        // the seconds per unit of liquidity on the _other_ side of this tick (relative to the current tick)
        // only has relative meaning, not absolute — the value depends on when the tick is initialized
        uint160 secondsPerLiquidityOutsideX128;
        // the seconds spent on the other side of the tick (relative to the current tick)
        // only has relative meaning, not absolute — the value depends on when the tick is initialized
        uint32 secondsOutside;
        // true iff the tick is initialized, i.e. the value is exactly equivalent to the expression liquidityGross != 0
        // these 8 bits are set to prevent fresh sstores when crossing newly initialized ticks
        bool initialized;
    }
 //Position.Info
struct Info {
        // the amount of liquidity owned by this position
        uint128 liquidity;
        // fee growth per unit of liquidity as of the last update to liquidity or fees owed
        uint256 feeGrowthInside0LastX128;
        uint256 feeGrowthInside1LastX128;
        // the fees owed to the position owner in token0/token1
        uint128 tokensOwed0;
        uint128 tokensOwed1;
    }

Tick.Info中计算单位增长费率的参数

  • feeGrowthOutside0X128:代币0在该tick相对池中当前tick的单位增长费率,即不包含当前tick的那一方
  • feeGrowthOutside01X128:代币1在该tick相对池中当前tick的单位增长费率,即不包含当前tick的那一方

tick,position,pool 的feeGrowth的关系

  • tick中fee的修改是在更新(即tick小于池中当前tick时,会初始经为池中的全局值)以及跨tick时(翻转即全局值减去tick记录的外部值)
  • position中fee的修改是在更新即增加区间流动性时
  • pool 中fee的修改是在资金交易时

主要方法有

方法 说明
mint 流动性提供者(LP)向特定价格区间注入资金
burn 移除流动性,实际没有把钱转给用户,只是销毁账本上的流动性记录
collect 把钱转给用户
swap zeroForOne为true表示用 token0 买 token1即抛售token0,买入token1会导致价值下降,为false时表示用token1买token0即抛售token1买入token0会导致价值上升
amountSpecified正数表示知道需要花多少输入代币,负数表示知道需要收到多少输出代币

mint主要步骤为

  • 修改position信息(_modifyPosition)
    • 检查tickLowertickUpper的有效性,要求tickLower小于tickUppertickLower不能小于TickMath.MIN_TICK, tickUpper不能大于TickMath.MAX_TICK
    • 更新positions,tickstickBitmap信息(_updatePosition),在ticks[tickLower]处增加流动性(liquidityNet)同时增加liquidityGross,在ticks[tickUpper] 处减少流动性,更新 positions映射,主要包含以下数据
      • liquidity:头寸拥有的流动性数量
      • feeGrowthInside0LastX128​ :上一次你与该头寸交互时,token0 在该价格区间内的每单位流动性累积手续费快照
      • feeGrowthInside1LastX128:上一次你与该头寸交互时,token1在该价格区间内的每单位流动性累积手续费快照
      • tokensOwed0:已经结算但尚未提取的 token0 手续费
      • tokensOwed1:已经结算但尚未提取的 token1 手续费
    • 如果slot0中的当前tick正好在这个区间内,会增加池子的流动性liquidity
    • 如果当前tick在区间内,你需要提供两种代币,如果tick在区间下方,只提供token0,如果tick在区间上方,只提供token1
  • 回调索取资金uniswapV3MintCallback,乐观记账,事后索偿

swap的步骤为

  1. 找下一tick(nextInitializedTickWithinOneWord),当zeroForOne为真时,即价格下跌时,取小于tick的一端,当zeroForOne为假时,即价格上升时,取大于tick的一端
  2. 算该步可以换多少(computeSwapStep
  3. 处理穿越(cross),更新tick时总是取下界
  4. 更新全局状态,即slot0中的sqrtPriceX96tick以及池中的流动性liquidity
  5. 池子帐转为用户帐,即作转帐safeTransfer
  6. 执行回调uniswapV3SwapCallback

periphery外围

主要是两个合约

合约 说明
NonfungiblePositionManager 用于管理流动性提供者行为
SwapRouter 用于管理交易者的行为

sdk

v2-sdk/v3-sdk基于ethers.js或者wagmi得到链上连接,主要是获取、构造交易数据

  • 读链上状态
  • 链下 quote
  • 生成调 Periphery Router​ 的 calldata

实体类(Entities)有

  • Pool:流动性池
  • Position:流动性仓位
  • Route:交易路由,用于跨池交易
  • Tick:价格刻度
  • Trade:交易

其中交易构造器(Trade & Router)负责生成最终发送给链上 Router 合约(如 SwapRouter)的调用数据(Calldata),包括处理单跳、多跳交换以及路径编码
SwapQuoter:封装了与 Quoter(报价器)合约的交互,用于获取预估价格

uniswap v3-sdk发起交易时如何调用

  1. 初始化核心对象与 Router 合约
import { Token, CurrencyAmount, TradeType, Percent } from '@uniswap/sdk-core';
import { Pool, Route, Trade, Position } from '@uniswap/v3-sdk';
import { ethers } from 'ethers';
import ISwapRouter from '@uniswap/v3-periphery/artifacts/contracts/SwapRouter.sol/SwapRouter.json';

// 1. 定义代币 (以 ETH 和 USDC 为例)
const WETH = new Token(1, '0xC02aa...', 18, 'WETH', 'Wrapped Ether');
const USDC = new Token(1, '0xA0b86...', 6, 'USDC', 'USD Coin');

// 2. 连接钱包和 Router 合约
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const routerAddress = '0xE592427A0AEce92De3Edee1F18E0157C05861564'; // 主网 SwapRouter 地址
const routerContract = new ethers.Contract(routerAddress, ISwapRouter.abi, signer);
  1. 获取链上池子数据并构建路由
    SDK 需要在链下复刻池子的状态来计算最优路径。你需要从链上读取当前池子的 sqrtPriceX96 和 liquidity 等数据
// 3. 获取特定费率(如 0.3%)的池子数据
const poolAddress = Pool.getAddress(WETH, USDC, 3000); 
// 这里需要通过 Multicall 或 RPC 读取链上 Pool 合约的 slot0 获取 sqrtPriceX96 和当前 tick
const pool = new Pool(WETH, USDC, 3000, sqrtPriceX96, liquidity, tickCurrent);

// 4. 构建路由 (这里演示单跳,多跳可以传入多个 Pool)
const route = new Route([pool], WETH, USDC);
  1. 构造交易并计算滑点保护
    使用 SDK 的 Trade 类来构建交易对象,它会自动帮你计算输入输出金额,并设置滑点容忍度
// 5. 设定交易金额 (例如卖出 1 个 WETH)
const amountIn = CurrencyAmount.fromRawAmount(WETH, ethers.utils.parseUnits('1', 18).toString());

// 6. 使用 SDK 构造交易
const trade = await Trade.fromRoute(route, amountIn, TradeType.EXACT_INPUT);

// 7. 设置滑点容忍度 (例如 0.5%)
const slippageTolerance = new Percent(50, 10000); // 0.5%
const amountOutMinimum = trade.minimumAmountOut(slippageTolerance).quotient.toString();
const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20分钟后过期
  1. 调用 Router 合约发起链上交易
    最后,将 SDK 计算好的参数填入 SwapRouter 的 exactInputSingle 方法中,通过钱包发起交易
// 8. 组装链上调用参数
const params = {
  tokenIn: WETH.address,
  tokenOut: USDC.address,
  fee: 3000,
  recipient: await signer.getAddress(), // 接收代币的地址
  deadline: deadline,
  amountIn: amountIn.quotient.toString(),
  amountOutMinimum: amountOutMinimum, // 考虑滑点后的最小输出
  sqrtPriceLimitX96: 0 // 设为 0 表示不限制价格
};

// 9. 发起交易 (需确保用户已授权 Router 花费 WETH)
const tx = await routerContract.exactInputSingle(params);
console.log('交易已发送,哈希:', tx.hash);

// 10. 等待交易上链确认
await tx.wait();
console.log('交易成功!');

资料

Uniswap V2 Book
Uniswap V3 开发手册

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐