/
LotteryRound.sol
456 lines (410 loc) · 13.8 KB
/
LotteryRound.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
450
451
452
453
454
455
456
pragma solidity ^0.4.8;
import "Common.sol";
import "LotteryRoundInterface.sol";
/**
* The meat of the game. Holds all the rules around picking numbers,
* attempting to establish good sources of entropy, holding the pre-selected
* entropy sources (salt) in a way that is not publicly-revealed, etc.
* The gist is that this is a bit of a PRNG, that advances its entropy each
* time a ticket is picked.
*
* Provides the means to both pick specific numbers or have the PRNG select
* them for the ticketholder.
*
* Also controls payout of winners for a particular round.
*/
contract LotteryRound is LotteryRoundInterface, Owned {
/*
Constants
*/
// public version string
string constant VERSION = '0.1.2';
// round length
uint256 constant ROUND_LENGTH = 43200; // approximately a week
// payout fraction (in thousandths):
uint256 constant PAYOUT_FRACTION = 950;
// Cost per ticket
uint constant TICKET_PRICE = 1 finney;
// valid pick mask
bytes1 constant PICK_MASK = 0x3f; // 0-63
/*
Public variables
*/
// Pre-selected salt, hashed N times
// serves as proof-of-salt
bytes32 public saltHash;
// single hash of salt.N.salt
// serves as proof-of-N
// 0 < N < 256
bytes32 public saltNHash;
// closing time.
uint256 public closingBlock;
// winning numbers
bytes4 public winningNumbers;
// This becomes true when the numbers have been picked
bool public winningNumbersPicked = false;
// This becomes populated if anyone wins
address[] public winners;
// Stores a flag to signal if the winner has winnings to be claimed
mapping(address => bool) public winningsClaimable;
/**
* Current picks are from 0 to 63, or 2^6 - 1.
* Current number of picks is 4
* Rough odds of winning will be 1 in (2^6)^4, assuming even distributions, etc
*/
mapping(bytes4 => address[]) public tickets;
uint256 public nTickets = 0;
// Set when winners are drawn, and represents the amount of the contract's current balance that can be paid out.
uint256 public prizePool;
// Set when winners are drawn, and signifies the amount each winner will receive. In the event of multiple
// winners, this will be prizePool / winners.length
uint256 public prizeValue;
// The fee at the time winners were picked (if there were winners). This is the portion of the contract's balance
// that goes to the contract owner.
uint256 public ownerFee;
// This will be the sha3 hash of the previous entropy + some additional inputs (e.g. randomly-generated hashes, etc)
bytes32 private accumulatedEntropy;
modifier beforeClose {
if (block.number > closingBlock) {
throw;
}
_;
}
modifier beforeDraw {
if (block.number <= closingBlock || winningNumbersPicked) {
throw;
}
_;
}
modifier afterDraw {
if (winningNumbersPicked == false) {
throw;
}
_;
}
// Emitted when the round starts, broadcasting the hidden entropy params, closing block
// and game version.
event LotteryRoundStarted(
bytes32 saltHash,
bytes32 saltNHash,
uint256 closingBlock,
string version
);
// Broadcasted any time a user purchases a ticket.
event LotteryRoundDraw(
address indexed ticketHolder,
bytes4 indexed picks
);
// Broadcast when the round is completed, revealing the hidden entropy sources
// and the winning picks.
event LotteryRoundCompleted(
bytes32 salt,
uint8 N,
bytes4 indexed winningPicks,
uint256 closingBalance
);
// Broadcast for each winner.
event LotteryRoundWinner(
address indexed ticketHolder,
bytes4 indexed picks
);
/**
* Creates a new Lottery round, and sets the round's parameters.
*
* Note that this will implicitly set the factory to be the owner,
* meaning the factory will need to be able to transfer ownership,
* to its owner, the C&C contract.
*
* @param _saltHash Hashed salt. Will be hashed with sha3 N times
* @param _saltNHash Hashed proof of N, in the format sha3(salt+N+salt)
*/
function LotteryRound(
bytes32 _saltHash,
bytes32 _saltNHash
) payable {
saltHash = _saltHash;
saltNHash = _saltNHash;
closingBlock = block.number + ROUND_LENGTH;
LotteryRoundStarted(
saltHash,
saltNHash,
closingBlock,
VERSION
);
// start this off with some really poor entropy.
accumulatedEntropy = block.blockhash(block.number - 1);
}
/**
* Attempt to generate a new pseudo-random number, while advancing the internal entropy
* of the contract. Uses a two-phase approach: first, generates a simple offset [0-255]
* from simple entropy sources (accumulated, sender, block number). Uses this offset
* to index into the history of blockhashes, to attempt to generate some stronger entropy
* by including previous block hashes.
*
* Then advances the interal entropy by rehashing it with the chosen number.
*/
function generatePseudoRand(bytes32 seed) internal returns(bytes32) {
uint8 pseudoRandomOffset = uint8(uint256(sha256(
seed,
block.difficulty,
block.coinbase,
block.timestamp,
accumulatedEntropy
)) & 0xff);
// WARNING: This assumes block.number > 256... If block.number < 256, the below block.blockhash could return 0
// This is probably only an issue in testing, but shouldn't be a problem there.
uint256 pseudoRandomBlock = block.number - pseudoRandomOffset - 1;
bytes32 pseudoRand = sha3(
block.number,
block.blockhash(pseudoRandomBlock),
block.difficulty,
block.timestamp,
accumulatedEntropy
);
accumulatedEntropy = sha3(accumulatedEntropy, pseudoRand);
return pseudoRand;
}
/**
* Buy a ticket with pre-selected picks
* @param picks User's picks.
*/
function pickTicket(bytes4 picks) payable beforeClose {
if (msg.value != TICKET_PRICE) {
throw;
}
// don't allow invalid picks.
for (uint8 i = 0; i < 4; i++) {
if (picks[i] & PICK_MASK != picks[i]) {
throw;
}
}
tickets[picks].push(msg.sender);
nTickets++;
generatePseudoRand(bytes32(picks)); // advance the accumulated entropy
LotteryRoundDraw(msg.sender, picks);
}
/**
* Interal function to generate valid picks. Used by both the random
* ticket functionality, as well as when generating winning picks.
* Even though the picks are a fixed-width byte array, each pick is
* chosen separately (e.g. a bytes4 will result in 4 separate sha3 hashes
* used as sources).
*
* Masks the first byte of the seed to use as an offset into the next PRNG,
* then replaces the seed with the new PRNG. Pulls a single byte from the
* resultant offset, masks it to be valid, then adds it to the accumulator.
*
* @param seed The PRNG seed used to pick the numbers.
*/
function pickValues(bytes32 seed) internal returns (bytes4) {
bytes4 picks;
uint8 offset;
for (uint8 i = 0; i < 4; i++) {
offset = uint8(seed[0]) & 0x1f;
seed = sha3(seed, msg.sender);
picks = (picks >> 8) | bytes1(seed[offset] & PICK_MASK);
}
return picks;
}
/**
* Picks a random ticket, using the internal PRNG and accumulated entropy
*/
function randomTicket() payable beforeClose {
if (msg.value != TICKET_PRICE) {
throw;
}
bytes32 pseudoRand = generatePseudoRand(bytes32(msg.sender));
bytes4 picks = pickValues(pseudoRand);
tickets[picks].push(msg.sender);
nTickets++;
LotteryRoundDraw(msg.sender, picks);
}
/**
* Public means to prove the salt after numbers are picked. Not technically necessary
* for this to be external, because it will be called during the round close process.
* If the hidden entropy parameters don't match, the contract will refuse to pick
* numbers or close.
*
* @param salt Hidden entropy source
* @param N Secret value proving how to obtain the hashed entropy from the source.
*/
function proofOfSalt(bytes32 salt, uint8 N) constant returns(bool) {
// Proof-of-N:
bytes32 _saltNHash = sha3(salt, N, salt);
if (_saltNHash != saltNHash) {
return false;
}
// Proof-of-salt:
bytes32 _saltHash = sha3(salt);
for (var i = 1; i < N; i++) {
_saltHash = sha3(_saltHash);
}
if (_saltHash != saltHash) {
return false;
}
return true;
}
/**
* Internal function to handle tabulating the winners, including edge cases around
* duplicate winners. Split out into its own method partially to enable proper
* testing.
*
* @param salt Hidden entropy source. Emitted here
* @param N Key to the hidden entropy source.
* @param winningPicks The winning picks.
*/
function finalizeRound(bytes32 salt, uint8 N, bytes4 winningPicks) internal {
winningNumbers = winningPicks;
winningNumbersPicked = true;
LotteryRoundCompleted(salt, N, winningNumbers, this.balance);
var _winners = tickets[winningNumbers];
// if we have winners:
if (_winners.length > 0) {
// let's dedupe and broadcast the winners before figuring out the prize pool situation.
for (uint i = 0; i < _winners.length; i++) {
var winner = _winners[i];
if (!winningsClaimable[winner]) {
winners.push(winner);
winningsClaimable[winner] = true;
LotteryRoundWinner(winner, winningNumbers);
}
}
// now let's wrap this up by finalizing the prize pool value:
// There may be some rounding errors in here, but it should only amount to a couple wei.
prizePool = this.balance * PAYOUT_FRACTION / 1000;
prizeValue = prizePool / winners.length;
// Note that the owner doesn't get to claim a fee until the game is won.
ownerFee = this.balance - prizePool;
}
// we done.
}
/**
* Reveal the secret sources of entropy, then use them to pick winning numbers.
*
* Note that by using no dynamic (e.g. blockhash-based) sources of entropy,
* censoring this transaction will not change the final outcome of the picks.
*
* @param salt Hidden entropy.
* @param N Number of times to hash the hidden entropy to produce the value provided at creation.
*/
function closeGame(bytes32 salt, uint8 N) onlyOwner beforeDraw {
// Don't allow picking numbers multiple times.
if (winningNumbersPicked == true) {
throw;
}
// prove the pre-selected salt is actually legit.
if (proofOfSalt(salt, N) != true) {
throw;
}
bytes32 pseudoRand = sha3(
salt,
nTickets,
accumulatedEntropy
);
finalizeRound(salt, N, pickValues(pseudoRand));
}
/**
* Sends the owner's fee to the specified address. Note that the
* owner can only be paid if there actually was a winner. In the
* event no one wins, the entire balance is carried over into the
* next round. No double-dipping here.
* @param payout Address to send the owner fee to.
*/
function claimOwnerFee(address payout) onlyOwner afterDraw {
if (ownerFee > 0) {
uint256 value = ownerFee;
ownerFee = 0;
if (!payout.send(value)) {
throw;
}
}
}
/**
* Used to withdraw the balance when the round is completed. This
* only works if there are either no winners, or all winners + the
* owner have been paid.
*/
function withdraw() onlyOwner afterDraw {
if (paidOut() && ownerFee == 0) {
if (!owner.send(this.balance)) {
throw;
}
}
}
/**
* Same as above. This is mostly here because it's overriding the method
* inherited from `Owned`
*/
function shutdown() onlyOwner afterDraw {
if (paidOut() && ownerFee == 0) {
selfdestruct(owner);
}
}
/**
* Attempt to pay the winners, if any. If any `send`s fail, the winner
* will have to collect their winnings on their own.
*/
function distributeWinnings() onlyOwner afterDraw {
if (winners.length > 0) {
for (uint i = 0; i < winners.length; i++) {
address winner = winners[i];
bool unclaimed = winningsClaimable[winner];
if (unclaimed) {
winningsClaimable[winner] = false;
if (!winner.send(prizeValue)) {
// If I can't send you money, dumbshit, you get to claim it on your own.
// maybe next time don't use a contract or try to exploit the game.
// Regardless, you're on your own. Happy birthday to the ground.
winningsClaimable[winner] = true;
}
}
}
}
}
/**
* Returns true if it's after the draw, and either there are no winners, or all the winners have been paid.
* @return {bool}
*/
function paidOut() constant returns(bool) {
// no need to use the modifier on this function, just do the same check
// and return false instead.
if (winningNumbersPicked == false) {
return false;
}
if (winners.length > 0) {
bool claimed = true;
// if anyone hasn't been sent or claimed their earnings,
// we still have money to pay out.
for (uint i = 0; claimed && i < winners.length; i++) {
claimed = claimed && !winningsClaimable[winners[i]];
}
return claimed;
} else {
// no winners, nothing to pay.
return true;
}
}
/**
* Winners can claim their own prizes using this. If they do
* something stupid like use a contract, this gives them a
* a second chance at withdrawing their funds. Note that
* this shares an interlock with `distributeWinnings`.
*/
function claimPrize() afterDraw {
if (winningsClaimable[msg.sender] == false) {
// get. out!
throw;
}
winningsClaimable[msg.sender] = false;
if (!msg.sender.send(prizeValue)) {
// you really are a dumbshit, aren't you.
throw;
}
}
// Man! What do I look like? A charity case?
// Please.
// You can't buy me, hot dog man!
function () {
throw;
}
}