# NFT Smart Contract Tutorial — ERC-721 Collection बनाएं
NFTs (Non-Fungible Tokens) ने digital ownership को revolutionize किया है। इस comprehensive tutorial में, हम scratch से production-ready ERC-721 NFT collection build करेंगे, जिसमें minting, metadata management, royalties और reveal mechanism शामिल हैं।
ERC-721 क्या है?
ERC-721 Ethereum पर non-fungible tokens का standard है। ERC-20 tokens के विपरीत जहां हर token identical होता है, हर ERC-721 token की unique identifier (tokenId) होती है और unique assets represent कर सकता है जैसे art, collectibles या game items।
Standard ये core functions define करता है:
interface IERC721 {
function balanceOf(address owner) external view returns (uint256);
function ownerOf(uint256 tokenId) external view returns (address);
function transferFrom(address from, address to, uint256 tokenId) external;
function approve(address to, uint256 tokenId) external;
function getApproved(uint256 tokenId) external view returns (address);
function setApprovalForAll(address operator, bool approved) external;
function isApprovedForAll(address owner, address operator) external view returns (bool);
}
NFT Contract Build करना
Production-grade NFT collection key features के साथ:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract MyNFTCollection is ERC721, ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
uint256 public constant MAX_SUPPLY = 10000;
uint256 public constant MINT_PRICE = 0.08 ether;
uint256 public constant MAX_PER_WALLET = 5;
bool public saleIsActive = false;
bool public revealed = false;
string private _baseTokenURI;
string private _preRevealURI;
mapping(address => uint256) public mintedPerWallet;
constructor() ERC721("MyNFTCollection", "MNFT") {
_preRevealURI = "ipfs://QmPreRevealHash/hidden.json";
}
// Minting function
function mint(uint256 quantity) public payable {
require(saleIsActive, "Sale not active");
require(quantity > 0 && quantity <= MAX_PER_WALLET, "Invalid quantity");
require(_tokenIds.current() + quantity <= MAX_SUPPLY, "Max supply reached");
require(mintedPerWallet[msg.sender] + quantity <= MAX_PER_WALLET, "Max per wallet");
require(msg.value >= MINT_PRICE * quantity, "Insufficient payment");
for (uint256 i = 0; i < quantity; i++) {
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();
_safeMint(msg.sender, newTokenId);
}
mintedPerWallet[msg.sender] += quantity;
}
// Owner minting (airdrops)
function ownerMint(address to, uint256 quantity) public onlyOwner {
require(_tokenIds.current() + quantity <= MAX_SUPPLY, "Max supply");
for (uint256 i = 0; i < quantity; i++) {
_tokenIds.increment();
_safeMint(to, _tokenIds.current());
}
}
// Toggle sale
function toggleSale() public onlyOwner {
saleIsActive = !saleIsActive;
}
// Reveal collection
function reveal(string memory baseURI) public onlyOwner {
_baseTokenURI = baseURI;
revealed = true;
}
// Metadata
function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
require(_exists(tokenId), "Token does not exist");
if (!revealed) {
return _preRevealURI;
}
return super.tokenURI(tokenId);
}
function _baseURI() internal view override returns (string memory) {
return _baseTokenURI;
}
// Withdraw funds
function withdraw() public onlyOwner {
uint256 balance = address(this).balance;
(bool success, ) = payable(owner()).call{value: balance}("");
require(success, "Withdrawal failed");
}
// Required overrides
function _burn(uint256 tokenId)
internal
override(ERC721, ERC721URIStorage)
{
super._burn(tokenId);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721URIStorage)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
ERC-2981 Royalties Add करना
NFT resales पर royalties earn करें:
import "@openzeppelin/contracts/token/common/ERC2981.sol";
contract MyNFTCollection is ERC721, ERC721URIStorage, ERC2981, Ownable {
constructor() ERC721("MyNFTCollection", "MNFT") {
// 5% royalty to contract owner
_setDefaultRoyalty(owner(), 500); // 500 = 5%
}
// Override supportsInterface
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721URIStorage, ERC2981)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
// Update royalty info
function setRoyaltyInfo(address receiver, uint96 feeNumerator)
public
onlyOwner
{
_setDefaultRoyalty(receiver, feeNumerator);
}
}
Metadata Management
IPFS पर Metadata Upload करना
# Pinata, NFT.Storage या IPFS Desktop use करें
ipfs add -r ./images
# Returns: QmImagesFolderHash
{
"name": "My NFT #1",
"description": "An awesome NFT from My Collection",
"image": "ipfs://QmImagesFolderHash/1.png",
"attributes": [
{
"trait_type": "Background",
"value": "Blue"
},
{
"trait_type": "Eyes",
"value": "Laser"
},
{
"trait_type": "Rarity",
"value": "Legendary"
}
]
}
ipfs add -r ./metadata
# Returns: QmMetadataFolderHash
reveal("ipfs://QmMetadataFolderHash/");
Advanced Features
Whitelist (Merkle Tree)
Presale के लिए gas-efficient whitelist:
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
contract MyNFTCollection is ERC721, Ownable {
bytes32 public merkleRoot;
mapping(address => bool) public whitelistClaimed;
function setMerkleRoot(bytes32 _merkleRoot) public onlyOwner {
merkleRoot = _merkleRoot;
}
function whitelistMint(bytes32[] calldata merkleProof) public payable {
require(!whitelistClaimed[msg.sender], "Already claimed");
bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
require(
MerkleProof.verify(merkleProof, merkleRoot, leaf),
"Invalid proof"
);
whitelistClaimed[msg.sender] = true;
_mint(msg.sender, _tokenIds.current());
_tokenIds.increment();
}
}
Merkle root generate करें:
const { MerkleTree } = require('merkletreejs');
const keccak256 = require('keccak256');
const whitelist = [
"0x1234...",
"0x5678...",
// ... whitelisted addresses
];
const leaves = whitelist.map(addr => keccak256(addr));
const tree = new MerkleTree(leaves, keccak256, { sortPairs: true });
const root = tree.getHexRoot();
console.log("Merkle Root:", root);
// Contract में set करें: setMerkleRoot(root)
Staking Mechanism
NFTs को stake करें rewards earn करने के लिए:
contract NFTStaking {
struct Stake {
uint256 tokenId;
uint256 timestamp;
address owner;
}
mapping(uint256 => Stake) public stakes;
mapping(address => uint256[]) public stakedTokens;
IERC721 public nftCollection;
uint256 public constant REWARD_RATE = 100 ether; // per day
function stake(uint256 tokenId) public {
require(nftCollection.ownerOf(tokenId) == msg.sender, "Not owner");
nftCollection.transferFrom(msg.sender, address(this), tokenId);
stakes[tokenId] = Stake({
tokenId: tokenId,
timestamp: block.timestamp,
owner: msg.sender
});
stakedTokens[msg.sender].push(tokenId);
}
function unstake(uint256 tokenId) public {
Stake memory staked = stakes[tokenId];
require(staked.owner == msg.sender, "Not staker");
uint256 rewards = calculateRewards(tokenId);
delete stakes[tokenId];
// Remove from stakedTokens array
nftCollection.transferFrom(address(this), msg.sender, tokenId);
// Transfer rewards token to msg.sender
}
function calculateRewards(uint256 tokenId) public view returns (uint256) {
Stake memory staked = stakes[tokenId];
uint256 duration = block.timestamp - staked.timestamp;
return (duration * REWARD_RATE) / 1 days;
}
}
Testing
Comprehensive test suite:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/MyNFTCollection.sol";
contract NFTTest is Test {
MyNFTCollection nft;
address alice = address(0x1);
address bob = address(0x2);
function setUp() public {
nft = new MyNFTCollection();
nft.toggleSale(); // Activate sale
}
function testMint() public {
vm.deal(alice, 1 ether);
vm.prank(alice);
nft.mint{value: 0.08 ether}(1);
assertEq(nft.balanceOf(alice), 1);
assertEq(nft.ownerOf(1), alice);
}
function testMaxSupply() public {
// Test minting beyond max supply fails
}
function testMaxPerWallet() public {
vm.deal(alice, 10 ether);
vm.prank(alice);
vm.expectRevert("Max per wallet");
nft.mint{value: 0.48 ether}(6); // 6 > MAX_PER_WALLET
}
function testReveal() public {
nft.mint{value: 0.08 ether}(1);
string memory preReveal = nft.tokenURI(1);
nft.reveal("ipfs://QmNewHash/");
string memory postReveal = nft.tokenURI(1);
assertFalse(
keccak256(bytes(preReveal)) == keccak256(bytes(postReveal))
);
}
}
Deployment Checklist
- [ ] Metadata IPFS पर uploaded
- [ ] Images IPFS पर uploaded
- [ ] Contract testnet पर deployed और tested
- [ ] Max supply correct
- [ ] Mint price correct
- [ ] Royalties configured
- [ ] Whitelist (अगर applicable) tested
- [ ] Reveal mechanism tested
- [ ] Withdraw function tested
- [ ] Contract verified on Etherscan
- [ ] Social media announced
- [ ] Community ready
Gas Optimization Tips
import "erc721a/contracts/ERC721A.sol";
contract MyNFT is ERC721A {
// 5 NFTs mint: ~180k gas (vs ~450k standard ERC721)
}
// ❌ Expensive
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
// ✅ Cheaper
uint256 private _tokenIds;
function mintBatch(uint256 quantity) public payable {
// Single loop बेहतर है multiple transactions से
}
निष्कर्ष
आपने सफलतापूर्वक production-ready NFT collection बनाया! Key features को याद रखें:
- ERC-721 standard compliance
- Minting limits (max supply, per wallet)
- Reveal mechanism
- Royalties (ERC-2981)
- Metadata management (IPFS)
- Security (ownership, reentrancy)
अगले कदम: Solingo पर advanced NFT concepts practice करें!
---
अतिरिक्त Resources: