GameFi, the fusion of gaming and decentralized finance, is reshaping how games are played and monetized. In this guide, I will walk you through creating a GameFi vault where players can deposit their in-game tokens, earn shares proportional to their deposits, accumulate yield over time, and withdraw their funds with accrued rewards. The techniques are those that are found in traditional finance. This tutorial is ideal for Web3 game developers looking to integrate DeFi mechanics into their games.
Code Walkthrough of the GameFi Vault Contract
Contract Overview:
Our GameFiVault contract is designed to manage deposits, share minting, yield accumulation, and withdrawals for an ERC-20-based in-game currency. Here’s how it works:
Key Components:
ERC-20 Integration: The contract accepts a token that acts as the in-game currency.Tokens and Yield: Players earn tokens based on the value of their deposit, and these shares accumulate yield over time.Withdrawal Mechanism: Players can redeem their shares and receive the equivalent amount of tokens, including their accumulated yield.
Code Explanation:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import “@openzeppelin/contracts/access/Ownable.sol”;
import “@openzeppelin/contracts/token/ERC20/IERC20.sol”;
import “@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol”;
import “@openzeppelin/contracts/utils/ReentrancyGuard.sol”; // Adding Ownable for admin control
contract GameFiVault is ReentrancyGuard, Ownable {
using SafeERC20 for IERC20;
IERC20 public gameToken; // In-game currency (ERC-20 token)
uint256 public totalShares; // Total shares in the vault
uint256 public vaultBalance; // Total balance in the vault (tracked in game tokens)
struct StakerInfo {
uint256 shares; // The number of shares owned by the user
uint256 depositTimestamp; // The timestamp of the last deposit
uint256 accumulatedYield; // Accumulated yield from staking
}
mapping(address => StakerInfo) public stakers; // Maps users to their staking info
event Deposit(address indexed user, uint256 amountDeposited, uint256 sharesMinted);
event Withdrawal(address indexed user, uint256 amountWithdrawn, uint256 sharesBurned);
event YieldPaid(address indexed user, uint256 yieldAmount);
// Constructor to initialize the contract with the game token and call the Ownable constructor
constructor(IERC20 _gameToken) Ownable(msg.sender) {
gameToken = _gameToken; // Assign the in-game currency
}
// Function to deposit tokens and mint shares
// Function to deposit tokens and mint shares
function deposit(uint256 amount) public nonReentrant {
require(amount > 0, “Deposit amount must be greater than zero”);
// Safely transfer tokens from the user to the contract
gameToken.safeTransferFrom(msg.sender, address(this), amount);
// Calculate shares to mint using the equation
uint256 sharesMinted = (totalShares == 0 || vaultBalance == 0) ? amount : (amount * totalShares) / vaultBalance;
require(sharesMinted > 0, “Calculated shares must be greater than zero”);
// Update contract state
totalShares += sharesMinted;
vaultBalance += amount;
StakerInfo storage staker = stakers[msg.sender];
if (staker.shares > 0) {
// Accumulate yield up to this point before adding new shares
staker.accumulatedYield += _calculateYield(msg.sender);
}
staker.shares += sharesMinted;
staker.depositTimestamp = block.timestamp;
emit Deposit(msg.sender, amount, sharesMinted);
}
// Internal function to calculate yield
function _calculateYield(address user) internal view returns (uint256) {
StakerInfo storage staker = stakers[user];
if (staker.shares == 0 || staker.depositTimestamp == 0) {
return 0;
}
uint256 stakingDuration = block.timestamp – staker.depositTimestamp;
uint256 annualYieldRate = 10; // 10% annual yield rate
// Improved precision by rearranging multiplications
uint256 yield = (staker.shares * annualYieldRate * stakingDuration) / (365 days * 100);
return yield + staker.accumulatedYield;
}
// Add this function to GameFiVault for testing purposes only
function calculateYieldForUser(address user) public view returns (uint256) {
return _calculateYield(user);
}
function withdraw(uint256 shares) public nonReentrant {
require(shares > 0, “Shares to withdraw must be greater than zero”);
StakerInfo storage staker = stakers[msg.sender];
require(staker.shares >= shares, “Insufficient shares to withdraw”);
uint256 amountToWithdraw = (shares * vaultBalance) / totalShares;
uint256 yield = _calculateYield(msg.sender);
// Get the actual token balance of the contract
uint256 actualBalance = gameToken.balanceOf(address(this));
require(actualBalance >= amountToWithdraw + yield, “Insufficient vault balance for withdrawal”);
// Update contract state before transferring funds
totalShares -= shares;
vaultBalance -= amountToWithdraw; // Deduct only the principal amount
staker.shares -= shares;
staker.accumulatedYield = 0; // Reset accumulated yield after withdrawal
if (staker.shares == 0) {
staker.depositTimestamp = 0; // Clear timestamp if no shares remain
}
// Safely transfer tokens to the user
gameToken.safeTransfer(msg.sender, amountToWithdraw + yield);
emit Withdrawal(msg.sender, amountToWithdraw, shares);
emit YieldPaid(msg.sender, yield);
}
}
Explanation of the Key Equation:
The equation used to calculate shares minted during a deposit is:
s: The number of shares, i.e. game tokens.a: The amount the user deposits.T: The total number of shares/tokens in the vault before the deposit.B: The balance of the vault before the deposit.
Derivation: This equation ensures that the proportion of new shares minted is consistent with the value of the user’s deposit relative to the total value already in the vault. This mechanism is vital for maintaining fair distribution and aligning the value of shares/game tokens with the underlying assets in the vault.
How Yield Accumulation Works:
Annual Yield Rate: The annualYieldRate variable represents the percentage return users receive over a year (e.g., 10%).Yield Calculation: The yield is calculated based on the duration the tokens are staked using the formula:
This formula calculates the yield accumulated by a user based on the number of shares they hold and the time they have staked them.
Critical Features to Note:
Initial Deposit Handling: If the vault is empty (totalShares == 0 or vaultBalance == 0), one token is equivalent to one share.Non-Reentrant: The use of ReentrancyGuard prevents reentrancy attacks, making the contract safer.Share Tracking: The contract maintains a mapping stakers to store user-specific data, including shares, deposit timestamps, and accumulated yield.Events: Emitted for Deposit, Withdrawal, and YieldPaid to help with transparency and logging.
Taking this example further:
Add NFT Rewards: Integrate ERC-721 logic to distribute NFTs to top stakers.Governance: Implement a governance model where players can vote on yield rates. Remember to consider potential inflationary problems.Dynamic Yield Rates: Implement logic to change yield rates based on specific criteria (e.g., total value locked).
Conclusion:
The GameFi vault we built in this guide provides a solid starting point for integrating staking and yield accumulation into Web3 games. While this example is implemented in Solidity for Ethereum-compatible blockchains, the math and logic can easily be adapted to other programming languages and blockchain ecosystems. This makes it a flexible and versatile concept for any developer. However, it’s important to note that this is a simplified version and not production-ready code.
It is an initial framework and foundational idea for Web3 game developers who want to explore and integrate GameFi mechanics into their projects. Further enhancements and security considerations are essential for deploying it in real-world games and applications.
Full Source Code == https://github.com/KBryan/GameFi2
Check out more Web3 game dev videos at https://www.youtube.com/@blockchaingamedev
Building a Basic GameFi Vault: A Guide for Web3 Game Developers was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story.