Intermediate
ERC-721 NFT Implementation — Hands-on lab
Understand token IDs and metadata.
Developer Intermediate
4/6 — ERC-721 NFT Implementation — Hands-on lab
Build for real users Start from audited libraries, lock down roles, and exercise every path in tests. Don’t guess that metadata renders—verify in a real wallet/marketplace. Document exactly who can mint, burn, pause, or change URIs.
Privacy is necessary for an open society in the electronic age.
Plan the lifecycle Decide if you ever change baseURI. If yes, require a role and emit an event; if no, add a “freeze” toggle. Treat soulbound as an explicit rule: enforce non-transfer in code and explain it.
Think about support Provide a read function that returns the current baseURI and royalty info so wallets can self-serve. Keep your AccessControl roles obvious: DEFAULT_ADMIN_ROLE should not be the same hot wallet you mint from.
contract KudosNFT is ERC721URIStorage, AccessControl, ERC2981 {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
uint256 public immutable maxSupply;
string private _base;
uint256 private _minted;
constructor(string memory name_, string memory symbol_, string memory base_, uint96 feeBps, address feeReceiver, uint256 cap)
ERC721(name_, symbol_) {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MINTER_ROLE, msg.sender);
_base = base_;
maxSupply = cap;
_setDefaultRoyalty(feeReceiver, feeBps);
}
function _baseURI() internal view override returns (string memory) { return _base; }
function setBaseURI(string memory newBase) external onlyRole(DEFAULT_ADMIN_ROLE) { _base = newBase; }
function safeMint(address to, uint256 tokenId) external onlyRole(MINTER_ROLE) {
require(_minted < maxSupply, "sold out");
_minted++;
_safeMint(to, tokenId);
_setTokenURI(tokenId, string.concat(Strings.toString(tokenId), ".json"));
}
function supportsInterface(bytes4 id) public view override(ERC721URIStorage, AccessControl, ERC2981) returns (bool) {
return super.supportsInterface(id);
}
}
Royalty payout
payout = salePrice × feeBps ÷ 10,000
EIP-2981 returns (receiver, feeAmount). Fee is advisory; marketplaces decide whether to enforce.
Remaining supply
remaining = maxSupply − totalMinted
Block mints when remaining ≤ 0; emit an event when the collection sells out.
Steps
- Start from OpenZeppelin ERC721;:Add name/symbol; wire ERC721URIStorage if you need per-token URIs.
- Implement safe mint with:AccessControl (MINTER_ROLE), supply cap, and a pause switch; emit Transfer on mint/burn.
- Override tokenURI to join:BaseURI + tokenId and allow a trusted role to update baseURI (or freeze it). Add a soulbound guard if needed.
- Add EIP-2981 royaltyInfo with:A default receiver + fee; test marketplace reads; document that royalties are opt-in.
- Write tests for mint,:Transfer, approvals, receiver checks, pausing, supply caps, and metadata changes; include fuzz cases for approvals.
- Deploy to a testnet,:Load tokenURI in an explorer/marketplace preview, and confirm images/descriptions render.