简介

整数溢出(Integer Overflow)

简要概述

在Solidity中,无符号整数(如uint256)在加/减/乘除运算时,若超出类型范围(例如uint256最大值为2^256-1),会发生“回绕”(overflow/underflow),导致意外结果,如余额错误计算、资金无限铸币或丢失。在Solidity <0.8.0版本中无内置检查,易被利用

典型示例

// 错误示范(无溢出检查)
pragma solidity ^0.7.0;

contract VulnerableToken {
    mapping(address => uint256) public balances;

    function transfer(address to, uint256 amount) external {
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount; // <-- 可能下溢(underflow)
        balances[to] += amount; // <-- 可能上溢(overflow)
    }
}

攻击者可利用下溢:若余额为0,减去1会回绕成最大值,导致盗取巨额资金

改进方法

升级到Solidity ^0.8.0+(内置溢出检查,会revert),或使用SafeMath库进行安全运算

示例:

// 使用内置检查(Solidity ^0.8.0)
pragma solidity ^0.8.0;

contract SecureToken {
    mapping(address => uint256) public balances;

    function transfer(address to, uint256 amount) external {
        require(balances[msg.sender] >= amount, "insufficient");
        balances[msg.sender] -= amount; // 内置检查:下溢会revert
        balances[to] += amount; // 内置检查:上溢会revert
    }
}

额外建议:对于旧版本,使用OpenZeppelin的SafeMath库(如balances[msg.sender] = balances[msg.sender].sub(amount));始终审计数学运算,并使用有界类型(如uint128)减少风险


不安全类型转换(Unsafe Casting)

简要概述

Solidity中进行类型转换时(如从较大类型uint256到较小类型uint8),若值超出目标类型范围,会发生隐式截断(truncation)或溢出,导致数据丢失、意外行为或安全漏洞,如余额计算错误或权限绕过。Solidity不默认检查转换安全,易被利用。

典型示例

// 错误示范(无安全检查)
pragma solidity ^0.8.0;

contract VulnerableVault {
    mapping(address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint8 amount) external { // uint8 仅0-255
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount;
        payable(msg.sender).transfer(amount); // <-- 若amount实际>255,转换截断导致少转账
    }
}

攻击者传入>255的值,转换截断成小值,导致实际提取远超预期(但余额扣除完整值)

改进方法

使用显式检查范围或OpenZeppelin的SafeCast库,确保转换安全;优先使用足够大的类型避免转换。

示例:

// 使用SafeCast
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/math/SafeCast.sol";

contract SecureVault {
    using SafeCast for uint256;
    mapping(address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) external {
        require(balances[msg.sender] >= amount, "insufficient");
        balances[msg.sender] -= amount;
        uint8 safeAmount = amount.toUint8(); // <-- 若溢出,会revert
        payable(msg.sender).transfer(safeAmount);
    }
}

额外建议:避免不必要转换;使用require手动检查(如require(amount <= type(uint8).max));审计所有类型转换点,优先用相同大小类型


重入攻击(Re-entrancy)

简要描述

合约在进行外部调用(例如 call/transfer/send)后,在更新自身状态之前被攻击者合约再次调用该合约的敏感函数,导致状态以为只执行一次但实际上被重复利用,从而盗取资金或破坏逻辑

典型模式

// 错误示范(先外部调用,后更新状态)
function withdraw(uint amount) external {
    require(balances[msg.sender] >= amount);
    (bool ok, ) = msg.sender.call{value: amount}("");
    require(ok);
    balances[msg.sender] -= amount; // <-- 状态更新在外部调用之后(易被重入)
}

msg.sender 是一个恶意合约时,它可以在 call 执行时的 receive() / fallback() 中再次调用 withdraw,因为 balances[msg.sender] 还没被更新,从而重复提取 ( 最后一次提取不能大于被攻击合约余额, 否则导致 revert, 攻击被回滚 )


改进方法

编码遵循 CEI 模式:

  1. Checks(检查):验证条件、权限、余额等

  2. Effects(状态更新):修改合约状态

  3. Interactions(外部交互):最后才调用外部合约或发送 ETH

示例:

function withdraw(uint256 amount) external {
    // Checks(检查)
    require(balances[msg.sender] >= amount, "insufficient");

    // Effects(先改状态)
    balances[msg.sender] -= amount;

    // Interactions(再外部交互)
    (bool ok, ) = msg.sender.call{value: amount}("");
    require(ok, "send failed");
}

弱随机性(Weak Randomness)

简要描述

在智能合约中,开发者常会使用一些“看起来随机”的链上变量作为随机数,例如:

  • block.timestamp
  • block.number
  • block.coinbase
  • blockhash
  • msg.sender
  • tx.origin
  • block.prevrandao(实际上较难预测,但在部分情况下仍能被矿工影响)

这些值都不能提供真正的不可预测随机性

  • 攻击者 可以预测这些值,从而提前知道合约“随机结果”
  • 矿工/验证者 能影响部分变量,例如:选择时间戳、选择出块顺序、决定 blockhash 是否上链

因此攻击者常利用弱随机性提前预测某些结果,从而操纵开奖、战斗判定、NFT 稀有度、博彩结果等。

典型示例

// 错误示范(弱随机)
// block.timestamp 与 block.number 都可预测
function getRandomNumber() internal view returns (uint256) {
    return uint256(keccak256(abi.encodePacked(
        block.timestamp,
        msg.sender,
        block.number
    )));
}

攻击者可以:

  • 不断尝试调用直到随机结果符合预期
  • 在同一区块内执行“先预测再决定是否提交”
  • 利用矿工控制部分区块字段影响结果

改进方法

避免使用任何链上可预测变量作为随机源,应采用更安全的随机性方案:

使用预言机(例如 Chainlink VRF)

Chainlink VRF 提供可验证、无法操控的随机数,业界最常用。

// 安全:使用 Chainlink VRF 作为随机数
uint256 public randomResult;

function fulfillRandomWords(uint256, uint256[] memory randomWords) internal override {
    randomResult = randomWords[0];
}

优点:不可预测、不可操控 缺点:需要预言机费用,非即时(异步)

Commit-Reveal(承诺-揭示)模式

让用户先提交一个哈希(commit),之后再公布原值(reveal),使得结果不能提前被操控。

// 安全示例:commit-reveal
mapping(address => bytes32) public commitments;

function commitHash(bytes32 hash) external {
    commitments[msg.sender] = hash;
}

function reveal(uint256 secret) external {
    require(keccak256(abi.encodePacked(secret)) == commitments[msg.sender]);
}

使用更安全的随机性聚合(VRF + 用户参与

在高安全场景中,可以组合:

  • VRF
  • 用户提交的 entropy
  • 多签随机性

从而获得更强弹性随机性。


预言机操纵(Oracle Manipulation)

简要概述

Oracle Manipulation攻击是指攻击者通过操纵资产价格预言机(Oracle)的报价,使其偏离真实市场价格,从而获利的安全漏洞

在ThunderLoan中的具体表现

  1. 价格来源:ThunderLoan使用getPriceInWeth(address token)获取代币价格,基于tswapAddress(假设是一个DEX)
  2. 攻击路径
    function calculateFee(IERC20 token, uint256 amount) public view returns (uint256) {
        uint256 tokenValue = (amount * getPriceInWeth(address(token))) / FEE_PRECISION;
        return (tokenValue * s_flashLoanFee) / FEE_PRECISION;
    }
    
    • 闪电贷费用基于代币价格计算
    • 如果攻击者能操纵价格,就能减少闪电贷费用

攻击方法

闪电贷 + DEX 价格操纵

// 攻击步骤:
1. 发起闪电贷借入大量代币A
2. 在目标DEXtswapAddress)大量卖出代币A,压低其价格
3. ThunderLoan基于被压低的价格计算低费用
4. 归还闪电贷(支付较少费用)
5. DEX买回代币A,恢复价格

三明治攻击预言机

// 攻击步骤:
1. 监控待处理的ThunderLoan交易
2. 在交易前向预言机使用的DEX注入大量流动性
3. 执行目标交易(基于被操纵的价格)
4. 移除流动性,获利退出

改进方法

使用多个价格源(推荐)

// 多预言机聚合
contract MultiSourceOracle {
    address[] public oracles;
    uint256 public constant MIN_SOURCES = 3;
    
    function getPrice(address token) external view returns (uint256) {
        uint256[] memory prices = new uint256[](oracles.length);
        
        for (uint i = 0; i < oracles.length; i++) {
            prices[i] = IOracle(oracles[i]).getPrice(token);
        }
        
        // 排序并取中位数(排除极端值)
        return _getMedian(prices);
    }
    
    function _getMedian(uint256[] memory prices) internal pure returns (uint256) {
        // 实现中位数计算
        // 1. 排序数组
        // 2. 取中间值
        // 3. 如果偶数个,取两个中间值的平均值
    }
}

使用TWAP(时间加权平均价格)

// 实现TWAP预言机
contract TWAPOracle {
    struct Observation {
        uint32 timestamp;
        uint256 price;
    }
    
    mapping(address => Observation[]) public observations;
    uint256 public constant WINDOW = 30 minutes; // 时间窗口
    
    function getTWAP(address token) external view returns (uint256) {
        Observation[] storage tokenObservations = observations[token];
        uint256 length = tokenObservations.length;
        require(length > 0, "No observations");
        
        uint256 totalWeightedPrice = 0;
        uint256 totalTime = 0;
        
        for (uint i = 1; i < length; i++) {
            uint256 timeDelta = tokenObservations[i].timestamp - tokenObservations[i-1].timestamp;
            if (timeDelta > 0) {
                uint256 avgPrice = (tokenObservations[i-1].price + tokenObservations[i].price) / 2;
                totalWeightedPrice += avgPrice * timeDelta;
                totalTime += timeDelta;
            }
        }
        
        require(totalTime > 0 && totalTime >= WINDOW, "Insufficient data");
        return totalWeightedPrice / totalTime;
    }
}

价格边界检查

// 添加价格合理性检查
contract ThunderLoan {
    // 价格波动限制(例如:±10%)
    uint256 public constant MAX_PRICE_DEVIATION = 10; // 10%
    mapping(address => uint256) public lastPrices;
    
    function getPriceInWethWithCheck(address token) internal returns (uint256) {
        uint256 currentPrice = getPriceInWeth(token);
        uint256 lastPrice = lastPrices[token];
        
        if (lastPrice > 0) {
            uint256 deviation = (currentPrice > lastPrice) ? 
                ((currentPrice - lastPrice) * 100) / lastPrice :
                ((lastPrice - currentPrice) * 100) / lastPrice;
                
            require(deviation <= MAX_PRICE_DEVIATION, "Price deviation too high");
        }
        
        lastPrices[token] = currentPrice;
        return currentPrice;
    }
}

延迟价格更新

// 使用延迟的价格更新
contract DelayedOracle {
    struct PriceData {
        uint256 price;
        uint256 timestamp;
        bool isValid;
    }
    
    mapping(address => PriceData) public pendingPrices;
    uint256 public constant DELAY = 5 minutes; // 5分钟延迟
    
    function requestPriceUpdate(address token) external {
        uint256 currentPrice = getRealTimePrice(token);
        pendingPrices[token] = PriceData({
            price: currentPrice,
            timestamp: block.timestamp,
            isValid: true
        });
    }
    
    function getPrice(address token) external view returns (uint256) {
        PriceData memory data = pendingPrices[token];
        require(data.isValid, "No valid price");
        require(block.timestamp >= data.timestamp + DELAY, "Price not ready");
        return data.price;
    }
}

集成Chainlink等去中心化预言机

// 使用Chainlink
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract ChainlinkOracle {
    AggregatorV3Interface internal priceFeed;
    
    constructor(address _aggregator) {
        priceFeed = AggregatorV3Interface(_aggregator);
    }
    
    function getPrice() public view returns (uint256) {
        (
            uint80 roundId,
            int256 price,
            uint256 startedAt,
            uint256 updatedAt,
            uint80 answeredInRound
        ) = priceFeed.latestRoundData();
        
        // 数据质量检查
        require(price > 0, "Invalid price");
        require(updatedAt >= block.timestamp - 1 hours, "Stale price");
        require(answeredInRound >= roundId, "Stale round");
        
        return uint256(price);
    }
}