Address
0x29b95c4e1c35e6616badf324ef593b9c8a122001Current Holdings
$0.00
TXs sent
not counted
First Active
2026-02-24
block 25,868,928
Last Active
203 days ago
block 25,868,928
Funded By
not identified
Net worth historyi
No net-worth snapshots recorded yet
exact matchW1NNRTournamentsolc 0.8.20+commit.a1b79de6runtime exact · creation exact
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title W1NNRTournament
* @notice Single-elimination bracket tournament with on-chain prize escrow.
* @dev Designed to never brick. Every failure mode has an escape hatch.
*
* ANTI-BRICK SCENARIOS:
* 1. No-show → claimNoShow() after noShowWindow
* 2. Dispute + no admin → forceAdvance() after disputeWindow (first-reporter wins)
* 3. Admin never locks → selfLockBracket() after deadline + 6h grace
* 4. Not enough players → cancelTournament() after deadline, full refunds
* 5. Treasury fails → fee stored in pendingTreasuryFee, prizes still distribute
* 6. Rounding dust → added to 1st place prize
*/
interface IERC20 {
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
interface IW1NNRPlayerRegistry {
function registerFor(address wallet) external;
}
struct TournamentConfig {
address admin;
address treasury;
address token;
address playerRegistry;
uint256 entryFee;
uint256 maxPlayers;
uint256 sportId;
uint256 registrationDeadline;
string name;
uint256 noShowWindow;
uint256 disputeWindow;
}
contract W1NNRTournament {
uint256 public constant FEE_BPS = 800;
uint256 public constant BPS_DENOMINATOR = 10000;
uint256 public constant LOCK_GRACE_PERIOD = 6 hours;
enum TournamentState { Registration, Locked, Complete, Cancelled }
enum MatchState { Pending, Active, Disputed, Complete }
// ── Immutable config ──────────────────────────────────
address public admin;
address public treasury;
address public token;
address public playerRegistry;
uint256 public entryFee;
uint256 public maxPlayers;
uint256 public sportId;
uint256 public registrationDeadline;
uint256 public noShowWindow;
uint256 public disputeWindow;
string public tournamentName;
// ── Mutable state ─────────────────────────────────────
TournamentState public state;
address[] public entrants;
mapping(address => bool) public isEntrant;
mapping(address => uint256) public entrantRating;
mapping(address => bool) public refundClaimed;
address[] public seeds;
uint256 public roundCount;
uint256 public currentRound;
uint256 public roundStartTime;
bool public bracketLocked;
bool private _initialized;
mapping(uint256 => mapping(uint256 => Match)) public matches;
struct Match {
address playerA;
address playerB;
address reportedByA;
address reportedByB;
uint256 reportedByA_time;
uint256 reportedByB_time;
address winner;
MatchState state;
uint256 disputeTime;
}
mapping(address => uint256) public prizeOwed;
mapping(address => bool) public prizeClaimed;
uint256 public pendingTreasuryFee;
// ── Events ────────────────────────────────────────────
event PlayerRegistered(address indexed player, uint256 rating, uint256 count);
event BracketLocked(address[] seeds, uint256 rounds, address lockedBy);
event RoundStarted(uint256 indexed round, uint256 startTime);
event WinnerReported(uint256 indexed round, uint256 indexed matchIndex, address indexed reporter, address reportedWinner);
event MatchComplete(uint256 indexed round, uint256 indexed matchIndex, address indexed winner, string method);
event MatchDisputed(uint256 indexed round, uint256 indexed matchIndex);
event PrizesAllocated(address indexed first, uint256 firstAmt, address indexed second, uint256 secondAmt);
event PrizeClaimed(address indexed player, uint256 amount);
event RefundClaimed(address indexed player, uint256 amount);
event TournamentCancelled(address indexed by, string reason);
event TreasuryFeeFailed(uint256 amount);
event TreasuryFeeSwept(uint256 amount);
// ── View structs (avoids stack-too-deep in ABI encoder) ──
struct MatchView {
address playerA;
address playerB;
address reportedByA;
address reportedByB;
address winner;
MatchState matchState;
uint256 disputeTime;
}
struct TournamentInfoView {
string name;
uint256 sport;
uint256 fee;
uint256 maxP;
uint256 currentP;
uint256 deadline;
TournamentState tState;
}
struct PrizeView {
uint256 totalPool;
uint256 platformFee;
uint256 netPool;
uint256 firstPrize;
uint256 secondPrize;
uint256 thirdFourthPrize;
}
struct ProgressView {
uint256 curRound;
uint256 rounds;
uint256 totalPool;
uint256 noShow;
uint256 dispute;
}
struct RoundMatchesView {
address[] playerAs;
address[] playerBs;
address[] winners;
MatchState[] states;
}
struct PlayerStatusView {
bool registered;
uint256 rating;
uint256 seedPos;
uint256 prize;
bool claimed;
}
// ── Constructor ───────────────────────────────────────
constructor(TournamentConfig memory cfg) {
require(cfg.admin != address(0), "Invalid admin");
require(cfg.treasury != address(0), "Invalid treasury");
require(cfg.token != address(0), "Invalid token");
require(cfg.entryFee > 0, "Entry fee required");
require(cfg.maxPlayers == 4 || cfg.maxPlayers == 8 || cfg.maxPlayers == 16, "Players must be 4, 8, or 16");
require(cfg.registrationDeadline > block.timestamp, "Deadline must be future");
require(bytes(cfg.name).length > 0, "Name required");
require(cfg.noShowWindow >= 30 minutes, "noShowWindow min 30m");
require(cfg.disputeWindow >= 30 minutes, "disputeWindow min 30m");
_initConfig(cfg);
roundCount = cfg.maxPlayers == 4 ? 2 : cfg.maxPlayers == 8 ? 3 : 4;
state = TournamentState.Registration;
}
function _initConfig(TournamentConfig memory cfg) private {
admin = cfg.admin;
treasury = cfg.treasury;
token = cfg.token;
playerRegistry = cfg.playerRegistry;
entryFee = cfg.entryFee;
maxPlayers = cfg.maxPlayers;
sportId = cfg.sportId;
registrationDeadline = cfg.registrationDeadline;
tournamentName = cfg.name;
noShowWindow = cfg.noShowWindow;
disputeWindow = cfg.disputeWindow;
}
/**
* @notice Initializer for EIP-1167 clone deployments.
* @dev Called by the factory immediately after cloning.
* Can only be called once. Constructor is used for the master.
*/
function initialize(TournamentConfig calldata cfg) external {
require(!_initialized, "Already initialized");
_initialized = true;
require(cfg.admin != address(0), "Invalid admin");
require(cfg.treasury != address(0), "Invalid treasury");
require(cfg.token != address(0), "Invalid token");
require(cfg.entryFee > 0, "Entry fee required");
require(cfg.maxPlayers == 4 || cfg.maxPlayers == 8 || cfg.maxPlayers == 16, "Players must be 4, 8, or 16");
require(cfg.registrationDeadline > block.timestamp, "Deadline must be future");
require(bytes(cfg.name).length > 0, "Name required");
require(cfg.noShowWindow >= 30 minutes, "noShowWindow min 30m");
require(cfg.disputeWindow >= 30 minutes, "disputeWindow min 30m");
_initConfig(cfg);
roundCount = cfg.maxPlayers == 4 ? 2 : cfg.maxPlayers == 8 ? 3 : 4;
state = TournamentState.Registration;
}
modifier onlyAdmin() {
require(msg.sender == admin, "Not admin");
_;
}
modifier inState(TournamentState s) {
require(state == s, "Wrong tournament state");
_;
}
// ══════════════════════════════════════════
// REGISTRATION
// ══════════════════════════════════════════
function register(uint256 rating) external inState(TournamentState.Registration) {
require(block.timestamp < registrationDeadline, "Registration closed");
require(entrants.length < maxPlayers, "Tournament full");
require(!isEntrant[msg.sender], "Already registered");
require(IERC20(token).transferFrom(msg.sender, address(this), entryFee), "Transfer failed");
isEntrant[msg.sender] = true;
entrantRating[msg.sender] = rating > 0 ? rating : 1000;
entrants.push(msg.sender);
if (playerRegistry != address(0)) {
try IW1NNRPlayerRegistry(playerRegistry).registerFor(msg.sender) {} catch {}
}
emit PlayerRegistered(msg.sender, entrantRating[msg.sender], entrants.length);
if (entrants.length == maxPlayers) {
_lockWithSeeds(_sortByRating(entrants));
}
}
// ══════════════════════════════════════════
// BRACKET LOCKING
// ══════════════════════════════════════════
function lockBracket(address[] calldata _seeds)
external
onlyAdmin
inState(TournamentState.Registration)
{
require(!bracketLocked, "Already locked");
require(block.timestamp >= registrationDeadline, "Deadline not passed");
require(_seeds.length == entrants.length, "Must include all entrants");
require(_seeds.length >= 4, "Minimum 4 players");
require(
_seeds.length == 4 || _seeds.length == 8 || _seeds.length == 16,
"Must be 4, 8, or 16"
);
for (uint256 i = 0; i < _seeds.length; i++) {
require(isEntrant[_seeds[i]], "Not registered");
for (uint256 j = i + 1; j < _seeds.length; j++) {
require(_seeds[i] != _seeds[j], "Duplicate");
}
}
address[] memory s = new address[](_seeds.length);
for (uint256 i = 0; i < _seeds.length; i++) s[i] = _seeds[i];
_lockWithSeeds(s);
emit BracketLocked(s, roundCount, msg.sender);
}
function selfLockBracket() external inState(TournamentState.Registration) {
require(!bracketLocked, "Already locked");
require(isEntrant[msg.sender], "Not an entrant");
require(block.timestamp >= registrationDeadline + LOCK_GRACE_PERIOD, "Grace period not passed");
require(entrants.length >= 4, "Not enough players");
address[] memory sorted = _sortByRating(entrants);
uint256 bracketSize = _nearestBracketSize(entrants.length);
address[] memory bracketSeeds = new address[](bracketSize);
for (uint256 i = 0; i < bracketSize; i++) bracketSeeds[i] = sorted[i];
for (uint256 i = bracketSize; i < sorted.length; i++) {
refundClaimed[sorted[i]] = true;
require(IERC20(token).transfer(sorted[i], entryFee), "Refund failed");
}
_lockWithSeeds(bracketSeeds);
emit BracketLocked(bracketSeeds, roundCount, msg.sender);
}
// ══════════════════════════════════════════
// MATCH PLAY
// ══════════════════════════════════════════
function reportWinner(uint256 round, uint256 matchIndex, address winner)
external
inState(TournamentState.Locked)
{
require(round == currentRound, "Not current round");
Match storage m = matches[round][matchIndex];
require(m.state == MatchState.Active, "Match not active");
require(msg.sender == m.playerA || msg.sender == m.playerB, "Not a participant");
require(winner == m.playerA || winner == m.playerB, "Invalid winner");
if (msg.sender == m.playerA) {
require(m.reportedByA == address(0), "Already reported");
m.reportedByA = winner;
m.reportedByA_time = block.timestamp;
} else {
require(m.reportedByB == address(0), "Already reported");
m.reportedByB = winner;
m.reportedByB_time = block.timestamp;
}
emit WinnerReported(round, matchIndex, msg.sender, winner);
if (m.reportedByA != address(0) && m.reportedByB != address(0)) {
if (m.reportedByA == m.reportedByB) {
_confirmWinner(round, matchIndex, winner, "agreement");
} else {
m.state = MatchState.Disputed;
m.disputeTime = block.timestamp;
emit MatchDisputed(round, matchIndex);
}
}
}
function claimNoShow(uint256 round, uint256 matchIndex)
external
inState(TournamentState.Locked)
{
require(round == currentRound, "Not current round");
require(block.timestamp >= roundStartTime + noShowWindow, "Window not passed");
Match storage m = matches[round][matchIndex];
require(m.state == MatchState.Active, "Match not active");
require(msg.sender == m.playerA || msg.sender == m.playerB, "Not a participant");
if (msg.sender == m.playerA) {
require(m.reportedByB == address(0), "Opponent reported - use forceAdvance");
} else {
require(m.reportedByA == address(0), "Opponent reported - use forceAdvance");
}
_confirmWinner(round, matchIndex, msg.sender, "no-show");
}
function forceAdvance(uint256 round, uint256 matchIndex)
external
inState(TournamentState.Locked)
{
Match storage m = matches[round][matchIndex];
require(m.state == MatchState.Disputed, "Not disputed");
require(
msg.sender == m.playerA || msg.sender == m.playerB || msg.sender == admin,
"Not authorized"
);
require(block.timestamp >= m.disputeTime + disputeWindow, "Window not passed");
address winner = _firstReporter(m);
require(winner == m.playerA || winner == m.playerB, "Invalid winner");
_confirmWinner(round, matchIndex, winner, "force-advance");
}
function adminResolveDispute(uint256 round, uint256 matchIndex, address winner)
external
onlyAdmin
inState(TournamentState.Locked)
{
Match storage m = matches[round][matchIndex];
require(m.state == MatchState.Disputed, "Not disputed");
require(winner == m.playerA || winner == m.playerB, "Invalid winner");
_confirmWinner(round, matchIndex, winner, "admin-resolved");
}
// ══════════════════════════════════════════
// CANCELLATION
// ══════════════════════════════════════════
function cancelTournament(string calldata reason)
external
inState(TournamentState.Registration)
{
require(
msg.sender == admin ||
(block.timestamp >= registrationDeadline && entrants.length < 4),
"Cannot cancel yet"
);
state = TournamentState.Cancelled;
emit TournamentCancelled(msg.sender, reason);
}
// ══════════════════════════════════════════
// CLAIMS
// ══════════════════════════════════════════
function claimPrize() external inState(TournamentState.Complete) {
uint256 amount = prizeOwed[msg.sender];
require(amount > 0, "No prize owed");
require(!prizeClaimed[msg.sender], "Already claimed");
prizeClaimed[msg.sender] = true;
prizeOwed[msg.sender] = 0;
require(IERC20(token).transfer(msg.sender, amount), "Transfer failed");
emit PrizeClaimed(msg.sender, amount);
}
function claimRefund() external inState(TournamentState.Cancelled) {
require(isEntrant[msg.sender], "Not registered");
require(!refundClaimed[msg.sender], "Already refunded");
refundClaimed[msg.sender] = true;
require(IERC20(token).transfer(msg.sender, entryFee), "Refund failed");
emit RefundClaimed(msg.sender, entryFee);
}
function sweepTreasuryFee() external onlyAdmin {
uint256 amount = pendingTreasuryFee;
require(amount > 0, "Nothing to sweep");
pendingTreasuryFee = 0;
require(IERC20(token).transfer(treasury, amount), "Sweep failed");
emit TreasuryFeeSwept(amount);
}
// ══════════════════════════════════════════
// INTERNAL: BRACKET
// ══════════════════════════════════════════
function _lockWithSeeds(address[] memory _seeds) internal {
require(!bracketLocked, "Already locked");
bracketLocked = true;
uint256 n = _seeds.length;
roundCount = n == 4 ? 2 : n == 8 ? 3 : 4;
delete seeds;
for (uint256 i = 0; i < n; i++) seeds.push(_seeds[i]);
for (uint256 i = 0; i < n / 2; i++) {
matches[0][i] = Match({
playerA: _seeds[i],
playerB: _seeds[n - 1 - i],
reportedByA: address(0),
reportedByB: address(0),
reportedByA_time: 0,
reportedByB_time: 0,
winner: address(0),
state: MatchState.Active,
disputeTime: 0
});
}
state = TournamentState.Locked;
currentRound = 0;
roundStartTime = block.timestamp;
emit RoundStarted(0, block.timestamp);
}
/// @dev Confirms winner, marks match complete, checks if round is done.
function _confirmWinner(
uint256 round,
uint256 matchIndex,
address winner,
string memory method
) internal {
matches[round][matchIndex].winner = winner;
matches[round][matchIndex].state = MatchState.Complete;
emit MatchComplete(round, matchIndex, winner, method);
_tryAdvanceRound(round);
}
/// @dev Checks if all matches in `round` are complete. If so, builds next round or distributes prizes.
function _tryAdvanceRound(uint256 round) internal {
uint256 count = _matchCount(round);
for (uint256 i = 0; i < count; i++) {
if (matches[round][i].state != MatchState.Complete) return;
}
// All done
if (round + 1 >= roundCount) {
_distributePrizes(round);
} else {
_buildNextRound(round);
}
}
/// @dev Pairs winners from `round` into `round+1` matches.
function _buildNextRound(uint256 round) internal {
uint256 count = _matchCount(round);
uint256 nextRound = round + 1;
for (uint256 i = 0; i < count / 2; i++) {
matches[nextRound][i] = Match({
playerA: matches[round][i * 2].winner,
playerB: matches[round][i * 2 + 1].winner,
reportedByA: address(0),
reportedByB: address(0),
reportedByA_time: 0,
reportedByB_time: 0,
winner: address(0),
state: MatchState.Active,
disputeTime: 0
});
}
currentRound = nextRound;
roundStartTime = block.timestamp;
emit RoundStarted(nextRound, block.timestamp);
}
// ══════════════════════════════════════════
// INTERNAL: PRIZES
// ══════════════════════════════════════════
function _distributePrizes(uint256 finalRound) internal {
uint256 prizePool = _sendFee();
address first = matches[finalRound][0].winner;
address second = matches[finalRound][0].playerA == first
? matches[finalRound][0].playerB
: matches[finalRound][0].playerA;
_allocatePrizes(finalRound, prizePool, first, second);
state = TournamentState.Complete;
}
function _sendFee() internal returns (uint256 prizePool) {
uint256 totalPool = entryFee * seeds.length;
uint256 fee = (totalPool * FEE_BPS) / BPS_DENOMINATOR;
prizePool = totalPool - fee;
bool ok;
try IERC20(token).transfer(treasury, fee) returns (bool result) { ok = result; } catch {}
if (!ok) {
pendingTreasuryFee += fee;
emit TreasuryFeeFailed(fee);
}
}
function _allocatePrizes(uint256 finalRound, uint256 prizePool, address first, address second) internal {
uint256 n = seeds.length;
uint256 firstPrize;
uint256 secondPrize;
uint256 allocated;
if (n == 4) {
firstPrize = (prizePool * 7000) / BPS_DENOMINATOR;
secondPrize = prizePool - firstPrize;
allocated = prizePool;
} else {
uint256 semiBps = n == 8 ? 500 : 1000;
uint256 firstBps = n == 8 ? 6500 : 5500;
firstPrize = (prizePool * firstBps) / BPS_DENOMINATOR;
secondPrize = (prizePool * 2500) / BPS_DENOMINATOR;
uint256 semiEach = (prizePool * semiBps) / BPS_DENOMINATOR;
allocated = firstPrize + secondPrize + (semiEach * 2);
_awardSemifinalists(finalRound - 1, semiEach);
}
firstPrize += prizePool - allocated; // dust → 1st
prizeOwed[first] += firstPrize;
prizeOwed[second] += secondPrize;
emit PrizesAllocated(first, firstPrize, second, secondPrize);
}
function _awardSemifinalists(uint256 semiRound, uint256 prize) internal {
uint256 count = _matchCount(semiRound);
for (uint256 i = 0; i < count; i++) {
address w = matches[semiRound][i].winner;
address loser = matches[semiRound][i].playerA == w
? matches[semiRound][i].playerB
: matches[semiRound][i].playerA;
prizeOwed[loser] += prize;
}
}
// ══════════════════════════════════════════
// INTERNAL: HELPERS
// ══════════════════════════════════════════
function _matchCount(uint256 round) internal view returns (uint256) {
return (seeds.length / 2) >> round;
}
/// @dev First-reporter wins a dispute. Tie goes to playerA.
function _firstReporter(Match storage m) internal view returns (address) {
if (m.reportedByA != address(0) && m.reportedByB != address(0)) {
return m.reportedByA_time <= m.reportedByB_time ? m.reportedByA : m.reportedByB;
}
return m.reportedByA != address(0) ? m.reportedByA : m.reportedByB;
}
function _sortByRating(address[] storage arr) internal view returns (address[] memory) {
uint256 n = arr.length;
address[] memory sorted = new address[](n);
for (uint256 i = 0; i < n; i++) sorted[i] = arr[i];
for (uint256 i = 1; i < n; i++) {
address key = sorted[i];
uint256 keyRating = entrantRating[key];
int256 j = int256(i) - 1;
while (j >= 0 && entrantRating[sorted[uint256(j)]] < keyRating) {
sorted[uint256(j + 1)] = sorted[uint256(j)];
j--;
}
sorted[uint256(j + 1)] = key;
}
return sorted;
}
function _nearestBracketSize(uint256 n) internal pure returns (uint256) {
if (n >= 16) return 16;
if (n >= 8) return 8;
return 4;
}
// ══════════════════════════════════════════
// VIEWS
// ══════════════════════════════════════════
function getEntrants() external view returns (address[] memory) { return entrants; }
function getSeeds() external view returns (address[] memory) { return seeds; }
function getMatch(uint256 round, uint256 matchIndex)
external view
returns (MatchView memory v)
{
Match storage m = matches[round][matchIndex];
v.playerA = m.playerA;
v.playerB = m.playerB;
v.reportedByA = m.reportedByA;
v.reportedByB = m.reportedByB;
v.winner = m.winner;
v.matchState = m.state;
v.disputeTime = m.disputeTime;
}
function getRoundMatches(uint256 round)
external view
returns (RoundMatchesView memory v)
{
uint256 count = _matchCount(round);
v.playerAs = new address[](count);
v.playerBs = new address[](count);
v.winners = new address[](count);
v.states = new MatchState[](count);
for (uint256 i = 0; i < count; i++) {
v.playerAs[i] = matches[round][i].playerA;
v.playerBs[i] = matches[round][i].playerB;
v.winners[i] = matches[round][i].winner;
v.states[i] = matches[round][i].state;
}
}
function getTournamentInfo()
external view
returns (TournamentInfoView memory v)
{
v.name = tournamentName;
v.sport = sportId;
v.fee = entryFee;
v.maxP = maxPlayers;
v.currentP = entrants.length;
v.deadline = registrationDeadline;
v.tState = state;
}
function getTournamentProgress()
external view
returns (ProgressView memory v)
{
v.curRound = currentRound;
v.rounds = roundCount;
v.totalPool = entryFee * entrants.length;
v.noShow = noShowWindow;
v.dispute = disputeWindow;
}
function getPlayerStatus(address player)
external view
returns (PlayerStatusView memory v)
{
v.registered = isEntrant[player];
v.rating = entrantRating[player];
v.prize = prizeOwed[player];
v.claimed = prizeClaimed[player];
for (uint256 i = 0; i < seeds.length; i++) {
if (seeds[i] == player) { v.seedPos = i + 1; break; }
}
}
function getPrizeBreakdown()
external view
returns (PrizeView memory v)
{
uint256 n = seeds.length > 0 ? seeds.length : maxPlayers;
v.totalPool = entryFee * n;
v.platformFee = (v.totalPool * FEE_BPS) / BPS_DENOMINATOR;
v.netPool = v.totalPool - v.platformFee;
if (n <= 4) {
v.firstPrize = (v.netPool * 7000) / BPS_DENOMINATOR;
v.secondPrize = v.netPool - v.firstPrize;
v.thirdFourthPrize = 0;
} else if (n <= 8) {
v.firstPrize = (v.netPool * 6500) / BPS_DENOMINATOR;
v.secondPrize = (v.netPool * 2500) / BPS_DENOMINATOR;
v.thirdFourthPrize = (v.netPool * 500) / BPS_DENOMINATOR;
} else {
v.firstPrize = (v.netPool * 5500) / BPS_DENOMINATOR;
v.secondPrize = (v.netPool * 2500) / BPS_DENOMINATOR;
v.thirdFourthPrize = (v.netPool * 1000) / BPS_DENOMINATOR;
}
}
function canClaimNoShow(uint256 round, uint256 matchIndex, address caller)
external view
returns (bool eligible, string memory reason)
{
if (state != TournamentState.Locked) return (false, "Not locked");
if (round != currentRound) return (false, "Not current round");
if (block.timestamp < roundStartTime + noShowWindow) return (false, "Window not passed");
Match storage m = matches[round][matchIndex];
if (m.state != MatchState.Active) return (false, "Match not active");
if (caller != m.playerA && caller != m.playerB) return (false, "Not a participant");
if (caller == m.playerA && m.reportedByB != address(0)) return (false, "Opponent did report");
if (caller == m.playerB && m.reportedByA != address(0)) return (false, "Opponent did report");
return (true, "");
}
function canForceAdvance(uint256 round, uint256 matchIndex)
external view
returns (bool eligible, string memory reason)
{
if (state != TournamentState.Locked) return (false, "Not locked");
Match storage m = matches[round][matchIndex];
if (m.state != MatchState.Disputed) return (false, "Not disputed");
if (block.timestamp < m.disputeTime + disputeWindow) return (false, "Window not passed");
return (true, "");
}
}