严重预言机与价格静态扫描查不出
闪电贷操纵价格
Flash Loan Price Manipulation
攻击者借来巨额资金,在一个交易内把某交易对的现货价格推歪,让依赖这个价格的协议做出错误判断(多放贷、错误清算、错误铸币),然后归还贷款。
根因
协议把可以被单笔交易改变的值当成了可信价格。AMM 的现货价就等于「储备量之比」,而储备量是可以被一笔大额 swap 直接推着走的。这个值在攻击结束后会回到原处,所以攻击者甚至不需要承担价格风险。
攻击是怎么一步步发生的
- 1闪电贷协议
借入巨额 TOKEN_A(无抵押,只要在同一交易内归还)
- 2AMM 池
把 TOKEN_A 全部砸进池子 → TOKEN_A 价格暴跌、TOKEN_B 相对暴涨
- 3被攻击协议
用被推歪的现货价计算:抵押品「价值」被高估(或债务被低估)
- 4被攻击协议
借出远超真实抵押能力的资金
- 5AMM 池
反向 swap 把价格还原,拿回绝大部分 TOKEN_A
- 6闪电贷协议
归还贷款 + 利息,净赚差额
有漏洞的版本
不要照抄soliditySolidity17 行
contract SpotPriceLending {
IUniswapV2Pair public pair;
mapping(address => uint256) public collateralOf;
// 直接用资金池的当前储备量当价格
function collateralValue(address user) public view returns (uint256) {
(uint112 r0, uint112 r1, ) = pair.getReserves();
// 现货比:攻击者只要在同一个交易里把 r0 推大,这个价格立刻变小
uint256 priceToken1PerToken0 = (uint256(r1) * 1e18) / uint256(r0);
return (collateralOf[user] * priceToken1PerToken0) / 1e18;
}
function borrow(uint256 amount) external {
require(amount <= collateralValue(msg.sender) / 2, "insufficient collateral");
// ...放款
}
}getReserves()返回的是此刻的储备量,没有时间维度,无法抵抗单笔交易的影响- 整个操纵在同一笔交易内完成,攻击者用完就还,资金成本接近于零(只有 gas 和手续费)
- 静态扫描完全看不出来:这段代码语法、逻辑、权限都没毛病
修复版本
推荐写法soliditySolidity20 行
contract SafePriceLending {
AggregatorV3Interface public immutable feed; // Chainlink 喂价
IUniswapV3Pool public immutable pool; // 兜底:TWAP
function price() public view returns (uint256) {
(, int256 answer, , uint256 updatedAt, ) = feed.latestRoundData();
require(answer > 0, "invalid price");
require(block.timestamp - updatedAt <= 1 hours, "stale price"); // 喂价不能太旧
return uint256(answer);
}
// 关键补丁:给「单笔能借走多少」设上限,
// 让操纵价格的成本高于能借走的收益
uint256 public constant MAX_BORROW_PER_TX = 100_000e18;
function borrow(uint256 amount) external {
require(amount <= MAX_BORROW_PER_TX, "exceeds per-tx cap");
// ...后续按 price() 计算抵押率
}
}- 换用外部喂价(Chainlink 等),它的值不由本协议所在的池子决定,单笔 swap 推不动
- 检查
updatedAt时效,避免用了几天前的旧价格 - 设单笔上限是最容易被忽略但最有效的一招:即便预言机被绕过,损失也被封顶
- 还可以叠加 TWAP(时间加权均价)+ 与现货价的偏离度校验,偏离过大就暂停
怎么防
- 不要用 AMM 现货价当预言机,改用 Chainlink / Pyth 等外部喂价
- 确实要用链上价格就用 TWAP(≥30 分钟窗口),拉长操纵成本
- 多源交叉验证:两个独立来源偏离超过阈值就暂停
- 给单笔操作设上限,把最坏损失封顶
- 区分「价格」与「可清算性」:清算阈值留足缓冲,不要卡在边界
真实案例
2020bZx约 95 万美元(两起)
闪电贷操纵的早期经典案例,开创了这一攻击范式
2020Harvest Finance约 3380 万美元
用闪电贷推歪 Curve 池价格,套利协议自身的策略
2021PancakeBunny约 4500 万美元
BNB/BUNNY 池被操纵,BUNNY 被超量铸出后砸盘
2022Mango Markets约 1.16 亿美元
自买自卖推高 MNGO 价格 → 抵押品虚高 → 借空金库
审计这个合约时要问的问题
- 1.合约里的每一个「价格」从哪来?它能不能被同一笔交易改变?
- 2.这个价格有多个来源吗?来源之间会互相依赖吗?
- 3.单笔交易的收益上限是多少?攻击成本是否高于收益?
- 4.清算逻辑有没有依赖「现货余额 / 储备量」这类瞬时值?
这些问题也是审计检查清单里对应的条目——那里可以把每一条的检查结果记下来,最后导出成报告。
静态扫描能发现它吗
不能。源码扫描引擎里没有能覆盖这一条的规则。 原因不是规则写得不够多,而是这类漏洞的代码语法完全正常: 权限检查写了、外部调用顺序对了、整数运算也没问题, 危险来自运行时的状态组合。
这正是「扫一遍没发现问题」不能当作安全结论的原因。 本站的合约体检会把这一类明确标成「工具完全查不出来」的检查项,而不是给你一个绿灯。