// 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;
}
}