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
Mint with Anchor:
mintWithAnchor(
to,
tokenId,
keccak256(pdfBytes),
"ipfs://CID/metadata.json"
);Batch Mint:
batchMintWithAnchor(recipients, ids, hashes, uris);Update Metadata URI:
updateDataURI(tokenId, "ipfs://newCID/metadata.json");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