Skip to content

Instantly share code, notes, and snippets.

@pcaversaccio
Last active September 30, 2022 23:45
Show Gist options
  • Save pcaversaccio/4fff170db1a25876dc13e6b7e1942bf8 to your computer and use it in GitHub Desktop.
Save pcaversaccio/4fff170db1a25876dc13e6b7e1942bf8 to your computer and use it in GitHub Desktop.
DoubleOrNothing: Send ETH to the contract and either get double back (if the randomness beacon + a pre-committed nonce is even) or lose the bet (if the randomness beacon + a pre-committed nonce is odd). The assumption is that the contract is sufficiently funded by enough lost bets!
// SPDX-License-Identifier: WTFPL
pragma solidity 0.8.17;
/**
* @dev Error that occurs when called by a contract account.
* @param emitter The contract that emits the error.
*/
error ContractCallsAreNotAllowed(address emitter);
/**
* @dev Error that occurs when no bet has been prepared.
* @param emitter The contract that emits the error.
*/
error NoBetPrepared(address emitter);
/**
* @dev Error that occurs when a game cannot be played yet.
* @param emitter The contract that emits the error.
*/
error TooEarly(address emitter);
/**
* @dev Error that occurs when a message digest,
* given a nonce, cannot be verified.
* @param emitter The contract that emits the error.
*/
error NonceMismatch(address emitter);
/**
* @title Double or Nothing on Ethereum Mainnet
* @author pcaversaccio
* @dev Send ETH to the contract and either get double back
* (if the randomness beacon + a pre-committed nonce is even)
* or lose the bet (if the randomness beacon + a pre-committed
* nonce is odd). The assumption is that the contract is sufficiently
* funded by enough lost bets!
*/
contract DoubleOrNothing {
/**
* @dev To prevent manipulations, a player must wait 2 epochs
* (each epoch has 32 slots with 12 seconds intervals) before
* a game can be played.
*/
uint16 private constant _WAITING_PERIOD = 12 * 32 * 2;
/**
* @dev The struct that represents a bet made by `msg.sender`.
* To prevent manipulation (e.g. front running), a commit-reveal
* scheme is used.
*/
struct Bet {
bytes32 digest;
uint256 timestamp;
}
mapping(address => Bet) public bets;
/**
* @dev Event that is emitted when a bet is prepared.
* @param player The 20-byte address of the player.
*/
event BetPrepared(address indexed player);
/**
* @dev Event that is emitted when a bet is won.
* @param player The 20-byte address of the player.
* @param amount The amount (i.e. 2 * msg.value) that is won.
*/
event Won(address indexed player, uint256 amount);
/**
* @dev Event that is emitted when a bet is lost.
* @param player The 20-byte address of the player.
* @param amount The amount (i.e. msg.value) that is lost.
*/
event Lost(address indexed player, uint256 amount);
constructor() payable {}
/**
* @notice Prepares a future bet.
* @param nonceDigest The 32-byte nonce digest.
*/
function prepareBet(bytes32 nonceDigest) external {
/**
* @dev Since EIP-3074 (https://eips.ethereum.org/EIPS/eip-3074)
* allows for `authorized` to equal `tx.origin`,
* the following check will fail under this new regime.
*/
// solhint-disable-next-line avoid-tx-origin
if (tx.origin != msg.sender)
revert ContractCallsAreNotAllowed(address(this));
bets[msg.sender] = Bet({
digest: nonceDigest,
// solhint-disable-next-line not-rely-on-time
timestamp: block.timestamp
});
emit BetPrepared(msg.sender);
}
/**
* @notice Good luck!
* @param nonce The 8-byte nonce number.
*/
function play(uint64 nonce) external payable {
Bet memory bet = bets[msg.sender];
if (bet.digest != 0) revert NoBetPrepared(address(this));
// solhint-disable-next-line not-rely-on-time
if (block.timestamp <= bet.timestamp + _WAITING_PERIOD)
revert TooEarly(address(this));
if (bet.digest != keccak256(abi.encodePacked(nonce, msg.sender)))
revert NonceMismatch(address(this));
/**
* @dev It is theoretically possible that this addition overflows.
* In such a case, the transaction is reverted and the user must
* retry in the next block.
*/
if ((block.difficulty + nonce) % 2 == 0) {
payable(msg.sender).transfer(2 * msg.value);
emit Won(msg.sender, 2 * msg.value);
} else {
emit Lost(msg.sender, msg.value);
}
delete bets[msg.sender];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment