区块链安全—详谈代币合约ERC20
字数 1222 2025-08-22 12:22:15
ERC20代币合约安全详解
一、ERC20概述
ERC20是以太坊上代币的标准接口协议,于2015年11月推出。它定义了一套统一的规则,使得所有遵循该标准的代币能够:
- 兼容以太坊钱包
- 易于交易所整合
- 实现即时交易
- 保持操作方式的可预测性
二、ERC20核心代码分析
1. 基础结构
pragma solidity ^0.4.24;
import "./IERC20.sol";
import "../../math/SafeMath.sol";
- 声明Solidity版本
- 引入IERC20接口定义
- 引入SafeMath安全数学库
2. IERC20接口定义
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address who) external view returns (uint256);
function allowance(address owner, address spender) external view returns (uint256);
function transfer(address to, uint256 value) external returns (bool);
function approve(address spender, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
3. SafeMath安全数学库
library SafeMath {
// 乘法(防溢出)
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) return 0;
uint256 c = a * b;
require(c / a == b);
return c;
}
// 除法(防除零)
function div(uint256 a, uint256 b) internal pure returns (uint256) {
require(b > 0);
uint256 c = a / b;
return c;
}
// 减法(防负数溢出)
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a);
return a - b;
}
// 加法(防溢出)
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a);
return c;
}
// 取模(防除零)
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
require(b != 0);
return a % b;
}
}
4. 核心存储变量
mapping (address => uint256) private _balances; // 账户余额映射
mapping (address => mapping (address => uint256)) private _allowed; // 授权额度映射
uint256 private _totalSupply; // 代币总供应量
三、ERC20核心功能实现
1. 查询功能
// 查询总供应量
function totalSupply() public view returns (uint256) {
return _totalSupply;
}
// 查询账户余额
function balanceOf(address owner) public view returns (uint256) {
return _balances[owner];
}
// 查询授权额度
function allowance(address owner, address spender) public view returns (uint256) {
return _allowed[owner][spender];
}
2. 转账功能
// 直接转账
function transfer(address to, uint256 value) public returns (bool) {
require(value <= _balances[msg.sender]);
require(to != address(0));
_balances[msg.sender] = _balances[msg.sender].sub(value);
_balances[to] = _balances[to].add(value);
emit Transfer(msg.sender, to, value);
return true;
}
3. 授权功能
// 设置授权额度
function approve(address spender, uint256 value) public returns (bool) {
require(spender != address(0));
_allowed[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}
// 增加授权额度
function increaseAllowance(address spender, uint256 addedValue) public returns (bool) {
require(spender != address(0));
_allowed[msg.sender][spender] = _allowed[msg.sender][spender].add(addedValue);
emit Approval(msg.sender, spender, _allowed[msg.sender][spender]);
return true;
}
// 减少授权额度
function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) {
require(spender != address(0));
_allowed[msg.sender][spender] = _allowed[msg.sender][spender].sub(subtractedValue);
emit Approval(msg.sender, spender, _allowed[msg.sender][spender]);
return true;
}
4. 授权转账功能
// 使用授权额度转账
function transferFrom(address from, address to, uint256 value) public returns (bool) {
require(value <= _balances[from]);
require(value <= _allowed[from][msg.sender]);
require(to != address(0));
_balances[from] = _balances[from].sub(value);
_balances[to] = _balances[to].add(value);
_allowed[from][msg.sender] = _allowed[from][msg.sender].sub(value);
emit Transfer(from, to, value);
return true;
}
5. 代币管理功能
// 增发代币(挖矿)
function _mint(address account, uint256 amount) internal {
require(account != address(0));
_totalSupply = _totalSupply.add(amount);
_balances[account] = _balances[account].add(amount);
emit Transfer(address(0), account, amount);
}
// 销毁代币
function _burn(address account, uint256 amount) internal {
require(account != address(0));
require(amount <= _balances[account]);
_totalSupply = _totalSupply.sub(amount);
_balances[account] = _balances[account].sub(amount);
emit Transfer(account, address(0), amount);
}
// 从授权额度中销毁代币
function _burnFrom(address account, uint256 amount) internal {
require(amount <= _allowed[account][msg.sender]);
_allowed[account][msg.sender] = _allowed[account][msg.sender].sub(amount);
_burn(account, amount);
}
四、ERC20安全风险分析
1. 接口实现不一致风险
问题描述:
接口中定义的函数修饰符(如view、pure)在实现时可能被忽略或修改,导致预期外的状态修改。
示例:
// 接口定义
interface Building {
function isLastFloor(uint) view public returns (bool);
}
// 实际实现(缺少view修饰符)
function isLastFloor(uint) public returns (bool) {
ls = !ls; // 修改了状态变量
return ls;
}
解决方案:
- 严格遵循接口定义的所有修饰符
- 使用静态分析工具检查实现一致性
2. Approve授权竞争条件漏洞
攻击流程:
- 用户A授权B转账N个代币(调用approve(B, N))
- A决定将授权额度从N改为M,发起新交易approve(B, M)
- B在交易2打包前,使用transferFrom花费N个代币
- 交易2打包后,B仍可再花费M个代币
- 最终B花费了N+M个代币,超出A的预期
解决方案:
- 使用increaseAllowance/decreaseAllowance代替直接approve
- 或采用以下模式:
function approve(address spender, uint256 value) public returns (bool) {
require((value == 0) || (_allowed[msg.sender][spender] == 0));
_allowed[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}
3. 整数溢出风险
问题描述:
直接使用算术运算符可能导致整数溢出,如:
uint256 a = 2^256 - 1;
uint256 b = a + 1; // 溢出为0
解决方案:
- 始终使用SafeMath进行算术运算
- 对所有算术操作进行边界检查
4. 重入攻击风险
问题描述:
在状态更新前进行外部调用可能导致重入攻击。
解决方案:
- 遵循"检查-生效-交互"模式
- 使用互斥锁或重入保护
五、最佳实践建议
-
始终使用SafeMath:所有算术运算都应通过SafeMath进行
-
严格权限控制:
- 关键函数(如_mint、_burn)应设置适当的访问控制
- 使用OpenZeppelin的Ownable或Role-based控制
-
事件记录:
- 所有状态变更都应触发相应事件
- 事件参数应包含足够的信息
-
接口一致性:
- 严格遵循ERC20接口定义
- 不修改函数签名和可见性
-
授权安全:
- 推荐使用increaseAllowance/decreaseAllowance模式
- 或实现"先置零再授权"模式
-
测试覆盖:
- 应覆盖所有边界条件
- 特别测试大数运算和边界值
-
静态分析:
- 使用Slither、MythX等工具进行静态分析
- 检查常见漏洞模式
六、参考资料
通过深入理解ERC20标准实现及其安全风险,开发者可以构建更安全可靠的代币合约,避免常见的安全陷阱。