Solidity合约:事件触发时从管理员存款向用户正确发放ETH及bug排查
核心Bug定位
- 收益转账目标完全写反:触发价格条件时,代码将2倍锁仓额的收益转给了管理员
payable(manager).transfer(amountOfToken),而非取款用户,这是用户无法拿到额外收益的最直接原因 - 价格取数逻辑不符合业务要求:全局变量
ethPrice仅在用户存款时更新一次,取款时没有重新拉取Chainlink预言机的锁仓到期时刻最新价格,且全局单值会被多用户的存款操作覆盖,根本无法作为收益发放的判断依据 - 用户锁仓数据存储结构错误:
deadline(锁仓到期时间)等核心字段使用全局单变量存储,多用户存款时,后一个用户的数据会直接覆盖前一个用户的所有锁仓信息,仅最后一个存款的用户数据能被正常读取 - 管理员保证金逻辑完全失效:管理员补充保证金的
depositVault函数仅做了身份校验,没有将转入的msg.value计入保证金池余额,发放收益时也没有校验保证金是否充足、没有做对应余额扣减 - 存在大量无效冗余代码:合约顶层定义的
lockAmount、startTime全局变量属于无效代码,既不在函数内执行,也无实际业务作用;startTimes映射用时间戳作为key,完全不符合按用户维度存储数据的需求 - 基础逻辑漏洞:原代码未校验用户是否存在有效锁仓、是否已经提取过资金,存在重复取款风险;使用
transfer方法硬编码2300gas限制,在部分网络升级后容易出现转账失败问题;价格判断逻辑放在到期时间校验之前,执行顺序不合理。
正确实现代码
// SPDX-License-Identifier: MIT pragma solidity ^0.8.6; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; contract EthVault { // 管理员地址 address public immutable manager; // 管理员保证金总余额 uint256 public managerDepositBalance; // Chainlink ETH/USD预言机地址(Kovan测试网) AggregatorV3Interface internal immutable priceFeed; // 价格触发阈值:2000美元,预言机精度为8位,对应值为2000 * 1e8 int256 public constant PRICE_THRESHOLD = 2000 * 100000000; // 用户锁仓信息结构 struct LockInfo { uint256 amount; // 锁仓ETH金额 uint256 deadline; // 锁仓到期时间戳 bool isWithdrawn; // 是否已取款 } // 按用户地址映射锁仓信息 mapping(address => LockInfo) public userLockInfo; constructor() payable { manager = msg.sender; managerDepositBalance = msg.value; // 初始化Kovan网ETH/USD预言机 priceFeed = AggregatorV3Interface(0x9326BFA02ADD2366b30bacB125260Af641031331); } // 管理员补充保证金 function addManagerDeposit() external payable { require(msg.sender == manager, "Only manager"); require(msg.value > 0, "Deposit amount must be greater than 0"); managerDepositBalance += msg.value; } // 用户锁仓ETH function lockEth(uint256 lockSeconds) external payable { require(msg.value > 0, "Lock amount must be greater than 0"); require(lockSeconds > 0, "Lock period must be greater than 0"); LockInfo storage info = userLockInfo[msg.sender]; require(info.amount == 0, "You already have an active lock"); info.amount = msg.value; info.deadline = block.timestamp + lockSeconds; info.isWithdrawn = false; } // 用户取款 function withdraw() external { LockInfo storage info = userLockInfo[msg.sender]; require(info.amount > 0, "No active lock"); require(!info.isWithdrawn, "Already withdrawn"); require(block.timestamp >= info.deadline, "Lock not expired"); // 拉取到期时刻最新ETH价格 (,int256 latestPrice,,,) = priceFeed.latestRoundData(); require(latestPrice > 0, "Invalid price feed data"); uint256 totalPay = info.amount; // 价格低于2000美元时,加发2倍本金的收益 if (latestPrice < PRICE_THRESHOLD) { uint256 reward = info.amount * 2; require(managerDepositBalance >= reward, "Insufficient reward balance"); // 从保证金中扣除收益 managerDepositBalance -= reward; totalPay += reward; } // 先标记为已取款,防重入 info.isWithdrawn = true; info.amount = 0; // 执行转账,用call替代transfer避免gas限制问题 (bool success,) = payable(msg.sender).call{value: totalPay}(""); require(success, "Transfer failed"); } // 接收ETH的兜底函数 receive() external payable {} }
优化建议
- 加重入锁:建议引入重入保护修饰器,所有涉及外部转账的函数优先执行状态修改,再做外部调用,进一步降低重入攻击风险
- 加预言机数据校验:读取预言机价格时,需要校验价格返回的时间戳,若最新价格更新时间超过预设阈值(比如1小时),则暂停取款逻辑,避免使用过期价格造成资金损失
- 动态适配预言机精度:不要硬编码价格精度,可通过预言机的
decimals()方法动态读取精度值计算价格阈值,方便合约部署到不同网络时适配不同预言机配置 - 加紧急管控能力:可增加管理员紧急暂停、提取剩余保证金的功能,在出现预言机故障、合约漏洞时及时止损
- 去掉不必要的payable修饰:
withdraw等不需要用户转入资金的函数不要加payable,避免用户误转资金。
内容的提问来源于stack exchange,提问作者Ale Ale
相关产品推荐
相关产品推荐

