Ronin: Make a game's smart contract
Introduction to Ronin
Ronin, an Ethereum Virtual Machine (EVM) compatible blockchain, is purpose-built to serve the unique needs of the gaming industry. Developed by Sky Mavis, the creators of Axie Infinity, Ronin stands out for its ability to support large-scale online games, specifically in the realm of Web3 gaming.
Key technical features of Ronin
-
Optimized for gaming: Ronin is designed to streamline the gaming experience by removing the complexities commonly found in other blockchains. This results in a platform that is efficient, with minimal spam and optimized uptime for games.
-
Security enhancements: in response to security challenges faced in the past, Ronin has undergone extensive security overhauls. These include rigorous internal security protocols, comprehensive code reviews, and architecture audits to ensure robust security measures are in place.
Consensus mechanisms in Ronin
-
Proof-of-authority (PoA) — Ronin initially utilized the PoA consensus mechanism. In this system, a select group of validators, trusted for their expertise and reputation, were responsible for maintaining the network. This approach facilitated faster transaction speeds and lower fees due to its energy-efficient design.
-
Transition to delegated-proof-of-stake (DPoS) — Ronin integrated the DPoS consensus mechanism to advance decentralization. This allowed broader participation in the network's maintenance, where anyone holding enough RON tokens could become a validator. While retaining the benefits of PoA, such as efficiency and low costs, this shift markedly improved the blockchain's decentralization.
Check out the Ronin docs to learn more.
Since Ronin is designed to develop games, today, we'll make a smart contract that can handle for games.
In this tutorial, you will:
- Deploy a Ronin node on the Saigon Testnet.
- Create the game smart contract with Hardhat.
- Deploy the smart contract with Hardhat
Prerequisites
- Chainstack account to deploy a Ronin node.
- node.js as the JavaScript framework.
- Hardhat to create, deploy, and interact with contracts.
Overview
This tutorial guides you through the process of developing a smart contract for a blockchain-based game specifically tailored for deployment on the Ronin Saigon Testnet. We aim to develop a versatile and robust smart contract capable of managing the core game logic on the blockchain.
The game itself is designed to be played on the client side, typically within a web browser. Players will have the ability to connect their Ronin wallet and deposit a specified fee to begin gameplay. The smart contract plays a pivotal role in the gaming experience: it securely handles the deposit and, depending on the game's outcome, executes the payout.
To get from zero to a working game, do the following:
- With Chainstack, create a public chain project.
- With Chainstack, join the Ronin Saigon Testnet.
- With Chainstack, access your Ronin node endpoint.
- With Hardhat, create and set up the project.
Step-by-step
Create a public chain project
See Create a project.
Join the Ronin Saigon testnet
Get your Ronin node endpoint
See View node access and credentials.
Fund your wallet
Before diving into the game project, make sure to top up your wallet with testnet RON.
- Install the Ronin wallet.
- Use the Ronin faucet.
Install Hardhat
See Installing Hardhat.
Install dotenv
Install the dotenv
package to securely manage environment variables.
npm i dotenv
Create a Hardhat project
Create a new directory for your project, then run the following from a terminal:
npx hardhat
This will launch the Hardhat CLI, prompting you to choose a starter project. For this project, answer yes to the following:
Create a JavaScript project
Do you want to install this sample project's dependencies with npm (hardhat @nomicfoundation/hardhat-toolbox)?
Edit the Hardhat configuration file
You will find a file named hardhat.config.js
in the root directory. This file configures various settings for your Hardhat projects, such as the network you want to deploy your contracts on, the compilers you want to use, and the plugins you want to enable.
Delete the default code in the file and replace it with the following:
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
const RONIN_SAIGON_CHAINSTACK = process.env.RONIN_SAIGON_CHAINSTACK;
const PRIVATE_KEY = process.env.RONIN_PRIVATE_KEY;
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
networks: {
saigon: {
url: RONIN_SAIGON_CHAINSTACK,
accounts: [PRIVATE_KEY],
},
},
solidity: "0.8.23",
};
Let's break down what each part of the file does:
require("@nomicfoundation/hardhat-toolbox");
imports the Hardhat Toolbox plugin, which provides several useful tools and utilities for Hardhat projects.require("dotenv").config();
loads environment variables from a.env
file using thedotenv
package.module.exports = { ... }
exports a JavaScript object containing the configuration for the Hardhat project.solidity: "0.8.23",
sets the Solidity compiler version to 0.8.23.networks: { ... }
defines the network configurations for the Hardhat project. In this case, it defines a network calledsaigon
that connects to the Ronin Saigon blockchain network.saigon: { ... }
defines the configuration for thesaigon
network.url: RONIN_SAIGON_CHAINSTACK,
sets the URL for the Saigon network using theRONIN_SAIGON_CHAINSTACK
environment variable.accounts: [PRIVATE_KEY],
sets the accounts for thesaigon
network using thePRIVATE_KEY
environment variable. This will allow the Hardhat project to deploy contracts and interact with the Saigon network using the specified private key.
Create the game-handling smart contract
In the root directory, you will find a directory named contracts
with a sample contract in it. Rename this contract to Game.sol
and replace its code with the following:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
/// @title Game Contract
/// @notice This contract allows players to deposit ETH to play a game and handles game results and payouts.
contract Game {
/// @notice Minimum deposit amount required to play the game
uint256 public constant MINIMUM_DEPOSIT = 1 ether;
/// @notice Maximum deposit amount allowed for the game
uint256 public constant MAXIMUM_DEPOSIT = 2 ether;
/// @notice Mapping to track user deposits
mapping(address => uint256) public deposits;
/// @notice Owner address
address private owner;
/// @notice Sets the contract deployer as the owner
constructor() payable {
owner = msg.sender;
}
/// @notice Ensures that only the owner can call the function
modifier onlyOwner() {
require(msg.sender == owner, "Only the owner can call this function");
_;
}
/// @dev Deposits ETH to the contract to play the game
/// @notice Deposits must be between 1 and 2 Ethers
/// @notice Contract must have enough funds to pay potential winnings
function deposit() public payable {
require(msg.value >= MINIMUM_DEPOSIT && msg.value <= MAXIMUM_DEPOSIT, "Deposit must be between 1 and 2 Ethers");
require(address(this).balance + msg.value >= MAXIMUM_DEPOSIT * 2, "Contract does not have enough funds to cover potential winnings");
deposits[msg.sender] += msg.value;
}
/// @notice Checks if the user has enough deposit to play
/// @param user The address of the user to check
/// @return True if the user has enough deposit, false otherwise
function canPlay(address user) public view returns (bool) {
return deposits[user] >= MINIMUM_DEPOSIT;
}
/// @notice Handles the game result and processes payouts or deposit retention
/// @dev Only callable by the owner
/// @param player The address of the player
/// @param userWon Indicates whether the player won or not
function gameResult(address player, bool userWon) public onlyOwner {
require(canPlay(player), "Player did not deposit enough to play");
uint256 depositAmount = deposits[player];
if (userWon) {
require(address(this).balance >= depositAmount * 2, "Contract does not have enough funds");
payable(player).transfer(depositAmount * 2);
} else {
// Keep the deposit in the contract if AI wins
}
deposits[player] = 0;
}
/// @notice Allows the owner to withdraw all funds from the contract
/// @dev Only callable by the owner
function withdraw() public onlyOwner {
uint256 balance = address(this).balance;
require(balance > 0, "No funds to withdraw");
payable(owner).transfer(balance);
}
// Additional functions can be added here
}
Default logic
Keep in mind that this smart contract is a proof of concept and it should not be used in production as is. A security audit is higly recomended.
Understanding the smart contract
This smart contract, designed for a blockchain-based game, operates on a simple yet effective mechanism. It allows players to deposit RON (or the native currency of the chain you are using) within a specified range—a minimum of 1 ether and a maximum of 2 ethers—to participate in the game. This range ensures fair play and manages the contract's ability to pay winnings. The contract tracks these deposits against each player's address, maintaining a balance reflecting their current game stake.
Let's break down each element and function of the smart contract.
Contract overview
- Contract name —
Game
- Purpose — this contract allows players to deposit ETH to play a game, and it manages the game results and payouts.
Constants
-
MINIMUM_DEPOSIT
- Type —
uint256
(unsigned integer) - Purpose — specifies the minimum amount of ether a player must deposit to play the game.
- Value — 1 ether
- Type —
-
MAXIMUM_DEPOSIT
- Type —
uint256
- Purpose — indicates the maximum amount of ether a player can deposit.
- Value — 2 ethers
- Type —
State variables
-
deposits
- Type —
mapping(address => uint256)
- Purpose — keeps track of the amount of ether each player (address) has deposited.
- Type —
-
owner
- Type —
address
- Purpose — stores the address of the contract owner, who has special privileges (like executing the
gameResult
andwithdraw
functions).
- Type —
Constructor
- Functionality — sets the deployer of the contract as the
owner
.
Modifiers
- onlyOwner
- Purpose — restricts the execution of certain functions to only the contract owner.
Functions
-
deposit
- Access — public
- Payment Type — payable (can receive ether)
- Purpose — allows players to deposit ETH within the allowed range (1 to 2 ethers). It also ensures the contract has enough funds to cover potential winnings.
- Logic — updates the
deposits
mapping with the player's deposit amount.
-
canPlay
- Access — public
- Purpose — checks if a user has deposited enough ETH to play the game.
- Parameters —
user
(address of the player) - Returns —
bool
(True
if the player has enough deposit,False
otherwise)
-
gameResult
- Access — public, but restricted to
onlyOwner
- Purpose — processes the outcome of the game. It either pays out double the deposit to the player if they win or retains the deposit in the contract if they lose.
- Parameters:
player
— address of the playeruserWon
— boolean indicating whether the player won or not
- Logic — if the player wins, it transfers double the deposit amount to them and resets their deposit to zero. If the player loses, just resets their deposit.
- Access — public, but restricted to
-
withdraw
- Access — public, but restricted to
onlyOwner
- Purpose — allows the owner to withdraw all ETH stored in the contract.
- Logic — transfers the entire contract balance to the owner's address.
- Access — public, but restricted to
TL;DR
- Players can participate in the game by depositing a certain amount of ether (between 1 and 2 ethers).
- The contract ensures fairness and readiness for payouts before accepting deposits.
- After the game ends, the result is communicated to the contract. Winners receive double their stake, while the stakes of those who lose remain with the contract.
- Only the contract owner can process game results and withdraw funds from the contract, ensuring controlled and secure operations.
Environment variables
In the root directory of the Hardat project, create a .env
file for your endpoint and private keys:
RONIN_SAIGON_CHAINSTACK="YOUR_CHAINSTACK_RONIN_ENDPOINT"
RONIN_PRIVATE_KEY="YOUR_RONIN_WALLET_PRIVATE_KEY"
Create the deploying script
In the scripts
directory inside the root of your project, you will find a file named deploy.js
. Replace its content with the following:
const hre = require("hardhat");
async function main() {
console.log("Deploying contract...");
const GameContract = await hre.ethers.deployContract("Game");
// Deploy the contract.
await GameContract.waitForDeployment();
console.log("Contract deployed to:", GameContract.target);
const roninAddress = GameContract.target.substring(2);
console.log(
`Find the contract at https://saigon-app.roninchain.com/address/ronin:${roninAddress}`
);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
This code is a script that deploys a Game
smart contract.
Here's a breakdown of what each part of the script does:
-
Import Hardhat runtime environment (HRE):
const hre = require("hardhat");
This line imports the Hardhat runtime environment, which provides various utilities for working with Ethereum, such as deploying contracts.
-
Main function:
async function main() { console.log("Deploying contract..."); const GameContract = await hre.ethers.deployContract("Game"); // Deploy the contract. await GameContract.waitForDeployment(); console.log("Contract deployed to:", GameContract.target); const roninAddress = GameContract.target.substring(2); console.log( `Find the contract at https://saigon-app.roninchain.com/address/ronin:${roninAddress}` ); }
The
main
function is an asynchronous function where the main logic of the script is executed. -
Starting deployment process:
console.log("Deploying contract...");
This line prints a message to the console indicating that the deployment process is starting.
-
Deploying the contract:
const GameContract = await hre.ethers.deployContract("Game");
This line uses Hardhat's
ethers
plugin to deploy a contract namedGame
. Theawait
keyword is used becausedeployContract
is an asynchronous operation. -
Waiting for deployment completion:
await GameContract.waitForDeployment();
This line waits for the contract deployment to be completed. It's important to wait for the deployment to finish before proceeding.
-
Logging the deployed contract address:
console.log("Contract deployed to:", GameContract.target);
After the contract is successfully deployed, this line logs the address of the deployed contract to the console.
-
Removing the
0x
prefix from the address:const roninAddress = GameContract.target.substring(2);
This line removes the
0x
prefix from the Ethereum address using thesubstring
method. As the Ronin explorer uses this format:https://saigon-app.roninchain.com/address/ronin:49a1EA88e5F81850DE30Dc038c1d08028ecFc9b5
. -
Providing the contract address on Ronin explorer:
console.log( `Find the contract at https://saigon-app.roninchain.com/address/ronin:${roninAddress}` );
This line constructs a URL to view the contract on the Ronin blockchain explorer and logs it to the console. It appends the modified address to the explorer's URL.
-
Error handling:
main().catch((error) => { console.error(error); process.exitCode = 1; });
This part of the script ensures that if any errors occur during the execution of the
main
function, they are caught and printed to the console, and the script exits with an error code.
Deploy the smart contract
To deploy the Game
contract, run the following command in the terminal:
npx hardhat run --network saigon scripts/deploy.js
This will deploy the contract on Ronin Saigon Testnet displaying something similar to the following:
Deploying contract...
Contract deployed to: 0x49a1EA88e5F81850DE30Dc038c1d08028ecFc9b5
Find the contract at https://saigon-app.roninchain.com/address/ronin:49a1EA88e5F81850DE30Dc038c1d08028ecFc9b5
You can now find the contract on the Saigon Explorer. You can also find the transactions from the Ronin wallet.
Next steps
Now you have a working smart contract deployed, the next step will be to build a front end with your game and wallet interaction.
Conclusion
In this comprehensive tutorial, we journeyed through the exciting world of blockchain-based game development on the Ronin blockchain, an EVM-compatible platform optimized for gaming. From setting up a node on the Ronin Saigon testnet to deploying a game-centric smart contract using Hardhat, we've laid down a robust foundation for blockchain game developers.
The key takeaway from this tutorial is the seamless integration of blockchain technology into gaming. By deploying a smart contract on Ronin, we have created a system that enhances the gaming experience and ensures secure and fair gameplay. The ability to handle in-game financial transactions directly on the blockchain, including player deposits and payouts, showcases the power and versatility of smart contracts in gaming environments.
About the author
Updated 8 months ago