Skip to main content

Crafting System

Dual-mode crafting with trustless and server-gated recipes.


Architecture Overview

┌─────────────────────────────────────────────────────────────────────────┐
│ PLAYER │
│ │
│ 1. Approve CraftingRecipes: MonsutaNFT.setApprovalForAll(craft, true)│
│ 2. Call craft(recipeId, inputTokenIds[], nonce, deadline, signature) │
│ │
└─────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────┐
│ CraftingRecipes.sol (On-Chain) │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ RECIPE VALIDATION │ │
│ │ ├─ Recipe active? │ │
│ │ ├─ Output type active? │ │
│ │ └─ Supply cap not exceeded? │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ SERVER SIGNATURE (if requiresServerSig) │ │
│ │ ├─ deadline not expired? │ │
│ │ ├─ nonce not used? │ │
│ │ └─ EIP-712 signature valid? │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ INPUT CONSUMPTION │ │
│ │ ├─ For each NFTInputSpec: │ │
│ │ │ ├─ Verify ownership │ │
│ │ │ ├─ Verify approval │ │
│ │ │ ├─ Range check (minTokenId ≤ id ≤ maxTokenId) │ │
│ │ │ └─ burn(tokenId) │ │
│ │ └─ If thcCost > 0: transferFrom(player, this, thcCost) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ OUTPUT PRODUCTION │ │
│ │ │ │
│ │ MINT_ON_DEMAND: │ │
│ │ ├─ serial = ++_typeSerial[typeId] │ │
│ │ ├─ tokenId = typeId × 1,000,000 + serial │ │
│ │ └─ MonsutaNFT.mint(player, tokenId) │ │
│ │ │ │
│ │ PRE_MINTED: │ │
│ │ ├─ tokenId = _premintedQueue[typeId][head] │ │
│ │ ├─ head++ │ │
│ │ └─ ERC721.transferFrom(this, player, tokenId) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

Two Crafting Modes

Trustless (requiresServerSig = false)

No game server required. Anyone holding the correct NFTs can craft.

// Recipe: 4 Rocks → 1 Marble Slab
craft(1, [rockId1, rockId2, rockId3, rockId4], 0, 0, "")
  • Contract independently verifies ownership
  • Burns input NFTs
  • Mints output NFT
  • No signature, no nonce, no deadline

Use for: Pure NFT↔NFT recipes where all conditions are on-chain verifiable.


Server-Gated (requiresServerSig = true)

Requires EIP-712 signature from authorizedSigner (game server).

// Server signs: { player, recipeId, nonce, deadline }
craft(2, [rareItemId], nonce, deadline, signature)

Flow:

  1. Player requests craft from game server
  2. Server verifies off-chain conditions (XP, quests, resources)
  3. Server signs EIP-712 typed data
  4. Player calls craft() with signature
  5. Contract verifies signature before proceeding

Use for: Recipes with off-chain gates (game progress, non-NFT resources, anti-exploit checks).


Two Output Modes

MINT_ON_DEMAND

Contract mints a fresh NFT at craft time. Requires MINTER_ROLE on output NFT.

ItemType({
mintMode: MintMode.MINT_ON_DEMAND,
outputNFT: monsutaNFTAddress,
maxSupply: 10000 // 0 = unlimited
});

Token ID formula:

tokenId = typeId × 1,000,000 + serial

Example (typeId = 1, Marble Slab):
1st mint: 1,000,001
2nd mint: 1,000,002
...

Gas: ~220k for 4-NFT burn + mint


PRE_MINTED

Tokens are pre-minted and deposited into the contract. Crafting transfers one from the queue.

ItemType({
mintMode: MintMode.PRE_MINTED,
outputNFT: existingCollectionAddress,
maxSupply: 0 // Ignored, cap = deposited amount
});

Flow:

  1. Admin mints tokens externally
  2. Admin calls depositPremintedTokens(typeId, tokenIds[])
  3. Crafting transfers queued tokens FIFO to players

Use for: Limited editions, existing collections, unique-art tokens.


Token ID Encoding

┌────────────────────────────────────────────────────────────┐
│ TOKEN ID STRUCTURE │
│ │
│ tokenId = typeId × 1,000,000 + serial │
│ │
│ ┌─────────────────┬──────────────────────────────────┐ │
│ │ Type ID │ Serial │ │
│ │ (0-999,999) │ (1-999,999) │ │
│ │ 6+ digits │ 6 digits │ │
│ └─────────────────┴──────────────────────────────────┘ │
│ │
│ Examples: │
│ ───────────────────────────────────────────────────── │
│ tokenId < 1,000,000 → Base Token (Rock) │
│ tokenId = 1,000,001 → Marble Slab #1 │
│ tokenId = 1,000,002 → Marble Slab #2 │
│ tokenId = 2,000,001 → Tetra Goldium #1 │
│ tokenId = 3,000,001 → Future Type #1 │
│ │
└────────────────────────────────────────────────────────────┘

Decoding:

function decodeTypeId(uint256 tokenId) external pure returns (uint32) {
return uint32(tokenId / TYPE_ID_MULTIPLIER);
}

Input Specification (NFTInputSpec)

Recipes specify NFT inputs with flexible range matching:

struct NFTInputSpec {
address nftContract; // Collection address
uint32 requiredCount; // How many tokens needed
uint256 minTokenId; // Range start (0 = any)
uint256 maxTokenId; // Range end (0 = any)
}

Examples:

RecipeInput SpecMeaning
4 Rocks → Slab{monsutaNFT, 4, 0, 999999}Any 4 tokens with ID < 1,000,000
2 Slabs → Item{monsutaNFT, 2, 1000000, 1999999}Any 2 Marble Slabs
Any NFT → Scroll{anyNFT, 1, 0, 0}Any 1 token from collection

EIP-712 Signature Structure

Domain:

{
name: "MonsutaCrafting",
version: "1",
chainId: 43113, // Fuji
verifyingContract: craftingAddress
}

Type:

Craft(address player, uint32 recipeId, uint256 nonce, uint256 deadline)

Server-side signing:

const { domain, types, value } = {
domain: { name: "MonsutaCrafting", version: "1", chainId, verifyingContract },
types: { Craft: [
{ name: "player", type: "address" },
{ name: "recipeId", type: "uint32" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" }
]},
value: { player: playerAddress, recipeId: 1, nonce: 12345, deadline: expiry }
};

const signature = await signer.signTypedData(domain, types, value);

Metadata Routing

Crafted NFTs use the metadata router approach (no contract changes needed):

baseURI = "https://meta.fadedmonsuta.com/crafted/"

GET /crafted/1000001.json

├─▶ Server computes: typeId = 1000001 / 1000000 = 1
└─▶ Returns: Marble Slab metadata

GET /crafted/2000001.json

├─▶ Server computes: typeId = 2000001 / 1000000 = 2
└─▶ Returns: Tetra Goldium metadata

Benefits:

  • Zero contract changes
  • Single metadata file per type (not per token)
  • Easy updates if needed
  • IPFS-ready for future

Gas Costs (Fuji / C-Chain)

OperationGasCost @ 10 gwei
Trustless 4-NFT burn + mint~220k~0.0022 AVAX
Server-gated adds sig verify+~6k+~0.00006 AVAX
PRE_MINTED transfer~same~same
THC cost adds+~50k+~0.0005 AVAX

Contract Interface

// Item Type Management
function registerItemType(
string calldata name,
string calldata metadataURI,
MintMode mintMode,
address outputNFT,
uint256 maxSupply
) external onlyOwner returns (uint32 typeId);

// Recipe Management
function registerRecipe(
string calldata name,
NFTInputSpec[] calldata nftInputs,
uint256 thcCost,
uint32 outputTypeId,
bool requiresServerSig
) external onlyOwner returns (uint32 recipeId);

// Pre-Minted Token Management
function depositPremintedTokens(uint32 typeId, uint256[] calldata tokenIds) external;
function withdrawPremintedTokens(uint32 typeId, uint256 count, address recipient) external onlyOwner;

// Crafting
function craft(
uint32 recipeId,
uint256[] calldata inputTokenIds,
uint256 nonce,
uint256 deadline,
bytes calldata signature
) external nonReentrant whenNotPaused returns (uint256 outputTokenId);

// Views
function getItemType(uint32 typeId) external view returns (ItemType memory);
function getRecipe(uint32 recipeId) external view returns (Recipe memory);
function decodeTypeId(uint256 tokenId) external pure returns (uint32);
function availablePremintedCount(uint32 typeId) external view returns (uint256);

Security Properties

| Property | Mechanism | |---|---|---| | Input ownership | ownerOf(tokenId) == msg.sender check | | Input approval | isApprovedForAll or getApproved check | | Range restriction | minTokenId ≤ tokenId ≤ maxTokenId | | Supply cap | minted < maxSupply for MINT_ON_DEMAND | | Replay protection | nonce mapping for server-gated | | Signature expiry | deadline timestamp check | | EIP-712 integrity | Typed data hash + ECDSA recovery | | Reentrancy | nonReentrant modifier | | Pause switch | PausableUpgradeable |


Deployment Notes

  1. Deploy CraftingRecipes proxy to Fuji
  2. Call MonsutaNFT.setMinter(craftingAddress, true) to grant MINTER_ROLE
  3. Register item types via registerItemType()
  4. Register recipes via registerRecipe()
  5. Set authorizedSigner for server-gated recipes
  6. Configure metadata router endpoint

Design for Collection Agnosticism

The CraftingRecipes contract is designed to support ANY ERC-721 collection, not just MonsutaNFT.

Multi-Collection Support

The contract accepts any ERC-721 as an input or output:

struct NFTInputSpec {
address nftContract; // Any ERC-721 collection
uint32 requiredCount; // How many tokens needed
uint256 minTokenId; // Range start (0 = any)
uint256 maxTokenId; // Range end (0 = any)
}

This means:

  • Input NFTs can come from ANY collection
  • Output NFTs can be minted to ANY collection (if it grants MINTER_ROLE)
  • Recipes can mix inputs from multiple collections

Flexible Output Modes

ModeUse Case
MINT_ON_DEMANDNew collections where you want dynamic supply
PRE_MINTEDExisting collections or limited editions

Adapting for Different Games

To use CraftingRecipes for a different game:

  1. Deploy the contract - Works with any ERC-721
  2. Register item types - Define what can be crafted
  3. Register recipes - Specify inputs (any collection), costs, outputs
  4. Configure metadata - Point to your game's metadata server
  5. Grant roles - Give MINTER_ROLE to the crafting contract

The contract knows NOTHING about Faded Monsuta specifically - it only understands generic NFT operations.


THC Sink Analysis

Crafting is a primary THC sink in the Monsuta Core economy:

ActionTHC ConsumedFrequencyAggregate Impact
Basic crafting100–500Several/dayMedium
Legendary crafting1000–5000WeeklyHigh
Upgrades (low)50–200DailyMedium
Upgrades (high)500–2000WeeklyHigh

The game designer controls inflation by tuning recipe THC costs.