// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {PokerBuyIn} from "./PokerBuyIn.sol"; interface IERC20Bank { 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); } library BankSafeTransfer { function safeTransferFrom(IERC20Bank 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(IERC20Bank 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 Shared BIBI ledger for every 6-max PokerTable (ERC-1967 Proxy). /// Sit / rebuy take one of 6 tiers; wallet only tops up the ledger deficit. contract PokerBank { using BankSafeTransfer for IERC20Bank; bytes32 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; bool private _initialized; bool private _initializing; string public version; IERC20Bank public token; address public owner; /// @notice Default tier (1000亿) for lock() / display. uint256 public constant buyInChips = 100_000_000_000; uint256 public chipScale; /// @notice BIBI chips charged per dealer-forced leave (user pays on next sit/withdraw). uint256 public autoLeaveFeeChips; mapping(address => bool) public isTable; mapping(address => uint256) public chipsOf; mapping(address => uint256) public gasDebtChips; mapping(address => address) public seatedTable; mapping(address => uint8) public seatedSeat; /// @notice Optional second admin allowed to addTable. Must not be the hot dealer key. address public registrar; /// @notice Blocks lock / sit / rebuy token inflows. Cash-out and withdraw stay open. bool public depositsPaused; /// @notice Chips this table may still pay() or stand back. Grows on sit/reload. mapping(address => uint256) public floatOf; uint256[30] private __gap; event Initialized(uint64 indexed version); event Upgraded(address indexed implementation); event Locked(address indexed player, uint256 chips); event Sat(address indexed player, address indexed table, uint8 seat, uint256 chips); event Reloaded(address indexed player, address indexed table, uint256 chips); event Stood(address indexed player, address indexed table, uint256 chipsBack); event Withdrawn(address indexed player, uint256 chips); event GasDebtAdded(address indexed player, uint256 chips); event GasDebtPaid(address indexed player, uint256 chips); event TableRegistered(address indexed table); event TableUnregistered(address indexed table); event RegistrarUpdated(address indexed registrar); event AutoLeaveFeeUpdated(uint256 chips); event ChipsWrittenOff(address indexed player, uint256 chips); error NotOwner(); error NotTable(); error Seated(); error NotSeated(); error NeedBuyIn(); error BadTable(); error BadBuyIn(); error NoToken(); error DepositsPaused(); error OverFloat(); modifier onlyOwner() { if (msg.sender != owner) revert NotOwner(); _; } modifier whenDepositsOpen() { if (depositsPaused) revert DepositsPaused(); _; } modifier onlyTable() { if (!isTable[msg.sender]) revert NotTable(); _; } 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_, uint256 chipScale_, uint256 autoLeaveFeeChips_ ) external initializer { require(token_ != address(0) && owner_ != address(0), "ZERO"); require(chipScale_ > 0, "SCALE"); token = IERC20Bank(token_); owner = owner_; chipScale = chipScale_; autoLeaveFeeChips = autoLeaveFeeChips_; version = "1.1.0-ledger-pay"; } 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 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 setAutoLeaveFee(uint256 chips) external onlyOwner { autoLeaveFeeChips = chips; emit AutoLeaveFeeUpdated(chips); } function setRegistrar(address next) external onlyOwner { registrar = next; emit RegistrarUpdated(next); } /// @notice Open self-registration was the drain: any address became a /// "table", called pay() to mint unbacked chips, then withdraw(). function registerTable() external pure { revert NotOwner(); } function addTable(address table) external { _addTable(table); } function addTables(address[] calldata tables) external { if (msg.sender != owner && msg.sender != registrar) revert NotOwner(); if (tables.length == 0 || tables.length > 24) revert BadTable(); for (uint256 i = 0; i < tables.length; i++) { _addTable(tables[i]); } } function _addTable(address table) private { if (msg.sender != owner && msg.sender != registrar) revert NotOwner(); if (table == address(0)) revert BadTable(); isTable[table] = true; emit TableRegistered(table); } function setDepositsPaused(bool paused) external onlyOwner { depositsPaused = paused; } /// @notice Zero a ledger balance. Does not move ERC20. function writeOff(address player) external onlyOwner { uint256 chips = chipsOf[player]; if (chips == 0) return; chipsOf[player] = 0; emit ChipsWrittenOff(player, chips); } /// @notice Seed float after upgrade if players were already seated. function creditTableFloat(address table, uint256 chips) external onlyOwner { if (table == address(0) || chips == 0) revert BadTable(); floatOf[table] += chips; } function removeTable(address table) external onlyOwner { isTable[table] = false; emit TableUnregistered(table); } /// @notice Kick a fake table, pause deposits, optional ledger wipe, no hot registrar. function applySecurityPatch(address attacker, address registrar_, address wipePlayer) external onlyOwner { if (attacker != address(0)) { isTable[attacker] = false; emit TableUnregistered(attacker); } registrar = registrar_; emit RegistrarUpdated(registrar_); depositsPaused = true; if (wipePlayer != address(0)) { uint256 chips = chipsOf[wipePlayer]; if (chips > 0) { chipsOf[wipePlayer] = 0; emit ChipsWrittenOff(wipePlayer, chips); } } version = "1.1.4-table-float"; } /// @notice Pre-fund the default 1000亿 tier. function lock() external whenDepositsOpen { _lockTier(msg.sender, buyInChips); } /// @notice Pre-fund one allowed tier from the caller wallet. function lock(uint256 chips) external whenDepositsOpen { _lockTier(msg.sender, chips); } function sitFromTable(address player, uint8 seat, uint256 chips) external onlyTable whenDepositsOpen returns (uint256) { if (seatedTable[player] != address(0)) revert Seated(); _ensureChips(player, chips); chipsOf[player] -= chips; seatedTable[player] = msg.sender; seatedSeat[player] = seat; floatOf[msg.sender] += chips; emit Sat(player, msg.sender, seat, chips); return chips; } /// @notice Already-seated bust reload: same 6 tiers, ledger top-up only. function reloadFromTable(address player, uint256 chips) external onlyTable whenDepositsOpen returns (uint256) { if (seatedTable[player] != msg.sender) revert NotSeated(); _ensureChips(player, chips); chipsOf[player] -= chips; floatOf[msg.sender] += chips; emit Reloaded(player, msg.sender, chips); return chips; } function standFromTable(address player, uint256 stackBack) external onlyTable { if (seatedTable[player] != msg.sender) revert NotSeated(); seatedTable[player] = address(0); seatedSeat[player] = 0; if (stackBack > 0) { _debitFloat(msg.sender, stackBack); chipsOf[player] += stackBack; } emit Stood(player, msg.sender, stackBack); } function addGasDebt(address player, uint256 chipAmount) external onlyTable { if (chipAmount == 0) return; gasDebtChips[player] += chipAmount; emit GasDebtAdded(player, chipAmount); } /// Credit the shared ledger. Invite/ops rake is withdrawn later so /// settleHand does not pay ERC20 transfer gas on every pot. function pay(address to, uint256 chipAmount) external onlyTable { if (chipAmount == 0 || to == address(0)) return; _debitFloat(msg.sender, chipAmount); chipsOf[to] += chipAmount; } /// @notice Pull as many chips as the vault can cover. Leftover stays on the /// ledger when `pay()` / a bank switch left chips unbacked by ERC20. function withdraw() external { if (seatedTable[msg.sender] != address(0)) revert Seated(); _collectGasDebt(msg.sender); uint256 chips = chipsOf[msg.sender]; if (chips == 0) return; uint256 scale = chipScale; uint256 backed = scale == 0 ? 0 : token.balanceOf(address(this)) / scale; uint256 send = chips < backed ? chips : backed; if (send == 0) revert NoToken(); chipsOf[msg.sender] = chips - send; token.safeTransfer(msg.sender, send * scale); emit Withdrawn(msg.sender, send); } function withdrawable(address player) external view returns (uint256) { if (seatedTable[player] != address(0)) return 0; uint256 chips = chipsOf[player]; uint256 debt = gasDebtChips[player]; uint256 owed = chips > debt ? chips - debt : 0; uint256 scale = chipScale; uint256 backed = scale == 0 ? 0 : token.balanceOf(address(this)) / scale; return owed < backed ? owed : backed; } function _debitFloat(address table, uint256 chips) private { uint256 have = floatOf[table]; if (chips > have) revert OverFloat(); floatOf[table] = have - chips; } function _lockTier(address player, uint256 chips) private { if (!PokerBuyIn.isAllowed(chips)) revert BadBuyIn(); _lockFrom(player, chips); } function _ensureChips(address player, uint256 chips) private { if (!PokerBuyIn.isAllowed(chips)) revert BadBuyIn(); _collectGasDebt(player); uint256 have = chipsOf[player]; if (have < chips) { _lockFrom(player, chips - have); } if (chipsOf[player] < chips) revert NeedBuyIn(); } function _lockFrom(address player, uint256 chips) private { if (chips == 0) return; token.safeTransferFrom(player, address(this), chips * chipScale); chipsOf[player] += chips; emit Locked(player, chips); } function _collectGasDebt(address player) private { uint256 debt = gasDebtChips[player]; if (debt == 0) return; uint256 chips = chipsOf[player]; uint256 paid = debt > chips ? chips : debt; if (paid == 0) return; chipsOf[player] = chips - paid; gasDebtChips[player] = debt - paid; emit GasDebtPaid(player, paid); } 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); } }