严重业务逻辑静态扫描有 8 条规则
蜜罐代币
Honeypot / Trap Token
能买不能卖:合约里埋了只有 owner 能解的开关、黑名单、可调税率或额度上限,让普通用户买进去就出不来。这不是技术漏洞,而是故意设计的骗局。
根因
ERC-20 只是一个接口约定,它对「转账逻辑」几乎没有任何约束。任何人都可以在 transfer 里加条件:白名单之外的人不能转出、税率随时可改、超过额度就 revert。这些代码在语法与权限上可能完全规范,因此只能靠阅读源码来识别,不能靠扫描工具一锤定音。
攻击是怎么一步步发生的
- 1部署方
部署代币,税率 0、上限无限、交易开启 —— 一切指标都很健康
- 2部署方
在 DEX 建池注入流动性,把价格拉起来,配合宣传
- 3散户
买入并推高价格
- 4部署方
把 sellTax 改成 99%、或把散户加进黑名单、或 setMaxTx(1)
- 5散户
发现无法卖出;尝试绕过也会被黑名单挡住
- 6部署方
撤走流动性(rug pull),池子里的钱全部提走
有漏洞的版本
不要照抄soliditySolidity54 行
contract HoneypotToken {
string public name = "Free Money";
string public symbol = "FREE";
uint8 public decimals = 18;
uint256 public totalSupply = 1_000_000 ether;
address public owner;
mapping(address => uint256) public balanceOf;
mapping(address => bool) public isMarketPair; // 哪些地址算「卖」的对手方
mapping(address => bool) public blacklisted; // 黑名单
uint256 public sellTax = 0; // 初始为 0,看起来很好
uint256 public maxTxAmount = type(uint256).max;
bool public tradingEnabled = true;
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function setSellTax(uint256 t) external onlyOwner {
require(t <= 99, "too high");
sellTax = t; // 随时能改成 99%
}
function setMaxTx(uint256 v) external onlyOwner {
maxTxAmount = v; // 随时能改成 1 wei,谁都卖不掉
}
function setBlacklist(address a, bool v) external onlyOwner {
blacklisted[a] = v; // 把想卖的人拉黑
}
function addMarketPair(address pair) external onlyOwner {
isMarketPair[pair] = true;
}
// 到处都可以埋条件,而 ERC-20 接口对此毫无约束
function transfer(address to, uint256 amount) external returns (bool) {
require(tradingEnabled, "trading off");
require(!blacklisted[msg.sender], "blacklisted");
require(amount <= maxTxAmount, "over max tx");
uint256 fee = isMarketPair[to] ? (amount * sellTax) / 100 : 0;
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount - fee;
balanceOf[owner] += fee; // 税全部进 owner 口袋
return true;
}
function setTrading(bool v) external onlyOwner {
tradingEnabled = v; // 一句话冻结所有人的资产
}
}sellTax初始 0 是刻意的:部署时的状态检查看不出问题,改税发生在你买入之后maxTxAmount可以改成 1 wei:买入时上限很大,卖出时卡死blacklisted可以把任何想卖的人拉黑setTrading(false)能让所有人的资产一次性冻结isMarketPair由 owner 任意添加,意味着「什么时候收税」也由 owner 决定- 这些全都是合法 Solidity:没有语法错误、没有溢出、权限也写对了
修复版本
推荐写法soliditySolidity36 行
// 说明:蜜罐不是一个「写错了」的合约,因此没有「修好的版本」。
// 下面这段给的是**如何识别**与**如何安全发布自己的代币**。
// ---- 识别侧:看到这些就该退出 ----
// 1. 税率 / 黑名单 / 交易开关由单一 owner 即时修改,没有时间锁
// 2. 存在 isMarketPair / isBlacklisted 这类由 owner 控制的「是否可卖」开关
// 3. setMaxTx 可把额度改到极小(卖出被卡)
// 4. owner 是普通 EOA(不是多签),且没有放弃权限
// 5. 源码未验证 —— 你看不到上面这些就不用谈风险判断了
// 6. 上线时间极短、流动性由单一地址提供且可随时撤走(rug pull 前置条件)
// ---- 建设侧:如果你自己发代币,这些应该是不可变的 ----
contract HonestToken {
string public constant name = "Honest";
string public constant symbol = "HNST";
uint8 public constant decimals = 18;
uint256 public immutable maxSupply;
uint256 public constant TAX_BPS = 0; // 税率是常量:部署后不可改
mapping(address => uint256) public balanceOf;
constructor(uint256 maxSupply_) {
maxSupply = maxSupply_;
balanceOf[msg.sender] = maxSupply_;
}
// 没有 owner、没有黑名单、没有交易开关、没有可改税率。
// 「合约里没有任何人能对你做的事」才是最好的保证。
function transfer(address to, uint256 amount) external returns (bool) {
require(to != address(0), "zero address");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
return true;
}
}- 蜜罐没有「修复版」,所以这一栏给的是识别清单与「正规做法」的对照
- 关键判断标准:能改的东西越少越安全。常量 > immutable > 一次性设置 > 可随时修改
- 权留在
owner手上且是 EOA 的,风险等级直接拉满 - 放弃权限(renounceOwnership)后要看代码里是否还有其他后门(比如硬编码的另一个地址)
怎么防
- 只看已验证源码。未验证的代币不要碰,这不是「看不懂技术」而是没有判断依据
- 检查税率、黑名单、maxTx、tradingEnabled 是否有 owner 可控的修改入口
- 确认 owner 是否已放弃权限;没有放弃就要问「他能对我做什么」
- 在小额实盘测试「买入再卖出」的完整闭环,观察实际到账比例
- 用 Rug Pull 扫描器做初筛,但不要把它当结论——它能发现的是模式,伪造不了的是你的判断
- 警惕上线极短、流动性集中、社群只谈收益不谈技术的项目
真实案例
2021 至今BNB Chain / 以太坊上的大量新发代币单起数万至数百万美元,累计规模极大
蜜罐是散户损失最普遍的形态,远多于技术型漏洞
2021Squid Game 代币约 338 万美元
无法卖出 + 开发者撤池跑路,典型蜜罐 + rug pull 组合
2022多个「貔貅盘」代币持续发生
部分代币在交易早期一切正常,在特定区块高度后才触发限制
审计这个合约时要问的问题
- 1.代币的 transfer 里有没有对「卖出方向」的特殊处理?
- 2.税率是常量、immutable、还是可改的状态变量?可改的话谁能改?
- 3.有没有黑名单 / 白名单?谁能改?
- 4.交易开关归谁管?关掉之后用户还能赎回吗?
- 5.owner 权限放弃了吗?放弃得彻底吗(有没有硬编码的后门地址)?
这些问题也是审计检查清单里对应的条目——那里可以把每一条的检查结果记下来,最后导出成报告。
静态扫描能发现它吗
能——扫描引擎里有 8 条规则与它相关:
honeypot-conditionmutable-taxblacklisttrading-gatetx-limitpausableno-renounceunrestricted-mint但「命中规则」只说明代码里出现了相关的写法,不说明它一定有问题、也不说明没有它就没问题。命中了就回到源码确认调用路径。