W3
严重业务逻辑静态扫描有 8 条规则

蜜罐代币

Honeypot / Trap Token

能买不能卖:合约里埋了只有 owner 能解的开关、黑名单、可调税率或额度上限,让普通用户买进去就出不来。这不是技术漏洞,而是故意设计的骗局。

根因

ERC-20 只是一个接口约定,它对「转账逻辑」几乎没有任何约束。任何人都可以在 transfer 里加条件:白名单之外的人不能转出、税率随时可改、超过额度就 revert。这些代码在语法与权限上可能完全规范,因此只能靠阅读源码来识别,不能靠扫描工具一锤定音

攻击是怎么一步步发生的

  1. 1
    部署方

    部署代币,税率 0、上限无限、交易开启 —— 一切指标都很健康

  2. 2
    部署方

    在 DEX 建池注入流动性,把价格拉起来,配合宣传

  3. 3
    散户

    买入并推高价格

  4. 4
    部署方

    把 sellTax 改成 99%、或把散户加进黑名单、或 setMaxTx(1)

  5. 5
    散户

    发现无法卖出;尝试绕过也会被黑名单挡住

  6. 6
    部署方

    撤走流动性(rug pull),池子里的钱全部提走

有漏洞的版本

不要照抄solidity
Solidity54
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:没有语法错误、没有溢出、权限也写对了

修复版本

推荐写法solidity
Solidity36
// 说明:蜜罐不是一个「写错了」的合约,因此没有「修好的版本」。
// 下面这段给的是**如何识别**与**如何安全发布自己的代币**。

// ---- 识别侧:看到这些就该退出 ----
// 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

但「命中规则」只说明代码里出现了相关的写法,不说明它一定有问题、也不说明没有它就没问题。命中了就回到源码确认调用路径。

延伸阅读

同属「业务逻辑」的其它 1

下一步:想知道这条在手上的合约里有没有出现,用Rug Pull 扫描器,它会检查前面列出的 8 条相关规则。 审计检查清单可以把它落成一条可勾选、可写备注的记录,最后导出 Markdown 报告;想先弄懂这一类漏洞背后的通用思路,去参考手册看对应的语言与标准条目。