// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; interface IERC20 { function transfer(address to, uint256 amount) external returns (bool); function balanceOf(address a) external view returns (uint256); } /// @title Squatchi 1-Month Play-to-Earn Victory Pool /// @notice Pays 1 $SQUATCHI once per wallet to champions who beat all 10 levels, for 30 days. contract SquatchiVictoryRewards { IERC20 public immutable SQUATCHI; uint256 public constant REWARD_AMOUNT = 1e18; uint256 public constant ALL_LEVELS_MASK = 0x3FF; uint256 public immutable campaignEndTime; bytes32 private immutable depositorHash; mapping(address => bool) public hasClaimed; uint256 public totalWinners; bool private locked; event VictoryRewarded(address indexed champion, uint256 amount, uint256 timestamp); event RemainingRecovered(address indexed to, uint256 amount); constructor(address tokenAddress) { require(tokenAddress != address(0), "invalid token"); SQUATCHI = IERC20(tokenAddress); campaignEndTime = block.timestamp + 30 days; depositorHash = keccak256(abi.encodePacked(msg.sender)); } function expectedProof(address champion) public view returns (bytes32) { return keccak256(abi.encodePacked("SQUATCHI_CHAMPION_10_LEVELS", champion, ALL_LEVELS_MASK, address(this))); } function claimVictoryReward(uint256 starsMask, bytes32 completionProof) external { require(!locked, "reentrant"); require(block.timestamp <= campaignEndTime, "campaign ended"); require(msg.sender == tx.origin, "EOA only"); require(!hasClaimed[msg.sender], "already claimed"); require(starsMask == ALL_LEVELS_MASK, "beat all 10 levels"); require(completionProof == expectedProof(msg.sender), "bad proof"); locked = true; hasClaimed[msg.sender] = true; totalWinners += 1; require(SQUATCHI.transfer(msg.sender, REWARD_AMOUNT), "transfer failed"); locked = false; emit VictoryRewarded(msg.sender, REWARD_AMOUNT, block.timestamp); } function poolBalance() external view returns (uint256) { return SQUATCHI.balanceOf(address(this)); } function withdrawTokens(address to, uint256 amount) external { require(keccak256(abi.encodePacked(msg.sender)) == depositorHash, "not depositor"); require(SQUATCHI.transfer(to, amount), "transfer failed"); emit RemainingRecovered(to, amount); } function recoverRemainingAfterExpiry(address to) external { require(block.timestamp > campaignEndTime, "campaign active"); require(keccak256(abi.encodePacked(msg.sender)) == depositorHash, "not depositor"); uint256 bal = SQUATCHI.balanceOf(address(this)); require(SQUATCHI.transfer(to, bal), "transfer failed"); emit RemainingRecovered(to, bal); } }