Welcome to part two of the coin flip game series!

In this tutorial, we will guide you through setting up a Hardhat project and deploying a CoinFlip smart contract on the blockchain. Imagine flipping a coin but with Ethereum's robust blockchain technology as your playground. You'll get hands-on with Hardhat, a favourite tool among Ethereum devs for its ease in developing, testing, and deploying smart contracts.

To check out the previous guide on setting up email onboarding with Particle Auth and Biconomy, click here.

Prerequisites

Before you begin, ensure you have the following prerequisites installed:

  • Node.js and npm are installed on your machine.

  • Hardhat: Install Hardhat globally using the command npm install -g hardhat.

Tutorial: Building and Deploying a Coin Flip Game with Hardhat

1

Set Up the Hardhat Project

Initialize a new project: Create a new directory for your project and run the following commands:

mkdir coinflipgame
cd coinflipgame
npx hardhat init
1.1

Follow the prompts to create the Project. Open the project in your favourite code editor and rename the default Lock.sol file to CoinFlip.sol Your project directory will look like this:

/coinflipgame
/contracts
CoinFlip.sol
/scripts
deploy.js
hardhat.config.js
package.json
2

Install Dependencies

OpenZeppelin is an open-source framework to build secure smart contracts. OpenZeppelin provides a complete suite of security products and audit services to build, manage, and inspect all aspects of software development and operations for decentralized applications.

npm install @openzeppelin/contracts
3

Modify CoinFlip.sol

Open the CoinFlip.sol file and delete the code. Copy and paste the code below in the CoinFlip.sol file.

solidity
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/utils/Strings.sol";

contract CoinFlip {
    uint256 public totalMatches;
    uint256 public lifetimeValue;
    uint256 public minimumAmount = 0.001 ether;

    mapping(uint256 => Match) public matches;

    struct Match {
        address player1;
        address player2;
        uint256 bet1;
        uint256 bet2;
        bool complete;
        uint256 result;
    }

    function createMatch() public payable returns (uint256) {
        require(msg.value > minimumAmount, "You must send some ether to create a match");
        matches[totalMatches].player1 = msg.sender;
        matches[totalMatches].bet1 = msg.value;
        totalMatches++;
        lifetimeValue += msg.value;
        return totalMatches - 1;
    }

    function joinMatch(uint matchId) public payable returns (string memory) {
        require(matchId < totalMatches, "Invalid match ID");
        require(
            matches[matchId].complete == false,
            "This match is already complete"
        );
        require(
            msg.value == matches[matchId].bet1,
            "You must send the same amount of ether as the bet1 value"
        );
        matches[matchId].player2 = msg.sender;
        matches[matchId].bet2 = msg.value;
        lifetimeValue += msg.value;
        return flipCoin(matchId);
    }

    function flipCoin(uint matchId) private returns (string memory) {
        require(matchId < totalMatches, "Invalid match ID");
        uint randomNumber = uint(block.timestamp) % 2;
        if (randomNumber == 0) {
            payable(matches[matchId].player1).transfer(
                matches[matchId].bet1 + matches[matchId].bet2
            );
            matches[matchId].complete = true;
            matches[matchId].result = 1;
            return "The coin came up heads. player 1 wins";
        }
        else {
            payable(matches[matchId].player2).transfer(
                matches[matchId].bet1 + matches[matchId].bet2
            );
            matches[matchId].complete = true;
            matches[matchId].result = 2;
            return "The coin came up tails, player 2 wins";
        }
    }
}

We will walk through an explanation of the smart contract code in the section below the tutorial.


4

Hardhat Configuration

We will deploy the Smart Contract to the Polygon Mumbai Testnet. Edit the hardhat.config.js file to include the necessary imports. This will also include setting up a networks section where you will parse a node provider that will aid in deploying the smart contract and a private key for paying gas for deploying the smart contract.

require("@nomiclabs/hardhat-waffle");
require("@openzeppelin/hardhat-upgrades");

module.exports = {
  solidity: "0.8.20",
networks: {
    mumbai: {
      url: `https://mumbai.infura.io/v3/${INFURA_API_KEY}`,
      accounts: [EOA_PRIVATE_KEY]
    }
  }
};
5

Deployment Script

Open the deploy.js inside the scripts directory and replace the script with the following code:

const { ethers } = require("hardhat");

async function main() {
  const CoinFlip = await ethers.getContractFactory("CoinFlip");
  const coinFlip = await CoinFlip.deploy();
  console.log("CoinFlip deployed to:", coinFlip.address);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });
5.1

Run the deployment script using the following command:

The address to which the smart contract is deployed will be logged into the console.

npx hardhat run scripts/deploy.js --network mumbai
6

Interacting with the Contract

You can interact with the deployed contract using Hardhat's console. Add the following lines to the end of deploy.js:

console.log("CoinFlip deployed to:", coinFlip.address);

// Interact with the contract
const totalMatches = await coinFlip.totalMatches();
console.log("Total Matches:", totalMatches.toNumber());
6.1

Run the script again to deploy and interact:

npx hardhat run scripts/deploy.js --network mumbai

Understanding the CoinFlip Smart Contract

Here’s a break down of the various components of the CoinFlip.sol contract:

Contract Declaration and Importing Libraries

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/utils/Strings.sol";
  • SPDX-License-Identifier: Identifies the license under which the contract's source code is released.

  • pragma solidity ^0.8.20: Specifies the version of the Solidity compiler.

  • import "@openzeppelin/contracts/utils/Strings.sol": Imports the Strings library from OpenZeppelin Contracts for string manipulation.

Contract Definition and State Variables

solidity
contract CoinFlip {
    uint256 public totalMatches;
    uint256 public lifetimeValue;
    uint256 public minimumAmount = 0.001 ether;

    // Make all public matches accessible based on a match ID
    mapping(uint256 => Match) public matches;
  • totalMatches: Public variable tracking the total number of matches played.

  • lifetimeValue: Public variable tracking the total amount of ether sent in all matches.

  • minimumAmount: Minimum amount of ether required to create a match.

  • matches: Mapping to store match details based on match IDs.

Match Struct Definition

solidity
    struct Match {
        address player1;
        address player2;
        uint256 bet1;
        uint256 bet2;
        bool complete;
        uint256 result;
    }
  • Match Struct: Struct to store details of each match.

createMatch Function

solidity
    function createMatch() public payable returns (uint256) {
        // Require that the player has sent some ether
        require(msg.value > minimumAmount, "You must send some ether to create a match");
        // Set match details for player1
        matches[totalMatches].player1 = msg.sender;
        matches[totalMatches].bet1 = msg.value;

        // Increment counters
        totalMatches++;
        lifetimeValue += msg.value;
        // Return the match ID
        return totalMatches - 1;
    }
  • createMatch Function: Allows a player to create a new match by sending ether.

joinMatch Function

solidity
    function joinMatch(uint matchId) public payable returns (string memory) {
        // Require that the match ID is valid
        require(matchId < totalMatches, "Invalid match ID");
        // Require that the match is not already complete
        require(matches[matchId].complete == false, "This match is already complete");
        // Require that the player has sent an amount of ether equal to the bet1 value
        require(msg.value == matches[matchId].bet1, "You must send the same amount of ether as the bet1 value");

        // Set match details for player2
        matches[matchId].player2 = msg.sender;
        matches[matchId].bet2 = msg.value;

        // Increment lifetimeValue counter
        lifetimeValue += msg.value;
        // Flip the coin and determine the winner
        return flipCoin(matchId);
    }
  • joinMatch Function: Allows a player to join an existing match by sending the same amount of ether as player1. It also automatically calls the flipCoin method and selects a winner, thus completing the gameplay.

flipCoin Function

solidity
    function flipCoin(uint matchId) private returns (string memory) {
        // Require that the match ID is valid
        require(matchId < totalMatches, "Invalid match ID");

        // Generate a random number between 0 and 1
        uint randomNumber = uint(block.timestamp) % 2;

        // Determine the winner based on the random number
        if (randomNumber == 0) {
            // Player1 wins
            // Transfer the ether to player1
            payable(matches[matchId].player1).transfer(matches[matchId].bet1 + matches[matchId].bet2);
            matches[matchId].complete = true;
            matches[matchId].result = 1;
            return "The coin came up heads. Player 1 wins";
        } else {
            // Player2 wins
            // Transfer the ether to player2
            payable(matches
[matchId].player2).transfer(matches[matchId].bet1 + matches[matchId].bet2);
            matches[matchId].complete = true;
            matches[matchId].result = 2;
            return "The coin came up tails. Player 2 wins";
        }
    }
}
  • flipCoin Function: Simulates a coin flip by generating a random number (0 or 1) using the current timestamp.

    • If the number is 0, Player1 wins. The ether is transferred to Player1, and the match is marked as complete.

    • If the number is 1, Player2 wins. The ether is transferred to Player2, and the match is marked as complete.

This concludes the breakdown of the CoinFlip.sol contract. Each section of the contract serves a specific purpose, from initializing matches to determining the winner through a simulated coin flip.

Conclusion

Congrats for making it through this tutorial! You've learned how to set up a Hardhat project, deploy a smart contract, and understand the core components of the provided CoinFlip.sol code. By breaking down the code into sections, we've covered the contract definition, state variables, functions for creating and joining matches, and the coin flip logic. Happy building!