RealAssetNFT.sol

Overview

The RealAssetNFT contract is the core building block for tokenizing real-world assets (RWAs) on the Real Network. It extends the ERC-721 standard with additional features for anchoring provenance, enforcing compliance, and providing flexible administrative control.

  • Purpose: Represent unique real-world assets (e.g., property titles, certificates, or products) as non-fungible tokens (NFTs).

  • Standards: Built on OpenZeppelin ERC-721.

  • Features:

    • Anchors document hashes to each NFT.

    • Supports metadata URIs for asset details and provenance.

    • Role-based access control for minting and pausing.

    • Optional revocation and compliance integration.

    • Pausable transfers for security response.

    • Burnable tokens for lifecycle management.


Responsibilities

  • Issuer / Admin

    • Configure contract (base URI, registry).

    • Manage minter/pauser roles.

    • Anchor authoritative records to NFTs.

  • Minter

    • Mint new NFTs with document hashes and metadata URIs.

    • Batch mint for operational efficiency.

  • Pauser

    • Pause and unpause contract in emergencies.

  • Compliance Registry (optional)

    • Define revocation rules and blacklists.


Key Components

Anchor Structure

Each NFT stores an immutable record:

  • documentHash: SHA-256/keccak256 hash of authoritative file.

  • issuedAt: Block timestamp when anchored.

  • issuer: Address that created the anchor.

  • dataURI: IPFS/HTTPS pointer to metadata JSON.

Roles

  • ADMIN_ROLE: Full control; can set registry and base URI.

  • MINTER_ROLE: Can mint NFTs and update metadata URIs.

  • PAUSER_ROLE: Can pause/unpause transfers.

Compliance Registry

  • Optional external contract implementing IRevocationRegistry.

  • Checks for revocation (document hash invalidated).

  • Checks for blacklists (blocked addresses).

Pausable and Burnable

  • Transfers blocked when paused.

  • NFTs can be burned by holders or admins if needed.

Metadata Resolution

  • Per-token URI: Stored in Anchor.dataURI.

  • Fallback URI: baseURI + tokenId.


Example Usage

  1. Mint with Anchor:

mintWithAnchor(
  to,
  tokenId,
  keccak256(pdfBytes),
  "ipfs://CID/metadata.json"
);
  1. Batch Mint:

batchMintWithAnchor(recipients, ids, hashes, uris);
  1. Update Metadata URI:

updateDataURI(tokenId, "ipfs://newCID/metadata.json");
  1. Check Anchor:

(anchor, revoked) = anchorOf(tokenId);

Security Considerations

  • Key Management: Issuer keys must be secured; compromise may allow unauthorized minting.

  • Compliance Integration: Use registry to enforce revocation/blacklist policies.

  • Data Integrity: Off-chain documents should be stored redundantly and hash-verified.

  • Pausing: Emergency pause should be monitored and tested.


Extensibility

  • Can integrate with:

    • Fractionalization modules (ERC-20 wrapper for shares).

    • Staking / yield modules for rental income.

    • On-chain registries for title transfer and notary attestation.


RealAssetNFT.sol

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

/**
 * @title RealAssetNFT
 * @notice ERC-721 contract for representing Real-World Assets (RWAs) on the Real Network.
 *         Each token anchors a tamper-evident hash of an authoritative off-chain record
 *         (e.g., a title deed PDF, certificate, bill of lading) and an IPFS/HTTPS metadata URI.
 *
 *         Key features:
 *         - On-chain "Anchor" (documentHash, issuedAt, issuer, dataURI) per tokenId
 *         - Role-based access control for minting/administration
 *         - Optional Revocation/Compliance registry integration
 *         - Pausable transfers for incident response
 *         - Upgrade-friendly storage layout (no inheritance diamonds)
 *
 * @dev Uses OpenZeppelin contracts. Link versions that match your toolchain.
 */

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721Burnable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";

/// @notice Optional external registry for compliance and revocation checks.
interface IRevocationRegistry {
    /// @dev Returns true if a given document hash has been revoked.
    function isRevoked(bytes32 documentHash) external view returns (bool);

    /// @dev Returns true if the address is blocked from sending/receiving.
    function isBlacklisted(address account) external view returns (bool);
}

contract RealAssetNFT is ERC721, ERC721Burnable, AccessControl, Pausable {
    using Strings for uint256;

    // ----------------------------
    // Roles
    // ----------------------------
    bytes32 public constant ADMIN_ROLE  = DEFAULT_ADMIN_ROLE; // full control
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");   // can mint/update dataURI
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");   // can pause/unpause

    // ----------------------------
    // Storage
    // ----------------------------
    struct Anchor {
        bytes32 documentHash;  // keccak256/SHA-256 of authoritative document blob
        uint64  issuedAt;      // unix seconds when anchor was recorded
        address issuer;        // account that anchored the document
        string  dataURI;       // ipfs://CID/metadata.json or https://... JSON
    }

    // tokenId => Anchor
    mapping(uint256 => Anchor) private _anchors;

    // Optional global base URI (used only if per-token dataURI is empty)
    string public baseURI;

    // Optional revocation/compliance registry
    IRevocationRegistry public registry;

    // ----------------------------
    // Events
    // ----------------------------
    event Anchored(uint256 indexed tokenId, bytes32 indexed documentHash, address indexed issuer);
    event DataURIUpdated(uint256 indexed tokenId, string dataURI);
    event RegistryUpdated(address indexed newRegistry);
    event BaseURIUpdated(string newBaseURI);

    // ----------------------------
    // Constructor
    // ----------------------------
    constructor(
        string memory name_,
        string memory symbol_,
        string memory baseURI_,
        address admin
    ) ERC721(name_, symbol_) {
        baseURI = baseURI_;
        _grantRole(ADMIN_ROLE, admin);
        _grantRole(MINTER_ROLE, admin);
        _grantRole(PAUSER_ROLE, admin);
    }

    // ----------------------------
    // Admin / Config
    // ----------------------------
    function setRegistry(address _registry) external onlyRole(ADMIN_ROLE) {
        registry = IRevocationRegistry(_registry);
        emit RegistryUpdated(_registry);
    }

    function setBaseURI(string calldata _base) external onlyRole(ADMIN_ROLE) {
        baseURI = _base;
        emit BaseURIUpdated(_base);
    }

    function pause() external onlyRole(PAUSER_ROLE) { _pause(); }
    function unpause() external onlyRole(PAUSER_ROLE) { _unpause(); }

    // ----------------------------
    // Minting & Anchoring
    // ----------------------------

    /**
     * @notice Mints a new token and writes its anchor in a single transaction.
     * @param to Recipient of the NFT
     * @param tokenId Unique token identifier
     * @param documentHash keccak256/SHA-256 hash of the authoritative document
     * @param dataURI Metadata URI (ipfs:// or https://) pointing to JSON with provenance
     */
    function mintWithAnchor(
        address to,
        uint256 tokenId,
        bytes32 documentHash,
        string calldata dataURI
    ) external onlyRole(MINTER_ROLE) {
        require(documentHash != bytes32(0), "RealAssetNFT: bad hash");
        _safeMint(to, tokenId);
        _anchors[tokenId] = Anchor({
            documentHash: documentHash,
            issuedAt: uint64(block.timestamp),
            issuer: _msgSender(),
            dataURI: dataURI
        });
        emit Anchored(tokenId, documentHash, _msgSender());
        if (bytes(dataURI).length != 0) emit DataURIUpdated(tokenId, dataURI);
    }

    /**
     * @notice Batch mint helper for operational efficiency.
     */
    function batchMintWithAnchor(
        address[] calldata recipients,
        uint256[] calldata tokenIds,
        bytes32[] calldata docHashes,
        string[] calldata uris
    ) external onlyRole(MINTER_ROLE) {
        uint256 n = recipients.length;
        require(
            n == tokenIds.length && n == docHashes.length && n == uris.length,
            "RealAssetNFT: length mismatch"
        );
        for (uint256 i = 0; i < n; i++) {
            mintWithAnchor(recipients[i], tokenIds[i], docHashes[i], uris[i]);
        }
    }

    /**
     * @notice Update the metadata URI without changing the anchored hash.
     *         Useful for rotating IPFS CIDs while preserving the anchor.
     */
    function updateDataURI(uint256 tokenId, string calldata dataURI)
        external
        onlyRole(MINTER_ROLE)
    {
        require(_exists(tokenId), "RealAssetNFT: nonexistent token");
        _anchors[tokenId].dataURI = dataURI;
        emit DataURIUpdated(tokenId, dataURI);
    }

    // ----------------------------
    // Views
    // ----------------------------

    /**
     * @return Anchor struct and whether it's revoked per the registry (if set).
     */
    function anchorOf(uint256 tokenId)
        external
        view
        returns (Anchor memory a, bool revoked)
    {
        require(_exists(tokenId), "RealAssetNFT: nonexistent token");
        a = _anchors[tokenId];
        revoked = address(registry) != address(0) && registry.isRevoked(a.documentHash);
    }

    /**
     * @dev ERC721 metadata resolver. Uses per-token dataURI if present, otherwise baseURI + tokenId.
     */
    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        require(_exists(tokenId), "RealAssetNFT: nonexistent token");
        string memory u = _anchors[tokenId].dataURI;
        if (bytes(u).length > 0) return u; // e.g., ipfs://CID/metadata.json
        if (bytes(baseURI).length == 0) return "";
        return string(abi.encodePacked(baseURI, tokenId.toString()));
    }

    // ----------------------------
    // Hooks & Compliance
    // ----------------------------

    function _update(address to, uint256 tokenId, address auth)
        internal
        override(ERC721)
        returns (address)
    {
        // Pause guard
        require(!paused(), "RealAssetNFT: paused");

        // Compliance checks (if registry set)
        if (address(registry) != address(0)) {
            Anchor memory a = _anchors[tokenId];
            require(!registry.isRevoked(a.documentHash), "RealAssetNFT: anchor revoked");
            if (auth != address(0)) {
                require(!registry.isBlacklisted(auth), "RealAssetNFT: sender blocked");
            }
            if (to != address(0)) {
                require(!registry.isBlacklisted(to), "RealAssetNFT: recipient blocked");
            }
        }
        return super._update(to, tokenId, auth);
    }

    // Required by Solidity for multiple inheritance
    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, AccessControl)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

Last updated