OwnershipTokenizerV7
Overview
Version: 7.0.0 Type: Concrete Upgradeable Contract License: MIT Token Standard: ERC-721 Inherits: ERC721Upgradeable, BaseTokenizerV7
OwnershipTokenizerV7 implements single-owner document tokenization using ERC-721 NFTs. One NFT per document represents exclusive, non-divisible ownership.
Purpose
- Tokenize single-owner documents as unique NFTs
- Provide clear ownership semantics
- Enable composability with NFT ecosystem
- Support standard ERC-721 transfers
Key Features
- One NFT per document (non-divisible)
- Auto-incrementing token IDs
- Named and anonymous reservations
- Transferable via standard ERC-721
- Metadata via configurable base URI
Use Cases
Real Estate
- Property deeds (house, land)
- Commercial real estate titles
- Development rights
Vehicles
- Car titles
- Boat registration
- Aircraft ownership
Intellectual Property
- Copyright ownership
- Patent rights
- Trademark ownership
Licensing
- Exclusive licenses
- Franchise rights
- Distribution rights
Architecture
State Variables
struct OwnershipTokenData {
bytes32 integraHash; // Document identifier
address owner; // Current owner (after mint)
bool minted; // Whether NFT minted
address reservedFor; // Reserved recipient (or address(0))
bytes encryptedLabel; // Role label (for anonymous)
}
mapping(uint256 => OwnershipTokenData) private tokenData;
mapping(bytes32 => uint256) public integraHashToTokenId;
uint256 private _nextTokenId; // Auto-incrementing
string private _baseTokenURI; // Metadata base URIDesign: One integraHash maps to one tokenId, enforcing single ownership.
Token Lifecycle
1. Reserve
Named Reservation (recipient known):
ownershipTokenizer.reserveToken(
integraHash,
0, // tokenId (auto-assigned)
buyerAddress, // recipient
1, // amount (always 1 for ERC-721)
processHash
);Anonymous Reservation (recipient unknown):
ownershipTokenizer.reserveTokenAnonymous(
integraHash,
0,
1,
encryptedLabel, // e.g., encrypt("new owner", buyerIntegraID)
processHash
);2. Claim
ownershipTokenizer.claimToken(
integraHash,
tokenId, // or 0 to auto-lookup
capabilityAttestationUID,
processHash
);Result: ERC-721 NFT minted to claimant
3. Transfer (Optional)
// Standard ERC-721 transfer
nft.transferFrom(currentOwner, newOwner, tokenId);Core Functions
reserveToken
function reserveToken(
bytes32 integraHash,
uint256 tokenId,
address recipient,
uint256 amount,
bytes32 processHash
) external override
requireOwnerOrExecutor(integraHash)
nonReentrant
whenNotPausedAccess: Document owner or authorized executor
Validation: Ensures no existing reservation for document
Effect: Creates reservation, assigns new token ID
Emits: TokenReserved
Example:
// Owner reserves deed for buyer
ownershipTokenizer.reserveToken(
deedIntegraHash,
0,
buyerAddress,
1,
workflowProcessHash
);reserveTokenAnonymous
function reserveTokenAnonymous(
bytes32 integraHash,
uint256 tokenId,
uint256 amount,
bytes calldata encryptedLabel,
bytes32 processHash
) external override
requireOwnerOrExecutor(integraHash)
nonReentrant
whenNotPausedAccess: Document owner or authorized executor
Use Case: Recipient address unknown at reservation time
Emits: TokenReservedAnonymous
Example:
// Reserve for unknown future owner
ownershipTokenizer.reserveTokenAnonymous(
integraHash,
0,
1,
encrypt("new owner", futureOwnerIntegraID),
processHash
);claimToken
function claimToken(
bytes32 integraHash,
uint256 tokenId,
bytes32 capabilityAttestationUID,
bytes32 processHash
) external override
requiresCapabilityWithUID(integraHash, CAPABILITY_CLAIM_TOKEN, capabilityAttestationUID)
nonReentrant
whenNotPausedAccess: Must have valid CORE_CLAIM attestation Validation:
- Checks reservation exists
- Verifies not already minted
- For named reservations, ensures caller = reservedFor
Effect: Mints ERC-721 NFT to claimant
Emits: TokenClaimed
Example:
// Buyer claims deed NFT
ownershipTokenizer.claimToken(
deedIntegraHash,
0, // or specific tokenId
buyerAttestationUID,
workflowProcessHash
);cancelReservation
function cancelReservation(
bytes32 integraHash,
uint256 tokenId,
bytes32 processHash
) external override
requireOwnerOrExecutor(integraHash)
nonReentrant
whenNotPausedAccess: Document owner or authorized executor
Validation: Ensures not already minted
Effect: Deletes reservation data
Emits: ReservationCancelled
View Functions
balanceOf
function balanceOf(address account, uint256 tokenId) public view returns (uint256)Returns:
- If tokenId == 0: Total NFT count for account (standard ERC-721)
- If tokenId != 0: 1 if account owns that tokenId, else 0
getTokenInfo
function getTokenInfo(bytes32 integraHash, uint256 tokenId)
external view returns (IDocumentTokenizerV7.TokenInfo memory)Returns: Complete token information Fields:
- integraHash
- tokenId (auto-assigned during reservation)
- totalSupply (1 if minted, 0 if reserved)
- reserved (1 if reserved, 0 if minted)
- holders (array with owner if minted)
- encryptedLabel
- reservedFor
- claimed (minted status)
- claimedBy (owner address)
getClaimStatus
function getClaimStatus(bytes32 integraHash, uint256 tokenId)
external view returns (bool claimed, address claimedBy)Returns: Whether token minted and who owns it
ERC-721 Overrides
_baseURI
function _baseURI() internal view override returns (string memory) {
return _baseTokenURI;
}Usage: Metadata URI for tokenId N = {baseURI}{tokenId}
setBaseURI
function setBaseURI(string memory baseURI_) external onlyRole(GOVERNOR_ROLE)Access: Governor only Purpose: Update metadata base URI
Example:
// Set IPFS base URI
ownershipTokenizer.setBaseURI("ipfs://QmHash/");
// Token 123 metadata: ipfs://QmHash/123Security Features
Single Ownership Enforcement
uint256 existingTokenId = integraHashToTokenId[integraHash];
if (existingTokenId != 0) {
if (tokenData[existingTokenId].minted || tokenData[existingTokenId].reservedFor != address(0)) {
revert AlreadyReserved(integraHash);
}
}Protection: One document → one token mapping enforced
Reservation Protection
if (data.reservedFor != address(0) && data.reservedFor != msg.sender) {
revert NotReservedForYou(msg.sender, data.reservedFor);
}Protection: Named reservations only claimable by designated recipient
Attestation Verification
requiresCapabilityWithUID(integraHash, CAPABILITY_CLAIM_TOKEN, capabilityAttestationUID)Protection:
- Verifies claimant has valid attestation
- Prevents unauthorized claims
- Front-running protected (attestation bound to address)
Integration Examples
Basic Flow
// 1. Owner reserves deed for buyer
ownershipTokenizer.reserveToken(
deedHash,
0,
buyerAddress,
1,
processHash
);
// 2. Issue claim capability to buyer
attestationProvider.issueCapability(
buyerAddress,
deedHash,
CAPABILITY_CLAIM_TOKEN
);
// 3. Buyer claims NFT
ownershipTokenizer.claimToken(
deedHash,
0,
attestationUID,
processHash
);
// 4. Buyer now owns ERC-721 NFT
uint256 tokenId = ownershipTokenizer.integraHashToTokenId(deedHash);
address owner = ownershipTokenizer.ownerOf(tokenId);
// owner == buyerAddress ✓Anonymous Reservation
// Owner doesn't know buyer's address yet
ownershipTokenizer.reserveTokenAnonymous(
integraHash,
0,
1,
encrypt("future owner", buyerIntegraID),
processHash
);
// Later: Buyer discovers they're the "future owner"
// (off-chain decryption of encryptedLabel)
// Buyer gets attestation and claims
ownershipTokenizer.claimToken(integraHash, 0, attestationUID, processHash);With Executor Authorization
// Owner authorizes backend server as executor
documentRegistry.setDocumentExecutor(deedHash, backendAddress);
// Backend server can now reserve on owner's behalf
// (backend is msg.sender, passes requireOwnerOrExecutor)
ownershipTokenizer.reserveToken(
deedHash,
0,
buyerAddress,
1,
processHash
);Gas Costs
Reserve: ~80,000 gas Claim (mint): ~120,000 gas Cancel: ~30,000 gas
Optimizations:
- Auto-incrementing token ID (no search)
- Single storage slot per token
- Unchecked arithmetic where safe
Error Handling
AlreadyMinted
error AlreadyMinted(uint256 tokenId);Trigger: Attempting to claim already-minted token Resolution: Check claim status before claiming
AlreadyReserved
error AlreadyReserved(bytes32 integraHash);Trigger: Attempting to reserve when document already has reservation Resolution: Cancel existing reservation first, or use different document
TokenNotFound
error TokenNotFound(bytes32 integraHash, uint256 tokenId);Trigger: Invalid integraHash/tokenId lookup Resolution: Verify document has reservation
Best Practices
For Issuers
-
Reserve with known recipient when possible:
// Preferred reserveToken(hash, 0, buyerAddress, 1, processHash); // Only if necessary reserveTokenAnonymous(hash, 0, 1, label, processHash); -
Always provide processHash:
// Good reserveToken(..., workflowProcessHash); // Bad (will revert) reserveToken(..., bytes32(0)); -
Cancel if plans change:
// Clean up unused reservations cancelReservation(hash, tokenId, processHash);
For Claimants
-
Verify reservation before claiming:
TokenInfo memory info = tokenizer.getTokenInfo(hash, 0); require(!info.claimed, "Already claimed"); require(info.reservedFor == address(0) || info.reservedFor == msg.sender, "Not for you"); -
Handle claim failures gracefully:
try tokenizer.claimToken(hash, tokenId, attestationUID, processHash) { // Success } catch Error(string memory reason) { // Handle specific error }
Metadata
Token URI Structure
baseURI/{tokenId}Example:
- Base URI:
ipfs://QmHash/ - Token ID:
123 - Full URI:
ipfs://QmHash/123
Metadata JSON
{
"name": "Property Deed #123",
"description": "Ownership deed for 123 Main St",
"image": "ipfs://QmImageHash",
"attributes": [
{
"trait_type": "Document Type",
"value": "Real Estate Deed"
},
{
"trait_type": "Integra Hash",
"value": "0x..."
}
]
}Upgradeability
Storage Gap
/**
* State variables:
* - tokenData (1)
* - integraHashToTokenId (1)
* - _nextTokenId (1)
* - _baseTokenURI (1)
* Total: 4 slots
* Gap: 50 - 4 = 46 slots
*/
uint256[46] private __gap;Important: When adding state variables in upgrades, reduce gap accordingly.
Composability
Works With
- NFT Marketplaces: OpenSea, Rarible, LooksRare
- NFT Wallets: MetaMask, Rainbow, Coinbase Wallet
- NFT Aggregators: Gem, Genie
- DeFi Protocols: NFT lending, fractional ownership
Standard Compliance
- Full ERC-721 compliance
- ERC-165 interface detection
- Standard metadata format
Code Location
Repository: smart-contracts-evm-v7
Path: /src/layer3/OwnershipTokenizerV7.sol
Version: 7.0.0
Audited: Yes (Foundation & Tokenization Security Audit 2024)
Related Contracts
- Tokenization Overview - Tokenization architecture
- Tokenizer Comparison - Choose the right tokenizer
- SoulboundTokenizerV7 - Non-transferable variant