[00] — Smart Contract Onetappy / Sepolia(Testnet) / v0.1

Contract Architecture
Verifiably Fair

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

Treasury.partnerDeposit()

Deposit Module

01 Partner deposits the full amount (ticket price × quantity) when creating a game
02 Deposit is atomically locked by the contract; no one can withdraw it early
03 Automatically refunded after settlement or slashed as penalty
04 Deposit amount covers potential manipulation gains, eliminating economic motive
SessionConfig.sessionCommitment

Commitment Module

01 Partner Commit = keccak256(revealData, salt)
02 Player single atomic action: ticket payment + random number written simultaneously
03 Player does not need to execute a Reveal step (Player One Tappy)
04 Partner Reveal triggers the final random number calculation
Session.reveal(revealData, salt)

Settlement Module

01 Iteratively mixes all player random numbers and on-chain block data via keccak256
02 Combines partner seed to generate the final irreversible random number
03 Winner determined on-chain; result is independently verifiable by anyone
04 Auto-distributes: winner prize + partner revenue + deposit refund
Deposit
Commit
Reveal

[02] — Core Functions

createSession(SessionConfig)

Partner creates a group-buy game. Locks the full deposit (ticket price × quantity) and simultaneously submits a hash commitment — both atomically bound and inseparable.

← Deposit locked in full
← Commitment and deposit atomically written
← Game parameters immutable
OntappyFactory.sol
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);
}

playerBuyAndCommitTicket()

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.

← Ticket payment + random number atomically submitted
← Player does not need to Reveal
← Written to on-chain entropy source
OnetappySession.sol
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);
}

reveal()

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.

← Verifies keccak256(revealData) == commitHash
← Triggers multi-entropy mixing calculation
← Auto-penalty if timeout expires without reveal
OnetappySession.sol
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

ABI & Interface Docs

Full TypeScript type support

View Docs
Onetappy · Smart Contract · v0.1