无限铸造攻击及其预防
无限铸造攻击指的是当一个代币合约允许攻击者在没有有效授权、没有限制最大供应量,或两者兼而有之的情况下创建代币时发生的安全漏洞。攻击者可以在市场和其他系统反应之前,任意铸造一定数量的代币,然后出售、交换、存入或作为抵押品使用新创建的代币。
这种攻击的后果不仅仅是通货膨胀。无限铸造可能摧毁代币的交换价值,抽干流动性池,甚至在借贷或财政系统中造成没有支撑的抵押品。Elrond 在 2022-06-05 曾发生一次被归类为无限铸造的事件,损失金额高达 1.13 亿美元,显示出一旦发行边界被破坏,整个生态系统就可能遭受重大损失。
什么是无限铸造攻击及其工作原理?
无限铸造攻击利用了没有可靠验证铸造权限的代币发行路径。一个公开或保护不充分的 mint 函数使得攻击者能够创建被协议视为合法的 ERC-20 代币的余额。
只有当 mint 函数的调用者权限、发行条件以及供应量核算符合协议的经济设计时,该函数才是安全的。如果任何调用者都能触发 _mint 类似逻辑,或者特权调用者能够绕过供应上限,那么该代币的稀缺性就只是一个假设。
典型的无限铸造攻击流程如下:
- 攻击者识别出一个可被外部调用的铸造路径或绕过的访问控制。
- 攻击者用自己控制的接收地址和金额调用相应函数。
- 合约增加攻击者的余额,通常还会增加
totalSupply。 - 攻击者将铸造的代币转入去中心化交易所(DEX)池、借贷市场、跨链桥或金库。
- 攻击者用这些人工制造的代币换取实际资产或作为抵押品。
- 流动性提供者、借贷方和持币者因此承担经济损失。
无限铸造漏洞尤为危险的是那些其他合约在接受余额时,没有独立验证余额创建方式的场景。例如,借贷协议无法区分通过预期发行方式铸造的代币和通过缺失访问控制的非法铸造。
哪些 Solidity 模式会导致无限铸造漏洞?
无限铸造漏洞通常源于缺失授权、缺少供应限制,或授权虽表面有效但可通过不当路径绕过的设计。应审查每一个可能增加余额的函数,不论其调用方式如何。
最明显的模式是没有调用者限制的公共铸造函数:
function mint(address to, uint256 amount) external {
balanceOf[to] += amount;
totalSupply += amount;
}
此外,安全隐患也存在于辨别身份错误的情形。例如,tx.origin 不应作为铸造权限的依据,因为攻击者可以诱导高权限的钱包调用恶意中介合约。同样,检验接收地址是否具有特权并不能证明调用者有权铸币。
其他常见原因包括:
setMinter函数被任何账户调用,允许攻击者授予自己发行权限。- 允许升级的代理合约,其升级权限可能会移除或修改铸造限制。
- 信任任意消息作为铸造指令的桥接或跨链处理器。
- 在某个
mint函数中有供应上限限制,但在奖励发放或迁移函数中没有。 - 由于算术运算或单位转换错误导致名义上的上限失去效用。
- Burn-和铸造的核算未严格验证,导致燃烧后再铸造的问题。
仅仅依靠 totalSupply 变量并不能防止无限铸造。合约必须在任何铸造操作前,确保 totalSupply + amount 不会超过最高预设的最大供应量,这一不变式应贯穿所有发行路径,如管理恢复、跨链铸造、奖励及迁移等。
有漏洞和已修复的无限铸造 Solidity 代码示例
漏洞合约:暴露了无限制的铸造函数,任何外部账户都可以铸造任意数量的代币,且没有最大供应限制。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract VulnerableInfiniteMintToken {
string public constant name = "Vulnerable Token";
string public constant symbol = "VULN";
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
event Transfer(address indexed from, address indexed to, uint256 amount);
function mint(address to, uint256 amount) external {
// 危险:任何调用者都可以创建任意数量的代币。
totalSupply += amount;
balanceOf[to] += amount;
emit Transfer(address(0), to, amount);
}
function transfer(address to, uint256 amount) external returns (bool) {
require(balanceOf[msg.sender] >= amount, "余额不足");
unchecked {
balanceOf[msg.sender] -= amount;
}
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
}
修复版 separate 管理员与授权铸造者,并在每次增加余额前强制最大供应量限制。供应上限不是授权的替代品,授权也不能作为限制的替代。两者应同时存在,覆盖不同的失败模式。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract CappedRoleMintToken {
string public constant name = "Capped Token";
string public constant symbol = "CAP";
address public immutable admin;
uint256 public immutable maxSupply;
uint256 public totalSupply;
mapping(address => bool) public isMinter;
mapping(address => uint256) public balanceOf;
event Transfer(address indexed from, address indexed to, uint256 amount);
event MinterUpdated(address indexed minter, bool allowed);
modifier onlyAdmin() {
require(msg.sender == admin, "不是管理员");
_;
}
modifier onlyMinter() {
require(isMinter[msg.sender], "非授权铸造者");
_;
}
constructor(uint256 maxSupply_) {
require(maxSupply_ > 0, "最大供应量为零");
admin = msg.sender;
maxSupply = maxSupply_;
isMinter[msg.sender] = true;
}
function setMinter(address minter, bool allowed) external onlyAdmin {
isMinter[minter] = allowed;
emit MinterUpdated(minter, allowed);
}
function mint(address to, uint256 amount) external onlyMinter {
// 修复:授权铸造不能超过固定最大供应量。
require(totalSupply + amount <= maxSupply, "超过最大供应量");
totalSupply += amount;
balanceOf[to] += amount;
emit Transfer(address(0), to, amount);
}
function transfer(address to, uint256 amount) external returns (bool) {
require(balanceOf[msg.sender] >= amount, "余额不足");
unchecked {
balanceOf[msg.sender] -= amount;
}
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
}
如果你的协议中包含奖励、跨链、金库份额或迁移的铸造机制,建议通过智能合约审计与安全评估,追踪每一条发行路径,确保其按照授权和供应不变式执行。
如何在 Solidity 中防止无限铸造攻击?
防止无限铸造攻击的关键在于采用多层次的发行控制:明确权限、强制供应限制、严格的管理权限,以及验证不存在其他路径可以非法创建余额。具体方案应依据供应是固定、有限、基于发行、抵押支持还是通过桥接实现而定。
| 技术手段 | 目的 | 成本 | 限制 |
|---|---|---|---|
角色控制 mint |
阻止任意外部调用铸币 | 存储空间读取角色 | 被攻破的铅铸造者依然能在权限范围内铸币 |
| 设定最大供应量 | 超出预定总量铸币 | 每次铸造一次算术检查 | 不阻止授权方提前铸完全部最大值 |
| 分离管理员和铸造者角色 | 防止铸造者私自提升权限 | 额外的角色管理代码 | 管理员仍是关键信任节点 |
| 多签或时间锁管理 | 单点钥匙变更权限 | 管理操作延时,治理复杂 | 无法修复已部署的铸造逻辑缺陷 |
内置 _mint 路径 |
在二次发行中遗漏限购检查 | 重构成本 | 审查仍需找到所有外部通入 _mint 的路径 |
| 事件监控 | 延迟检测异常铸造 | 线下监控基础设施 | 不能追溯已完成的恶意铸造 |
对于供应固定的代币,最佳做法通常是部署时即可铸完全部,之后便不允许再铸。对于基于发行的代币,应定义可衡量的时间表,内部发行函数中强制执行总供应上限,并确保每个外部接口都具有明确授权目的。
Soken 方法论视铸造为资产创建的边界,而非便利功能。每一条增加余额的途径都应有明确的授权模型、经济限制和可测试的不变式。
哪些实际案件被归类为无限铸造攻击?
历史上,发生的无限铸造事件表明,发行缺陷影响着许多链上的老牌资产和新兴协议。以下是被 DefiLlama 黑客数据库 分类为 Infinite Mint 的部分事件:
| 协议 | 日期 | 损失金额 | 链 |
|---|---|---|---|
| Elrond | 2022-06-05 | $113.0M | Elrond |
| Paid Network | 2021-03-05 | $27.0M | Ethereum |
| Gala | 2024-05-20 | $22.3M | Ethereum |
| Haven Protocol | 2021-07-03 | $8.2M | Haven |
| OcelotDex | 2024-09-20 | $4.9M | Ethereum |
| TSR | 2026-06-01 | $2.5M | BSC |
| Raydium AMM | 2026-06-10 | $1.3M | Solana |
| Flooring Protocol | 2026-06-08 | $500K | Ethereum |
这些事件的核心都在于:一旦没有支撑的代币进入市场,其攻击便不仅限于合约本身。流动性池会购买人工制造的代币,借贷系统可能接受其作为抵押,跨链桥或金库也会将余额扩散到其他系统。
因此,相关的审查问题不仅是“攻击者是否能调用 mint?” 更重要的是:“是否存在任何未被信任或过度信任的路径,使协议将未支撑的代币当作有效资产?”
如何在部署前发现无限铸造漏洞?
检测无限铸造漏洞的关键在于映射所有可能增加余额的状态变迁,并证明每一次变迁都获得了授权且保持了发行不变式。手工审查应从代币合约开始,同时还要深入桥接合约、金库、奖励分发、升级模块及管理助手。
审查清单包括:
- 查找所有写入余额、股份、供应计数和内部铸造函数的操作。
- 验证每个外部发行路径都拥有合适的调用者权限。
- 确认每个路径都强制执行相同的供应上限或发行限制。
- 审查铸造者和管理者的增减逻辑。
- 跟踪升级、跨链、迁移和回调路径是否会触达铸造逻辑。
- 检查是否有特权账户可以意外更改最大供应量、发行者或会计单元。
- 核实铸造事件与
totalSupply是否与实际余额相符。 - 测试接受代币作为抵押、流动性或金库资产的集成系统。
Slither 在列出外部可调用函数、特权变更和可疑访问控制模式方面非常有用,但无法保障协议的供应模型在经济上是正确的。对于间接的铸造路径和跨合约权限,仍需人工分析。
Foundry 的 fuzz 测试可以验证任何非授权调用者都无法进行铸造,且供应总量不会超过最大值。用不变式测试可直接表达安全性关切,例如:
function invariant_supplyNeverExceedsCap() public view {
require(token.totalSupply() <= token.maxSupply(), "超过最大供应");
}
此外,还应增加确保只有被信任的角色能增加供应的不变式检测。在 Soken 方法论中,这一属性会在所有公开函数中测试,而不仅限于名为 mint 的函数。更多关于访问控制和账户核算的研究资料,欢迎访问 Soken Hub。
安全洞察: Soken 方法论视每个新代币单位为由协议创造的责任。如果合约不能证明是谁授权了该责任、并且该责任为何在发行模型内,铸造路径就成为需重新设计的安全边界。
通过角色检查和供应上限,将无限制铸造变为可控和可追责的操作,关键在于将余额创建流程集中、构建不可绕过的机制。
在上线或升级之前,应用简明的语言写出预期的供应不变式,在共享的铸造路径中实现它,并用 Foundry 不变式测试强化执行。