NFT Crafting System Development
NFT crafting is a mechanic where multiple NFTs or resources combine to create a new NFT. It's simultaneously a sink mechanism (consumes or burns NFT/tokens) and source of new valuable items. Properly implemented crafting system creates economic cycle and retains players.
Types of Crafting
Fusion (merger): N tokens of one type → 1 token of higher tier. Classic example: 3 Common swords → 1 Rare sword. Simplifies inventory, creates demand for low-tier NFTs.
Recipe crafting: specific material combinations → specific result. Alchemist recipe book: 1 Iron Ore + 2 Coal + 1 Fire Essence → Steel Ingot.
Random crafting: materials + randomness → result from possible range. Risk/reward: can get legendary, can get common.
Upgrade (leveling): existing NFT + materials → same NFT with improved attributes.
Smart Contract Implementation
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/AccessControl.sol";
contract NFTCraftingSystem is AccessControl, VRFConsumerBaseV2Plus {
bytes32 public constant RECIPE_MANAGER = keccak256("RECIPE_MANAGER");
struct CraftingRecipe {
uint256 recipeId;
string name;
// Input materials
address[] inputContracts; // addresses of NFT contracts for materials
uint256[] inputTokenIds; // tokenId (0 = any from collection)
uint256[] inputAmounts; // quantity (for ERC-1155)
// Input ERC-20 tokens
address[] tokenInputs;
uint256[] tokenAmounts;
// Output
address outputContract;
uint256 outputTokenId; // 0 = random from range
uint256 minOutputId; // for random: minimum tokenId
uint256 maxOutputId; // for random: maximum tokenId
bool burnInputs; // burn or just consume
bool requiresVRF; // need random?
bool isActive;
uint256 cooldown; // seconds between crafts by one address
}
mapping(uint256 => CraftingRecipe) public recipes;
mapping(address => mapping(uint256 => uint256)) public lastCraftTime; // player → recipeId → timestamp
mapping(uint256 => PendingCraft) public pendingCrafts; // vrfRequestId → craft
struct PendingCraft {
address crafter;
uint256 recipeId;
bool fulfilled;
}
function craft(uint256 recipeId, uint256[][] calldata inputTokenIds)
external returns (uint256 requestId)
{
CraftingRecipe storage recipe = recipes[recipeId];
require(recipe.isActive, "Recipe not active");
// Cooldown check
require(
block.timestamp >= lastCraftTime[msg.sender][recipeId] + recipe.cooldown,
"Crafting cooldown active"
);
lastCraftTime[msg.sender][recipeId] = block.timestamp;
// Validate and consume materials
_consumeInputMaterials(recipe, inputTokenIds);
_consumeInputTokens(recipe);
if (recipe.requiresVRF) {
// For random crafting—request VRF
requestId = _requestRandomWords(1);
pendingCrafts[requestId] = PendingCraft({
crafter: msg.sender,
recipeId: recipeId,
fulfilled: false,
});
emit CraftingInitiated(msg.sender, recipeId, requestId);
} else {
// Deterministic crafting—mint immediately
_mintCraftingResult(msg.sender, recipe, 0);
}
}
function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords)
internal override
{
PendingCraft storage pending = pendingCrafts[requestId];
require(!pending.fulfilled, "Already fulfilled");
pending.fulfilled = true;
CraftingRecipe storage recipe = recipes[pending.recipeId];
_mintCraftingResult(pending.crafter, recipe, randomWords[0]);
}
function _mintCraftingResult(
address crafter,
CraftingRecipe storage recipe,
uint256 random
) internal {
uint256 outputTokenId;
if (recipe.outputTokenId != 0) {
// Deterministic output
outputTokenId = recipe.outputTokenId;
} else {
// Random output in range [minOutputId, maxOutputId]
outputTokenId = recipe.minOutputId + (random % (recipe.maxOutputId - recipe.minOutputId + 1));
}
// Mint result
IGameItems(recipe.outputContract).mintCraftingResult(crafter, outputTokenId, 1);
emit CraftingCompleted(crafter, recipe.recipeId, outputTokenId);
}
function _consumeInputMaterials(
CraftingRecipe storage recipe,
uint256[][] calldata inputTokenIds
) internal {
for (uint i = 0; i < recipe.inputContracts.length; i++) {
IERC1155 nft = IERC1155(recipe.inputContracts[i]);
if (recipe.burnInputs) {
// Burn materials
IERC1155Burnable(recipe.inputContracts[i]).burn(
msg.sender,
inputTokenIds[i][0],
recipe.inputAmounts[i]
);
} else {
// Transfer to contract (without burn)
nft.safeTransferFrom(
msg.sender,
address(this),
inputTokenIds[i][0],
recipe.inputAmounts[i],
""
);
}
}
}
}
Upgrade System (Leveling)
contract NFTUpgradeSystem {
struct UpgradePath {
uint256 itemTypeId;
uint256 currentLevel;
uint256 maxLevel;
uint256[] materialCosts; // materials for each level
uint256[] tokenCosts;
uint256 successRate; // in basis points, 10000 = 100%
bool destroyOnFail; // burn on failure?
}
// Upgrade with destruction risk (Korean-MMO style)
function upgradeItem(
uint256 tokenId,
uint256 itemTypeId,
uint256 targetLevel
) external returns (bool success) {
UpgradePath storage path = upgradePaths[itemTypeId][targetLevel];
// Consume materials
_burnUpgradeMaterials(path);
// Determine success (off-chain random or VRF)
// For simplicity—pseudo-random via block hash
uint256 rand = uint256(keccak256(abi.encodePacked(
blockhash(block.number - 1),
msg.sender,
tokenId,
block.timestamp
))) % 10000;
success = rand < path.successRate;
if (success) {
gameItems.setItemLevel(tokenId, targetLevel);
emit UpgradeSuccess(msg.sender, tokenId, targetLevel);
} else if (path.destroyOnFail) {
gameItems.burn(msg.sender, itemTypeId, 1);
emit UpgradeFailed(msg.sender, tokenId, targetLevel, true);
} else {
// Just failure without item loss
emit UpgradeFailed(msg.sender, tokenId, targetLevel, false);
}
}
}
Important: for upgrade with destruction risk, VRF is necessary—player must be sure casino can't manipulate odds.
Crafting UI Patterns
Drag-and-drop slots for materials, result preview before crafting, probabilities for random recipes, crafting animation (progress bar or particle effect).
Developing basic crafting system (recipes + fusion + deterministic output)—3-4 weeks. With VRF random crafting and upgrade system—5-7 weeks.







