30 帮忙查看这个合约存在哪些安全风险?

合约有功能:

  • 可以存入ETH和erc20代币(分别调用接口depNativeToken和depErc20Token),存入时需要指定存款时间,获得一个存款凭证。存款时间到了之后,存款人持存款凭证里的编码取回资金(调用refund接口),取回的资金返回原地址。
  • 不经过存款接口转到该合约地址的资金,可以由owner取出(调用getDeadFund接口)。
  • 操作成功后会生成对应的事件,如存款、取款等操作。

其它特点:

  • 为了保障安全,用修饰器isEoaAddress使得所有接口只接受EOA地址的调用,不接受合约地址的调用。
  • 函数computeReceiptKey用于生成存款凭证的编码,由存款地址、计数器和区块号生成;
  • 存款时间小于10年;
  • owner可以转移;
//SPDX-License-Identifier: MIT
pragma solidity 0.8.25;

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";

contract TimeLockBox {
    address public owner;
    uint128 public counter;
    mapping(address => Receipt) private receiptRepo;
    mapping(address => bool) private hasReceipt;
    address[] private receiptKeys;
    struct Receipt {
        address customer;
        // address(0) in this contract means native token
        address token;
        uint256 amount;
        uint256 unlockTime;
        bool isNativeToken;
    }

    using SafeERC20 for IERC20;

    event Deposit(
        address receiptKey,
        address customer,
        address token,
        uint256 amount,
        uint256 lockDays,
        uint256 unlockTime
    );
    event Withdraw(
        address receiptKey,
        address customer,
        address token,
        uint256 amount,
        uint256 time
    );
    event NewOwner(address oldOwner, address newOwner);

    constructor() {
        owner = msg.sender;
        counter = 0;
    }

    function computeReceiptKey(address customer)
        private
        view
        returns (address)
    {
        return
            address(
                uint160(
                    uint256(
                        keccak256(abi.encode(customer, counter + block.number))
                    )
                )
            );
    }

    modifier isEoaAddress(address sender) {
        require(tx.origin == sender, "not support contract address");
        _;
    }

    modifier isLegalLockdays(uint256 lockDays) {
        require(
            (lockDays > 0) && (lockDays <= 3650),
            "lockDays is too large or small"
        );
        _;
    }

    function getUnlockTime(uint256 lockDays) private view returns (uint256) {
        return lockDays * 86400 + block.timestamp;
    }

    function incrementCounter() private {
        unchecked {
            counter = counter + 1;
        }
    }

    function changeOwner(address newOwner) external {
        require(msg.sender == owner, "only owner can change owner");
        owner = newOwner;
        emit NewOwner(msg.sender, owner);
    }

    function getReceipt(address receiptKey) external {
        Receipt memory receipt = receiptRepo[receiptKey];
        emit Deposit(
            receiptKey,
            receipt.customer,
            receipt.token,
            receipt.amount,
            0,
            receipt.unlockTime
        );
    }

    function depNativeToken(uint256 lockDays)
        external
        payable
        isEoaAddress(msg.sender)
        isLegalLockdays(lockDays)
    {
        require(msg.value > 0, "native token amount <= 0");
        incrementCounter();
        uint256 unlockTime = getUnlockTime(lockDays);
        Receipt memory receipt = Receipt(
            msg.sender,
            address(0),
            msg.value,
            unlockTime,
            true
        );
        address receiptKey = computeReceiptKey(receipt.customer);
        require(!hasReceipt[receiptKey], "receipt key collision");
        receiptRepo[receiptKey] = receipt;
        hasReceipt[receiptKey] = true;
        receiptKeys.push(receiptKey);

        emit Deposit(
            receiptKey,
            msg.sender,
            address(0),
            msg.value,
            lockDays,
            unlockTime
        );
    }

    function depErc20Token(
        address token,
        uint256 amount,
        uint256 lockDays
    ) external isEoaAddress(msg.sender) isLegalLockdays(lockDays) {
        require(amount > 0, "token amount <= 0");
        incrementCounter();

        uint256 unlockTime = getUnlockTime(lockDays);
        Receipt memory receipt = Receipt(
            msg.sender,
            token,
            amount,
            unlockTime,
            false
        );
        address receiptKey = computeReceiptKey(receipt.customer);
        require(!hasReceipt[receiptKey], "receipt key collision");
        receiptRepo[receiptKey] = receipt;
        hasReceipt[receiptKey] = true;
        receiptKeys.push(receiptKey);

        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);

        emit Deposit(
            receiptKey,
            msg.sender,
            token,
            amount,
            lockDays,
            unlockTime
        );
    }

    function refund(address receiptKey) isEoaAddress(msg.sender) external {
        require(hasReceipt[receiptKey], "has not receipt or already drawn");

        Receipt memory receipt = receiptRepo[receiptKey];
        require(
            msg.sender == receipt.customer,
            "can only refund your own receipt"
        );

        require(
            receipt.unlockTime < block.timestamp,
            "unlock time not reached"
        );
        hasReceipt[receiptKey] = false;

        if (receipt.isNativeToken) {
            payable(receipt.customer).transfer(receipt.amount);
        } else {
            IERC20(receipt.token).safeTransfer(
                receipt.customer,
                receipt.amount
            );
        }
        emit Withdraw(
            receiptKey,
            receipt.customer,
            receipt.token,
            receipt.amount,
            block.timestamp
        );
    }

    function getDeadFund(address deadToken) isEoaAddress(msg.sender) external {
        require(msg.sender == owner, "only owner can get dead fund");

        uint256 tokenDepAmount = 0;
        Receipt memory receipt;
        for (uint256 i = 0; i < receiptKeys.length; i++) {
            address receiptKey = receiptKeys[i];
            receipt = receiptRepo[receiptKey];
            if (hasReceipt[receiptKey] && receipt.token == deadToken) {
                tokenDepAmount = tokenDepAmount + receipt.amount;
            }
        }

        uint256 deadAmount;
        if (deadToken == address(0)) {
            deadAmount = address(this).balance - tokenDepAmount;
            require(deadAmount > 0, "amount of dead fund must > 0");
            payable(owner).transfer(deadAmount);
        } else {
            deadAmount = IERC20(deadToken).balanceOf(address(this)) - tokenDepAmount;
            require(deadAmount > 0, "amount of dead fund must > 0");
            IERC20(deadToken).safeTransfer(owner, deadAmount);
        }

        emit Withdraw(
            address(0),
            owner,
            deadToken,
            deadAmount,
            block.timestamp
        );
    }
}
请先 登录 后评论

最佳答案 2024-08-27 15:15

南风先生,这个合约整体上功能较为完备,但也存在一些潜在的问题和改进空间:

  1. computeReceiptKey函数的冲突问题:

    • computeReceiptKey函数使用的是keccak256(abi.encode(customer, counter + block.number))来生成存款凭证的编码,尽管加入了counterblock.number,但在高并发情况下仍有可能发生碰撞,因为block.number在一个区块内是相同的。尤其是在同一个区块内有多个交易时,可能会有两个不同的客户产生相同的receiptKey。虽然合约检查了冲突(require(!hasReceipt[receiptKey], "receipt key collision")),但这种情况仍然可能导致存款失败。

    建议: 可以使用counter的值直接作为receiptKey的一部分来减少冲突的可能性,或在计算时结合更多的信息,如block.timestampnonce等。

  2. 锁定时间和区块时间依赖:

    • getUnlockTime函数依赖于block.timestamp来计算解锁时间,但区块时间的操控性可能会使得这个方法产生一些不确定性,例如矿工可能故意操控block.timestamp,从而影响解锁时间的准确性。

    建议: 如果对精确的时间要求较高,可以考虑引入更加稳定的时间源或合理的时间延迟策略来缓解这一问题。

  3. 循环中的Gas问题:

    • getDeadFund函数中,通过遍历receiptKeys数组来检查和累加已过期未取款的资金。这种方式在数组规模较大时会消耗大量的Gas,可能会导致函数调用失败。

    建议: 需要考虑通过优化数据结构或分批处理来减少每次循环消耗的Gas,例如分步执行或将处理过程分解为多个事务。

  4. 未使用的导入:

    • 合约中导入了SafeMathStrings,但代码中并未使用到这两个库。这会增加合约代码的复杂度和部署成本。

    建议: 如果不需要使用这些库,可以将它们移除,以减小合约的字节码体积和部署成本。

  5. 缺少应急停止功能:

    • 合约中没有设计应急停止机制(例如circuit breaker),如果发生安全漏洞或紧急情况,owner无法快速停止合约的主要功能。

    建议: 可以考虑增加应急停止功能(例如使用Pausable),以应对紧急情况。

  6. 合约的兼容性问题:

    • 合约硬编码了Solidity编译器版本为0.8.25,这可能会导致将来合约无法兼容更新的Solidity版本,尤其是在新版本引入重要改进或修复时。

    建议: 可以使用更宽松的版本声明(例如^0.8.0)来增加兼容性,或在必要时升级代码以支持新版本。

请先 登录 后评论

其它 2 个回答

Tiny熊
  擅长:智能合约,以太坊
请先 登录 后评论
BY_DLIFE
请先 登录 后评论
  • 3 关注
  • 0 收藏,669 浏览
  • rabbitHello 提出于 2024-08-09 15:33