// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {PokerDeck} from "./PokerDeck.sol"; import {PokerBuyIn} from "./PokerBuyIn.sol"; interface IERC20Poker { function transfer(address to, uint256 value) external returns (bool); function transferFrom(address from, address to, uint256 value) external returns (bool); function balanceOf(address account) external view returns (uint256); } interface IPokerBank { function token() external view returns (address); function sitFromTable(address player, uint8 seat, uint256 chips) external returns (uint256); function reloadFromTable(address player, uint256 chips) external returns (uint256); function standFromTable(address player, uint256 stackBack) external; function pay(address to, uint256 chipAmount) external; function addGasDebt(address player, uint256 chipAmount) external; function autoLeaveFeeChips() external view returns (uint256); function registerTable() external; function addTable(address table) external; } 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; /// @notice Shared chip ledger. address(0) keeps the original per-table lock. IPokerBank public bank; uint256[32] 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); event BankUpdated(address indexed bank); error NotOwner(); error NotDealer(); error BadSeat(); error SeatTaken(); error NotOccupant(); error HandInProgress(); error HandNotOpen(); error AlreadySeeded(); error BadCommit(); error BadPut(); error PotMismatch(); error SeatsOccupied(); error HasChips(); error BadBuyIn(); 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(chipScale_ > 0 && PokerBuyIn.isAllowed(buyInChips_), "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.3.0-midhand-buyin"; } 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); } /// @notice Owner or dealer may point this table at the shared bank (clone flow). function setBank(address bank_) external { if (msg.sender != owner && msg.sender != dealer) revert NotOwner(); if (bank_ == address(0)) revert("ZERO"); if (IPokerBank(bank_).token() != address(token)) revert("TOKEN"); bank = IPokerBank(bank_); try bank.addTable(address(this)) {} catch { try bank.registerTable() {} catch {} } emit BankUpdated(bank_); } 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(scale > 0 && PokerBuyIn.isAllowed(chips), "SCALE"); buyInChips = chips; chipScale = scale; emit BuyInUpdated(chips, scale); } function tokenForBuyIn() public view returns (uint256) { return buyInChips * chipScale; } function tokenForChips(uint256 chips) public view returns (uint256) { return chips * chipScale; } function isAllowedBuyIn(uint256 chips) public pure returns (bool) { return PokerBuyIn.isAllowed(chips); } function rakeOf(uint256 pot) public pure returns (uint256) { return (pot * RAKE_BPS) / BPS; } function buyIn(uint8 seat) external { _buyIn(seat, buyInChips); } function buyIn(uint8 seat, uint256 chips) external { _buyIn(seat, chips); } /// @notice Mid-hand buy-in sits the player immediately. They wait for the /// next deal off-chain. cashOut / rebuy / startHand still revert /// while `handOpen`. function _buyIn(uint8 seat, uint256 chips) private { if (seat >= SEATS) revert BadSeat(); if (occupants[seat] != address(0)) revert SeatTaken(); if (_playerSeat(msg.sender) < SEATS) revert SeatTaken(); if (!PokerBuyIn.isAllowed(chips)) revert BadBuyIn(); uint256 seatedChips = chips; if (address(bank) != address(0)) { seatedChips = bank.sitFromTable(msg.sender, seat, chips); } else { token.safeTransferFrom(msg.sender, address(this), chips * chipScale); } occupants[seat] = msg.sender; stack[seat] = seatedChips; emit SeatBought(msg.sender, seat, seatedChips); } /// @notice Busted occupant reloads a chosen tier onto the same seat /// between hands. Ledger leftover covers first; wallet tops up the rest. function rebuy() external { _rebuy(buyInChips); } function rebuy(uint256 chips) external { _rebuy(chips); } function _rebuy(uint256 chips) private { if (handOpen) revert HandInProgress(); uint8 seat = _playerSeat(msg.sender); if (seat >= SEATS) revert NotOccupant(); if (stack[seat] != 0) revert HasChips(); if (!PokerBuyIn.isAllowed(chips)) revert BadBuyIn(); uint256 seatedChips = chips; if (address(bank) != address(0)) { seatedChips = bank.reloadFromTable(msg.sender, chips); } else { token.safeTransferFrom(msg.sender, address(this), chips * chipScale); } stack[seat] = seatedChips; emit SeatBought(msg.sender, seat, seatedChips); } /// @notice Leave the seat. With a bank, chips return to the ledger (not the wallet). function cashOut(uint8 seat) external { _stand(seat, msg.sender, false); } /// @notice Dealer-forced leave (idle / sit-out timeout). Charges bank gas debt. function forceStand(uint8 seat) external onlyDealer { if (seat >= SEATS) revert BadSeat(); address player = occupants[seat]; if (player == address(0)) revert NotOccupant(); _stand(seat, player, true); } function _playerSeat(address player) private view returns (uint8) { for (uint8 i = 0; i < SEATS; i++) { if (occupants[i] == player) return i; } return SEATS; } function _flushTableTokensToBank(uint256 chips) private { if (chips == 0 || address(bank) == address(0)) return; uint256 want = chips * chipScale; uint256 bal = token.balanceOf(address(this)); if (bal == 0) return; uint256 send = want > bal ? bal : want; token.safeTransfer(address(bank), send); } function _stand(uint8 seat, address player, bool chargeGas) private { if (seat >= SEATS) revert BadSeat(); if (occupants[seat] != player) revert NotOccupant(); if (handOpen) revert HandInProgress(); uint256 chips = stack[seat]; occupants[seat] = address(0); stack[seat] = 0; if (address(bank) != address(0)) { _flushTableTokensToBank(chips); bank.standFromTable(player, chips); if (chargeGas) { uint256 fee = bank.autoLeaveFeeChips(); if (fee > 0) bank.addGasDebt(player, fee); } } else if (chips > 0) { token.safeTransfer(player, chips * chipScale); } emit CashedOut(player, 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; _payChips(pay, cut); emit InviteRebate(player, pay, cut, put[i]); } uint256 toInviters = rake - left; if (left > 0) { _payChips(opsReceiver, left); } emit RakePaid(rake, toInviters, left); } function _payChips(address to, uint256 chipAmount) private { if (chipAmount == 0 || to == address(0)) return; if (address(bank) != address(0)) { bank.pay(to, chipAmount); } else { token.safeTransfer(to, chipAmount * chipScale); } } 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); } }