// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {PokerDeck} from "./PokerDeck.sol"; interface IERC20Poker { function transfer(address to, uint256 value) external returns (bool); function transferFrom(address from, address to, uint256 value) external returns (bool); } interface IInviteBook { function traderInviter(address trader) external view returns (address); function inviterPayout(address inviter) external view returns (address); } library PokerSafeTransfer { function safeTransferFrom(IERC20Poker token, address from, address to, uint256 value) internal { (bool ok, bytes memory data) = address(token).call( abi.encodeWithSelector(token.transferFrom.selector, from, to, value) ); require(ok && (data.length == 0 || abi.decode(data, (bool))), "TRANSFER_FROM_FAILED"); } function safeTransfer(IERC20Poker token, address to_, uint256 value) internal { (bool ok, bytes memory data) = address(token).call( abi.encodeWithSelector(token.transfer.selector, to_, value) ); require(ok && (data.length == 0 || abi.decode(data, (bool))), "TRANSFER_FAILED"); } } /// @notice 6-max BIBI cash table (UUPS via ERC-1967 Proxy). /// Betting / blinds stay off-chain. Chain: buy-in, cash-out, commit, /// settle with 1% pot rake (0.2% of invited put → inviter, rest → ops). contract PokerTable { using PokerSafeTransfer for IERC20Poker; uint8 public constant SEATS = 6; uint256 public constant BPS = 10_000; uint256 public constant RAKE_BPS = 100; // 1% of pot uint256 public constant INVITE_REBATE_BPS = 20; // 0.2% of invited player's put bytes32 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; bool private _initialized; bool private _initializing; string public version; IERC20Poker public token; address public owner; address public dealer; address public opsReceiver; address public inviteRegistry; /// @notice Existing miner (or shared) invite book. Settle falls back here. address public inviteBook; uint256 public buyInChips; uint256 public chipScale; address[SEATS] public occupants; uint256[SEATS] public stack; uint256 public handNo; bool public handOpen; bytes32 public deckCommit; bytes32 public vrfSeed; mapping(address => address) public traderInviter; mapping(address => address) public inviterPayout; uint256[33] private __gap; event Initialized(uint64 indexed version); event Upgraded(address indexed implementation); event SeatBought(address indexed player, uint8 indexed seat, uint256 chips); event CashedOut(address indexed player, uint8 indexed seat, uint256 chips); event HandStarted(uint256 indexed handNo, bytes32 deckCommit); event VrfSeedSet(uint256 indexed handNo, bytes32 seed); event HandSettled( uint256 indexed handNo, uint256[SEATS] put, uint256[SEATS] won, bytes32 key, uint256 rake ); event DealerUpdated(address indexed dealer); event BuyInUpdated(uint256 chips, uint256 scale); event OpsReceiverUpdated(address indexed ops); event InviteRegistryUpdated(address indexed registry); event InviteBookUpdated(address indexed book); event TraderInviterSet(address indexed trader, address indexed inviter, address indexed operator); event InviterPayoutSet(address indexed inviter, address indexed payout); event InviteRebate(address indexed player, address indexed payout, uint256 chips, uint256 put); event RakePaid(uint256 rake, uint256 toInviters, uint256 toOps); error NotOwner(); error NotDealer(); error BadSeat(); error SeatTaken(); error NotOccupant(); error HandInProgress(); error HandNotOpen(); error AlreadySeeded(); error BadCommit(); error BadPut(); error PotMismatch(); error SeatsOccupied(); modifier onlyOwner() { if (msg.sender != owner) revert NotOwner(); _; } modifier onlyDealer() { if (msg.sender != dealer && msg.sender != owner) revert NotDealer(); _; } modifier onlyInviteRegistry() { require(msg.sender == inviteRegistry && inviteRegistry != address(0), "NOT_REGISTRY"); _; } constructor() { _initialized = true; version = "impl-locked"; } modifier initializer() { require(!_initialized || _initializing, "ALREADY_INIT"); bool isTopLevel = !_initializing; if (isTopLevel) { _initializing = true; _initialized = true; } _; if (isTopLevel) { _initializing = false; emit Initialized(1); } } function initialize( address token_, address owner_, address dealer_, address opsReceiver_, address inviteBook_, uint256 buyInChips_, uint256 chipScale_ ) external initializer { require( token_ != address(0) && owner_ != address(0) && dealer_ != address(0) && opsReceiver_ != address(0), "ZERO" ); require(buyInChips_ > 0 && chipScale_ > 0, "SCALE"); token = IERC20Poker(token_); owner = owner_; dealer = dealer_; opsReceiver = opsReceiver_; // Prefer the live miner Proxy so existing traderInviter/inviterPayout apply at settle. inviteBook = inviteBook_; buyInChips = buyInChips_; chipScale = chipScale_; version = "1.1.0-proxy-invite"; } function implementation() public view returns (address impl) { bytes32 slot = _IMPLEMENTATION_SLOT; assembly { impl := sload(slot) } } function upgradeToAndCall(address newImplementation, bytes memory data) external onlyOwner { _setImplementation(newImplementation); if (data.length > 0) { (bool ok, bytes memory ret) = newImplementation.delegatecall(data); if (!ok) { if (ret.length == 0) revert("UPGRADE_CALL_FAILED"); assembly { revert(add(ret, 32), mload(ret)) } } } } function upgradeTo(address newImplementation) external onlyOwner { _setImplementation(newImplementation); } function setVersion(string calldata newVersion) external onlyOwner { version = newVersion; } function setDealer(address next) external onlyOwner { require(next != address(0), "ZERO"); dealer = next; emit DealerUpdated(next); } function setOpsReceiver(address next) external onlyOwner { require(next != address(0), "ZERO"); opsReceiver = next; emit OpsReceiverUpdated(next); } function setInviteRegistry(address registry) external onlyOwner { inviteRegistry = registry; emit InviteRegistryUpdated(registry); } function setInviteBook(address book) external onlyOwner { inviteBook = book; emit InviteBookUpdated(book); } function setTraderInviter(address trader, address inviter) external onlyInviteRegistry { require(trader != address(0), "ZERO_TRADER"); traderInviter[trader] = inviter; emit TraderInviterSet(trader, inviter, msg.sender); } function setInviterPayout(address inviter, address payout) external onlyInviteRegistry { require(inviter != address(0), "ZERO_INVITER"); inviterPayout[inviter] = payout; emit InviterPayoutSet(inviter, payout); } function setBuyIn(uint256 chips, uint256 scale) external onlyOwner { if (handOpen) revert HandInProgress(); for (uint8 i = 0; i < SEATS; i++) { if (occupants[i] != address(0)) revert SeatsOccupied(); } require(chips > 0 && scale > 0, "SCALE"); buyInChips = chips; chipScale = scale; emit BuyInUpdated(chips, scale); } function tokenForBuyIn() public view returns (uint256) { return buyInChips * chipScale; } function rakeOf(uint256 pot) public pure returns (uint256) { return (pot * RAKE_BPS) / BPS; } function buyIn(uint8 seat) external { if (seat >= SEATS) revert BadSeat(); if (occupants[seat] != address(0)) revert SeatTaken(); if (handOpen) revert HandInProgress(); uint256 pay = tokenForBuyIn(); token.safeTransferFrom(msg.sender, address(this), pay); occupants[seat] = msg.sender; stack[seat] = buyInChips; emit SeatBought(msg.sender, seat, buyInChips); } function cashOut(uint8 seat) external { if (seat >= SEATS) revert BadSeat(); if (occupants[seat] != msg.sender) revert NotOccupant(); if (handOpen) revert HandInProgress(); uint256 chips = stack[seat]; occupants[seat] = address(0); stack[seat] = 0; if (chips > 0) { token.safeTransfer(msg.sender, chips * chipScale); } emit CashedOut(msg.sender, seat, chips); } function startHand(bytes32 commit) external onlyDealer { if (handOpen) revert HandInProgress(); if (commit == bytes32(0)) revert BadCommit(); uint256 seated; for (uint8 i = 0; i < SEATS; i++) { if (occupants[i] != address(0) && stack[i] > 0) seated++; } require(seated >= 2, "NEED_2"); handOpen = true; unchecked { handNo += 1; } deckCommit = commit; vrfSeed = bytes32(0); emit HandStarted(handNo, deckCommit); } function setVrfSeed(bytes32 seed) external onlyDealer { if (!handOpen) revert HandNotOpen(); if (vrfSeed != bytes32(0)) revert AlreadySeeded(); if (seed == bytes32(0)) revert BadCommit(); vrfSeed = seed; emit VrfSeedSet(handNo, seed); } /// @notice `won` must already be the 99% player share (sum == putSum - 1%). function settleHand( uint256[SEATS] calldata put, uint256[SEATS] calldata won, bytes32 key ) external onlyDealer { if (!handOpen) revert HandNotOpen(); if (PokerDeck.commitOf(key) != deckCommit) revert BadCommit(); uint256 putSum; uint256 wonSum; for (uint8 i = 0; i < SEATS; i++) { if (put[i] > stack[i]) revert BadPut(); putSum += put[i]; wonSum += won[i]; } uint256 rake = rakeOf(putSum); if (wonSum != putSum - rake) revert PotMismatch(); for (uint8 i = 0; i < SEATS; i++) { stack[i] = stack[i] - put[i] + won[i]; } _payoutRake(put, rake); emit HandSettled(handNo, put, won, key, rake); handOpen = false; deckCommit = bytes32(0); vrfSeed = bytes32(0); } function _payoutRake(uint256[SEATS] calldata put, uint256 rake) private { if (rake == 0) return; uint256 left = rake; for (uint8 i = 0; i < SEATS; i++) { address player = occupants[i]; if (player == address(0) || put[i] == 0) continue; address pay = _rebatePayout(player); if (pay == address(0) || pay == player) continue; uint256 cut = (put[i] * INVITE_REBATE_BPS) / BPS; if (cut > left) cut = left; if (cut == 0) continue; left -= cut; token.safeTransfer(pay, cut * chipScale); emit InviteRebate(player, pay, cut, put[i]); } uint256 toInviters = rake - left; if (left > 0) { token.safeTransfer(opsReceiver, left * chipScale); } emit RakePaid(rake, toInviters, left); } function _rebatePayout(address player) internal view returns (address pay) { pay = _payoutFromBook(inviteBook, player); if (pay != address(0)) return pay; address inv = traderInviter[player]; if (inv == address(0)) return address(0); return inviterPayout[inv]; } function _payoutFromBook(address book, address player) private view returns (address) { if (book == address(0) || book == address(this)) return address(0); try IInviteBook(book).traderInviter(player) returns (address inv) { if (inv == address(0)) return address(0); try IInviteBook(book).inviterPayout(inv) returns (address pay) { return pay; } catch { return address(0); } } catch { return address(0); } } function _setImplementation(address newImplementation) private { require(newImplementation != address(0), "ZERO_IMPL"); require(newImplementation.code.length > 0, "NOT_CONTRACT"); bytes32 slot = _IMPLEMENTATION_SLOT; assembly { sstore(slot, newImplementation) } emit Upgraded(newImplementation); } }