一、基础认知
1. 什么是 Uniswap?
- 去中心化交易所(DEX),基于「自动做市商(AMM)」机制,无需对手方,用户直接与智能合约资金池交易
- 前端核心交互场景:代币兑换、添加/移除流动性、查询价格、闪电兑换调用
- 相关的核心依赖:智能合约ABI、链上数据读取(价格、储备金)、交易发送(签名+上链)
2. V1 vs V2 核心差异
|
|
|
|
| 对比维度 |
Uniswap V1 |
Uniswap V2 |
前端关注重点 |
| 开发语言 |
Vyper(类Python) |
Solidity(主流) |
后续交互、调试均基于Solidity合约,需熟悉Solidity基础语法 |
| 预言机功能 |
实时价格,易操纵 |
TWAP时间加权平均价,抗操纵 |
前端价格查询逻辑变更,需实现「打点存累加值」逻辑 |
| 核心功能 |
基础代币兑换 |
闪电兑换、双价格跟踪、非标ERC20兼容 |
新增闪电兑换回调合约调用、计价单位切换逻辑 |
| 存储优化 |
无打包存储 |
256位插槽打包存储 |
前端读取储备金、价格数据时,需对应合约存储结构 |
3. 关键技术选型原因
- V2 改用 Solidity:因 Vyper 当时不支持「非标准 ERC20 返回值解析」「内联汇编调用 chainid」—— 前端调用代币合约时,需处理非标代币的兼容问题(比如有些代币 transfer 不返回布尔值)
二、AMM 底层逻辑
1. 恒定乘积公式(x*y=k)
- 核心规则:资金池内两种代币余额乘积恒定(x=代币A余额,y=代币B余额,k=常数)
- 前端视角:用户兑换时,合约根据 x*y=k 计算兑换数量,前端需实时展示「预估到账金额」「滑点」
- 举例:用户用1 ETH换USDT,合约减少ETH余额、增加USDT余额,确保交易后 x*y 仍等于k
2. 套利定价机制(价格稳定的核心)
- 逻辑:当 Uniswap 价格与市场价格偏离(偏差>手续费),套利者低买高卖,将价格拉回真实水平
- 前端价值:Uniswap 价格可作为「链上价格源」,前端可基于此实现价格监控、套利工具等功能
- 关键概念:套利者(arbitrageurs)、手续费(0.3%)、滑点(价格波动导致实际到账与预估差异)
三、V1 预言机漏洞
1. 漏洞本质(面试高频)
- V1 直接用「实时交易价格」作为预言机数据,无抗操纵能力
- 前端风险:若前端集成 V1 价格数据做业务逻辑(如衍生品结算、借贷抵押评估),会导致资金风险
2. 攻击流程(必须背会)
- 攻击者在 V1 资金池买入大量代币 → 人为抬高价格(x*y=k,买入方代币减少,价格上涨)
- 触发依赖该价格的合约结算(如衍生品合约)→ 按虚假高价获利
- 攻击者立即卖回代币 → 价格回归,全程通过「原子交易」完成(要么全成,要么全败)
- 前端视角:需能识别「不安全的价格源」,避免在产品中集成 V1 预言机
四、V2 预言机改进:TWAP 时间加权平均价(重中之重)
1. 核心目标:解决 V1 操纵问题,提供安全价格源
- 前端应用场景:DeFi 产品定价(如借贷利率、衍生品结算)、前端价格展示(避免实时价格波动过大)
2. TWAP 原理
(1)核心工具:价格累加器(accumulator)
- 合约逻辑:记录「每个区块起始价格 × 价格持续时间」,并累计求和
- 前端关键认知:合约不存储历史累加值,需前端在「时间段起始时刻」主动调用合约,读取并存储累加值(aₜ₁)
(2)TWAP 计算步骤
- 初始化:在 t₁ 时刻调用合约
getReserves() 或 priceCumulativeLast(),存储累加值 aₜ₁ 和当前时间戳 t₁
- 结束:在 t₂ 时刻再次调用合约,获取累加值 aₜ₂ 和时间戳 t₂
- 计算平均价:
P_TWAP = (aₜ₂ - aₜ₁) / (t₂ - t₁)
- 前端注意:需处理「定点数转换」(后续详细讲),且时间差单位为秒
(3)代码示例
// 假设使用 Ethers.js 交互
const getTWAP = async (pairContract, t1, a1) => {
// t1:起始时间戳,a1:起始累加值(前端已存储)
const [a2, t2] = await Promise.all([
pairContract.priceCumulativeLast(), // 获取当前累加值
Math.floor(Date.now() / 1000) // 当前时间戳(秒)
]);
// 计算 TWAP(注意:需处理 BigInt 避免溢出)
const twap = (a2 - a1) / BigInt(t2 - t1);
// 转换为可读价格(UQ112.112 转小数)
const readableTWAP = twap / BigInt(2 ** 112);
return readableTWAP;
};
3. 计价单位坑
- 核心问题:A/B 现货价格 = 1 / B/A 现货价格,但「算术平均价不互为倒数」
- 举例:ETH/USDT 价格 1000 和 3000,平均价 2000;USDT/ETH 平均价 ≈ 0.000666,≠ 1/2000
- 前端解决方案:V2 同时存储 A/B 和 B/A 两个累加器,前端需根据用户选择的「计价单位」调用对应数据
4. 恶意转账防护
- 攻击原理:用户直接向资金池合约转代币,改变余额但不触发合约交互,导致价格异常
- V2 解决方案:合约缓存储备金(每次交互后更新),用缓存数据计算价格,而非实时余额
- 前端视角:无需额外开发,但需知道「读取储备金应调用
getReserves() 而非直接查合约余额」
五、定点数存储:UQ112.112
1. 为什么需要定点数?
- Solidity 无原生小数类型,用「二进制定点数」编码价格,避免精度丢失
- 前端核心工作:从合约读取定点数后,转换为用户可读的小数价格
2. UQ112.112 详解
- 格式含义:U(无符号)、Q(定点数)、112.112(整数112位 + 小数112位)
- 存储:占用 uint224(112+112=224位),剩余32位用于存储时间戳、溢出位
- 转换逻辑:
<!---->
-
- 合约返回的价格是「放大 2¹¹² 倍的整数」
- 转换公式:
可读价格 = 合约返回值 / 2¹¹²
- 代码示例(Ethers.js):
// 假设从合约读取到 UQ112.112 格式的价格(BigInt)
const encodedPrice = await pairContract.price0CumulativeLast();
// 转换为可读价格(ETH/USDT 为例)
const decimalPrice = encodedPrice / BigInt(1n << 112n);
console.log("可读价格:", decimalPrice.toString());
六、闪电兑换(Flash Swaps)
1. 核心价值(解决 V1 痛点)
- V1 限制:必须先转付款代币,才能拿到目标代币,无法满足「先拿币再交易」场景
- 闪电兑换:允许用户「先接收代币→执行操作→同一原子交易内还款」
- 应用场景:套利工具、清算机器人、无本金链上操作(如用闪电兑换的代币做抵押)
2. 执行流程
- 调用 V2 合约
swap() 函数,指定:
-
- 要借的代币数量、目标代币
- 回调合约地址(用户自定义,包含还款逻辑)
- 合约逻辑:
-
- 先转出目标代币给用户
- 调用回调合约,用户在回调中执行操作(如套利、平仓)
- 回调结束后,检查资金池余额是否满足 x*y≥k(确保还款)
- 关键注意点:
-
- 回调合约必须实现
uniswapV2Call() 接口(否则调用失败)
- 需预估足够 Gas(闪电兑换涉及多步操作,Gas 不足会回滚)
- 原子交易特性:要么全成,要么全败,无中间状态风险
3. 调用示例(简化版)
const flashSwap = async (pairContract, borrowToken, borrowAmount, callbackContract) => {
// 构造交易参数
const params = [
borrowToken.address, // 要借的代币地址
ethers.utils.parseUnits(borrowAmount, 18), // 借款数量(需处理小数)
"0x", // 额外数据(可选)
callbackContract.address // 回调合约地址
];
// 发送交易(需用户签名)
const tx = await pairContract.swap(...params, {
gasLimit: 300000 // 预估足够 Gas
});
await tx.wait();
console.log("闪电兑换成功");
};
七、Uniswap V2 交互核心技能
1. 合约交互基础
- 必备工具:Ethers.js/Web3.js(前端链上交互库)、MetaMask(钱包连接)
- 核心步骤:
-
- 连接钱包(获取用户地址、链ID)
- 加载合约ABI(从Etherscan下载或项目源码获取)
- 读取数据(价格、累加值、储备金,call 方法,无需Gas)
- 发送交易(兑换、闪电兑换,send 方法,需用户签名+Gas)
2. 关键合约方法
|
|
|
| 方法用途 |
合约方法 |
前端调用场景 |
| 查询储备金 |
getReserves() |
计算实时价格、展示资金池规模 |
| 查询价格累加值 |
price0CumulativeLast()/price1CumulativeLast() |
计算 TWAP 平均价 |
| 代币兑换 |
swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) |
普通兑换、闪电兑换 |
| 添加流动性 |
addLiquidity(address tokenA, address tokenB, uint amountADesired, uint amountBDesired, uint amountAMin, uint amountBMin, address to, uint deadline) |
前端添加流动性功能 |
3. 常见问题与解决方案
- 问题1:价格转换精度错误 → 解决方案:严格按 UQ112.112 格式转换,使用 BigInt 避免溢出
- 问题2:交易回滚 → 解决方案:检查 Gas 充足性、代币授权(approve)、参数格式(如金额单位为 wei)
- 问题3:非标 ERC20 兼容 → 解决方案:调用代币合约时,处理「transfer 不返回布尔值」的情况(用 staticCall 预校验)
八、面试高频考点速记
- V1 预言机漏洞原理及攻击流程(必背)
- V2 TWAP 计算逻辑及前端实现步骤
- 闪电兑换的核心特性(原子交易、回调合约、还款约束)
- UQ112.112 格式的含义及前端转换方法
- V2 为什么同时存储 A/B 和 B/A 两个价格累加器
- 恶意转账攻击的防护逻辑(缓存储备金)
九、学习资源推荐
- 合约交互实践:Ethers.js 官方文档 + Uniswap V2 合约ABI(Etherscan下载)
- 可视化工具:Uniswap Info(查看链上数据,对照前端交互逻辑)
- 实战项目:仿写简易 Uniswap 前端(实现兑换、价格查询功能)
- 避坑指南:Uniswap 官方文档「Frontend Integration」章节