UniswapV2
UniswapV2
Uniswap V2 是以太坊上去中心化交易所(DEX)的第二个主要版本。它使用 自动做市商(AMM) 机制,允许用户在无需订单簿的情况下进行代币交易。以下是 Uniswap V2 的核心概念和关键点:
1. Uniswap V2 主要特性
基于 AMM(自动做市商)
- 采用 恒定乘积公式 x * y = L^2^,保证流动性池中两个代币的乘积保持不变。
流动性池(Liquidity Pools)
- 由两个代币组成,例如 ETH/USDC。
- 交易由池内资产重新平衡完成。
去中心化 & 无需许可
- 任何人都可以提供流动性或创建新交易对。
流动性提供者(LP)
- 任何人可以向池子存入 等值 的两种代币,赚取交易费用(默认 0.3%)。
- LP 份额用
ERC-20代币表示(LP 代币)。
价格预言机
- 通过 累积时间加权平均价格(TWAP) 提供更可靠的价格信息。
支持 ERC-20 到 ERC-20 直接交易
- V1 只能 ETH <-> ERC-20,V2 允许 ERC-20 <-> ERC-20 直接交易。
闪电贷(Flash Swaps)
- 允许用户借用资金并在同一笔交易中归还(无抵押贷款)。
智能合约
Uniswap V2 采用二元智能合约系统。核心合约(Core)为所有交互方提供基本的安全保证。外围合约(Periphery)与一个或多个核心合约交互,但本身不属于核心部分。
==swap需要使用智能合约,值得注意的是,只有智能合约能够与swap函数交互,因为EOA无法在没有另一个智能合约的帮助下同时发送传入的ERC20代币和调用swap。==
==如果未使用闪电贷,则必须在调用 swap 函数时发送传入的代币。==
部署Uniswap V2 pair合约有两种方式,第一种方式是调用合约Uniswap V2 Rounter02 合约,第二种方法是调用Uniswap V2 Factory合约,在Uniswap V2 Factory合约中的createPair函数将部署一个Uniswap V2pair合约,该合约将持有一对代币,并将管理代币的交换以及流动性的增加和删除。
事实证明地址是按照其数值排序的,而地址是如何转换成数值的呢?
地址是20字节的数据,以十六进制编码,一个字节是8位,因此这20个字节可以转换成160位数字,同样我们也可以将160位数字转换成20字节的十六进制。所以地址转换成数字就可以像普通数字一样排序。
creationCode(创建代码)的简单解释:它是运行时代码加上构造函数参数。
runtimeCode(运行时代码)是编译成字节码的智能合约代码,而且也是发送交易所执行的代码。
create2允许我们在合约部署之前轻松计算出合约地址:
1 | keccak256(0xff, address(deployer), salt, keccak256(creation bytecode)) |
增加流动性所调用的函数:
首先就是调用add_indiquidity函数到Router合约,Router合约将检查交易池是否存在,如果不存在交易池,则Factory合约将使用create2部署。接下来,Router合约将直接从用户转移两种代币到Pair合约,然后调用Pair合约中的_mint函数,进行Router合约和Pair合约之间的交互。当swap函数被调用时,请注意,代币被直接转移到Pair合约。最后将存入等值的两种 ERC20 代币到交易对的流动性池中的用户中。
- 用户批准(approve)Router 合约可以使用 TokenA 和 TokenB
- 用户调用 Router 合约的
addLiquidity函数- 提供 tokenA、tokenB 地址
- 指定愿意提供的最大 token 数量(
amountADesired,amountBDesired) - 设置最小可接受数量(
amountAMin,amountBMin)防止滑点
- Router 查询 Pair 地址
- 如果 Pair 不存在,通过 Factory 创建 Pair 合约
- Router 把 tokenA 和 tokenB 转入 Pair 合约
- Router 调用 Pair 合约的
mint(to)函数mint()计算这次添加的 token 占总池子的比例- 给流动性提供者铸造对应比例的 LP token
- LP token 发送给用户(
to)地址
减少流动性所调用的函数:
要从Uniswap V2 Pair合约中减少流动性,用户首先需要调用Uniswap V2 Router合约上的removeLiquidity函数,然后批准(approve)Router合约之后转移LP代币,接下来Router合约会在Pair合约上调用burn函数,销毁股份,然后转移代币返回给用户。
用户先 approve LP token 给 Router 合约
UniswapV2Pair的 LP token 是 ERC20,所以需要先批准 Router 使用它。
用户调用 removeLiquidity
- 指定要移除多少流动性(即 LP token 数量)
Router 调用 Pair 合约的 burn(to) 函数
burn()销毁 LP token,并返回对应数量的 tokenA 和 tokenB 到to地址
Router 检查是否满足最小数量要求 amountAMin / amountBMin
- 防止滑点过大造成损失
tokenA 和 tokenB 转入用户的钱包
核心(Core)
源码
核心部分由一个单例工厂(Factory)和多个交易对(Pairs)组成,工厂负责创建和索引这些交易对。
这些合约设计极为简约,甚至可谓“极简主义”。其主要逻辑是:
- 代码简洁的合约更易理解,漏洞更少,功能更优雅。
- 许多系统的预期特性可以直接在代码中验证,减少错误空间。
然而,这种设计的缺点是,核心合约对用户不够友好。实际上,大多数情况下不建议直接与核心合约交互,而是应通过外围合约进行操作。
工厂(Factory)
工厂合约存储了用于生成交易对(Pair)的通用字节码。它的主要作用是:
- 确保每个唯一代币对只创建一个智能合约。
- 包含开启协议费用(Protocol Charge)的逻辑。
交易对(Pairs)
交易对合约的主要作用包括:
- 自动化做市(AMM)
- 维护流动性池的代币余额
- 提供去中心化价格预言机的数据
外围(Periphery)
外围部分是一组智能合约集合,用于支持与核心合约的特定交互。由于 Uniswap 具有无许可性(permissionless),这些合约没有特殊权限,只是可能外围合约的一小部分示例,但它们展示了如何安全、高效地与 Uniswap V2 交互。
库(Library)
提供了多种便捷函数,用于数据获取和价格计算。
路由(Router)
路由器基于库(Library)**构建,支持前端提供的所有基础功能,包括**交易**和流动性管理**。其特点包括:
- 支持多交易对路径(例如:X → Y → Z)
- ETH 作为一等公民(即对 ETH 进行特殊支持)
- 支持元交易(Meta-Transactions),用于流动性移除
设计决策
以下部分介绍了 Uniswap V2 的重要设计决策,适用于希望深入了解其底层机制或进行智能合约集成的开发者。
代币传输机制
通常,智能合约在执行某些代币操作前,需要用户先批准(approve),然后调用合约方法,该方法再通过 transferFrom 进行转账。
Uniswap V2 不采用 这种方式。相反,它在每次交互结束时检查自身的代币余额,并在下一次交互开始时,计算当前余额与之前存储值的差额,从而确定用户发送的代币数量。
关键点:在调用需要代币的函数前,代币必须先转移到交易对合约。
例外情况:闪电兑换(Flash Swaps)。
WETH 处理
与 Uniswap V1 不同,V2 不直接支持 ETH,而是使用 WETH 模拟 ETH⇄ERC-20 交易对。
动机:移除核心合约中的 ETH 相关代码,使代码更加简洁。
用户体验:外围合约会自动封装/解封 ETH,用户无需关注这一实现细节。
路由合约完全支持通过 ETH 交互 WETH 交易对。
最小流动性(Minimum Liquidity)
为了减少舍入误差并增加流动性最小单位的理论精度,每个交易对的流动性池会燃烧(burn)第一批 MINIMUM_LIQUIDITY 代币。
- 对于大多数交易对来说,这个数值是极小的,影响可以忽略不计。
- 燃烧机制是自动完成的,发生在首次流动性提供时,此后
totalSupply将保持永久受限。
Uniswap V2 的架构
Uniswap V2 的架构被拆分为两个主要的库(libraries)主要是为了优化 gas 费用、提高代码复用性,并确保安全性。具体来说,Uniswap V2 采用了以下两个核心库:
1. UniswapV2Library
UniswapV2Library 主要是 纯函数(pure functions) 组成的工具库,不存储任何状态,目的是计算交易对地址、获取价格、进行兑换计算等。它的主要功能包括:
- 计算交易对地址(pair address):基于两个代币的地址计算 Uniswap V2 池子的地址。
- 获取储备量(getReserves):用于获取某个交易对的流动性池储备量。
- 计算兑换比率(getAmountOut、getAmountIn):用于计算在给定输入或输出时的代币交换数量。
- 计算最优路径(pairFor):用于在交换路径中找到正确的交易对地址。
这个库被设计成 纯计算工具,因为 Solidity 的纯函数不会修改状态,调用时的 gas 费用更低,且可以被多个合约复用。
2. UniswapV2Pair
UniswapV2Pair 是核心的 AMM 交易对(流动性池) 合约,每个交易对(例如 ETH/USDT)都有一个 UniswapV2Pair 实例。其核心功能包括:
- 存储流动性池的状态(例如代币储备量、累计价格等)。
- 实现 Swap 交易(swap 函数)。
- 管理流动性(mint 和 burn 函数)。
- 跟踪价格变化(更新
priceCumulative和blockTimestampLast)。
这个合约负责实际的代币转移,并存储 Uniswap V2 交易对的状态。
为什么要拆分?
- 优化 gas 费用:
UniswapV2Library作为纯工具库,不存储状态,不需要SSTORE操作(修改存储变量的成本很高),使得调用计算函数的 gas 费用更低。 - 提高代码复用性:
UniswapV2Library可以被多个不同的合约(如路由器合约UniswapV2Router02)直接调用,减少重复代码,提高可维护性。 - 安全性:
交易对的状态和计算逻辑分开,避免合约直接修改关键的储备数据,减少潜在攻击面。
结论
Uniswap V2 的两个库各自承担不同的职责:
UniswapV2Library:负责计算,纯函数,低 gas,易复用。UniswapV2Pair:管理交易对,存储状态,执行 swap 交易。
这种设计让 Uniswap V2 既高效又安全,同时减少了 gas 成本,是去中心化交易所(DEX)智能合约架构的经典案例。
🔹 2. 恒定乘积公式
x * y = K(L^2^)
**
x**:池中的代币 A 数量**
y**:池中的代币 B 数量**K(L^2^)**:一个恒定值,表示池子的总流动性
==K实际上并不恒定:==
通常 Uniswap V2 采用 XY=K 公式,但实际上 K 不是严格恒定,可以增大但不能减少。尽管AMM公式有时被称作“恒定乘积公式”
- 如果用户多支付代币或捐赠代币,
K增大,流动性池变大。 - 如果
K变小,交易会失败,确保资金池不会被恶意消耗。
例如:
- 如果用户 免费存入额外代币,池子变大,但 Uniswap 不会阻止这种情况。
- 但如果
balance0 × balance1 < reserve0 × reserve1,交易会回滚。
==Uniswap v2 最初按所提供金额的几何平均值铸造股份,流动性 = sqrt(xy)。该公式确保任何时候流动性池股份的价值基本上与最初存入的流动性比例无关……上述公式确保流动性池股份的价值不会低于该池中储备的几何平均值。==
为什么流动性计算使用 sqrt(K)?
- Uniswap V2 采用 几何均值 计算流动性: $liquidity = \sqrt{amount0 \times amount1} - MINIMUM_LIQUIDITY$
- 为什么使用平方根?
- 保证 LP 代币价值独立于最初存入的代币比例。
- 防止流动性计算变得不合理(如流动性翻倍却导致计算上变成四倍)。
==流动性提供者无法控制其资产售出的价格,只能根据池中代币的当前比例提供资产。如果有 100 个代币 x 和 200 个代币 y,新流动性提供者必须提供两倍于 x 的代币 y。同样的,如果用户销毁一定比例的代币,则可以赎回相应比例的代币x和代币y。==
公式变换:
x0:表示流入池中的代币A数量
y0:表示流出池中的代币B数量
(y - y
0) =K/(x+x0)y
0=y - K/(x+x0)==y
0= y * x0/(x+x0)==(没有手续费)
交换费率为F(0.3%(固定))
计算交换费:F*流入的代币数量
则:==y0 = (1-F)y * x0/(x+x0(1-F))==
在实践中,交换费用会添加到储备金中。因此,每笔交易实际上都会增加 K。
恒定乘积公式的流动性并不是实际应用的流动性,它会减去一些最低流动性。这样做是为了防止被称为“买入通胀攻击”的攻击。
买入通胀攻击:简单的解释是,当总供应量等于0时,有很大的空间可以操纵铸造的股票数量,这可能会对后续用户铸造股份造成伤害。而防止买入通胀攻击的方法之一是铸造一些股票到0地址,通过这样做,我们可以保证一些代币被锁定在0地址内。之二是在合约部署时,由可信方注入一定的 token 和 ETH/WETH,并铸造 LP token 到一个多签或销毁地址,降低后续加入者的稀释风险。之三是在前端警告用户当总供应为 0 时交易风险。
Mint(铸造流动性)
如果池子是空的(即 LP 代币总供应量 = 0),那么表示还没有人提供流动性。
代码关键点解析:
- 池子为空的检查
- 如果池子是空的,进行初始铸造(下面详细解释)。
- 一般情况的 Mint 逻辑:
- 用户铸造的流动性是
amount0 / reserve0和amount1 / reserve1两者取最小值(绿色框,行 126)。 - 为什么取最小值?
- 防止用户只存入一种代币,却获取了过多的 LP 代币。
- 保证存入的
token0和token1保持相同比例
- 用户铸造的流动性是
例子:
假设池子有:
token0 = 10token1 = 10
用户存入:
token0 = 10token1 = 0
计算公式:
min(10/10, 0/10) = min(1, 0) = 0- 用户将无法获得 LP 代币!
为什么要这样设计?
假设池子最初有 100 token0 和 1 token1,LP 代币供应量是 1。
- 现在某个用户存入 1 个
token1,总池子价值变为200美元。 - 如果我们采用最大值计算,该用户可能获得 1 个 LP 代币,总供应量变为 2。
- 这意味着他们以 100 美元的成本,直接占有了 50% 的流动性池,而原来的 LP 持有者被稀释了!
- 因此,必须取最小值,确保存入代币的比例保持不变,防止被利用。
安全性检查
- 供应比例检查:
- 交易执行前,池子的
token0和token1比例可能会发生变化。 - 如果另一个交易改变了储备量,用户最终获得的 LP 代币数量可能会低于预期。
- Uniswap V2 允许一定的误差,以防止交易因为小幅变化而回滚(slippage 保护)。
- 交易执行前,池子的
解决首次铸造问题(First Minter Problem)
在流动性池中,第一个提供流动性的人 面临“通胀攻击”风险。
- Uniswap V2 销毁初始
MINIMUM_LIQUIDITY代币,防止某个人拥有全部 LP 代币,并随意操控价格。 - 这个方法避免了流动性恶意操纵问题,具体内容可以参考 ERC4626 的防御机制。
mintFee 的计算逻辑
(1) 重要变量
fee:交易者支付的 0.3% swap 费用。mintFee:协议分得的 1/6 swap 费用(即 0.05% 交易量)。L:池子的 **流动性(liquidity),即 √(token0 reserves * token1 reserves)**。rootK:当前池子的 流动性(含手续费)。kLast:上一次 mint/burn 时的流动性,用于计算增长部分。totalSupply:LP 代币的总供应量。
(2) 计算 mintFee
计算池子的总流动性增长:
- 初始流动性:
L_old = sqrt(reserve0_old * reserve1_old) - 当前流动性:
L_new = sqrt(reserve0_new * reserve1_new) - 增量流动性 =
L_new - L_old
- 初始流动性:
计算协议应得份额:
- 由于协议收取 1/6 swap 费用,协议应得的流动性: $L_{protocol} = \frac{(L_{new} - L_{old})}{6}$
计算协议应该铸造的 LP 代币数(避免直接转移 token,改为稀释 LP 代币):
$$
mintFee = \frac{L_{protocol} \times totalSupply}{L_{new}}
$$
增加流动性
为了避免原始股被稀释,新的用户需要用自己要铸造的的股份除以当前总股数,进而再重新计算占比。(换了个大蛋糕,每个人分的量不变,只是蛋糕变大了,多出的是新用户的注入的)。换句话说,无论我们从L0增加到L1的百分比是多少,就应该铸造相同百分比的股份。同理,当用户想取回他那份股份,即可用他所占的股份比例乘以池子的总价值。
一般众筹合约都是两个合约,一个合约是给参与的用户发放凭证(代币),一个进行具体的众筹合约。前者发放的代币一方面是用于股份所属的凭证,另一方面,当众筹合约完成时,获得的报酬按凭证领取。当然凭证也可以转让给别人。相比之一个合约更灵活。
T是总股数
s是要铸造的股份
L0是交易池的铸造新股份之前的价值
L1是交易池的铸造新股份之后的价值
添加流动性之前,流动性等于添加后AMM的价格,另一种说法是,添加流动性前后,两种代币的汇率不变(比例相同)。汇率相同的两点连线过原点,相差的dx和dy即我们在保持相同汇率前提下需要我们添加的代币数量。dx/dy同样也必须满足汇率。
添加流动性后,股票数量的计算:
同时根据汇率代换出流动性等于两倍的代币x(y)的数量,下图均以x、dx为计算量,y、dy同理
减少流动性
现在,当我们从AMM中减少流动性时,我们要再次遵循同增加流动性一样的规则,即减少流动性后的价格必须等于之前的价格。换句话说,减少、增加流动性时两种代币的汇率必须保持不变。
示例
用 25 ETH 交换 25 USDC 意味着我们向 AMM 存入 25 ETH,并从中提取 25 USDC。这将调节流动性池的流动性到 125 ETH 和 75 USDC。AMM 将拒绝此交换,因为池的 恒定乘积 在交换后减少了。(如果考虑手续费,更不会出现25 ETH 交换 25 USDC 的情况)
假设池中有:
100 ETH & 10,000 USDC
用户想要买入 1 ETH:
计算:y = L^2^ / x’
- 取出 1 ETH,USDC 余额减少
x变为101 ETHy变为9,900 USDC- ETH 价格上升(滑点)
当我们投入一些代币x并取回一些代币y
dx即变化的代币x数量,dy即变化的代币y数量
把交换前后的两点连接后,会有一条线,它会告诉我们投入的每个dx所获得的代币y的数量,换句话说,这条线的斜率将给出代币x到代币y的汇率。即:
1 | 执行价格 = 代币收到的金额除以发送的金额 |
而当dx无限接近于0(我们的交易规模越来越小)就会有一条线,这个线即为池子流动代币曲线的切线。
这条切线的斜率给出了这两种代币的当前价格,当前价格也被称为现货价格,在Uniswap V2 中,也被称为中间价。
- ==y
0/x0== - y
0和x0是当前AMM中的两种代币数量
事实证明,代币的价格与这个线的斜率有关。当我们进行小额的交换时,汇率近似于现货价格,如图
通常,当你在AMM上交易时,AMM给你的价格并不是交易执行时你将获得的价格,这称为滑点(Slippage)
滑点是指预期收到的价格与实际价格之间的差额
滑点的主要原因是市场波动,以及时间(发送交易之间的延迟)引起的代币价格变化。
- 市场波动:
假设 ETH/USDT 池中目前有 1 ETH 和 2000 USDT,k 为 2000。我们计划使用 500USDT 兑换 0.25 ETH,在此应用此公式:
这意味着我们只能获得 0.2 ETH 而不是 0.25 ETH。交换越多,滑点造成的损失就越大。 例如,如果交换 2000USDT,理论上你可以获得 1 ETH,但实际上你只能获得 0.5 ETH。这种滑点我们可以通过增加流动性来降低损失,但是无法避免。更高的流动性意味着更小的价格影响。这意味着:在投入相同数量代币的情况下,交易者将获得更多代币作为回报。
- 时间:
假设在同一时间内,Alice和Bob都发送了相同的交易,在他们的交易提交后,假设他们的交易排序是Alice的交易首先执行,然后是Bob的交易被执行。(顺序是由块构建者(矿工)决定的) 。同时,这两笔交易可以在同一个区块中,也可以是Alice的交易在一个区块中,Bob的交易出现在Alice的交易所在区块之后的另一个区块中。现在Alice的交易被执行,她将获得预期数量的代币。但是由于AMM中的代币数量和执行之前的不同(汇率不同),这就会导致后执行的Bob只能获得少于预期数量的代币。
🔹 3. Uniswap V2 关键智能合约
核心合约
UniswapV2Factory- 负责创建交易对(pair)
- 记录所有交易对的地址
UniswapV2Pair- 每个交易对(流动性池)都是一个
Pair合约 - 处理流动性提供、交易和移除流动性
- 每个交易对(流动性池)都是一个
UniswapV2Router- 提供用户友好的交易方法,如
swapExactTokensForTokens - 负责路径计算、多跳交易(ETH -> DAI -> USDC)
- 提供用户友好的交易方法,如
🔹 4. Uniswap V2 Swap 交易
主要函数
swapExactTokensForTokens()- 以固定数量的
input兑换尽可能多的output - 适用于 卖出一定量的代币。
1
2
3
4
5
6
7function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external;- 以固定数量的
swapTokensForExactTokens()- 以固定数量的
output兑换尽可能少的input - 适用于 想买入确定数量的代币。
- 以固定数量的
getAmountOut()- 计算交换后能获得多少代币
1
2
3
4
5function getAmountOut(
uint amountIn,
uint reserveIn,
uint reserveOut
) external pure returns (uint amountOut);
5. 代码示例
查询交易对
1 | address factory = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; // Uniswap V2 Factory 地址 |
添加流动性
1 | IERC20(tokenA).approve(router, amountA); |
交换代币
1 | IERC20(tokenA).approve(router, amountIn); |
🔹 6. Uniswap V2 闪电交换(Flash Swap)
在DeFi中,有一种叫做Flashnow(闪电贷)的东西。闪电贷被认为是Defi中最大的创新,其最大的挖掘了资金利用效率,消除了资本差异。如果DeFi协议支持闪电贷,那么这将允许智能合约从该DeFi协议中获得任意数量的代币,然后在同一笔交易中进行操作,最终在交易结束前还回代币(或者支付等值代币)。Uniswap V2中也有个flashnow,但这里称之为Flash Swap(闪电交换)。当智能合约使用闪电贷借代币时,它必须支付多于借入资金的量(借入资金加上一些费用)。Uniswap 要求使用 Flash Swap 的用户 支付最小费用,这个费用取决于借出的代币数量。由于以太坊交易时原子性的,如果事实证明合约没有收到足够的代币,则整个交换可能会回退,也不会扣除0.3%的费用。
债务再融资:债务再融资:将已有的贷款从一个平台转移到另一个平台,通常是为了获得更好的利率或更低的抵押要求。
抵押率(Collateral Ratio):借款人需要提供的超额抵押比例。例如,150% 意味着借 100 的资产需要提供价值 150 的抵押物。
图中流程详解(右侧图)
- 用户进行 Flash Swap:
- 用户使用 Flash Swap(例如从 Uniswap)获取一笔资金(例如 ETH)。
- 注意,这笔资金无需前期抵押,但必须在一个交易中偿还。
- 用户用 Flash Swap 资金偿还原贷款:
- 用户使用拿到的 ETH 偿还在原平台(如 Compound)上的贷款。
- 原平台的抵押率高(150%),债务清除后,抵押资产释放。
- 用户将释放的抵押资产重新抵押到新平台(如 Liquid):
- Liquid 的抵押率较低(120%),因此可以在相同的抵押资产下借出更多资金。
- 用户使用新借出的资金偿还 Flash Swap:
- Flash Swap 得到偿还,交易结束。
举个例子来帮助理解:
假设:
- 在 Compound 上用户抵押了 $150 的 ETH 借了 $100 的 DAI。
- Liquid 平台只要求 120% 抵押比,即 $120 的 ETH 可以借 $100 的 DAI。
流程如下:
- 用户用 Flash Swap 借来 $100 DAI。
- 用这 $100 DAI 偿还 Compound 的贷款,释放 $150 ETH。
- 将 $150 ETH 抵押到 Liquid,借出 $125 DAI。
- 拿出其中 $100 偿还 Flash Swap,剩下$25 DAI。
总结
图中展示的“债务再融资”场景,是使用 Flash Swap 无需预先资金即可:
- 偿还高抵押率的旧债务,
- 并转移到抵押要求更低的新平台,
- 同时可能还能获得额外净收益。
套利不是一个完全弊大于利的行为,相反,我觉得它是一个有利于维护市场稳定的健康的获利行为。更多情况下,如果市场参与度高的话,套利会稳固价格的稳定从而被动的维护市场价格机制。如果一个市场,套利交易进行的少,会更不安全。套利是交换逻辑下,市场默许的(闪电贷更是进一步方便套利)。总要有套利的发生来维护市场的平衡。一个健康的市场需要繁荣的资金流动,这就说明市场离不开套利。
- 可以无抵押借出代币
- 但必须在同一交易内归还(否则交易回滚)
1 | function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external { |
Uniswap V2 使用如下不等式来计算最小费用:
x0 - dx0 + 0.997 * dx1 >= x0
其中:
x0:闪电交换前,合约中 token X 的数量dx0:借出的 token X 数量dx1:偿还给合约的 token X 数量
这个公式表示:即使借出并归还 token,池中最终资产不能低于原始状态。
推导过程(通过类比普通交易)
普通 Swap(dx 换 dy)
- 你输入 dx(代币 X)
- 合约收取 0.3% 手续费,实际用于交换的是
0.997 * dx - 得到 dy(代币 Y)
Flash Swap 场景
- 借出 dx0(代币 X)
- 最终归还 dx1(注意:dx1 ≥ dx0,因为要加费用)
- 相当于你从合约拿走 dx0,最后放回 dx1(带手续费)
数学推导过程
给定以下两条等式:
1 | x0 - dx0 + 0.997 * dx1 >= x0 // 保护池子不亏 |
推导步骤:
- 消去
x0:
1 | - dx0 + 0.997 * dx1 >= 0 |
- 用第二个式子代入:
1 | 0.997 * (dx0 + fee) >= dx0 |
- 展开并整理:
1 | 0.997 * dx0 + 0.997 * fee >= dx0 |
最终结论:
最小费用 = (0.003 / 0.997) * 借出数量
结论
- Flash Swap 的费用机制是为了保护流动性池。
- 它确保无论你借多少,最后偿还的数量要多于借出,防止套利攻击。
- 本质上,这和普通 Swap 的 0.3% 手续费逻辑保持一致,只是换了一个方向:“你先拿,再还”。
闪电交换的步骤:
用户先通过调用内部函数来启动闪电交换,这个合约必须调用Pair 合约上的swap函数。由于Pair合约将发送请求的代币数量,因此在闪电贷之前,我们不必发送任何代币,还能借出代币。
用户通过调用flash swap智能合约中的函数来启动flash swap,然后flash swap合约调用Uniswap V2 Pair合约上的swap函数。Uniswap V2 Pair合约同时将请求数量的代币发送到flash swap合约。然后flash swap合约可以使用借入的代币执行任何自定义逻辑。例如可以执行套利,执行我们的自定义逻辑后,我们需要偿还借入的代币以及一些费用。这些都是通过调用flash swap合约内的uniswapv2call函数来完成的。
Uniswap V2 Pair合约验证flash swap合约是否已经偿还借入的代币和费用。如果成功,则交易完成。如果失败,则swap函数回退,导致交易失败。
🔹7. Uniswap V2 vs V3
| 特性 | Uniswap V2 | Uniswap V3 |
|---|---|---|
| AMM 机制 | 恒定乘积公式 x * y = L^2^ | 集中流动性(指定价格范围) |
| 流动性提供 | 资金分布在整个价格范围 | LP 资金可集中在指定范围 |
| 交易费用 | 0.3%(固定) | 0.05%, 0.3%, 1%(可选) |
| 滑点 | 较大 | 较小 |
| 闪电贷 | ✅ 支持 | ❌ 不支持 |
| 预言机 | TWAP 价格 | 更精准的 TWAP 价格 |
** 结论**
- Uniswap V2 仍然是 去中心化交易所的基础,支持 ERC-20 交易、流动性池、闪电贷。
- 如果需要更精准的价格管理(LP 收益最大化),可考虑 Uniswap V3。
- 套利、流动性挖矿、MEV 等策略 仍然可在 V2 上运作。
Uniswap V2 克隆版本开发清单
1. Solidity 版本 & 基础优化
- 使用最新的 Solidity 版本 (>=0.8.0) → 这会影响语法,并移除对 SafeMath 的需求。
- 用 自定义数据类型 替换 定点数,提高效率。
- 使用 Solady ERC20 来优化 ERC20 逻辑(降低 Gas 费用)。
- 避免 Uniswap V2 的重入保护机制,它不够高效 → 使用 **OpenZeppelin 的
ReentrancyGuard**。
2. 安全性 & 重入攻击防护
- 在 价格预言机中使用
unchecked代码块 → Uniswap 依赖溢出计算。 - 如果不单独实现路由器(Router),需要在合约中加入滑点检查(EOA 账户无法在交易中直接发送代币)。
- 正确放置重入锁 → Uniswap V2 是否容易受到只读重入攻击?
- 注意
fee-on-transfer代币或重基代币,它们可能影响 Swap 逻辑。 - Uniswap V2 的
_safeTransfer可能受到内存扩展攻击 → 修复方式:仅returndatacopy一个bool值。
3. 工厂合约 & 交易对合约优化
- 优化工厂合约(Factory) → Solidity 更新后可省去
assembly。 - Uniswap V2 交易对管理方式不够高效 → 需要改进 Gas 费用。
- 使用
immutable修饰符优化存储变量(Uniswap V2 早期版本未支持)。 - **用自定义错误 (
custom errors) 代替require**,减少部署成本。 - 确保销毁初始流动性时
totalSupply不降为 0,否则会破坏 首次存款攻击防护机制。 - 确保在计算份额时,销毁 (burn)、铸造 (mint) 和更新储备 (update reserves) 顺序正确。
4. Gas 优化 & 数学计算
使用 Solady 的
sqrt方法 优化平方根计算 → 确保 向正确方向舍入。避免硬编码的魔法数字,比如 Swap 费率等。
流动性不应在未调用
burn()的情况下减少(编写不变量测试)。不要用池子余额作为预言机 → 容易受到 闪电贷操纵。
交易、铸造和销毁 LP 份额时,一定要向池子有利的方向舍入。
5. 测试 & 进阶学习
- 编写单元测试,确保合约安全性。
- 阅读 Solidity Gas 优化书籍(在完成 v2 或 v3 版本后)。
- 如果想 更进一步,可以尝试用 Yul/assembly 优化 Gas 费用。
- 尝试 DamnVulnerableDefi Puppet V2 挑战** → 完成 Uniswap 克隆后,你应该可以轻松解决它。
预言机(Oracle)的定义
在计算机科学中,预言机(Oracle) 是“真相的来源”。价格预言机 作为价格信息的来源,允许其他智能合约获取价格。
Uniswap 作为一个自动做市商(AMM),天然地隐含价格,其他智能合约可以利用这个机制作为价格预言机,而无需依赖中心化交易所(CEX)。
但直接使用资产余额计算价格并不安全。
TWAP(时间加权平均价格)的动机
如果直接使用池子中资产的快照价格,很容易受到闪电贷攻击(Flash Loan Attack)。攻击者可以用闪电贷进行大额交易,短时间内改变价格,并利用依赖这个价格的智能合约来套利。
Uniswap V2 预言机通过两种方式防止这种攻击:
- 提供价格的时间加权平均值(由用户选择的时间窗口),意味着攻击者需要在多个区块持续操纵价格,这比单次闪电贷更昂贵。
- 不使用当前余额计算价格,而是依赖累计价格(price accumulator)。
⚠️ 但是,使用移动平均的预言机仍然可能受到操纵:
- 如果池子的流动性很低,攻击者仍然可以控制价格一段时间。
- 如果时间窗口太短,攻击者可以在较短时间内操纵平均价格。
TWAP 是如何工作的?
TWAP(时间加权平均价格)类似于移动平均,但不同之处在于:
- 价格保持的时间越长,其权重越大。
例如,某资产过去一天的价格:
- 前 12 小时 = $10 , 后 12 小时 = $11
- TWAP = ($10×12 + $11×12) / 24 = $10.5
- 前 23 小时 = $10,最后 1 小时 = $11
- TWAP = ($10×23 + $11×1) / 24 = $10.0417(更接近 $10)
- 前 1 小时 = $10,后 23 小时 = $11
- TWAP = ($10×1 + $11×23) / 24 = $10.9583(更接近 $11)
通用 TWAP 公式:
$$
\text{TWAP} = \frac{\sum (价格 \times 持续时间)}{\sum \text{时间}}
$$
其中 T 代表时间窗口的长度。
Uniswap V2 只存储累积价格
如果用户想要查看最近 1 小时、1 天 或 1 周 的价格,Uniswap 不能存储所有可能的回溯时间窗口,也没有办法定期快照价格(因为需要支付 Gas 费)。
解决方案:
Uniswap 仅存储价格累积值的分子,每次流动性比率发生变化时(mint、burn、swap、sync),它会记录新价格和上次价格持续的时间。
变量:
price0CumulativeLast和 **price1CumulativeLast**:累积价格- 这两个变量只能递增,直到溢出,不会减少
如何限制回溯窗口?
通常,我们不关心池子自创建以来的平均价格,而是希望计算最近 1 小时或 1 天的 TWAP。
设 T4 是我们关心的时间窗口起点。我们需要计算:
$$
\text{TWAP} = \frac{\text{price0CumulativeLast} - \text{过去的快照值}}{\text{时间间隔}}
$$
我们可以在 T3 进行快照,然后等 T4 结束后计算价格差,并除以时间窗口大小,即可得到最近一段时间的 TWAP。
如果最近一次快照超过 3 小时怎么办?
如果交互的交易对在 过去 3 小时内没有交易,那么 lastSnapshotTime 可能会过时,导致合约无法更新快照。
Uniswap V2 的 _update 函数 仅在 mint、burn 和 swap 时调用,如果这些操作都没有发生,lastSnapshotTime 可能是很久以前的时间。
解决方案
**预言机在进行快照时调用 sync**,因为 sync 会在内部调用 _update,保证数据是最新的。
为什么TWAP必须跟踪两个比率?:
资产 A 相对于 B 的价格可以简单地表示为 A/B,反之亦然。例如,如果池子中有 2000 USDC(忽略小数位),以及 1 ETH,那么 1 ETH 的价格就是 2000 USDC / 1 ETH = 2000 USDC。
USDC 相对于 ETH 的价格 则是上述数值的倒数,即 1 / (2000 USDC / 1 ETH) = 1 ETH / 2000 USDC。
然而,在累积价格时,我们不能简单地对其中一个价格取倒数来得到另一个价格。例如,如果价格累加器(price accumulator)初始值为 2,然后增加了 3,我们无法直接计算其倒数来得到正确的值:
$$
\frac{1}{2 + 3} \neq \frac{1}{2} + \frac{1}{3}
$$
尽管如此,这两个价格仍然是 “某种程度上的对称”,因此在使用定点数运算(fixed point arithmetic) 时,整数部分和小数部分的存储能力必须相同。例如,如果 ETH 的价值是 USDC 的 1000 倍,那么 USDC 的价值就是 ETH 的 1/1000。为了准确存储这个比例,Uniswap 选择了 u112x112 作为定点数格式,即 112 位用于整数部分,112 位用于小数部分。
priceCumulativeLast 一直递增,直到溢出后继续递增
Uniswap V2 诞生于solidity 0.8.0 之前,当时的solidity版本默认支持溢出(overflow)和下溢(underflow)。在现代 Solidity 版本中,正确的价格预言机实现需要使用 unchecked 代码块,以确保计算发生溢出时的行为仍然符合预期。最终,priceAccumulator 和 block.timestamp 都会发生溢出。在这种情况下,新的储备量(reserve)可能会小于之前的储备量。当预言机计算价格变化时,可能会得到一个负值。然而,由于模运算(modular arithmetic) 的特性,这不会影响最终的计算结果。
举个简单的例子,假设我们使用的无符号整数 溢出边界为 100:
- 在快照时,
priceAccumulator记录为 80。 - 过了一些区块后,
priceAccumulator增加到了 110,但由于溢出,它变成了 10(即 110 mod 100 = 10)。 - 计算变化量:10 - 80 = -70。
- 由于这是无符号整数,计算结果变成 -70 mod 100 = 30,这与没有溢出的情况相同(110 - 80 = 30)。
这个原理适用于所有的溢出边界,不仅仅是示例中的 100。因此,无论 timestamp 还是 priceAccumulator 发生溢出,都不会影响 Uniswap 预言机的正确性。
时间戳溢出
同样的现象也适用于时间戳(timestamp)。在 Uniswap V2 中,block.timestamp 使用的是 uint32 类型,因此它不会变成负数。
假设时间戳的溢出边界为 100,我们在 time = 98 进行快照,然后在 time = 4 查询预言机,则时间差计算如下:
$$
4 - 98 \mod 100 = 6
$$
这与真实经过的 6 秒时间一致,再次验证了模运算的正确性。因此,即使时间戳溢出,也不会影响预言机的计算。
导入Uniswapv2库(核心):forge install Uniswap/v2-core --no-commit
流动性提供者(Liquidity Providers)
流动性提供者(LPs)并不是一个同质化的群体:
- 被动 LPs 是希望通过投资其资产来累积交易手续费的代币持有者。
- 专业 LPs 以做市为主要策略,通常会开发自定义工具来跟踪其在不同 DeFi 项目中的流动性头寸。
- 代币项目方 有时会选择成为 LP,以为其代币创造一个流动的市场。这使得代币更容易买卖,并通过 Uniswap 实现与其他 DeFi 项目的互操作性。
- DeFi 先锋 正在探索更复杂的流动性提供模式,如激励流动性、流动性作为抵押品等实验性策略。Uniswap 是这些创新的理想实验平台。
交易者(Traders)
在 Uniswap 协议生态系统中,交易者可以分为几类:
- 投机者 使用各种社区构建的工具和产品,在 Uniswap 协议中交换代币。
- 套利机器人 通过比较不同平台的价格来寻找套利机会。(虽然这种行为看似是在提取利润,但实际上有助于平衡更广泛的以太坊市场价格,使其更公平。)
- DAPP 用户 在 Uniswap 购买代币,以便在以太坊上的其他应用程序中使用。
- 智能合约 通过实现交换功能(如 DEX 聚合器或自定义 Solidity 脚本)在协议上执行交易。
在所有情况下,交易者都需支付相同的固定交易手续费。这些交易者在提升价格准确性和激励流动性方面都发挥着重要作用。
术语表(Glossary)
Defi
Defi是基于新兴区块链的无许可和透明金融服务的生态系统。
“Money Legos” 是 DeFi(去中心化金融)领域中的一个核心概念,指的是各种去中心化协议和智能合约可以像乐高积木一样被组合、叠加,从而构建出复杂而强大的金融应用。
DEX
去中心化交易所是一个P2P(peer-to-peer 点对点)市场,用户拒可以在这里以非检测方式交易加密货币,而无需中介机构进行资金的转让和托管,DEX可以替代传统意义上的中介机构——银行、经纪人、付款机以及其他机构,甚至是基于区块链的用于资产交换的智能合约。
与传统的金融交易相比,这些交易时不透明的,并且通过中介机构对他们的行为提供提供极为有限的简洁。DEXs为资金的发展和促进交流的机制提供了完全透明度。此外,由于用户资金在交易旗舰没有通过第三方的加密货币钱包,因此DEX降低了交易风险,并可以降低加密货币生态系统中的中心化风险。
DEX是Defi(去中心化金融)的基石。
自动化做市商(Automated Market Maker, AMM)
一种运行在以太坊上的智能合约,持有链上流动性储备。用户可以根据自动化做市公式设定的价格与这些储备进行交易。做市商是为在交易所提供流动性的可交易资产提供流动性的实体,否则可能是流动性的。做市商通过自己的帐户购买和销售资产来实现这一目标,目的是从利差(差价)中获利,这是最高买卖和最低卖出产品之间的差距。他们的交易活动创造了流动性,降低了较大交易的价格影响。
恒定乘积公式(Constant Product Formula)
Uniswap 采用的自动化做市算法,公式为:x * y = k。
ERC20
以太坊上的可互换代币标准。Uniswap 支持所有标准的 ERC20 代币实现。
工厂(Factory)
一个智能合约,为每个 ERC20/ERC20 交易对部署唯一的智能合约。
交易对(Pair)
由 Uniswap V2 工厂(Factory) 部署的智能合约,用于在两个 ERC20 代币之间进行交易。
流动性池(Pool)
一个交易对的所有流动性提供者共同提供的流动性。
流动性提供者(Liquidity Provider / LP)
LP 需要承担价格风险,并通过交易手续费获得补偿。
中间价格(Mid Price)
用户在某一时刻买卖代币的价格之间的中间值。在 Uniswap 中,中间价格等于两个 ERC20 代币储备的比例。
价格影响(Price Impact)
交易执行价格与中间价格之间的差值。
滑点(Slippage)
在交易被提交和执行之间,交易对价格变动的幅度。
核心(Core)
Uniswap 的关键智能合约,其存在对协议至关重要。升级核心合约需要迁移流动性。
外围(Periphery)
非核心但有用的智能合约,即使没有它们,Uniswap 也能正常运行。 可以随时部署新的外围合约,而无需迁移流动性。
闪电贷(Flash Swap)
一种交易方式,允许用户在支付前使用购买的代币。只要用户可以在同一笔区块链交易中偿还借贷资产,允许用户借入没有前期抵押品的资产。闪电贷是用户能够在链上流动性池借入资产,而没有前期抵押品,只要借入的流动性量(加上一些手续费)将在同一笔交易中退还交易池。如果借款人无法再同一笔交易里偿还贷款,整个交易将会回退,包括初始借款和后来采取的任何行动。这种创新的机制增加了各种实例中用户获得资本的机会,同时确保了基本的链上流动性池的持续偿付能力。
闪电贷最常见的用法是用于套利,通过利用大量资本来填补市场的低效率。由于资产在不同市场上的汇率不同。套利这可以通过让市场达到平衡并提高Defi市场中每个人的流动性来产生利润。
闪电贷的另一个常见的例子是清算。许多贷款方案激励了第三方清算人。这些清算人可以为无法满足一定的抵押比率要求的清算贷款获得奖励。通过闪电贷获得大量资本可以帮助及时清算欠款贷款,并且基础协议依然保持偿付能力。
此外,闪电贷还可以用于资助对Defi协议的各种攻击。一旦有人发现了漏洞,攻击者就可以使用通过闪光灯获得的资本来操纵协议的某些功能,并获利,同时潜在地从其智能合约中耗尽资金。此外,由于闪电贷交易因失败而回退,因此黑客不必将大量自己的资本带来危险即可为攻击提供资金。
虽然闪电贷助力了各种攻击的产生,但是能发生这些攻击的根本是协议本身漏洞的存在,闪电贷只是提供了恶意攻击所需的资金。从长远来看,闪电贷甚至可能对Defi生态系统的安全有益。
x * y = k
恒定乘积公式,用于 Uniswap 做市算法。
不变量(Invariant)
恒定乘积公式中的 “k” 值,表示流动性池中总流动性的恒定值。
无偿损失(Impermanent Loss)
当流动性池中代币的价格与添加流动性时的价格发生变化时,就会发生无常损失,从而导致流动性提供者的价值损失。也称(发散损失)。
当你将资产(例如 ETH 和 DAI)以 50/50 比例添加到 Uniswap V2 的池中后,池子会自动执行 恒定乘积公式 $x \cdot y = k$。
当外部价格发生变化时,套利者会不断买卖,导致池内资产比例发生变化。结果就是:你的资产虽然价值没有消失,但数量上发生了变化,导致你取出来的价值比单纯“持币不动”更低。
- 当价格波动越大(离开初始价格越远,无论涨还是跌),无常损失越大;
- 最大损失约为 **-100%**,当价格趋近于 0 或无限大时;
- 在 价格不变(100%) 的点上,损失为 0;
- 这只是理论损失,称为「无常」是因为如果价格最终回到原点,损失就会恢复;
- 实际中,交易手续费(0.3%)和流动性挖矿奖励可以抵消甚至超过无常损失。
时间加权平均价格(TWAP, Time-Weighted Average Price)
TWAP 是在一段时间内对价格的平均,而不是某一瞬间的价格。
通过拉长观察时间区间,TWAP 能大大降低价格被操纵的可能性,因为攻击者必须在整个时间窗口内维持操纵状态,成本非常高。
累计价格
累计价格是一个随时间累积的值,它并不是”价格”,而是“价格 × 时间”的总和。
利用累计价格计算时间加权平均价格:
UQ112x112
UQ112x112 是一种 定点数(Fixed Point) 表示法,全称是 Unsigned Q112.112:
- Q112.112 表示这个数有 112 位整数部分 和 112 位小数部分。
- 它不是浮点数,而是用一个
uint224来表示的定点数,主要用来避免精度误差,尤其是在 Solidity 中没有原生浮点数的情况下。
1 | // 定义方式(Uniswap V2) |
这个表示法常在 _update 函数中用来计算 价格累积器(price accumulator),用于后续的 TWAP(时间加权平均价格)。
为什么用 UQ112x112?
- Solidity 不支持小数,所以 Uniswap 用定点数自己实现了高精度数学。
- 使用
UQ112x112可以 在不丢失太多精度的情况下进行乘除运算。 - TWAP 的核心依赖这个结构来保证价格在时间上的累积是准确的。
翻译理解
1 | priceCumulativeLast += price * timeElapsed; |
其中 price 是 UQ112x112 格式的,也就是:
“112 位整数 + 112 位小数的定点数,用于精确表示价格。”
使用Uniswap v2现货价格作为价格预言机的危险:
1. 易受闪电贷攻击(Flash Loan Attack)
Uniswap V2 使用的是 基于恒定乘积(x*y=k) 的定价模型,价格会随着交易发生而立即变动。如果你在合约中直接使用当前池子的现货价格(即 reserve1 / reserve0)来判断资产价格,很容易被操纵。
攻击流程:
- 攻击者使用闪电贷借入大量资金。
- 在 Uniswap 上制造巨大的买卖,从而操纵池子的价格。
- 在你的合约中触发与价格相关的逻辑(比如低价清算、套利、存款评估等)。
- 攻击者再用剩下的时间把池子的价格恢复(还闪电贷)。
- 获取利润离开。
2. 没有时间加权平均价格(TWAP)保护
Uniswap V2 提供了一个简易的 TWAP 机制(时间加权平均价格),但默认并不会启用,需要开发者在外部手动计算。而使用现货价格就完全没有时间加权,几乎可以被瞬时操纵。
3. 流动性浅时风险更大
在流动性不足的交易对中,只需要较少的资金就可以显著影响价格,攻击成本低廉,特别是在那些非主流代币对(比如某些新发币 / 山寨币对)中,现货价格更容易成为攻击目标。
4. 不能反映真实市场价格
Uniswap 的价格是根据链上供需实时决定的。如果某资产在 Uniswap 上交易量小,而在 CEX 或其他 DEX 上流动性大,那 Uniswap 的现货价格可能与真实市场脱节,误导智能合约做出错误判断。
正确做法建议:
- 使用 Uniswap V2 TWAP 或 Uniswap V3 Oracle(带有观察窗和流动性阈值)
- 使用 Chainlink 等专业的 去中心化预言机服务
- 实现自己的价格平均机制(例如每小时读取一次价格并存储,计算滑动窗口平均值)
套利公式
| 变量 | 含义 |
|---|---|
| $F$ | 手续费率,例如 0.003 |
| $X_A$ | AMMA 的输出代币储备(token X) |
| $Y_A$ | AMMA 的输入代币储备(token Y) |
| $X_B$ | AMMB 的输入代币储备(token X) |
| $Y_B$ | AMMB 的输出代币储备(token Y) |
| $dy_a$ | 向 AMMA 输入的 token Y 的数量(我们要求解的变量) |
| $dy_b$ | 从 AMMB 得到的 token Y 数量(套利收益) |
将这些带入 $F(dy_a)$,对其求导,然后令 $F’(dy_a) = 0$,我们可以求出最优值。

























