/** * Source Code first verified at https://etherscan.io on Monday, March 18, 2019 (UTC) */ pragma solidity ^0.5.0; /** * @title ERC20 interface * @dev see https://eips.ethereum.org/EIPS/eip-20 */ interface IERC20 { function transfer(address to, uint256 value) external returns (bool); function approve(address spender, uint256 value) external returns (bool); function transferFrom(address from, address to, uint256 value) external returns (bool); function totalSupply() external view returns (uint256); function balanceOf(address who) external view returns (uint256); function allowance(address owner, address spender) external view returns (uint256); event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); } /** * @title Ownable * @dev The Ownable contract has an owner address, and provides basic authorization control * functions, this simplifies the implementation of "user permissions". */ contract Ownable { address private _owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev The Ownable constructor sets the original `owner` of the contract to the sender * account. */ constructor () internal { _owner = msg.sender; emit OwnershipTransferred(address(0), _owner); } /** * @return the address of the owner. */ function owner() public view returns (address) { return _owner; } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { require(isOwner()); _; } /** * @return true if `msg.sender` is the owner of the contract. */ function isOwner() public view returns (bool) { return msg.sender == _owner; } /** * @dev Allows the current owner to relinquish control of the contract. * It will not be possible to call the functions with the `onlyOwner` * modifier anymore. * @notice Renouncing ownership will leave the contract without an owner, * thereby removing any functionality that is only available to the owner. */ function renounceOwnership() public onlyOwner { emit OwnershipTransferred(_owner, address(0)); _owner = address(0); } /** * @dev Allows the current owner to transfer control of the contract to a newOwner. * @param newOwner The address to transfer ownership to. */ function transferOwnership(address newOwner) public onlyOwner { _transferOwnership(newOwner); } /** * @dev Transfers control of the contract to a newOwner. * @param newOwner The address to transfer ownership to. */ function _transferOwnership(address newOwner) internal { require(newOwner != address(0)); emit OwnershipTransferred(_owner, newOwner); _owner = newOwner; } } contract EthMadness is Ownable { // Represents the submission to the contest. struct Entrant { // The user who submitted this entry address submitter; // The "index" of this entry. Used to break ties incase two submissions are the same. (earlier submission wins) uint48 entryIndex; } // Represents a current top score in the contest struct TopScore { // The index of this entry (used for tie-breakas a de-dups) uint48 entryIndex; // This bracket's score uint32 score; // The total point differential for this bracket uint64 difference; // The account which submitted this bracket address submitter; } // Represents the results of the contest. struct Result { // The encoded results of the tournament bytes16 winners; // Team A's score in the final uint8 scoreA; // Team B's score in the final uint8 scoreB; // Whether or not this is the final Results (used to tell if a vote is real or not) bool isFinal; } // Represents the various states that the contest will go through. enum ContestState { // The contest is open for people to submit entries. Oracles can also be added during this period. OPEN_FOR_ENTRIES, // The tournament is in progress, no more entries can be received and no oracles can vote TOURNAMENT_IN_PROGRESS, // The tournament is over and we're waiting for all the oracles to submit the results WAITING_FOR_ORACLES, // The oracels have submitted the results and we're waiting for winners to claim their prize WAITING_FOR_WINNING_CLAIMS, // The contest has completed and the winners have been paid out COMPLETED } // Maximum number of entries that will be allowed uint constant MAX_ENTRIES = 2**48; // The number of entries which have been received. uint48 entryCount = 0; // Map of the encoded entry to the user who crreated it. mapping (uint256 => Entrant) public entries; // The times where we're allowed to transition the contract's state mapping (uint => uint) public transitionTimes; // The current state of the contest ContestState public currentState; // The recorded votes of our oracles mapping (address => Result) public oracleVotes; // The oracles who will submit the results of the tournament address[] public oracles; // The maximum number of oracles we'll allow vote in our contest uint constant MAX_ORACLES = 10; // The final result of the tournament that the oracles agreed on Result public finalResult; // Keeps the current top 3 best scores and who submitted them. When the contest ends, they'll be paid out TopScore[3] public topThree; // The address of the ERC20 token that defines our prize address public prizeERC20TokenAddress; // The amount of the prize to reward uint public prizeAmount; // Event emitted when a new entry gets submitted to the contest event EntrySubmitted( // The account who submitted this bracket address indexed submitter, // A compressed representation of the entry combining the picks and final game scores uint256 indexed entryCompressed, // The order this entry was received. Used for tiebreaks uint48 indexed entryIndex, // Optional bracket name provided by the submitter string bracketName ); // Constructs a new instance of the EthMadness contract with the given transition times constructor(uint[] memory times, address erc20Token, uint erc20Amount) public { // Initialize the oracles array with the sender's address oracles = [msg.sender]; // Set up our prize info prizeERC20TokenAddress = erc20Token; prizeAmount = erc20Amount; // Set up our transition times require(times.length == 4); transitionTimes[uint(ContestState.TOURNAMENT_IN_PROGRESS)] = times[0]; transitionTimes[uint(ContestState.WAITING_FOR_ORACLES)] = times[1]; transitionTimes[uint(ContestState.WAITING_FOR_WINNING_CLAIMS)] = times[2]; transitionTimes[uint(ContestState.COMPLETED)] = times[3]; // The initial state should be allowing people to make entries currentState = ContestState.OPEN_FOR_ENTRIES; } // Gets the total number of entries we've received function getEntryCount() public view returns (uint256) { return entryCount; } // Gets the number of Oracles we have registered function getOracleCount() public view returns(uint256) { return oracles.length; } // Returns the transition times for our contest function getTransitionTimes() public view returns (uint256, uint256, uint256, uint256) { return ( transitionTimes[uint(ContestState.TOURNAMENT_IN_PROGRESS)], transitionTimes[uint(ContestState.WAITING_FOR_ORACLES)], transitionTimes[uint(ContestState.WAITING_FOR_WINNING_CLAIMS)], transitionTimes[uint(ContestState.COMPLETED)] ); } // Internal function for advancing the state of the bracket function advanceState(ContestState nextState) private { require(uint(nextState) == uint(currentState) + 1, "Can only advance state by 1"); require(now > transitionTimes[uint(nextState)], "Transition time hasn't happened yet"); currentState = nextState; } // Helper to make sure the picks submitted are legal function arePicksOrResultsValid(bytes16 picksOrResults) public pure returns (bool) { // Go through and make sure that this entry has 1 pick for each game for (uint8 gameId = 0; gameId < 63; gameId++) { uint128 currentPick = extractResult(picksOrResults, gameId); if (currentPick != 2 && currentPick != 1) { return false; } } return true; } // Submits a new entry to the tournament function submitEntry(bytes16 picks, uint64 scoreA, uint64 scoreB, string memory bracketName) public { require(currentState == ContestState.OPEN_FOR_ENTRIES, "Must be in the open for entries state"); require(arePicksOrResultsValid(picks), "The supplied picks are not valid"); // Do some work to encode the picks and scores into a single uint256 which becomes a key uint256 scoreAShifted = uint256(scoreA) * (2 ** (24 * 8)); uint256 scoreBShifted = uint256(scoreB) * (2 ** (16 * 8)); uint256 picksAsNumber = uint128(picks); uint256 entryCompressed = scoreAShifted | scoreBShifted | picksAsNumber; require(entries[entryCompressed].submitter == address(0), "This exact bracket & score has already been submitted"); // Emit the event that this entry was received and save the entry emit EntrySubmitted(msg.sender, entryCompressed, entryCount, bracketName); Entrant memory entrant = Entrant(msg.sender, entryCount); entries[entryCompressed] = entrant; entryCount++; } // Adds an allowerd oracle who will vote on the results of the contest. Only the contract owner can do this // and it can only be done while the tournament is still open for entries function addOracle(address oracle) public onlyOwner { require(currentState == ContestState.OPEN_FOR_ENTRIES, "Must be accepting entries"); require(oracles.length < MAX_ORACLES - 1, "Must be less than max number of oracles"); oracles.push(oracle); } // In case something goes wrong, allow the owner to eject from the contract // but only while picks are still being made or after the contest completes function refundRemaining(uint256 amount) public onlyOwner { require(currentState == ContestState.OPEN_FOR_ENTRIES || currentState == ContestState.COMPLETED, "Must be accepting entries"); IERC20 erc20 = IERC20(prizeERC20TokenAddress); erc20.transfer(msg.sender, amount); } // Submits a new oracle's vote describing the results of the tournament function submitOracleVote(uint oracleIndex, bytes16 winners, uint8 scoreA, uint8 scoreB) public { require(currentState == ContestState.WAITING_FOR_ORACLES, "Must be in waiting for oracles state"); require(oracles[oracleIndex] == msg.sender, "Wrong oracle index"); require(arePicksOrResultsValid(winners), "Results are not valid"); oracleVotes[msg.sender] = Result(winners, scoreA, scoreB, true); } // Close the voting and set the final result. Pass in what should be the consensus agreed by the // 70% of the oracles function closeOracleVoting(bytes16 winners, uint8 scoreA, uint8 scoreB) public { require(currentState == ContestState.WAITING_FOR_ORACLES); // Count up how many oracles agree with this result uint confirmingOracles = 0; for (uint i = 0; i < oracles.length; i++) { Result memory oracleVote = oracleVotes[oracles[i]]; if (oracleVote.isFinal && oracleVote.winners == winners && oracleVote.scoreA == scoreA && oracleVote.scoreB == scoreB) { confirmingOracles++; } } // Require 70%+ of Oracles to have voted and agree on the result uint percentAggreement = (confirmingOracles * 100) / oracles.length; require(percentAggreement > 70, "To close oracle voting, > 70% of oracles must agree"); // Change the state and set our final result which will be used to compute scores advanceState(ContestState.WAITING_FOR_WINNING_CLAIMS); finalResult = Result(winners, scoreA, scoreB, true); } // Closes the entry period and marks that the actual tournament is in progress function markTournamentInProgress() public { advanceState(ContestState.TOURNAMENT_IN_PROGRESS); require(oracles.length > 0, "Must have at least 1 oracle registered"); // Require that we have the amount of funds locked in the contract we expect IERC20 erc20 = IERC20(prizeERC20TokenAddress); require(erc20.balanceOf(address(this)) >= prizeAmount, "Must have a balance in this contract"); } // Mark that the tournament has completed and oracles can start submitting results function markTournamentFinished() public { advanceState(ContestState.WAITING_FOR_ORACLES); } // After the oracles have voted and winners have claimed their prizes, this closes the contest and // pays out the winnings to the 3 winners function closeContestAndPayWinners() public { advanceState(ContestState.COMPLETED); require(topThree[0].submitter != address(0), "Not enough claims"); require(topThree[1].submitter != address(0), "Not enough claims"); require(topThree[2].submitter != address(0), "Not enough claims"); uint firstPrize = (prizeAmount * 70) / 100; uint secondPrize = (prizeAmount * 20) / 100; uint thirdPrize = (prizeAmount * 10) / 100; IERC20 erc20 = IERC20(prizeERC20TokenAddress); erc20.transfer(topThree[0].submitter, firstPrize); erc20.transfer(topThree[1].submitter, secondPrize); erc20.transfer(topThree[2].submitter, thirdPrize); } // Scores an entry and places it in the right sort order function scoreAndSortEntry(uint256 entryCompressed, bytes16 results, uint64 scoreAActual, uint64 scoreBActual) private returns (uint32) { require(currentState == ContestState.WAITING_FOR_WINNING_CLAIMS, "Must be in the waiting for claims state"); require(entries[entryCompressed].submitter != address(0), "The entry must have actually been submitted"); // Pull out the pick information from the compressed entry bytes16 picks = bytes16(uint128((entryCompressed & uint256((2 ** 128) - 1)))); uint256 shifted = entryCompressed / (2 ** 128); // shift over 128 bits uint64 scoreA = uint64((shifted & uint256((2 ** 64) - 1))); shifted = entryCompressed / (2 ** 192); uint64 scoreB = uint64((shifted & uint256((2 ** 64) - 1))); // Compute the score and the total difference uint32 score = scoreEntry(picks, results); uint64 difference = computeFinalGameDifference(scoreA, scoreB, scoreAActual, scoreBActual); // Make a score and place it in the right sort order TopScore memory scoreResult = TopScore(entries[entryCompressed].entryIndex, score, difference, entries[entryCompressed].submitter); if (isScoreBetter(scoreResult, topThree[0])) { topThree[2] = topThree[1]; topThree[1] = topThree[0]; topThree[0] = scoreResult; } else if (isScoreBetter(scoreResult, topThree[1])) { topThree[2] = topThree[1]; topThree[1] = scoreResult; } else if (isScoreBetter(scoreResult, topThree[2])) { topThree[2] = scoreResult; } return score; } function claimTopEntry(uint256 entryCompressed) public { require(currentState == ContestState.WAITING_FOR_WINNING_CLAIMS, "Must be in the waiting for winners state"); require(finalResult.isFinal, "The final result must be marked as final"); scoreAndSortEntry(entryCompressed, finalResult.winners, finalResult.scoreA, finalResult.scoreB); } function computeFinalGameDifference( uint64 scoreAGuess, uint64 scoreBGuess, uint64 scoreAActual, uint64 scoreBActual) private pure returns (uint64) { // Don't worry about overflow here, not much you can really do with it uint64 difference = 0; difference += ((scoreAActual > scoreAGuess) ? (scoreAActual - scoreAGuess) : (scoreAGuess - scoreAActual)); difference += ((scoreBActual > scoreBGuess) ? (scoreBActual - scoreBGuess) : (scoreBGuess - scoreBActual)); return difference; } // Gets the bit at index n in a function getBit16(bytes16 a, uint16 n) private pure returns (bool) { uint128 mask = uint128(2) ** n; return uint128(a) & mask != 0; } // Sets the bit at index n to 1 in a function setBit16(bytes16 a, uint16 n) private pure returns (bytes16) { uint128 mask = uint128(2) ** n; return a | bytes16(mask); } // Sets the bit at index n to 0 in a function clearBit16(bytes16 a, uint16 n) private pure returns (bytes16) { uint128 mask = uint128(2) ** n; mask = mask ^ uint128(-1); return a & bytes16(mask); } // Returns either 0 if there is no possible winner, 1 if team B is chosen, or 2 if team A is chosen function extractResult(bytes16 a, uint8 n) private pure returns (uint128) { uint128 mask = uint128(0x00000000000000000000000000000003) * uint128(2) ** (n * 2); uint128 masked = uint128(a) & mask; // Shift back to get either 0, 1 or 2 return (masked / (uint128(2) ** (n * 2))); } // Gets which round a game belongs to based on its id function getRoundForGame(uint8 gameId) private pure returns (uint8) { if (gameId < 32) { return 0; } else if (gameId < 48) { return 1; } else if (gameId < 56) { return 2; } else if (gameId < 60) { return 3; } else if (gameId < 62) { return 4; } else { return 5; } } // Gets the first game in a round given the round number function getFirstGameIdOfRound(uint8 round) private pure returns (uint8) { if (round == 0) { return 0; } else if (round == 1) { return 32; } else if (round == 2) { return 48; } else if (round == 3) { return 56; } else if (round == 4) { return 60; } else { return 62; } } // Looks at two scores and decided whether newScore is a better score than old score function isScoreBetter(TopScore memory newScore, TopScore memory oldScore) private pure returns (bool) { if (newScore.score > oldScore.score) { return true; } if (newScore.score < oldScore.score) { return false; } // Case where we have a tie if (newScore.difference < oldScore.difference) { return true; } if (newScore.difference < oldScore.difference) { return false; } require(newScore.entryIndex != oldScore.entryIndex, "This entry has already claimed a prize"); // Crazy case where we have the same score and same diference. Return the earlier entry as the winnner return newScore.entryIndex < oldScore.entryIndex; } // Scores an entry given the picks and the results function scoreEntry(bytes16 picks, bytes16 results) private pure returns (uint32) { uint32 score = 0; uint8 round = 0; bytes16 currentPicks = picks; for (uint8 gameId = 0; gameId < 63; gameId++) { // Update which round we're in when on the transitions round = getRoundForGame(gameId); uint128 currentPick = extractResult(currentPicks, gameId); if (currentPick == extractResult(results, gameId)) { score += (uint32(2) ** round); } else if (currentPick != 0) { // If we actually had a pick, propagate forward // Mark all the future currentPicks which required this team winning as null uint16 currentPickId = (gameId * 2) + (currentPick == 2 ? 1 : 0); for (uint8 futureRound = round + 1; futureRound < 6; futureRound++) { uint16 currentPickOffset = currentPickId - (getFirstGameIdOfRound(futureRound - 1) * 2); currentPickId = (getFirstGameIdOfRound(futureRound) * 2) + (currentPickOffset / 2); bool pickedLoser = getBit16(currentPicks, currentPickId); if (pickedLoser) { currentPicks = clearBit16(currentPicks, currentPickId); } else { break; } } } } return score; } }