How to Create On-Chain SVG NFTs with Solidity: A Step-by-Step Guide

Introduction

Non-Fungible Tokens (NFTs) have transformed digital ownership, and on-chain NFTs take this innovation even further by storing metadata and artwork directly on the blockchain. In this article, we will explore a Solidity smart contract that creates an on-chain SVG NFT, ensuring its metadata and image are permanently stored without relying on external services like IPFS.

Understanding the Smart Contract

The NFTManager contract is an ERC-721 implementation that generates and stores NFT metadata on-chain using SVG rendering. Below, we analyze its components step by step.

Contract Imports and Inheritance

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

import {Utils} from "./Utils.sol";
import {SVGRenderer} from "./SvgRenderer.sol";

This contract relies on several dependencies:

  • ERC721 and ERC721Enumerable from OpenZeppelin to define NFT functionality and enable enumeration of token IDs.

  • Ownable to manage contract ownership and restrict privileged functions.

  • Utils and SVGRenderer for encoding and generating SVG artwork dynamically.

Defining the Contract

contract NFTManager is ERC721, ERC721Enumerable, Ownable {
    uint256 private _nextTokenId;
    uint256 public constant MAX_NFT_ITEMS = 121;
    SVGRenderer public renderer;

This contract extends ERC721 and ERC721Enumerable for NFT support and enumeration. Key variables include:

  • _nextTokenId: Tracks the next available token ID for minting.

  • MAX_NFT_ITEMS: Defines the maximum number of NFTs that can be minted.

  • renderer: A reference to the SVGRenderer contract responsible for generating the on-chain SVG images.

Constructor and Initial Minting

constructor(uint256 _reservedForTeam) ERC721("MYNFT", "NFT") Ownable(msg.sender) {
    renderer = new SVGRenderer();

    for (uint i = 0; i < _reservedForTeam; i++) {
        uint256 tokenId = _nextTokenId++;
        _safeMint(msg.sender, tokenId);
    }
}
  • The constructor initializes the NFT collection with the name MYNFT and symbol NFT.

  • A new instance of SVGRenderer is created to handle SVG generation.

  • A specified number of NFTs (_reservedForTeam) are minted and assigned to the contract owner at deployment.

Minting New NFTs

modifier mintIsOpen() {
    require(totalSupply() < MAX_NFT_ITEMS, "Mint has ended");
    _;
}

function safeMint(address to) public payable mintIsOpen {
    uint256 tokenId = _nextTokenId++;
    _safeMint(to, tokenId);
}
  • mintIsOpen: Ensures that minting can only occur if the total supply has not exceeded MAX_NFT_ITEMS.

  • safeMint: Mints a new NFT to the specified address while maintaining unique token IDs.

Generating On-Chain Metadata and SVG Images

function tokenURI(uint256 tokenId) public view override returns (string memory) {
    _requireOwned(tokenId);
    return renderAsDataUri(tokenId);
}

function renderAsDataUri(uint256 _tokenId) public view returns (string memory) {
    string memory svg;
    string memory attributes;
    (svg, attributes) = renderer.renderSVG(_tokenId);

    string memory image = string.concat(
        '"image":"data:image/svg+xml;base64,',
        Utils.encode(bytes(svg)),
        '"'
    );

    string memory json = string.concat(
        '{"name":"My Onchain NFT #',
        Utils.toString(_tokenId),
        '","description":"This is My NFT",',
        attributes,
        ',',
        image,
        '}'
    );

    return string.concat("data:application/json;base64,", Utils.encode(bytes(json)));
}
  • tokenURI: Returns the token metadata as a Base64-encoded JSON string.

  • renderAsDataUri: Generates an on-chain SVG and encodes it in Base64, making it directly accessible from the blockchain.

  • The metadata includes the name, description, attributes, and Base64-encoded SVG image.

Overriding ERC-721 Functions for Compatibility

function _update(address to, uint256 tokenId, address auth) internal override(ERC721, ERC721Enumerable) returns (address) {
    return super._update(to, tokenId, auth);
}

function _increaseBalance(address account, uint128 value) internal override(ERC721, ERC721Enumerable) {
    super._increaseBalance(account, value);
}

function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool) {
    return super.supportsInterface(interfaceId);
}

These functions ensure compatibility with both ERC721 and ERC721Enumerable, preventing conflicts between inherited classes.

Conclusion

This contract effectively demonstrates how to store NFT metadata entirely on-chain, ensuring durability and decentralization. By leveraging SVG rendering, it eliminates the need for external storage, providing a fully verifiable on-chain asset.