合约有功能:
其它特点:
//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
);
}
}
南风先生,这个合约整体上功能较为完备,但也存在一些潜在的问题和改进空间:
computeReceiptKey
函数的冲突问题:
computeReceiptKey
函数使用的是keccak256(abi.encode(customer, counter + block.number))
来生成存款凭证的编码,尽管加入了counter
和block.number
,但在高并发情况下仍有可能发生碰撞,因为block.number
在一个区块内是相同的。尤其是在同一个区块内有多个交易时,可能会有两个不同的客户产生相同的receiptKey
。虽然合约检查了冲突(require(!hasReceipt[receiptKey], "receipt key collision")
),但这种情况仍然可能导致存款失败。建议: 可以使用counter
的值直接作为receiptKey
的一部分来减少冲突的可能性,或在计算时结合更多的信息,如block.timestamp
或nonce
等。
锁定时间和区块时间依赖:
getUnlockTime
函数依赖于block.timestamp
来计算解锁时间,但区块时间的操控性可能会使得这个方法产生一些不确定性,例如矿工可能故意操控block.timestamp
,从而影响解锁时间的准确性。建议: 如果对精确的时间要求较高,可以考虑引入更加稳定的时间源或合理的时间延迟策略来缓解这一问题。
循环中的Gas问题:
getDeadFund
函数中,通过遍历receiptKeys
数组来检查和累加已过期未取款的资金。这种方式在数组规模较大时会消耗大量的Gas,可能会导致函数调用失败。建议: 需要考虑通过优化数据结构或分批处理来减少每次循环消耗的Gas,例如分步执行或将处理过程分解为多个事务。
未使用的导入:
SafeMath
和Strings
,但代码中并未使用到这两个库。这会增加合约代码的复杂度和部署成本。建议: 如果不需要使用这些库,可以将它们移除,以减小合约的字节码体积和部署成本。
缺少应急停止功能:
circuit breaker
),如果发生安全漏洞或紧急情况,owner无法快速停止合约的主要功能。建议: 可以考虑增加应急停止功能(例如使用Pausable
),以应对紧急情况。
合约的兼容性问题:
0.8.25
,这可能会导致将来合约无法兼容更新的Solidity版本,尤其是在新版本引入重要改进或修复时。建议: 可以使用更宽松的版本声明(例如^0.8.0
)来增加兼容性,或在必要时升级代码以支持新版本。