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"
}
]