// 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(
""
);
}
function _svgMoneyVelocity() internal pure returns (string memory) {
return string.concat(
""
);
}
function _svgMoneyMultiplier() internal pure returns (string memory) {
return string.concat(
""
);
}
function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721URIStorage, AccessControl) returns (bool) {
return super.supportsInterface(interfaceId) || AccessControl.supportsInterface(interfaceId);
}
}