BlockWard Smart Contract

Solidity implementation for Polygon deployment

Testnet Only for Students

This contract is production-ready but should only be deployed to Polygon testnet by students. Mainnet deployment requires adult supervision, security audits, and proper gas wallet management.

Contract Overview

Contract Details

  • • Solidity Version: ^0.8.20
  • • Standard: ERC721 + Extensions
  • • Network: Polygon (Mumbai testnet / Mainnet)
  • • Name: BlockWard
  • • Symbol: BWARD

Key Features

  • • Soulbound (non-transferable) NFTs
  • • Role-based access control
  • • On-chain metadata storage
  • • Revocation capability
  • • Event emissions for tracking
Access Roles
DEFAULT_ADMIN_ROLE

Granted to contract deployer. Can revoke BlockWards and manage roles.

ISSUER_ROLE

Granted to backend gas wallet. Can mint BlockWards to students.

Complete Solidity Contract
// 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/AccessControl.sol";

/**
 * @title BlockWardSoulbound
 * @dev Soulbound NFT for student achievements on Polygon
 * Non-transferable tokens that permanently record student accomplishments
 */
contract BlockWardSoulbound is ERC721, ERC721URIStorage, AccessControl {
    bytes32 public constant ISSUER_ROLE = keccak256("ISSUER_ROLE");

    uint256 private _nextTokenId;

    struct WardData {
        address student;      // Student's wallet address
        bytes32 schoolId;     // School identifier
        string title;         // Achievement title
        string description;   // Achievement description
        string category;      // Category: academic, sports, arts, etc.
        uint64 issuedAt;      // Timestamp of issuance
        bool revoked;         // Revocation status
    }

    // Mapping from token ID to achievement data
    mapping(uint256 => WardData) public wardData;

    // Events
    event BlockWardIssued(
        address indexed student,
        uint256 indexed tokenId,
        bytes32 indexed schoolId,
        string category,
        uint64 issuedAt
    );

    event BlockWardRevoked(
        uint256 indexed tokenId,
        address indexed admin,
        uint64 revokedAt
    );

    /**
     * @dev Constructor sets up roles and initializes token counter
     * @param issuer Address of the backend wallet that will mint tokens
     */
    constructor(address issuer) ERC721("BlockWard", "BWARD") {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(ISSUER_ROLE, issuer);
        _nextTokenId = 1;
    }

    /**
     * @dev Issue a new BlockWard to a student
     * @param student The student's wallet address
     * @param schoolId The school's unique identifier
     * @param title Achievement title
     * @param description Achievement description
     * @param category Achievement category
     * @param tokenURI_ Metadata URI (typically IPFS)
     */
    function issueBlockWard(
        address student,
        bytes32 schoolId,
        string calldata title,
        string calldata description,
        string calldata category,
        string calldata tokenURI_
    ) external onlyRole(ISSUER_ROLE) {
        require(student != address(0), "Invalid student address");

        uint256 tokenId = _nextTokenId++;
        _safeMint(student, tokenId);
        _setTokenURI(tokenId, tokenURI_);

        wardData[tokenId] = WardData({
            student: student,
            schoolId: schoolId,
            title: title,
            description: description,
            category: category,
            issuedAt: uint64(block.timestamp),
            revoked: false
        });

        emit BlockWardIssued(
            student, 
            tokenId, 
            schoolId, 
            category, 
            uint64(block.timestamp)
        );
    }

    /**
     * @dev Revoke a BlockWard (admin only)
     * @param tokenId The token ID to revoke
     */
    function revokeBlockWard(uint256 tokenId)
        external
        onlyRole(DEFAULT_ADMIN_ROLE)
    {
        require(_ownerOf(tokenId) != address(0), "Token does not exist");
        
        wardData[tokenId].revoked = true;
        _burn(tokenId);

        emit BlockWardRevoked(
            tokenId, 
            msg.sender, 
            uint64(block.timestamp)
        );
    }

    /**
     * @dev Get all BlockWards for a student
     * @param student The student's address
     * @return Array of token IDs owned by the student
     */
    function getStudentBlockWards(address student) 
        external 
        view 
        returns (uint256[] memory) 
    {
        uint256 balance = balanceOf(student);
        uint256[] memory tokens = new uint256[](balance);
        uint256 index = 0;
        
        for (uint256 tokenId = 1; tokenId < _nextTokenId; tokenId++) {
            if (_ownerOf(tokenId) == student) {
                tokens[index] = tokenId;
                index++;
            }
        }
        
        return tokens;
    }

    // --- SOULBOUND: Disable transfers and approvals ---

    /**
     * @dev Override _update to prevent transfers (soulbound)
     * Allows minting (from address(0)) and burning (to address(0)) only
     */
    function _update(
        address to,
        uint256 tokenId,
        address auth
    ) internal override(ERC721) returns (address) {
        address from = _ownerOf(tokenId);
        
        // Allow minting and burning, block transfers
        if (from != address(0) && to != address(0)) {
            revert("Soulbound: transfers disabled");
        }
        
        return super._update(to, tokenId, auth);
    }

    /**
     * @dev Disable approve function for soulbound tokens
     */
    function approve(address, uint256) public pure override {
        revert("Soulbound: approvals disabled");
    }

    /**
     * @dev Disable setApprovalForAll for soulbound tokens
     */
    function setApprovalForAll(address, bool) public pure override {
        revert("Soulbound: approvals disabled");
    }

    // --- Required overrides ---

    function _burn(uint256 tokenId)
        internal
        override(ERC721, ERC721URIStorage)
    {
        super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721URIStorage, AccessControl)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}
How It Works

Main Functions:

issueBlockWard()

Called by backend (ISSUER_ROLE) to mint a new achievement NFT to a student. Stores all metadata on-chain and emits an event for indexing.

revokeBlockWard()

Admin-only function to burn a token if an achievement needs to be revoked. Sets the revoked flag and permanently removes the token.

getStudentBlockWards()

View function that returns all token IDs owned by a specific student. Useful for displaying a student's achievement collection.

_update()

Internal override that enforces soulbound behavior. Blocks all transfers between addresses while still allowing minting and burning.

Soulbound Implementation:

Non-transferable: Once minted, BlockWards cannot be transferred, sold, or given away. They are permanently bound to the student's wallet address, ensuring achievements remain authentic and cannot be traded.

Deployment Steps (Testnet)
  1. 1.Install Hardhat or Foundry development environment
  2. 2.Install OpenZeppelin contracts: npm install @openzeppelin/contracts
  3. 3.Get Mumbai testnet MATIC from a faucet (free)
  4. 4.Create backend wallet address to pass as issuer parameter
  5. 5.Deploy contract: npx hardhat run scripts/deploy.js --network mumbai
  6. 6.Save the deployed contract address
  7. 7.Verify on PolygonScan: npx hardhat verify --network mumbai [ADDRESS] [ISSUER]
Required Dependencies
{
  "dependencies": {
    "@openzeppelin/contracts": "^5.0.0"
  },
  "devDependencies": {
    "hardhat": "^2.19.0",
    "@nomicfoundation/hardhat-toolbox": "^4.0.0"
  }
}