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
andERC721Enumerable
from OpenZeppelin to define NFT functionality and enable enumeration of token IDs.Ownable
to manage contract ownership and restrict privileged functions.Utils
andSVGRenderer
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 theSVGRenderer
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 symbolNFT
.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 exceededMAX_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.
Subscribe to my newsletter
Read articles from Ayoola Victor directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
