// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /** * @title FORGE ART — On-Chain SACM Agent Generative NFT * @notice Each token is a unique piece of ASCII art generated by a SnapKitty * SACM agent, sealed to the WORM chain, stored entirely on-chain. * No IPFS. No external dependencies. Art lives on Base forever. * * @dev ERC-721 with fully on-chain SVG tokenURI. * Mint is gated by MINTER_ROLE — only the FORGE settlement bridge mints. * Each mint records: agent name, WORM chain tip, SVG art, token seed. * Art is deterministically reproducible from (agent, wormTip, tokenId). * * @custom:deployed-by SnapKitty Collective LLC — EIN 41-5105572 * @custom:holding-entity Bel Esprit D'Accord Trust — EIN 41-6630640 * @custom:legal UTILITY COLLECTIBLE ONLY. Not a security or investment instrument. */ import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/Base64.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; contract FORGEART is ERC721, ERC721Burnable, AccessControl { bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant ARCHITECT_ROLE = keccak256("ARCHITECT_ROLE"); uint256 public nextTokenId; uint256 public constant MAX_SUPPLY = 2_100; // 1/10 of FORGE cap — scarce struct ArtToken { string agent; // NOVA | ENKI | CIPHER | VAULT string wormTip; // WORM chain tip at mint time (provenance) string tokenSeed; // deterministic seed for off-chain reproduction string svgArt; // full SVG stored on-chain uint256 mintedAt; } mapping(uint256 => ArtToken) public artTokens; mapping(string => uint256) public agentMintCount; event ArtMinted( uint256 indexed tokenId, address indexed recipient, string agent, string tokenSeed, string wormTip ); constructor(address architect, address minter) ERC721("FORGE ART", "FART") { require(architect != address(0), "FORGEART: zero architect"); require(minter != address(0), "FORGEART: zero minter"); _grantRole(DEFAULT_ADMIN_ROLE, architect); _grantRole(ARCHITECT_ROLE, architect); _grantRole(MINTER_ROLE, minter); } /** * @notice Mint a new FORGE ART token with fully on-chain SVG art. * @param recipient Address receiving the NFT * @param agent Agent name — "NOVA", "ENKI", "CIPHER", or "VAULT" * @param wormTip WORM chain tip at time of sealing (provenance anchor) * @param tokenSeed Deterministic seed — keccak256(agent:wormTip:tokenId) * @param svgArt Complete SVG string to store on-chain */ function mintArt( address recipient, string calldata agent, string calldata wormTip, string calldata tokenSeed, string calldata svgArt ) external onlyRole(MINTER_ROLE) { require(nextTokenId < MAX_SUPPLY, "FORGEART: max supply reached"); require(recipient != address(0), "FORGEART: zero recipient"); require(bytes(agent).length > 0, "FORGEART: empty agent"); require(bytes(svgArt).length > 0, "FORGEART: empty art"); uint256 tokenId = nextTokenId++; artTokens[tokenId] = ArtToken({ agent: agent, wormTip: wormTip, tokenSeed: tokenSeed, svgArt: svgArt, mintedAt: block.timestamp }); agentMintCount[agent]++; _safeMint(recipient, tokenId); emit ArtMinted(tokenId, recipient, agent, tokenSeed, wormTip); } /** * @notice Returns fully on-chain tokenURI as a base64-encoded data URI. */ function tokenURI(uint256 tokenId) public view override returns (string memory) { _requireOwned(tokenId); ArtToken storage art = artTokens[tokenId]; string memory svgBase64 = Base64.encode(bytes(art.svgArt)); string memory json = string(abi.encodePacked( '{"name":"FORGE ART #', Strings.toString(tokenId), ' - ', art.agent, '","description":"On-chain generative ASCII art sealed to the SnapKitty SACM WORM chain. Agent: ', art.agent, '. Every pixel is a proof of productive work.","image":"data:image/svg+xml;base64,', svgBase64, '","attributes":[', '{"trait_type":"Agent","value":"', art.agent, '"},', '{"trait_type":"WORM Tip","value":"', art.wormTip, '"},', '{"trait_type":"Token Seed","value":"', art.tokenSeed, '"},', '{"trait_type":"Minted At","value":', Strings.toString(art.mintedAt), '}', ']}' )) ; return string(abi.encodePacked( 'data:application/json;base64,', Base64.encode(bytes(json)) )); } function remainingSupply() external view returns (uint256) { return MAX_SUPPLY - nextTokenId; } function supportsInterface(bytes4 interfaceId) public view override(ERC721, AccessControl) returns (bool) { return super.supportsInterface(interfaceId); } }