从 Solv Protocol 273 万美元被黑事件,深入理解 Solidity 自重入攻击 —— ERC-3525 标准中的暗坑

0 阅读4分钟

2026 年 3 月 5 日,Solv Protocol 遭到攻击,损失 273 万美元(38.0474 SolvBTC)。攻击者仅用 一笔交易,将 135 个 BRO 代币经过 22 次循环操作,膨胀为 5.67 亿 BRO,最终通过 BRO→SolvBTC→WBTC→WETH→ETH 的路径在 Uniswap V3 兑换成 1,211 ETH。

没有闪电贷,没有预言机操纵,没有复杂的跨链利用。就是最朴素的 Solidity 重入攻击。

但这件事之所以值得单独拿出来写,是因为它暴露了一个被严重忽视的风险面:新兴代币标准 ERC-3525 的继承体系带来的安全问题,以及"审计覆盖"这个概念本身可能存在的系统性盲区。

攻击发生了什么

项目
攻击者 EOA0xa407fe273db74184898cb56d2cb685615e1c0d6e
受害合约0x014e6F6ba7a9f4C9a51a0Aa3189B5c0a21006869
攻击交易 TX0x44e637c7d85190d376a52d89ca75f2d208089bb02b7c4708ad2aaae3a97a958d
损失38.0474 SolvBTC(约 273 万美元)

攻击的核心逻辑极其简洁:

  1. 攻击者调用受害合约的 mint() 函数,存入质押品并转入 NFT
  2. NFT 转入时触发 ERC-721 的 onERC721Received 回调
  3. 回调函数里执行了 _mint()——第一次铸造
  4. 回调返回后,mint() 函数继续执行 _mint()——第二次铸造

同一笔质押品,铸了两次。重复 22 次,135 个代币变成 5.67 亿个。

漏洞代码深度拆解

function mint(address to, uint256 collateralAmount, uint256 nftId) external {
    collateralToken.safeTransferFrom(msg.sender, address(this), collateralAmount);
    nftToken.safeTransferFrom(msg.sender, address(this), nftId); // 触发回调!
    _mint(to, collateralAmount);
}

function onERC721Received(address, address from, uint256, bytes calldata data) 
    external returns (bytes4) {
    uint256 amount = _parseCollateralAmount(data);
    _mint(from, amount); // 在回调里就 mint 了!
    return this.onERC721Received.selector;
}

执行流程:

攻击者调用 mint(to, 100, nftId)
  ├─ safeTransferFrom(collateralToken, 100)
  ├─ safeTransferFrom(nftToken, nftId)
  │    └─ 触发 onERC721Received()
  │         └─ _mint(attacker, 100)       // 第一次 mint
  └─ _mint(attacker, 100)                 // 第二次 mint

为什么叫"自"重入?

传统重入是 A 合约调用 B 合约,B 回调 A。而自重入是 A 合约在同一函数执行过程中,通过内部回调再次进入同一个合约的另一个函数

这种攻击往往比跨合约重入更隐蔽,因为外部调用看起来是正常的转账操作,回调函数是标准要求的,开发者会觉得"能有什么问题"。

ERC-3525:半同质化代币标准的暗坑

ERC-3525 是半同质化代币标准,介于 ERC-20 和 ERC-721 之间。ERC-3525 继承了 ERC-721,包括 onERC721Received 回调接口。

如果你实现了 ERC-3525 合约,你的合约同时也是一个"能接收 NFT 的合约"。任何向你的合约转入 NFT 的操作,都会触发 onERC721Received 回调。

审计的系统性盲区

出问题的合约没有任何审计覆盖。Solv 有 5 家审计公司背书,但都没覆盖这个合约。Bug Bounty 也没有覆盖。

"经过审计"和"所有合约都经过审计"是两个完全不同的概念。

三种修复方案

方案一:ReentrancyGuard

contract SolvPool is ReentrancyGuard {
    function mint(...) external nonReentrant { ... }
}

方案二:CEI 模式

function mint(...) external {
    _mint(to, collateralAmount);      // 先改状态
    collateralToken.safeTransferFrom(...);  // 后调用外部
    nftToken.safeTransferFrom(...);
}

方案三:回调函数中不做 mint

function onERC721Received(...) external returns (bytes4) {
    return this.onERC721Received.selector; // 只返回selector
}

给 Solidity 开发者的实用清单

  1. 所有外部调用都是潜在的回调入口safeTransferFrom 也一样危险
  2. 继承 ERC-721/ERC-3525 的合约,必须审查 onERC721Received
  3. 默认使用 CEI 模式,状态修改在前,外部调用在后
  4. 关键函数加 nonReentrant,成本极低,收益极高
  5. 不要迷信"审计过了" ,明确审计覆盖了哪些合约
  6. 用 Foundry 做 fuzz testing

参考链接