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 的继承体系带来的安全问题,以及"审计覆盖"这个概念本身可能存在的系统性盲区。
攻击发生了什么
| 项目 | 值 |
|---|---|
| 攻击者 EOA | 0xa407fe273db74184898cb56d2cb685615e1c0d6e |
| 受害合约 | 0x014e6F6ba7a9f4C9a51a0Aa3189B5c0a21006869 |
| 攻击交易 TX | 0x44e637c7d85190d376a52d89ca75f2d208089bb02b7c4708ad2aaae3a97a958d |
| 损失 | 38.0474 SolvBTC(约 273 万美元) |
攻击的核心逻辑极其简洁:
- 攻击者调用受害合约的
mint()函数,存入质押品并转入 NFT - NFT 转入时触发 ERC-721 的
onERC721Received回调 - 回调函数里执行了
_mint()——第一次铸造 - 回调返回后,
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 开发者的实用清单
- 所有外部调用都是潜在的回调入口,
safeTransferFrom也一样危险 - 继承 ERC-721/ERC-3525 的合约,必须审查 onERC721Received
- 默认使用 CEI 模式,状态修改在前,外部调用在后
- 关键函数加 nonReentrant,成本极低,收益极高
- 不要迷信"审计过了" ,明确审计覆盖了哪些合约
- 用 Foundry 做 fuzz testing