pragma solidity ^0.4.24;
import "./CroupierRole.sol";
import "./SafeMath.sol";
contract BlackJack is CroupierRole {
using SafeMath for *;
uint constant THIS_DIVISOR = 1000;
uint constant LOCK_RATIO = 2500;
uint8 constant MAX_CUT_CARD = 15;
mapping (address => uint256) private _balances;
mapping (address => uint256) private _locked;
mapping (address => uint256) private _last_apply_time;
mapping (address => uint256) private _apply_amount;
uint256 private _totalBalance = 0;
uint256 public expireBlocks = 255;
uint256 public statedPeriod = 30 minutes;
uint256 public maxBet = 1 ether;
uint256 public minBet = 0.1 ether;
uint256 public feeRatio = 10;
struct Bet {
// gambler's address, 20 bytes.
address gambler;
// cut card position, the number range is 0-15.
uint8 cutCard;
// gambler's action list, per 4-bit representing an action, push-down storage.
/**
* action encoding rules:
* 0 - reserved
* 1 - Get
* 2 - Hit
* 3 - Stand
* 4 - Double
* 5 - Split
* 6 - Insurance
* 7 - Surrender
* 8 - timeout
*/
bytes11 actions;
// betting amount, 128 bits number is enough.
uint128 amount;
// block number of deal.
uint128 dealBlockNumber;
}
mapping (uint256 => Bet) public bets;
event Deposit(address indexed from, uint256 value);
event Withdraw(address indexed from, uint256 value);
event Apply(address indexed from, uint256 value);
event Deal(uint256 indexed commit);
event Settle(uint256 indexed commit);
event Refund(uint256 indexed commit, uint128 amount);
/**
* @dev constructor
*/
constructor() public payable{
}
/**
* @dev Fallback function. It's another entry for deposit. While owner transfer ether to
* this contract, it means increase the pot.
*/
function () public payable {
if(!isOwner(msg.sender)){
_deposit(msg.sender, msg.value);
}
}
/**
* @dev Total number of tokens deposit by gamblers.
* @return An uint256 representing the total amount owned by gamblers.
*/
function totalBalance() public view returns (uint256) {
return _totalBalance;
}
/**
* @dev Gets the balance of specified address.
* @param owner The address to query the balance of.
* @return An uint256 representing the amount owned by the passed address.
*/
function balanceOf(address owner) public view returns (uint256) {
return _balances[owner];
}
/**
* @dev Gets the locked value of specified address.
* @param owner The address to query the locked amount of.
* @return An uint256 representing the amount locked by the passed address.
*/
function lockedOf(address owner) public view returns (uint256) {
return _locked[owner];
}
/**
* @dev Gets the last apply-withdraw time of specified address.
* @param owner The address to query the last apply time of.
* @return An uint256 representing the last apply time by the passed address.
*/
function lastApplyTime(address owner) public view returns (uint256) {
return _last_apply_time[owner];
}
/**
* @dev Gets the apply-withdraw amount of specified address.
* @param owner The address to query the apply amount of.
* @return An uint256 representing the apply amount by the passed address.
*/
function applyAmount(address owner) public view returns (uint256) {
return _apply_amount[owner];
}
/**
* @dev Deal action to start a new game with proxy mode, submit by croupier bot.
* @param gambler gambler's address.
* @param commit generated by keccak of 2 256-bit reveals, used to unique identify a deck.
* gambler get commit but don't know the deck, dealer can't change the deck because of keccak is one-way irreversible.
* @param amount 128-bit number of bet amount.
* @param cutCard cut card position, gambler set it after receive the commit, so this process can guarantee fairness.
* @param v
* @param r
* @param s v, r,s are components of ECDSA signature. Ensure the deck is signed by the gambler himself.
*/
function deal(address gambler, uint256 commit, uint128 amount, uint8 cutCard, uint8 v, bytes32 r, bytes32 s)
public
onlyCroupier
{
// verify signature.
bytes32 signatureHash = keccak256(abi.encodePacked(amount, cutCard, commit));
require (gambler == ecrecover(signatureHash, v, r, s), "ECDSA signature is not valid.");
_dealCore(gambler, commit, amount, cutCard);
}
/**
* @dev Settle a deck by croupier.
* @param reveal_1 Per byte of 1-26 bytes in reveal_1, reveal_2 representing a single card, 2 256-bit reveal combine a 52 cards deck.
* Single card coding rules:
* low 4-bit : 0001-1010 points of single card(1-10).
* 5-6 bit : suit, 00 - spades, 01 - hearts, 10 - clubs, 11 - diamonds.
* 7-8 bit : face cards, 00 - 10, 01 - Jack, 10 - Queen, 11 - King.
* @param reveal_2 same as reveal_1.
* @param actions gambler's actions.
* @param win true - gambler win, false - lose.
* @param amount winnings or losses amount.
*/
function settle(uint256 reveal_1, uint256 reveal_2, bytes11 actions, bool win, uint128 amount)
public
onlyCroupier
{
uint commit = uint(keccak256(abi.encodePacked(reveal_1, reveal_2)));
Bet storage bet = bets[commit];
// verify commit.
address gambler = bet.gambler;
uint256 value = uint256(bet.amount);
require(gambler != address(0) && value > 0, "Bet should be in 'active' state.");
// verify bet is not expired.
require(block.number > bet.dealBlockNumber, "Settle in the same block as placeBet, or before.");
require(block.number <= uint256(bet.dealBlockNumber).add(expireBlocks), "Bet expired.");
// Store actions.
bet.actions = actions;
bet.amount = 0;
// unlock.
uint256 lockValue = value.mul(LOCK_RATIO).div(THIS_DIVISOR);
_locked[gambler] = _locked[gambler].sub(lockValue);
// calculate balance.
if(win) {
_balances[gambler] = _balances[gambler].add(uint256(amount));
_totalBalance = _totalBalance.add(uint256(amount));
}
else{
_balances[gambler] = _balances[gambler].sub(uint256(amount));
_totalBalance = _totalBalance.sub(uint256(amount));
}
emit Settle(commit);
}
/**
* @dev Refund a commit while it's expired.
* @param commit which one bet been refunded.
*/
function refund(uint256 commit) public onlyCroupier {
// Verify that bet is in 'active' state.
Bet storage bet = bets[commit];
uint256 value = uint256(bet.amount);
address gambler = bet.gambler;
require(gambler != address(0) && value > 0, "Bet should be in 'active' state.");
// Verify that bet has already expired.
require (block.number > uint256(bet.dealBlockNumber).add(expireBlocks), "Bet not yet expired.");
//unlock.
uint256 lockValue = value.mul(LOCK_RATIO).div(THIS_DIVISOR);
_locked[gambler] = _locked[gambler].sub(lockValue);
bet.amount = 0;
emit Refund(commit, uint128(value));
}
/**
* @dev Deposit in this contract.
*/
function deposit() public payable returns (bool){
_deposit(msg.sender, msg.value);
return true;
}
/**
* @dev apply for withdrawal.
* @param amount the amount to apply for withdrawal, should be less than balance subtract locked.
*/
function apply(uint256 amount) public returns (bool){
require(amount <= _balances[msg.sender].sub(_locked[msg.sender]), "Not enough balance.");
_last_apply_time[msg.sender] = now;
_apply_amount[msg.sender] = amount;
emit Apply(msg.sender, amount);
return true;
}
/**
* @dev Withdraw from this contract. Should apply at first, and withdraw after the stated apply period.
*/
function withdraw() public returns (bool){
require(_apply_amount[msg.sender] > 0, "");
require(now >= _last_apply_time[msg.sender].add(statedPeriod), "");
_withdraw(msg.sender, _apply_amount[msg.sender]);
_apply_amount[msg.sender] = 0;
return true;
}
/**
* @dev Withdraw all by croupier in special cases, such as contract upgrade.
* @param from The address to withdraw.
*/
function withdrawProxy(address from) public onlyCroupier returns(bool) {
uint256 amount = balanceOf(from);
_withdraw(from, amount);
return true;
}
/**
* @dev Deposit for a specified address, internal function.
* @param from The address to deposit.
* @param value The amount to be deposited.
*/
function _deposit(address from, uint256 value) internal {
require(from != address(0), "Invalid address.");
_balances[from] = _balances[from].add(value);
_totalBalance = _totalBalance.add(value);
emit Deposit(from, value);
}
/**
* @dev Withdraw for a specified address, internal function. Due to house edge of blackjack can't cover the cost of gas,
* platform charges 1% fee while withdraw.
* @param from The address to withdraw.
* @param value The amount to be withdrawed, should be less than balance subtract locked, and this contract can afford.
*/
function _withdraw(address from, uint256 value) internal {
require(from != address(0), "Invalid address.");
require(value <= _balances[from].sub(_locked[from]), "Not enough balance.");
_balances[from] = _balances[from].sub(value);
_totalBalance = _totalBalance.sub(value);
uint256 fee = value.mul(feeRatio).div(THIS_DIVISOR);
require(value.sub(fee) <= address(this).balance, "Can't afford.");
from.transfer(value.sub(fee));
emit Withdraw(from, value);
}
/**
* @dev Check uint256-uint128 type conversion is safe
*/
function _safeTypeConversion(uint256 a, uint128 b) internal pure returns(bool) {
require(a == uint256(b) && uint128(a) == b, "Not safe type conversion.");
return true;
}
/**
* @dev Deal action core.
*/
function _dealCore(address gambler, uint256 commit, uint128 amount, uint8 cutCard) internal {
// verify commit is "Clean".
Bet storage bet = bets[commit];
require(bet.gambler == address(0), "Bet should be in 'clean' state.");
// verify cut card position.
require(cutCard <= MAX_CUT_CARD, "Cut card position is not valid.");
//verify bet amount range.
uint256 value = uint256(amount);
require(_safeTypeConversion(value, amount), "Not safe type conversion");
require(value >= minBet && value <= maxBet, "Bet amount is out of range.");
uint256 lockValue = value.mul(LOCK_RATIO).div(THIS_DIVISOR);
require(lockValue <= balanceOf(gambler).sub(lockedOf(gambler)), "Balance is not enough for locked.");
// Store bet parameters on blockchain.
_locked[gambler] = _locked[gambler].add(lockValue);
bet.gambler = gambler;
bet.cutCard = cutCard;
bet.amount = amount;
bet.dealBlockNumber = uint128(block.number);
emit Deal(commit);
}
/**
* @dev Set max bet amount.
* @param input in wei.
*/
function setMaxBet(uint256 input) public onlyOwner {
maxBet = input;
}
/**
* @dev Set min bet amount.
* @param input in wei.
*/
function setMinBet(uint256 input) public onlyOwner {
minBet = input;
}
/**
* @dev Set fee ratio.
* @param input new fee ratio, div by 1000.
*/
function setFeeRatio(uint256 input) public onlyOwner {
feeRatio = input;
}
/**
* @dev Set expiration blocks.
* @param input new number of expiration blocks.
*/
function setExpireBlocks(uint256 input) public onlyOwner {
expireBlocks = input;
}
/**
* @dev Set stated apply period.
* @param input new number of stated apply period.
*/
function setStatedPeriod(uint256 input) public onlyOwner {
statedPeriod = input;
}
/**
* @dev Withdraw funds to cover costs of operation.
* @param amount should ensure the total balances of palyers.
*/
function withdrawFunds(uint256 amount) public onlyOwner {
require(amount <= address(this).balance.sub(_totalBalance), "Not enough funds.");
msg.sender.transfer(amount);
}
/**
* @dev kill this contract while upgraded.
*/
function kill() public onlyOwner {
require(_totalBalance == 0, "All of gambler's balances need to be withdrawn.");
selfdestruct(msg.sender);
}
}
pragma solidity ^0.4.24;
import "./OwnerRole.sol";
contract CroupierRole is OwnerRole{
using Roles for Roles.Role;
event CroupierAdded(address indexed account);
event CroupierRemoved(address indexed account);
Roles.Role private _croupiers;
constructor () internal {
}
modifier onlyCroupier() {
require(isCroupier(msg.sender));
_;
}
function isCroupier(address account) public view returns (bool) {
return _croupiers.has(account);
}
function addCroupier(address account) public onlyOwner {
_addCroupier(account);
}
function removeCroupier(address account) public onlyOwner {
_removeCroupier(account);
}
function _addCroupier(address account) internal {
_croupiers.add(account);
emit CroupierAdded(account);
}
function _removeCroupier(address account) internal {
_croupiers.remove(account);
emit CroupierRemoved(account);
}
}
pragma solidity ^0.4.24;
import "./Roles.sol";
contract OwnerRole {
using Roles for Roles.Role;
event OwnerAdded(address indexed account);
event OwnerRemoved(address indexed account);
Roles.Role private _owners;
constructor () internal {
_addOwner(msg.sender);
}
modifier onlyOwner() {
require(isOwner(msg.sender));
_;
}
function isOwner(address account) public view returns (bool) {
return _owners.has(account);
}
function addOwner(address account) public onlyOwner {
_addOwner(account);
}
function removeOwner(address account) public onlyOwner{
_removeOwner(account);
}
function _addOwner(address account) internal {
_owners.add(account);
emit OwnerAdded(account);
}
function _removeOwner(address account) internal {
_owners.remove(account);
emit OwnerRemoved(account);
}
}
pragma solidity ^0.4.24;
/**
* @title Roles
* @dev Library for managing addresses assigned to a Role.
*/
library Roles {
struct Role {
mapping (address => bool) bearer;
}
/**
* @dev give an account access to this role
*/
function add(Role storage role, address account) internal {
require(account != address(0));
require(!has(role, account));
role.bearer[account] = true;
}
/**
* @dev remove an account's access to this role
*/
function remove(Role storage role, address account) internal {
require(account != address(0));
require(has(role, account));
role.bearer[account] = false;
}
/**
* @dev check if an account has this role
* @return bool
*/
function has(Role storage role, address account) internal view returns (bool) {
require(account != address(0));
return role.bearer[account];
}
}
pragma solidity ^0.4.24;
/**
* @title SafeMath
* @dev Math operations with safety checks that revert on error
*/
library SafeMath {
/**
* @dev Multiplies two numbers, reverts on overflow.
*/
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
if (a == 0) {
return 0;
}
uint256 c = a * b;
require(c / a == b);
return c;
}
/**
* @dev Integer division of two numbers truncating the quotient, reverts on division by zero.
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// Solidity only automatically asserts when dividing by 0
require(b > 0);
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
/**
* @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a);
uint256 c = a - b;
return c;
}
/**
* @dev Adds two numbers, reverts on overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a);
return c;
}
/**
* @dev Divides two numbers and returns the remainder (unsigned integer modulo),
* reverts when dividing by zero.
*/
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
require(b != 0);
return a % b;
}
}
{
"compilationTarget": {
"BlackJack.sol": "BlackJack"
},
"evmVersion": "byzantium",
"libraries": {},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": []
}
[{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"withdrawFunds","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"account","type":"address"}],"name":"removeOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"input","type":"uint256"}],"name":"setFeeRatio","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"bets","outputs":[{"name":"gambler","type":"address"},{"name":"cutCard","type":"uint8"},{"name":"actions","type":"bytes11"},{"name":"amount","type":"uint128"},{"name":"dealBlockNumber","type":"uint128"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"commit","type":"uint256"}],"name":"refund","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"maxBet","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"account","type":"address"}],"name":"isOwner","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"from","type":"address"}],"name":"withdrawProxy","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"withdraw","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"feeRatio","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"kill","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"account","type":"address"}],"name":"addCroupier","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"input","type":"uint256"}],"name":"setStatedPeriod","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"gambler","type":"address"},{"name":"commit","type":"uint256"},{"name":"amount","type":"uint128"},{"name":"cutCard","type":"uint8"},{"name":"v","type":"uint8"},{"name":"r","type":"bytes32"},{"name":"s","type":"bytes32"}],"name":"deal","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"statedPeriod","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"account","type":"address"}],"name":"addOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"input","type":"uint256"}],"name":"setMaxBet","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"input","type":"uint256"}],"name":"setMinBet","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"minBet","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"account","type":"address"}],"name":"removeCroupier","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"account","type":"address"}],"name":"isCroupier","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"reveal_1","type":"uint256"},{"name":"reveal_2","type":"uint256"},{"name":"actions","type":"bytes11"},{"name":"win","type":"bool"},{"name":"amount","type":"uint128"}],"name":"settle","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"owner","type":"address"}],"name":"lockedOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"owner","type":"address"}],"name":"lastApplyTime","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"totalBalance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"owner","type":"address"}],"name":"applyAmount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"deposit","outputs":[{"name":"","type":"bool"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"input","type":"uint256"}],"name":"setExpireBlocks","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"apply","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"expireBlocks","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":true,"stateMutability":"payable","type":"constructor"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Withdraw","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Apply","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"commit","type":"uint256"}],"name":"Deal","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"commit","type":"uint256"}],"name":"Settle","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"commit","type":"uint256"},{"indexed":false,"name":"amount","type":"uint128"}],"name":"Refund","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"account","type":"address"}],"name":"CroupierAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"account","type":"address"}],"name":"CroupierRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"account","type":"address"}],"name":"OwnerAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"account","type":"address"}],"name":"OwnerRemoved","type":"event"}]