Blockchain / Global / Address

Contract

0x3ff67d6Ae70226bb2372c5C49EE69ba05b7BD392

Balance
0 SOMI
Transactions sent
1
Type
Contract · 6,529 bytes of code

ShuffleFacet Verified

Compiler
solc 0.8.28+commit.7893614a
Language
Solidity
Optimization
Yes, 200 runs
EVM version
shanghai
License
—
Verified
2026-10-12

project/contracts/facets/ShuffleFacet.sol

and 9 more files

Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import "../libraries/LibStorage.sol";
import "../libraries/LibGame.sol";
import "../libraries/MafiaTypes.sol";

/// @title ShuffleFacet — Deck commit/reveal, key sharing, role confirmation
contract ShuffleFacet {

    modifier nonReentrant() {
        LibGame.nonReentrantBefore();
        _;
        LibGame.nonReentrantAfter();
    }

    function commitDeck(uint256 roomId, bytes32 deckHash) external nonReentrant {
        LibGame.requireNotPaused();
        address player = LibGame.requireActiveParticipant(roomId);
        LibGame.requireBeforeDeadline(roomId);

        LibStorage.Storage storage ds = LibStorage.s();
        MafiaTypes.GameRoom storage room = ds.rooms[roomId];
        if (room.phase != MafiaTypes.GamePhase.SHUFFLING) revert LibGame.WrongPhase();
        if (ds.roomPlayers[roomId][room.currentShufflerIndex].wallet != player) revert LibGame.NotYourTurn();
        if (LibGame.hasFlag(roomId, player, LibGame.FLAG_DECK_COMMITTED)) revert LibGame.AlreadyCommitted();

        ds.deckCommits[roomId][player].commitHash = deckHash;
        ds.deckCommits[roomId][player].revealed = false;
        LibGame.setFlag(roomId, player, LibGame.FLAG_DECK_COMMITTED);

        emit LibGame.DeckCommitted(roomId, player, deckHash);
    }

    function revealDeck(uint256 roomId, string[] calldata deck, string calldata salt) external nonReentrant {
        LibGame.requireNotPaused();
        address player = LibGame.requireActiveParticipant(roomId);
        LibGame.requireBeforeDeadline(roomId);

        LibStorage.Storage storage ds = LibStorage.s();
        MafiaTypes.GameRoom storage room = ds.rooms[roomId];
        if (room.phase != MafiaTypes.GamePhase.SHUFFLING) revert LibGame.WrongPhase();

        bytes32 calculatedHash = keccak256(abi.encode(deck, salt));
        if (calculatedHash != ds.deckCommits[roomId][player].commitHash) revert LibGame.InvalidReveal();
        if (deck.length > LibGame.MAX_ARRAY_SIZE) revert LibGame.ArrayTooLarge();
        if (ds.revealedDeck[roomId].length == 0 && deck.length < room.playersCount) revert LibGame.InvalidDeckSize();
        if (ds.revealedDeck[roomId].length != 0 && deck.length != ds.revealedDeck[roomId].length) revert LibGame.InvalidDeckSize();

        ds.revealedDeck[roomId] = deck;
        ds.deckCommits[roomId][player].revealed = true;

        uint8 nextIndex = LibGame.findNextActive(roomId, room.currentShufflerIndex + 1);
        room.currentShufflerIndex = nextIndex;
        room.lastActionTimestamp = uint32(block.timestamp);
        room.phaseDeadline = uint32(block.timestamp + LibGame.PHASE_TIMEOUT);
        room.revealedCount++;

        emit LibGame.DeckRevealed(roomId, player, deck);

        if (nextIndex >= ds.roomPlayers[roomId].length) {
            LibGame.transitionToReveal(roomId);
        }
    }

    function shareKeysToAll(
        uint256 roomId,
        address[] calldata recipients,
        bytes[] calldata encryptedKeys
    ) external nonReentrant {
        LibGame.requireNotPaused();
        address player = LibGame.requireActiveParticipant(roomId);
        LibGame.requireBeforeDeadline(roomId);

        LibStorage.Storage storage ds = LibStorage.s();
        MafiaTypes.GameRoom storage room = ds.rooms[roomId];
        if (room.phase != MafiaTypes.GamePhase.REVEAL) revert LibGame.WrongPhase();
        if (recipients.length != encryptedKeys.length) revert LibGame.InvalidArrayLength();
        if (recipients.length > LibGame.MAX_ARRAY_SIZE) revert LibGame.ArrayTooLarge();
        if (LibGame.hasFlag(roomId, player, LibGame.FLAG_HAS_SHARED_KEYS)) revert LibGame.AlreadySharedKeys();

        for (uint256 i = 0; i < recipients.length; i++) {
            address to = recipients[i];
            if (!ds.isPlayerInRoom[roomId][to]) revert LibGame.NotParticipant();
            if (to == player) revert LibGame.InvalidSessionAddress();
            ds.playerDeckKeys[roomId][player][to] = encryptedKeys[i];
        }

        LibGame.setFlag(roomId, player, LibGame.FLAG_HAS_SHARED_KEYS);
        room.keysSharedCount++;
        emit LibGame.KeysSharedToAll(roomId, player);
        if (room.keysSharedCount == room.aliveCount) emit LibGame.AllKeysShared(roomId);
    }

    function commitAndConfirmRole(uint256 roomId, bytes32 roleHash) external nonReentrant {
        LibGame.requireNotPaused();
        address player = LibGame.requireActiveParticipant(roomId);
        LibGame.requireBeforeDeadline(roomId);

        LibStorage.Storage storage ds = LibStorage.s();
        MafiaTypes.GameRoom storage room = ds.rooms[roomId];
        if (room.phase != MafiaTypes.GamePhase.REVEAL) revert LibGame.WrongPhase();
        if (ds.roleCommits[roomId][player] != bytes32(0)) revert LibGame.RoleAlreadyCommitted();
        if (LibGame.hasFlag(roomId, player, LibGame.FLAG_CONFIRMED_ROLE)) revert LibGame.AlreadyRevealed();

        ds.roleCommits[roomId][player] = roleHash;
        emit LibGame.RoleCommitted(roomId, player, roleHash);

        LibGame.setFlag(roomId, player, LibGame.FLAG_CONFIRMED_ROLE);
        room.confirmedCount++;
        emit LibGame.RoleConfirmed(roomId, player);

        if (room.confirmedCount == room.aliveCount) {
            LibGame.transitionToDay(roomId);
            emit LibGame.AllRolesConfirmed(roomId);
        }
    }

    function commitRole(uint256 roomId, bytes32 roleHash) external nonReentrant {
        LibGame.requireNotPaused();
        address player = LibGame.requireActiveParticipant(roomId);

        LibStorage.Storage storage ds = LibStorage.s();
        if (ds.rooms[roomId].phase != MafiaTypes.GamePhase.REVEAL) revert LibGame.WrongPhase();
        if (ds.roleCommits[roomId][player] != bytes32(0)) revert LibGame.RoleAlreadyCommitted();
        ds.roleCommits[roomId][player] = roleHash;
        emit LibGame.RoleCommitted(roomId, player, roleHash);
    }

    function confirmRole(uint256 roomId) external nonReentrant {
        LibGame.requireNotPaused();
        address player = LibGame.requireActiveParticipant(roomId);
        LibGame.requireBeforeDeadline(roomId);

        LibStorage.Storage storage ds = LibStorage.s();
        MafiaTypes.GameRoom storage room = ds.rooms[roomId];
        if (room.phase != MafiaTypes.GamePhase.REVEAL) revert LibGame.WrongPhase();
        if (ds.roleCommits[roomId][player] == bytes32(0)) revert LibGame.RoleNotCommitted();
        if (LibGame.hasFlag(roomId, player, LibGame.FLAG_CONFIRMED_ROLE)) revert LibGame.AlreadyRevealed();

        LibGame.setFlag(roomId, player, LibGame.FLAG_CONFIRMED_ROLE);
        room.confirmedCount++;
        emit LibGame.RoleConfirmed(roomId, player);
        if (room.confirmedCount == room.aliveCount) {
            LibGame.transitionToDay(roomId);
            emit LibGame.AllRolesConfirmed(roomId);
        }
    }

    // ---- View ----
    function getRevealedDeck(uint256 roomId, uint256 index) external view returns (string memory) {
        return LibStorage.s().revealedDeck[roomId][index];
    }
}

Other files

  • npm/@openzeppelin/contracts@5.4.0/interfaces/IERC1363.sol
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.4.0) (interfaces/IERC1363.sol)
    
    pragma solidity >=0.6.2;
    
    import {IERC20} from "./IERC20.sol";
    import {IERC165} from "./IERC165.sol";
    
    /**
     * @title IERC1363
     * @dev Interface of the ERC-1363 standard as defined in the https://eips.ethereum.org/EIPS/eip-1363[ERC-1363].
     *
     * Defines an extension interface for ERC-20 tokens that supports executing code on a recipient contract
     * after `transfer` or `transferFrom`, or code on a spender contract after `approve`, in a single transaction.
     */
    interface IERC1363 is IERC20, IERC165 {
        /*
         * Note: the ERC-165 identifier for this interface is 0xb0202a11.
         * 0xb0202a11 ===
         *   bytes4(keccak256('transferAndCall(address,uint256)')) ^
         *   bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^
         *   bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^
         *   bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^
         *   bytes4(keccak256('approveAndCall(address,uint256)')) ^
         *   bytes4(keccak256('approveAndCall(address,uint256,bytes)'))
         */
    
        /**
         * @dev Moves a `value` amount of tokens from the caller's account to `to`
         * and then calls {IERC1363Receiver-onTransferReceived} on `to`.
         * @param to The address which you want to transfer to.
         * @param value The amount of tokens to be transferred.
         * @return A boolean value indicating whether the operation succeeded unless throwing.
         */
        function transferAndCall(address to, uint256 value) external returns (bool);
    
        /**
         * @dev Moves a `value` amount of tokens from the caller's account to `to`
         * and then calls {IERC1363Receiver-onTransferReceived} on `to`.
         * @param to The address which you want to transfer to.
         * @param value The amount of tokens to be transferred.
         * @param data Additional data with no specified format, sent in call to `to`.
         * @return A boolean value indicating whether the operation succeeded unless throwing.
         */
        function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool);
    
        /**
         * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
         * and then calls {IERC1363Receiver-onTransferReceived} on `to`.
         * @param from The address which you want to send tokens from.
         * @param to The address which you want to transfer to.
         * @param value The amount of tokens to be transferred.
         * @return A boolean value indicating whether the operation succeeded unless throwing.
         */
        function transferFromAndCall(address from, address to, uint256 value) external returns (bool);
    
        /**
         * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
         * and then calls {IERC1363Receiver-onTransferReceived} on `to`.
         * @param from The address which you want to send tokens from.
         * @param to The address which you want to transfer to.
         * @param value The amount of tokens to be transferred.
         * @param data Additional data with no specified format, sent in call to `to`.
         * @return A boolean value indicating whether the operation succeeded unless throwing.
         */
        function transferFromAndCall(address from, address to, uint256 value, bytes calldata data) external returns (bool);
    
        /**
         * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
         * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
         * @param spender The address which will spend the funds.
         * @param value The amount of tokens to be spent.
         * @return A boolean value indicating whether the operation succeeded unless throwing.
         */
        function approveAndCall(address spender, uint256 value) external returns (bool);
    
        /**
         * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
         * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
         * @param spender The address which will spend the funds.
         * @param value The amount of tokens to be spent.
         * @param data Additional data with no specified format, sent in call to `spender`.
         * @return A boolean value indicating whether the operation succeeded unless throwing.
         */
        function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool);
    }
    
  • npm/@openzeppelin/contracts@5.4.0/interfaces/IERC165.sol
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.4.0) (interfaces/IERC165.sol)
    
    pragma solidity >=0.4.16;
    
    import {IERC165} from "../utils/introspection/IERC165.sol";
    
  • npm/@openzeppelin/contracts@5.4.0/interfaces/IERC20.sol
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.4.0) (interfaces/IERC20.sol)
    
    pragma solidity >=0.4.16;
    
    import {IERC20} from "../token/ERC20/IERC20.sol";
    
  • npm/@openzeppelin/contracts@5.4.0/token/ERC20/IERC20.sol
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/IERC20.sol)
    
    pragma solidity >=0.4.16;
    
    /**
     * @dev Interface of the ERC-20 standard as defined in the ERC.
     */
    interface IERC20 {
        /**
         * @dev Emitted when `value` tokens are moved from one account (`from`) to
         * another (`to`).
         *
         * Note that `value` may be zero.
         */
        event Transfer(address indexed from, address indexed to, uint256 value);
    
        /**
         * @dev Emitted when the allowance of a `spender` for an `owner` is set by
         * a call to {approve}. `value` is the new allowance.
         */
        event Approval(address indexed owner, address indexed spender, uint256 value);
    
        /**
         * @dev Returns the value of tokens in existence.
         */
        function totalSupply() external view returns (uint256);
    
        /**
         * @dev Returns the value of tokens owned by `account`.
         */
        function balanceOf(address account) external view returns (uint256);
    
        /**
         * @dev Moves a `value` amount of tokens from the caller's account to `to`.
         *
         * Returns a boolean value indicating whether the operation succeeded.
         *
         * Emits a {Transfer} event.
         */
        function transfer(address to, uint256 value) external returns (bool);
    
        /**
         * @dev Returns the remaining number of tokens that `spender` will be
         * allowed to spend on behalf of `owner` through {transferFrom}. This is
         * zero by default.
         *
         * This value changes when {approve} or {transferFrom} are called.
         */
        function allowance(address owner, address spender) external view returns (uint256);
    
        /**
         * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
         * caller's tokens.
         *
         * Returns a boolean value indicating whether the operation succeeded.
         *
         * IMPORTANT: Beware that changing an allowance with this method brings the risk
         * that someone may use both the old and the new allowance by unfortunate
         * transaction ordering. One possible solution to mitigate this race
         * condition is to first reduce the spender's allowance to 0 and set the
         * desired value afterwards:
         * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
         *
         * Emits an {Approval} event.
         */
        function approve(address spender, uint256 value) external returns (bool);
    
        /**
         * @dev Moves a `value` amount of tokens from `from` to `to` using the
         * allowance mechanism. `value` is then deducted from the caller's
         * allowance.
         *
         * Returns a boolean value indicating whether the operation succeeded.
         *
         * Emits a {Transfer} event.
         */
        function transferFrom(address from, address to, uint256 value) external returns (bool);
    }
    
  • npm/@openzeppelin/contracts@5.4.0/token/ERC20/utils/SafeERC20.sol
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.3.0) (token/ERC20/utils/SafeERC20.sol)
    
    pragma solidity ^0.8.20;
    
    import {IERC20} from "../IERC20.sol";
    import {IERC1363} from "../../../interfaces/IERC1363.sol";
    
    /**
     * @title SafeERC20
     * @dev Wrappers around ERC-20 operations that throw on failure (when the token
     * contract returns false). Tokens that return no value (and instead revert or
     * throw on failure) are also supported, non-reverting calls are assumed to be
     * successful.
     * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
     * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
     */
    library SafeERC20 {
        /**
         * @dev An operation with an ERC-20 token failed.
         */
        error SafeERC20FailedOperation(address token);
    
        /**
         * @dev Indicates a failed `decreaseAllowance` request.
         */
        error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);
    
        /**
         * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
         * non-reverting calls are assumed to be successful.
         */
        function safeTransfer(IERC20 token, address to, uint256 value) internal {
            _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));
        }
    
        /**
         * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
         * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
         */
        function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
            _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));
        }
    
        /**
         * @dev Variant of {safeTransfer} that returns a bool instead of reverting if the operation is not successful.
         */
        function trySafeTransfer(IERC20 token, address to, uint256 value) internal returns (bool) {
            return _callOptionalReturnBool(token, abi.encodeCall(token.transfer, (to, value)));
        }
    
        /**
         * @dev Variant of {safeTransferFrom} that returns a bool instead of reverting if the operation is not successful.
         */
        function trySafeTransferFrom(IERC20 token, address from, address to, uint256 value) internal returns (bool) {
            return _callOptionalReturnBool(token, abi.encodeCall(token.transferFrom, (from, to, value)));
        }
    
        /**
         * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
         * non-reverting calls are assumed to be successful.
         *
         * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
         * smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
         * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
         * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
         */
        function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
            uint256 oldAllowance = token.allowance(address(this), spender);
            forceApprove(token, spender, oldAllowance + value);
        }
    
        /**
         * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no
         * value, non-reverting calls are assumed to be successful.
         *
         * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
         * smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
         * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
         * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
         */
        function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {
            unchecked {
                uint256 currentAllowance = token.allowance(address(this), spender);
                if (currentAllowance < requestedDecrease) {
                    revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);
                }
                forceApprove(token, spender, currentAllowance - requestedDecrease);
            }
        }
    
        /**
         * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
         * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval
         * to be set to zero before setting it to a non-zero value, such as USDT.
         *
         * NOTE: If the token implements ERC-7674, this function will not modify any temporary allowance. This function
         * only sets the "standard" allowance. Any temporary allowance will remain active, in addition to the value being
         * set here.
         */
        function forceApprove(IERC20 token, address spender, uint256 value) internal {
            bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value));
    
            if (!_callOptionalReturnBool(token, approvalCall)) {
                _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0)));
                _callOptionalReturn(token, approvalCall);
            }
        }
    
        /**
         * @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no
         * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
         * targeting contracts.
         *
         * Reverts if the returned value is other than `true`.
         */
        function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
            if (to.code.length == 0) {
                safeTransfer(token, to, value);
            } else if (!token.transferAndCall(to, value, data)) {
                revert SafeERC20FailedOperation(address(token));
            }
        }
    
        /**
         * @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target
         * has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
         * targeting contracts.
         *
         * Reverts if the returned value is other than `true`.
         */
        function transferFromAndCallRelaxed(
            IERC1363 token,
            address from,
            address to,
            uint256 value,
            bytes memory data
        ) internal {
            if (to.code.length == 0) {
                safeTransferFrom(token, from, to, value);
            } else if (!token.transferFromAndCall(from, to, value, data)) {
                revert SafeERC20FailedOperation(address(token));
            }
        }
    
        /**
         * @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no
         * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
         * targeting contracts.
         *
         * NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}.
         * Opposedly, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall}
         * once without retrying, and relies on the returned value to be true.
         *
         * Reverts if the returned value is other than `true`.
         */
        function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
            if (to.code.length == 0) {
                forceApprove(token, to, value);
            } else if (!token.approveAndCall(to, value, data)) {
                revert SafeERC20FailedOperation(address(token));
            }
        }
    
        /**
         * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
         * on the return value: the return value is optional (but if data is returned, it must not be false).
         * @param token The token targeted by the call.
         * @param data The call data (encoded using abi.encode or one of its variants).
         *
         * This is a variant of {_callOptionalReturnBool} that reverts if call fails to meet the requirements.
         */
        function _callOptionalReturn(IERC20 token, bytes memory data) private {
            uint256 returnSize;
            uint256 returnValue;
            assembly ("memory-safe") {
                let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
                // bubble errors
                if iszero(success) {
                    let ptr := mload(0x40)
                    returndatacopy(ptr, 0, returndatasize())
                    revert(ptr, returndatasize())
                }
                returnSize := returndatasize()
                returnValue := mload(0)
            }
    
            if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {
                revert SafeERC20FailedOperation(address(token));
            }
        }
    
        /**
         * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
         * on the return value: the return value is optional (but if data is returned, it must not be false).
         * @param token The token targeted by the call.
         * @param data The call data (encoded using abi.encode or one of its variants).
         *
         * This is a variant of {_callOptionalReturn} that silently catches all reverts and returns a bool instead.
         */
        function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
            bool success;
            uint256 returnSize;
            uint256 returnValue;
            assembly ("memory-safe") {
                success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
                returnSize := returndatasize()
                returnValue := mload(0)
            }
            return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1);
        }
    }
    
  • npm/@openzeppelin/contracts@5.4.0/utils/introspection/IERC165.sol
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.4.0) (utils/introspection/IERC165.sol)
    
    pragma solidity >=0.4.16;
    
    /**
     * @dev Interface of the ERC-165 standard, as defined in the
     * https://eips.ethereum.org/EIPS/eip-165[ERC].
     *
     * Implementers can declare support of contract interfaces, which can then be
     * queried by others ({ERC165Checker}).
     *
     * For an implementation, see {ERC165}.
     */
    interface IERC165 {
        /**
         * @dev Returns true if this contract implements the interface defined by
         * `interfaceId`. See the corresponding
         * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section]
         * to learn more about how these ids are created.
         *
         * This function call must use less than 30 000 gas.
         */
        function supportsInterface(bytes4 interfaceId) external view returns (bool);
    }
    
  • project/contracts/libraries/LibGame.sol
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.28;
    
    import "./MafiaTypes.sol";
    import "./LibStorage.sol";
    import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
    import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
    
    /// @title LibGame — Shared helpers used across multiple facets
    library LibGame {
        // ---- Constants ----
        uint32 constant TURN_TIMEOUT = 2 minutes;
        uint32 constant PHASE_TIMEOUT = 3 minutes;
        // 90s — gives the GM server (60s timeout) a 30s buffer to land its
        // resolveNightAsGameMaster tx before any player can call forcePhaseTimeout.
        // Keep strictly GREATER than gm-server NIGHT_TIMEOUT_MS.
        uint32 constant NIGHT_TIMEOUT = 90 seconds;
        uint32 constant VOTING_TIMEOUT = 1 minutes;
        uint32 constant DAY_PER_PLAYER = 1 minutes; // DAY duration = aliveCount * DAY_PER_PLAYER
        uint32 constant MAX_ROUNDS = 50; // Hard cap — prevents infinite games from draining GM gas
        uint32 constant SESSION_DURATION = 4 hours;
        uint32 constant MAX_ARRAY_SIZE = 50;
        uint32 public constant CANCEL_GRACE_PERIOD = 2 hours; // 🆕 Grace period after which anyone can cancel tournament
        uint32 constant REGISTRATION_PERIOD = 24 hours;
        // AFK_COOLDOWN removed with the pre-DAY abort model: pre-DAY ghosters no
        // longer incur a penalty because the game isn't considered "started"
        // until DAY. See LibGame.abortPreGame + LobbyFacet.forfeitGame.
    
        using SafeERC20 for IERC20;
    
        // ---- Player flags (bitfield) ----
        uint32 constant FLAG_CONFIRMED_ROLE    = 0x1;
        uint32 constant FLAG_ACTIVE            = 0x2;
        uint32 constant FLAG_HAS_VOTED         = 0x4;
        uint32 constant FLAG_HAS_COMMITTED     = 0x8;
        uint32 constant FLAG_HAS_REVEALED      = 0x10;
        uint32 constant FLAG_HAS_SHARED_KEYS   = 0x20;
        uint32 constant FLAG_DECK_COMMITTED    = 0x40;
        uint32 constant FLAG_CLAIMED_MAFIA     = 0x80;
        uint32 constant FLAG_CLAIMED_DETECTIVE = 0x100;
        uint32 constant FLAG_FORFEITED         = 0x200;
    
        /// @dev Required mafia count for a given player count. Enforced in
        ///      revealRoles so a malicious host/GM cannot rig the deck composition.
        function expectedMafiaCount(uint8 n) internal pure returns (uint8) {
            if (n <= 5)  return 1;
            if (n <= 8)  return 2;
            if (n <= 11) return 3;
            return 4;
        }
    
        // ---- Errors ----
        error NotParticipant();
        error NotYourTurn();
        error WrongPhase();
        error Unauthorized();
        error RoomFull();
        error AlreadyJoined();
        error InvalidDeckSize();
        error TimeNotExpired();
        error PlayerInactive();
        error InvalidReveal();
        error AlreadyRevealed();
        error AlreadyVoted();
        error AlreadyCommitted();
        error AlreadySharedKeys();
        error SessionExpired();
        error SessionNotForThisRoom();
        error InvalidSessionKey();
        error InvalidArrayLength();
        error PhaseDeadlinePassed();
        error SessionAlreadyRegistered();
        error InvalidSessionAddress();
        error ArrayTooLarge();
        error NicknameTooLong();
        error PublicKeyTooLong();
        error RoleAlreadyCommitted();
        error RoleAlreadyRevealed();
        error InvalidRoleReveal();
        error NotEnoughPlayers();
        error NotAllRolesRevealed();
        error WinConditionNotMet();
        error RoleNotCommitted();
        error NotCommitted();
        error InvalidPlayerCount();
        error SaltTooLong();
        error RoomNameTooLong();
        error ContractPaused();
        error Reentrancy();
        error InsufficientDeposit();
        error DepositAlreadyRefunded();
        error NotGameMaster();
        error NotOwner();
        error TooEarly();
        error AlreadyClaimed();
    
        // ---- Events ----
        event RoomCreated(uint256 indexed roomId, address host, string name, uint256 maxPlayers);
        event PlayerJoined(uint256 indexed roomId, address indexed player, string nickname, address sessionKey);
        event GameStarted(uint256 indexed roomId);
        event DeckCommitted(uint256 indexed roomId, address player, bytes32 commitHash);
        event DeckRevealed(uint256 indexed roomId, address player, string[] deck);
        event PlayerKicked(uint256 indexed roomId, address player, string reason);
        event KeysSharedToAll(uint256 indexed roomId, address from);
        event AllKeysShared(uint256 indexed roomId);
        event RoleConfirmed(uint256 indexed roomId, address player);
        event AllRolesConfirmed(uint256 indexed roomId);
        event DayStarted(uint256 indexed roomId, uint256 dayNumber);
        event VotingStarted(uint256 indexed roomId);
        event VoteCast(uint256 indexed roomId, address voter, address target);
        event VotingFinalized(uint256 indexed roomId, address eliminated, uint256 voteCount);
        event NightStarted(uint256 indexed roomId);
        event NightActionCommitted(uint256 indexed roomId, address player, bytes32 commitHash);
        event NightActionRevealed(uint256 indexed roomId, address player, MafiaTypes.NightActionType action, address target);
        event NightFinalized(uint256 indexed roomId, address killed, address healed);
        event PhaseTimeout(uint256 indexed roomId, MafiaTypes.GamePhase phase);
        event PlayerEliminated(uint256 indexed roomId, address indexed player, string reason);
        event GameEnded(uint256 indexed roomId, string winCondition);
        event SessionKeyRegistered(address indexed mainWallet, address indexed sessionKey, uint256 roomId, uint256 expiresAt);
        event SessionKeyRevoked(address indexed mainWallet, address indexed sessionKey);
        event EmergencyPause(address indexed admin);
        event EmergencyUnpause(address indexed admin);
        event RoleCommitted(uint256 indexed roomId, address player, bytes32 commitHash);
        event RoleRevealed(uint256 indexed roomId, address player, MafiaTypes.Role role);
        event ZkVerifierUpdated(address indexed newVerifier);
        event GameMasterUpdated(address indexed newGameMaster);
        event NightResolvedByGM(uint256 indexed roomId, address killed, address healed);
        event DepositCollected(uint256 indexed roomId, address player, uint256 amount);
        event DepositSlashed(uint256 indexed roomId, address player, uint256 amount, string reason);
        event DepositRefunded(uint256 indexed roomId, address player, uint256 amount);
        event FeeWithdrawalInitiated(address indexed owner, uint256 amount, uint256 readyAt);
        event TournamentCreated(uint256 indexed tournamentId, address organizer, string name, uint256 buyIn);
        event TournamentJoined(uint256 indexed tournamentId, address indexed player);
        event TournamentCancelled(uint256 indexed tournamentId);
        event RefundFailed(uint256 indexed tournamentId, address player, uint256 amount);
        event RoomReturnedToLobby(uint256 indexed roomId);
        event TournamentFundsSwept(uint256 indexed tournamentId, address token, uint256 amount);
        // PR1: push refund failed, credited to pendingRefunds
        event RefundQueued(address indexed player, address token, uint256 amount);
        event RefundClaimed(address indexed player, address token, uint256 amount);
        // PR2a: AFK buy-in confiscated to platform fees
        event BuyInSlashed(uint256 indexed tournamentId, address indexed player, address token, uint256 amount, string reason);
        // PR2b: cooldown applied after AFK kick
        event AfkCooldownSet(address indexed player, uint32 until);
        // PR3 (D): separate event so log consumers can tell buy-in refunds from room-deposit refunds
        event BuyInRefunded(uint256 indexed tournamentId, address indexed player, address token, uint256 amount);
        // PR5 (Bug Y): dangling tournament participant exit
        event TournamentLeft(uint256 indexed tournamentId, address indexed player, uint256 refund);
        // GM gas reimbursement
        event GmGasCostReported(uint256 indexed roomId, uint128 amount);
        event SessionGasDrained(uint256 indexed roomId, address indexed player, uint128 gmShare, uint128 playerRefund);
    
        // ---- Reentrancy guard ----
        function nonReentrantBefore() internal {
            LibStorage.Storage storage ds = LibStorage.s();
            if (ds.reentrancyStatus == 2) revert Reentrancy();
            ds.reentrancyStatus = 2;
        }
    
        function nonReentrantAfter() internal {
            LibStorage.s().reentrancyStatus = 1;
        }
    
        // ---- Pause guard ----
        function requireNotPaused() internal view {
            if (LibStorage.s().paused) revert ContractPaused();
        }
    
        function requireOwner() internal view {
            if (msg.sender != LibStorage.s().owner) revert NotOwner();
        }
    
        // ---- Access helpers ----
        function resolvePlayer(uint256 roomId) internal view returns (address) {
            LibStorage.Storage storage ds = LibStorage.s();
            address potentialMain = ds.sessionToMain[msg.sender];
            if (potentialMain != address(0)) {
                MafiaTypes.SessionKey storage session = ds.sessionKeys[potentialMain];
                if (!session.isActive) revert InvalidSessionKey();
                if (block.timestamp > session.expiresAt) revert SessionExpired();
                if (session.roomId != uint64(roomId)) revert SessionNotForThisRoom();
                return potentialMain;
            }
            return msg.sender;
        }
    
        function requireActiveParticipant(uint256 roomId) internal view returns (address) {
            address player = resolvePlayer(roomId);
            LibStorage.Storage storage ds = LibStorage.s();
            if (!ds.isPlayerInRoom[roomId][player]) revert NotParticipant();
            if (!hasFlag(roomId, player, FLAG_ACTIVE)) revert PlayerInactive();
            return player;
        }
    
        function requireBeforeDeadline(uint256 roomId) internal view {
            uint32 deadline = LibStorage.s().rooms[roomId].phaseDeadline;
            if (deadline != 0 && block.timestamp > deadline) revert PhaseDeadlinePassed();
        }
    
        // ---- Safe index lookup ----
        function safePlayerIndex(uint256 roomId, address wallet) internal view returns (uint8) {
            LibStorage.Storage storage ds = LibStorage.s();
            require(ds.isPlayerInRoom[roomId][wallet], "Not in room");
            return ds.playerIndex[roomId][wallet];
        }
    
        // ---- Flag helpers ----
        function hasFlag(uint256 roomId, address wallet, uint32 flag) internal view returns (bool) {
            uint8 idx = safePlayerIndex(roomId, wallet);
            return (LibStorage.s().roomPlayers[roomId][idx].flags & flag) != 0;
        }
    
        function setFlag(uint256 roomId, address wallet, uint32 flag) internal {
            uint8 idx = safePlayerIndex(roomId, wallet);
            LibStorage.s().roomPlayers[roomId][idx].flags |= flag;
        }
    
        function clearFlag(uint256 roomId, address wallet, uint32 flag) internal {
            uint8 idx = safePlayerIndex(roomId, wallet);
            LibStorage.s().roomPlayers[roomId][idx].flags &= ~flag;
        }
    
        // ---- Player management ----
        function killPlayer(uint256 roomId, address victim) internal {
            if (!hasFlag(roomId, victim, FLAG_ACTIVE)) return;
            clearFlag(roomId, victim, FLAG_ACTIVE);
            LibStorage.s().rooms[roomId].aliveCount--;
        }
    
        function findNextActive(uint256 roomId, uint8 startIndex) internal view returns (uint8) {
            MafiaTypes.Player[] storage players = LibStorage.s().roomPlayers[roomId];
            for (uint8 i = startIndex; i < players.length; i++) {
                if ((players[i].flags & FLAG_ACTIVE) != 0) return i;
            }
            return uint8(players.length);
        }
    
        // ---- Phase transitions ----
        function transitionToDay(uint256 roomId) internal {
            MafiaTypes.GameRoom storage room = LibStorage.s().rooms[roomId];
    
            // Hard cap: end as DRAW if game exceeds MAX_ROUNDS to prevent
            // infinite games from draining GM server gas.
            if (room.dayCount >= MAX_ROUNDS) {
                endGame(roomId, "Draw: max rounds reached");
                return;
            }
    
            room.phase = MafiaTypes.GamePhase.DAY;
            room.dayCount++;
            room.lastActionTimestamp = uint32(block.timestamp);
            // DAY duration scales with alive players: each gets DAY_PER_PLAYER seconds
            room.phaseDeadline = uint32(block.timestamp + uint256(room.aliveCount) * DAY_PER_PLAYER);
            emit DayStarted(roomId, room.dayCount);
        }
    
        function transitionToNight(uint256 roomId) internal {
            LibStorage.Storage storage ds = LibStorage.s();
            MafiaTypes.GameRoom storage room = ds.rooms[roomId];
            room.phase = MafiaTypes.GamePhase.NIGHT;
            room.committedCount = 0;
            room.revealedCount = 0;
            room.phaseDeadline = uint32(block.timestamp + NIGHT_TIMEOUT);
    
            MafiaTypes.Player[] storage players = ds.roomPlayers[roomId];
            for (uint8 i = 0; i < players.length; i++) {
                address p = players[i].wallet;
                players[i].flags &= ~(FLAG_HAS_COMMITTED | FLAG_HAS_REVEALED | FLAG_CLAIMED_MAFIA | FLAG_CLAIMED_DETECTIVE);
                delete ds.nightCommits[roomId][p];
                delete ds.revealedActions[roomId][p];
                delete ds.revealedTargets[roomId][p];
            }
    
            ds.nightSubmittedByGM[roomId] = false;
            emit NightStarted(roomId);
        }
    
        function transitionToReveal(uint256 roomId) internal {
            LibStorage.Storage storage ds = LibStorage.s();
            MafiaTypes.GameRoom storage room = ds.rooms[roomId];
            room.phase = MafiaTypes.GamePhase.REVEAL;
            room.keysSharedCount = 0;
            room.confirmedCount = 0;
            room.phaseDeadline = uint32(block.timestamp + PHASE_TIMEOUT);
    
            MafiaTypes.Player[] storage players = ds.roomPlayers[roomId];
            for (uint8 i = 0; i < players.length; i++) {
                if ((players[i].flags & FLAG_ACTIVE) != 0) {
                    players[i].flags &= ~(FLAG_HAS_SHARED_KEYS | FLAG_CONFIRMED_ROLE | FLAG_DECK_COMMITTED);
                }
            }
        }
    
        // ---- Win condition ----
        function checkWinCondition(uint256 roomId) internal returns (bool) {
            MafiaTypes.GameRoom storage room = LibStorage.s().rooms[roomId];
            if (room.aliveCount <= 1) {
                endGame(roomId, room.aliveCount == 0 ? "Draw" : "Last player standing");
                return true;
            }
            if (allRolesRevealed(roomId)) {
                (bool mafiaWins, bool townWins) = calculateWinCondition(roomId);
                if (mafiaWins) {
                    LibStorage.s().gameResult[roomId] = true;
                    endGame(roomId, "Mafia wins");
                    return true;
                } else if (townWins) {
                    LibStorage.s().gameResult[roomId] = false;
                    endGame(roomId, "Town wins");
                    return true;
                }
            }
            return false;
        }
    
        function allRolesRevealed(uint256 roomId) internal view returns (bool) {
            LibStorage.Storage storage ds = LibStorage.s();
            MafiaTypes.Player[] storage players = ds.roomPlayers[roomId];
            for (uint8 i = 0; i < players.length; i++) {
                if ((players[i].flags & FLAG_ACTIVE) != 0 &&
                    ds.playerRoles[roomId][players[i].wallet] == MafiaTypes.Role.NONE) return false;
            }
            return true;
        }
    
        function calculateWinCondition(uint256 roomId) internal view returns (bool mafiaWins, bool townWins) {
            LibStorage.Storage storage ds = LibStorage.s();
            uint8 mafiaCount = 0;
            uint8 townCount = 0;
            MafiaTypes.Player[] storage players = ds.roomPlayers[roomId];
            for (uint8 i = 0; i < players.length; i++) {
                if ((players[i].flags & FLAG_ACTIVE) != 0) {
                    MafiaTypes.Role role = ds.playerRoles[roomId][players[i].wallet];
                    if (role == MafiaTypes.Role.MAFIA) mafiaCount++;
                    else if (role != MafiaTypes.Role.NONE) townCount++;
                }
            }
            return (mafiaCount > 0 && mafiaCount >= townCount, mafiaCount == 0 && townCount > 0);
        }
    
        function finalizeVotingInternal(uint256 roomId) internal {
            LibStorage.Storage storage ds = LibStorage.s();
            MafiaTypes.Player[] storage players = ds.roomPlayers[roomId];
    
            address eliminated = address(0);
            uint8 maxVotes = 0;
            bool tie = false;
    
            for (uint256 i = 0; i < players.length; i++) {
                if ((players[i].flags & FLAG_ACTIVE) == 0) continue;
                address p = players[i].wallet;
                uint8 v = ds.voteCounts[roomId][p];
                if (v > maxVotes) {
                    maxVotes = v;
                    eliminated = p;
                    tie = false;
                } else if (v == maxVotes && v > 0) {
                    tie = true;
                }
            }
    
            // Strict majority: maxVotes > aliveCount / 2 (equivalent to maxVotes >= floor(aliveCount/2)+1).
            // Matches the "Quorum: N" shown in GameLog and the ">50% of alive players" promise in HowToPlayModal.
            uint8 aliveCount = ds.rooms[roomId].aliveCount;
            bool hasMajority = uint256(maxVotes) * 2 > uint256(aliveCount);
    
            if (!tie && eliminated != address(0) && hasMajority) {
                killPlayer(roomId, eliminated);
                emit VotingFinalized(roomId, eliminated, maxVotes);
            } else {
                emit VotingFinalized(roomId, address(0), 0);
            }
    
            // Cleanup voteCounts to prevent pollution of next day or night repurpose
            for (uint256 i = 0; i < players.length; i++) {
                delete ds.voteCounts[roomId][players[i].wallet];
            }
    
            if (!checkWinCondition(roomId)) {
                transitionToNight(roomId);
            }
        }
    
        /// @notice Failsafe finalize for NIGHT phase. The on-chain mafia commit-reveal flow has
        ///         been removed — all kills come exclusively from `resolveNightAsGameMaster`. This
        ///         function only runs when the GM failed to resolve in time and a player calls
        ///         `forcePhaseTimeout`. It produces a peaceful night so the game does not get stuck.
        function finalizeNight(uint256 roomId) internal {
            emit NightFinalized(roomId, address(0), address(0));
            if (!checkWinCondition(roomId)) {
                transitionToDay(roomId);
            }
        }
    
        function endGame(uint256 roomId, string memory reason) internal {
            LibStorage.Storage storage ds = LibStorage.s();
            ds.rooms[roomId].phase = MafiaTypes.GamePhase.ENDED;
            ds.rooms[roomId].phaseDeadline = 0;
            emit GameEnded(roomId, reason);
        }
    
        // ---- Tournament room binding (PR5, Bugs X+Y) ----
        /// @dev Bind a player to a specific room of a tournament. Called from joinRoom,
        ///      joinTournamentAndRoom, and createAndJoinRoomInternal whenever the room
        ///      belongs to a tournament. Powers two things:
        ///      (1) distributeMafiaPrizes uses `roomBoundCount` as the denominator so
        ///          dangling `joinTournament`-only players don't skew the math.
        ///      (2) leaveTournament gates on `!tournamentRoomBound[tId][player]` so only
        ///          dangling players can exit through it.
        function bindToTournamentRoom(uint256 tournamentId, address player) internal {
            if (tournamentId == 0) return;
            LibStorage.Storage storage ds = LibStorage.s();
            if (ds.tournamentRoomBound[tournamentId][player]) return; // idempotent
            ds.tournamentRoomBound[tournamentId][player] = true;
            ds.tournaments[tournamentId].roomBoundCount += 1;
        }
    
        /// @dev Undo bindToTournamentRoom. Called from LOBBY-phase forfeit and AFK kick.
        function unbindFromTournamentRoom(uint256 tournamentId, address player) internal {
            if (tournamentId == 0) return;
            LibStorage.Storage storage ds = LibStorage.s();
            if (!ds.tournamentRoomBound[tournamentId][player]) return; // idempotent
            ds.tournamentRoomBound[tournamentId][player] = false;
            MafiaTypes.Tournament storage t = ds.tournaments[tournamentId];
            if (t.roomBoundCount > 0) t.roomBoundCount -= 1;
        }
    
        // ---- Tournament participant cleanup ----
        /// @dev Remove a player from `t.participants` via swap-and-pop. No-op if absent.
        ///      Must be called alongside `isTournamentParticipant[tId][player] = false`
        ///      and `t.prizePool -= t.buyIn` so the array, mapping, and pool stay in sync.
        function removeTournamentParticipant(uint256 tournamentId, address player) internal {
            MafiaTypes.Tournament storage t = LibStorage.s().tournaments[tournamentId];
            address[] storage arr = t.participants;
            uint256 len = arr.length;
            for (uint256 i = 0; i < len; i++) {
                if (arr[i] == player) {
                    if (i != len - 1) {
                        arr[i] = arr[len - 1];
                    }
                    arr.pop();
                    return;
                }
            }
        }
    
        // ---- Abort pre-DAY room (SHUFFLING / REVEAL / LOBBY-leave) ----
        /// @dev Terminate a room that hasn't reached DAY phase yet. The game is
        ///      NOT considered "started" until DAY. Any pre-DAY exit — one player
        ///      ghosted during shuffle, someone hit the phase-timeout, a voluntary
        ///      forfeit during shuffle/reveal — ends the room outright and refunds
        ///      EVERYONE fully (deposits, tournament buy-ins, caller-forwarded
        ///      session gas). No slashing, no cooldown, no penalty — the game
        ///      never started so there is nothing to punish.
        ///
        ///      Refund rules:
        ///      - Room deposit: refunded to every player who paid one.
        ///      - Tournament buy-in: refunded to every tournament participant in
        ///        this room, including the one who ghosted.
        ///      - Freeroll sponsor prize: refunded to the tournament organizer
        ///        ONLY when this abort drains the last room-bound players
        ///        (`roomBoundCount == 0`). Multi-room tournaments keep their pool
        ///        for the remaining rooms.
        ///      - Session gas: the forfeit caller can forward `msg.value` in
        ///        LobbyFacet.forfeitGame; it's credited to their deposit and
        ///        flows back through the refund loop below.
        ///
        ///      Push refunds go through `trySafeTransfer` so a single reverting
        ///      `receive()` can't DoS the whole abort — failed pushes queue in
        ///      `pendingRefunds` and the recipient can pull via RefundsFacet.
        ///
        ///      Callers: TimeoutsFacet.forcePhaseTimeout (SHUFFLING/REVEAL branch)
        ///      and LobbyFacet.forfeitGame (pre-DAY branch). Both gate the phase
        ///      check themselves; this helper doesn't re-check.
        function abortPreGame(uint256 roomId) internal {
            LibStorage.Storage storage ds = LibStorage.s();
            MafiaTypes.GameRoom storage room = ds.rooms[roomId];
            MafiaTypes.Player[] storage players = ds.roomPlayers[roomId];
            uint256 tId = room.tournamentId;
            address paymentToken = tId > 0 ? ds.tournaments[tId].paymentToken : address(0);
            // Snapshot for the freeroll-refund guard below. Legacy tournaments
            // created before the PR5 room-binding upgrade start at 0 and stay
            // at 0; we must NOT confuse that with "this abort drained the last
            // cohort" because doing so would double-spend a multi-room legacy
            // tournament's sponsor prize.
            uint8 roomBoundBefore = tId > 0 ? ds.tournaments[tId].roomBoundCount : 0;
    
            uint256 n = players.length;
            address[] memory refundPlayers = new address[](n);
            uint256[] memory refundNative = new uint256[](n);
            uint256[] memory refundErc20 = new uint256[](n);
    
            // ---- Effects: drain every player's claims from room state ----
            for (uint256 i = 0; i < n; i++) {
                address p = players[i].wallet;
                refundPlayers[i] = p;
    
                // 1. Room deposit (security stake) → back to player.
                uint128 deposit = ds.playerDeposits[roomId][p];
                if (deposit > 0) {
                    ds.playerDeposits[roomId][p] = 0;
                    room.depositPool -= deposit;
                    ds.totalLockedFundsByToken[address(0)] -= deposit;
                    refundNative[i] += deposit;
                    emit DepositRefunded(roomId, p, deposit);
                }
    
                // 2. Tournament buy-in → back to player (NO slashing in pre-DAY).
                if (tId > 0 && ds.isTournamentParticipant[tId][p]) {
                    ds.isTournamentParticipant[tId][p] = false;
                    removeTournamentParticipant(tId, p);
                    unbindFromTournamentRoom(tId, p);
    
                    MafiaTypes.Tournament storage t = ds.tournaments[tId];
                    if (t.buyIn > 0) {
                        uint128 bi = t.buyIn;
                        t.prizePool -= bi;
                        ds.totalLockedFundsByToken[paymentToken] -= bi;
                        if (paymentToken == address(0)) {
                            refundNative[i] += bi;
                        } else {
                            refundErc20[i] += bi;
                        }
                        emit BuyInRefunded(tId, p, paymentToken, bi);
                    }
                }
    
                // 3. Revoke session key — no cooldown.
                MafiaTypes.SessionKey storage session = ds.sessionKeys[p];
                if (session.isActive) {
                    emit SessionKeyRevoked(p, session.sessionAddress);
                    delete ds.sessionToMain[session.sessionAddress];
                    ds.isRegisteredSession[session.sessionAddress] = false;
                    session.isActive = false;
                }
    
                // 4. Scrub commit-reveal remnants.
                delete ds.deckCommits[roomId][p];
                delete ds.roleCommits[roomId][p];
    
                emit PlayerEliminated(roomId, p, "aborted");
            }
    
            // 5. Freeroll sponsor prize → organizer ONLY if this abort drained
            //    the LAST room-bound cohort (roomBoundBefore > 0 AND is now 0).
            //    Legacy tournaments with roomBoundBefore == 0 are skipped here —
            //    their sponsor prize can be reclaimed through the existing
            //    sweepStrandedTournamentFunds admin path. Multi-room tournaments
            //    keep the pool alive for their other rooms.
            if (tId > 0) {
                MafiaTypes.Tournament storage t = ds.tournaments[tId];
                if (
                    t.buyIn == 0 &&
                    t.prizePool > 0 &&
                    roomBoundBefore > 0 &&
                    t.roomBoundCount == 0
                ) {
                    uint128 remaining = t.prizePool;
                    t.prizePool = 0;
                    ds.totalLockedFundsByToken[t.paymentToken] -= remaining;
                    trySafeTransfer(t.paymentToken, t.organizer, remaining);
                    emit BuyInRefunded(tId, t.organizer, t.paymentToken, remaining);
                }
            }
    
            // 5b. Auto-cancel tournament if this abort drained the last room
            //     AND no dangling participants remain.
            if (tId > 0 && roomBoundBefore > 0) {
                MafiaTypes.Tournament storage t2 = ds.tournaments[tId];
                if (
                    t2.roomBoundCount == 0 &&
                    t2.participants.length == 0 &&
                    t2.phase != MafiaTypes.TournamentPhase.COMPLETED &&
                    t2.phase != MafiaTypes.TournamentPhase.CANCELLED
                ) {
                    t2.phase = MafiaTypes.TournamentPhase.CANCELLED;
                    if (ds.activeTournaments[t2.organizer] > 0) {
                        ds.activeTournaments[t2.organizer]--;
                    }
                    emit TournamentCancelled(tId);
                }
            }
    
            // 6. Close the room. Leave players[] / playerIndex / isPlayerInRoom
            //    intact so post-mortem views still resolve names & wallets.
            delete ds.revealedDeck[roomId];
            room.phase = MafiaTypes.GamePhase.ENDED;
            room.phaseDeadline = 0;
            room.lastActionTimestamp = uint32(block.timestamp);
            ds.prizesClaimed[roomId] = true;
            emit GameEnded(roomId, "Aborted pre-game");
    
            // ---- Interactions (CEI): flush refunds. trySafeTransfer queues
            //      unreachable recipients into pendingRefunds instead of reverting.
            for (uint256 i = 0; i < n; i++) {
                address to = refundPlayers[i];
                if (to == address(0)) continue;
                if (refundNative[i] > 0) {
                    trySafeTransfer(address(0), to, refundNative[i]);
                }
                if (refundErc20[i] > 0) {
                    trySafeTransfer(paymentToken, to, refundErc20[i]);
                }
            }
        }
    
        // ---- Session keys ----
        function registerSessionKey(address mainWallet, address sessionAddress, uint256 roomId) internal {
            LibStorage.Storage storage ds = LibStorage.s();
            if (sessionAddress == address(0)) revert InvalidSessionAddress();
            if (ds.isRegisteredSession[sessionAddress]) revert SessionAlreadyRegistered();
            if (ds.sessionToMain[sessionAddress] != address(0)) revert SessionAlreadyRegistered();
            // Prevent registering another player's main wallet as a session address.
            // If sessionAddress has an active session key entry, it is somebody's main
            // wallet — using it as a session key would let resolvePlayer hijack their
            // identity (sessionToMain[victim] → attacker).
            if (ds.sessionKeys[sessionAddress].isActive) revert SessionAlreadyRegistered();
    
            if (ds.sessionKeys[mainWallet].isActive) {
                address oldSession = ds.sessionKeys[mainWallet].sessionAddress;
                delete ds.sessionToMain[oldSession];
                ds.isRegisteredSession[oldSession] = false;
            }
    
            uint32 expiresAt = uint32(block.timestamp + SESSION_DURATION);
            ds.sessionKeys[mainWallet] = MafiaTypes.SessionKey({
                sessionAddress: sessionAddress,
                expiresAt: expiresAt,
                roomId: uint64(roomId),
                isActive: true
            });
            ds.sessionToMain[sessionAddress] = mainWallet;
            ds.isRegisteredSession[sessionAddress] = true;
            emit SessionKeyRegistered(mainWallet, sessionAddress, roomId, expiresAt);
        }
    
        // ---- Deposit helpers ----
        function collectDeposit(uint256 roomId, address player, uint128 required) internal {
            if (msg.value < required) revert InsufficientDeposit();
            LibStorage.Storage storage ds = LibStorage.s();
            uint128 amount = required;
            ds.playerDeposits[roomId][player] += amount;
            ds.rooms[roomId].depositPool += amount;
            ds.totalLockedFundsByToken[address(0)] += amount;
            emit DepositCollected(roomId, player, amount);
        }
    
        function slashDeposit(uint256 roomId, address player, string memory reason) internal {
            LibStorage.Storage storage ds = LibStorage.s();
            uint128 amount = ds.playerDeposits[roomId][player];
            if (amount > 0) {
                ds.playerDeposits[roomId][player] = 0;
                
                address token = address(0); // Default to native
                uint256 tId = ds.rooms[roomId].tournamentId;
                if (tId > 0) {
                    token = ds.tournaments[tId].paymentToken;
                }
    
                // If prizes already distributed, slashed funds go to platform fees
                if (ds.prizesClaimed[roomId]) {
                    ds.platformFeeBalances[token] += amount;
                    ds.totalLockedFundsByToken[token] -= amount;
                } else {
                    ds.rooms[roomId].depositPool -= amount;
                    ds.platformFeeBalances[token] += amount;
                    ds.totalLockedFundsByToken[token] -= amount;
                }
                emit DepositSlashed(roomId, player, amount, reason);
            }
        }
    
        function refundDeposit(uint256 roomId, address player) internal {
            LibStorage.Storage storage ds = LibStorage.s();
            if (ds.depositRefunded[roomId][player]) revert DepositAlreadyRefunded();
            uint128 amount = ds.playerDeposits[roomId][player];
            if (amount > 0) {
                address token = address(0);
                uint256 tId = ds.rooms[roomId].tournamentId;
                if (tId > 0) {
                    token = ds.tournaments[tId].paymentToken;
                }
    
                // Checks-Effects-Interactions (CEI)
                ds.depositRefunded[roomId][player] = true;
                ds.playerDeposits[roomId][player] = 0;
                ds.rooms[roomId].depositPool -= amount;
                ds.totalLockedFundsByToken[token] -= amount;
    
                safeTransfer(token, player, amount);
                emit DepositRefunded(roomId, player, amount);
            }
        }
    
        // ---- Signature Verification ----
        function verifyGmSignature(uint256 roomId, address player, bytes calldata signature) internal view {
            LibStorage.Storage storage ds = LibStorage.s();
            if (signature.length != 65) revert Unauthorized();
            
            // Include chainId to prevent cross-chain replay attacks
            bytes32 messageHash = keccak256(abi.encodePacked(block.chainid, roomId, player));
            bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash));
    
            bytes32 r;
            bytes32 s;
            uint8 v;
            assembly {
                r := calldataload(signature.offset)
                s := calldataload(add(signature.offset, 32))
                v := byte(0, calldataload(add(signature.offset, 64)))
            }
            if (v < 27) v += 27;
    
            address signer = ecrecover(ethHash, v, r, s);
            if (signer != ds.gameMaster) revert Unauthorized();
        }
    
        // ---- Token Helpers ----
    
        /**
         * @notice Safe transfer of native ETH or ERC20 tokens
         */
        function safeTransfer(address token, address to, uint256 amount) internal {
            if (amount == 0) return;
            if (token == address(0)) {
                (bool sent, ) = payable(to).call{value: amount}("");
                require(sent, "ETH transfer failed");
            } else {
                IERC20(token).safeTransfer(to, amount);
            }
        }
    
        /**
         * @notice Best-effort refund. Tries to push the payment; on failure (reverting
         *         receive(), ERC20 transfer reverts, etc.) credits the amount to
         *         `pendingRefunds[to][token]` so the recipient can pull it later via
         *         LobbyFacet.claimRefund. This removes the DoS vector where a single
         *         malicious contract player can freeze a whole refund loop.
         *
         *         Only use this in bulk refund loops (AFK kick, phase-abort cleanup).
         *         Normal single-player refunds can keep using `safeTransfer`.
         */
        function trySafeTransfer(address token, address to, uint256 amount) internal {
            if (amount == 0 || to == address(0)) return;
    
            if (token == address(0)) {
                // Capped gas so a receiver can't burn the whole block gas limit. 30k is
                // enough for a minimal bookkeeping receive(); a contract that needs more
                // should pull via claimRefund instead.
                (bool ok, ) = payable(to).call{value: amount, gas: 30000}("");
                if (ok) return;
            } else {
                // Raw call so a reverting token doesn't bubble up and take the whole tx
                // down with it. We accept legacy tokens that don't return a bool (data.length == 0).
                (bool ok, bytes memory data) = token.call(
                    abi.encodeWithSelector(IERC20.transfer.selector, to, amount)
                );
                bool transferOk = ok && (data.length == 0 || abi.decode(data, (bool)));
                if (transferOk) return;
            }
    
            // Push failed — queue for pull.
            LibStorage.s().pendingRefunds[to][token] += amount;
            emit RefundQueued(to, token, amount);
        }
    
        /**
         * @notice No-op under the new pre-DAY model — kept as a zero-op so old
         *         call sites (and the RefundsFacet view `getAfkCooldown`) remain
         *         compile-compatible. Pre-DAY exits no longer apply any cooldown
         *         because the game is not considered "started" until DAY, so
         *         there is nothing to punish. Any stale cooldown written by
         *         previous contract bytecode will naturally expire.
         */
        function requireNotAfkBlocked(address /* player */) internal pure {
            // intentionally empty
        }
    
        /**
         * @notice Safe transfer from user to contract (ERC20 only)
         */
        function safeReceive(address token, address from, uint256 amount) internal {
            if (amount == 0) return;
            if (token != address(0)) {
                IERC20(token).safeTransferFrom(from, address(this), amount);
            } else {
                // Native ETH is already in the contract via msg.value
                require(msg.value >= amount, "Insufficient ETH sent");
            }
        }
    
        function createTournamentInternal(
            string memory name,
            uint128 buyIn,
            uint8 maxPlayers,
            uint8 playersPerTable,
            bytes32 passwordHash,
            address paymentToken,
            uint128 initialPrize,
            uint128 sessionFee, // 🆕
            uint256 value // 🆕 explicit value handling
        ) internal {
    
            requireNotPaused();
            LibStorage.Storage storage ds = LibStorage.s();
            uint256 tId = ++ds.nextTournamentId;
            require(ds.activeTournaments[msg.sender] < 3, "Too many active tournaments");
            ds.activeTournaments[msg.sender]++;
    
            MafiaTypes.Tournament storage t = ds.tournaments[tId];
            t.id = tId;
            t.organizer = msg.sender;
            t.name = name;
            t.buyIn = buyIn;
            t.maxPlayers = maxPlayers;
            t.playersPerTable = playersPerTable;
            t.phase = MafiaTypes.TournamentPhase.REGISTRATION;
            t.registrationDeadline = uint32(block.timestamp + REGISTRATION_PERIOD);
            t.paymentToken = paymentToken;
            t.passwordHash = passwordHash;
            t.sessionFee = sessionFee;
    
    
            // If Freeroll (buyIn == 0), organizer must fund the prize pool
            if (buyIn == 0) {
                require(initialPrize > 0 || value > 0, "Freeroll requires prize pool");
                uint128 funding = (paymentToken == address(0)) ? uint128(value) : initialPrize;
                
                if (paymentToken != address(0)) {
                    safeReceive(paymentToken, msg.sender, funding);
                }
                
                t.prizePool = funding;
                ds.totalLockedFundsByToken[paymentToken] += funding;
            }
    
            emit TournamentCreated(tId, msg.sender, name, buyIn);
        }
    
        function joinTournamentInternal(uint256 tournamentId, string memory password, uint256 value) internal {
            requireNotPaused();
            requireNotAfkBlocked(msg.sender); // PR2b anti-grief cooldown
            LibStorage.Storage storage ds = LibStorage.s();
            MafiaTypes.Tournament storage t = ds.tournaments[tournamentId];
            require(t.phase == MafiaTypes.TournamentPhase.REGISTRATION, "Not in registration");
            require(block.timestamp <= t.registrationDeadline, "Registration closed");
            require(t.participants.length < t.maxPlayers, "Tournament full");
            require(!ds.isTournamentParticipant[tournamentId][msg.sender], "Already joined");
    
            // Whitelist check
            if (ds.tournamentWhitelistEnabled[tournamentId]) {
                require(ds.tournamentWhitelist[tournamentId][msg.sender], "Not whitelisted");
            }
    
            // Password check
            if (t.passwordHash != 0) {
                require(keccak256(abi.encodePacked(password)) == t.passwordHash, "Invalid password");
            }
    
            if (t.buyIn > 0 || t.sessionFee > 0) {
                if (t.paymentToken != address(0)) {
                    require(value >= t.sessionFee, "Must send STT for session gas");
                    safeReceive(t.paymentToken, msg.sender, t.buyIn);
                    ds.totalLockedFundsByToken[t.paymentToken] += t.buyIn;
                } else {
                    require(value >= uint256(t.buyIn) + t.sessionFee, "Insufficient ETH for buy-in and gas");
                    ds.totalLockedFundsByToken[address(0)] += t.buyIn;
                }
                t.prizePool += t.buyIn;
                
                // Store gas reserve for the session key
                if (t.sessionFee > 0) {
                    ds.tournamentGasReserves[tournamentId][msg.sender] = t.sessionFee;
                    ds.totalLockedFundsByToken[address(0)] += t.sessionFee;
                }
            }
    
    
            t.participants.push(msg.sender);
            ds.isTournamentParticipant[tournamentId][msg.sender] = true;
    
            emit TournamentJoined(tournamentId, msg.sender);
        }
    
        // ---- Lobby Helpers ----
    
        function createAndJoinRoomInternal(
            string memory roomName,
            uint8 maxPlayers,
            string memory nickname,
            bytes memory publicKey,
            address sessionAddress,
            bool isPrivate,
            uint256 tournamentId,
            uint256 value // 🆕 explicit value handling for atomic calls
        ) internal returns (uint256) {
            requireNotPaused();
            requireNotAfkBlocked(msg.sender); // PR2b anti-grief cooldown
    
            if (maxPlayers < 4 || maxPlayers > 16) revert InvalidPlayerCount();
            if (bytes(roomName).length > 32) revert RoomNameTooLong();
            if (bytes(nickname).length > 128) revert NicknameTooLong();
            if (publicKey.length > 1024) revert PublicKeyTooLong();
    
            LibStorage.Storage storage ds = LibStorage.s();
    
            // Non-refundable entry fee → platform revenue.
            uint128 fee = ds.entryFee;
            if (fee > 0) {
                require(value >= fee, "Insufficient ETH for entry fee");
                ds.platformFeeBalances[address(0)] += fee;
                value -= fee;
            }
    
            uint256 roomId = ++ds.nextRoomId;
            uint128 depositRequired = ds.defaultDeposit;
    
            ds.rooms[roomId] = MafiaTypes.GameRoom({
                id: uint64(roomId),
                host: msg.sender,
                name: roomName,
                phase: MafiaTypes.GamePhase.LOBBY,
                maxPlayers: maxPlayers,
                playersCount: 0,
                aliveCount: 0,
                dayCount: 0,
                currentShufflerIndex: 0,
                lastActionTimestamp: uint32(block.timestamp),
                phaseDeadline: 0,
                confirmedCount: 0,
                votedCount: 0,
                committedCount: 0,
                revealedCount: 0,
                keysSharedCount: 0,
                depositPool: 0,
                depositPerPlayer: depositRequired,
                isPrivate: isPrivate,
                tournamentId: tournamentId
            });
    
            emit RoomCreated(roomId, msg.sender, roomName, maxPlayers);
    
            ds.roomPlayers[roomId].push(MafiaTypes.Player({
                wallet: msg.sender,
                nickname: nickname,
                publicKey: publicKey,
                flags: FLAG_ACTIVE
            }));
    
            ds.isPlayerInRoom[roomId][msg.sender] = true;
            ds.playerIndex[roomId][msg.sender] = 0;
            ds.rooms[roomId].playersCount = 1;
            ds.rooms[roomId].aliveCount = 1;
    
            // Collect deposit if required (even in tournaments, as a security measure/extra prize)
            if (depositRequired > 0) {
                require(value >= depositRequired, "Insufficient ETH for deposit");
                ds.playerDeposits[roomId][msg.sender] += depositRequired;
                ds.rooms[roomId].depositPool += depositRequired;
                ds.totalLockedFundsByToken[address(0)] += depositRequired;
                emit DepositCollected(roomId, msg.sender, depositRequired);
            }
    
            // Tournament membership check - only if buy-in > 0
            if (tournamentId != 0) {
                MafiaTypes.Tournament storage t = ds.tournaments[tournamentId];
                if (t.buyIn > 0) {
                    require(
                        ds.isTournamentParticipant[tournamentId][msg.sender],
                        "Must join tournament first"
                    );
                }
                // PR5: bind creator to this tournament room so distribute math counts them
                bindToTournamentRoom(tournamentId, msg.sender);
            }
    
            if (sessionAddress != address(0)) {
                registerSessionKey(msg.sender, sessionAddress, roomId);
                
                uint256 sessionFunds = value > depositRequired ? value - depositRequired : 0;
                
                // 🆕 Pull from tournament gas reserve if available
                if (tournamentId > 0 && ds.tournamentGasReserves[tournamentId][msg.sender] > 0) {
                    uint128 reserve = ds.tournamentGasReserves[tournamentId][msg.sender];
                    ds.tournamentGasReserves[tournamentId][msg.sender] = 0;
                    ds.totalLockedFundsByToken[address(0)] -= reserve;
                    sessionFunds += reserve;
                }
    
                if (sessionFunds > 0) {
                    (bool sent, ) = payable(sessionAddress).call{value: sessionFunds}("");
                    require(sent, "Failed to fund session");
                }
            }
    
    
            emit PlayerJoined(roomId, msg.sender, nickname, sessionAddress);
    
            return roomId;
        }
    }
    
  • project/contracts/libraries/LibStorage.sol
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.28;
    
    import "./MafiaTypes.sol";
    
    /// @title MafiaStorage — Shared storage layout for all facets (Diamond storage pattern)
    /// @dev All facets use `LibStorage.s()` to access the same storage slot, avoiding collisions.
    library LibStorage {
        bytes32 constant STORAGE_SLOT = keccak256("mafia.game.storage.v1");
    
        struct Storage {
            // ---- Core game state ----
            mapping(uint256 => MafiaTypes.GameRoom) rooms;
            mapping(uint256 => MafiaTypes.Player[]) roomPlayers;
            mapping(uint256 => mapping(address => bool)) isPlayerInRoom;
            mapping(uint256 => mapping(address => uint8)) playerIndex;
    
            // ---- Shuffle / Deck ----
            mapping(uint256 => mapping(address => MafiaTypes.DeckCommit)) deckCommits;
            mapping(uint256 => string[]) revealedDeck;
            mapping(uint256 => mapping(address => mapping(address => bytes))) playerDeckKeys;
    
            // ---- Voting ----
            mapping(uint256 => mapping(address => address)) votes;
            mapping(uint256 => mapping(address => uint8)) voteCounts;
    
            // ---- Night ----
            mapping(uint256 => mapping(address => MafiaTypes.NightCommit)) nightCommits;
            mapping(uint256 => mapping(address => MafiaTypes.NightActionType)) revealedActions;
            mapping(uint256 => mapping(address => address)) revealedTargets;
    
            // ---- Roles ----
            mapping(uint256 => mapping(address => MafiaTypes.Role)) playerRoles;
            mapping(uint256 => mapping(address => bytes32)) roleCommits;
    
            // ---- Mafia coordination (DEPRECATED) ----
            // The on-chain mafia commit-reveal flow has been removed (NightFacet rewrite).
            // Mafia chat moved off-chain to LiveKit signaling. These slots are kept ONLY to
            // preserve the Diamond storage layout for upgradability — DO NOT reuse, DO NOT
            // remove, DO NOT reorder. Adding fields BELOW this block is safe.
            mapping(uint256 => MafiaTypes.MafiaMessage[]) _deprecated_mafiaChat;
            mapping(uint256 => mapping(address => MafiaTypes.MafiaTargetCommit)) _deprecated_mafiaTargetCommits;
            mapping(uint256 => uint8) _deprecated_mafiaCommittedCount;
            mapping(uint256 => uint8) _deprecated_mafiaRevealedCount;
            mapping(uint256 => address) _deprecated_mafiaConsensusTarget;
    
            // ---- Session keys ----
            mapping(address => MafiaTypes.SessionKey) sessionKeys;
            mapping(address => address) sessionToMain;
            mapping(address => bool) isRegisteredSession;
    
            // ---- Game Master ----
            address gameMaster;                     // trusted server for night actions
            mapping(uint256 => bool) nightSubmittedByGM; // GM already submitted night result
    
            // ---- Deposit / Slashing ----
            uint128 defaultDeposit;                 // global default deposit amount
            uint128 entryFee;                       // non-refundable fee charged on room entry → platformFeeBalances
            mapping(uint256 => mapping(address => uint128)) playerDeposits;  // per-player deposit
            mapping(uint256 => mapping(address => bool)) depositRefunded;
    
            // ---- ZK ----
            address zkVerifier;
            address poseidon2;
            mapping(uint256 => bool) gameResult;    // true = mafia won, false = town won
            mapping(uint256 => bool) prizesClaimed; // track if prizes were distributed
            mapping(bytes32 => bool) proofNullifiers; // 🆕 Prevent ZK proof replay attacks
    
            // ---- Admin & Funds ----
            uint256 nextRoomId;
            uint256 nextTournamentId;
            mapping(uint256 => MafiaTypes.Tournament) tournaments;
            mapping(address => uint256) platformFeeBalances; // 🆕 per-token fees
            mapping(address => uint256) totalLockedFundsByToken; // 🆕 per-token locked funds
            mapping(address => uint8) activeTournaments; // 🆕 Track active tournaments per organizer
            mapping(address => uint32) feeWithdrawalReadyAt; // 🆕 Timelock per token
            mapping(uint256 => mapping(address => bool)) isTournamentParticipant; // 🆕 Prevent double join
            mapping(uint256 => bool) tournamentWhitelistEnabled; // 🆕 Toggle for whitelist
            mapping(uint256 => mapping(address => bool)) tournamentWhitelist; // 🆕 Whitelisted addresses per tournament
            mapping(uint256 => mapping(address => uint128)) tournamentGasReserves; // 🆕 STT reserves for session keys
    
    
            // ---- Reentrancy ----
            uint256 reentrancyStatus;
    
            // ---- Pause ----
            bool paused;
    
            // ---- Owner (synced from Diamond) ----
            address owner;
    
            // ---- Pull-based refunds (PR1 DoS fix) ----
            // Credited when a push refund (.call / ERC20 transfer) to a player fails, e.g.
            // when the player wallet is a contract with a reverting receive(). The player
            // can withdraw via LobbyFacet.claimRefund(token). Mapping: user => token => amount
            // (token == address(0) means native STT/ETH).
            mapping(address => mapping(address => uint256)) pendingRefunds;
    
            // ---- Anti-grief cooldown (PR2b) ----
            // Unix timestamp until which an address is blocked from joining any room or
            // tournament. Set when the player is AFK-kicked in the pre-DAY phase so the
            // same wallet can't immediately rejoin and restart the grief loop.
            mapping(address => uint32) afkCooldownUntil;
    
            // ---- Tournament room-binding (PR5, Bug Y fix) ----
            // Tracks whether a tournament participant is currently bound to a specific
            // room of that tournament. Used as the guard for leaveTournament (only
            // dangling participants — those who never joined a room — can leave through
            // it). The per-tournament counter lives in Tournament.roomBoundCount and is
            // what distributeMafiaPrizes uses as the denominator.
            mapping(uint256 => mapping(address => bool)) tournamentRoomBound;
    
            // ---- GM gas reimbursement ----
            // Total gas cost the GM spent on a room (reported by GM after game end).
            // drainSessionGas divides this by playersCount and deducts from each
            // session wallet's remaining balance before refunding the player.
            mapping(uint256 => uint128) gmGasCost;
            mapping(uint256 => mapping(address => bool)) sessionDrained;
        }
    
        function s() internal pure returns (Storage storage ds) {
            bytes32 slot = STORAGE_SLOT;
            assembly { ds.slot := slot }
        }
    }
    
  • project/contracts/libraries/MafiaTypes.sol
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.28;
    
    /// @title MafiaTypes — Shared types for Mafia game
    library MafiaTypes {
        enum GamePhase { LOBBY, SHUFFLING, REVEAL, DAY, VOTING, NIGHT, ENDED }
        enum NightActionType { NONE, KILL, HEAL, CHECK }
        enum Role { NONE, MAFIA, DOCTOR, DETECTIVE, CITIZEN }
    
        struct Player {
            address wallet;
            string nickname;
            bytes publicKey;
            uint32 flags;
        }
    
        struct GameRoom {
            uint64 id;
            address host;
            string name;
            GamePhase phase;
            uint8 maxPlayers;
            uint8 playersCount;
            uint8 aliveCount;
            uint16 dayCount;
            uint8 currentShufflerIndex;
            uint32 lastActionTimestamp;
            uint32 phaseDeadline;
            uint8 confirmedCount;
            uint8 votedCount;
            uint8 committedCount;
            uint8 revealedCount;
            uint8 keysSharedCount;
            uint128 depositPool;    // total ETH deposited for this room
            uint128 depositPerPlayer; // required deposit amount
            bool isPrivate;         // 🆕 Requires GM signature to join
            uint256 tournamentId;   // 0 = standalone, >0 = part of tournament
        }
    
        struct SessionKey {
            address sessionAddress;
            uint32 expiresAt;
            uint64 roomId;
            bool isActive;
        }
    
        struct NightCommit {
            bytes32 commitHash;
            bool revealed;
            uint32 commitTime;
        }
    
        struct DeckCommit {
            bytes32 commitHash;
            string[] deck;
            bool revealed;
        }
    
        struct MafiaMessage {
            bytes encryptedMessage;
            uint32 timestamp;
            address sender;
        }
    
        enum TournamentPhase { REGISTRATION, IN_PROGRESS, COMPLETED, CANCELLED }
    
        struct Tournament {
            uint256 id;
            address organizer;
            string name;
            uint128 buyIn;            // 0 = freeroll
            uint128 prizePool;        // Isolated tournament funds
            uint128 sessionFee;       // 🆕 Mandatory STT deposit for session key gas
            uint128 platformFeePool;  // Platform fee from this tournament
    
            address paymentToken;     // address(0) = native, otherwise ERC20
            uint8 maxPlayers;
            uint8 playersPerTable;
            uint8 currentRound;
            TournamentPhase phase;
            bytes32 passwordHash;     // 0x0 = open
            bool prizesClaimed;       // Have prizes been distributed?
            uint32 registrationDeadline; // 🆕 Timeout for tournament start
            address[] participants;
    
            // PR5 (Bug X fix): multi-room distribution accounting.
            // Both appended at the end of the struct → diamond-storage safe
            // because Tournament is only stored in a `mapping(uint256 => Tournament)`.
            // Legacy tournaments created before the upgrade will read these as 0;
            // distributeMafiaPrizes falls back to the old math when roomBoundCount == 0.
            uint8 roomBoundCount;         // players currently bound to a room of this tournament
            uint8 distributedPlayerCount; // cumulative players already counted in a completed distribute
        }
    
        struct MafiaTargetCommit {
            bytes32 commitHash;
            address target;
            bool revealed;
        }
    }
    
ABI · 36 entries
json
[
  {
    "name": "AlreadyCommitted",
    "type": "error",
    "inputs": []
  },
  {
    "name": "AlreadyRevealed",
    "type": "error",
    "inputs": []
  },
  {
    "name": "AlreadySharedKeys",
    "type": "error",
    "inputs": []
  },
  {
    "name": "ArrayTooLarge",
    "type": "error",
    "inputs": []
  },
  {
    "name": "ContractPaused",
    "type": "error",
    "inputs": []
  },
  {
    "name": "InvalidArrayLength",
    "type": "error",
    "inputs": []
  },
  {
    "name": "InvalidDeckSize",
    "type": "error",
    "inputs": []
  },
  {
    "name": "InvalidReveal",
    "type": "error",
    "inputs": []
  },
  {
    "name": "InvalidSessionAddress",
    "type": "error",
    "inputs": []
  },
  {
    "name": "InvalidSessionKey",
    "type": "error",
    "inputs": []
  },
  {
    "name": "NotParticipant",
    "type": "error",
    "inputs": []
  },
  {
    "name": "NotYourTurn",
    "type": "error",
    "inputs": []
  },
  {
    "name": "PhaseDeadlinePassed",
    "type": "error",
    "inputs": []
  },
  {
    "name": "PlayerInactive",
    "type": "error",
    "inputs": []
  },
  {
    "name": "Reentrancy",
    "type": "error",
    "inputs": []
  },
  {
    "name": "RoleAlreadyCommitted",
    "type": "error",
    "inputs": []
  },
  {
    "name": "RoleNotCommitted",
    "type": "error",
    "inputs": []
  },
  {
    "name": "SessionExpired",
    "type": "error",
    "inputs": []
  },
  {
    "name": "SessionNotForThisRoom",
    "type": "error",
    "inputs": []
  },
  {
    "name": "WrongPhase",
    "type": "error",
    "inputs": []
  },
  {
    "name": "AllKeysShared",
    "type": "event",
    "inputs": [
      {
        "name": "roomId",
        "type": "uint256",
        "indexed": true,
        "internalType": "uint256"
      }
    ],
    "anonymous": false
  },
  {
    "name": "AllRolesConfirmed",
    "type": "event",
    "inputs": [
      {
        "name": "roomId",
        "type": "uint256",
        "indexed": true,
        "internalType": "uint256"
      }
    ],
    "anonymous": false
  },
  {
    "name": "DayStarted",
    "type": "event",
    "inputs": [
      {
        "name": "roomId",
        "type": "uint256",
        "indexed": true,
        "internalType": "uint256"
      },
      {
        "name": "dayNumber",
        "type": "uint256",
        "indexed": false,
        "internalType": "uint256"
      }
    ],
    "anonymous": false
  },
  {
    "name": "DeckCommitted",
    "type": "event",
    "inputs": [
      {
        "name": "roomId",
        "type": "uint256",
        "indexed": true,
        "internalType": "uint256"
      },
      {
        "name": "player",
        "type": "address",
        "indexed": false,
        "internalType": "address"
      },
      {
        "name": "commitHash",
        "type": "bytes32",
        "indexed": false,
        "internalType": "bytes32"
      }
    ],
    "anonymous": false
  },
  {
    "name": "DeckRevealed",
    "type": "event",
    "inputs": [
      {
        "name": "roomId",
        "type": "uint256",
        "indexed": true,
        "internalType": "uint256"
      },
      {
        "name": "player",
        "type": "address",
        "indexed": false,
        "internalType": "address"
      },
      {
        "name": "deck",
        "type": "string[]",
        "indexed": false,
        "internalType": "string[]"
      }
    ],
    "anonymous": false
  },
  {
    "name": "GameEnded",
    "type": "event",
    "inputs": [
      {
        "name": "roomId",
        "type": "uint256",
        "indexed": true,
        "internalType": "uint256"
      },
      {
        "name": "winCondition",
        "type": "string",
        "indexed": false,
        "internalType": "string"
      }
    ],
    "anonymous": false
  },
  {
    "name": "KeysSharedToAll",
    "type": "event",
    "inputs": [
      {
        "name": "roomId",
        "type": "uint256",
        "indexed": true,
        "internalType": "uint256"
      },
      {
        "name": "from",
        "type": "address",
        "indexed": false,
        "internalType": "address"
      }
    ],
    "anonymous": false
  },
  {
    "name": "RoleCommitted",
    "type": "event",
    "inputs": [
      {
        "name": "roomId",
        "type": "uint256",
        "indexed": true,
        "internalType": "uint256"
      },
      {
        "name": "player",
        "type": "address",
        "indexed": false,
        "internalType": "address"
      },
      {
        "name": "commitHash",
        "type": "bytes32",
        "indexed": false,
        "internalType": "bytes32"
      }
    ],
    "anonymous": false
  },
  {
    "name": "RoleConfirmed",
    "type": "event",
    "inputs": [
      {
        "name": "roomId",
        "type": "uint256",
        "indexed": true,
        "internalType": "uint256"
      },
      {
        "name": "player",
        "type": "address",
        "indexed": false,
        "internalType": "address"
      }
    ],
    "anonymous": false
  },
  {
    "name": "commitAndConfirmRole",
    "type": "function",
    "inputs": [
      {
        "name": "roomId",
        "type": "uint256",
        "internalType": "uint256"
      },
      {
        "name": "roleHash",
        "type": "bytes32",
        "internalType": "bytes32"
      }
    ],
    "outputs": [],
    "stateMutability": "nonpayable"
  },
  {
    "name": "commitDeck",
    "type": "function",
    "inputs": [
      {
        "name": "roomId",
        "type": "uint256",
        "internalType": "uint256"
      },
      {
        "name": "deckHash",
        "type": "bytes32",
        "internalType": "bytes32"
      }
    ],
    "outputs": [],
    "stateMutability": "nonpayable"
  },
  {
    "name": "commitRole",
    "type": "function",
    "inputs": [
      {
        "name": "roomId",
        "type": "uint256",
        "internalType": "uint256"
      },
      {
        "name": "roleHash",
        "type": "bytes32",
        "internalType": "bytes32"
      }
    ],
    "outputs": [],
    "stateMutability": "nonpayable"
  },
  {
    "name": "confirmRole",
    "type": "function",
    "inputs": [
      {
        "name": "roomId",
        "type": "uint256",
        "internalType": "uint256"
      }
    ],
    "outputs": [],
    "stateMutability": "nonpayable"
  },
  {
    "name": "getRevealedDeck",
    "type": "function",
    "inputs": [
      {
        "name": "roomId",
        "type": "uint256",
        "internalType": "uint256"
      },
      {
        "name": "index",
        "type": "uint256",
        "internalType": "uint256"
      }
    ],
    "outputs": [
      {
        "name": "",
        "type": "string",
        "internalType": "string"
      }
    ],
    "stateMutability": "view"
  },
  {
    "name": "revealDeck",
    "type": "function",
    "inputs": [
      {
        "name": "roomId",
        "type": "uint256",
        "internalType": "uint256"
      },
      {
        "name": "deck",
        "type": "string[]",
        "internalType": "string[]"
      },
      {
        "name": "salt",
        "type": "string",
        "internalType": "string"
      }
    ],
    "outputs": [],
    "stateMutability": "nonpayable"
  },
  {
    "name": "shareKeysToAll",
    "type": "function",
    "inputs": [
      {
        "name": "roomId",
        "type": "uint256",
        "internalType": "uint256"
      },
      {
        "name": "recipients",
        "type": "address[]",
        "internalType": "address[]"
      },
      {
        "name": "encryptedKeys",
        "type": "bytes[]",
        "internalType": "bytes[]"
      }
    ],
    "outputs": [],
    "stateMutability": "nonpayable"
  }
]

Send feedback

Agents: POST the same to /api/feedback.