Verified Contract
0x420DA43A7DfA95a92506019588bbB70f256bf7aB
Pin to watchlist
scan to copy
zkLTC Balance
82.8462
hard money native
Transactions
204,469
Token transfers
5
Validations
0
blocks produced
Hard Money Score
100
/100
PURE HARD MONEY
share of activity in native zkLTC vs. tokens
native
204,469
tokens
5
Contract name
LitGamesMinesMain
Compiler
v0.8.24+commit.e11b9ed9
Optimization
enabled · 300 runs
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
/// @title LitGamesMinesMain
/// @notice Long-lived LitGames Mines contract with owner-managed stake options.
/// @dev Flow: createGame (on-chain) -> reveal off-chain -> finalizeCashOut/finalizeLoss (on-chain)
contract LitGamesMinesMain is Ownable2Step, Pausable, ReentrancyGuard, EIP712 {
uint8 public constant TOTAL_CELLS = 16;
uint32 public constant BPS = 10_000;
uint32 public constant RTP_BPS = 9_500;
uint8 public constant MIN_BOMBS = 2;
uint8 public constant MAX_BOMBS = 5;
uint8 public constant MAX_ALLOWED_STAKE_OPTIONS = 16;
uint64 public constant MIN_GAME_TIMEOUT = 15 seconds;
uint64 public constant MAX_GAME_TIMEOUT = 15 minutes;
uint32 public constant MIN_LOCK_MULTIPLIER_BPS = BPS;
uint32 public constant MAX_LOCK_MULTIPLIER_BPS = 5_000_000; // 500x
uint64 public constant MAX_TIMEOUT_COOLDOWN = 1 days;
bytes32 private constant GAME_QUOTE_TYPEHASH =
keccak256(
"GameQuote(address player,uint96 stake,uint8 bombs,bytes32 boardRoot,uint64 validUntil,uint256 nonce)"
);
error InvalidSigner();
error InvalidQuote();
error QuoteExpired();
error NonceAlreadyUsed();
error InsufficientStake();
error InvalidBombCount();
error InvalidCell();
error CellAlreadyOpened();
error InvalidProof();
error InvalidRevealBatch();
error GameNotFound();
error NotGamePlayer();
error GameResolved();
error GameStillActive();
error NoSafeCellsOpened();
error NotEnoughBankroll();
error InvalidAddress();
error LiquidityLocked();
error NotAuthorizedRelayer();
error PlayerHasActiveGame();
error SessionNotAuthorized();
error SessionExpired();
error StakeAboveSessionLimit();
error InsufficientPlayerBalance();
error InvalidStakeOption();
error InvalidGameTimeout();
error InvalidLockMultiplier();
error InvalidTimeoutCooldown();
error NoPendingPayout();
error PendingPayoutNotReady();
error CooldownActive();
struct GameQuote {
address player;
uint96 stake;
uint8 bombs;
bytes32 boardRoot;
uint64 validUntil;
uint256 nonce;
}
struct Game {
address player;
uint96 stake;
uint96 maxPayout;
uint96 reservedLiquidity;
uint96 payout;
uint8 bombs;
uint8 safeOpened;
uint32 resultMultiplierBps;
uint16 openedMask;
uint64 createdAt;
uint64 timeoutAt;
bool resolved;
bool won;
bool usesSessionBalance;
bytes32 boardRoot;
bytes32 gameKey;
}
struct SessionConfig {
address sessionKey;
uint64 validUntil;
uint96 maxStake;
}
address public quoteSigner;
uint256 public nextGameId = 1;
uint256 public lockedLiquidity;
uint256 public totalPlayerBalances;
uint256 public totalPendingPayouts;
uint64 public gameTimeout;
uint64 public timeoutCooldown = 60;
uint32 public lockMultiplier2BombsBps = 100_000; // 10x
uint32 public lockMultiplier3BombsBps = 100_000; // 10x
uint32 public lockMultiplier4BombsBps = 100_000; // 10x
uint32 public lockMultiplier5BombsBps = 100_000; // 10x
uint96[] private allowedStakes;
mapping(uint256 => Game) public games;
mapping(address => mapping(uint256 => bool)) public nonceUsed;
mapping(address => uint256) public activeGameByPlayer;
mapping(address => bool) public relayers;
mapping(uint96 => bool) public isAllowedStake;
mapping(address => uint256) public playerBalances;
mapping(address => SessionConfig) public sessions;
mapping(address => uint256) public pendingPayouts;
mapping(address => uint64) public nextPlayableAt;
mapping(address => uint32) public timeoutRefundStreak;
event QuoteSignerUpdated(address indexed previousSigner, address indexed newSigner);
event BankrollFunded(address indexed sender, uint256 amount, uint256 balanceAfter);
event BankrollWithdrawn(address indexed to, uint256 amount, uint256 balanceAfter);
event GameCreated(
uint256 indexed gameId,
address indexed player,
uint96 stake,
uint8 bombs,
bytes32 boardRoot,
uint96 maxPayout,
bytes32 gameKey
);
event GameFinalized(
uint256 indexed gameId,
address indexed player,
bool won,
uint8 safeOpened,
uint32 multiplierBps,
uint96 payout
);
event RelayerUpdated(address indexed relayer, bool enabled);
event GameRefunded(uint256 indexed gameId, address indexed player, uint96 stake);
event PausedByOwner(address indexed account);
event UnpausedByOwner(address indexed account);
event SessionConfigured(address indexed player, address indexed sessionKey, uint64 validUntil, uint96 maxStake);
event SessionRevoked(address indexed player);
event PlayerBalanceDeposited(address indexed player, uint256 amount, uint256 balanceAfter);
event PlayerBalanceWithdrawn(address indexed player, uint256 amount, uint256 balanceAfter);
event AllowedStakesUpdated(uint96[] stakes);
event GameTimeoutUpdated(uint64 previousTimeout, uint64 nextTimeout);
event TimeoutCooldownUpdated(uint64 previousCooldown, uint64 nextCooldown);
event LockMultipliersUpdated(
uint32 prev2BombsBps,
uint32 prev3BombsBps,
uint32 prev4BombsBps,
uint32 prev5BombsBps,
uint32 next2BombsBps,
uint32 next3BombsBps,
uint32 next4BombsBps,
uint32 next5BombsBps
);
event PayoutDeferred(uint256 indexed gameId, address indexed player, uint96 payout);
event PendingPayoutClaimed(address indexed player, uint256 amount);
event CooldownApplied(address indexed player, uint32 timeoutStreak, uint64 nextPlayableAt);
constructor(address initialQuoteSigner, uint96[] memory initialAllowedStakes, uint64 initialGameTimeout)
Ownable(msg.sender)
EIP712("LitGamesReaperMines", "4")
{
if (initialQuoteSigner == address(0)) {
revert InvalidAddress();
}
quoteSigner = initialQuoteSigner;
_setAllowedStakes(initialAllowedStakes);
_setGameTimeout(initialGameTimeout);
emit AllowedStakesUpdated(initialAllowedStakes);
emit GameTimeoutUpdated(0, initialGameTimeout);
}
receive() external payable {
emit BankrollFunded(msg.sender, msg.value, address(this).balance);
}
function setQuoteSigner(address newSigner) external onlyOwner {
if (newSigner == address(0)) {
revert InvalidAddress();
}
address prev = quoteSigner;
quoteSigner = newSigner;
emit QuoteSignerUpdated(prev, newSigner);
}
function pause() external onlyOwner {
_pause();
emit PausedByOwner(msg.sender);
}
function unpause() external onlyOwner {
_unpause();
emit UnpausedByOwner(msg.sender);
}
function fundBankroll() external payable onlyOwner {
emit BankrollFunded(msg.sender, msg.value, address(this).balance);
}
function setRelayer(address relayer, bool enabled) external onlyOwner {
if (relayer == address(0)) {
revert InvalidAddress();
}
relayers[relayer] = enabled;
emit RelayerUpdated(relayer, enabled);
}
function getAllowedStakes() external view returns (uint96[] memory) {
return allowedStakes;
}
function setAllowedStakes(uint96[] calldata newAllowedStakes) external onlyOwner {
_setAllowedStakes(newAllowedStakes);
emit AllowedStakesUpdated(newAllowedStakes);
}
function setGameTimeout(uint64 newGameTimeout) external onlyOwner {
uint64 previousTimeout = gameTimeout;
_setGameTimeout(newGameTimeout);
emit GameTimeoutUpdated(previousTimeout, newGameTimeout);
}
function setTimeoutCooldown(uint64 newTimeoutCooldown) external onlyOwner {
uint64 previousCooldown = timeoutCooldown;
_setTimeoutCooldown(newTimeoutCooldown);
emit TimeoutCooldownUpdated(previousCooldown, newTimeoutCooldown);
}
function setLockMultipliersByBombs(
uint32 next2BombsBps,
uint32 next3BombsBps,
uint32 next4BombsBps,
uint32 next5BombsBps
) external onlyOwner {
_requireValidLockMultiplier(next2BombsBps);
_requireValidLockMultiplier(next3BombsBps);
_requireValidLockMultiplier(next4BombsBps);
_requireValidLockMultiplier(next5BombsBps);
uint32 prev2 = lockMultiplier2BombsBps;
uint32 prev3 = lockMultiplier3BombsBps;
uint32 prev4 = lockMultiplier4BombsBps;
uint32 prev5 = lockMultiplier5BombsBps;
lockMultiplier2BombsBps = next2BombsBps;
lockMultiplier3BombsBps = next3BombsBps;
lockMultiplier4BombsBps = next4BombsBps;
lockMultiplier5BombsBps = next5BombsBps;
emit LockMultipliersUpdated(prev2, prev3, prev4, prev5, next2BombsBps, next3BombsBps, next4BombsBps, next5BombsBps);
}
function withdrawBankroll(address payable to, uint256 amount) external onlyOwner nonReentrant {
if (to == address(0)) {
revert InvalidAddress();
}
uint256 freeBalance = _freeBalance();
if (amount > freeBalance) {
revert LiquidityLocked();
}
(bool ok, ) = to.call{value: amount}("");
require(ok, "Withdraw failed");
emit BankrollWithdrawn(to, amount, address(this).balance);
}
function configureSession(address sessionKey, uint64 validUntil, uint96 maxStake) external whenNotPaused {
_setSession(msg.sender, sessionKey, validUntil, maxStake);
}
function configureSessionAndDeposit(address sessionKey, uint64 validUntil, uint96 maxStake)
external
payable
whenNotPaused
{
_setSession(msg.sender, sessionKey, validUntil, maxStake);
if (msg.value > 0) {
playerBalances[msg.sender] += msg.value;
totalPlayerBalances += msg.value;
emit PlayerBalanceDeposited(msg.sender, msg.value, playerBalances[msg.sender]);
}
}
function revokeSession() external {
delete sessions[msg.sender];
emit SessionRevoked(msg.sender);
}
function depositPlayerBalance() external payable whenNotPaused {
if (msg.value == 0) {
revert InsufficientStake();
}
playerBalances[msg.sender] += msg.value;
totalPlayerBalances += msg.value;
emit PlayerBalanceDeposited(msg.sender, msg.value, playerBalances[msg.sender]);
}
function withdrawPlayerBalance(uint256 amount) external nonReentrant {
if (amount == 0) {
revert InsufficientStake();
}
uint256 bal = playerBalances[msg.sender];
if (bal < amount) {
revert InsufficientPlayerBalance();
}
playerBalances[msg.sender] = bal - amount;
totalPlayerBalances -= amount;
(bool ok, ) = payable(msg.sender).call{value: amount}("");
require(ok, "Player withdraw failed");
emit PlayerBalanceWithdrawn(msg.sender, amount, playerBalances[msg.sender]);
}
function claimPendingPayout() external nonReentrant {
uint256 amount = pendingPayouts[msg.sender];
if (amount == 0) {
revert NoPendingPayout();
}
uint256 liabilitiesExcludingClaim = lockedLiquidity + totalPlayerBalances + (totalPendingPayouts - amount);
uint256 balance = address(this).balance;
if (balance <= liabilitiesExcludingClaim) {
revert PendingPayoutNotReady();
}
uint256 availableForClaim = balance - liabilitiesExcludingClaim;
if (availableForClaim < amount) {
revert PendingPayoutNotReady();
}
pendingPayouts[msg.sender] = 0;
totalPendingPayouts -= amount;
(bool ok, ) = payable(msg.sender).call{value: amount}("");
require(ok, "Pending payout transfer failed");
emit PendingPayoutClaimed(msg.sender, amount);
}
function getSession(address player) external view returns (address sessionKey, uint64 validUntil, uint96 maxStake) {
SessionConfig memory s = sessions[player];
return (s.sessionKey, s.validUntil, s.maxStake);
}
function createGame(GameQuote calldata quote, bytes calldata signature)
external
payable
nonReentrant
whenNotPaused
returns (uint256 gameId)
{
if (msg.sender != quote.player || quote.player == address(0)) {
revert InvalidQuote();
}
_requireNotInCooldown(msg.sender);
uint256 activeGameId = activeGameByPlayer[msg.sender];
if (activeGameId != 0) {
Game storage active = games[activeGameId];
if (!active.resolved) {
revert PlayerHasActiveGame();
}
activeGameByPlayer[msg.sender] = 0;
}
if (block.timestamp > quote.validUntil) {
revert QuoteExpired();
}
if (quote.stake == 0 || msg.value != quote.stake) {
revert InsufficientStake();
}
_requireAllowedStake(quote.stake);
if (quote.bombs < MIN_BOMBS || quote.bombs > MAX_BOMBS) {
revert InvalidBombCount();
}
if (quote.boardRoot == bytes32(0)) {
revert InvalidQuote();
}
if (nonceUsed[msg.sender][quote.nonce]) {
revert NonceAlreadyUsed();
}
_verifyQuote(quote, signature);
nonceUsed[msg.sender][quote.nonce] = true;
uint96 maxPayout = uint96(_payoutFromStake(quote.stake, quote.bombs, TOTAL_CELLS - quote.bombs));
uint96 reservedLiquidity = 0;
bytes32 gameKey = _gameKey(msg.sender, quote.nonce);
gameId = nextGameId++;
games[gameId] = Game({
player: msg.sender,
stake: quote.stake,
maxPayout: maxPayout,
reservedLiquidity: reservedLiquidity,
payout: 0,
bombs: quote.bombs,
safeOpened: 0,
resultMultiplierBps: BPS,
openedMask: 0,
createdAt: uint64(block.timestamp),
timeoutAt: uint64(block.timestamp) + gameTimeout,
resolved: false,
won: false,
usesSessionBalance: false,
boardRoot: quote.boardRoot,
gameKey: gameKey
});
activeGameByPlayer[msg.sender] = gameId;
emit GameCreated(gameId, msg.sender, quote.stake, quote.bombs, quote.boardRoot, maxPayout, gameKey);
}
/// @notice Session + relayer path: player signs session once, then relayer creates rounds using player escrow balance.
function createGameByRelayer(address player, GameQuote calldata quote, bytes calldata signature)
external
nonReentrant
whenNotPaused
returns (uint256 gameId)
{
_requireRelayer();
if (player == address(0) || quote.player != player) {
revert InvalidQuote();
}
_requireNotInCooldown(player);
_requireActiveSession(player, quote.stake);
uint256 activeGameId = activeGameByPlayer[player];
if (activeGameId != 0) {
Game storage active = games[activeGameId];
if (!active.resolved) {
revert PlayerHasActiveGame();
}
activeGameByPlayer[player] = 0;
}
if (block.timestamp > quote.validUntil) {
revert QuoteExpired();
}
if (quote.stake == 0) {
revert InsufficientStake();
}
_requireAllowedStake(quote.stake);
if (quote.bombs < MIN_BOMBS || quote.bombs > MAX_BOMBS) {
revert InvalidBombCount();
}
if (quote.boardRoot == bytes32(0)) {
revert InvalidQuote();
}
if (nonceUsed[player][quote.nonce]) {
revert NonceAlreadyUsed();
}
_verifyQuote(quote, signature);
nonceUsed[player][quote.nonce] = true;
uint256 userBalance = playerBalances[player];
if (userBalance < quote.stake) {
revert InsufficientPlayerBalance();
}
playerBalances[player] = userBalance - quote.stake;
totalPlayerBalances -= quote.stake;
uint96 maxPayout = uint96(_payoutFromStake(quote.stake, quote.bombs, TOTAL_CELLS - quote.bombs));
uint96 reservedLiquidity = 0;
bytes32 gameKey = _gameKey(player, quote.nonce);
gameId = nextGameId++;
games[gameId] = Game({
player: player,
stake: quote.stake,
maxPayout: maxPayout,
reservedLiquidity: reservedLiquidity,
payout: 0,
bombs: quote.bombs,
safeOpened: 0,
resultMultiplierBps: BPS,
openedMask: 0,
createdAt: uint64(block.timestamp),
timeoutAt: uint64(block.timestamp) + gameTimeout,
resolved: false,
won: false,
usesSessionBalance: true,
boardRoot: quote.boardRoot,
gameKey: gameKey
});
activeGameByPlayer[player] = gameId;
emit GameCreated(gameId, player, quote.stake, quote.bombs, quote.boardRoot, maxPayout, gameKey);
}
/// @notice Finalize a winning game by proving all safe cells the player opened off-chain.
function finalizeCashOut(
uint256 gameId,
uint8[] calldata safeCellIndexes,
bytes32[] calldata safeSalts,
bytes32[][] calldata safeProofs
) external nonReentrant whenNotPaused returns (uint96 payout) {
Game storage g = games[gameId];
_requireActiveGame(g);
g.safeOpened = _verifyAndApplySafeBatch(g, safeCellIndexes, safeSalts, safeProofs);
uint32 multiplierBps = _multiplierBps(g.bombs, g.safeOpened);
payout = uint96((uint256(g.stake) * multiplierBps) / BPS);
_resolve(gameId, g, true, multiplierBps, payout);
}
/// @notice Finalize a losing game by proving the mine cell that ended the round.
function finalizeLoss(uint256 gameId, uint8 mineCellIndex, bytes32 mineSalt, bytes32[] calldata mineProof)
external
nonReentrant
whenNotPaused
{
Game storage g = games[gameId];
_requireActiveGame(g);
if (mineCellIndex >= TOTAL_CELLS) {
revert InvalidCell();
}
uint16 bit = uint16(1) << mineCellIndex;
if ((g.openedMask & bit) != 0) {
revert CellAlreadyOpened();
}
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(g.gameKey, mineCellIndex, true, mineSalt))));
if (!MerkleProof.verifyCalldata(mineProof, g.boardRoot, leaf)) {
revert InvalidProof();
}
g.openedMask |= bit;
_resolve(gameId, g, false, 0, 0);
}
/// @notice Relayer finalization path for cash out (player signs only createGame).
function finalizeCashOutByRelayer(
uint256 gameId,
address player,
uint8[] calldata safeCellIndexes,
bytes32[] calldata safeSalts,
bytes32[][] calldata safeProofs
) external nonReentrant whenNotPaused returns (uint96 payout) {
_requireRelayer();
Game storage g = games[gameId];
_requireActiveGameForPlayer(g, player);
g.safeOpened = _verifyAndApplySafeBatch(g, safeCellIndexes, safeSalts, safeProofs);
uint32 multiplierBps = _multiplierBps(g.bombs, g.safeOpened);
payout = uint96((uint256(g.stake) * multiplierBps) / BPS);
_resolve(gameId, g, true, multiplierBps, payout);
}
/// @notice Relayer finalization path for losses.
function finalizeLossByRelayer(
uint256 gameId,
address player,
uint8 mineCellIndex,
bytes32 mineSalt,
bytes32[] calldata mineProof
) external nonReentrant whenNotPaused {
_requireRelayer();
Game storage g = games[gameId];
_requireActiveGameForPlayer(g, player);
if (mineCellIndex >= TOTAL_CELLS) {
revert InvalidCell();
}
uint16 bit = uint16(1) << mineCellIndex;
if ((g.openedMask & bit) != 0) {
revert CellAlreadyOpened();
}
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(g.gameKey, mineCellIndex, true, mineSalt))));
if (!MerkleProof.verifyCalldata(mineProof, g.boardRoot, leaf)) {
revert InvalidProof();
}
g.openedMask |= bit;
_resolve(gameId, g, false, 0, 0);
}
function timeoutRefund(uint256 gameId) external nonReentrant {
Game storage g = games[gameId];
if (g.player == address(0)) {
revert GameNotFound();
}
if (g.resolved) {
revert GameResolved();
}
if (block.timestamp < uint256(g.timeoutAt)) {
revert GameStillActive();
}
g.resolved = true;
g.won = false;
g.resultMultiplierBps = BPS;
g.payout = g.stake;
lockedLiquidity -= g.reservedLiquidity;
activeGameByPlayer[g.player] = 0;
uint32 streak = ++timeoutRefundStreak[g.player];
if (streak >= 2 && timeoutCooldown > 0) {
uint64 playableAt = uint64(block.timestamp) + timeoutCooldown;
nextPlayableAt[g.player] = playableAt;
emit CooldownApplied(g.player, streak, playableAt);
}
if (g.usesSessionBalance) {
playerBalances[g.player] += g.stake;
totalPlayerBalances += g.stake;
} else {
(bool ok, ) = payable(g.player).call{value: g.stake}("");
if (!ok) {
uint256 deferredAmount = uint256(g.stake);
pendingPayouts[g.player] += deferredAmount;
totalPendingPayouts += deferredAmount;
emit PayoutDeferred(gameId, g.player, g.stake);
}
}
emit GameRefunded(gameId, g.player, g.stake);
}
function currentMultiplierBps(uint256 gameId) external view returns (uint32) {
Game storage g = games[gameId];
if (g.player == address(0)) {
revert GameNotFound();
}
if (g.resolved || g.safeOpened == 0) {
return BPS;
}
return _multiplierBps(g.bombs, g.safeOpened);
}
function potentialPayout(uint256 gameId) external view returns (uint256) {
Game storage g = games[gameId];
if (g.player == address(0)) {
revert GameNotFound();
}
if (g.safeOpened == 0) {
return g.stake;
}
return _payoutFromStake(g.stake, g.bombs, g.safeOpened);
}
function _resolve(uint256 gameId, Game storage g, bool won, uint32 multiplierBps, uint96 payout) internal {
g.resolved = true;
g.won = won;
g.resultMultiplierBps = multiplierBps;
g.payout = payout;
lockedLiquidity -= g.reservedLiquidity;
activeGameByPlayer[g.player] = 0;
timeoutRefundStreak[g.player] = 0;
if (won) {
if (g.usesSessionBalance) {
playerBalances[g.player] += payout;
totalPlayerBalances += payout;
} else {
uint256 payoutAmount = uint256(payout);
if (_freeBalance() >= payoutAmount) {
(bool ok, ) = payable(g.player).call{value: payout}("");
if (!ok) {
pendingPayouts[g.player] += payoutAmount;
totalPendingPayouts += payoutAmount;
emit PayoutDeferred(gameId, g.player, payout);
}
} else {
pendingPayouts[g.player] += payoutAmount;
totalPendingPayouts += payoutAmount;
emit PayoutDeferred(gameId, g.player, payout);
}
}
}
emit GameFinalized(gameId, g.player, won, g.safeOpened, multiplierBps, payout);
}
function _requireActiveGame(Game storage g) internal view {
if (g.player == address(0)) {
revert GameNotFound();
}
if (g.player != msg.sender) {
revert NotGamePlayer();
}
if (g.resolved) {
revert GameResolved();
}
}
function _requireOwnedUnresolvedGame(Game storage g) internal view {
if (g.player == address(0)) {
revert GameNotFound();
}
if (g.player != msg.sender) {
revert NotGamePlayer();
}
if (g.resolved) {
revert GameResolved();
}
}
function _requireActiveGameForPlayer(Game storage g, address player) internal view {
if (g.player == address(0)) {
revert GameNotFound();
}
if (g.player != player || player == address(0)) {
revert NotGamePlayer();
}
if (g.resolved) {
revert GameResolved();
}
}
function _requireRelayer() internal view {
if (msg.sender != owner() && !relayers[msg.sender]) {
revert NotAuthorizedRelayer();
}
}
function _setSession(address player, address sessionKey, uint64 validUntil, uint96 maxStake) internal {
if (sessionKey == address(0)) {
revert InvalidAddress();
}
if (validUntil <= block.timestamp) {
revert QuoteExpired();
}
if (maxStake == 0) {
revert InsufficientStake();
}
sessions[player] = SessionConfig({sessionKey: sessionKey, validUntil: validUntil, maxStake: maxStake});
emit SessionConfigured(player, sessionKey, validUntil, maxStake);
}
function _requireActiveSession(address player, uint96 stake) internal view {
SessionConfig memory s = sessions[player];
if (s.sessionKey == address(0)) {
revert SessionNotAuthorized();
}
if (msg.sender != s.sessionKey) {
revert NotAuthorizedRelayer();
}
if (block.timestamp > s.validUntil) {
revert SessionExpired();
}
if (stake > s.maxStake) {
revert StakeAboveSessionLimit();
}
}
function _verifyQuote(GameQuote calldata quote, bytes calldata signature) internal view {
bytes32 structHash = keccak256(
abi.encode(
GAME_QUOTE_TYPEHASH,
quote.player,
quote.stake,
quote.bombs,
quote.boardRoot,
quote.validUntil,
quote.nonce
)
);
bytes32 digest = _hashTypedDataV4(structHash);
address recovered = ECDSA.recover(digest, signature);
if (recovered != quoteSigner) {
revert InvalidSigner();
}
}
function _verifyAndApplySafeBatch(
Game storage g,
uint8[] calldata safeCellIndexes,
bytes32[] calldata safeSalts,
bytes32[][] calldata safeProofs
) internal returns (uint8) {
uint256 len = safeCellIndexes.length;
if (len == 0 || safeSalts.length != len || safeProofs.length != len) {
revert InvalidRevealBatch();
}
uint256 safeCap = TOTAL_CELLS - g.bombs;
if (len > safeCap) {
revert InvalidRevealBatch();
}
uint16 openedMaskLocal = g.openedMask;
for (uint256 i = 0; i < len; i++) {
uint8 cellIndex = safeCellIndexes[i];
if (cellIndex >= TOTAL_CELLS) {
revert InvalidCell();
}
uint16 bit = uint16(1) << cellIndex;
if ((openedMaskLocal & bit) != 0) {
revert CellAlreadyOpened();
}
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(g.gameKey, cellIndex, false, safeSalts[i]))));
if (!MerkleProof.verifyCalldata(safeProofs[i], g.boardRoot, leaf)) {
revert InvalidProof();
}
openedMaskLocal |= bit;
}
g.openedMask = openedMaskLocal;
return uint8(len);
}
function _gameKey(address player, uint256 nonce) internal pure returns (bytes32) {
return keccak256(abi.encode(player, nonce));
}
function _multiplierBps(uint8 bombs, uint8 safeOpened) internal pure returns (uint32) {
if (safeOpened == 0) {
return BPS;
}
uint256 safeTotal = TOTAL_CELLS - bombs;
uint256 numer = 1;
uint256 denom = 1;
for (uint256 i = 0; i < safeOpened; i++) {
numer *= safeTotal - i;
denom *= TOTAL_CELLS - i;
}
uint256 multiplier = (uint256(RTP_BPS) * denom) / numer;
if (multiplier < BPS) {
multiplier = BPS;
}
require(multiplier <= type(uint32).max, "Multiplier overflow");
return uint32(multiplier);
}
function _payoutFromStake(uint96 stake, uint8 bombs, uint8 safeOpened) internal pure returns (uint256) {
uint256 multiplierBps = _multiplierBps(bombs, safeOpened);
return (uint256(stake) * multiplierBps) / BPS;
}
function _requireAllowedStake(uint96 stake) internal view {
if (!isAllowedStake[stake]) {
revert InvalidStakeOption();
}
}
function _setAllowedStakes(uint96[] memory newAllowedStakes) internal {
uint256 nextLen = newAllowedStakes.length;
if (nextLen == 0 || nextLen > MAX_ALLOWED_STAKE_OPTIONS) {
revert InvalidStakeOption();
}
uint96 previousStake;
for (uint256 i = 0; i < nextLen; i++) {
uint96 stake = newAllowedStakes[i];
if (stake == 0 || (i > 0 && stake <= previousStake)) {
revert InvalidStakeOption();
}
previousStake = stake;
}
uint256 currentLen = allowedStakes.length;
for (uint256 i = 0; i < currentLen; i++) {
isAllowedStake[allowedStakes[i]] = false;
}
delete allowedStakes;
for (uint256 i = 0; i < nextLen; i++) {
uint96 stake = newAllowedStakes[i];
allowedStakes.push(stake);
isAllowedStake[stake] = true;
}
}
function _setGameTimeout(uint64 newGameTimeout) internal {
if (newGameTimeout < MIN_GAME_TIMEOUT || newGameTimeout > MAX_GAME_TIMEOUT) {
revert InvalidGameTimeout();
}
gameTimeout = newGameTimeout;
}
function _setTimeoutCooldown(uint64 newTimeoutCooldown) internal {
if (newTimeoutCooldown > MAX_TIMEOUT_COOLDOWN) {
revert InvalidTimeoutCooldown();
}
timeoutCooldown = newTimeoutCooldown;
}
function _lockedAmountForGame(uint96 stake, uint8 bombs, uint96 maxPayout) internal view returns (uint96) {
uint256 lockByStake = (uint256(stake) * _lockMultiplierBpsForBombs(bombs)) / BPS;
uint256 effectiveLock = lockByStake > uint256(maxPayout) ? uint256(maxPayout) : lockByStake;
require(effectiveLock <= type(uint96).max, "Lock overflow");
return uint96(effectiveLock);
}
function _lockMultiplierBpsForBombs(uint8 bombs) internal view returns (uint32) {
if (bombs == 2) return lockMultiplier2BombsBps;
if (bombs == 3) return lockMultiplier3BombsBps;
if (bombs == 4) return lockMultiplier4BombsBps;
if (bombs == 5) return lockMultiplier5BombsBps;
revert InvalidBombCount();
}
function _requireValidLockMultiplier(uint32 value) internal pure {
if (value < MIN_LOCK_MULTIPLIER_BPS || value > MAX_LOCK_MULTIPLIER_BPS) {
revert InvalidLockMultiplier();
}
}
function _requireNotInCooldown(address player) internal view {
if (block.timestamp < uint256(nextPlayableAt[player])) {
revert CooldownActive();
}
}
function _freeBalance() internal view returns (uint256) {
uint256 liabilities = lockedLiquidity + totalPlayerBalances + totalPendingPayouts;
uint256 balance = address(this).balance;
if (balance <= liabilities) {
return 0;
}
return balance - liabilities;
}
}