Mystery Box NFT
Overview
Quick Start
# Create new project from this template
npx labz create mystery-box my-project
# Navigate and install
cd my-project
npm install
# Run tests
npx hardhat testContract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import { FHE, euint64, euint8, ebool, eaddress, externalEuint64, externalEuint8, externalEbool, externalEaddress } from "@fhevm/solidity/lib/FHE.sol";
import { ZamaEthereumConfig } from "@fhevm/solidity/config/ZamaConfig.sol";
/**
* @title MysteryBox
* @notice NFT Mystery Box with encrypted rarity - fair distribution guaranteed
* @dev Uses FHE.random() for provably fair rarity assignment
*
* Rarity Tiers:
* - Legendary: 0-5 (5% chance)
* - Epic: 6-20 (15% chance)
* - Rare: 21-45 (25% chance)
* - Common: 46-100 (55% chance)
*
* FHE Operations Used:
* - random: Generate unpredictable rarity values
* - rem: Map random to 0-100 range
* - lt/lte/gt/gte: Determine rarity tier
* - select: Choose tier based on conditions
* - eq: Check specific rarity values
*/
contract MysteryBox is ZamaEthereumConfig {
// ============ Errors ============
error BoxNotFound();
error BoxAlreadyRevealed();
error NotBoxOwner();
error InsufficientPayment();
error NoBoxesAvailable();
// ============ Events ============
event BoxPurchased(uint256 indexed boxId, address indexed buyer);
event BoxRevealed(uint256 indexed boxId, address indexed owner);
event RarityAssigned(uint256 indexed boxId, uint8 tier);
// ============ Enums ============
enum Tier { Common, Rare, Epic, Legendary }
// ============ Structs ============
struct Box {
address owner;
euint8 rarity; // Encrypted rarity (0-100)
bool revealed;
uint8 revealedTier; // Only set after reveal
uint256 purchasedAt;
}
// ============ State Variables ============
mapping(uint256 => Box) public _boxes;
mapping(address => uint256[]) public _userBoxes;
uint256 public _boxCount;
uint256 public boxPrice;
uint256 public maxSupply;
// Tier thresholds (cumulative percentages)
uint8 public constant LEGENDARY_THRESHOLD = 5; // 0-5 = Legendary (5%)
uint8 public constant EPIC_THRESHOLD = 20; // 6-20 = Epic (15%)
uint8 public constant RARE_THRESHOLD = 45; // 21-45 = Rare (25%)
// 46-100 = Common (55%)
// ============ Modifiers ============
modifier boxExists(uint256 boxId) {
if (boxId >= _boxCount) revert BoxNotFound();
_;
}
modifier isBoxOwner(uint256 boxId) {
if (_boxes[boxId].owner != msg.sender) revert NotBoxOwner();
_;
}
// ============ Constructor ============
constructor(uint256 _boxPrice, uint256 _maxSupply) {
boxPrice = _boxPrice;
maxSupply = _maxSupply;
}
// ============ External Functions ============
/**
* @notice Purchase a mystery box
* @dev Rarity is assigned immediately but encrypted
*/
function purchaseBox() external payable returns (uint256) {
if (msg.value < boxPrice) revert InsufficientPayment();
if (_boxCount >= maxSupply) revert NoBoxesAvailable();
uint256 boxId = _boxCount++;
// Generate random rarity (0-100) using FHE
euint64 randomValue = FHE.randEuint64();
euint8 rarity = FHE.asEuint8(FHE.rem(randomValue, uint64(101)));
_boxes[boxId] = Box({
owner: msg.sender,
rarity: rarity,
revealed: false,
revealedTier: 0,
purchasedAt: block.timestamp
});
_userBoxes[msg.sender].push(boxId);
// Allow contract to use the rarity value
FHE.allowThis(rarity);
emit BoxPurchased(boxId, msg.sender);
return boxId;
}
/**
* @notice Reveal your box's rarity
* @param boxId The box to reveal
*/
function revealBox(uint256 boxId)
external
boxExists(boxId)
isBoxOwner(boxId)
returns (euint8)
{
Box storage box = _boxes[boxId];
if (box.revealed) revert BoxAlreadyRevealed();
box.revealed = true;
// Allow owner to see their rarity
FHE.allow(box.rarity, msg.sender);
emit BoxRevealed(boxId, msg.sender);
return box.rarity;
}
/**
* @notice Get rarity tier from encrypted rarity value
* @dev Uses encrypted comparisons to determine tier
* @param boxId The box to check
*/
function getRarityTier(uint256 boxId)
external
boxExists(boxId)
isBoxOwner(boxId)
returns (ebool isLegendary, ebool isEpic, ebool isRare)
{
Box storage box = _boxes[boxId];
euint8 rarity = box.rarity;
// Check each tier using encrypted comparisons
isLegendary = FHE.le(rarity, FHE.asEuint8(LEGENDARY_THRESHOLD));
isEpic = FHE.and(
FHE.gt(rarity, FHE.asEuint8(LEGENDARY_THRESHOLD)),
FHE.le(rarity, FHE.asEuint8(EPIC_THRESHOLD))
);
isRare = FHE.and(
FHE.gt(rarity, FHE.asEuint8(EPIC_THRESHOLD)),
FHE.le(rarity, FHE.asEuint8(RARE_THRESHOLD))
);
// Common is implicit: rarity > 45
// Allow owner to see tier results
FHE.allow(isLegendary, msg.sender);
FHE.allow(isEpic, msg.sender);
FHE.allow(isRare, msg.sender);
FHE.allowThis(isLegendary);
FHE.allowThis(isEpic);
FHE.allowThis(isRare);
return (isLegendary, isEpic, isRare);
}
/**
* @notice Transfer box to another address
* @param boxId The box to transfer
* @param to The recipient
*/
function transferBox(uint256 boxId, address to)
external
boxExists(boxId)
isBoxOwner(boxId)
{
Box storage box = _boxes[boxId];
box.owner = to;
// Update ownership for FHE access
FHE.allow(box.rarity, to);
}
// ============ View Functions ============
/**
* @notice Get box info
*/
function getBox(uint256 boxId) external view returns (
address owner,
bool revealed,
uint256 purchasedAt
) {
Box storage box = _boxes[boxId];
return (box.owner, box.revealed, box.purchasedAt);
}
/**
* @notice Get total boxes sold
*/
function getBoxCount() external view returns (uint256) {
return _boxCount;
}
/**
* @notice Get remaining supply
*/
function getRemainingSupply() external view returns (uint256) {
return maxSupply - _boxCount;
}
/**
* @notice Get user's boxes
*/
function getUserBoxes(address user) external view returns (uint256[] memory) {
return _userBoxes[user];
}
/**
* @notice Get tier probabilities
*/
function getTierProbabilities() external pure returns (
uint8 legendary,
uint8 epic,
uint8 rare,
uint8 common
) {
return (5, 15, 25, 55);
}
// ============ Internal Functions ============
}
FHE Operations Used
FHE Types Used
Tags
Related Examples
Prerequisites
Next Steps
Last updated
