区块链安全—详谈代币合约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授权竞争条件漏洞

攻击流程

  1. 用户A授权B转账N个代币(调用approve(B, N))
  2. A决定将授权额度从N改为M,发起新交易approve(B, M)
  3. B在交易2打包前,使用transferFrom花费N个代币
  4. 交易2打包后,B仍可再花费M个代币
  5. 最终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. 重入攻击风险

问题描述
在状态更新前进行外部调用可能导致重入攻击。

解决方案

  • 遵循"检查-生效-交互"模式
  • 使用互斥锁或重入保护

五、最佳实践建议

  1. 始终使用SafeMath:所有算术运算都应通过SafeMath进行

  2. 严格权限控制

    • 关键函数(如_mint、_burn)应设置适当的访问控制
    • 使用OpenZeppelin的Ownable或Role-based控制
  3. 事件记录

    • 所有状态变更都应触发相应事件
    • 事件参数应包含足够的信息
  4. 接口一致性

    • 严格遵循ERC20接口定义
    • 不修改函数签名和可见性
  5. 授权安全

    • 推荐使用increaseAllowance/decreaseAllowance模式
    • 或实现"先置零再授权"模式
  6. 测试覆盖

    • 应覆盖所有边界条件
    • 特别测试大数运算和边界值
  7. 静态分析

    • 使用Slither、MythX等工具进行静态分析
    • 检查常见漏洞模式

六、参考资料

  1. ERC20标准文档
  2. OpenZeppelin ERC20实现
  3. ERC20 Approve漏洞分析
  4. Ethernaut挑战题

通过深入理解ERC20标准实现及其安全风险,开发者可以构建更安全可靠的代币合约,避免常见的安全陷阱。

ERC20代币合约安全详解 一、ERC20概述 ERC20是以太坊上代币的标准接口协议,于2015年11月推出。它定义了一套统一的规则,使得所有遵循该标准的代币能够: 兼容以太坊钱包 易于交易所整合 实现即时交易 保持操作方式的可预测性 二、ERC20核心代码分析 1. 基础结构 声明Solidity版本 引入IERC20接口定义 引入SafeMath安全数学库 2. IERC20接口定义 3. SafeMath安全数学库 4. 核心存储变量 三、ERC20核心功能实现 1. 查询功能 2. 转账功能 3. 授权功能 4. 授权转账功能 5. 代币管理功能 四、ERC20安全风险分析 1. 接口实现不一致风险 问题描述 : 接口中定义的函数修饰符(如view、pure)在实现时可能被忽略或修改,导致预期外的状态修改。 示例 : 解决方案 : 严格遵循接口定义的所有修饰符 使用静态分析工具检查实现一致性 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 或采用以下模式: 3. 整数溢出风险 问题描述 : 直接使用算术运算符可能导致整数溢出,如: 解决方案 : 始终使用SafeMath进行算术运算 对所有算术操作进行边界检查 4. 重入攻击风险 问题描述 : 在状态更新前进行外部调用可能导致重入攻击。 解决方案 : 遵循"检查-生效-交互"模式 使用互斥锁或重入保护 五、最佳实践建议 始终使用SafeMath :所有算术运算都应通过SafeMath进行 严格权限控制 : 关键函数(如_ mint、_ burn)应设置适当的访问控制 使用OpenZeppelin的Ownable或Role-based控制 事件记录 : 所有状态变更都应触发相应事件 事件参数应包含足够的信息 接口一致性 : 严格遵循ERC20接口定义 不修改函数签名和可见性 授权安全 : 推荐使用increaseAllowance/decreaseAllowance模式 或实现"先置零再授权"模式 测试覆盖 : 应覆盖所有边界条件 特别测试大数运算和边界值 静态分析 : 使用Slither、MythX等工具进行静态分析 检查常见漏洞模式 六、参考资料 ERC20标准文档 OpenZeppelin ERC20实现 ERC20 Approve漏洞分析 Ethernaut挑战题 通过深入理解ERC20标准实现及其安全风险,开发者可以构建更安全可靠的代币合约,避免常见的安全陷阱。