先打个生活中的比方:
想象你在玩一个老式计算器,最大只能显示9999。你输入9999再加1,结果不是10000,而是“溢出”变成了0。
溢出攻击就类似这样:黑客利用智能合约里数字的上限漏洞,让计算结果“翻车”归零,绕过检查,干坏事。在BEC代币事件中,攻击者就是靠这个“计算器Bug”,凭空转走了天量代币,把市场砸崩了。
溢出攻击的核心是Solidity中uint256的环绕特性:它的最大值是2^256-1,一旦超过就直接归零,要是减法出了负数还会跳到最大值。
在区块链世界,智能合约管着“真金白银”,一旦有漏洞,后果不堪设想。2018年的BEC代币事件就是个经典案例,价值几近归零的项目背后,藏着一个粗心的溢出漏洞。今天我们就来聊聊溢出攻击,看看它咋发生的,又该咋防?
BEC(BeautyChain)是美图公司合作推出的代币,定位是个“区块链+美妆”的项目。用户可以用BEC交易美容服务,2018年初上线时挺火爆,总供应量70亿枚,基于ERC20标准开发。可这么个“明星项目”,安全真有保障吗?
2018年4月23日中午,黑客盯上了BEC的智能合约,用溢出攻击几小时内转出了天量代币。具体来说,他们调用了batchTransfer函数,传了个超大数字和两个地址,合约里一个乘法溢出归零,balances[msg.sender] >= amount的检查直接失效,黑客白捡了巨额BEC,然后在市场上抛售,代币价格瞬间崩到几乎为0。
BEC被攻击后,市场一片恐慌。抛售潮让BEC价值归零,投资者血本无归,项目直接“凉凉”。这次事件还波及了其他代币项目,大家开始质疑ERC20合约的安全性,开发者也意识到:一个小小的溢出漏洞,就能毁掉一个项目。
想看原版代码?戳这里:
https://etherscan.io/address/0xc5d105e63711398af9bbff092d4b6769c82f793d#code
重点看batchTransfer函数。
致命一击在这行:
uint256 amount = uint256(cnt) * _value;
这行代码直接做了个乘法,没检查溢出。结果呢?输入超大数字时,amount溢出归零,绕过了balances[msg.sender] >= amount的余额检查,黑客就能随便给自己加钱。
BEC加钱现场:
https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f
我们用BEC的例子还原一下攻击过程,看看是咋回事?
contract BecToken {
using SafeMath for uint256;
mapping(address => uint256) balances;
// 批量转账
function batchTransfer(address[] _receivers, uint256 _value) public returns (bool) {
uint cnt = _receivers.length;
uint256 amount = uint256(cnt) * _value; // 这行就是坑
require(cnt > 0 && cnt <= 20);
require(_value > 0 && balances[msg.sender] >= amount);
balances[msg.sender] = balances[msg.sender].sub(amount);
for (uint i = 0; i < cnt; i++) {
balances[_receivers[i]] = balances[_receivers[i]].add(_value);
Transfer(msg.sender, _receivers[i], _value);
}
return true;
}
}
问题在哪?
amount = uint256(cnt) * _value没防溢出。黑客传了个超大_value,让它溢出归零,绕过检查。
batchTransfer,传参:_receivers是个长度为2的地址数组,_value设为0x8000000000000000000000000000000000000000000000000000000000000000(也就是2^255)。amount = 2 * 2^255 = 2^256,溢出后amount = 0。余额检查一看“0”,放行!但for循环里按_value给两个地址各加了2^255个代币,黑客就像进了无人看管的银库。针对以上的溢出漏洞,我们再来做个简单的测试:
pragma solidity ^0.4.18;
contract BECTest {
uint256 _value = 2**256/2;
function mulTest() public constant returns (uint) {
uint256 amount = 2 * _value;
return amount;
}
}
在Remix中运行,可以看到运行结果为0,溢出归零一目了然。

从技术角度看,溢出攻击的根源在于未检查的数学运算:
uint256不防溢出,超过2^256-1就归零,BEC的batchTransfer直接用裸乘法,没挡住。mul,检查被绕过。amount,实际转账用_value,黑客靠这个“错位”凭空加钱。本质上,这是开发者没重视大数字运算的风险,才让黑客钻了空子。
知道坑在哪,我们就得填上。以下是三招实用办法:
function batchTransfer(address[] _receivers, uint256 _value) public returns (bool) {
uint cnt = _receivers.length;
uint256 amount = uint256(cnt).mul(_value); // 用SafeMath
require(cnt > 0 && cnt <= 20);
require(_value > 0 && balances[msg.sender] >= amount);
balances[msg.sender] = balances[msg.sender].sub(amount);
for (uint i = 0; i < cnt; i++) {
balances[_receivers[i]] = balances[_receivers[i]].add(_value);
Transfer(msg.sender, _receivers[i], _value);
}
return true;
}
效果:SafeMath的mul溢出时会抛异常,交易回滚,不会让黑客得逞。
function batchTransfer(address[] _receivers, uint256 _value) public returns (bool) {
uint cnt = _receivers.length;
uint256 amount = uint256(cnt) * _value;
require(cnt > 0 && cnt <= 20);
require(_value > 0 && balances[msg.sender] >= amount);
require(amount / cnt == _value); // 检查溢出
balances[msg.sender] = balances[msg.sender].sub(amount);
for (uint i = 0; i < cnt; i++) {
balances[_receivers[i]] = balances[_receivers[i]].add(_value);
Transfer(msg.sender, _receivers[i], _value);
}
return true;x
}
效果:溢出时amount / cnt不等于_value,条件失败,挡住攻击。
Solidity 0.8.0+默认启用了溢出检查,超限会自动报错。改用新版本,裸运算也能防溢出,既省心又安全。
溢出攻击,说到底就是“数字归零”的致命陷阱。BEC事件给我们上了生动一课:一个粗心的乘法,就能让天量代币凭空消失,项目瞬间崩盘。智能合约代码写得再漂亮,运算没管好就是纸老虎。尤其涉及资金时,每一行数学代码都得瞪大眼睛检查。开发者别心存侥幸,赶紧用SafeMath或新版Solidity把溢出这块“漏水地板”修牢,别等黑客上门才后悔!
基于全文检索与主题相似度
说在前面 生活中,我们习惯了依赖时间来安排一切。闹钟提醒我们起床,高铁准点发车,考试到点交卷,股票交易精确到毫秒撮合…… 在 Web2 世界里,软件系统同样靠精准的时间戳来自动执行任务,比如定时扣款、会员到期、订单结算,这一切都建立在可信的时间基础上。但到了 Web3 世界里,时间居然能被“调表”!
0转账钓鱼攻击是个啥? 0转账钓鱼攻击(Zero Transfer Phishing Attack)是一种简单却阴险的骗术,虽然技术含量不高,但在币圈屡屡得手,甚至酿成过惊天大案(见下文)。 每次听说这种攻击得逞,连我都忍不住暗想:这黑客咋不是我呢? 先打个生活中的比方: 假设你和好友张三经常通过银
说在前面 玩过币的人都知道,ERC20是以太坊上一个常见的代币标准(支持钱包交易交互),里面有个类似开支票的功能——approve授权和transferFrom转账兑现。 如同现实生活中你给人开了一张支票,写上金额,对方拿着支票去银行就能把钱取走。你账户里的钱少了,他的账户里钱多了。 在智能合约里,