forked from edgeless-casino/smart-contracts
-
Notifications
You must be signed in to change notification settings - Fork 0
/
EdgelessCasino.sol
592 lines (527 loc) · 21.6 KB
/
EdgelessCasino.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
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
/**
* The edgeless casino contract v2 holds the players's funds and provides state channel functionality.
* The casino has at no time control over the players's funds.
* State channels can be updated and closed from both parties: the player and the casino.
* author: Julia Altenried
**/
pragma solidity ^0.4.21;
contract SafeMath {
function safeSub(uint a, uint b) pure internal returns(uint) {
assert(b <= a);
return a - b;
}
function safeSub(int a, int b) pure internal returns(int) {
if(b < 0) assert(a - b > a);
else assert(a - b <= a);
return a - b;
}
function safeAdd(uint a, uint b) pure internal returns(uint) {
uint c = a + b;
assert(c >= a && c >= b);
return c;
}
function safeMul(uint a, uint b) pure internal returns (uint) {
uint c = a * b;
assert(a == 0 || c / a == b);
return c;
}
}
contract Token {
function transferFrom(address sender, address receiver, uint amount) public returns(bool success);
function transfer(address receiver, uint amount) public returns(bool success);
function balanceOf(address holder) public view returns(uint);
}
contract Owned {
address public owner;
modifier onlyOwner {
require(msg.sender == owner);
_;
}
function Owned() public{
owner = msg.sender;
}
}
/** owner should be able to close the contract is nobody has been using it for at least 30 days */
contract Mortal is Owned {
/** contract can be closed by the owner anytime after this timestamp if non-zero */
uint public closeAt;
/** the edgeless token contract */
Token edg;
function Mortal(address tokenContract) internal{
edg = Token(tokenContract);
}
/**
* lets the owner close the contract if there are no player funds on it or if nobody has been using it for at least 30 days
*/
function closeContract(uint playerBalance) internal{
if(closeAt == 0) closeAt = now + 30 days;
if(closeAt < now || playerBalance == 0){
edg.transfer(owner, edg.balanceOf(address(this)));
selfdestruct(owner);
}
}
/**
* in case close has been called accidentally.
**/
function open() onlyOwner public{
closeAt = 0;
}
/**
* make sure the contract is not in process of being closed.
**/
modifier isAlive {
require(closeAt == 0);
_;
}
/**
* delays the time of closing.
**/
modifier keepAlive {
if(closeAt > 0) closeAt = now + 30 days;
_;
}
}
contract RequiringAuthorization is Mortal {
/** indicates if an address is authorized to act in the casino's name */
mapping(address => bool) public authorized;
/** tells if an address is allowed to receive funds from the bankroll **/
mapping(address => bool) public allowedReceiver;
modifier onlyAuthorized {
require(authorized[msg.sender]);
_;
}
/**
* Constructor. Authorize the owner.
* */
function RequiringAuthorization() internal {
authorized[msg.sender] = true;
allowedReceiver[msg.sender] = true;
}
/**
* authorize a address to call game functions and set configs.
* @param addr the address to be authorized
**/
function authorize(address addr) public onlyOwner {
authorized[addr] = true;
}
/**
* deauthorize a address to call game functions and set configs.
* @param addr the address to be deauthorized
**/
function deauthorize(address addr) public onlyOwner {
authorized[addr] = false;
}
/**
* allow authorized wallets to withdraw funds from the bonkroll to this address
* @param receiver the receiver's address
* */
function allowReceiver(address receiver) public onlyOwner {
allowedReceiver[receiver] = true;
}
/**
* disallow authorized wallets to withdraw funds from the bonkroll to this address
* @param receiver the receiver's address
* */
function disallowReceiver(address receiver) public onlyOwner {
allowedReceiver[receiver] = false;
}
/**
* changes the owner of the contract. revokes authorization of the old owner and authorizes the new one.
* @param newOwner the address of the new owner
* */
function changeOwner(address newOwner) public onlyOwner {
deauthorize(owner);
authorize(newOwner);
disallowReceiver(owner);
allowReceiver(newOwner);
owner = newOwner;
}
}
contract ChargingGas is RequiringAuthorization, SafeMath {
/** 1 EDG has 5 decimals **/
uint public constant oneEDG = 100000;
/** the price per kgas and GWei in tokens (with decimals) */
uint public gasPrice;
/** the amount of gas used per transaction in kGas */
mapping(bytes4 => uint) public gasPerTx;
/** the number of tokens (5 decimals) payed by the users to cover the gas cost */
uint public gasPayback;
function ChargingGas(uint kGasPrice) internal{
//deposit, withdrawFor, updateChannel, updateBatch, transferToNewContract
bytes4[5] memory signatures = [bytes4(0x3edd1128),0x9607610a, 0xde48ff52, 0xc97b6d1f, 0x6bf06fde];
//amount of gas consumed by the above methods in GWei
uint[5] memory gasUsage = [uint(146), 100, 65, 50, 85];
setGasUsage(signatures, gasUsage);
setGasPrice(kGasPrice);
}
/**
* sets the amount of gas consumed by methods with the given sigantures.
* only called from the edgeless casino constructor.
* @param signatures an array of method-signatures
* gasNeeded the amount of gas consumed by these methods
* */
function setGasUsage(bytes4[5] signatures, uint[5] gasNeeded) public onlyOwner {
require(signatures.length == gasNeeded.length);
for (uint8 i = 0; i < signatures.length; i++)
gasPerTx[signatures[i]] = gasNeeded[i];
}
/**
* updates the price per 1000 gas in EDG.
* @param price the new gas price (with decimals, max 0.1 EDG)
**/
function setGasPrice(uint price) public onlyAuthorized {
require(price < oneEDG/10);
gasPrice = price;
}
/**
* returns the gas cost of the called function.
* */
function getGasCost() internal view returns(uint) {
return safeMul(safeMul(gasPerTx[msg.sig], gasPrice), tx.gasprice) / 1000000000;
}
}
contract CasinoBank is ChargingGas {
/** the total balance of all players with virtual decimals **/
uint public playerBalance;
/** the balance per player in edgeless tokens with virtual decimals */
mapping(address => uint) public balanceOf;
/** in case the user wants/needs to call the withdraw function from his own wallet, he first needs to request a withdrawal */
mapping(address => uint) public withdrawAfter;
/** a number to count withdrawal signatures to ensure each signature is different even if withdrawing the same amount to the same address */
mapping(address => uint) public withdrawCount;
/** the maximum amount of tokens the user is allowed to deposit (with decimals) */
uint public maxDeposit;
/** the maximum withdrawal of tokens the user is allowed to withdraw on one day (only enforced when the tx is not sent from an authorized wallet) **/
uint public maxWithdrawal;
/** waiting time for withdrawal if not requested via the server **/
uint public waitingTime;
/** the address of the predecessor **/
address public predecessor;
/** informs listeners how many tokens were deposited for a player */
event Deposit(address _player, uint _numTokens, uint _gasCost);
/** informs listeners how many tokens were withdrawn from the player to the receiver address */
event Withdrawal(address _player, address _receiver, uint _numTokens, uint _gasCost);
/**
* Constructor.
* @param depositLimit the maximum deposit allowed
* predecessorAddr the address of the predecessing contract
* */
function CasinoBank(uint depositLimit, address predecessorAddr) internal {
maxDeposit = depositLimit * oneEDG;
maxWithdrawal = maxDeposit;
waitingTime = 24 hours;
predecessor = predecessorAddr;
}
/**
* accepts deposits for an arbitrary address.
* retrieves tokens from the message sender and adds them to the balance of the specified address.
* edgeless tokens do not have any decimals, but are represented on this contract with decimals.
* @param receiver address of the receiver
* numTokens number of tokens to deposit (0 decimals)
* chargeGas indicates if the gas cost is subtracted from the user's edgeless token balance
**/
function deposit(address receiver, uint numTokens, bool chargeGas) public isAlive {
require(numTokens > 0);
uint value = safeMul(numTokens, oneEDG);
uint gasCost;
if (chargeGas) {
gasCost = getGasCost();
value = safeSub(value, gasCost);
gasPayback = safeAdd(gasPayback, gasCost);
}
uint newBalance = safeAdd(balanceOf[receiver], value);
require(newBalance <= maxDeposit);
assert(edg.transferFrom(msg.sender, address(this), numTokens));
balanceOf[receiver] = newBalance;
playerBalance = safeAdd(playerBalance, value);
emit Deposit(receiver, numTokens, gasCost);
}
/**
* If the user wants/needs to withdraw his funds himself, he needs to request the withdrawal first.
* This method sets the earliest possible withdrawal date to 'waitingTime from now (default 90m, but up to 24h).
* Reason: The user should not be able to withdraw his funds, while the the last game methods have not yet been mined.
**/
function requestWithdrawal() public {
withdrawAfter[msg.sender] = now + waitingTime;
}
/**
* In case the user requested a withdrawal and changes his mind.
* Necessary to be able to continue playing.
**/
function cancelWithdrawalRequest() public {
withdrawAfter[msg.sender] = 0;
}
/**
* withdraws an amount from the user balance if the waiting time passed since the request.
* @param amount the amount of tokens to withdraw
**/
function withdraw(uint amount) public keepAlive {
require(amount <= maxWithdrawal);
require(withdrawAfter[msg.sender] > 0 && now > withdrawAfter[msg.sender]);
withdrawAfter[msg.sender] = 0;
uint value = safeMul(amount, oneEDG);
balanceOf[msg.sender] = safeSub(balanceOf[msg.sender], value);
playerBalance = safeSub(playerBalance, value);
assert(edg.transfer(msg.sender, amount));
emit Withdrawal(msg.sender, msg.sender, amount, 0);
}
/**
* lets the owner withdraw from the bankroll
* @param receiver the receiver's address
* numTokens the number of tokens to withdraw (0 decimals)
**/
function withdrawBankroll(address receiver, uint numTokens) public onlyAuthorized {
require(numTokens <= bankroll());
require(allowedReceiver[receiver]);
assert(edg.transfer(receiver, numTokens));
}
/**
* withdraw the gas payback to the owner
**/
function withdrawGasPayback() public onlyAuthorized {
uint payback = gasPayback / oneEDG;
assert(payback > 0);
gasPayback = safeSub(gasPayback, payback * oneEDG);
assert(edg.transfer(owner, payback));
}
/**
* returns the current bankroll in tokens with 0 decimals
**/
function bankroll() view public returns(uint) {
return safeSub(edg.balanceOf(address(this)), safeAdd(playerBalance, gasPayback) / oneEDG);
}
/**
* updates the maximum deposit.
* @param newMax the new maximum deposit (0 decimals)
**/
function setMaxDeposit(uint newMax) public onlyAuthorized {
maxDeposit = newMax * oneEDG;
}
/**
* updates the maximum withdrawal.
* @param newMax the new maximum withdrawal (0 decimals)
**/
function setMaxWithdrawal(uint newMax) public onlyAuthorized {
maxWithdrawal = newMax * oneEDG;
}
/**
* sets the time the player has to wait for his funds to be unlocked before withdrawal (if not withdrawing with help of the casino server).
* the time may not be longer than 24 hours.
* @param newWaitingTime the new waiting time in seconds
* */
function setWaitingTime(uint newWaitingTime) public onlyAuthorized {
require(newWaitingTime <= 24 hours);
waitingTime = newWaitingTime;
}
/**
* transfers an amount from the contract balance to the owner's wallet.
* @param receiver the receiver address
* amount the amount of tokens to withdraw (0 decimals)
* v,r,s the signature of the player
**/
function withdrawFor(address receiver, uint amount, uint8 v, bytes32 r, bytes32 s) public onlyAuthorized keepAlive {
address player = ecrecover(keccak256(receiver, amount, withdrawCount[receiver]), v, r, s);
withdrawCount[receiver]++;
uint gasCost = getGasCost();
uint value = safeAdd(safeMul(amount, oneEDG), gasCost);
gasPayback = safeAdd(gasPayback, gasCost);
balanceOf[player] = safeSub(balanceOf[player], value);
playerBalance = safeSub(playerBalance, value);
assert(edg.transfer(receiver, amount));
emit Withdrawal(player, receiver, amount, gasCost);
}
/**
* transfers the player's tokens directly to the new casino contract after an update.
* @param newCasino the address of the new casino contract
* v, r, s the signature of the player
* chargeGas indicates if the gas cost is payed by the player.
* */
function transferToNewContract(address newCasino, uint8 v, bytes32 r, bytes32 s, bool chargeGas) public onlyAuthorized keepAlive {
address player = ecrecover(keccak256(address(this), newCasino), v, r, s);
uint gasCost = 0;
if(chargeGas) gasCost = getGasCost();
uint value = safeSub(balanceOf[player], gasCost);
require(value > oneEDG);
//fractions of one EDG cannot be withdrawn
value /= oneEDG;
playerBalance = safeSub(playerBalance, balanceOf[player]);
balanceOf[player] = 0;
assert(edg.transfer(newCasino, value));
emit Withdrawal(player, newCasino, value, gasCost);
CasinoBank cb = CasinoBank(newCasino);
assert(cb.credit(player, value));
}
/**
* receive a player balance from the predecessor contract.
* @param player the address of the player to credit the value for
* value the number of tokens to credit (0 decimals)
* */
function credit(address player, uint value) public returns(bool) {
require(msg.sender == predecessor);
uint valueWithDecimals = safeMul(value, oneEDG);
balanceOf[player] = safeAdd(balanceOf[player], valueWithDecimals);
playerBalance = safeAdd(playerBalance, valueWithDecimals);
emit Deposit(player, value, 0);
return true;
}
/**
* lets the owner close the contract if there are no player funds on it or if nobody has been using it for at least 30 days
* */
function close() public onlyOwner {
closeContract(playerBalance);
}
}
contract EdgelessCasino is CasinoBank{
/** the most recent known state of a state channel */
mapping(address => State) public lastState;
/** fired when the state is updated */
event StateUpdate(address player, uint128 count, int128 winBalance, int difference, uint gasCost);
/** fired if one of the parties chooses to log the seeds and results */
event GameData(address player, bytes32[] serverSeeds, bytes32[] clientSeeds, int[] results, uint gasCost);
struct State{
uint128 count;
int128 winBalance;
}
/**
* creates a new edgeless casino contract.
* @param predecessorAddress the address of the predecessing contract
* tokenContract the address of the Edgeless token contract
* depositLimit the maximum deposit allowed
* kGasPrice the price per kGas in WEI
**/
function EdgelessCasino(address predecessorAddress, address tokenContract, uint depositLimit, uint kGasPrice) CasinoBank(depositLimit, predecessorAddress) Mortal(tokenContract) ChargingGas(kGasPrice) public{
}
/**
* updates several state channels at once. can be called by authorized wallets only.
* 1. determines the player address from the signature.
* 2. verifies if the signed game-count is higher than the last known game-count of this channel.
* 3. updates the balances accordingly. This means: It checks the already performed updates for this channel and computes
* the new balance difference to add or subtract from the player‘s balance.
* @param winBalances array of the current wins or losses
* gameCounts array of the numbers of signed game moves
* v,r,s array of the players's signatures
* chargeGas indicates if the gas costs should be subtracted from the players's balances
* */
function updateBatch(int128[] winBalances, uint128[] gameCounts, uint8[] v, bytes32[] r, bytes32[] s, bool chargeGas) public onlyAuthorized{
require(winBalances.length == gameCounts.length);
require(winBalances.length == v.length);
require(winBalances.length == r.length);
require(winBalances.length == s.length);
require(winBalances.length <= 50);
address player;
uint gasCost = 0;
if(chargeGas)
gasCost = getGasCost();
gasPayback = safeAdd(gasPayback, safeMul(gasCost, winBalances.length));
for(uint8 i = 0; i < winBalances.length; i++){
player = ecrecover(keccak256(winBalances[i], gameCounts[i]), v[i], r[i], s[i]);
_updateState(player, winBalances[i], gameCounts[i], gasCost);
}
}
/**
* updates a state channel. can be called by both parties.
* 1. verifies the signature.
* 2. verifies if the signed game-count is higher than the last known game-count of this channel.
* 3. updates the balances accordingly. This means: It checks the already performed updates for this channel and computes
* the new balance difference to add or subtract from the player‘s balance.
* @param winBalance the current win or loss
* gameCount the number of signed game moves
* v,r,s the signature of either the casino or the player
* chargeGas indicates if the gas costs should be subtracted from the player's balance
* */
function updateState(int128 winBalance, uint128 gameCount, uint8 v, bytes32 r, bytes32 s, bool chargeGas) public{
address player = determinePlayer(winBalance, gameCount, v, r, s);
uint gasCost = 0;
if(player == msg.sender)//if the player closes the state channel himself, make sure the signer is a casino wallet
require(authorized[ecrecover(keccak256(player, winBalance, gameCount), v, r, s)]);
else if (chargeGas){//subtract the gas costs from the player balance only if the casino wallet is the sender
gasCost = getGasCost();
gasPayback = safeAdd(gasPayback, gasCost);
}
_updateState(player, winBalance, gameCount, gasCost);
}
/**
* internal method to perform the actual state update.
* @param player the player address
* winBalance the player's win balance
* gameCount the player's game count
* */
function _updateState(address player, int128 winBalance, uint128 gameCount, uint gasCost) internal {
State storage last = lastState[player];
require(gameCount > last.count);
int difference = updatePlayerBalance(player, winBalance, last.winBalance, gasCost);
lastState[player] = State(gameCount, winBalance);
emit StateUpdate(player, gameCount, winBalance, difference, gasCost);
}
/**
* determines if the msg.sender or the signer of the passed signature is the player. returns the player's address
* @param winBalance the current winBalance, used to calculate the msg hash
* gameCount the current gameCount, used to calculate the msg.hash
* v, r, s the signature of the non-sending party
* */
function determinePlayer(int128 winBalance, uint128 gameCount, uint8 v, bytes32 r, bytes32 s) view internal returns(address){
if (authorized[msg.sender])//casino is the sender -> player is the signer
return ecrecover(keccak256(winBalance, gameCount), v, r, s);
else
return msg.sender;
}
/**
* computes the difference of the win balance relative to the last known state and adds it to the player's balance.
* in case the casino is the sender, the gas cost in EDG gets subtracted from the player's balance.
* @param player the address of the player
* winBalance the current win-balance
* lastWinBalance the win-balance of the last known state
* gasCost the gas cost of the tx
* */
function updatePlayerBalance(address player, int128 winBalance, int128 lastWinBalance, uint gasCost) internal returns(int difference){
difference = safeSub(winBalance, lastWinBalance);
int outstanding = safeSub(difference, int(gasCost));
uint outs;
if(outstanding < 0){
outs = uint256(outstanding * (-1));
playerBalance = safeSub(playerBalance, outs);
balanceOf[player] = safeSub(balanceOf[player], outs);
}
else{
outs = uint256(outstanding);
assert(bankroll() * oneEDG > outs);
playerBalance = safeAdd(playerBalance, outs);
balanceOf[player] = safeAdd(balanceOf[player], outs);
}
}
/**
* logs some seeds and game results for players wishing to have their game history logged by the contract
* @param serverSeeds array containing the server seeds
* clientSeeds array containing the client seeds
* results array containing the results
* v, r, s the signature of the non-sending party (to make sure the correct results are logged)
* */
function logGameData(bytes32[] serverSeeds, bytes32[] clientSeeds, int[] results, uint8 v, bytes32 r, bytes32 s) public{
address player = determinePlayer(serverSeeds, clientSeeds, results, v, r, s);
uint gasCost;
//charge gas in case the server is logging the results for the player
if(player != msg.sender){
gasCost = (57 + 768 * serverSeeds.length / 1000)*gasPrice;
balanceOf[player] = safeSub(balanceOf[player], gasCost);
playerBalance = safeSub(playerBalance, gasCost);
gasPayback = safeAdd(gasPayback, gasCost);
}
emit GameData(player, serverSeeds, clientSeeds, results, gasCost);
}
/**
* determines if the msg.sender or the signer of the passed signature is the player. returns the player's address
* @param serverSeeds array containing the server seeds
* clientSeeds array containing the client seeds
* results array containing the results
* v, r, s the signature of the non-sending party
* */
function determinePlayer(bytes32[] serverSeeds, bytes32[] clientSeeds, int[] results, uint8 v, bytes32 r, bytes32 s) view internal returns(address){
address signer = ecrecover(keccak256(serverSeeds, clientSeeds, results), v, r, s);
if (authorized[msg.sender])//casino is the sender -> player is the signer
return signer;
else if (authorized[signer])
return msg.sender;
else
revert();
}
}