Skip to content

Instantly share code, notes, and snippets.

@stwiname
Created June 1, 2020 20:41
Show Gist options
  • Save stwiname/9bb6b348e50c5cc7a4d81a268d84fcd4 to your computer and use it in GitHub Desktop.
Save stwiname/9bb6b348e50c5cc7a4d81a268d84fcd4 to your computer and use it in GitHub Desktop.
GameLib.sol
// File: @openzeppelin/contracts/math/SafeMath.sol
pragma solidity ^0.6.0;
/**
* @dev Wrappers over Solidity's arithmetic operations with added overflow
* checks.
*
* Arithmetic operations in Solidity wrap on overflow. This can easily result
* in bugs, because programmers usually assume that an overflow raises an
* error, which is the standard behavior in high level programming languages.
* `SafeMath` restores this intuition by reverting the transaction when an
* operation overflows.
*
* Using this library instead of the unchecked operations eliminates an entire
* class of bugs, so it's recommended to use it always.
*/
library SafeMath {
/**
* @dev Returns the addition of two unsigned integers, reverting on
* overflow.
*
* Counterpart to Solidity's `+` operator.
*
* Requirements:
* - Addition cannot overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "SafeMath: addition overflow");
return c;
}
/**
* @dev Returns the subtraction of two unsigned integers, reverting on
* overflow (when the result is negative).
*
* Counterpart to Solidity's `-` operator.
*
* Requirements:
* - Subtraction cannot overflow.
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
return sub(a, b, "SafeMath: subtraction overflow");
}
/**
* @dev Returns the subtraction of two unsigned integers, reverting with custom message on
* overflow (when the result is negative).
*
* Counterpart to Solidity's `-` operator.
*
* Requirements:
* - Subtraction cannot overflow.
*/
function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
require(b <= a, errorMessage);
uint256 c = a - b;
return c;
}
/**
* @dev Returns the multiplication of two unsigned integers, reverting on
* overflow.
*
* Counterpart to Solidity's `*` operator.
*
* Requirements:
* - Multiplication cannot 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-contracts/pull/522
if (a == 0) {
return 0;
}
uint256 c = a * b;
require(c / a == b, "SafeMath: multiplication overflow");
return c;
}
/**
* @dev Returns the integer division of two unsigned integers. Reverts on
* division by zero. The result is rounded towards zero.
*
* Counterpart to Solidity's `/` operator. Note: this function uses a
* `revert` opcode (which leaves remaining gas untouched) while Solidity
* uses an invalid opcode to revert (consuming all remaining gas).
*
* Requirements:
* - The divisor cannot be zero.
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
return div(a, b, "SafeMath: division by zero");
}
/**
* @dev Returns the integer division of two unsigned integers. Reverts with custom message on
* division by zero. The result is rounded towards zero.
*
* Counterpart to Solidity's `/` operator. Note: this function uses a
* `revert` opcode (which leaves remaining gas untouched) while Solidity
* uses an invalid opcode to revert (consuming all remaining gas).
*
* Requirements:
* - The divisor cannot be zero.
*/
function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
// Solidity only automatically asserts when dividing by 0
require(b > 0, errorMessage);
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
/**
* @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
* Reverts when dividing by zero.
*
* Counterpart to Solidity's `%` operator. This function uses a `revert`
* opcode (which leaves remaining gas untouched) while Solidity uses an
* invalid opcode to revert (consuming all remaining gas).
*
* Requirements:
* - The divisor cannot be zero.
*/
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
return mod(a, b, "SafeMath: modulo by zero");
}
/**
* @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
* Reverts with custom message when dividing by zero.
*
* Counterpart to Solidity's `%` operator. This function uses a `revert`
* opcode (which leaves remaining gas untouched) while Solidity uses an
* invalid opcode to revert (consuming all remaining gas).
*
* Requirements:
* - The divisor cannot be zero.
*/
function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
require(b != 0, errorMessage);
return a % b;
}
}
// File: @openzeppelin/contracts/utils/Address.sol
pragma solidity ^0.6.2;
/**
* @dev Collection of functions related to the address type
*/
library Address {
/**
* @dev Returns true if `account` is a contract.
*
* [IMPORTANT]
* ====
* It is unsafe to assume that an address for which this function returns
* false is an externally-owned account (EOA) and not a contract.
*
* Among others, `isContract` will return false for the following
* types of addresses:
*
* - an externally-owned account
* - a contract in construction
* - an address where a contract will be created
* - an address where a contract lived, but was destroyed
* ====
*/
function isContract(address account) internal view returns (bool) {
// According to EIP-1052, 0x0 is the value returned for not-yet created accounts
// and 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 is returned
// for accounts without code, i.e. `keccak256('')`
bytes32 codehash;
bytes32 accountHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470;
// solhint-disable-next-line no-inline-assembly
assembly { codehash := extcodehash(account) }
return (codehash != accountHash && codehash != 0x0);
}
/**
* @dev Replacement for Solidity's `transfer`: sends `amount` wei to
* `recipient`, forwarding all available gas and reverting on errors.
*
* https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
* of certain opcodes, possibly making contracts go over the 2300 gas limit
* imposed by `transfer`, making them unable to receive funds via
* `transfer`. {sendValue} removes this limitation.
*
* https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more].
*
* IMPORTANT: because control is transferred to `recipient`, care must be
* taken to not create reentrancy vulnerabilities. Consider using
* {ReentrancyGuard} or the
* https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
*/
function sendValue(address payable recipient, uint256 amount) internal {
require(address(this).balance >= amount, "Address: insufficient balance");
// solhint-disable-next-line avoid-low-level-calls, avoid-call-value
(bool success, ) = recipient.call{ value: amount }("");
require(success, "Address: unable to send value, recipient may have reverted");
}
}
// File: @openzeppelin/contracts/utils/ReentrancyGuard.sol
pragma solidity ^0.6.0;
/**
* @dev Contract module that helps prevent reentrant calls to a function.
*
* Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
* available, which can be applied to functions to make sure there are no nested
* (reentrant) calls to them.
*
* Note that because there is a single `nonReentrant` guard, functions marked as
* `nonReentrant` may not call one another. This can be worked around by making
* those functions `private`, and then adding `external` `nonReentrant` entry
* points to them.
*
* TIP: If you would like to learn more about reentrancy and alternative ways
* to protect against it, check out our blog post
* https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
*/
contract ReentrancyGuard {
bool private _notEntered;
constructor () internal {
// Storing an initial non-zero value makes deployment a bit more
// expensive, but in exchange the refund on every call to nonReentrant
// will be lower in amount. Since refunds are capped to a percetange of
// the total transaction's gas, it is best to keep them low in cases
// like this one, to increase the likelihood of the full refund coming
// into effect.
_notEntered = true;
}
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
* Calling a `nonReentrant` function from another `nonReentrant`
* function is not supported. It is possible to prevent this from happening
* by making the `nonReentrant` function external, and make it call a
* `private` function that does the actual work.
*/
modifier nonReentrant() {
// On the first call to nonReentrant, _notEntered will be true
require(_notEntered, "ReentrancyGuard: reentrant call");
// Any calls to nonReentrant after this point will fail
_notEntered = false;
_;
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
_notEntered = true;
}
}
// File: contracts/AuctionLib.sol
pragma solidity ^0.6.0;
library AuctionLib {
using Address for address payable;
enum Result {
UNSET,
MISS,
HIT
}
struct Bid {
address payable bidder;
uint amount;
uint16[2] move;
}
struct Data {
Bid leadingBid;
Result result;
// Start the auction at a later point in time
uint256 startTime;
// How long the auction runs after the first bid
uint256 duration;
// When the auction ends
uint256 endTime;
}
function placeBid(
Data storage data,
uint16[2] memory move
) public returns(uint256){
// Validate auction
require(hasStarted(data), "Auction has not started");
require(!hasEnded(data), "Auction has ended");
// Validate input
require(msg.value > data.leadingBid.amount, "Bid must be greater than current bid");
Bid memory previousBid = data.leadingBid;
data.leadingBid = Bid(tx.origin, msg.value, move);
// First bid, auction is started and will end after duration from now
if (data.endTime == 0) {
data.endTime = now + data.duration;
}
// Transfer the bid back to the previous bidder
if (previousBid.bidder != address(0)) {
previousBid.bidder.sendValue(previousBid.amount);
}
return data.endTime;
}
function setResult(Data storage data, bool hit) public {
require(hasEnded(data), "Auction has not yet ended");
require(data.result == Result.UNSET, "Auction result already set");
data.result = hit ? Result.HIT : Result.MISS;
}
/* End the auction immediately,
this is used when the other team wins to stop it instantly,
return the funds to the leading bidder
*/
function cancel(Data storage data) public {
if (!hasEnded(data)) {
data.endTime = now - 1;
}
if (data.leadingBid.bidder != address(0)) {
data.leadingBid.bidder.sendValue(data.leadingBid.amount);
data.leadingBid = Bid(address(0), 0, [uint16(0), uint16(0)]);
}
}
function hasStarted(Data storage data) public view returns(bool) {
return now >= data.startTime;
}
function hasEnded(Data storage data) public view returns(bool) {
return data.endTime != 0 && now > data.endTime;
}
}
// File: contracts/Auction.sol
pragma solidity ^0.6.0;
contract Auction is ReentrancyGuard {
using AuctionLib for AuctionLib.Data;
using Address for address payable;
AuctionLib.Data data;
address payable owner;
modifier ownerOnly(){
require(msg.sender == owner, "Only the owner can call this");
_;
}
constructor(uint256 _startTime, uint256 _duration) public {
data.startTime = _startTime;
data.duration = _duration;
owner = msg.sender;
}
function placeBid(uint16[2] memory move) payable public nonReentrant returns(uint256){
return data.placeBid(move);
}
function withdrawFunds() public ownerOnly {
owner.sendValue(address(this).balance);
}
function setResult(bool hit) public ownerOnly {
data.setResult(hit);
}
/* End the auction immediately,
this is used when the other team wins to stop it instantly,
return the funds to the leading bidder
*/
function cancel() public ownerOnly {
data.cancel();
}
function hasStarted() public view returns(bool) {
return data.hasStarted();
}
function hasEnded() public view returns(bool) {
return data.hasEnded();
}
function getLeadingBid() public view returns(address payable bidder, uint amount, uint16[2] memory move) {
return (data.leadingBid.bidder, data.leadingBid.amount, data.leadingBid.move);
}
function getLeadingMove() public view returns(uint16[2] memory move) {
return data.leadingBid.move;
}
function getResult() public view returns(AuctionLib.Result) {
return data.result;
}
/* DEV ONLY*/
function getBalance() public view returns(uint balance) {
return address(this).balance;
}
function getStartTime() public view returns(uint256) {
return data.startTime;
}
function getEndTime() public view returns(uint256) {
return data.endTime;
}
function getDuration() public view returns(uint256) {
return data.duration;
}
}
// File: contracts/GameLib.sol
pragma solidity ^0.6.0;
pragma experimental ABIEncoderV2;
library GameLib {
using Address for address payable;
using SafeMath for uint256;
using SafeMath for uint;
using AuctionLib for AuctionLib.Data;
enum Result {
UNSET,
RED_WINNER,
BLUE_WINNER,
ABORTED
}
enum Team {
RED,
BLUE
}
struct Data {
mapping(uint => bytes32) fieldHashes;
mapping(address => bool) withdrawn;
uint16 fieldSize;
uint fieldUnits;
Result result;
uint256 auctionDuration;
/*
* Auctions support a max dimension of 2^16
* So we need to support at least 2^16 * 2 moves
*/
mapping(uint32 => AuctionLib.Data)[2] auctions;
uint32[2] auctionsCount;
}
event HighestBidPlaced(Team team, address bidder, uint amount, uint16[2] move, uint256 endTime, uint32 auctionIndex);
event MoveConfirmed(Team team, bool hit, uint16[2] move, uint32 auctionIndex);
event AuctionCreated(Team team, uint32 auctionIndex);
event GameCompleted(Team winningTeam);
function isMoveInField(Data storage data, uint16[2] memory move) public view returns(bool) {
return move[0] < data.fieldSize && move[1] < data.fieldSize;
}
function isValidMove(Data storage data, uint16[2] memory move, Team team) private view returns(bool) {
return isMoveInField(data, move) && !hasMoveBeenMade(data, team, move);
}
function placeBid(
Data storage data,
uint16[2] memory move,
Team team,
uint32 auctionIndex
) public returns(uint256){
require(isValidMove(data, move, team), "Move is out of field bounds");
AuctionLib.Data storage auction = getAuctionByIndex(data, team, auctionIndex);
uint256 endTime = auction.placeBid(move);
uint32 otherCount = getAuctionsCount(data, otherTeam(team));
if (otherCount == 0) {
startAuction(data, otherTeam(team));
} else {
AuctionLib.Data storage otherAuction = getCurrentAuction(data, otherTeam(team));
if (otherAuction.hasEnded()) {
startAuction(data, otherTeam(team));
}
}
emit HighestBidPlaced(team, msg.sender, msg.value, move, endTime, auctionIndex);
return endTime;
}
// Gets called by the oracle when the first bid is made for an auction
function startAuction(Data storage data, Team team) public returns(AuctionLib.Data memory) {
// We might be starting the first auction for the team
if (data.auctionsCount[uint(team)] > 0) {
require(
getCurrentAuction(data, team).hasEnded(),
"Cannot start an auction while one is already running"
);
}
AuctionLib.Data memory otherAuction = getCurrentAuction(data, otherTeam(team));
require(
otherAuction.endTime > 0,
"First bid must be made on other auction first"
);
return createAuction(data, team, otherAuction.endTime - data.auctionDuration/2);
}
function confirmMove(Data storage data, Team team, bool hit, uint32 auctionIndex) public {
AuctionLib.Data storage auction = getAuctionByIndex(data, team, auctionIndex);
auction.setResult(hit);
// This auction has finished but the othe team has none,
// we can now create it though
if (data.auctionsCount[uint(otherTeam(team))] <= 0) {
startAuction(data, otherTeam(team));
}
AuctionLib.Data storage otherAuction = getCurrentAuction(data, otherTeam(team));
// Auction has ended or has had a bid (has an end time)
if ((otherAuction.hasEnded() || otherAuction.endTime > 0)
// Only start auction if we're confirming the currentAuction
&& auctionIndex == getAuctionsCount(data, team) - 1
) {
// Start the next auction
startAuction(data, team);
}
emit MoveConfirmed(team, hit, auction.leadingBid.move, auctionIndex);
}
function finalize(Data storage data, Team winner, bytes32 fieldData, bytes32 salt) public {
require(
keccak256(abi.encodePacked(fieldData, salt)) == data.fieldHashes[uint(winner)],
'Invalid verification of field'
);
data.result = winner == Team.RED ? Result.RED_WINNER : Result.BLUE_WINNER;
/* Set the current auction as a hit, */
AuctionLib.Data storage currentAuction = getCurrentAuction(data, winner);
if (currentAuction.hasEnded() && currentAuction.result == AuctionLib.Result.UNSET) {
currentAuction.setResult(true);
}
/* Stop current auction of losing team, return funds to latest bidder */
AuctionLib.Data storage losingAuction = getCurrentAuction(data, otherTeam(winner));
losingAuction.cancel();
emit GameCompleted(winner);
// Users should be able to withdraw their winnings now
}
function withdraw(Data storage data) public {
require(data.result == Result.RED_WINNER || data.result == Result.BLUE_WINNER, 'Game must be completed first');
require(!data.withdrawn[msg.sender], 'Cannot withdraw multiple times');
Team winningTeam = data.result == Result.RED_WINNER ? Team.RED : Team.BLUE;
uint winnings = getPotentialWinnings(data, msg.sender, winningTeam);
if (winnings == 0) {
return;
}
data.withdrawn[msg.sender] = true;
msg.sender.sendValue(winnings);
}
function hasMoveBeenMade(Data storage data, Team team, uint16[2] memory move) public view returns (bool) {
uint teamId = uint(team);
for(uint32 i = 0; i < data.auctionsCount[teamId]; i++) {
AuctionLib.Data storage auction = getAuctionByIndex(data, team, i);
// Move isnt considered made if the auction has not ended
if (!auction.hasEnded()) {
continue;
}
AuctionLib.Bid memory bid = auction.leadingBid;
if (bid.bidder != address(0) && bid.move[0] == move[0] && bid.move[1] == move[1]) {
return true;
}
}
return false;
}
function getAuctionsCount(Data storage data, Team team) public view returns(uint32) {
uint teamId = uint(team);
return data.auctionsCount[teamId];
}
function getCurrentAuctionIndex(Data storage data, Team team) public view returns(uint32) {
uint32 count = getAuctionsCount(data, team);
require(count > 0, "No auctions exist for this team");
return count - 1;
}
function getCurrentAuction(Data storage data, Team team) public view returns(AuctionLib.Data storage) {
uint32 count = getAuctionsCount(data, team);
require(count > 0, "No auction exists for this team");
return getAuctionByIndex(data, team, count - 1);
}
function getAuctionByIndex(Data storage data, Team team, uint32 index) public view returns(AuctionLib.Data storage) {
uint teamId = uint(team);
return data.auctions[teamId][index];
}
function createAuction(Data storage data, Team team, uint256 startTime) internal returns(AuctionLib.Data memory) {
uint teamId = uint(team);
AuctionLib.Data memory auction = AuctionLib.Data({
startTime: startTime,
endTime: 0,
duration: data.auctionDuration,
result: AuctionLib.Result.UNSET,
leadingBid: AuctionLib.Bid({
bidder: address(0),
amount: 0,
move: [uint16(0), uint16(0)]
})
});
data.auctionsCount[teamId] ++;
uint32 auctionIndex = data.auctionsCount[teamId]-1;
data.auctions[teamId][auctionIndex] = auction;
emit AuctionCreated(team, auctionIndex);
return auction;
}
function otherTeam(Team team) internal pure returns(Team) {
return team == Team.BLUE ? Team.RED : Team.BLUE;
}
function getRewardPool(Data storage data, Team team) public view returns(uint) {
uint rewardPool = 0;
for (uint32 i = 0; i < getAuctionsCount(data, team); i++) {
rewardPool += getAuctionByIndex(data, team, i).leadingBid.amount;
}
return rewardPool;
}
function getPotentialWinnings(Data storage data, address player, Team team) public view returns(uint) {
// Calculate total amount of rewards
uint rewardPool = getRewardPool(data, otherTeam(team));
// Get num valid auctions, if the last auction has zero bid it is either not played or cancelled
uint32 numAuctions = getAuctionsCount(data, team);
if (numAuctions <= 0) {
return 0;
}
// Exclude current auction if no bids made
uint256 leadingAmount = getCurrentAuction(data, team).leadingBid.amount;
if (leadingAmount <= 0) {
numAuctions--;
}
if (numAuctions <= 0) {
return 0;
}
// Save 10% for owner
// TODO see if percentage covers oracle costs
uint rewardPerMove = rewardPool.div(10).mul(9).div(numAuctions);
uint reward = 0;
/* Return move cost + reward to each player on the winning team */
for (uint32 i = 0; i < numAuctions; i++) {
AuctionLib.Bid memory bid = getAuctionByIndex(data, team, i).leadingBid;
if (bid.bidder == player) {
reward += rewardPerMove + bid.amount;
}
}
return reward;
}
function hasWithdrawnWinnings(Data storage data) public view returns(bool) {
return data.withdrawn[msg.sender];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment