NFTs have changed the digital ecosystem, allowing creators to tokenize art, music, and more. However, most NFTs store their metadata (image, name, and description) off-chain on IPFS(Pinata) or centralized servers.
But what if everything—including the artwork—was stored on-chain? That’s exactly what on-chain NFTs do. These NFTs embed their SVG image and metadata directly into the Ethereum blockchain, ensuring they last forever without relying on external storage.
In this guide, we’ll explore how to build an on-chain NFT that generates an SVG image within Solidity, making it fully decentralized.
Let’s rock 🚀
Prerequisites
Before diving in, you should be familiar with:
✔️ Solidity basics (functions, mappings)
✔️ ERC-721 standard for NFTs
✔️ Base64 encoding (used for embedding metadata)
✔️ SVG (Scalable Vector Graphics) to create on-chain images
Understanding the smart contract
So basically, our smart contract is a NFT smart contract storing the metadata of the nfts which include the json and the image on the blockchain, this is why they are called OnChainNFT.
Let’s break the code down step by step, are you scared? don’t worry, this is very easy and straightforward.
🚀Importing the necessary libraries
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
ERC721URIStorage → Gives us NFT storage functions,
ERC721URIStorage
is an extension ofERC721
that adds storage for the tokenURI inside the contract.Ownable → Restricts certain functions to only the contract owner.
Base64 & Strings → Help with encoding metadata and formatting numbers as strings.
🚀Contract Initialization
contract onchainNFT is ERC721URIStorage, Ownable {
uint256 tokenIdCounter;
constructor() ERC721("DripNFT", "DNFT") Ownable(msg.sender) {}
ERC721("DripNFT", "DNFT") → Sets the NFT name as DripNFT with the symbol DNFT.
Ownable(msg.sender) → The person who deploys the contract is the owner.
tokenIdCounter → Keeps track of how many NFTs have been minted.
🚀Minting NFTs
function mint(address _to) external onlyOwner {
tokenIdCounter++;
uint256 newTokenId = tokenIdCounter;
_mint(_to, newTokenId);
}
onlyOwner → Only the contract owner can mint NFTs.
tokenIdCounter++ → Increments the token count to assign a unique ID to each NFT.
mint(to, newTokenId) → Mints the NFT to the recipient’s wallet.
🚀The tokenURI
Function: The Heart of the On-Chain NFT
This is the most important part of our contract! It defines:
✅ The SVG image (stored as a string)
✅ Metadata (name, description, attributes)
✅ Base64 encoding to make it readable by NFT marketplaces
function tokenURI(uint256 tokenId) public pure override returns (string memory) {
string memory svg = "<svg xmlns='http://www.w3.org/2000/svg' width='500' height='500'>"
"<rect width='500' height='500' fill='linear-gradient(135deg, #ff9a9e, #fad0c4)'/>"
"<text x='50%' y='50%' font-family='Arial' font-size='48' fill='white' font-weight='bold' text-anchor='middle' alignment-baseline='middle'>DripNFT</text>"
"</svg>";
string memory imageURI = string(
abi.encodePacked(
"data:image/svg+xml;base64,",
Base64.encode(bytes(svg))
)
);
string memory json = string(
abi.encodePacked(
'{"name": "DripNFT #',
Strings.toString(tokenId),
'", "description": "Let us drip, one love", "image": "',
imageURI,
'", "attributes": [{"trait_type": "Style", "value": "Neon"}, {"trait_type": "Background", "value": "Black"}]}'
)
);
return string(
abi.encodePacked(
"data:application/json;base64,",
Base64.encode(bytes(json))
)
);
}
🍾Breaking It Down Step by Step
1️⃣ Creating the SVG Image
string memory svg = "<svg xmlns='http://www.w3.org/2000/svg' width='500' height='500'>"
"<rect width='500' height='500' fill='linear-gradient(135deg, #ff9a9e, #fad0c4)'/>"
"<text x='50%' y='50%' font-family='Arial' font-size='48' fill='white' font-weight='bold' text-anchor='middle' alignment-baseline='middle'>DripNFT</text>"
"</svg>";
if you are good with creating svgs, you can create your own nft, it is basically html and css and if you cant you can visit a site like maketext.io
2️⃣ Encoding the SVG to Base64
NFT marketplaces don’t understand raw SVG, so we Base64 encode it.
string memory imageURI = string(
abi.encodePacked(
"data:image/svg+xml;base64,",
Base64.encode(bytes(svg))
)
);
Base64.encode(bytes(svg))
→ Converts SVG into Base64 format.data:image/svg+xml;base64,
→ Prefix so browsers understand it's an image.
3️⃣ Creating the Metadata (JSON)
We define the NFT name, description, image, and attributes inside a JSON structure.
string memory json = string(
abi.encodePacked(
'{"name": "DripNFT #',
Strings.toString(tokenId),
'", "description": "Let us drip, one love", "image": "',
imageURI,
'", "attributes": [{"trait_type": "Style", "value": "Neon"}, {"trait_type": "Background", "value": "Black"}]}'
)
);
name →
"DripNFT #X"
(X is the tokenId)description →
"Let us drip, one love"
image → Embedded Base64 SVG
attributes → NFT traits (Style: Neon, Background: Black)
4️⃣ Encoding the Metadata to Base64
return string(
abi.encodePacked(
"data:application/json;base64,",
Base64.encode(bytes(json))
)
);
Converts the JSON metadata to Base64.
Prefixed with
data:application/json;base64,
so marketplaces can parse it.
🚀Putting everything together
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
contract onchainNFT is ERC721URIStorage, Ownable {
uint256 tokenIdCounter;
constructor() ERC721("DripNFT", "DNFT") Ownable(msg.sender) {}
function mint(address _to) external onlyOwner {
tokenIdCounter++;
uint256 newTokenId = tokenIdCounter;
_mint(_to, newTokenId);
}
function tokenURI(
uint256 tokenId
) public pure override returns (string memory) {
string
memory svg = "<svg xmlns='http://www.w3.org/2000/svg' width='500' height='500'>"
"<rect width='500' height='500' fill='linear-gradient(135deg, #ff9a9e, #fad0c4)'/>"
"<text x='50%' y='50%' font-family='Arial' font-size='48' fill='white' font-weight='bold' text-anchor='middle' alignment-baseline='middle'>DripNFT</text>"
"</svg>";
string memory imageURI = string(
abi.encodePacked(
"data:image/svg+xml;base64,",
Base64.encode(bytes(svg))
)
);
string memory json = string(
abi.encodePacked(
'{"name": "DripNFT #',
Strings.toString(tokenId),
'", "description": "Let us drip, one love", "image": "',
imageURI,
'", "attributes": [{"trait_type": "Style", "value": "Neon"}, {"trait_type": "Background", "value": "Black"}]}'
)
);
return
string(
abi.encodePacked(
"data:application/json;base64,",
Base64.encode(bytes(json))
)
);
}
}
So with this little code, you have sucessfully built a fully on-chain NFT that stores the metadata on chain.
Displaying how it looks like on a NFT marketplace(Opensea)
This is the link to the onChainNFT https://testnets.opensea.io/assets/sepolia/0x11841e747515b3e8e445d7ea5200eae96d554d8b/1
I hope you now understand how onchainNFT works and how you can create one for yourself.
Leaving you at this point, go apply to openzeppelin and show them my article, the job is yours!