113 lines
5.7 KiB
Solidity
113 lines
5.7 KiB
Solidity
// 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(
|
||
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 120' width='320' height='120'>",
|
||
"<rect width='320' height='120' fill='#1a1a2e' rx='8'/>",
|
||
"<text x='160' y='42' text-anchor='middle' fill='#eaeaea' font-family='sans-serif' font-size='14'>Money Supply (M) - GRU layers</text>",
|
||
"<text x='160' y='68' text-anchor='middle' fill='#a0e0a0' font-family='monospace' font-size='16'>1 M00 = 5 M0 = 25 M1</text>",
|
||
"<text x='160' y='92' text-anchor='middle' fill='#888' font-family='sans-serif' font-size='11'>M00 base | M0 collateral | M1 credit</text>",
|
||
"</svg>"
|
||
);
|
||
}
|
||
|
||
function _svgMoneyVelocity() internal pure returns (string memory) {
|
||
return string.concat(
|
||
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 120' width='320' height='120'>",
|
||
"<rect width='320' height='120' fill='#1a1a2e' rx='8'/>",
|
||
"<text x='160' y='42' text-anchor='middle' fill='#eaeaea' font-family='sans-serif' font-size='14'>Money Velocity (V)</text>",
|
||
"<text x='160' y='72' text-anchor='middle' fill='#a0c0ff' font-family='serif' font-size='20'>M * V = P * Y</text>",
|
||
"<text x='160' y='98' text-anchor='middle' fill='#888' font-family='sans-serif' font-size='10'>Equation of exchange</text>",
|
||
"</svg>"
|
||
);
|
||
}
|
||
|
||
function _svgMoneyMultiplier() internal pure returns (string memory) {
|
||
return string.concat(
|
||
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 120' width='320' height='120'>",
|
||
"<rect width='320' height='120' fill='#1a1a2e' rx='8'/>",
|
||
"<text x='160' y='42' text-anchor='middle' fill='#eaeaea' font-family='sans-serif' font-size='14'>Money Multiplier (m)</text>",
|
||
"<text x='160' y='72' text-anchor='middle' fill='#ffc0a0' font-family='serif' font-size='18'>m = Reserve / Supply = 1.0</text>",
|
||
"<text x='160' y='98' text-anchor='middle' fill='#888' font-family='sans-serif' font-size='10'>No fractional reserve (GRU / ISO4217W)</text>",
|
||
"</svg>"
|
||
);
|
||
}
|
||
|
||
function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721URIStorage, AccessControl) returns (bool) {
|
||
return super.supportsInterface(interfaceId) || AccessControl.supportsInterface(interfaceId);
|
||
}
|
||
}
|