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;
}
}
{
"compilationTarget": {
"EthMadness.sol": "EthMadness"
},
"evmVersion": "byzantium",
"libraries": {},
"optimizer": {
"enabled": false,
"runs": 0
},
"remappings": []
}
[{"constant":true,"inputs":[],"name":"currentState","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"markTournamentInProgress","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"topThree","outputs":[{"name":"entryIndex","type":"uint48"},{"name":"score","type":"uint32"},{"name":"difference","type":"uint64"},{"name":"submitter","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getOracleCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"closeContestAndPayWinners","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"oracleVotes","outputs":[{"name":"winners","type":"bytes16"},{"name":"scoreA","type":"uint8"},{"name":"scoreB","type":"uint8"},{"name":"isFinal","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"oracles","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"picks","type":"bytes16"},{"name":"scoreA","type":"uint64"},{"name":"scoreB","type":"uint64"},{"name":"bracketName","type":"string"}],"name":"submitEntry","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"renounceOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"prizeAmount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getEntryCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"oracleIndex","type":"uint256"},{"name":"winners","type":"bytes16"},{"name":"scoreA","type":"uint8"},{"name":"scoreB","type":"uint8"}],"name":"submitOracleVote","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"picksOrResults","type":"bytes16"}],"name":"arePicksOrResultsValid","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isOwner","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"refundRemaining","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"entryCompressed","type":"uint256"}],"name":"claimTopEntry","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"entries","outputs":[{"name":"submitter","type":"address"},{"name":"entryIndex","type":"uint48"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getTransitionTimes","outputs":[{"name":"","type":"uint256"},{"name":"","type":"uint256"},{"name":"","type":"uint256"},{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"winners","type":"bytes16"},{"name":"scoreA","type":"uint8"},{"name":"scoreB","type":"uint8"}],"name":"closeOracleVoting","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"prizeERC20TokenAddress","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"oracle","type":"address"}],"name":"addOracle","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"transitionTimes","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"markTournamentFinished","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"finalResult","outputs":[{"name":"winners","type":"bytes16"},{"name":"scoreA","type":"uint8"},{"name":"scoreB","type":"uint8"},{"name":"isFinal","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"times","type":"uint256[]"},{"name":"erc20Token","type":"address"},{"name":"erc20Amount","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"submitter","type":"address"},{"indexed":true,"name":"entryCompressed","type":"uint256"},{"indexed":true,"name":"entryIndex","type":"uint48"},{"indexed":false,"name":"bracketName","type":"string"}],"name":"EntrySubmitted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"previousOwner","type":"address"},{"indexed":true,"name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"}]