This repository was archived by the owner on Feb 27, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathDice2Win.sol
505 lines (433 loc) · 19.7 KB
/
Dice2Win.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
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
// * dice2.win - fair games that pay Ether. Version 5.
//
// * Ethereum smart contract, deployed at 0xD1CEeeeee83F8bCF3BEDad437202b6154E9F5405.
//
// * Uses hybrid commit-reveal + block hash random number generation that is immune
// to tampering by players, house and miners. Apart from being fully transparent,
// this also allows arbitrarily high bets.
//
// * Refer to https://dice2.win/whitepaper.pdf for detailed description and proofs.
contract Dice2Win {
// Constants section
// This are some constants making O(1) population count in placeBet possible.
// See whitepaper for intuition and proofs behind it.
uint256 constant POPCNT_MULT =
0x0000000000002000000000100000000008000000000400000000020000000001;
uint256 constant POPCNT_MASK =
0x0001041041041041041041041041041041041041041041041041041041041041;
uint256 constant POPCNT_MODULO = 0x3F;
// Each bet is deducted 1% in favour of the house, but no less than some minimum.
// The lower bound is dictated by gas costs of the settleBet transaction, providing
// headroom for up to 10 Gwei prices.
// 每次赌博抽取 1%用于支持游戏, 但是不小于最小金额
uint256 constant HOUSE_EDGE_PERCENT = 1;
// uint constant HOUSE_EDGE_MINIMUM_AMOUNT = 0.0003 ether;
uint256 constant HOUSE_EDGE_MINIMUM_AMOUNT = 0.03 * 10**8; // 0.03 HTDF
// 不开通大奖功能
// Bets lower than this amount do not participate in jackpot rolls (and are
// not deducted JACKPOT_FEE).
// 参与大奖池的最小金额, 如果下注金额超过此值, 则默认将0.5HTDF加入大奖奖池,参加大奖抽奖
// uint constant MIN_JACKPOT_BET = 0.1 ether;
// uint constant MIN_JACKPOT_BET = 1.1*10**8; // 兼容HTDF
// Chance to win jackpot (currently 0.1%) and fee deducted into jackpot fund.
// 赢得大奖池的机率, 默认是0.1%, 我们加大10倍
// 大奖的奖金是从每笔下注的金额中抽取很小一部分加入大奖奖池
// uint constant JACKPOT_MODULO = 1000;
// uint constant JACKPOT_FEE = 0.001 ether;
// uint constant JACKPOT_FEE = 0.1*10**8 ; // 加大 500倍
// There is minimum and maximum bets.
// 最小最大金额
uint256 constant MIN_BET = 1 * 10**8; // 最小下注金额: 1 HTDF
uint256 constant MAX_AMOUNT = 1000000 * 10**8 + 1; // 最大下注金额: 1000000 HTDF
// Modulo is a number of equiprobable outcomes in a game:
// - 2 for coin flip
// - 6 for dice
// - 6*6 = 36 for double dice
// - 100 for etheroll
// - 37 for roulette
// etc.
// It's called so because 256-bit entropy is treated like a huge integer and
// the remainder of its division by modulo is considered bet outcome.
uint256 constant MAX_MODULO = 36; // 其他游戏不开通
// For modulos below this threshold rolls are checked against a bit mask,
// thus allowing betting on any combination of outcomes. For example, given
// modulo 6 for dice, 101000 mask (base-2, big endian) means betting on
// 4 and 6; for games with modulos higher than threshold (Etheroll), a simple
// limit is used, allowing betting on any outcome in [0, N) range.
//
// The specific value is dictated by the fact that 256-bit intermediate
// multiplication result allows implementing population count efficiently
// for numbers that are up to 42 bits, and 40 is the highest multiple of
// eight below 42.
uint256 constant MAX_MASK_MODULO = 40;
// This is a check on bet mask overflow.
uint256 constant MAX_BET_MASK = 2**MAX_MASK_MODULO;
// EVM BLOCKHASH opcode can query no further than 256 blocks into the
// past. Given that settleBet uses block hash of placeBet as one of
// complementary entropy sources, we cannot process bets older than this
// threshold. On rare occasions dice2.win croupier may fail to invoke
// settleBet in this timespan due to technical issues or extreme Ethereum
// congestion; such bets can be refunded via invoking refundBet.
uint256 constant BET_EXPIRATION_BLOCKS = 250;
// Some deliberately invalid address to initialize the secret signer with.
// Forces maintainers to invoke setSecretSigner before processing any bets.
address constant DUMMY_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
// Standard contract ownership transfer.
address public owner;
address private nextOwner;
// Adjustable max bet profit. Used to cap bets against dynamic odds.
uint256 public maxProfit;
// The address corresponding to a private key used to sign placeBet commits.
address public secretSigner;
// 不开通大奖功能
// Accumulated jackpot fund.
// 超级大奖的奖池金额
// uint128 public jackpotSize;
// Funds that are locked in potentially winning bets. Prevents contract from
// committing to bets it cannot pay out.
uint128 public lockedInBets;
// A structure representing a single bet.
struct Bet {
// Wager amount in wei.
uint256 amount;
// Modulo of a game.
uint8 modulo;
// Number of winning outcomes, used to compute winning payment (* modulo/rollUnder),
// and used instead of mask for games with modulo > MAX_MASK_MODULO.
uint8 rollUnder;
// Block number of placeBet tx.
uint40 placeBlockNumber;
// Bit mask representing winning bet outcomes (see MAX_MASK_MODULO comment).
uint40 mask;
// Address of a gambler, used to pay out winning bets.
address gambler;
}
// Mapping from commits to all currently active & processed bets.
mapping(uint256 => Bet) bets;
// Croupier account.
address public croupier;
// Events that are issued to make statistic recovery easier.
event FailedPayment(address indexed beneficiary, uint256 amount);
event Payment(address indexed beneficiary, uint256 amount);
event JackpotPayment(address indexed beneficiary, uint256 amount);
// This event is emitted in placeBet to record commit in the logs.
event Commit(uint256 commit);
// Constructor. Deliberately does not take any parameters.
constructor() payable {
// owner 当合约消耗时, owner接收合约中所有的HTDF
owner = msg.sender;
secretSigner = payable(0xda693C5307CD5aBCb1CC395d6a7Eab3d5612989F); // mainnet
croupier = payable(0xffBe74a0193ed1C51E7952713B6E47827c91F4b1);
maxProfit = 1000 * 36 * (10**8); // 默认最大获利金额: 1000*36 HTDF
}
// Standard modifier on methods invokable only by contract owner.
modifier onlyOwner {
require(msg.sender == owner, "OnlyOwner methods called by non-owner.");
_;
}
// Standard modifier on methods invokable only by contract owner.
modifier onlyCroupier {
require(
msg.sender == croupier,
"OnlyCroupier methods called by non-croupier."
);
_;
}
// 转移所有权
// Standard contract ownership transfer implementation,
function approveNextOwner(address _nextOwner) external onlyOwner {
require(_nextOwner != owner, "Cannot approve current owner.");
nextOwner = _nextOwner;
}
// 正式转移所有到nextOwner
function acceptNextOwner() external {
require(
msg.sender == nextOwner,
"Can only accept preapproved new owner."
);
owner = nextOwner;
}
// Fallback function deliberately left empty. It's primary use case
// is to top up the bank roll.
// function () public payable {
receive() external payable {}
// See comment for "secretSigner" variable.
function setSecretSigner(address newSecretSigner) external onlyOwner {
secretSigner = newSecretSigner;
}
// Change the croupier address.
function setCroupier(address newCroupier) external onlyOwner {
croupier = newCroupier;
}
// Change max bet reward. Setting this to zero effectively disables betting.
function setMaxProfit(uint256 _maxProfit) public onlyOwner {
require(_maxProfit < MAX_AMOUNT, "maxProfit should be a sane number.");
maxProfit = _maxProfit;
}
// This function is used to bump up the jackpot fund. Cannot be used to lower it.
// function increaseJackpot(uint increaseAmount) external onlyOwner {
// require (increaseAmount <= address(this).balance, "Increase amount larger than balance.");
// require (jackpotSize + lockedInBets + increaseAmount <= address(this).balance, "Not enough funds.");
// jackpotSize += uint128(increaseAmount);
// }
// Funds withdrawal to cover costs of dice2.win operation.
function withdrawFunds(address beneficiary, uint256 withdrawAmount)
external
onlyOwner
{
require(
withdrawAmount <= address(this).balance,
"Increase amount larger than balance."
);
require(
lockedInBets + withdrawAmount <= address(this).balance,
"Not enough funds."
);
sendFunds(beneficiary, withdrawAmount, withdrawAmount);
}
// Contract may be destroyed only when there are no ongoing bets,
// either settled or refunded. All funds are transferred to contract owner.
function kill() external onlyOwner {
// 防止跑路, 销毁合约之前, 必须处理完所有未完成的赌注
require(
lockedInBets == 0,
"All bets should be processed (settled or refunded) before self-destruct."
);
// 将合约中的HTDF转给owner
selfdestruct(payable(owner));
}
// 下注
/// *** Betting logic
// Bet states:
// amount == 0 && gambler == 0 - 'clean' (can place a bet)
// amount != 0 && gambler != 0 - 'active' (can be settled or refunded)
// amount == 0 && gambler != 0 - 'processed' (can clean storage)
//
// NOTE: Storage cleaning is not implemented in this contract version; it will be added
// with the next upgrade to prevent polluting Ethereum state with expired bets.
// Bet placing transaction - issued by the player.
// betMask - bet outcomes bit mask for modulo <= MAX_MASK_MODULO,
// [0, betMask) for larger modulos.
// modulo - game modulo.
// commitLastBlock - number of the maximum block where "commit" is still considered valid.
// commit - Keccak256 hash of some secret "reveal" random number, to be supplied
// by the dice2.win croupier bot in the settleBet transaction. Supplying
// "commit" ensures that "reveal" cannot be changed behind the scenes
// after placeBet have been mined.
// r, s - components of ECDSA signature of (commitLastBlock, commit). v is
// guaranteed to always equal 27.
//
// Commit, being essentially random 256-bit number, is used as a unique bet identifier in
// the 'bets' mapping.
//
// Commits are signed with a block limit to ensure that they are used at most once - otherwise
// it would be possible for a miner to place a bet with a known commit/reveal pair and tamper
// with the blockhash. Croupier guarantees that commitLastBlock will always be not greater than
// placeBet block number plus BET_EXPIRATION_BLOCKS. See whitepaper for details.
function placeBet(
uint256 betMask,
uint256 modulo,
uint256 commitLastBlock,
uint256 commit,
bytes32 r,
bytes32 s
) external payable {
// Check that the bet is in 'clean' state.
Bet storage bet = bets[commit];
require(bet.gambler == address(0), "Bet should be in a 'clean' state.");
// Validate input data ranges.
uint256 amount = msg.value;
require(
modulo > 1 && modulo <= MAX_MODULO,
"Modulo should be within range."
);
require(
amount >= MIN_BET && amount <= MAX_AMOUNT,
"Amount should be within range."
);
require(
betMask > 0 && betMask < MAX_BET_MASK,
"Mask should be within range."
);
// Check that commit is valid - it has not expired and its signature is valid.
require(block.number <= commitLastBlock, "Commit has expired.");
bytes32 signatureHash = keccak256(
abi.encodePacked(uint40(commitLastBlock), commit)
);
require(
secretSigner == ecrecover(signatureHash, 27, r, s),
"ECDSA signature is not valid."
);
uint256 rollUnder;
uint256 mask;
if (modulo <= MAX_MASK_MODULO) {
// Small modulo games specify bet outcomes via bit mask.
// rollUnder is a number of 1 bits in this mask (population count).
// This magic looking formula is an efficient way to compute population
// count on EVM for numbers below 2**40. For detailed proof consult
// the dice2.win whitepaper.
rollUnder = ((betMask * POPCNT_MULT) & POPCNT_MASK) % POPCNT_MODULO;
mask = betMask;
} else {
// Larger modulos specify the right edge of half-open interval of
// winning bet outcomes.
require(
betMask > 0 && betMask <= modulo,
"High modulo range, betMask larger than modulo."
);
rollUnder = betMask;
}
// Winning amount and jackpot increase.
uint256 possibleWinAmount = getDiceWinAmount(amount, modulo, rollUnder);
// Enforce max profit limit.
require(
possibleWinAmount <= amount + maxProfit,
"maxProfit limit violation."
);
// Lock funds.
lockedInBets += uint128(possibleWinAmount);
// Check whether contract has enough funds to process this bet.
require(
lockedInBets <= address(this).balance,
"Cannot afford to lose this bet."
);
// Record commit in logs.
emit Commit(commit);
// Store bet parameters on blockchain.
bet.amount = amount;
bet.modulo = uint8(modulo);
bet.rollUnder = uint8(rollUnder);
bet.placeBlockNumber = uint40(block.number);
bet.mask = uint40(mask);
bet.gambler = msg.sender;
}
// 开奖
// This is the method used to settle 99% of bets. To process a bet with a specific
// "commit", settleBet should supply a "reveal" number that would Keccak256-hash to
// "commit". "blockHash" is the block hash of placeBet block as seen by croupier; it
// is additionally asserted to prevent changing the bet outcomes on Ethereum reorgs.
function settleBet(uint256 reveal, bytes32 blockHash)
external
onlyCroupier
{
uint256 commit = uint256(keccak256(abi.encodePacked(reveal)));
Bet storage bet = bets[commit];
uint256 placeBlockNumber = bet.placeBlockNumber;
// Check that bet has not expired yet (see comment to BET_EXPIRATION_BLOCKS).
require(
block.number > placeBlockNumber,
"settleBet in the same block as placeBet, or before."
);
require(
block.number <= placeBlockNumber + BET_EXPIRATION_BLOCKS,
"Blockhash can't be queried by EVM."
);
require(
blockhash(placeBlockNumber) == blockHash,
"blockhash doesn't matched"
);
// Settle bet using reveal and blockHash as entropy sources.
settleBetCommon(bet, reveal, blockHash);
}
// Common settlement code for settleBet & settleBetUncleMerkleProof.
function settleBetCommon(
Bet storage bet,
uint256 reveal,
bytes32 entropyBlockHash
) private {
// Fetch bet parameters into local variables (to save gas).
uint256 amount = bet.amount;
uint256 modulo = bet.modulo;
uint256 rollUnder = bet.rollUnder;
address gambler = bet.gambler;
// Check that bet is in 'active' state.
require(amount != 0, "Bet should be in an 'active' state");
// Move bet into 'processed' state already.
bet.amount = 0;
// The RNG - combine "reveal" and blockhash of placeBet using Keccak256. Miners
// are not aware of "reveal" and cannot deduce it from "commit" (as Keccak256
// preimage is intractable), and house is unable to alter the "reveal" after
// placeBet have been mined (as Keccak256 collision finding is also intractable).
bytes32 entropy = keccak256(abi.encodePacked(reveal, entropyBlockHash));
// Do a roll by taking a modulo of entropy. Compute winning amount.
uint256 dice = uint256(entropy) % modulo;
uint256 diceWinAmount = getDiceWinAmount(amount, modulo, rollUnder);
uint256 diceWin = 0;
// Determine dice outcome.
if (modulo <= MAX_MASK_MODULO) {
// For small modulo games, check the outcome against a bit mask.
if ((2**dice) & bet.mask != 0) {
diceWin = diceWinAmount;
}
} else {
// For larger modulos, check inclusion into half-open interval.
if (dice < rollUnder) {
diceWin = diceWinAmount;
}
}
// Unlock the bet amount, regardless of the outcome.
lockedInBets -= uint128(diceWinAmount);
// Send the funds to gambler.
sendFunds(gambler, diceWin == 0 ? 1 wei : diceWin, diceWin);
}
// Refund transaction - return the bet amount of a roll that was not processed in a
// due timeframe. Processing such blocks is not possible due to EVM limitations (see
// BET_EXPIRATION_BLOCKS comment above for details). In case you ever find yourself
// in a situation like this, just contact the dice2.win support, however nothing
// precludes you from invoking this method yourself.
function refundBet(uint256 commit) external {
// Check that bet is in 'active' state.
Bet storage bet = bets[commit];
uint256 amount = bet.amount;
require(amount != 0, "Bet should be in an 'active' state");
// Check that bet has already expired.
require(
block.number > bet.placeBlockNumber + BET_EXPIRATION_BLOCKS,
"Blockhash can't be queried by EVM."
);
// Move bet into 'processed' state, release funds.
bet.amount = 0;
uint256 diceWinAmount = getDiceWinAmount(
amount,
bet.modulo,
bet.rollUnder
);
lockedInBets -= uint128(diceWinAmount);
// jackpotSize -= uint128(jackpotFee);
// Send the refund.
sendFunds(bet.gambler, amount, amount);
}
// Get the expected win amount after house edge is subtracted.
function getDiceWinAmount(
uint256 amount,
uint256 modulo,
uint256 rollUnder
) private pure returns (uint256 winAmount) {
require(
0 < rollUnder && rollUnder <= modulo,
"Win probability out of range."
);
// 暂时不开通大奖功能
// jackpotFee = amount >= MIN_JACKPOT_BET ? JACKPOT_FEE : 0;
uint256 houseEdge = (amount * HOUSE_EDGE_PERCENT) / 100;
if (houseEdge < HOUSE_EDGE_MINIMUM_AMOUNT) {
houseEdge = HOUSE_EDGE_MINIMUM_AMOUNT;
}
require(houseEdge <= amount, "Bet doesn't even cover house edge.");
winAmount = ((amount - houseEdge) * modulo) / rollUnder;
}
// Helper routine to process the payment.
function sendFunds(
address beneficiary,
uint256 amount,
uint256 successLogAmount
) private {
if (payable(beneficiary).send(amount)) {
emit Payment(beneficiary, successLogAmount);
} else {
emit FailedPayment(beneficiary, amount);
}
}
}