RentalTokenizerV7
Overview
Version: 7.0.0 Type: Concrete Upgradeable Contract License: MIT Token Standard: ERC-1155 Inherits: ERC1155Upgradeable, BaseTokenizerV7, TrustGraphIntegration
RentalTokenizerV7 implements time-based access tokenization using ERC-1155 semi-fungible tokens. It enables temporary access rights with expiration tracking for rentals, leases, and subscription-based agreements.
Purpose
- Tokenize rental and lease agreements
- Enable time-limited access rights
- Track expiration timestamps per token holder
- Support property rentals, equipment leases, and subscriptions
- Issue trust credentials when rental agreements complete
Key Features
- ERC-1155 semi-fungible token standard
- Time-based expiration tracking per holder
- Named and anonymous reservations
- Trust Graph integration
- Holder-specific expiration timestamps
- Revocable access after expiration
Purpose and Use Cases
Property Rentals
Scenario: Annual apartment lease
// Reserve rental token for tenant
rentalTokenizer.reserveToken(
apartmentHash,
1, // Token ID 1 = tenant position
tenantAddress,
1, // 1 token = access rights
processHash
);
// Tenant claims
rentalTokenizer.claimToken(apartmentHash, 1, attestationUID, processHash);
// Tenant receives token granting 1-year access
// Expiration: block.timestamp + 365 daysBenefits:
- On-chain proof of rental rights
- Automatic expiration tracking
- Verifiable access for smart locks/building systems
- Transferable (sublease) if allowed
Equipment Leases
Scenario: Construction equipment rental
// 30-day excavator lease
rentalTokenizer.reserveToken(
equipmentHash,
1,
contractorAddress,
1,
processHash
);
// After claim, contractor has token
// IoT device checks token + expiration
// Access revoked after 30 daysSubscription Access
Scenario: Monthly SaaS subscription
// Monthly subscription token
rentalTokenizer.reserveToken(
serviceHash,
1,
subscriberAddress,
1,
processHash
);
// Claim → 30-day access
// After 30 days, must renew (new token)Temporary Licenses
Scenario: Short-term software license
// 90-day trial license
rentalTokenizer.reserveToken(
softwareHash,
1,
companyAddress,
1,
processHash
);
// License check:
// balanceOf(company, 1) > 0 && !isExpired(company, 1)Parking Permits
Scenario: Annual parking permit
// Parking space rental
rentalTokenizer.reserveToken(
parkingHash,
1,
driverAddress,
1,
processHash
);
// Parking gate checks token + expirationKey Features
1. Time-Based Expiration
Each holder has individual expiration timestamp:
struct RentalData {
// ...
mapping(address => uint64) holderExpiration;
}
// Set during claim
data.holderExpiration[msg.sender] = uint64(block.timestamp + rentalDuration);2. Per-Holder Tracking
Multiple holders can have same token ID with different expirations:
// Holder 1: Expires in 30 days
// Holder 2: Expires in 60 days
// Same token ID, different expiration times3. Expiration Validation
Check if rental expired:
function isExpired(bytes32 integraHash, uint256 tokenId, address holder)
public view returns (bool)
{
uint64 expiration = rentalData[integraHash][tokenId].holderExpiration[holder];
return block.timestamp > expiration;
}4. Access Control Integration
Smart contracts can verify active rental:
modifier hasActiveRental(bytes32 integraHash, uint256 tokenId) {
require(rentalTokenizer.balanceOf(msg.sender, tokenId) > 0, "No token");
require(!rentalTokenizer.isExpired(integraHash, tokenId, msg.sender), "Expired");
_;
}Architecture
State Variables
struct RentalData {
bytes32 integraHash; // Document identifier
uint256 totalSupply; // Total claimed tokens
uint256 reservedAmount; // Reserved but unclaimed
bytes encryptedLabel; // Role label (for anonymous)
address reservedFor; // Reserved recipient
bool claimed; // Whether claimed
address claimedBy; // Who claimed
uint64 expirationTime; // Default expiration (optional)
address[] holders; // All token holders
mapping(address => bool) isHolder;
mapping(address => uint64) holderExpiration; // Per-holder expiration
}
mapping(bytes32 => mapping(uint256 => RentalData)) private rentalData;
string private _baseURI;
IEAS private eas;Custom Errors
error RentalExpired(bytes32 integraHash, uint256 tokenId, uint64 expiredAt);Inheritance Hierarchy
ERC1155Upgradeable (OpenZeppelin)
├─ Semi-fungible token standard
├─ Batch transfers
└─ Metadata via URI
BaseTokenizerV7
├─ Access control
├─ Capability verification
├─ Document registry integration
└─ Process hash validation
TrustGraphIntegration
├─ Trust credential issuance
├─ Document completion detection
└─ EAS integrationFunctions
Initialization
initialize
function initialize(
string memory baseURI_,
address governor,
address _documentRegistry,
address _namespace,
address _providerRegistry,
bytes32 _defaultProviderId,
bytes32 _credentialSchema,
address _trustRegistry,
address _easAddress
) external initializerInitializes the upgradeable contract.
Parameters:
baseURI_: Base URI for token metadatagovernor: Governor address for admin operations_documentRegistry: IntegraDocumentRegistryV7_Immutable address_namespace: CapabilityNamespaceV7_Immutable address_providerRegistry: AttestationProviderRegistryV7_Immutable address_defaultProviderId: Default attestation provider ID_credentialSchema: EAS schema UID for trust credentials_trustRegistry: Trust registry address_easAddress: EAS contract address
Reserve Functions
reserveToken
function reserveToken(
bytes32 integraHash,
uint256 tokenId,
address recipient,
uint256 amount,
bytes32 processHash
) external override requireOwnerOrExecutor(integraHash) nonReentrant whenNotPausedReserves rental token for a specific recipient.
Parameters:
integraHash: Document identifiertokenId: Unique identifier for this rental positionrecipient: Address receiving the rental tokenamount: Number of tokens (typically 1 for access rights)processHash: Off-chain process correlation ID
Requirements:
- Caller must be document owner or authorized executor
- Recipient cannot be zero address
- Amount must be greater than 0
- Token ID not already reserved
- Contract must not be paused
Effects:
- Creates reservation for recipient at token ID
- Emits
TokenReservedevent
Example:
// Reserve apartment rental
rentalTokenizer.reserveToken(
apartmentHash,
1,
tenantAddress,
1, // 1 token = access rights
processHash
);reserveTokenAnonymous
function reserveTokenAnonymous(
bytes32 integraHash,
uint256 tokenId,
uint256 amount,
bytes calldata encryptedLabel,
bytes32 processHash
) external override requireOwnerOrExecutor(integraHash) nonReentrant whenNotPausedReserves rental token with encrypted recipient identity.
Parameters:
integraHash: Document identifiertokenId: Token identifieramount: Number of tokensencryptedLabel: Encrypted role description (e.g., “tenant”, “sublessee”)processHash: Process correlation ID
Use Case:
// Reserve without revealing tenant identity
bytes memory encryptedLabel = encrypt("tenant_unit_101", recipientIntegraID);
rentalTokenizer.reserveTokenAnonymous(
apartmentHash,
1,
1,
encryptedLabel,
processHash
);Claim Functions
claimToken
function claimToken(
bytes32 integraHash,
uint256 tokenId,
bytes32 capabilityAttestationUID,
bytes32 processHash
) external override requiresCapabilityWithUID(integraHash, CAPABILITY_CLAIM_TOKEN, capabilityAttestationUID) nonReentrant whenNotPausedClaims reserved rental token and mints ERC-1155 tokens.
Parameters:
integraHash: Document identifiertokenId: Token ID to claimcapabilityAttestationUID: EAS attestation proving claim capabilityprocessHash: Process correlation ID
Requirements:
- Caller must have valid capability attestation
- Token must be reserved
- Token not already claimed
- If named reservation, caller must be recipient
- Contract must not be paused
Effects:
- Mints
reservedAmountERC-1155 tokens to caller attokenId - Sets holder expiration timestamp (if duration specified in off-chain metadata)
- Increments
totalSupply - Clears
reservedAmount - Marks token as
claimed - Adds to holders array
- Emits
TokenClaimedevent - Triggers trust credential if document complete
Example:
// Tenant claims rental token
rentalTokenizer.claimToken(
apartmentHash,
1,
tenantAttestationUID,
processHash
);
// Tenant receives token
// holderExpiration[tenant] = block.timestamp + lease_durationCancellation Functions
cancelReservation
function cancelReservation(
bytes32 integraHash,
uint256 tokenId,
bytes32 processHash
) external override requireOwnerOrExecutor(integraHash) nonReentrant whenNotPausedCancels a token reservation before it’s claimed.
Parameters:
integraHash: Document identifiertokenId: Token ID to cancelprocessHash: Process correlation ID
Requirements:
- Caller must be owner or executor
- Token must be reserved
- Token not yet claimed
Effects:
- Deletes reservation data
- Emits
ReservationCancelledevent
View Functions
balanceOf
function balanceOf(address account, uint256 tokenId)
public view override returns (uint256)Returns rental token balance for account.
Note: Balance > 0 doesn’t guarantee active rental. Check expiration separately.
getTokenInfo
function getTokenInfo(bytes32 integraHash, uint256 tokenId)
external view override returns (IDocumentTokenizerV7.TokenInfo memory)Returns comprehensive rental token information.
Returns:
struct TokenInfo {
bytes32 integraHash;
uint256 tokenId;
uint256 totalSupply; // Total claimed tokens
uint256 reserved; // Unclaimed tokens
address[] holders; // All token holders
bytes encryptedLabel; // Role label
address reservedFor; // Reserved recipient
bool claimed; // Whether claimed
address claimedBy; // Who claimed
}getClaimStatus
function getClaimStatus(bytes32 integraHash, uint256 tokenId)
external view override returns (bool, address)Returns claim status for token.
tokenType
function tokenType() external pure override returns (IDocumentTokenizerV7.TokenType)Returns token standard identifier.
Returns: TokenType.ERC1155
Expiration Functions
Note: These would typically be added in a production implementation:
// Check if rental expired
function isExpired(bytes32 integraHash, uint256 tokenId, address holder)
public view returns (bool)
{
uint64 expiration = rentalData[integraHash][tokenId].holderExpiration[holder];
return block.timestamp > expiration;
}
// Get expiration timestamp
function getExpiration(bytes32 integraHash, uint256 tokenId, address holder)
public view returns (uint64)
{
return rentalData[integraHash][tokenId].holderExpiration[holder];
}
// Check active rental (balance > 0 && not expired)
function hasActiveRental(bytes32 integraHash, uint256 tokenId, address holder)
public view returns (bool)
{
return balanceOf(holder, tokenId) > 0 && !isExpired(integraHash, tokenId, holder);
}Token Lifecycle
Complete Flow Example
// 1. DEPLOY
RentalTokenizerV7 rentalTokenizer = new RentalTokenizerV7();
rentalTokenizer.initialize(
"https://metadata.integra.network/rental/",
governor,
documentRegistry,
namespace,
providerRegistry,
defaultProviderId,
credentialSchema,
trustRegistry,
easAddress
);
// 2. RESERVE RENTAL
// Apartment lease - 1 year
bytes32 apartmentHash = keccak256("apartment_101_lease");
rentalTokenizer.reserveToken(
apartmentHash,
1, // Token ID 1 = tenant position
tenantAddress,
1, // 1 token = access rights
processHash
);
// 3. CLAIM RENTAL TOKEN
rentalTokenizer.claimToken(
apartmentHash,
1,
tenantAttestationUID,
processHash
);
// Tenant now holds rental token
// Expiration set: block.timestamp + 365 days
// 4. ACCESS CONTROL
// Smart lock checks rental status
bool canEnter = rentalTokenizer.balanceOf(tenant, 1) > 0 &&
!rentalTokenizer.isExpired(apartmentHash, 1, tenant);
if (canEnter) {
// Grant access
} else {
// Deny access (expired or no token)
}
// 5. RENEWAL (if applicable)
// After expiration, issue new token
// Either extend or create new reservationState Transitions
UNRESERVED → RESERVED → CLAIMED → ACTIVE → EXPIRED
↓
CANCELLEDUNRESERVED: No reservation exists
- Action:
reserveToken()orreserveTokenAnonymous() - Transition to: RESERVED
RESERVED: Token reserved for recipient
- Action:
claimToken()orcancelReservation() - Transition to: CLAIMED or CANCELLED
CLAIMED: Token minted to holder
- State:
claimed = true - State:
holderExpirationset - Transition to: ACTIVE
ACTIVE: Rental period ongoing
- State:
block.timestamp ≤ expiration - Access granted
- Transition to: EXPIRED (automatic)
EXPIRED: Rental period ended
- State:
block.timestamp > expiration - Access revoked
- Token still exists but access denied
CANCELLED: Reservation removed before claim
Security Considerations
1. Expiration Enforcement
Off-chain validation required:
// Smart contract / IoT device must check expiration
modifier requireActiveRental(bytes32 hash, uint256 tokenId) {
require(balanceOf(msg.sender, tokenId) > 0, "No rental token");
require(!isExpired(hash, tokenId, msg.sender), "Rental expired");
_;
}2. Token Persistence After Expiration
Note: Expired tokens still exist in wallet
// IMPORTANT: Holding token ≠ active rental
// Always check expiration separately
// BAD:
if (balanceOf(user, 1) > 0) {
grantAccess(); // User might be expired!
}
// GOOD:
if (balanceOf(user, 1) > 0 && !isExpired(hash, 1, user)) {
grantAccess();
}3. Transfer After Expiration
Risk: Expired tokens can still be transferred
Mitigation: Override transfer to check expiration
// Option 1: Block expired token transfers
function _beforeTokenTransfer(address from, address to, uint256 tokenId, uint256 amount)
internal override
{
if (from != address(0)) { // Not minting
require(!isExpired(integraHash, tokenId, from), "Cannot transfer expired rental");
}
super._beforeTokenTransfer(from, to, tokenId, amount);
}
// Option 2: Allow transfer but recalculate expiration4. Sublease Control
Risk: Unauthorized subleasing
Options:
- Allow transfers (default ERC-1155)
- Block transfers (override
_beforeTokenTransfer) - Require approval from document owner
5. Early Termination
Risk: No built-in early termination
Workaround:
- Implement revocation function
- Or burn token to terminate
function terminateRental(bytes32 integraHash, uint256 tokenId, address tenant)
external requireOwnerOrExecutor(integraHash)
{
_burn(tenant, tokenId, balanceOf(tenant, tokenId));
}Usage Examples
Basic Rental
// 1. Reserve
rentalTokenizer.reserveToken(integraHash, 1, tenant, 1, procHash);
// 2. Claim
rentalTokenizer.claimToken(integraHash, 1, attestation, procHash);
// 3. Check access
bool hasAccess = rentalTokenizer.balanceOf(tenant, 1) > 0 &&
!rentalTokenizer.isExpired(integraHash, 1, tenant);Multiple Rentals (Same Property)
// Apartment with parking space
rentalTokenizer.reserveToken(apartmentHash, 1, tenant, 1, procHash); // Apartment
rentalTokenizer.reserveToken(apartmentHash, 2, tenant, 1, procHash); // Parking
// Tenant claims both
// Different token IDs, same holderSubscription Service
// Monthly subscription
function subscribeMonthly(address user) external {
bytes32 subHash = keccak256(abi.encodePacked(user, block.timestamp));
rentalTokenizer.reserveToken(subHash, 1, user, 1, procHash);
rentalTokenizer.claimToken(subHash, 1, attestation, procHash);
// Set 30-day expiration in holderExpiration mapping
}
// Check subscription status
function hasActiveSubscription(address user) public view returns (bool) {
return rentalTokenizer.balanceOf(user, 1) > 0 &&
!rentalTokenizer.isExpired(subHash, 1, user);
}Equipment Lease with Access Control
contract EquipmentAccess {
RentalTokenizerV7 public rentalTokenizer;
bytes32 public equipmentHash;
modifier onlyActiveRenter() {
require(
rentalTokenizer.balanceOf(msg.sender, 1) > 0,
"No rental token"
);
require(
!rentalTokenizer.isExpired(equipmentHash, 1, msg.sender),
"Rental expired"
);
_;
}
function startEquipment() external onlyActiveRenter {
// Start equipment
}
}Integration Guide
Frontend Integration
// Check rental status
async function checkRentalStatus(
integraHash: string,
tokenId: number,
userAddress: string
) {
const balance = await rentalTokenizer.balanceOf(userAddress, tokenId);
const expiration = await rentalTokenizer.getExpiration(integraHash, tokenId, userAddress);
const now = Math.floor(Date.now(./ 1000);
const isExpired = now > expiration;
const isActive = balance.gt(0) && !isExpired;
return {
hasToken: balance.gt(0),
balance: balance.toNumber(),
expiration: new Date(expiration * 1000),
isExpired,
isActive,
daysRemaining: isExpired ? 0 : Math.ceil((expiration - now) / 86400)
};
}
// Display rental info
function RentalStatus({ status }) {
if (!status.hasToken) {
return <div>No rental token</div>;
}
if (status.isExpired) {
return <div>Rental expired on {status.expiration.toDateString()}</div>;
}
return (
<div>
<p>Active rental</p>
<p>Expires: {status.expiration.toDateString()}</p>
<p>Days remaining: {status.daysRemaining}</p>
</div>
);
}IoT Device Integration
// Smart lock / access control
class SmartLock {
constructor(rentalTokenizer, integraHash, tokenId) {
this.rentalTokenizer = rentalTokenizer;
this.integraHash = integraHash;
this.tokenId = tokenId;
}
async checkAccess(userAddress) {
try {
// Check token balance
const balance = await this.rentalTokenizer.balanceOf(userAddress, this.tokenId);
if (balance.eq(0)) {
return { granted: false, reason: "No rental token" };
}
// Check expiration
const isExpired = await this.rentalTokenizer.isExpired(
this.integraHash,
this.tokenId,
userAddress
);
if (isExpired) {
return { granted: false, reason: "Rental expired" };
}
return { granted: true, reason: "Active rental" };
} catch (error) {
return { granted: false, reason: "Error checking rental" };
}
}
async unlock(userAddress) {
const access = await this.checkAccess(userAddress);
if (access.granted) {
// Unlock door
this.actuateLock(true);
console.log(`Access granted for ${userAddress}`);
} else {
console.log(`Access denied: ${access.reason}`);
}
return access;
}
}Subscription Service Integration
// Subscription manager
class SubscriptionManager {
async createSubscription(userId, plan) {
const integraHash = keccak256(userId + plan);
// Reserve subscription token
await rentalTokenizer.reserveToken(
integraHash,
1,
userId,
1,
processHash
);
// Auto-claim (or wait for user)
await rentalTokenizer.claimToken(
integraHash,
1,
attestationUID,
processHash
);
// Record subscription in database
await db.subscriptions.create({
userId,
plan,
integraHash,
startDate: new Date(),
expirationDate: new Date(Date.now() + 30 * 86400 * 1000) // 30 days
});
}
async checkSubscription(userId) {
const sub = await db.subscriptions.findOne({ userId, active: true });
if (!sub) return { active: false };
// Verify on-chain
const balance = await rentalTokenizer.balanceOf(userId, 1);
const isExpired = await rentalTokenizer.isExpired(sub.integraHash, 1, userId);
return {
active: balance.gt(0) && !isExpired,
plan: sub.plan,
expirationDate: sub.expirationDate
};
}
}Best Practices
1. Always Check Expiration
// ALWAYS verify both balance and expiration
bool hasAccess = balanceOf(user, tokenId) > 0 &&
!isExpired(integraHash, tokenId, user);2. Set Expiration During Claim
// Track rental duration in off-chain metadata
// Set expiration when token claimed
data.holderExpiration[msg.sender] = uint64(block.timestamp + LEASE_DURATION);3. Monitor Expiring Rentals
// Notify users of approaching expiration
async function checkExpiringRentals() {
const expiringThreshold = 7 * 86400; // 7 days
for (const rental of activeRentals) {
const expiration = await rentalTokenizer.getExpiration(
rental.integraHash,
rental.tokenId,
rental.holder
);
const timeRemaining = expiration - Math.floor(Date.now(./ 1000);
if (timeRemaining < expiringThreshold && timeRemaining > 0) {
// Send notification
notifyUser(rental.holder, `Rental expires in ${Math.ceil(timeRemaining / 86400)} days`);
}
}
}4. Handle Renewal
// Option 1: Extend expiration (if supported)
function extendRental(bytes32 integraHash, uint256 tokenId, address tenant, uint64 additionalTime)
external requireOwnerOrExecutor(integraHash)
{
data.holderExpiration[tenant] += additionalTime;
}
// Option 2: Issue new token
// Reserve and claim new token with new expiration5. Document Rental Terms
// Store rental terms in metadata
{
"integraHash": "0x123...",
"tokenId": 1,
"rentalType": "apartment_lease",
"duration": "365 days",
"terms": {
"rent": "1000 USD/month",
"deposit": "2000 USD",
"sublease_allowed": false
}
}Gas Optimization
Similar to other ERC-1155 tokenizers:
Reserve: ~150,000 gas Claim: ~180,000 gas (with expiration tracking)
Optimization Tips:
- Batch multiple rentals in single transaction
- Use efficient expiration storage (uint64 vs uint256)
- Cache repeated lookups
Related Contracts
Base Contracts
- BaseTokenizerV7: Access control and capability verification
- TrustGraphIntegration: Trust credentials
- ERC1155Upgradeable: Semi-fungible token standard
Related Tokenizers
- OwnershipTokenizerV7: Use for permanent ownership (not temporary)
- BadgeTokenizerV7: Use for achievements (not time-based)
- MultiPartyTokenizerV7: Use for multi-party contracts
Upgradeability
Pattern: UUPS (Universal Upgradeable Proxy Standard) Storage Gap: 47 slots
FAQ
Q: What happens when rental expires? A: Token still exists but access should be denied by checking expiration.
Q: Can expired tokens be transferred?
A: Yes, unless you override _beforeTokenTransfer. But recipient should check expiration.
Q: How do I renew a rental? A: Either extend expiration (if supported) or issue new token.
Q: Can I sublease?
A: Transferable by default (ERC-1155). Disable if needed via _beforeTokenTransfer.
Q: Do rentals auto-terminate? A: No, tokens persist. Access control systems must check expiration.
Q: Can multiple people rent same property? A: Different time periods = different token IDs or documents.
Q: How do I implement auto-renewal? A: Off-chain service that creates new reservation when nearing expiration.