// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; interface IArena56Membership {function isMember(address wallet) external view returns(bool);} /// @notice Escrowed RPS. Testnet deployment only until independently reviewed. contract Arena56 { uint256 public constant FEE_BPS = 250; uint256 public constant MIN_STAKE = 0.0005 ether; uint256 public constant PHASE_SECONDS = 30; uint256 public constant CANCEL_SECONDS = 10; address public immutable treasury; address public immutable referralSetupOwner; address public referralRegistrar; address public membershipRegistry; // Eligibility is captured for both players when the opponent joins, never mid-match. mapping(uint256 => uint8) public foundingMemberMask; event MembershipRegistryConfigured(address registry); event MembershipEligibilityCaptured(uint256 indexed id,uint8 mask); // SEC-001: mainnet deposits start closed; existing-match exits are never paused. bool public depositsEnabled; mapping(address => address) public affiliateOf; mapping(address => bool) public referralAssigned; mapping(address => uint256) public affiliateEarned; mapping(address => uint256) public affiliatePaid; mapping(address => uint256) public affiliateCredits; mapping(address => bool) public affiliateRevoked; uint256 public nextId = 1; enum Phase { Waiting, Commit, Reveal, Finished, Cancelled } struct Match { address p1; address p2; address invited; uint256 stake; uint64 deadline; uint32 attempt; uint8 target; uint8 score1; uint8 score2; Phase phase; address winner; bytes32 c1; bytes32 c2; uint8 m1; uint8 m2; uint8 paymentMask; uint256 settledFee; } mapping(uint256 => Match) public matches; mapping(bytes32 => uint256) public queues; mapping(address => uint256) public credits; mapping(address => uint256) public streak; mapping(address => uint256) public volume; address[] public players; mapping(address => bool) private knownPlayer; struct PlayerStat { address player; uint256 winsInRow; uint256 wagered; } uint256[] private liveIds; mapping(uint256=>uint256) private livePosition; uint256[] private openIds; mapping(uint256=>uint256) private openPosition; mapping(address=>uint256[]) private historyIds; mapping(address=>uint256[]) private activeIds; mapping(address=>mapping(uint256=>uint256)) private activePosition; uint256 private entered; function _track(address player,uint256 id) internal { historyIds[player].push(id);activeIds[player].push(id);activePosition[player][id]=activeIds[player].length; } function _removeActive(address player,uint256 id) internal {uint256 pos=activePosition[player][id];if(pos==0)return;uint256 last=activeIds[player][activeIds[player].length-1];activeIds[player][pos-1]=last;activePosition[player][last]=pos;activeIds[player].pop();delete activePosition[player][id];} function _removeOpen(uint256 id) internal {uint256 pos=openPosition[id];if(pos==0)return;uint256 last=openIds[openIds.length-1];openIds[pos-1]=last;openPosition[last]=pos;openIds.pop();delete openPosition[id];} function _close(uint256 id) internal {uint256 pos=livePosition[id];if(pos!=0){uint256 last=liveIds[liveIds.length-1];liveIds[pos-1]=last;livePosition[last]=pos;liveIds.pop();delete livePosition[id];}_removeActive(matches[id].p1,id);_removeActive(matches[id].p2,id);} function _page(uint256[] storage source,uint256 offset,uint256 count,bool reverse) internal view returns(uint256[] memory ids,Match[] memory ms,uint256 total){require(count<=50,"Page too large");total=source.length;if(offset>=total)return(new uint256[](0),new Match[](0),total);if(count>total-offset)count=total-offset;ids=new uint256[](count);ms=new Match[](count);for(uint256 i=0;i= MIN_STAKE,"Minimum stake 0.0005 ETH"); require(invited != msg.sender,"Self challenge"); id=nextId++; Match storage m=matches[id]; m.p1=msg.sender;m.invited=invited;m.stake=msg.value;m.target=target;m.phase=Phase.Waiting;m.attempt=1; openIds.push(id);openPosition[id]=openIds.length;_track(msg.sender,id); emit Created(id,msg.sender,msg.value,target,invited); } function create(uint8 target,address invited) external payable nonReentrant returns(uint256) { return _create(target,invited); } function quick(uint8 target) external payable returns(uint256 id) { require(msg.value==0.001 ether || msg.value==0.005 ether || msg.value==0.01 ether,"Preset required"); require(target==1 || target==2,"Format"); bytes32 key=keccak256(abi.encode(msg.value,target));id=queues[key]; if(id!=0 && matches[id].phase==Phase.Waiting && matches[id].deadline==0) { require(matches[id].p1!=msg.sender,"Already queued");_join(id);delete queues[key]; } else {id=_create(target,address(0));queues[key]=id;} } function join(uint256 id) external payable nonReentrant { _join(id); } function _join(uint256 id) internal { require(depositsEnabled,"New deposits paused"); require(referralRegistrar==address(0)||referralAssigned[msg.sender],"Signup proof required"); require(activeIds[msg.sender].length==0,"Finish active match"); Match storage m=matches[id];require(m.p1!=address(0) && m.phase==Phase.Waiting,"Not open");require(msg.sender!=m.p1,"Self match"); require(m.deadline==0 || block.timestamp=m.deadline,"Cancellation pending"); _cancelWaiting(id); } function _cancelWaiting(uint256 id) internal { Match storage m=matches[id];m.phase=Phase.Cancelled;_removeOpen(id);_close(id); emit Cancelled(id);if(_pay(id,m.p1,m.stake))m.paymentMask|=1; } function playerCount() external view returns(uint256) { return players.length; } function getPlayers(uint256 offset,uint256 count) external view returns(PlayerStat[] memory result) { require(count<=50 && offset<=players.length,"Invalid page"); if(count>players.length-offset) count=players.length-offset;result=new PlayerStat[](count); for(uint256 i=0;i 0 && from < nextId,"Invalid cursor"); if(count > from) count=from; result=new Match[](count); for(uint256 i=0;i=1 && move<=3,"Move"); bytes32 c=commitment(id,m.attempt,player,move,salt); if(player==m.p1){require(m.m1==0 && m.c1==c,"Bad reveal");m.m1=move;}else{require(player==m.p2 && m.m2==0 && m.c2==c,"Bad reveal");m.m2=move;} emit Revealed(id,player,move); if(m.m1!=0 && m.m2!=0) { if(m.m1!=m.m2){if((m.m1==1&&m.m2==3)||(m.m1==2&&m.m2==1)||(m.m1==3&&m.m2==2))m.score1++;else m.score2++;} emit Round(id,m.attempt,m.m1,m.m2,m.score1,m.score2); if(m.score1==m.target || m.score2==m.target) _settle(id,m.score1==m.target?m.p1:m.p2,false); else _nextHand(m); } } function _nextHand(Match storage m) internal {m.attempt++;m.c1=0;m.c2=0;m.m1=0;m.m2=0;m.phase=Phase.Commit;m.deadline=uint64(block.timestamp+PHASE_SECONDS);} function claimTimeout(uint256 id) external nonReentrant { _claimTimeout(id); } function settleExpired(uint256[] calldata ids) external nonReentrant {require(ids.length<=25,"Batch too large");for(uint256 i=0;i=m.deadline)_cancelWaiting(ids[i]);else if((m.phase==Phase.Commit || m.phase==Phase.Reveal) && block.timestamp>=m.deadline)_claimTimeout(ids[i]);}} function _claimTimeout(uint256 id) internal { Match storage m=matches[id];require((m.phase==Phase.Commit || m.phase==Phase.Reveal) && block.timestamp>=m.deadline,"Not expired"); bool a=m.phase==Phase.Commit?m.c1!=0:m.m1!=0; bool b=m.phase==Phase.Commit?m.c2!=0:m.m2!=0; if(a!=b){ if(a)m.score1++;else m.score2++; emit HandForfeit(id,m.attempt,a?m.p1:m.p2,m.score1,m.score2); if(m.score1==m.target||m.score2==m.target)_settle(id,a?m.p1:m.p2,true); else _nextHand(m); }else { m.score1++;m.score2++; emit BothMissed(id,m.attempt,m.score1,m.score2); if(m.score1m.score2?m.p1:m.p2,true);return;} m.phase=Phase.Cancelled;_close(id);streak[m.p1]=0;streak[m.p2]=0; uint256 fee=2*m.stake*FEE_BPS/10000; m.settledFee=fee; uint256 refund1=m.stake-fee/2;uint256 refund2=m.stake-(fee-fee/2); emit Cancelled(id);emit Abandoned(id,refund1,refund2,fee); if(_pay(id,m.p1,refund1))m.paymentMask|=1;if(_pay(id,m.p2,refund2))m.paymentMask|=2; _distributeFee(id,fee); } } function _settle(uint256 id,address winner,bool forfeit) internal { Match storage m=matches[id];m.phase=Phase.Finished;m.winner=winner;_close(id);uint256 pot=2*m.stake; bool exempt=(foundingMemberMask[id]&(winner==m.p1?1:2))!=0;uint256 fee=exempt?0:pot*FEE_BPS/10000; m.settledFee=fee; streak[winner]++;streak[winner==m.p1?m.p2:m.p1]=0; emit Settled(id,winner,pot-fee,fee,forfeit); if(_pay(id,winner,pot-fee))m.paymentMask|=winner==m.p1?1:2; if(fee!=0)_distributeFee(id,fee); } function _pay(uint256 id,address recipient,uint256 amount) internal returns(bool ok) { // A recipient cannot stall settlement or reenter any state-changing function. (ok,)=recipient.call{value:amount,gas:50000}(""); if(!ok)credits[recipient]+=amount; emit Payment(id,recipient,amount,ok); } function withdraw() external nonReentrant { uint256 amount=credits[msg.sender];require(amount!=0,"No balance");credits[msg.sender]=0;affiliatePaid[msg.sender]+=affiliateCredits[msg.sender];affiliateCredits[msg.sender]=0; (bool ok,)=msg.sender.call{value:amount}("");require(ok,"Transfer failed"); } }