Intermediate

ERC-721 NFT Implementation — Kudos build — collectibles step

Understand token IDs and metadata.

Developer Intermediate
5/6 — ERC-721 NFT Implementation — Kudos build — collectibles step

Objective Add ERC-721 badges to the Kudos app for contributors who hold Kudos tokens.

Flow Gate minting on holding Kudos, emit Transfer/Approval events, and expose a stable baseURI so marketplaces can render badges.

What to write

  1. Scaffold
    ERC721URIStorage + AccessControl + counter.
    contract KudosBadge is ERC721URIStorage, AccessControl {
        bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
        IERC20 public immutable kudos;
        uint256 private _nextId = 1;
        string private _base;

    Sets roles, links to Kudos ERC-20, and tracks token IDs.

  2. Constructor
    Wire token address, base URI, and roles.
        constructor(address kudosToken, string memory base) ERC721("Kudos Badge", "KBDG") {
            kudos = IERC20(kudosToken);
            _base = base;
            _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
            _grantRole(MINTER_ROLE, msg.sender);
        }

    Anchors metadata and roles; sets the ERC-721 name/symbol.

  3. Gate minting
    Require Kudos balance and set tokenURI.
        function safeMint(address to, string calldata uri) external onlyRole(MINTER_ROLE) {
            require(kudos.balanceOf(to) > 0, "needs kudos");
            uint256 id = _nextId++;
            _safeMint(to, id);
            _setTokenURI(id, uri);
        }

    Ties badges to contributors; emits standard events.

  4. Base URI
    Expose baseURI for consistent metadata.
        function _baseURI() internal view override returns (string memory) { return _base; }

    Keeps metadata stable and indexer-friendly.

Why Gating on Kudos ties badges to real contribution, stable URIs make marketplaces happy, and role-gated minting avoids rogue supply.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC721URIStorage, ERC721} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract KudosBadge is ERC721URIStorage, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    IERC20 public immutable kudos;
    uint256 private _nextId = 1;
    string private _base;

    constructor(address kudosToken, string memory base) ERC721("Kudos Badge", "KBDG") {
        kudos = IERC20(kudosToken);
        _base = base;
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(MINTER_ROLE, msg.sender);
    }

    function safeMint(address to, string calldata uri) external onlyRole(MINTER_ROLE) {
        require(kudos.balanceOf(to) > 0, "needs kudos");
        uint256 id = _nextId++;
        _safeMint(to, id);
        _setTokenURI(id, uri);
    }

    function _baseURI() internal view override returns (string memory) { return _base; }

    function supportsInterface(bytes4 id) public view override(ERC721URIStorage, AccessControl) returns (bool) {
        return super.supportsInterface(id);
    }
}

Next Plug in oracles and randomness to enrich badge traits.

← Previous section
Next section →