Solution: Part 3

Build our own play-to earn game.

Final derived contract

Next, we'll define our final contract, which will inherit from TreasureHuntVRF.sol (and thus implicitly from TreasureHuntNoVRF.sol). Here, we'll implement the constructor, various functions to handle gameplay and payments, as well as OpenZeppelin design patterns.

Press + to interact
input.sol
TreasureHuntNoVRF.sol
TreasureHuntVRF.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.14;
/// @author ndehouche
/// @title TreasureHunt
import "./TreasureHuntVRF.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract TreasureHunt is Ownable, Pausable, ReentrancyGuard, TreasureHuntVRF {
/**
* @notice Constructor inherits VRFConsumerBaseV2
*
* @param subscriptionId subscription id that this consumer contract can use
*/
constructor(
address _vrfCoordinator,
bytes32 _s_keyHash,
uint64 subscriptionId,
address _tokenContract,
uint _fee,
uint _seasonStart,
uint _seasonEnd)
VRFConsumerBaseV2(_vrfCoordinator) {
COORDINATOR = VRFCoordinatorV2Interface(_vrfCoordinator);
s_keyHash=_s_keyHash;
s_subscriptionId = subscriptionId;
tokenContract = _tokenContract;
fee=_fee;
seasonStart=_seasonStart;
seasonEnd=_seasonEnd;
}
/**
* @notice Requests randomness
* Will revert if subscription is not set and funded.
*/
function flipCoin(uint _tokenId) public payable
onlyIfSeasonOpen
onlyIfPaidEnough
onlyIfNotLocked(_tokenId)
onlyIfHoldsToken(_tokenId)
onlyIfNotWon(_tokenId)
whenNotPaused
returns (uint requestId)
{
require(s_results[_tokenId] != 42, 'Already flipped');
if (stage[_tokenId]==0){
participants.push(_tokenId);
}
stage[_tokenId]++;
requestId = COORDINATOR.requestRandomWords(
s_keyHash,
s_subscriptionId,
requestConfirmations,
callbackGasLimit,
numWords
);
s_players[requestId] = _tokenId;
s_results[_tokenId] = FLIP_IN_PROGRESS;
emit CoinFlipped(requestId, msg.sender);
}
function getResult(uint _tokenId) public view returns (uint result){
return(s_results[_tokenId]);
}
function getCurrentStage(uint _tokenId) public view returns (uint _stage){
return(stage[_tokenId]);
}
// Computing the number of winners at the end of the season
// The owner has to call this function before withdrawing treasury,
// but it can be called by anyone, as a backup.
function computeNumWinners() public
onlyIfSeasonEnded
{
if (numWinners==0){
for (uint i=0;i<participants.length;i++){
if (stage[participants[i]]==6&&s_results[participants[i]]==2 ){
numWinners++;
}
}
}
}
// Player prizes withdrawal function
function withdrawPrize(uint _tokenId) public
onlyIfSeasonEnded
onlyIfNotZero(numWinners)
onlyIfHoldsToken(_tokenId)
onlyIfWon(_tokenId)
whenNotPaused
nonReentrant
{
(bool sent, ) = msg.sender.call{value: address(this).balance*7/(10*numWinners)}("");
require(sent, "Failed to send Ether");
}
// Treasury withdrawal function
// Forces the owner to allow player prizes withdrawal before withdrawing the treasury
function withdrawTreasury() public
onlyOwner
onlyIfSeasonEnded
whenNotPaused
nonReentrant
{ computeNumWinners();
(bool sent, ) = msg.sender.call{value: address(this).balance*3/10}("");
require(sent, "Failed to send Ether");
}
// Remaining balance withdrawal function
// If there are no winners at the end of season, after calling computeNumWinners(),
// the remaining balance is withdrawn by the owner
function withdrawRemainingBalance() public
onlyOwner
onlyIfSeasonEnded
whenNotPaused
nonReentrant
{ computeNumWinners();
if (numWinners==0){
(bool sent, ) = msg.sender.call{value: address(this).balance}("");
require(sent, "Failed to send Ether");}
}
// @dev Toggle pause boolean
function togglePause() external onlyOwner {
if (paused()) {_unpause();}
else _pause();
}
/// @dev Receive function
receive() external payable {
}
/// @dev Fallback function. We check data length in fallback functions as a best practice
fallback() external payable {
require(msg.data.length == 0);
}
}

Constructor

The constructor of our contract is implemented in lines 15–31. In addition to setting the value of the VRF parameters, we use it to set the address of the NFT contract gating our game, the fee paid by players at each level, and the start and end date of the current season, in lines 27–30.

Design patterns

In ...