-
Notifications
You must be signed in to change notification settings - Fork 6
/
EthMadness.sol
449 lines (360 loc) · 18.4 KB
/
EthMadness.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
pragma solidity ^0.5.0;
import "./IERC20.sol";
import "./Ownable.sol";
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;
}
}