// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/Base64.sol"; /** * @title GRUFormulasNFT * @notice ERC-721 NFT depicting the three GRU-related monetary formulas as on-chain SVG graphics * @dev Token IDs: 0 = Money Supply (GRU M00/M0/M1), 1 = Money Velocity (M×V=P×Y), 2 = Money Multiplier (m=1.0) */ contract GRUFormulasNFT is ERC721, ERC721URIStorage, AccessControl { bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); uint256 public constant TOKEN_ID_MONEY_SUPPLY = 0; uint256 public constant TOKEN_ID_MONEY_VELOCITY = 1; uint256 public constant TOKEN_ID_MONEY_MULTIPLIER = 2; uint256 public constant MAX_TOKEN_ID = 2; constructor(address admin) ERC721("GRU Formulas", "GRUF") { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(MINTER_ROLE, admin); } /** * @notice Mint one of the three formula NFTs (tokenId 0, 1, or 2) * @param to Recipient * @param tokenId 0 = Money Supply, 1 = Money Velocity, 2 = Money Multiplier */ function mint(address to, uint256 tokenId) external onlyRole(MINTER_ROLE) { require(tokenId <= MAX_TOKEN_ID, "GRUFormulasNFT: invalid tokenId"); _safeMint(to, tokenId); } function _baseURI() internal pure override returns (string memory) { return ""; } /** * @notice Returns metadata URI with on-chain SVG image for the formula * @param tokenId 0 = Money Supply, 1 = Money Velocity, 2 = Money Multiplier */ function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) { require(_ownerOf(tokenId) != address(0), "ERC721: invalid token ID"); (string memory name, string memory description, string memory svg) = _formulaData(tokenId); string memory imageData = string.concat("data:image/svg+xml;base64,", Base64.encode(bytes(svg))); string memory json = string.concat( '{"name":"', name, '","description":"', description, '","image":"', imageData, '"}' ); return string.concat("data:application/json;base64,", Base64.encode(bytes(json))); } function _formulaData(uint256 tokenId) internal pure returns (string memory name, string memory description, string memory svg) { if (tokenId == TOKEN_ID_MONEY_SUPPLY) { name = "GRU Money Supply (M)"; description = "GRU monetary layers: 1 M00 = 5 M0 = 25 M1 (base, collateral, credit). Non-ISO synthetic unit of account."; svg = _svgMoneySupply(); } else if (tokenId == TOKEN_ID_MONEY_VELOCITY) { name = "Money Velocity (V)"; description = "Equation of exchange: M x V = P x Y (money supply, velocity, price level, output)."; svg = _svgMoneyVelocity(); } else if (tokenId == TOKEN_ID_MONEY_MULTIPLIER) { name = "Money Multiplier (m)"; description = "m = Reserve / Supply; GRU and ISO4217W enforce m = 1.0 (no fractional reserve)."; svg = _svgMoneyMultiplier(); } else { revert("GRUFormulasNFT: invalid tokenId"); } } function _svgMoneySupply() internal pure returns (string memory) { return string.concat( "", "", "Money Supply (M) - GRU layers", "1 M00 = 5 M0 = 25 M1", "M00 base | M0 collateral | M1 credit", "" ); } function _svgMoneyVelocity() internal pure returns (string memory) { return string.concat( "", "", "Money Velocity (V)", "M * V = P * Y", "Equation of exchange", "" ); } function _svgMoneyMultiplier() internal pure returns (string memory) { return string.concat( "", "", "Money Multiplier (m)", "m = Reserve / Supply = 1.0", "No fractional reserve (GRU / ISO4217W)", "" ); } function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721URIStorage, AccessControl) returns (bool) { return super.supportsInterface(interfaceId) || AccessControl.supportsInterface(interfaceId); } }