pragma solidity ^0.4.25; contract Boomerang { /////////// //STORAGE// /////////// address public admin; uint256 public contestCount; bool internal processingStarted; uint256 internal salt; uint256 internal processingBegunAtBlock; uint256 internal RNGblockDelay = 1; bytes32 internal seedA; bytes32 internal seedB; mapping(uint256 => bool) public prizeAwarded; mapping(uint256 => bool) public contestStarted; mapping(uint256 => address) public volumeWinner; mapping(uint256 => address) public randomWinner; mapping(uint256 => uint256) public winningIndex; mapping(uint256 => uint256) public prizePool; mapping(uint256 => uint256) public highest; mapping(uint256 => uint256) public contestClose; mapping(uint256 => uint256) public totalBounceVolume; mapping(uint256 => address[]) public players; mapping(uint256 => mapping(address => uint256)) public playerTotal; mapping(uint256 => mapping(address => bool)) public playerExists; /////////////// //CONSTRUCTOR// /////////////// constructor() public { admin = msg.sender; } ///////////// //MODIFIERS// ///////////// modifier notContract() { require(msg.sender == tx.origin, "no contracts allowed"); _; } modifier onlyAdmin() { require(msg.sender == admin, "you must be the admin"); _; } ////////// //EVENTS// ////////// event newContest(uint256 _prizeTotal, uint256 _contestClose, string _message); event prizeAdded(address _caller, uint256 _amountAdded, uint256 _newPrizeTotal, string _message); event playerBounce(address _player, uint256 _bounceAmount, uint256 _playerTotal, uint256 _volume, string _message); event winnerPayed(address _caller, address _randomWinner, address _volumeWinner, uint256 _prize, string _message); /////////////////// //ADMIN FUNCTIONS// /////////////////// function openContest(uint256 _durationHours) external payable onlyAdmin() { require (block.timestamp > contestClose[contestCount], "there is an active contest"); require (msg.value >= 1 ether, "you must provide a prize seed"); require (_durationHours >= 1, "the contest must be at least 1 hour"); //payout winner from last round if (prizeAwarded[contestCount] == false && contestCount > 0) { payoutWinner(); } contestCount++; contestStarted[contestCount] = true; contestClose[contestCount] = block.timestamp + _durationHours * 3600; //multiply _duration (hours) by 3600 seconds/hour prizePool[contestCount] += msg.value; emit prizeAdded(msg.sender, msg.value, prizePool[contestCount], "ETH was just added to the prize"); emit newContest(prizePool[contestCount], contestClose[contestCount], "a new contest just started"); } // emergency withdraw after contest closes // can only be called if ether is left over in contract after game ends and prize is awarded function withdrawPostPrize() external onlyAdmin() { require (block.timestamp >= contestClose[contestCount] && prizeAwarded[contestCount] == true, "the contest is not active"); admin.transfer(address(this).balance); } ////////////////// //USER FUCNTIONS// ////////////////// //generous donations accepted function addPrize() external payable notContract() { require (block.timestamp < contestClose[contestCount] && contestCount > 0, "the contest is not active"); prizePool[contestCount] += msg.value; emit prizeAdded(msg.sender, msg.value, prizePool[contestCount], "ETH was just added to the prize"); } function() external payable notContract() { bounce(); } //user sends funds to contract and they are immediately returned function bounce() public payable notContract() { require (contestStarted[contestCount] == true && block.timestamp < contestClose[contestCount], "the contest is not active"); //update player accounting if (msg.value >= 0 && playerExists[contestCount][msg.sender] == false) { playerExists[contestCount][msg.sender] = true; players[contestCount].push(msg.sender); } playerTotal[contestCount][msg.sender] += msg.value; //update contest accounting totalBounceVolume[contestCount] += msg.value; //check if there is a new winner and update if necessary //if there is a tie, the first person to reach the highest balance wins if (playerTotal[contestCount][msg.sender] > highest[contestCount]){ highest[contestCount] = playerTotal[contestCount][msg.sender]; volumeWinner[contestCount] = msg.sender; } //return user funds msg.sender.transfer(msg.value); //increment salt salt++; emit playerBounce(msg.sender, msg.value, playerTotal[contestCount][msg.sender], totalBounceVolume[contestCount], "a player just bounced some ETH"); } //this needs to be called first for secure RNG function startProcessing() public notContract() { require(block.timestamp >= contestClose[contestCount], "the contest is still active"); require(processingStarted == false || block.number > processingBegunAtBlock + 256, "you cannot regenerate the seed right now"); //seed A can be regenerated if more than 256 blocks have passed generateSeedA(); } //call after startProcessing() //anyone can call this when a contest ends to distribute the prize function payoutWinner() public notContract() { require(block.timestamp >= contestClose[contestCount], "the contest is still active"); require(prizeAwarded[contestCount] == false, "the prize was awarded already"); require(processingStarted = true && block.number < processingBegunAtBlock + 255, "call startProcessing() first"); //confirm there is actually a winner and send them the prize if(players[contestCount].length > 0){ //generate second seed and figure out winner generateSeedB(); RNG(); prizeAwarded[contestCount] = true; uint256 prizeShare = prizePool[contestCount] / 2; randomWinner[contestCount].transfer(prizeShare); volumeWinner[contestCount].transfer(prizeShare); } else { //return balance to admin if there is no valid player and reset //will only hold true if there are zero players //prevents locked ether if no valid players join during contract duration prizeAwarded[contestCount] = true; processingStarted = false; admin.transfer(prizePool[contestCount]); } emit winnerPayed(msg.sender, randomWinner[contestCount], volumeWinner[contestCount], prizePool[contestCount], "the winner was just payed"); } ////////////////// //VIEW FUNCTIONS// ////////////////// function isContestOpen() external view returns(bool _isOpen) { if(contestStarted[contestCount] && block.timestamp < contestClose[contestCount]){ return true; } else { return false; } } function timeUntilClose() external view returns(uint256 _timeLeft) { if(contestStarted[contestCount] && block.timestamp < contestClose[contestCount]){ uint256 timeLeft = contestClose[contestCount] - block.timestamp; if (timeLeft < 0) { return 0; } else { return timeLeft; } } else { return 0; } } function contractBalance() external view returns(uint256 _contractBalance) { return address(this).balance; } function activePlayers() external view returns(uint256 _activePlayers) { return players[contestCount].length; } ////////////////////// //INTERNAL FUNCTIONS// ////////////////////// function generateSeedA() internal { processingBegunAtBlock = block.number; processingStarted = true; seedA = blockhash(processingBegunAtBlock - 1); } function generateSeedB() internal { seedB = blockhash(processingBegunAtBlock + RNGblockDelay); processingStarted = false; } function RNG() internal { bytes32 hash = keccak256(abi.encodePacked(seedA, seedB, salt)); winningIndex[contestCount] = uint256(hash) % players[contestCount].length; randomWinner[contestCount] = players[contestCount][winningIndex[contestCount]]; } }
0.4.25