// SPDX-License-Identifier: MIT pragma solidity 0.8.30; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {IWinRegistry,IWinRandomness} from "./WinJackpotVault.sol"; /** PONS-only candidate. The immutable indexer attests complete trade snapshots. * Merkle-sum proofs verify selection, not the truth/completeness of the indexer's inputs. * Starts closed. No deployed gateway vault is upgraded by this contract. */ contract WinPonsSnapshotVault is ReentrancyGuard { uint256 public constant COIN_THRESHOLD=0.5 ether; uint256 public constant GLOBAL_THRESHOLD=2 ether; uint256 public constant ENTRY_UNIT=0.01 ether; uint256 public constant MAX_WEIGHT=4; uint256 public constant ENTRY_LIFETIME=24 hours; uint256 public constant MIN_ENTRANTS=3; uint256 public constant CONFIRMATIONS=32; address public immutable owner; address public immutable indexer; IWinRegistry public registry; IWinRandomness public randomness; bool public configured; bool public paused=true; bool public emergencyMode; uint256 public roundCount; uint256 public totalReceived; uint256 public totalAvailable; uint256 public totalReserved; uint256 public totalCredits; uint256 public totalPaid; uint256 public totalEmergencyWithdrawn; uint256 public totalEmergencyReturned; address public recoveryDestination; enum Phase {Missing,Open,Frozen,Requested,Ready,Settled} struct Pool {uint256 available;uint256 remainder;uint256 activeRound;} struct Round { address scope; uint256 award; uint256 requestId; uint256 word; bytes32 commitment; Phase phase; uint256 entrants; uint256 totalWeight; bytes32 root; bytes32 manifestHash; uint256 openedBlock; uint256 afterBlock; uint256 cutoffBlock; bytes32 cutoffHash; address[3] winners; uint256[3] prizes; bool[3] paid; } struct EntryProof {uint256 index;address wallet;uint256 weight;bytes32[] hashes;uint256[] sums;} mapping(address=>Pool) public pools; mapping(address=>uint256) public lastCutoff; mapping(uint256=>Round) private rounds; mapping(uint256=>uint256) public roundForRequest; event PauseChanged(bool paused); event CreatorFeesReceived(address indexed token,uint256 amount,uint256 coinPart,uint256 globalPart); event RoundOpened(uint256 indexed roundId,address indexed scope,uint256 award,uint256 afterBlock); event SnapshotCommitted(uint256 indexed roundId,bytes32 root,bytes32 manifestHash,uint256 totalWeight,uint256 cutoffBlock,bytes32 cutoffHash); event RoundFrozen(uint256 indexed roundId,address indexed scope,uint256 award,uint256 entrants,bytes32 commitment); event RandomnessRequested(uint256 indexed roundId,uint256 indexed requestId); event RandomnessStored(uint256 indexed roundId,uint256 word); event WinnersSelected(uint256 indexed roundId,address[3] winners,uint256[3] amounts); event PrizePaid(uint256 indexed roundId,uint256 indexed rank,address indexed winner,address destination,uint256 amount); event RecoveryStarted(); event EmergencyWithdrawal(address indexed destination,uint256 amount); event EmergencyFundsReturned(address indexed sender,uint256 amount); event RecoveryFinished(); error Unauthorized(); error InvalidConfiguration(); error WrongPhase(); error InvalidEntry(); error TransferFailed(); error RecoveryActive(); error UnfundedLiabilities(); error InvalidProof(); modifier onlyOwner(){if(msg.sender!=owner)revert Unauthorized();_;} constructor(address owner_,address indexer_){ if(owner_==address(0)||indexer_==address(0))revert InvalidConfiguration();owner=owner_;indexer=indexer_; } function configure(address registry_,address randomness_) external onlyOwner { if(configured||registry_.code.length==0||randomness_.code.length==0)revert InvalidConfiguration(); registry=IWinRegistry(registry_);randomness=IWinRandomness(randomness_);configured=true; } function setPaused(bool value) external onlyOwner { if(!configured)revert InvalidConfiguration();if(!value&&emergencyMode)revert RecoveryActive();paused=value;emit PauseChanged(value); } function depositCreatorFees(address token) external payable nonReentrant { if(!configured||token==address(0)||registry.collectorFor(token)!=msg.sender||msg.value==0)revert Unauthorized(); Pool storage coin=pools[token];uint256 scaled=msg.value*8000+coin.remainder; uint256 coinPart=scaled/10000;coin.remainder=scaled%10000;uint256 globalPart=msg.value-coinPart; coin.available+=coinPart;pools[address(0)].available+=globalPart;totalReceived+=msg.value;totalAvailable+=msg.value; emit CreatorFeesReceived(token,msg.value,coinPart,globalPart);_open(token);_open(address(0)); } function openRound(address scope) external {if(scope!=address(0)&®istry.collectorFor(scope)==address(0))revert InvalidEntry();_open(scope);} function _open(address scope) private { Pool storage p=pools[scope];uint256 threshold=scope==address(0)?GLOBAL_THRESHOLD:COIN_THRESHOLD; if(paused||emergencyMode||p.activeRound!=0||p.available1000000||totalWeightentrants*4|| cutoffBlock256||cutoffHash==bytes32(0)||blockhash(cutoffBlock)!=cutoffHash)revert InvalidEntry(); r.root=root;r.manifestHash=manifestHash;r.entrants=entrants;r.totalWeight=totalWeight;r.cutoffBlock=cutoffBlock;r.cutoffHash=cutoffHash; r.commitment=keccak256(abi.encode(block.chainid,address(this),id,r.scope,r.award,root,manifestHash,totalWeight,r.afterBlock,cutoffBlock,cutoffHash)); r.phase=Phase.Frozen;lastCutoff[r.scope]=cutoffBlock;pools[r.scope].activeRound=0; emit SnapshotCommitted(id,root,manifestHash,totalWeight,cutoffBlock,cutoffHash); emit RoundFrozen(id,r.scope,r.award,entrants,r.commitment); // The next pool can start while this draw awaits randomness/payouts. _open(r.scope); } function requestRound(uint256 id) external payable nonReentrant { if(paused||emergencyMode)revert RecoveryActive();Round storage r=rounds[id];if(r.phase!=Phase.Frozen)revert WrongPhase(); if(msg.value!=randomness.fee())revert InvalidConfiguration();r.phase=Phase.Requested; uint256 requestId=randomness.request{value:msg.value}(r.commitment); if(requestId==0||roundForRequest[requestId]!=0)revert InvalidConfiguration();r.requestId=requestId;roundForRequest[requestId]=id; emit RandomnessRequested(id,requestId); } function receiveRandomness(uint256 requestId,uint256 word) external { if(msg.sender!=address(randomness))revert Unauthorized();uint256 id=roundForRequest[requestId];Round storage r=rounds[id]; if(id==0||r.phase!=Phase.Requested)return;r.word=word;r.phase=Phase.Ready;emit RandomnessStored(id,word); } function _proof(Round storage r,EntryProof calldata p) private view returns(uint256 start){ if(p.index>=r.entrants||p.wallet==address(0)||p.weight==0||p.weight>4||p.hashes.length!=p.sums.length||p.hashes.length>20)revert InvalidProof(); bytes32 hash=keccak256(abi.encode(uint8(0),p.index,p.wallet,p.weight));uint256 sum=p.weight;uint256 index=p.index; for(uint256 i;i>=1; } if(index!=0||hash!=r.root||sum!=r.totalWeight)revert InvalidProof(); } function settleRound(uint256 id,EntryProof[3] calldata entries) external { if(paused||emergencyMode)revert RecoveryActive();Round storage r=rounds[id];if(r.phase!=Phase.Ready)revert WrongPhase(); uint256[3] memory starts;uint256 remaining=r.totalWeight; for(uint256 rank;rank<3;rank++){ starts[rank]=_proof(r,entries[rank]);uint256 limit=type(uint256).max-(type(uint256).max%remaining);uint256 sample;uint256 counter; do{sample=uint256(keccak256(abi.encode(r.commitment,r.word,rank,counter++)));}while(sample>=limit); uint256 ticket=sample%remaining; uint256 a;uint256 b=1;if(rank==2&&starts[b]=1&&ticket>=starts[a])ticket+=entries[a].weight; if(rank==2&&ticket>=starts[b])ticket+=entries[b].weight; if(ticket=starts[rank]+entries[rank].weight)revert InvalidProof(); for(uint256 previous;previous=3)revert WrongPhase();_pay(id,rank,rounds[id].winners[rank]);} function claimTo(uint256 id,uint256 rank,address payable destination) external nonReentrant { if(rank>=3||msg.sender!=rounds[id].winners[rank]||destination==address(0))revert Unauthorized();_pay(id,rank,destination); } function _pay(uint256 id,uint256 rank,address destination) private { if(paused||emergencyMode)revert RecoveryActive();Round storage r=rounds[id];if(r.phase!=Phase.Settled||r.paid[rank])revert WrongPhase(); uint256 amount=r.prizes[rank];r.paid[rank]=true;totalCredits-=amount;totalPaid+=amount; (bool ok,)=destination.call{value:amount}("");if(!ok)revert TransferFailed();emit PrizePaid(id,rank,r.winners[rank],destination,amount); } function startRecovery() external onlyOwner nonReentrant {if(!configured)revert InvalidConfiguration();emergencyMode=true;paused=true;emit PauseChanged(true);emit RecoveryStarted();} // Explicit custodial owner power retained at the user's request. Liabilities survive withdrawal. function emergencyWithdraw(address destination,uint256 amount) external onlyOwner nonReentrant { if(!configured||destination==address(0)||destination==address(this)||amount==0||amount>address(this).balance)revert InvalidConfiguration(); emergencyMode=true;paused=true;recoveryDestination=destination;totalEmergencyWithdrawn+=amount;emit PauseChanged(true);emit RecoveryStarted(); (bool ok,)=destination.call{value:amount}("");if(!ok)revert TransferFailed();emit EmergencyWithdrawal(destination,amount); } function restoreEmergencyFunds() external payable nonReentrant {if(!emergencyMode||msg.value==0)revert InvalidEntry();totalEmergencyReturned+=msg.value;emit EmergencyFundsReturned(msg.sender,msg.value);} function finishRecovery() external onlyOwner nonReentrant {if(!emergencyMode)revert WrongPhase();if(address(this).balanceaddress(this).balance?owed-address(this).balance:0,totalEmergencyWithdrawn,totalEmergencyReturned);} function liabilities() public view returns(uint256){return totalAvailable+totalReserved+totalCredits;} function roundInfo(uint256 id) external view returns(address,uint256,uint256,uint256,bytes32,Phase){Round storage r=rounds[id];return(r.scope,r.award,r.entrants,r.requestId,r.commitment,r.phase);} function snapshotInfo(uint256 id) external view returns(bytes32,bytes32,uint256,uint256,uint256,bytes32,uint256){Round storage r=rounds[id];return(r.root,r.manifestHash,r.totalWeight,r.afterBlock,r.cutoffBlock,r.cutoffHash,r.openedBlock);} function randomWord(uint256 id) external view returns(uint256){return rounds[id].word;} function winnerInfo(uint256 id) external view returns(address[3] memory,uint256[3] memory,bool[3] memory){Round storage r=rounds[id];return(r.winners,r.prizes,r.paid);} }