Copy // 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);
}
}