Fully open source · On-chain verifiable
Deposit escrow, commitment binding, random number generation, fund distribution —
all automatically executed by the smart contract, no trusted intermediary required.
Contract Address
0x0717DEB12839FE8D0DA8c6869E66d658ECc0b240
Etherscan ↗
[01] — Module Architecture
[02] — Core Functions
Partner creates a group-buy game. Locks the full deposit (ticket price × quantity) and simultaneously submits a hash commitment — both atomically bound and inseparable.
function createSession(SessionConfig memory partialConfig) returns (address) {
if (partialConfig.sessionCommitment == bytes32(0)) revert MissingCommitment();
uint16 totalFee = partialConfig.partnerShareBps + platformFeeBps;
if (totalFee > BPS_DENOMINATOR) revert FeeTooHigh(totalFee);
uint256 requiredDeposit = partialConfig.ticketPrice * partialConfig.totalTickets;
uint256 currentBalance = treasury.balances(msg.sender);
if (currentBalance < requiredDeposit) {
revert InsufficientDeposit(currentBalance, requiredDeposit);
}
partialConfig.admin = admin;
partialConfig.creator = msg.sender;
partialConfig.treasury = payable(address(treasury));
partialConfig.platformFeeBps = platformFeeBps;
partialConfig.creatorAbsentPartnerDepositSlashBps = creatorAbsentPartnerDepositSlashBps;
CommitRevealSession session = new CommitRevealSession(partialConfig);
treasury.registerSession(address(session), msg.sender);
treasury.lockPartnerDeposit(address(session), msg.sender, requiredDeposit);
emit SessionCreated(msg.sender, address(session), partialConfig);
return address(session);
}
Player joins the game. A single atomic action simultaneously submits ticket payment and personal random number. No second Reveal step required, completely eliminating the second-mover attack window.
function playerBuyAndCommitTicket(uint256 quantity, bytes32 secret, bool useBalance) external payable {
uint256 endTimestamp = unlockTimestamp + commitDurationSeconds;
if (block.timestamp < unlockTimestamp) {
revert TimeConstraintError(block.timestamp, unlockTimestamp);
}
if (block.timestamp > endTimestamp) {
revert TimeConstraintError(block.timestamp, endTimestamp);
}
if (quantity == 0) revert InvalidZeroInput();
if (nextTicketIndex + quantity > totalTickets) revert SoldOut(nextTicketIndex, quantity);
uint256 cost = ticketPrice * quantity;
if (useBalance) {
uint256 treasuryBalance = treasury.balances(msg.sender);
if (treasuryBalance < cost) revert IncorrectETHAmount(cost, treasuryBalance);
treasury.playerPayTicketUseBalance(msg.sender, cost);
} else {
if (msg.value != cost) revert IncorrectETHAmount(cost, msg.value);
treasury.playerPayTicket{value: msg.value}(msg.sender);
}
for (uint256 i = 0; i < quantity; i++) {
ticketToPlayer[nextTicketIndex + i] = msg.sender;
}
nextTicketIndex += quantity;
ticketCounts[msg.sender] += quantity;
playerCommitment ^= secret ^ blockhash(block.number - 1);
emit TicketsPurchased(msg.sender, quantity, nextTicketIndex);
}
Partner reveals the original seed, triggering random number mixing and final settlement. If not revealed within the timeout, anyone can call the penalty function to slash the deposit.
function reveal(bytes calldata revealData, bytes32 salt) external onlyCreator {
bytes32 calculatedResult = keccak256(abi.encodePacked(revealData, salt));
if (calculatedResult != sessionCommitment) revert InvalidReveal(sessionCommitment, calculatedResult);
_settle(SettlementType.Normal);
bytes32 combinedResult = keccak256(abi.encodePacked(revealData, playerCommitment));
uint256 winnerIndex = uint256(combinedResult) % totalTickets;
uint256 totalAmount = ticketPrice * totalTickets;
uint256 partnerShare = (totalAmount * partnerShareBps) / BPS_DENOMINATOR;
uint256 platformFee = (totalAmount * platformFeeBps) / BPS_DENOMINATOR;
uint256 rewardAmount = totalAmount - partnerShare - platformFee;
address[] memory users = new address[](3);
users[0] = creator;
users[1] = admin;
users[2] = ticketToPlayer[winnerIndex];
uint256[] memory amounts = new uint256[](3);
amounts[0] = partnerShare;
amounts[1] = platformFee;
amounts[2] = rewardAmount;
treasury.distributeFundsBatch(users, amounts);
treasury.unlockPartnerDeposit(ticketPrice * totalTickets);
emit WinnerSelected(ticketToPlayer[winnerIndex], winnerIndex);
emit SessionSettled(SettlementType.Normal);
}
[03] — Security Analysis
| # | Threat Vector | Status | Defense Mechanism |
|---|---|---|---|
| 01 | Second-Mover Attack | Mitigated | If the partner fails to Reveal within the timeout, the contract automatically triggers a penalty, slashing the full deposit and distributing it to affected players. Expected attack profit E=G−D≤0 |
| 02 | Random Number Manipulation | Mitigated | The final random number is generated by iteratively mixing the partner seed + block data + N player random numbers via keccak256; no single party can control the outcome alone |
| 03 | Miner / Validator Block Manipulation | Mitigated | blockhash from multiple independent blocks is used as one entropy source; the cost of a miner selectively dropping blocks exceeds any potential manipulation gain |
| 04 | Partner Refuses to Reveal | Mitigated | createSession() enforces msg.value == ticketPrice × ticketCount, ensuring deposit D ≥ maximum manipulation gain G |
| 05 | Sybil Attack | Mitigated | playerBuyAndCommitTicket mapping prevents the same address from purchasing tickets more than once |
| 06 | Reentrancy Attack | Mitigated | Settlement uses the CEI pattern (write state before transfer) + ReentrancyGuard to prevent reentrancy |
| 07 | Integer Overflow | Mitigated | Solidity ^0.8 has built-in overflow checks applied globally; SafeMath is not required |
[04] — Resources