错误处理是智能合约开发的关键部分。Solidity 提供了多种错误处理机制,从 Solidity 0.8.4 开始引入了自定义错误,大幅优化了 Gas 消耗和错误信息的可读性。
1. 错误处理机制概览
solidity/* Solidity 中的错误处理: 1. require(condition, message) - 用于验证输入和外部条件 - 失败时回滚所有状态变更 - 退还剩余 Gas 2. assert(condition) - 用于检查内部不变量 - 失败表示代码有 bug - 消耗所有 Gas(严重错误) 3. revert(message) / revert CustomError() - 显式回滚交易 - Solidity 0.8.4+ 支持自定义错误 4. try/catch - 捕获外部调用异常 - Solidity 0.6.0+ 支持 */
2. require 的使用
soliditycontract RequireExample { mapping(address => uint256) public balances; // 基本用法 function transfer(address _to, uint256 _amount) external { require(_to != address(0), "Invalid recipient address"); require(_amount > 0, "Amount must be greater than 0"); require(balances[msg.sender] >= _amount, "Insufficient balance"); balances[msg.sender] -= _amount; balances[_to] += _amount; } // 多个条件检查 function complexOperation( address _token, uint256 _amount, uint256 _minAmount, uint256 _deadline ) external { require(_token != address(0), "Invalid token"); require(_amount >= _minAmount, "Amount below minimum"); require(_amount <= 10000 ether, "Amount exceeds maximum"); require(block.timestamp < _deadline, "Transaction expired"); require( _amount % 100 == 0, "Amount must be multiple of 100" ); // 执行操作... } // 带错误消息的 require function withdraw(uint256 _amount) external { uint256 balance = balances[msg.sender]; require( balance >= _amount, string(abi.encodePacked( "Insufficient balance. Available: ", uint2str(balance), ", Requested: ", uint2str(_amount) )) ); balances[msg.sender] -= _amount; payable(msg.sender).transfer(_amount); } // 辅助函数:uint 转 string function uint2str(uint256 _i) internal pure returns (string memory) { if (_i == 0) return "0"; uint256 j = _i; uint256 length; while (j != 0) { length++; j /= 10; } bytes memory bstr = new bytes(length); uint256 k = length; j = _i; while (j != 0) { bstr[--k] = bytes1(uint8(48 + j % 10)); j /= 10; } return string(bstr); } }
3. assert 的使用
soliditycontract AssertExample { uint256 public constant MAX_SUPPLY = 1000000; uint256 public totalSupply; mapping(address => uint256) public balances; // assert 用于检查内部不变量 function mint(address _to, uint256 _amount) external { // 使用 require 检查外部输入 require(_to != address(0), "Invalid address"); require(_amount > 0, "Invalid amount"); uint256 newTotalSupply = totalSupply + _amount; require(newTotalSupply <= MAX_SUPPLY, "Exceeds max supply"); // 状态更新 totalSupply = newTotalSupply; balances[_to] += _amount; // 使用 assert 检查内部不变量 // 如果失败,说明代码有严重 bug assert(totalSupply >= balances[_to]); assert(balances[_to] >= _amount); } // 数学运算后的不变量检查 function divide(uint256 a, uint256 b) external pure returns (uint256) { require(b > 0, "Division by zero"); uint256 result = a / b; // 检查数学不变量 assert(result * b <= a); return result; } // 复杂操作后的状态检查 function complexTransfer( address _from, address _to, uint256 _amount ) external { uint256 fromBalanceBefore = balances[_from]; uint256 toBalanceBefore = balances[_to]; // 执行转账 balances[_from] -= _amount; balances[_to] += _amount; // 检查不变量 assert(balances[_from] == fromBalanceBefore - _amount); assert(balances[_to] == toBalanceBefore + _amount); assert( balances[_from] + balances[_to] == fromBalanceBefore + toBalanceBefore ); } }
4. revert 的使用
soliditycontract RevertExample { // 自定义错误(Solidity 0.8.4+) error InsufficientBalance(uint256 available, uint256 required); error InvalidAddress(address provided); error Unauthorized(address caller, bytes32 requiredRole); error TransferFailed(address from, address to, uint256 amount); error DeadlineExpired(uint256 deadline, uint256 currentTime); error SlippageExceeded(uint256 expected, uint256 actual); mapping(address => uint256) public balances; mapping(address => bytes32) public roles; // 使用自定义错误(Gas 优化) function transferWithCustomError( address _to, uint256 _amount ) external { if (_to == address(0)) { revert InvalidAddress(_to); } uint256 balance = balances[msg.sender]; if (balance < _amount) { revert InsufficientBalance(balance, _amount); } balances[msg.sender] -= _amount; balances[_to] += _amount; } // 带多个参数的自定义错误 function swapTokens( address _tokenIn, address _tokenOut, uint256 _amountIn, uint256 _minAmountOut, uint256 _deadline ) external { if (block.timestamp > _deadline) { revert DeadlineExpired(_deadline, block.timestamp); } // 模拟交换计算 uint256 amountOut = _amountIn * 95 / 100; // 5% 滑点 if (amountOut < _minAmountOut) { revert SlippageExceeded(_minAmountOut, amountOut); } // 执行交换... } // 权限检查 function adminFunction() external view { bytes32 requiredRole = keccak256("ADMIN_ROLE"); if (roles[msg.sender] != requiredRole) { revert Unauthorized(msg.sender, requiredRole); } // 执行管理员操作... } // 传统 revert(字符串消息) function legacyRevert(address _to, uint256 _amount) external { if (_to == address(0)) { revert("Transfer to zero address"); } if (balances[msg.sender] < _amount) { revert("Insufficient balance"); } // 执行转账... } }
5. 自定义错误的 Gas 优化
soliditycontract GasComparison { // 自定义错误 error CustomError(uint256 code); // 使用 require 字符串(消耗更多 Gas) function useRequire(uint256 _value) external pure { require(_value > 0, "Value must be greater than zero"); require(_value < 1000, "Value must be less than 1000"); require(_value % 2 == 0, "Value must be even"); } // 使用自定义错误(节省 Gas) function useCustomError(uint256 _value) external pure { if (_value == 0) revert CustomError(1); if (_value >= 1000) revert CustomError(2); if (_value % 2 != 0) revert CustomError(3); } /* Gas 对比: - require 字符串:约 200-300 gas + 字符串存储成本 - 自定义错误:约 50-100 gas 在频繁调用的函数中,自定义错误可以显著节省 Gas */ } // 实际应用中的自定义错误设计 contract TokenWithCustomErrors { // 定义所有可能的错误 error TransferFromZeroAddress(); error TransferToZeroAddress(); error TransferAmountExceedsBalance(address sender, uint256 balance, uint256 amount); error ApproveFromZeroAddress(); error ApproveToZeroAddress(); error InsufficientAllowance(address owner, address spender, uint256 allowance, uint256 amount); error MintToZeroAddress(); error BurnFromZeroAddress(); error BurnAmountExceedsBalance(address account, uint256 balance, uint256 amount); error InvalidSender(address sender); error InvalidReceiver(address receiver); error PermitDeadlineExpired(uint256 deadline, uint256 currentTime); error InvalidPermitSignature(address signer, address owner); mapping(address => uint256) private _balances; mapping(address => mapping(address => uint256)) private _allowances; uint256 private _totalSupply; function transfer(address _to, uint256 _amount) external returns (bool) { address owner = msg.sender; if (owner == address(0)) revert TransferFromZeroAddress(); if (_to == address(0)) revert TransferToZeroAddress(); uint256 fromBalance = _balances[owner]; if (fromBalance < _amount) { revert TransferAmountExceedsBalance(owner, fromBalance, _amount); } _balances[owner] = fromBalance - _amount; _balances[_to] += _amount; return true; } function transferFrom( address _from, address _to, uint256 _amount ) external returns (bool) { if (_from == address(0)) revert TransferFromZeroAddress(); if (_to == address(0)) revert TransferToZeroAddress(); uint256 currentAllowance = _allowances[_from][msg.sender]; if (currentAllowance < _amount) { revert InsufficientAllowance(_from, msg.sender, currentAllowance, _amount); } _allowances[_from][msg.sender] = currentAllowance - _amount; uint256 fromBalance = _balances[_from]; if (fromBalance < _amount) { revert TransferAmountExceedsBalance(_from, fromBalance, _amount); } _balances[_from] = fromBalance - _amount; _balances[_to] += _amount; return true; } function mint(address _to, uint256 _amount) external { if (_to == address(0)) revert MintToZeroAddress(); _totalSupply += _amount; _balances[_to] += _amount; } function burn(uint256 _amount) external { address account = msg.sender; if (account == address(0)) revert BurnFromZeroAddress(); uint256 accountBalance = _balances[account]; if (accountBalance < _amount) { revert BurnAmountExceedsBalance(account, accountBalance, _amount); } _balances[account] = accountBalance - _amount; _totalSupply -= _amount; } }
6. try/catch 异常处理
soliditycontract TryCatchExample { // 外部合约接口 interface IExternalContract { function riskyOperation(uint256 _value) external returns (uint256); function anotherFunction() external view returns (bool); } // 自定义错误 error ExternalCallFailed(address target, bytes reason); error ExternalCallReverted(address target, string reason); error ExternalCallPanic(address target, uint256 code); // 使用 try/catch 处理外部调用 function safeExternalCall( address _target, uint256 _value ) external returns (bool success, uint256 result) { try IExternalContract(_target).riskyOperation(_value) returns (uint256 _result) { // 调用成功 return (true, _result); } catch Error(string memory reason) { // 捕获 revert("string") 错误 revert ExternalCallReverted(_target, reason); } catch Panic(uint256 errorCode) { // 捕获 assert 失败或内部错误 // errorCode: // 0x01: assert 失败 // 0x11: 算术溢出/下溢 // 0x12: 除以零 // 0x21: 转换为无效枚举 // 0x22: 访问错误编码的 storage 字节数组 // 0x31: 空数组 pop // 0x32: 数组越界访问 // 0x41: 内存分配过多 // 0x51: 调用未初始化的内部函数 revert ExternalCallPanic(_target, errorCode); } catch (bytes memory lowLevelData) { // 捕获自定义错误或其他低级错误 revert ExternalCallFailed(_target, lowLevelData); } } // 批量调用,部分失败不影响其他 function batchExternalCalls( address[] calldata _targets, uint256[] calldata _values ) external returns (bool[] memory successes, uint256[] memory results) { require(_targets.length == _values.length, "Length mismatch"); successes = new bool[](_targets.length); results = new uint256[](_targets.length); for (uint i = 0; i < _targets.length; i++) { try IExternalContract(_targets[i]).riskyOperation(_values[i]) returns (uint256 result) { successes[i] = true; results[i] = result; } catch { // 记录失败,继续执行其他调用 successes[i] = false; results[i] = 0; } } return (successes, results); } // 带重试的外部调用 function callWithRetry( address _target, uint256 _value, uint256 _maxAttempts ) external returns (bool success, uint256 result) { for (uint i = 0; i < _maxAttempts; i++) { try IExternalContract(_target).riskyOperation(_value) returns (uint256 _result) { return (true, _result); } catch { // 重试前等待(实际应用中可能需要更复杂的逻辑) if (i == _maxAttempts - 1) { return (false, 0); } } } return (false, 0); } // 带超时检查的外部调用 function callWithTimeout( address _target, uint256 _value, uint256 _deadline ) external returns (bool success, uint256 result) { require(block.timestamp < _deadline, "Deadline passed"); try IExternalContract(_target).riskyOperation(_value) returns (uint256 _result) { return (true, _result); } catch { return (false, 0); } } }
7. 错误处理最佳实践
soliditycontract ErrorHandlingBestPractices { /* 最佳实践总结: 1. require 使用场景: - 验证函数输入参数 - 检查外部条件 - 验证返回值 2. assert 使用场景: - 检查内部不变量 - 验证数学运算结果 - 检查不应该发生的情况 3. revert 使用场景: - 复杂的条件判断 - 需要详细错误信息 - Gas 优化(自定义错误) 4. try/catch 使用场景: - 外部合约调用 - 需要优雅处理失败 - 批量操作 */ // 错误定义 error InvalidInput(string param, string reason); error StateInvariantViolation(string invariant); error ExternalDependencyFailed(address dependency); uint256 public constant MAX_VALUE = 10000; uint256 public constant MIN_VALUE = 1; uint256 public totalValue; mapping(address => uint256) public values; // 综合示例 function complexOperation( address _externalContract, uint256 _input, address _recipient ) external { // 1. 输入验证(require) require(_externalContract != address(0), "Invalid contract"); require(_recipient != address(0), "Invalid recipient"); // 使用自定义错误进行详细验证 if (_input < MIN_VALUE || _input > MAX_VALUE) { revert InvalidInput( "_input", "Must be between MIN_VALUE and MAX_VALUE" ); } // 2. 外部调用(try/catch) uint256 externalResult; try IExternal(_externalContract).getValue() returns (uint256 value) { externalResult = value; } catch Error(string memory reason) { revert ExternalDependencyFailed(_externalContract); } catch { revert ExternalDependencyFailed(_externalContract); } // 3. 计算和状态更新 uint256 oldTotal = totalValue; uint256 newValue = _input + externalResult; values[_recipient] += newValue; totalValue += newValue; // 4. 不变量检查(assert) assert(totalValue >= values[_recipient]); assert(totalValue == oldTotal + newValue); assert(values[_recipient] >= newValue); } } interface IExternal { function getValue() external view returns (uint256); }
8. 错误处理模式对比
soliditycontract ErrorComparison { /* 三种错误处理方式的对比: | 特性 | require | assert | revert | |------|---------|--------|--------| | 使用场景 | 输入验证 | 内部不变量 | 复杂条件 | | Gas 退还 | 是 | 否 | 是 | | 错误信息 | 字符串 | 无 | 自定义错误 | | Gas 消耗 | 中等 | 高 | 低(自定义错误)| | 严重性 | 一般 | 严重(bug)| 一般 | 推荐使用场景: - 频繁调用的函数:使用自定义错误 - 输入验证:使用 require - 内部检查:使用 assert - 外部调用:使用 try/catch */ // 示例:根据不同场景选择错误处理方式 // 场景 1:输入验证 - 使用 require function deposit(uint256 _amount) external { require(_amount > 0, "Amount must be positive"); // ... } // 场景 2:Gas 优化 - 使用自定义错误 error InvalidAmount(uint256 provided, uint256 minimum); function optimizedDeposit(uint256 _amount) external { if (_amount < 100) { revert InvalidAmount(_amount, 100); } // ... } // 场景 3:内部不变量 - 使用 assert function internalOperation() external { uint256 before = totalSupply; // ... 复杂操作 assert(totalSupply >= before); // 不应该减少 } uint256 public totalSupply; }
9. 实际项目中的错误处理
solidity// OpenZeppelin 风格的错误处理 contract ProductionGradeErrors { // 定义模块化的错误 // 通用错误 error ZeroAddress(); error ZeroAmount(); error ExceedsMax(uint256 provided, uint256 max); error InsufficientBalance(uint256 available, uint256 required); error Unauthorized(address caller); error AlreadyInitialized(); error NotInitialized(); // 特定功能错误 error TransferFailed(address from, address to, uint256 amount); error SwapFailed(address tokenIn, address tokenOut, uint256 amount); error StakingPeriodNotEnded(uint256 endTime, uint256 currentTime); error CooldownPeriodActive(uint256 cooldownEnd); // 状态变量 bool private _initialized; address public owner; mapping(address => uint256) public balances; mapping(address => uint256) public stakingEndTime; uint256 public constant COOLDOWN_PERIOD = 7 days; modifier onlyOwner() { if (msg.sender != owner) revert Unauthorized(msg.sender); _; } modifier initializer() { if (_initialized) revert AlreadyInitialized(); _initialized = true; _; } // 初始化函数 function initialize(address _owner) external initializer { if (_owner == address(0)) revert ZeroAddress(); owner = _owner; } // 存款函数 function deposit(uint256 _amount) external { if (_amount == 0) revert ZeroAmount(); balances[msg.sender] += _amount; } // 取款函数 function withdraw(uint256 _amount) external { uint256 balance = balances[msg.sender]; if (balance < _amount) { revert InsufficientBalance(balance, _amount); } // 检查冷却期 uint256 cooldownEnd = stakingEndTime[msg.sender] + COOLDOWN_PERIOD; if (block.timestamp < cooldownEnd) { revert CooldownPeriodActive(cooldownEnd); } balances[msg.sender] = balance - _amount; (bool success, ) = msg.sender.call{value: _amount}(""); if (!success) { revert TransferFailed(address(this), msg.sender, _amount); } } // 质押函数 function stake(uint256 _amount, uint256 _duration) external { if (_amount == 0) revert ZeroAmount(); uint256 balance = balances[msg.sender]; if (balance < _amount) { revert InsufficientBalance(balance, _amount); } uint256 maxDuration = 365 days; if (_duration > maxDuration) { revert ExceedsMax(_duration, maxDuration); } balances[msg.sender] = balance - _amount; stakingEndTime[msg.sender] = block.timestamp + _duration; } // 解质押函数 function unstake() external { uint256 endTime = stakingEndTime[msg.sender]; if (endTime == 0) revert NotInitialized(); if (block.timestamp < endTime) { revert StakingPeriodNotEnded(endTime, block.timestamp); } // 执行解质押... } }
10. 总结
Solidity 的错误处理机制各有特点:
-
require:
- 用于输入验证和外部条件检查
- 提供清晰的错误信息
- 退还剩余 Gas
-
assert:
- 用于内部不变量检查
- 失败表示代码有 bug
- 消耗所有 Gas(严重错误)
-
revert + 自定义错误:
- Solidity 0.8.4+ 推荐
- 显著节省 Gas
- 支持参数化错误信息
-
try/catch:
- 处理外部调用异常
- 支持优雅降级
- 批量操作必备
-
最佳实践:
- 频繁调用的函数使用自定义错误
- 输入验证使用 require
- 内部检查使用 assert
- 外部调用使用 try/catch
- 设计清晰、模块化的错误体系