{"sources":{"ShifenHaoyun.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\n/// @title 十分好运 (ShifenHaoyun) — a 10-minute on-chain lottery for AI agents.\n/// @notice Rounds align to wall-clock 10-minute windows (roundId = timestamp / 600).\n///         Agents holding a .ai Agent ID buy tickets at 0.001 BNB each. When a round\n///         ends, anyone can progress it: first the draw block is pinned two blocks\n///         ahead (its hash is unknown to everyone at pin time), then the winner is\n///         drawn weighted by tickets. Winner receives 85% of the pool, 15% goes to\n///         the treasury — both paid in the settle transaction itself. A round with a\n///         single unique player is refunded in full (no draw, no rake). There are no\n///         admin functions; the split and price can never change.\ninterface IAgentIdRegistry {\n    function balanceOf(address account) external view returns (uint256);\n}\n\ncontract ShifenHaoyun {\n    uint256 public constant ROUND_SECONDS = 600;\n    uint256 public constant TICKET_PRICE = 0.001 ether;\n    uint256 public constant RAKE_PERCENT = 15;\n    uint256 public constant MAX_TICKETS_PER_TX = 100;\n\n    IAgentIdRegistry public immutable registry;\n    address public immutable treasury;\n\n    struct Round {\n        uint256 totalTickets;\n        uint256 uniquePlayers;\n        address firstPlayer;\n        uint256 drawBlock;\n        bool settled;\n        bool refunded;\n        address winner;\n        uint256 prize;\n    }\n\n    mapping(uint256 => Round) public rounds;\n    mapping(uint256 => address[]) private entryPlayers;\n    mapping(uint256 => uint256[]) private entryCumEnds;\n    mapping(uint256 => mapping(address => uint256)) public ticketsOf;\n    mapping(address => uint256) public pendingWithdrawals;\n\n    address public lastWinner;\n    uint256 public lastPrize;\n    uint256 public lastSettledRound;\n    uint256 public totalPrizesPaid;\n    uint256 public totalRakePaid;\n    uint256 public totalTicketsAll;\n\n    bool private _lock;\n\n    event TicketsBought(uint256 indexed roundId, address indexed player, uint256 tickets, uint256 roundTotal);\n    event DrawPinned(uint256 indexed roundId, uint256 drawBlock);\n    event RoundSettled(uint256 indexed roundId, address indexed winner, uint256 prize, uint256 rake, uint256 winningTicket);\n    event RoundRefunded(uint256 indexed roundId, address indexed player, uint256 amount);\n    event RoundEmpty(uint256 indexed roundId);\n    event Withdrawn(address indexed account, uint256 amount);\n\n    modifier nonReentrant() {\n        require(!_lock, \"reentrant\");\n        _lock = true;\n        _;\n        _lock = false;\n    }\n\n    constructor(address registry_, address treasury_) {\n        require(registry_ != address(0) && treasury_ != address(0), \"zero address\");\n        registry = IAgentIdRegistry(registry_);\n        treasury = treasury_;\n    }\n\n    function currentRound() public view returns (uint256) {\n        return block.timestamp / ROUND_SECONDS;\n    }\n\n    function roundEnd(uint256 roundId) public pure returns (uint256) {\n        return (roundId + 1) * ROUND_SECONDS;\n    }\n\n    /// @notice Buy tickets for the current round. Value must cover tickets * price;\n    ///         any excess is refunded in the same transaction.\n    function buy(uint256 tickets) external payable nonReentrant {\n        require(tickets >= 1 && tickets <= MAX_TICKETS_PER_TX, \"bad ticket count\");\n        require(registry.balanceOf(msg.sender) > 0, \"no agent id\");\n        uint256 cost = tickets * TICKET_PRICE;\n        require(msg.value >= cost, \"insufficient payment\");\n\n        uint256 r = currentRound();\n        // Best-effort: progress the two rounds before this one.\n        if (r >= 1) _progress(r - 1);\n        if (r >= 2) _progress(r - 2);\n\n        Round storage rd = rounds[r];\n        if (ticketsOf[r][msg.sender] == 0) {\n            rd.uniquePlayers += 1;\n            if (rd.uniquePlayers == 1) rd.firstPlayer = msg.sender;\n        }\n        ticketsOf[r][msg.sender] += tickets;\n        rd.totalTickets += tickets;\n        totalTicketsAll += tickets;\n        entryPlayers[r].push(msg.sender);\n        entryCumEnds[r].push(rd.totalTickets);\n        emit TicketsBought(r, msg.sender, tickets, rd.totalTickets);\n\n        uint256 excess = msg.value - cost;\n        if (excess > 0) _pay(msg.sender, excess);\n    }\n\n    /// @notice Progress an ended round: pin its draw block, or settle it once the\n    ///         draw block has been mined. May need two calls (pin, then settle).\n    function settle(uint256 roundId) external nonReentrant {\n        require(roundId < currentRound(), \"round not ended\");\n        _progress(roundId);\n    }\n\n    function _progress(uint256 r) internal {\n        Round storage rd = rounds[r];\n        if (rd.settled || r >= currentRound()) return;\n        if (rd.totalTickets == 0) {\n            rd.settled = true;\n            emit RoundEmpty(r);\n            return;\n        }\n        if (rd.drawBlock == 0) {\n            rd.drawBlock = block.number + 2;\n            emit DrawPinned(r, rd.drawBlock);\n            return;\n        }\n        if (block.number <= rd.drawBlock) return;\n\n        rd.settled = true;\n        uint256 pool = rd.totalTickets * TICKET_PRICE;\n\n        if (rd.uniquePlayers == 1) {\n            rd.refunded = true;\n            rd.winner = rd.firstPlayer;\n            rd.prize = 0;\n            _pay(rd.firstPlayer, pool);\n            emit RoundRefunded(r, rd.firstPlayer, pool);\n            return;\n        }\n\n        uint256 seed = uint256(blockhash(rd.drawBlock));\n        if (seed == 0) {\n            // Draw block fell out of the 256-block blockhash window: fall back to\n            // the freshest blockhash mixed with the pinned block number.\n            seed = uint256(keccak256(abi.encode(r, rd.drawBlock, blockhash(block.number - 1), block.timestamp)));\n        }\n        uint256 winIdx = seed % rd.totalTickets;\n        address winner = _findPlayer(r, winIdx);\n        uint256 prize = (pool * (100 - RAKE_PERCENT)) / 100;\n        uint256 rake = pool - prize;\n\n        rd.winner = winner;\n        rd.prize = prize;\n        lastWinner = winner;\n        lastPrize = prize;\n        lastSettledRound = r;\n        totalPrizesPaid += prize;\n        totalRakePaid += rake;\n\n        _pay(winner, prize);\n        _pay(treasury, rake);\n        emit RoundSettled(r, winner, prize, rake, winIdx);\n    }\n\n    function _findPlayer(uint256 r, uint256 idx) internal view returns (address) {\n        uint256[] storage cum = entryCumEnds[r];\n        uint256 lo = 0;\n        uint256 hi = cum.length - 1;\n        while (lo < hi) {\n            uint256 mid = (lo + hi) / 2;\n            if (cum[mid] <= idx) lo = mid + 1;\n            else hi = mid;\n        }\n        return entryPlayers[r][lo];\n    }\n\n    function _pay(address to, uint256 amount) internal {\n        if (amount == 0) return;\n        (bool ok, ) = to.call{value: amount}(\"\");\n        if (!ok) pendingWithdrawals[to] += amount;\n    }\n\n    /// @notice Withdraw any prize/refund that could not be pushed to your wallet.\n    function withdraw() external nonReentrant {\n        uint256 amount = pendingWithdrawals[msg.sender];\n        require(amount > 0, \"nothing pending\");\n        pendingWithdrawals[msg.sender] = 0;\n        (bool ok, ) = msg.sender.call{value: amount}(\"\");\n        require(ok, \"transfer failed\");\n        emit Withdrawn(msg.sender, amount);\n    }\n\n    function entriesCount(uint256 roundId) external view returns (uint256) {\n        return entryPlayers[roundId].length;\n    }\n\n    function roundInfo(uint256 roundId)\n        external\n        view\n        returns (\n            uint256 start,\n            uint256 end,\n            uint256 totalTickets,\n            uint256 uniquePlayers,\n            uint256 pool,\n            uint256 drawBlock,\n            bool settled,\n            bool refunded,\n            address winner,\n            uint256 prize\n        )\n    {\n        Round storage rd = rounds[roundId];\n        return (\n            roundId * ROUND_SECONDS,\n            (roundId + 1) * ROUND_SECONDS,\n            rd.totalTickets,\n            rd.uniquePlayers,\n            rd.totalTickets * TICKET_PRICE,\n            rd.drawBlock,\n            rd.settled,\n            rd.refunded,\n            rd.winner,\n            rd.prize\n        );\n    }\n\n    function status()\n        external\n        view\n        returns (\n            uint256 curRound,\n            uint256 curEnd,\n            uint256 curTickets,\n            uint256 curPlayers,\n            uint256 curPool,\n            address lastWin,\n            uint256 lastPrz,\n            uint256 lastRound,\n            uint256 prizesPaid,\n            uint256 rakePaid\n        )\n    {\n        uint256 r = currentRound();\n        Round storage rd = rounds[r];\n        return (\n            r,\n            (r + 1) * ROUND_SECONDS,\n            rd.totalTickets,\n            rd.uniquePlayers,\n            rd.totalTickets * TICKET_PRICE,\n            lastWinner,\n            lastPrize,\n            lastSettledRound,\n            totalPrizesPaid,\n            totalRakePaid\n        );\n    }\n}\n"}},"abi":[{"type":"constructor","inputs":[{"name":"registry_","type":"address","internalType":"address"},{"name":"treasury_","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"name":"DrawPinned","type":"event","inputs":[{"name":"roundId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"drawBlock","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"name":"RoundEmpty","type":"event","inputs":[{"name":"roundId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"name":"RoundRefunded","type":"event","inputs":[{"name":"roundId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"player","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"name":"RoundSettled","type":"event","inputs":[{"name":"roundId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"winner","type":"address","indexed":true,"internalType":"address"},{"name":"prize","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"rake","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"winningTicket","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"name":"TicketsBought","type":"event","inputs":[{"name":"roundId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"player","type":"address","indexed":true,"internalType":"address"},{"name":"tickets","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"roundTotal","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"name":"Withdrawn","type":"event","inputs":[{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"name":"MAX_TICKETS_PER_TX","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"RAKE_PERCENT","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"ROUND_SECONDS","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"TICKET_PRICE","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"buy","type":"function","inputs":[{"name":"tickets","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"name":"currentRound","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"entriesCount","type":"function","inputs":[{"name":"roundId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"lastPrize","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"lastSettledRound","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"lastWinner","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"name":"pendingWithdrawals","type":"function","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"registry","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IAgentIdRegistry"}],"stateMutability":"view"},{"name":"roundEnd","type":"function","inputs":[{"name":"roundId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"pure"},{"name":"roundInfo","type":"function","inputs":[{"name":"roundId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"start","type":"uint256","internalType":"uint256"},{"name":"end","type":"uint256","internalType":"uint256"},{"name":"totalTickets","type":"uint256","internalType":"uint256"},{"name":"uniquePlayers","type":"uint256","internalType":"uint256"},{"name":"pool","type":"uint256","internalType":"uint256"},{"name":"drawBlock","type":"uint256","internalType":"uint256"},{"name":"settled","type":"bool","internalType":"bool"},{"name":"refunded","type":"bool","internalType":"bool"},{"name":"winner","type":"address","internalType":"address"},{"name":"prize","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"rounds","type":"function","inputs":[{"name":"","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"totalTickets","type":"uint256","internalType":"uint256"},{"name":"uniquePlayers","type":"uint256","internalType":"uint256"},{"name":"firstPlayer","type":"address","internalType":"address"},{"name":"drawBlock","type":"uint256","internalType":"uint256"},{"name":"settled","type":"bool","internalType":"bool"},{"name":"refunded","type":"bool","internalType":"bool"},{"name":"winner","type":"address","internalType":"address"},{"name":"prize","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"settle","type":"function","inputs":[{"name":"roundId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"status","type":"function","inputs":[],"outputs":[{"name":"curRound","type":"uint256","internalType":"uint256"},{"name":"curEnd","type":"uint256","internalType":"uint256"},{"name":"curTickets","type":"uint256","internalType":"uint256"},{"name":"curPlayers","type":"uint256","internalType":"uint256"},{"name":"curPool","type":"uint256","internalType":"uint256"},{"name":"lastWin","type":"address","internalType":"address"},{"name":"lastPrz","type":"uint256","internalType":"uint256"},{"name":"lastRound","type":"uint256","internalType":"uint256"},{"name":"prizesPaid","type":"uint256","internalType":"uint256"},{"name":"rakePaid","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"ticketsOf","type":"function","inputs":[{"name":"","type":"uint256","internalType":"uint256"},{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"totalPrizesPaid","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"totalRakePaid","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"totalTicketsAll","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"treasury","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"name":"withdraw","type":"function","inputs":[],"outputs":[],"stateMutability":"nonpayable"}],"matchId":"59061307","creationMatch":"exact_match","runtimeMatch":"exact_match","verifiedAt":"2026-11-10T19:54:47Z","match":"exact_match","chainId":"56","address":"0x8B050D60F1dCCE7323cA9D68Bf164040b60416b3"}