// SPDX-License-Identifier: MIT // BytesBuilder uses `mcopy`, so this file needs solc 0.8.24 / Cancun. pragma solidity ^0.8.24 <0.9.0; import "@franknft.eth/erc721-f/contracts/interfaces/IERC4883.sol"; import "@franknft.eth/erc721-f/contracts/token/ERC721/ERC721F.sol"; import "@franknft.eth/erc721-f/contracts/utils/BytesBuilder.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import "@openzeppelin/contracts/utils/Base64.sol"; /** * @title OnChainOptimized * @notice Example NFT collection (bunnies) with all metadata generated fully on-chain. * Demonstrates how to pack multiple discrete traits into a single `uint256` * using bitfield encoding, avoiding per-trait storage slots. * * @dev Trait layout — the collection has four trait categories: * * | Trait | Options | Bits needed | Bit positions in algorithmId | * |------------|---------|-------------|------------------------------| * | Glasses | 4 | 2 | bits 0, 2 | * | Purse | 4 | 2 | bits 1, 4 | * | Bracelet | 2 | 1 | bit 3 | * | Background | 6 | n/a | not in bitfield (modulo) | * * Total bitfield combinations: 4 × 4 × 2 = 32 → fits in 5 bits (values 0–31). * Background is distributed separately via `id % 6` so all 6 colours appear * uniformly across the 32 algorithmId values without consuming extra bits. * * The entire trait set for a token is stored as a single `uint256 algorithmId` * in `idToAlgorithmId[tokenId]`. A single SSTORE per mint covers all traits. * The alternative — one mapping per trait — would cost 4 SSTOREs per mint. * * @author @FrankNFT.eth */ contract OnChainOptimized is IERC4883, ERC721F { uint256 public constant MAX_TOKENS = 10; uint256 public constant MAX_PURCHASE = 10; uint256 public lastSelected = 0; // Accumulator: tracks the last algorithmId assigned; seeds next random draw. bool public saleIsActive; /** * @dev Maps each tokenId to its packed `algorithmId` (a 5-bit value, 0–31). * All four traits (glasses, purse, bracelet, background) are derived from * this single stored integer via bit extraction — no per-trait mappings needed. * One SSTORE per mint vs four if each trait were stored separately. */ mapping(uint256 => uint256) private idToAlgorithmId; error ContractsNotAllowed(); constructor() ERC721F("BunniesSamplingOwnAlgorithm", "OC", msg.sender) {} /** * Changes the state of saleIsActive from true to false and false to true */ function flipSaleState() external onlyOwner { saleIsActive = !saleIsActive; } /** * @notice Mints `numberOfTokens` tokens for `sender`. * * @dev Trait assignment uses a **running accumulator + random offset** pattern: * * `algorithmId = lastSelected + createRandomNumber(tokenId)` * * Adding the previous token's algorithmId as an offset prevents two tokens * minted in the same block from receiving identical traits even when * `block.timestamp` and `block.prevrandao` are identical (same block). * `lastSelected` is then updated to the new algorithmId, so each mint * cascades into the next. * * The result is stored in `idToAlgorithmId[tokenId]` — a single SSTORE that * encodes all three bitfield traits (glasses, purse, bracelet) at once. */ function mint(uint256 numberOfTokens) external { if (msg.sender.code.length != 0) revert ContractsNotAllowed(); require(saleIsActive, "Sale NOT active yet"); require(numberOfTokens != 0, "numberOfNfts cannot be 0"); require( numberOfTokens < MAX_PURCHASE, "Can only mint 9 tokens at a time" ); uint256 supply = totalSupply(); // number of tokens minted so far (used to derive next tokenId) require( supply + numberOfTokens <= MAX_TOKENS, "Purchase would exceed max supply of Tokens" ); unchecked { while (numberOfTokens != 0) { uint256 tokenId = supply + 1; uint256 algorithmId = lastSelected + createRandomNumber(tokenId); _mint(msg.sender, tokenId); idToAlgorithmId[tokenId] = algorithmId; lastSelected = algorithmId; supply++; numberOfTokens--; } } } /** * @notice Creates the tokenURI which contains the name, description, generated SVG image and token traits */ function tokenURI( uint256 tokenId ) public view override returns (string memory) { require(_exists(tokenId), "Non-Existing token"); string memory svgData = renderTokenById(tokenId); string memory traits = getTraits(tokenId); string memory json = Base64.encode( bytes( string( abi.encodePacked( '{"name": "', name(), " ", Strings.toString(tokenId), '", "description": "', getDescription(), '", "image": "data:image/svg+xml;base64,', Base64.encode(bytes(svgData)), bytes(traits).length == 0 ? '"' : '", "attributes": ', traits, "}" ) ) ) ); return string(abi.encodePacked("data:application/json;base64,", json)); } /** * @notice Generates a pseudo-random number in [0, 31] used as the trait seed. * * @dev The modulus 32 (= 2^5) matches the 5-bit bitfield: * 4 purses × 4 glasses × 2 bracelets = 32 possible trait combinations. * `% 32` therefore maps evenly onto every valid algorithmId without waste. * * Entropy sources mixed via keccak256: * - `block.timestamp` — changes each block, varies between transactions * - `block.prevrandao` — RANDAO beacon value from the previous block (EVM Cancun) * - `tokenId` — unique per token, prevents collisions within a batch * - `msg.sender` — caller address, differs between wallets * * @dev WARNING: This is not cryptographically secure randomness. A validator * can influence `prevrandao` at some cost. For a production collection * with high-value traits, use Chainlink VRF (see `examples/ChainLink.sol`). */ function createRandomNumber(uint256 tokenId) public view returns (uint256) { unchecked { uint256 random = uint256( keccak256( abi.encodePacked( block.timestamp, block.prevrandao, tokenId, msg.sender ) ) ) % 32; return random; } } /** * @notice Overridden function which creates custom SVG image * @dev `id` changes the displayed name in `parts[3]`, every 100th you get mew */ function renderTokenById( uint256 tokenId ) public view override returns (string memory) { require(_exists(tokenId), "Non-Existing token"); //header1, header2, body, rest, footer string[5] memory frame = [ '', '', '', "" ]; uint256 algorithmId = idToAlgorithmId[tokenId]; // Converting `string memory` to `bytes memory` is a free cast, so these // reuse the allocations `frame` and the trait getters already made. bytes memory background = bytes( getBackground(getBackgroundId(algorithmId)) ); bytes memory glasses = bytes(getGlasses(getGlassesId(algorithmId))); bytes memory bracelet = bytes(getBracelet(getBraceletId(algorithmId))); bytes memory purse = bytes(getPurse(getPurseId(algorithmId))); // Accumulated rather than written as one expression: the legacy codegen // runs out of stack slots on a nine-term sum alongside these locals. uint256 cap = background.length + glasses.length; cap += bracelet.length + purse.length; for (uint256 i; i < 5; ++i) { cap += bytes(frame[i]).length; } (bytes memory out, uint256 ptr) = BytesBuilder.start(cap); ptr = BytesBuilder.append(ptr, bytes(frame[0])); ptr = BytesBuilder.append(ptr, background); ptr = BytesBuilder.append(ptr, bytes(frame[1])); ptr = BytesBuilder.append(ptr, bytes(frame[2])); ptr = BytesBuilder.append(ptr, glasses); ptr = BytesBuilder.append(ptr, bytes(frame[3])); ptr = BytesBuilder.append(ptr, bracelet); ptr = BytesBuilder.append(ptr, purse); ptr = BytesBuilder.append(ptr, bytes(frame[4])); BytesBuilder.finish(out, ptr); return string(out); } /** * @notice Overridden function to show how to disable traits being included in the URI * Optional: with mapping int to text * Lot of extra work because recreate bunnie * Does not print out integer */ function getTraits(uint256 tokenId) public view returns (string memory) { require(_exists(tokenId), "Non-Existing token"); string memory tr1 = '[{"trait_type": "Background","value": "'; string memory tr2 = '"},{"trait_type": "Bracelet","value": "'; string memory tr3 = '"},{"trait_type": "Glasses","value": "'; string memory tr4 = '"},{"trait_type": "Purse","value": "'; string memory tr5 = '"}]'; uint256 algorithmId = idToAlgorithmId[tokenId]; // The four ids are bounded well below 10 (background id % 6, bracelet // 0-1, glasses and purse 0-3), so one digit each; 8 leaves slack and // finish() reverts rather than overrunning if that ever stops holding. uint256 cap = 8 + bytes(tr1).length + bytes(tr2).length + bytes(tr3).length + bytes(tr4).length + bytes(tr5).length; (bytes memory out, uint256 ptr) = BytesBuilder.start(cap); ptr = BytesBuilder.append(ptr, bytes(tr1)); ptr = BytesBuilder.appendNumber(ptr, getBackgroundId(algorithmId)); ptr = BytesBuilder.append(ptr, bytes(tr2)); ptr = BytesBuilder.appendNumber(ptr, getBraceletId(algorithmId)); ptr = BytesBuilder.append(ptr, bytes(tr3)); ptr = BytesBuilder.appendNumber(ptr, getGlassesId(algorithmId)); ptr = BytesBuilder.append(ptr, bytes(tr4)); ptr = BytesBuilder.appendNumber(ptr, getPurseId(algorithmId)); ptr = BytesBuilder.append(ptr, bytes(tr5)); BytesBuilder.finish(out, ptr); return string(out); } /** * @notice Overridden function to display description change in tokenURI */ function getDescription() public pure returns (string memory) { return string(abi.encodePacked(" - Overwrote description")); } /** * @notice Returns the background colour index for a given `algorithmId`. * * @dev Background is NOT encoded in the bitfield. The 5 bitfield bits are fully * consumed by glasses (2 bits), purse (2 bits), and bracelet (1 bit). * Adding a 6th background option would require a 3-bit slot (8 values) but * there are only 6 backgrounds — wasteful. * * Instead, background is derived with `id % 6`, which distributes evenly * across all 32 bitfield values (32 = 5 × 6 + 2, so IDs 0 and 1 appear once * more than the rest — a tiny bias that is acceptable for an example). * * This keeps the per-trait bit positions clean and avoids gaps. */ function getBackgroundId( uint256 id ) public pure returns (uint256 backgroundId) { return id % 6; } /** * @notice Extracts the purse variant (0–3) from the packed `algorithmId`. * * @dev Purse uses bits 1 and 4 of the algorithmId (0-indexed from LSB): * * bit1 = (id >> 1) & 1 → bit at position 1 * bit2 = (id >> 4) & 1 → bit at position 4 * purseId = (bit2 << 1) | bit1 → recombined as a 2-bit value [0, 3] * * The two bits are non-adjacent so they do not overlap with glasses (bits 0, 2) * or bracelet (bit 3). Every bit in positions 0–4 is used by exactly one trait. */ function getPurseId(uint256 id) public pure returns (uint256 purseId) { uint256 bit1 = (id >> 1) & 1; uint256 bit2 = (id >> 4) & 1; return ((bit2 << 1) | bit1); } /** * @notice Extracts the bracelet variant (0 or 1) from the packed `algorithmId`. * * @dev Bracelet occupies bit 3 only — a single-bit extract: * * braceletId = (id >> 3) & 1 → 0 or 1 * * Two bracelet colours map to these values in `getBracelet()`. */ function getBraceletId( uint256 id ) public pure returns (uint256 braceletId) { uint256 bit1 = (id >> 3) & 1; return bit1; } /** * @notice Extracts the glasses variant (0–3) from the packed `algorithmId`. * * @dev Glasses uses bits 0 and 2 of the algorithmId: * * bit1 = id & 1 → bit at position 0 (LSB) * bit2 = (id >> 2) & 1 → bit at position 2 * glassesId = (bit2 << 1) | bit1 → recombined as a 2-bit value [0, 3] * * Bit 1 is skipped here because it belongs to purse, so glasses bits * are positions 0 and 2 (non-contiguous). */ function getGlassesId(uint256 id) public pure returns (uint256 glassesId) { uint256 bit1 = id & 1; uint256 bit2 = (id >> 2) & 1; return ((bit2 << 1) | bit1); } /** * 2 colors represent the bracelets */ function getBracelet( uint256 braceletId ) public pure returns (string memory) { string[2] memory braceletColors = ["FFF093", "05faf6"]; string[2] memory bracelet = [ '' ]; return string( abi.encodePacked( bracelet[0], braceletColors[braceletId % 2], bracelet[1] ) ); } function getPurse(uint256 purseId) public pure returns (string memory) { string[2] memory upperPartPurse = [ '', '' ]; string memory lowerPartPurse = '', 'fill="#9F87FB"/>', '', '' ]; if (purseId == 0) { return string(abi.encodePacked(upperPartPurse[1], designPurse[3])); } else if (purseId == 1) { return string(abi.encodePacked(upperPartPurse[1], designPurse[2])); } else { return string( abi.encodePacked( upperPartPurse[0], lowerPartPurse, designPurse[purseId - 2] ) ); } } function getGlasses(uint256 glassesId) public pure returns (string memory) { string[4] memory glassesColors = [ "ed4949", "ffff56", "ff7f00", "00bf00" ]; string[4] memory bigGlasses = [ '' ]; bytes memory colorTwo = bytes(glassesColors[glassesId % 2]); bytes memory colorFour = bytes(glassesColors[glassesId % 4]); uint256 cap = colorTwo.length + (colorFour.length * 2); for (uint256 i; i < 4; ++i) { cap += bytes(bigGlasses[i]).length; } (bytes memory out, uint256 ptr) = BytesBuilder.start(cap); ptr = BytesBuilder.append(ptr, bytes(bigGlasses[0])); ptr = BytesBuilder.append(ptr, colorTwo); ptr = BytesBuilder.append(ptr, bytes(bigGlasses[1])); ptr = BytesBuilder.append(ptr, colorFour); ptr = BytesBuilder.append(ptr, bytes(bigGlasses[2])); ptr = BytesBuilder.append(ptr, colorFour); ptr = BytesBuilder.append(ptr, bytes(bigGlasses[3])); BytesBuilder.finish(out, ptr); return string(out); } /** * 6 background colors */ function getBackground(uint256 id) public pure returns (string memory) { string[6] memory colors = [ "CEC9DF", "FDF1CA", "EDCCB6", "B2C6DE", "E1E1E1", "C8DCB8" ]; //return ''; return string( abi.encodePacked(' style=" background-color:#', colors[id % 6]) ); } }