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:
- Player requests craft from game server
- Server verifies off-chain conditions (XP, quests, resources)
- Server signs EIP-712 typed data
- Player calls
craft()with signature - 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:
- Admin mints tokens externally
- Admin calls
depositPremintedTokens(typeId, tokenIds[]) - 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:
| Recipe | Input Spec | Meaning |
|---|---|---|
| 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)
| Operation | Gas | Cost @ 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
- Deploy
CraftingRecipesproxy to Fuji - Call
MonsutaNFT.setMinter(craftingAddress, true)to grantMINTER_ROLE - Register item types via
registerItemType() - Register recipes via
registerRecipe() - Set
authorizedSignerfor server-gated recipes - 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
| Mode | Use Case |
|---|---|
| MINT_ON_DEMAND | New collections where you want dynamic supply |
| PRE_MINTED | Existing collections or limited editions |
Adapting for Different Games
To use CraftingRecipes for a different game:
- Deploy the contract - Works with any ERC-721
- Register item types - Define what can be crafted
- Register recipes - Specify inputs (any collection), costs, outputs
- Configure metadata - Point to your game's metadata server
- 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:
| Action | THC Consumed | Frequency | Aggregate Impact |
|---|---|---|---|
| Basic crafting | 100–500 | Several/day | Medium |
| Legendary crafting | 1000–5000 | Weekly | High |
| Upgrades (low) | 50–200 | Daily | Medium |
| Upgrades (high) | 500–2000 | Weekly | High |
The game designer controls inflation by tuning recipe THC costs.