Proof of Snake - Building the Classic on Ethereum Using Infura, Part I

How to build a classic game on Ethereum using Infura, with ETH and NFT rewards to high scorers.

Proof of Snake - Building the Classic on Ethereum Using Infura, Part I

By: Sean Sing and Robbie Kruszynski

Edited by: Tom Hay, Chris Anatalio and Clarissa Watson

Building Ethereum Enabled Snake

Proof-of-Snake is based on the classic snake game with a twist; the high scorer earns Ether (ETH) from every attempt to beat the high score. In addition, a NFT is rewarded whenever a player beats the current high score.

This rewards system is enabled by tracking the high score and token ownership with smart contracts that run on Ethereum. The Proof-of-Snake project currently lives on the Rinkeby testnet. You will need to have ETH on the Rinkeby network to play. You can request test ETH from this Rinkeby faucet for free.

Proof-of-Snake was the final project submission for Sean Sing from the 2020 ConsenSys Developer Bootcamp. As Sean shares:

“The idea came to me when my internet connection was disrupted and I spent some time playing the Chrome browser’s Dinosaur Game. I had made it quite far into the game by my fifth attempt; and when my T-Rex avatar crashed into a pterodactyl, I started to wonder how someone could be officially recognized as the top scorer.

If the Dinosaur Game was hooked up to a smart contract, then the high scores could be official, accessible globally and its legacy would live on virtually, forever. This goal became the inspiration for the Proof-of-Snake smart contract. I also added token and game mechanics to incentivize and motivate players to become the top player of Proof-of-Snake!”

Understanding the Game Dynamics: How Does Proof-of-Snake Work?

Every time a player pays the fee to play the game, the fee is split between the game creator and the current high scorer.

The smart contract has a mapping that tracks the balance of the current high scorer. This is represented as the “Proof-of-Snake Pot”.

Players who beat the high score are also awarded a “Proof-of-Snake” NFT which can be displayed, sold or redeemed for rewards.

Smart Contract Walkthrough

Check out the smart contract repository to follow along.

Note: Please keep in mind that this repository was intended as a proof of concept and does not represent a secure, audited smart contract ready for mainnet. This article is part of a series which will identify the security vulnerabilities present in this smart contract and how to remediate those vulnerabilities in future articles.  

To elevate this smart contract to production–level code, we would have to update the code to the latest version of Solidity and standards, perform a peer review, add automated testing, perform static analysis using an automated service such as Diligence and also have the contract formally audited by a respected industry auditor.

This article is meant to highlight a developer’s journey as a student from Consensys Academy, through the steps required to be able to release secure, mainnet-ready code.  

Solidity Compiler Version Declaration and Library Import Statements

pragma solidity >=0.6.0 <0.7.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
Solidity version declaration and imports for OpenZeppelin Contracts

The contact is initialized with the name “ProofOfSnake” and inherits the imported Open Zeppelin ERC721 contract.

contract ProofOfSnake is ERC721 {

...

}
Contract declaration that extends the OpenZeppelin ERC721 contract

Variable Declaration

The contract then declares a few key variables:.

  • Two uint256 variables:
  • The highScore
  • The gameFee.
  • The addresses for the current high scorer: currentLeader
  • The address of the owner: owner
  • A mapping with an address key and uint value pair called potBalance to track the earnings of the current high scorer and past winners
  • A boolean called stopped initially set to false
using SafeMath for uint256
uint256 public highScore;
uint256 public gameFee = 0.1 ether;
address public currentLeader;
address public owner;
mapping(address => uint256) public potBalance;
bool public stopped = false;
Declaring variables for use in the smart contract

Modifiers

Next, let’s define a few modifiers which are reusable functions:

  • The onlyOwner modifier incorporates the access restriction design pattern where only the contract’s owner is allowed access to certain functions
  • The stopInEmergency and onlyInEmergency modifiers check the “stopped” variable. If true, the function’s downstream logic will fail to execute.

This approach is part of the circuit breaker design pattern for smart contract safety.  The emergency function swaps the value of the “stopped” boolean. Only the owner can call this function. If a bug was found, the contract owner could stop players from continuing to pay the game fee until an updated, fixed version of the contract is deployed.

//@dev Design pattern: Restricting access   
modifier onlyOwner {
	require(msg.sender == owner);
	_;
}

//@dev Design pattern: Circuit breaker to stop players from playing during emergencies and allows currentLeader to withdraw his earnings.
modifier stopInEmergency {
	require(!stopped);
	_;
}

modifier onlyInEmergency {
	require(stopped);
	_;
}

function emergency() public onlyOwner {
	if (stopped == false) {
		stopped = true;
	} 
	else {
		stopped = false;
	}
}
Reusable modifier functions

Constructor

The constructor function is executed during the deployment of the smart contract:

  • “owner” and “currentLeader” are set to the contract owner’s address,
  • The mapping to “potBalance” is set to 0
  • Initial “highScore” is set to 2

The contract inherits from Open Zeppelin’s ERC721 contract, so the arguments for the ERC721’s contract’s construction functions are required (the token’s name and symbol).


constructor() public ERC721("Proof-of-Snake High Scorer", "POSHS") { 
	owner = msg.sender;
	currentLeader = msg.sender;
	potBalance[owner] = 0;
	highScore = 2;
}
Smart Contract Constructor

Main Functions

The smart contract has three main functions.

  1. playGame()
  2. newLeader()
  3. withdrawEarnings()

playGame() Function

  • public and payable
  • Receives and transfers the game fee in Ether
  • Adopts the “stopInEmergency” modifier
  • Declares a require statement which checks if the “msg.value” (the amount of ETH sent) meets the gameFee that was set. If the amount sent was less than gameFee, the transaction reverts
  • Player’s potBalance mapping to 0
  • The contract owner and currentLeader’s balances in the “potBalance” is increased by half the amount of the “msg.value” received
//@notice Called when the user clicks to the Play button on the front end. Fee is split to the contract owner and the currentLeader, stored in potBalance mapping.   
function playGame() public payable stopInEmergency {
	//@notice Ensures minimum value to play the game is met.
	require(msg.value >= gameFee, "Minimum game fee is not met.");
	potBalance[msg.sender] = 0;
    
	//@dev Safety feature: use SafeMath's function to add half the fee to currentLeader's potBalance mapping
	//@dev Safety feature: use SafeMath's functions to add remaining fee to owner's potBalance mapping
	potBalance[owner] = potBalance[owner].add(msg.value.div(2));
	potBalance[currentLeader] = potBalance[currentLeader].add(
	msg.value.sub(msg.value.div(2))
    );
}
Play Game function

newLeader() function

  • Accepts a uint256 value, the verified score(validated by the front-end)
  • A require statement checks that the potBalance > 0
  • Ensures that the player has at least paid the fee to play the game once, otherwise it reverts the transaction with an error.
  • If the checks pass:
  • Player has played Proof-of-Snake before and has achieved a new high score
  • The “currentLeader” address is updated to “msg.sender” (the dApp initiates a transaction that the player signs)
  • The “highScore” is set to the _score argument received from the front-end
  • Then increases the “totalSupply” of this token by 1 and mints a Proof-of-Snake High Scorer NFT with the player’s address. The high score could also be added as an input.
//@notice Called when a new high score is achieved.
//@param Takes the current high score uint and stores in highScore.
function newLeader(uint256 _score) public stopInEmergency {
	//@notice Requires that msg.sender has at least paid to play the game once
	require(potBalance[msg.sender] >= 0, "Player has not played before.");
	//@notice Updates the current leader to msg.sender
	currentLeader = msg.sender;
	//@notice Updates the current high score
	highScore = _score;
    
	//Mint a POSHS token
	uint256 _tokenId = totalSupply().add(1);
	_mint(msg.sender, _tokenId);
}
New Leader function

withdrawEarnings() Function

  • Previous high scorers of Proof-of-Snake can call this function to withdraw their earnings
  • High scorers can only withdraw their earnings after they have been dethroned
  • A require statement checks that the “msg.sender” is not the “currentLeader” and has a “potBalance” of greater than 0 Eth.
  • If the checks fails, the transaction is reverted with an error message
  • If the checks pass, the function sets msg.sender’s potBalance to 0 and transfers the balance to “msg.sender”
    //@notice Called when previous high scorer wants to withdraw his earnings.
    //@dev Design Pattern: Withdrawal pattern rather than direct distribution.
    //@dev Safety feature: Mitigates Denial of Service with Failed Call (SWC-113).
function withdrawEarnings() public stopInEmergency {
	//@notice Ensures that withdrawer's high score has been beatan before being able to withdraw. This maintains the intended economics.
	require(
		msg.sender != currentLeader && potBalance[msg.sender] > 0,
		"Leader's high score has not been beaten or no earnings collected yet."
	);

	potBalance[msg.sender] = 0;

	//@notice Empty out balance of msg.sender and transfer to msg.sender.
	msg.sender.transfer(potBalance[msg.sender]);
}
Withdraw Earnings

Notice anything wrong?

The code first wipes out the sender's balance and then transfers them 0. The code should use a temporary variable to store the value, instead of operating directly on the source maps.

The code should store the value in a temporary variable before resetting the pot to 0 and transferring the balance. You can look more into this common pattern in the Solidity documentation.

Now, let’s deploy the smart contract using Infura!

Note: We won’t be covering automated testing of the smart contract in this article. Check out the Truffle Suite to learn more about testing your smart contracts prior to deployment.  We will also cover implementing testing in later articles in this series - follow our blog to stay updated!

Deploying our Smart Contract

First, let’s deploy Proof-of-Snake locally using a tool such as Ganache. Update your “truffle-config.js” file for local deployment. See more details on truffle configuration.

truffle-config.js

  ...

	networks: {
		develop: {
			host: "127.0.0.1",
			port: 8545,
			network_id: "*", // match any network
			websockets: true,
		}
	}

...
Declare network for local deployment

Signing Transactions


Since we are using public nodes, we will need to sign our transactions locally. The declaration of the “HDWalletProvider” variable at the top of our “truffle-config.js” file allows us to do this.

Note: This declaration is possible post-install with the following command

$: npm install @truffle/hdwallet-provider 

Check out the HDWalletProvider NPM repo for more information.

const path = require("path");
const HDWalletProvider = require("@truffle/hdwallet-provider");

const mnemonic = "";

module.exports = {
  // See <http://truffleframework.com/docs/advanced/configuration>
  // to customize your Truffle configuration!
  contracts_build_directory: path.join(__dirname, "client/src/contracts"),
  networks: {
    develop: {
      host: "127.0.0.1",
      port: 8545,
      network_id: "*", // match any network
      websockets: true,
    },
    rinkeby: {
      provider: function () {
        return new HDWalletProvider(
          mnemonic,
          "https://rinkeby.infura.io/v3/YOUR_PROJECT_ID"
        );
      },
      network_id: 4,
    },
  },
  compilers: {
    solc: {
      version: "^0.6.0", // A version or constraint - Ex. "^0.5.0"
      // Can also be set to "native" to use a native solc

      parser: "solcjs", // Leverages solc-js purely for speedy parsing
    },
  },
};
Full truffle-config.js file

We now have the ability to sign transactions.

Generating your own Mnemonic

Note: HDwalletProvider will automatically use the address of the first address that's generated from the mnemonic. If you pass in a specific index, it'll use that address instead; otherwise, you could supply your own mnemonic. An example of this below:

In your terminal, enter:

$: truffle console
Terminal command for Truffle


Note: If your network is not correctly configured between Ganache and your truffle-config.js file, this command will fail.

Next, update the following lines to create your wallet:

const HDWalletProvider = require('@truffle/hdwallet-provider');
Create your wallet


Enter your generated mnemonic:

const mnemonic = '12 words here';
Your generated mnemonic phrase

Define your wallet provider:

const wallet = new HDWalletProvider(mnemonic,"http://localhost:8545");
Wallet Provider declaration

Now, when you execute the terminal command:

$: wallet
Terminal command to view wallet


You’ll see a list of accounts/addresses:



By default, the first address in the array will be the account you need to fund with test ETH, in order to successfully deploy.

Using your Generated Mnemonic

We will now take the same mnemonic used and create a variable adding our mnemonic within the string.

const path = require("path");
const HDWalletProvider = require("@truffle/hdwallet-provider");

const mnemonic = "Your mnemonic goes here";
Using your generated mnemonic


Note: For the sake of this project example, we are placing our mnemonic and projectID inside of the truffle.config file. In practice, you want to make sure you abstract this information away and use an .env file or, if preferred, a json file that is added to the .gitignore file.

You do not want to leak this information in your public Github repository.

Infura Project Set-Up



Next, let’s configure the provider to connect to the test network, by using your Infura project ID.  Please review the Infura Getting Started docs for details on how to get this.



Now, in our “truffle-config.js” file, we will declare which testnet we are using.

We are using the Rinkeby testnet. Ensure that we are pulling in our declared HDWalletProvider. (The HDWalletProvider is a wrapped instance in a functional closure to ensure that only one network is connected.)

truffle-config.js

...

	rinkeby: {
      provider: function () {
        return new HDWalletProvider(
          mnemonic,
          "https://rinkeby.infura.io/v3/YOUR_PROJECT_ID"
        );
      },
      network_id: 4,
    }
...
Rinkeby testnet config

Deploy to Rinkeby

Now, let’s deploy our project to the testnet.

$: truffle migrate --network rinkeby
Terminal command to deploy to Rinkeby

Upon successful deployment to Rinkeby, we can now integrate our smart contract with our front-end to build out a full-stack dApp.

This contract is currently deployed here:

https://rinkeby.etherscan.io/address/0x92A326067ed1413c6a36D1D1f639feAe44a860cb

The full dApp is available at: https://proof-of-snake.vercel.app/

You may notice that the high score has been set to 8,888,888, which indicates that the deployed contract on testnet has been exploited. We will walk through how this contract was exploited and how to prevent this, in future articles in this series. Thanks to Larry Yang for identifying this exploit.

Reference this transaction to see how the exploit occurred:  https://rinkeby.etherscan.io/tx/0x6bea63a806ccdb4cf9a689e841da19bfdee902b26f2741fceee120bea4e0352a

Inspired to join the revolutionary world of blockchain? Check out https://consensys.net/careers/ today!

Want to get started on your journey as a Web3 developer? https://infura.io/register

Read more stories like this at: https://blog.infura.io/

Stay up to date with Infura with: The Infura Monthly Newsletter

Follow Infura:

About the Author

My first exposure to Blockchain was during the 2017 Bitcoin bull run, while I was working as a civil engineer. My background was not in Computer Science and it was not until 2019 that I started picking up HTML, CSS, JavaScript and React.

The decentralized application (dApp) Cryptokitties got me wondering how smart contracts were built, which then led to my discovery of Solidity. I also found out that ConsenSys, the company behind the widely used tools for smart contract development, also had a developer bootcamp!

I signed up for the ConsenSys Developer Bootcamp in 2020 and to this day I am still blown away by the amount of value that I gained from it. The course materials were rich and dove deep into many technical aspects of Ethereum. Explanations for key concepts and complex jargon were easy to digest. There were endless links to the articles, research papers, Medium posts, YouTube talks and sample dApps that helped solidify my understanding.

The hidden gem in the program was the heartfelt support from the mentors and leaders of the bootcamp who dedicated their time to office hours and Zoom calls for discussions that ranged from Uniswap walkthroughs, advanced topics like major DeFi hacks, technical guidance, brainstorming and pair programming. It was definitely an exciting, memorable and rewarding three months!

After bootcamp, I joined three hackathons back to back, the Finastra’s Hack to the Future 2020, EthDenver 2021 and the Solana x Serum DeFi Hackathon.

The nerve-wrecking process of job applications resulted in three out of the five applications leading to interview requests. These lead to an offer within a month or two. It was surreal and many friends and family asked how I was able to make a career transition from a traditional engineering role to a blockchain developer.

The blockchain community has been very welcoming and everyone shares a common goal to help this technology evolve and advance. Organic passion and grit is highly valued. Not once was my lack of formal computer science education questioned and everyone was genuinely curious and excited to hear about my journey of how I stumbled upon blockchain and why I dedicated the time and effort to learn the technical skill sets required. It was evident that to them, the technical skills required could be taught and polished so long as the candidate possessed genuine curiosity and the tenacity to learn and grow.

I hope my journey serves as an inspiration for others. I encourage anyone who is interested about blockchain to take that first step while it is still in its infancy. I cannot wait for the day that blockchain changes the world and look forward to working with many other like-minded and talented folks.  Connect with Sean Sing.

Special note: Since my participation in the 2020 Bootcamp, ConsenSys has taken the initiative to launch Basic Training based on feedback from past graduates. It’s a vital resource for students to become better prepared for the Bootcamp and also is a key reference to help them complete their assignments and projects.