第七部分:保护以太坊智能合约免受价格操纵攻击

本文深入探讨了DeFi中价格操纵攻击的原理、危害和防范措施。文章详细解释了攻击者如何利用低流动性DEX池、单一预言机和闪电贷来操纵价格,从而导致不公平的清算、资金盗取和信任危机。此外,文章还提供了防范价格操纵攻击的安全代码示例、防御策略、最佳实践和测试工具。

介绍:你需要了解的 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
    }
}

为什么容易受到攻击?

  • 依赖于一个现货价格 - 容易用闪电贷扭曲。
  • 没有针对陈旧或偏差的检查。
  • 清算立即发生。

攻击情景(分步)

  1. 用户存入 10 ETH(按 100 USDC/ETH 计价为 1,000 美元),借入 750 USDC。
  2. 攻击者闪电借入 10,000 USDC。
  3. 在低流动性池中兑换为 ETH - 将价格降至 50 USDC/ETH。
  4. 调用清算:抵押品“价值”为 500 美元 < 1,125 美元的阈值。
  5. 获取 10 ETH,换回,偿还贷款,获利 500 美元。
  6. 全部在 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 &lt;= 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 &lt; (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 &lt;= 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 &lt;= 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) &lt;= MAX_DEVIATION;
    }

    function pause() external onlyAdmin { paused = true; }
    receive() external payable { revert("No direct ETH"); }
}

它如何保护:

  • 多重预言机:平均两个信息源 - 没有单点故障。
  • TWAP:平均超过 1 小时 - 忽略短时峰值。
  • 理智检查:最小/最大范围 + 偏差限制。
  • 提交-揭示:清算延迟 1 小时。
  • 电路断路器:管理员暂停。
  • 可重入保护:安全以太币处理。
  • 状态上限:将历史记录限制为 100 - 避免膨胀。

安全工作流程图

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

工作流程比较表

防御策略:易于实施的方法

使用这些来构建强大的保护。每个都带有解释、优缺点和代码。

  1. 去中心化预言机 (DON)
  • 内容:从许多来源聚合(例如,Chainlink,Band)。
  • 原因:减少单点风险。
  • 优点:可靠,基于激励。
  • 缺点:Gas 成本更高。
  • 代码示例:
function getPrice() view returns (uint256) {
    (, int256 p1,,,) = primaryFeed.latestRoundData();
    (, int256 p2,,,) = secondaryFeed.latestRoundData();
    return (uint256(p1) + uint256(p2)) / 2; // Average
}
  1. TWAP(时间加权平均价格)
  • 内容:随时间推移的平均价格(例如,1 小时)。
  • 原因:平滑闪电贷峰值。
  • 优点:抗闪电贷。
  • 缺点:数据略微过时。
  • 代码示例:
function getTWAP() view returns (uint256) {
    uint256 total = 0; uint256 count = 0;
    uint256 start = block.timestamp - TWAP_WINDOW;
    for (uint t = start; t &lt;= block.timestamp; t++) {
        if (priceHistory[t] > 0) { total += priceHistory[t]; count++; }
    }
    return count > 0 ? total / count : 0;
}
  1. 理智检查与验证
  • 内容:检查范围、偏差、陈旧性。
  • 原因:拒绝错误数据。
  • 优点:简单,有效。
  • 缺点:需要调整。
  • 代码示例:
function validate(uint256 newPrice) view returns (bool) {
    uint256 last = priceHistory[lastUpdate];
    uint256 dev = newPrice > last ? newPrice - last : last - newPrice;
    return (dev * 100 / last) &lt;= MAX_DEVIATION && newPrice >= MIN_PRICE && newPrice &lt;= MAX_PRICE;
}
  1. 电路断路器与紧急情况
  • 内容:出现问题时暂停功能。
  • 原因:快速阻止攻击。
  • 优点:快速响应。
  • 缺点:中央管理员风险。
  • 代码示例:
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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
ankitacode11
ankitacode11
江湖只有他的大名,没有他的介绍。