一份简单的 EVM 充值合约,在上主网前需要做几项安全优化。
原始合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
contract SimpleDeposit {
address public owner;
bool public paused;
event Deposit(
address indexed user,
uint256 amount,
uint256 indexed orderId,
uint256 timestamp
);
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
constructor() {
owner = msg.sender;
}
function deposit(uint256 orderId) public payable {
require(!paused, "Paused");
require(msg.value > 0, "Amount zero");
emit Deposit(msg.sender, msg.value, orderId, block.timestamp);
}
function withdraw(uint256 amount) external onlyOwner {
(bool success, ) = payable(owner).call{value: amount}("");
require(success, "Transfer failed.");
}
function setPaused(bool _paused) external onlyOwner {
paused = _paused;
}
receive() external payable {
emit Deposit(msg.sender, msg.value, 0, block.timestamp);
}
}
优化 1:withdraw 前检查余额
余额不足时 call 会失败,但报错信息不明确:
function withdraw(uint256 amount) external onlyOwner {
require(address(this).balance >= amount, "Insufficient balance");
(bool success, ) = payable(owner).call{value: amount}("");
require(success, "Transfer failed");
}
优化 2:补充 Withdraw 事件
充值有事件,提现没有事件,后端无法追踪提现记录:
event Withdraw(
address indexed to,
uint256 amount,
uint256 timestamp
);
function withdraw(uint256 amount) external onlyOwner {
require(address(this).balance >= amount, "Insufficient balance");
(bool success, ) = payable(owner).call{value: amount}("");
require(success, "Transfer failed");
emit Withdraw(owner, amount, block.timestamp);
}
优化 3:自定义 error 替代 require 字符串
require("string") 会把字符串编码进 calldata,消耗更多 gas。用 custom error 替代:
error NotOwner();
error ContractPaused();
error ZeroAmount();
error InsufficientBalance();
error TransferFailed();
modifier onlyOwner() {
if (msg.sender != owner) revert NotOwner();
_;
}
function deposit(uint256 orderId) public payable {
if (paused) revert ContractPaused();
if (msg.value == 0) revert ZeroAmount();
emit Deposit(msg.sender, msg.value, orderId, block.timestamp);
}
自定义 error 平均节省 30~50% gas。
优化 4:owner 转移功能
现在 owner 一旦设置就无法更改,新增两步转移以防误操作:
address public pendingOwner;
event OwnershipTransferred(address indexed oldOwner, address indexed newOwner);
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "Zero address");
pendingOwner = newOwner;
}
function acceptOwnership() external {
require(msg.sender == pendingOwner, "Not pending owner");
emit OwnershipTransferred(owner, pendingOwner);
owner = pendingOwner;
pendingOwner = address(0);
}
优化 5:ReentrancyGuard 防重入
withdraw 使用 call,存在重入风险(尽管对当前合约逻辑影响有限):
bool private _locked;
modifier nonReentrant() {
require(!_locked, "Reentrant call");
_locked = true;
_;
_locked = false;
}
function withdraw(uint256 amount) external onlyOwner nonReentrant {
require(address(this).balance >= amount, "Insufficient balance");
(bool success, ) = payable(owner).call{value: amount}("");
require(success, "Transfer failed");
emit Withdraw(owner, amount, block.timestamp);
}
或者直接引入 OpenZeppelin:
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract SimpleDeposit is ReentrancyGuard {
// ...
function withdraw(uint256 amount) external onlyOwner nonReentrant { ... }
}
可升级代理(按需)
如果合约逻辑以后可能更新,考虑 UUPS 代理模式。但对于简单充值合约,升级能力也意味着合约不再是纯粹的”不可更改”,用户信任模型会变化,需要权衡。
最终版本概览
| 优化点 | 作用 |
|---|---|
| withdraw 余额检查 | 明确报错,节省 gas |
| Withdraw 事件 | 链上可追踪提现记录 |
| Custom error | 减少 gas 30~50% |
| 两步 owner 转移 | 防止误转到无效地址 |
| ReentrancyGuard | 防重入攻击 |
