Solidity 合约安全优化:SimpleDeposit 代码审查

一份简单的 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防重入攻击