// SPDX-License-Identifier: UNLICENSED // Proprietary - (c) 2026 Durian (durianfun). All rights reserved. Not open source; for Durianfun + authorized integrations only. See /LICENSE. pragma solidity 0.8.24; import "../EntropyConsumer.sol"; import "./ERC721Mini.sol"; /// @title RandomTierNFT /// @notice Example: mint an NFT whose tier is randomised on-chain by DurianEntropyV2, with NO keeper. /// The minter signs once (mint), pays the protocol fee, and the tier is sealed by a future /// block. Settlement is keeper-free: the minter WANTS their NFT so they (or the next minter, /// via self-crank) reveal it. The tier is unknowable at mint time, so no "see-and-revert". /// /// Tier odds (rng % 100): KING 1% · COOL 7% · OKAY 22% · NOOB 70%. Sparse token ids. contract RandomTierNFT is EntropyConsumer, ERC721Mini { enum Tier { NOOB, OKAY, COOL, KING } uint32 public constant CB_GAS = 200_000; mapping(uint256 => address) public pendingMinter; // entropy id => minter mapping(uint256 => Tier) public tierOf; // tokenId => tier uint256 public minted; uint256 private _nonce; uint256 private _lastPending = type(uint256).max; // for self-crank ("none" sentinel) event MintRequested(uint256 indexed id, address indexed minter); event Minted(uint256 indexed id, address indexed minter, uint256 indexed tokenId, Tier tier); constructor(address e) EntropyConsumer(e) ERC721Mini("Durian Gigakub", "GIGA") {} /// @notice One signature: pay the protocol fee, lock the roll to a future block. Also self-cranks /// the previous mint so its NFT lands without that minter sending a second transaction. function mint() external payable { uint256 f = entropy.fee(CB_GAS); require(msg.value >= f, "fee"); if (_lastPending != type(uint256).max) _tryReveal(_lastPending); bytes32 seed = keccak256(abi.encodePacked(msg.sender, block.number, _nonce++)); uint256 id = _roll(seed, CB_GAS); pendingMinter[id] = msg.sender; _lastPending = id; emit MintRequested(id, msg.sender); if (msg.value > f) { (bool ok, ) = msg.sender.call{value: msg.value - f}(""); // refund overpay require(ok, "refund failed"); } } function _onRandomness(uint256 id, uint256 rng) internal override { address to = pendingMinter[id]; require(to != address(0), "no pending mint"); delete pendingMinter[id]; Tier tier = _tier(rng % 100); uint256 tokenId = uint256(keccak256(abi.encodePacked(id, rng))); // sparse, unique-by-design tierOf[tokenId] = tier; minted += 1; _safeMint(to, tokenId); emit Minted(id, to, tokenId, tier); } function _tier(uint256 roll) private pure returns (Tier) { if (roll < 1) return Tier.KING; // 1% if (roll < 8) return Tier.COOL; // 7% if (roll < 30) return Tier.OKAY; // 22% return Tier.NOOB; // 70% } }