Created
June 1, 2020 20:41
-
-
Save stwiname/9bb6b348e50c5cc7a4d81a268d84fcd4 to your computer and use it in GitHub Desktop.
GameLib.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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