The Gaming Framework: Specialized Libraries

Example: The Asset Trading Library

evire
9 min readJul 12, 2024

Gaming in Web3 represents a significant evolution from traditional gaming paradigms, leveraging blockchain technology to create decentralized and immersive gaming experiences. The advent of Web3 in gaming allows players to have true ownership of in-game assets, transparent and tamper-proof gaming mechanics, and the ability to earn real-world value through gameplay. This shift not only empowers players but also fosters a new economy where digital assets hold tangible value.

To support this innovative landscape, specialized libraries tailored for gaming are crucial. Evire’s Gaming Framework includes such libraries, designed to address the unique demands of blockchain gaming. By integrating these libraries into their development processes, game developers can ensure that their games are not only engaging and fair but also secure and scalable. This framework thus enables the creation of next-generation gaming experiences that are at the forefront of the Web3 movement.

Example: The Asset Trading Library

The Asset Trading Library is a comprehensive Solidity smart contract designed to facilitate the creation, management and trading of digital assets in blockchain-based gaming ecosystems. This library provides a robust foundation for implementing complex in-game economies, allowing for secure, decentralized asset ownership and trading. It empowers players to have full control over their digital assets while enabling game developers to create rich, interactive economic systems within their games.

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

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";

contract AssetTradingLibrary is ERC721, ERC721Enumerable, ERC721URIStorage, ReentrancyGuard, AccessControl {
using Counters for Counters.Counter;
using SafeMath for uint256;

bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");

Counters.Counter private _tokenIdCounter;
Counters.Counter private _tradeIdCounter;

uint256 public constant MAX_SUPPLY = 10000;
uint256 public platformFeePercentage = 250; // 2.5%

struct Asset {
uint256 id;
string name;
string metadataURI;
uint256 price;
bool forSale;
uint256 royaltyPercentage;
address creator;
uint256 level;
uint256 experience;
bool isLocked;
}

struct Trade {
uint256 id;
uint256 assetId;
address seller;
address buyer;
uint256 price;
uint256 deadline;
bool isCompleted;
bool isCancelled;
}

mapping(uint256 => Asset) private assets;
mapping(uint256 => Trade) private trades;
mapping(address => uint256) private userBalance;

event AssetCreated(uint256 indexed assetId, string name, string metadataURI, uint256 price, address creator, uint256 royaltyPercentage);
event AssetForSale(uint256 indexed assetId, uint256 price);
event AssetSold(uint256 indexed assetId, address indexed seller, address indexed buyer, uint256 price);
event AssetRemovedFromSale(uint256 indexed assetId);
event TradeCreated(uint256 indexed tradeId, uint256 indexed assetId, address indexed seller, uint256 price, uint256 deadline);
event TradeCompleted(uint256 indexed tradeId, address indexed buyer);
event TradeCancelled(uint256 indexed tradeId);
event AssetUpgraded(uint256 indexed assetId, uint256 newLevel, uint256 newExperience);
event AssetLocked(uint256 indexed assetId);
event AssetUnlocked(uint256 indexed assetId);
event PlatformFeeUpdated(uint256 newFeePercentage);
event UserBalanceWithdrawn(address indexed user, uint256 amount);

constructor() ERC721("Asset", "EEVA") {
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
_setupRole(MINTER_ROLE, msg.sender);
_setupRole(ADMIN_ROLE, msg.sender);
}

modifier onlyAdmin() {
require(hasRole(ADMIN_ROLE, msg.sender), "Caller is not an admin");
_;
}

modifier onlyMinter() {
require(hasRole(MINTER_ROLE, msg.sender), "Caller is not a minter");
_;
}

function createAsset(string memory name, string memory metadataURI, uint256 price, uint256 royaltyPercentage) public onlyMinter {
require(_tokenIdCounter.current() < MAX_SUPPLY, "Max supply reached");
require(royaltyPercentage <= 1000, "Royalty percentage cannot exceed 10%");

uint256 assetId = _tokenIdCounter.current();
_tokenIdCounter.increment();

assets[assetId] = Asset(assetId, name, metadataURI, price, false, royaltyPercentage, msg.sender, 1, 0, false);

_safeMint(msg.sender, assetId);
_setTokenURI(assetId, metadataURI);

emit AssetCreated(assetId, name, metadataURI, price, msg.sender, royaltyPercentage);
}

function listAssetForSale(uint256 assetId, uint256 price) public {
require(_exists(assetId), "Asset does not exist");
require(ownerOf(assetId) == msg.sender, "You do not own this asset");
require(!assets[assetId].isLocked, "Asset is locked and cannot be sold");

assets[assetId].price = price;
assets[assetId].forSale = true;
emit AssetForSale(assetId, price);
}

function buyAsset(uint256 assetId) public payable nonReentrant {
require(_exists(assetId), "Asset does not exist");
require(assets[assetId].forSale, "Asset is not for sale");
require(msg.value == assets[assetId].price, "Incorrect price");

address seller = ownerOf(assetId);
require(seller != msg.sender, "You cannot buy your own asset");

uint256 platformFee = msg.value.mul(platformFeePercentage).div(10000);
uint256 royaltyFee = msg.value.mul(assets[assetId].royaltyPercentage).div(10000);
uint256 sellerProceeds = msg.value.sub(platformFee).sub(royaltyFee);

_transfer(seller, msg.sender, assetId);

userBalance[seller] = userBalance[seller].add(sellerProceeds);
userBalance[assets[assetId].creator] = userBalance[assets[assetId].creator].add(royaltyFee);
userBalance[owner()] = userBalance[owner()].add(platformFee);

assets[assetId].forSale = false;
emit AssetSold(assetId, seller, msg.sender, msg.value);
}

function createTrade(uint256 assetId, uint256 price, uint256 duration) public {
require(_exists(assetId), "Asset does not exist");
require(ownerOf(assetId) == msg.sender, "You do not own this asset");
require(!assets[assetId].isLocked, "Asset is locked and cannot be traded");

uint256 tradeId = _tradeIdCounter.current();
_tradeIdCounter.increment();

trades[tradeId] = Trade(tradeId, assetId, msg.sender, address(0), price, block.timestamp + duration, false, false);
emit TradeCreated(tradeId, assetId, msg.sender, price, block.timestamp + duration);
}

function acceptTrade(uint256 tradeId) public payable nonReentrant {
Trade storage trade = trades[tradeId];
require(trade.deadline > block.timestamp, "Trade has expired");
require(!trade.isCompleted && !trade.isCancelled, "Trade is no longer active");
require(msg.value == trade.price, "Incorrect price");

address seller = trade.seller;
uint256 assetId = trade.assetId;

uint256 platformFee = msg.value.mul(platformFeePercentage).div(10000);
uint256 royaltyFee = msg.value.mul(assets[assetId].royaltyPercentage).div(10000);
uint256 sellerProceeds = msg.value.sub(platformFee).sub(royaltyFee);

_transfer(seller, msg.sender, assetId);

userBalance[seller] = userBalance[seller].add(sellerProceeds);
userBalance[assets[assetId].creator] = userBalance[assets[assetId].creator].add(royaltyFee);
userBalance[owner()] = userBalance[owner()].add(platformFee);

trade.buyer = msg.sender;
trade.isCompleted = true;

emit TradeCompleted(tradeId, msg.sender);
}

function cancelTrade(uint256 tradeId) public {
Trade storage trade = trades[tradeId];
require(trade.seller == msg.sender, "Only the seller can cancel the trade");
require(!trade.isCompleted && !trade.isCancelled, "Trade is no longer active");

trade.isCancelled = true;
emit TradeCancelled(tradeId);
}

function upgradeAsset(uint256 assetId, uint256 experienceGained) public onlyAdmin {
require(_exists(assetId), "Asset does not exist");
Asset storage asset = assets[assetId];

asset.experience = asset.experience.add(experienceGained);
if (asset.experience >= 1000) {
asset.level = asset.level.add(1);
asset.experience = asset.experience.sub(1000);
}

emit AssetUpgraded(assetId, asset.level, asset.experience);
}

function lockAsset(uint256 assetId) public {
require(ownerOf(assetId) == msg.sender, "You do not own this asset");
require(!assets[assetId].isLocked, "Asset is already locked");

assets[assetId].isLocked = true;
emit AssetLocked(assetId);
}

function unlockAsset(uint256 assetId) public {
require(ownerOf(assetId) == msg.sender, "You do not own this asset");
require(assets[assetId].isLocked, "Asset is not locked");

assets[assetId].isLocked = false;
emit AssetUnlocked(assetId);
}

function setPlatformFee(uint256 newFeePercentage) public onlyAdmin {
require(newFeePercentage <= 1000, "Fee percentage cannot exceed 10%");
platformFeePercentage = newFeePercentage;
emit PlatformFeeUpdated(newFeePercentage);
}

function withdrawBalance() public nonReentrant {
uint256 balance = userBalance[msg.sender];
require(balance > 0, "No balance to withdraw");

userBalance[msg.sender] = 0;
payable(msg.sender).transfer(balance);

emit UserBalanceWithdrawn(msg.sender, balance);
}

function getAssetInfo(uint256 assetId) public view returns (Asset memory) {
require(_exists(assetId), "Asset does not exist");
return assets[assetId];
}

function getTradeInfo(uint256 tradeId) public view returns (Trade memory) {
return trades[tradeId];
}

function getUserBalance(address user) public view returns (uint256) {
return userBalance[user];
}

// Override functions
function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override(ERC721, ERC721Enumerable) {
super._beforeTokenTransfer(from, to, tokenId);
}

function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
super._burn(tokenId);
}

function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) {
return super.tokenURI(tokenId);
}

function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable, AccessControl) returns (bool) {
return super.supportsInterface(interfaceId);
}
}

Explanations

1. Asset Creation and Management

What: The library allows for the creation and management of unique digital assets.

How:

  • Assets are implemented as ERC721 tokens, ensuring each asset is unique and non-fungible.
  • The createAsset function mints new assets with properties like name, metadata URI, price, and royalty percentage.

Why: This provides a standardized way to represent in-game items, characters, or any other digital assets, ensuring interoperability and ownership rights.

2. Role-Based Access Control

What: The contract implements a role-based permission system.

How:

  • Uses OpenZeppelin’s AccessControl contract.
  • Defines roles such as MINTER_ROLE and ADMIN_ROLE.

Why: This allows for fine-grained control over who can perform certain actions (e.g., creating assets, upgrading assets), enhancing security and flexibility in managing the game economy.

3. Trading System

What: The library includes a comprehensive trading system for assets.

How:

  • Users can list assets for sale using listAssetForSale.
  • Direct purchases can be made using buyAsset.
  • A more complex trade creation and acceptance system is implemented with createTrade and acceptTrade.

Why: This facilitates a dynamic in-game economy, allowing players to trade assets securely and efficiently.

4. Economic Models

What: The contract implements various economic models including platform fees and royalties.

How:

  • Platform fees are deducted from each sale and sent to the contract owner.
  • Royalties are paid to the original creator of an asset on each subsequent sale.

Why: This allows for sustainable monetization of the platform and incentivizes content creation by rewarding creators for popular assets.

5. Asset Upgrading

What: Assets can be upgraded over time.

How:

  • The upgradeAsset function allows administrators to increase an asset's experience points.
  • Assets level up automatically when they reach certain experience thresholds.

Why: This feature adds depth to the game mechanics, allowing assets to grow in value and capabilities over time.

6. Asset Locking

What: Assets can be locked and unlocked by their owners.

How:

  • The lockAsset and unlockAsset functions allow owners to toggle the locked state of their assets.

Why: This provides additional control to asset owners, potentially useful for staking mechanics or preventing accidental sales.

7. User Balances and Withdrawals

What: The contract keeps track of user balances and allows for withdrawals.

How:

  • Balances are updated when assets are sold or royalties are earned.
  • Users can withdraw their balance using the withdrawBalance function.

Why: This creates a seamless economic system where users can accumulate earnings from their trading activities.

8. Safety Measures

What: The contract implements various safety measures to prevent common vulnerabilities.

How:

  • Uses OpenZeppelin’s ReentrancyGuard for functions involving transfers.
  • Implements SafeMath for arithmetic operations.

Why: These measures protect against common smart contract vulnerabilities, ensuring the security of user assets and funds.

Model of usage

This model demonstrates how to integrate and utilize the Asset Trading Library in a complex smart contract for a blockchain game called “CryptoWorlds”.

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

import "@evire/AssetTradingLibrary.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/Pausable.sol";

contract CryptoWorldsGame is Pausable, ReentrancyGuard {
AssetTradingLibrary public assetLibrary;
IERC20 public gameToken;

struct Player {
uint256 level;
uint256 experience;
uint256 lastActionTimestamp;
}

mapping(address => Player) public players;
mapping(uint256 => uint256) public assetPowers;

event PlayerRegistered(address indexed player);
event PlayerLeveledUp(address indexed player, uint256 newLevel);
event QuestCompleted(address indexed player, uint256 rewardExp, uint256 rewardTokens);
event AssetPowerIncreased(uint256 indexed assetId, uint256 newPower);

constructor(address _assetLibraryAddress, address _gameTokenAddress) {
assetLibrary = AssetTradingLibrary(_assetLibraryAddress);
gameToken = IERC20(_gameTokenAddress);
}

modifier onlyRegisteredPlayer() {
require(players[msg.sender].lastActionTimestamp > 0, "Player not registered");
_;
}

function registerPlayer() external {
require(players[msg.sender].lastActionTimestamp == 0, "Player already registered");
players[msg.sender] = Player(1, 0, block.timestamp);
emit PlayerRegistered(msg.sender);
}

function createGameAsset(string memory name, string memory metadataURI, uint256 price, uint256 royaltyPercentage, uint256 initialPower) external onlyRegisteredPlayer {
require(assetLibrary.hasRole(assetLibrary.MINTER_ROLE(), address(this)), "Game contract is not a minter");
uint256 assetId = assetLibrary.createAsset(name, metadataURI, price, royaltyPercentage);
assetPowers[assetId] = initialPower;
}

function completeQuest(uint256 assetId) external onlyRegisteredPlayer nonReentrant whenNotPaused {
require(assetLibrary.ownerOf(assetId) == msg.sender, "You don't own this asset");
require(block.timestamp >= players[msg.sender].lastActionTimestamp + 1 hours, "Quest cooldown not met");

uint256 questRewardExp = calculateQuestReward(assetId);
uint256 questRewardTokens = questRewardExp * 10; // 10 tokens per exp point

players[msg.sender].experience += questRewardExp;
players[msg.sender].lastActionTimestamp = block.timestamp;

if (players[msg.sender].experience >= 1000 * players[msg.sender].level) {
players[msg.sender].level++;
players[msg.sender].experience -= 1000 * (players[msg.sender].level - 1);
emit PlayerLeveledUp(msg.sender, players[msg.sender].level);
}

require(gameToken.transfer(msg.sender, questRewardTokens), "Token transfer failed");

// Upgrade the asset
assetLibrary.upgradeAsset(assetId, questRewardExp / 10);

emit QuestCompleted(msg.sender, questRewardExp, questRewardTokens);
}

function calculateQuestReward(uint256 assetId) internal view returns (uint256) {
(, , , , , , , uint256 assetLevel, ,) = assetLibrary.getAssetInfo(assetId);
return (assetPowers[assetId] * assetLevel * players[msg.sender].level) / 100;
}

function tradeAsset(uint256 assetId, uint256 price) external onlyRegisteredPlayer {
require(assetLibrary.ownerOf(assetId) == msg.sender, "You don't own this asset");
assetLibrary.listAssetForSale(assetId, price);
}

function buyGameAsset(uint256 assetId) external payable onlyRegisteredPlayer nonReentrant {
assetLibrary.buyAsset{value: msg.value}(assetId);

// Bonus: Increase asset power after purchase
assetPowers[assetId] += 10;
emit AssetPowerIncreased(assetId, assetPowers[assetId]);
}

function createAssetTrade(uint256 assetId, uint256 price, uint256 duration) external onlyRegisteredPlayer {
require(assetLibrary.ownerOf(assetId) == msg.sender, "You don't own this asset");
assetLibrary.createTrade(assetId, price, duration);
}

function acceptAssetTrade(uint256 tradeId) external payable onlyRegisteredPlayer nonReentrant {
assetLibrary.acceptTrade{value: msg.value}(tradeId);

// Bonus: Increase asset power after trade
(, uint256 assetId, , , , , ,) = assetLibrary.getTradeInfo(tradeId);
assetPowers[assetId] += 5;
emit AssetPowerIncreased(assetId, assetPowers[assetId]);
}

function withdrawGameEarnings() external onlyRegisteredPlayer nonReentrant {
assetLibrary.withdrawBalance();
}

function increaseAssetPower(uint256 assetId, uint256 powerIncrease) external payable onlyRegisteredPlayer {
require(assetLibrary.ownerOf(assetId) == msg.sender, "You don't own this asset");
require(msg.value == powerIncrease * 0.01 ether, "Incorrect payment for power increase");

assetPowers[assetId] += powerIncrease;
emit AssetPowerIncreased(assetId, assetPowers[assetId]);
}

// Admin functions
function setGameToken(address _newGameToken) external onlyOwner {
gameToken = IERC20(_newGameToken);
}

function pauseGame() external onlyOwner {
_pause();
}

function unpauseGame() external onlyOwner {
_unpause();
}

// Fallback and receive functions
fallback() external payable {}
receive() external payable {}
}

Explanation

  1. Importing and Initializing the Library: The contract imports the AssetTradingLibrary and initializes it in the constructor.
  2. Player Management: The game implements a basic player system with registration, leveling, and experience tracking.
  3. Asset Creation: The createGameAsset function demonstrates how to create new assets using the library, adding game-specific properties like assetPowers.
  4. Quest System: The completeQuest function showcases how in-game actions can interact with the assets, upgrading them and rewarding players.
  5. Trading Integration: Functions like tradeAsset, buyGameAsset, createAssetTrade, and acceptAssetTrade demonstrate how to utilize the library's trading features within the game's context.
  6. Economic Integration: The contract integrates with an ERC20 token for in-game rewards and uses Ether for certain transactions, showcasing how the library can be part of a broader economic system.
  7. Asset Enhancement: The increaseAssetPower function shows how game-specific enhancements can be added on top of the library's basic asset structure.
  8. Admin Controls: The contract includes admin functions for managing the game token and pausing/unpausing the game, demonstrating how the library can be part of a managed game ecosystem.

This model showcases how the Asset Trading Library can be seamlessly integrated into a game's core mechanics, handling asset creation, trading and upgrading while allowing the game contract to focus on game-specific logic and player management. It demonstrates the library's flexibility in supporting various game features and economic models.

--

--

evire

Evire is a layer 1 blockchain that aims to provide native support for AI, gaming, RWA and DePIN, empowering developers to build efficient, cutting-edge dApps.