Introduction
In the world of NFTs, most digital assets rely on off-chain storage solutions like IPFS or centralized servers for storing metadata and images. But what if you could store everything directly on the blockchain—including the NFT artwork?
In this article, we’ll walk through the creation of an on-chain NFT using Solidity. This NFT contract generates SVG images dynamically, encodes them in Base64, and serves them directly from the smart contract—ensuring permanence and full decentralization.
By the end, you'll understand how to:
- Generate on-chain SVG images for NFTs
- Use Base64 encoding to store images within smart contracts
- Implement a fully decentralized ERC-721 contract
Let's get into it!
Understanding On-Chain NFTs
Traditional NFTs store images and metadata off-chain, meaning if a hosting service goes down, the NFT might lose its artwork. On-chain NFTs, on the other hand, embed all data within the blockchain, ensuring that they remain accessible as long as Ethereum exists.
In this tutorial, we’ll create a fully on-chain NFT that:
- Uses Solidity to generate an SVG image
- Stores both the image and metadata directly on the blockchain
- Mints NFTs dynamically without relying on external storage
This means that every NFT image is programmatically generated and stored directly inside the smart contract.
The Smart Contract
Here’s the full Solidity code for our on-chain NFT contract.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
contract OnChainNFT is ERC721 {
using Strings for uint;
uint256 public tokenId;
constructor() ERC721 ("ceceNFT", "cece") {
tokenId = 1;
}
event NFTMinted(address indexed owner, uint256 tokenId);
function generateSVG() internal pure returns (string memory) {
string memory svg = string(
abi.encodePacked(
'<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500">',
'<rect width="100%" height="100%" fill="black" />',
'<circle cx="250" cy="250" r="210" fill="orange" />',
'<circle cx="250" cy="250" r="190" fill="white" />',
'<circle cx="250" cy="250" r="170" fill="red" />',
'<circle cx="250" cy="250" r="150" fill="yellow" />',
'<circle cx="250" cy="250" r="130" fill="green" />',
'<circle cx="250" cy="250" r="110" fill="blue" />',
'<circle cx="250" cy="250" r="90" fill="purple" />',
'<circle cx="250" cy="250" r="70" fill="pink" />',
'<circle cx="250" cy="250" r="50" fill="brown" />',
'<circle cx="250" cy="250" r="30" fill="black" />',
'</svg>'
));
return string(abi.encodePacked("data:image/svg+xml;base64,", Base64.encode(bytes(svg))));
}
function mintNFT() public {
tokenId++;
_safeMint(msg.sender, tokenId);
emit NFTMinted(msg.sender, tokenId);
}
function tokenURI(uint256 tokenTxId) public view override returns (string memory) {
ownerOf(tokenTxId);
string memory name = string(abi.encodePacked("cece ", Strings.toString(tokenTxId)));
string memory description = "My NFT";
string memory image = generateSVG();
string memory json = string(
abi.encodePacked(
'{"name": "', name, '", ',
'"description": "', description, '", ',
'"image": "', image, '"}'
)
);
return string(abi.encodePacked("data:application/json;base64,", Base64.encode(bytes(json))));
}
}
Now, let’s break this contract down step by step.
1. Contract Setup
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
- ERC721.sol → Standard implementation for NFTs.
- Base64.sol → Used to encode images & metadata in Base64 format.
- Strings.sol → Helps convert numbers to string format.
If you haven't installed OpenZeppelin yet, run in your terminal
npm install @openzeppelin/contracts
2. Generating the SVG Image
function generateSVG() internal pure returns (string memory) {
- This function creates the SVG artwork for the NFT.
- Each circle inside the SVG creates a layered bullseye effect.
- The final SVG is Base64 encoded so it can be stored directly in the smart contract.
Example Output of generateSVG()
data:image/svg+xml;base64,PHN2ZyB...
This can be directly displayed in browsers or NFT marketplaces.
3. Minting the NFT
function mintNFT() public {
tokenId++;
_safeMint(msg.sender, tokenId);
emit NFTMinted(msg.sender, tokenId);
}
- Calls _safeMint() to safely assign a new NFT to msg.sender.
- Uses an incrementing tokenId to ensure each NFT has a unique identifier.
- Emits an event to log the minting.
4. Generating Metadata for Marketplaces
function tokenURI(uint256 tokenTxId) public view override returns (string memory) {
- Generates the NFT metadata in JSON format.
- Encodes everything in Base64 to be fully on-chain.
Example Output (for tokenTxId = 1)
{
"name": "cece 1",
"description": "My NFT",
"image": "data:image/svg+xml;base64,PHN2ZyB..."
}
NB - This metadata is formatted to match OpenSea & other marketplaces.
Why On-Chain NFTs Are Revolutionary
Unlike traditional NFTs that rely on external servers or IPFS, on-chain NFTs ensure:
- Permanence → Your NFT never disappears (even if IPFS goes offline).
- Decentralization → No third-party hosting needed.
- Immutability → The image & metadata live forever on the blockchain.
Deploying Your On-Chain NFT to Base Sepolia
Now that we've built our fully on-chain NFT contract, let's deploy it to Base Sepolia, a testnet optimized for Ethereum L2 solutions.
Setting Up Your Deployment Script (deploy.ts)
Create a new file inside the scripts/ folder:
scripts/deploy.ts
`import { ethers } from "hardhat";
async function main() {
console.log("Deploying NFT contract...");
const NFT = await ethers.getContractFactory("OnChainNFT");
const nft = await NFT.deploy();
await nft.waitForDeployment();
const contractAddress = await nft.getAddress();
console.log("Minting first NFT...");
const tx = await nft.mintNFT();
await tx.wait();
console.log("NFT minted at address:", contractAddress);
}
main().catch((error) => {
console.error("deployment failed:",error);
process.exitCode = 1;
});
`
This script does the following:
- Compiles the NFT contract
- Deploys it to the Base Sepolia network
- Mints the first NFT automatically after deployment
Configuring Hardhat for Base Sepolia
Now, update your Hardhat configuration to support Base Sepolia.
hardhat.config.ts
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "@nomicfoundation/hardhat-verify";
import "dotenv/config"; // Use dotenv for environment variables
const {
ALCHEMY_BASE_SEPOLIA_API_KEY_URL,
ACCOUNT_PRIVATE_KEY,
ETHERSCAN_API_KEY
} = process.env;
const config: HardhatUserConfig = {
solidity: "0.8.28",
networks: {
base_sepolia: {
url: ALCHEMY_BASE_SEPOLIA_API_KEY_URL,
accounts: ACCOUNT_PRIVATE_KEY ? [`0x${ACCOUNT_PRIVATE_KEY}`] : [],
timeout: 120000,
},
},
etherscan: {
apiKey: ETHERSCAN_API_KEY,
},
};
export default config;
What This Does:
- Configures Base Sepolia testnet using Alchemy
- Loads private keys securely from .env
- Enables contract verification on Etherscan
Setting Up Environment Variables (.env File)
Create a .env file in the root of your Hardhat project and add:
ALCHEMY_BASE_SEPOLIA_API_KEY_URL="https://basesepolia.alchemyapi.io/v2/YOUR_ALCHEMY_KEY"
ACCOUNT_PRIVATE_KEY="YOUR_WALLET_PRIVATE_KEY"
ETHERSCAN_API_KEY="YOUR_ETHERSCAN_API_KEY"
NB - Never hardcode private keys in your code. Always use environment variables.
Deploying the NFT to Base Sepolia
A - Install Dependencies
If you haven’t already installed Hardhat and the required plugins:
npm install @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-verify dotenv ethers hardhat
B - Compile the Contract
Run:
npx hardhat compile
C - Deploy the Contract
Run:
npx hardhat run scripts/deploy.ts --network base_sepolia
Deploys the OnChainNFT contract to Base Sepolia
Automatically mints the first NFT after deployment
D - Expected output after deployment
npx hardhat run scripts/deploy.ts --network base_sepolia
Compiled 1 Solidity file successfully (evm target: paris).
Deploying NFT contract...
Minting first NFT...
NFT minted at address: 0x81783AC13C6Bdf030d8De279a13b63E4406287Da
Verifying the Contract on Etherscan
This is optional and it is to make your contract publicly viewable, verify it on Base Sepolia Etherscan:
npx hardhat verify --network base_sepolia 0xYourDeployedContractAddress
This allows anyone to interact with your NFT contract directly from Etherscan.
Viewing Your On-Chain NFT on OpenSea
To view on opensea, go to url, signup and connect your wallet if you are not signed up. Go to your account or profile and you can view your nft. Here is our minted nft
Top comments (0)