// RedPacketRoom.sol // 去中心化红包房间:N-1 记录,最后一人触发批量结算 // 说明:该合约支持基于 ERC-20 资产的红包房间。主持人先 approve 后,调用 deployAndFund 初始化房间并将资金转入合约。 // N-1 用户只记录领取(低 Gas),最后一位用户调用 finalizeAndDistribute 完成最后领取并批量转账(高 Gas)。 // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.20; interface IERC20 { function transfer(address to, uint256 amount) external returns (bool); function approve(address spender, uint256 amount) external returns (bool); function transferFrom(address from, address to, uint256 amount) external returns (bool); } contract RedPacketRoom { // 简易重入保护 uint256 private _locked = 1; modifier nonReentrant() { require(_locked == 1, "REENTRANCY"); _locked = 2; _; _locked = 1; } struct Packet { address host; address token; uint256 totalAmount; uint256 totalPacks; uint256 packsClaimed; bool isRandom; bool finalized; mapping(address => bool) claimed; address[] claimers; } mapping(bytes32 => Packet) private packets; mapping(bytes32 => bool) private packetExists; event PacketDeployed(bytes32 indexed packetId, address indexed host, address indexed token, uint256 totalAmount, uint256 totalPacks, bool isRandom); event Claimed(bytes32 indexed packetId, address indexed claimer, uint256 index); event Finalized(bytes32 indexed packetId, uint256 perPackAmount, uint256 totalPacks); error PacketNotFound(); error AlreadyFinalized(); error AlreadyClaimed(); error NotEnoughPacks(); error InvalidHost(); error InvalidInput(); // 主持人初始化红包:需要先在 token 合约里 approve 本合约地址为 spender,金额为 totalAmount function deployAndFund(bytes32 packetId, address token, uint256 totalAmount, uint256 totalPacks, bool isRandom) external nonReentrant { if (packetExists[packetId]) revert InvalidInput(); if (token == address(0) || totalAmount == 0 || totalPacks == 0) revert InvalidInput(); // 创建包 Packet storage p = packets[packetId]; p.host = msg.sender; p.token = token; p.totalAmount = totalAmount; p.totalPacks = totalPacks; p.packsClaimed = 0; p.isRandom = isRandom; p.finalized = false; packetExists[packetId] = true; // 将资金从主持人转入合约 bool ok = IERC20(token).transferFrom(msg.sender, address(this), totalAmount); require(ok, "TRANSFER_FROM_FAIL"); emit PacketDeployed(packetId, msg.sender, token, totalAmount, totalPacks, isRandom); } // N-1 记录领取(低 Gas),不发生转账 function recordClaim(bytes32 packetId) external nonReentrant { if (!packetExists[packetId]) revert PacketNotFound(); Packet storage p = packets[packetId]; if (p.finalized) revert AlreadyFinalized(); if (p.claimed[msg.sender]) revert AlreadyClaimed(); // 只能在还没到最后一个人时记录 if (p.packsClaimed >= p.totalPacks - 1) revert NotEnoughPacks(); p.claimed[msg.sender] = true; p.claimers.push(msg.sender); p.packsClaimed += 1; emit Claimed(packetId, msg.sender, p.claimers.length - 1); } // 最后一位领取者:同时记录并结算(批量转账) function finalizeAndDistribute(bytes32 packetId) external nonReentrant { if (!packetExists[packetId]) revert PacketNotFound(); Packet storage p = packets[packetId]; if (p.finalized) revert AlreadyFinalized(); if (p.claimed[msg.sender]) revert AlreadyClaimed(); // 必须恰好已有 N-1 人记录 require(p.packsClaimed == p.totalPacks - 1, "NOT_LAST"); // 记录最后一人 p.claimed[msg.sender] = true; p.claimers.push(msg.sender); p.packsClaimed += 1; // == totalPacks // 事件需要 perPackAmount 参数;随机分配场景下没有“等额”,统一按 0 输出 uint256 perPack = 0; if (p.isRandom) { // 随机分配:使用伪随机权重,按比例分配,余数给最后一人 uint256 n = p.claimers.length; uint256[] memory weights = new uint256[](n); uint256 sumW = 0; for (uint256 i = 0; i < n; i++) { bytes32 h = keccak256(abi.encode(packetId, i, p.claimers[i], blockhash(block.number - 1), block.timestamp)); uint256 w = (uint256(h) % 10000) + 1; // 1..10000,避免 0 weights[i] = w; sumW += w; } uint256 distributed = 0; for (uint256 i = 0; i < n; i++) { uint256 amount = (p.totalAmount * weights[i]) / sumW; if (i == n - 1) { // 最后一人加上余数,确保总和等于 totalAmount amount = p.totalAmount - distributed; } else { distributed += amount; } bool ok = IERC20(p.token).transfer(p.claimers[i], amount); require(ok, "TRANSFER_FAIL"); } } else { // 固定等额分配 perPack = p.totalAmount / p.totalPacks; uint256 remainder = p.totalAmount - (perPack * p.totalPacks); for (uint256 i = 0; i < p.claimers.length; i++) { bool ok = IERC20(p.token).transfer(p.claimers[i], perPack); require(ok, "TRANSFER_FAIL"); } if (remainder > 0) { bool ok2 = IERC20(p.token).transfer(p.host, remainder); require(ok2, "TRANSFER_REMAINDER_FAIL"); } } p.finalized = true; emit Finalized(packetId, perPack, p.totalPacks); } // 读取状态(前端用) function getPacketStats(bytes32 packetId) external view returns ( address host, address token, uint256 totalAmount, uint256 totalPacks, uint256 packsClaimed, bool finalized, bool isRandom ) { if (!packetExists[packetId]) revert PacketNotFound(); Packet storage p = packets[packetId]; return (p.host, p.token, p.totalAmount, p.totalPacks, p.packsClaimed, p.finalized, p.isRandom); } function hasClaimed(bytes32 packetId, address user) external view returns (bool) { if (!packetExists[packetId]) revert PacketNotFound(); return packets[packetId].claimed[user]; } function getClaimersCount(bytes32 packetId) external view returns (uint256) { if (!packetExists[packetId]) revert PacketNotFound(); return packets[packetId].claimers.length; } }