介绍:你需要了解的 DeFi 隐藏威胁
在 DeFi 中,价格操纵攻击利用了智能合约如何从外部来源获取价格数据。这些攻击可能导致不公平的清算、窃取资金并损害信任。这是智能合约安全:Solodit 检查清单系列的第 7 章,重点关注 SOL-AM-PriceManipulation(预言机或市场操纵)。
为什么要关心?截至 2025 年 8 月 13 日,Ethereum 的权益证明具有约 4500 万 gas 的区块限制和约 12 秒的区块时间。此设置使闪电贷成为攻击者的强大工具,攻击贷款应用程序、DEX、稳定币和预测市场。
我们将简单地分解它:什么是攻击,为什么它很糟糕,它是如何运作的,真实示例,易受攻击与安全的代码,防御措施,最佳实践,测试等等。使用项目符号、表格、代码片段和图表以便于阅读。
为什么价格操纵会伤害 DeFi
智能合约需要准确的价格才能:
- 借贷:检查抵押品价值和清算点。
- 交易/兑换:在 DEX 上计算公平交易。
- 稳定币:通过调整保持Hook稳定。
- 预测市场:根据价格结算赌注。
攻击者瞄准薄弱环节:
- 低流动性 DEX 池:小额交易会大幅改变价格(例如,在 Uniswap V2 中通过 x*y=k 公式)。
- 单一来源预言机:单一信息源=容易被黑客攻击、容易出错或容易中断。
- 闪电贷:借入巨额资金(来自 Aave/dYdX),操纵,利用,在 1 笔交易中偿还。
大风险:
- 不公平的清算:健康的用戶失去资产(例如,MakerDAO 黑色星期四造成 800 万美元的损失)。
- 资金耗尽:攻击者获取定价错误的资产(例如,Harvest Finance 损失 3380 万美元)。
- 失去信任:用户离开,采用率下降。
- 连锁反应:一次失败导致相互关联的 DeFi 崩溃(例如,2022 年 UST-LUNA 崩盘)。
价格操纵如何运作
攻击者会干扰价格数据源。主要方式:
- 低流动性池:在小型池(1 万美元储备)中进行大额交易以改变价格。
- 单一预言机:伪造数据,延迟更新或攻击来源。
- 闪电贷:立即借款,扭曲价格,触发错误的逻辑(例如,清算),解除。
- 链上数据:在一个区块中扭曲 DEX 储备。
常用策略:
- 闪电贷扭曲:借入,更改池价格,利用(清算/兑换),撤销。
- 拉高/砸盘:大量买入/卖出以推高价格,然后获利(例如,购买廉价抵押品)。
- 预言机篡改:发送错误数据或使用过时信息。
- 内存池技巧:与抢跑交易(第 4 部分)或恶意行为(第 5 部分)结合使用以阻止良好的交易。
Ethereum 的开放内存池和快速区块使攻击者可以在约 12 秒内完成此操作。
图表:基本攻击流程

真实世界的例子
备受瞩目的攻击表明了危险。这是一个比较表:

易受攻击的代码:易于攻击的目标
此借贷合约使用单个 DEX 池 - 非常适合操纵。
代码片段
// SPDX-License-Identifier: MIT pragma solidity ^0.8.10; contract VulnerableLending { address public priceFeed; // Single DEX pool mapping(address => uint256) public collateral; // ETH staked mapping(address => uint256) public debt; // USDC borrowed uint256 public constant COLLATERAL_RATIO = 150; // 150% min constructor(address _priceFeed) { priceFeed = _priceFeed; } function depositCollateral() external payable { collateral[msg.sender] += msg.value; // Add ETH } function borrow(uint256 amount) external { uint256 price = getPriceFromFeed(); // Vulnerable spot price uint256 collateralValue = (collateral[msg.sender] * price) / 1e18; require(collateralValue >= (amount * COLLATERAL_RATIO) / 100, "Low collateral"); debt[msg.sender] += amount; payable(msg.sender).transfer(amount); // Send USDC (simulated) } function liquidate(address user) external { uint256 price = getPriceFromFeed(); // Manipulable uint256 collateralValue = (collateral[user] * price) / 1e18; require(collateralValue < (debt[user] * COLLATERAL_RATIO) / 100, "Not liquidatable"); uint256 seized = collateral[user]; collateral[user] = 0; debt[user] = 0; payable(msg.sender).transfer(seized); // Attacker gets ETH } function getPriceFromFeed() internal view returns (uint256) { // Mock single DEX query return 100 * 1e18; // Fixed for demo; real would query pool } }为什么容易受到攻击?
- 依赖于一个现货价格 - 容易用闪电贷扭曲。
- 没有针对陈旧或偏差的检查。
- 清算立即发生。
攻击情景(分步)
- 用户存入 10 ETH(按 100 USDC/ETH 计价为 1,000 美元),借入 750 USDC。
- 攻击者闪电借入 10,000 USDC。
- 在低流动性池中兑换为 ETH - 将价格降至 50 USDC/ETH。
- 调用清算:抵押品“价值”为 500 美元 < 1,125 美元的阈值。
- 获取 10 ETH,换回,偿还贷款,获利 500 美元。
- 全部在 1 笔交易中 - 没有风险。
结果:用户受到不公平的损失;协议失去信任。
易受攻击的工作流程图

安全代码:分层防御
使用聚合、TWAP、检查、延迟和熔断器修复它。
代码片段
// SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; contract SecureLending is ReentrancyGuard { AggregatorV3Interface public primaryFeed; // Chainlink AggregatorV3Interface public secondaryFeed; // Band Protocol address public admin; bool public paused; mapping(address => uint256) public collateral; mapping(address => uint256) public debt; uint256 public constant COLLATERAL_RATIO = 150; uint256 public constant TWAP_WINDOW = 1 hours; uint256 public constant MAX_DEVIATION = 10; // % uint256 public constant MIN_PRICE = 1e16; // 0.01 USDC/ETH uint256 public constant MAX_PRICE = 1e22; // 10,000 USDC/ETH uint256 public constant MAX_HISTORY = 100; mapping(uint256 => uint256) public priceHistory; uint256 public lastPriceUpdate; mapping(address => uint256) public liquidationRequests; modifier onlyAdmin() { require(msg.sender == admin, "Not admin"); _; } modifier whenNotPaused() { require(!paused, "Paused"); _; } constructor(address _primary, address _secondary) { primaryFeed = AggregatorV3Interface(_primary); secondaryFeed = AggregatorV3Interface(_secondary); admin = msg.sender; lastPriceUpdate = block.timestamp; } function depositCollateral() external payable whenNotPaused nonReentrant { collateral[msg.sender] += msg.value; } function borrow(uint256 amount) external whenNotPaused nonReentrant { updateTWAP(); uint256 price = getTWAP(); require(price >= MIN_PRICE && price <= MAX_PRICE, "Invalid price"); uint256 collateralValue = (collateral[msg.sender] * price) / 1e18; require(collateralValue >= (amount * COLLATERAL_RATIO) / 100, "Low collateral"); debt[msg.sender] += amount; payable(msg.sender).transfer(amount); } function requestLiquidation(address user) external whenNotPaused { liquidationRequests[user] = block.timestamp; // Commit } function executeLiquidation(address user) external whenNotPaused nonReentrant { require(block.timestamp >= liquidationRequests[user] + 1 hours, "Too soon"); // Reveal delay updateTWAP(); uint256 price = getTWAP(); uint256 collateralValue = (collateral[user] * price) / 1e18; require(collateralValue < (debt[user] * COLLATERAL_RATIO) / 100, "Not liquidatable"); uint256 seized = collateral[user]; collateral[user] = 0; debt[user] = 0; payable(msg.sender).transfer(seized); } function updateTWAP() internal { (, int256 p1,, uint256 t1,) = primaryFeed.latestRoundData(); (, int256 p2,, uint256 t2,) = secondaryFeed.latestRoundData(); require(p1 > 0 && p2 > 0, "Invalid"); require(t1 >= lastPriceUpdate && t2 >= lastPriceUpdate, "Stale"); uint256 avg = (uint256(p1) + uint256(p2)) / 2; require(avg >= MIN_PRICE && avg <= MAX_PRICE, "Bounds error"); require(validatePrice(avg), "Deviation high"); priceHistory[block.timestamp] = avg; if (block.timestamp > lastPriceUpdate + TWAP_WINDOW / MAX_HISTORY) delete priceHistory[lastPriceUpdate - TWAP_WINDOW]; lastPriceUpdate = block.timestamp; } function getTWAP() internal view returns (uint256) { uint256 start = block.timestamp > TWAP_WINDOW ? block.timestamp - TWAP_WINDOW : 0; uint256 total = 0; uint256 count = 0; for (uint t = start; t <= block.timestamp; t++) { if (priceHistory[t] > 0) { total += priceHistory[t]; count++; } } require(count > 0, "No history"); return total / count; } function validatePrice(uint256 newPrice) internal view returns (bool) { uint256 last = priceHistory[lastPriceUpdate]; if (last == 0) return true; uint256 dev = newPrice > last ? newPrice - last : last - newPrice; return (dev * 100 / last) <= MAX_DEVIATION; } function pause() external onlyAdmin { paused = true; } receive() external payable { revert("No direct ETH"); } }它如何保护:
- 多重预言机:平均两个信息源 - 没有单点故障。
- TWAP:平均超过 1 小时 - 忽略短时峰值。
- 理智检查:最小/最大范围 + 偏差限制。
- 提交-揭示:清算延迟 1 小时。
- 电路断路器:管理员暂停。
- 可重入保护:安全以太币处理。
- 状态上限:将历史记录限制为 100 - 避免膨胀。
安全工作流程图

价格图:攻击期间的现货价格与 TWAP

工作流程比较表

防御策略:易于实施的方法
使用这些来构建强大的保护。每个都带有解释、优缺点和代码。
- 去中心化预言机 (DON)
- 内容:从许多来源聚合(例如,Chainlink,Band)。
- 原因:减少单点风险。
- 优点:可靠,基于激励。
- 缺点:Gas 成本更高。
- 代码示例:
function getPrice() view returns (uint256) { (, int256 p1,,,) = primaryFeed.latestRoundData(); (, int256 p2,,,) = secondaryFeed.latestRoundData(); return (uint256(p1) + uint256(p2)) / 2; // Average }- TWAP(时间加权平均价格)
- 内容:随时间推移的平均价格(例如,1 小时)。
- 原因:平滑闪电贷峰值。
- 优点:抗闪电贷。
- 缺点:数据略微过时。
- 代码示例:
function getTWAP() view returns (uint256) { uint256 total = 0; uint256 count = 0; uint256 start = block.timestamp - TWAP_WINDOW; for (uint t = start; t <= block.timestamp; t++) { if (priceHistory[t] > 0) { total += priceHistory[t]; count++; } } return count > 0 ? total / count : 0; }- 理智检查与验证
- 内容:检查范围、偏差、陈旧性。
- 原因:拒绝错误数据。
- 优点:简单,有效。
- 缺点:需要调整。
- 代码示例:
function validate(uint256 newPrice) view returns (bool) { uint256 last = priceHistory[lastUpdate]; uint256 dev = newPrice > last ? newPrice - last : last - newPrice; return (dev * 100 / last) <= MAX_DEVIATION && newPrice >= MIN_PRICE && newPrice <= MAX_PRICE; }- 电路断路器与紧急情况
- 内容:出现问题时暂停功能。
- 原因:快速阻止攻击。
- 优点:快速响应。
- 缺点:中央管理员风险。
- 代码示例:
function pause() external onlyAdmin { paused = true; } modifier notPaused() { require(!paused, "Paused"); _; }最佳实践清单
遵循此清单以获得可靠的防御:
- 使用聚合信息源:Chainlink + 备份,验证响应。
- 实施 TWAP:对于关键计算,如果可能,添加成交量权重。
- 强制检查:范围、偏差、陈旧性。
- 提交-揭示:延迟清算等操作。
- 电路断路器:在异常情况下暂停。
- 阻止可重入性:使用保护措施进行传输。
- 限制状态:限制历史记录以避免 gas 膨胀。
- 监控:Forta 用于实时警报。
用于弹性的测试与工具
像这样进行测试:
- 单元:模拟扭曲,预期恢复。
it("rejects bad prices", async () => { await attacker.skewPrice(); // Mock flash await expect(contract.borrow(100)).to.be.revertedWith("Invalid price"); });- Fuzz:Echidna 上的随机价格。
- 闪电模拟:用于攻击的 Foundry。
- Fork:Hardhat mainnet 用于真实预言机。
- 监控:Forta 异常。
工具(2025 年更新):
- Slither:检测预言机问题。
- MythX:扫描漏洞。
- Foundry:模拟攻击。
- Forta:实时监控。
- OZ Defender:自动暂停。
- Tenderly:可视化模拟。
- Chainlink 注册表:测试信息源。
结论:构建更安全的 DeFi
价格操纵是隐蔽的,但可以击败。使用多重预言机、TWAP、检查、延迟和熔断器进行保护。这会创建公平、可信的合约。接下来:使用 UUPS 代理的可升级性风险。掌握此内容以确保 DeFi 安全!
- 原文链接: medium.com/@ankitacode11...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~