ERC-4907: Rental NFT — User Role with Expiry
ERC-4907 splits ERC-721 ownership into two roles: the owner (holds the NFT, can sell it) and the user (has time-limited usage rights). The user role expires automatically at a unix timestamp; querying userOf after expiry returns address(0) without requiring an on-chain reset call. This is the standard rental primitive used by gaming guilds, scholarship programs, virtual-land subleasing, and Yuga Labs-style member portals.
Required Interface (delta from ERC-721)
interface IERC4907 {
event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires);
function setUser(uint256 tokenId, address user, uint64 expires) external;
function userOf(uint256 tokenId) external view returns (address);
function userExpires(uint256 tokenId) external view returns (uint256);
}Semantics:
setUser— owner sets the renter and expiry; only the owner (or approved operator) can call.userOf— returns the renter iffblock.timestamp <= expires, otherwiseaddress(0).- The user role is not transferable independently — transferring the NFT clears the user (per OpenZeppelin's reference implementation).
Neo Equivalent: NEP-11 + Per-Token Storage
NEP-11 has no native user-role concept, but the extension is a clean storage-only addition: store (tokenId) -> (user, expires) and check the timestamp on read. Two design choices to make:
A. Clear-on-transfer — owner-side transfers reset the user role (matches the OpenZeppelin reference). Use this when the rental is per-NFT and shouldn't survive secondary sales.
B. Persist-across-transfer — keep the user role even when the NFT moves (rare; useful for rentals that are independently tradeable). The ERC permits this but the de-facto convention is clear-on-transfer.
| ERC-4907 (Ethereum) | Neo Equivalent | Notes |
|---|---|---|
setUser(tokenId, user, expires) | SetUser(tokenId, user, expires) with Runtime.CheckWitness(OwnerOf(tokenId)) | Owner-only auth via witness |
userOf(tokenId) returns user iff block.timestamp <= expires | UserOf(tokenId) returns user iff Runtime.Time <= expires | Direct equivalent; Runtime.Time is unix ms |
UpdateUser event | OnUserUpdated notification | Wire-format equivalent |
| Transfer clears user (OZ reference) | Override NEP-11 Transfer to delete user/expires keys before delegating to base impl | Pattern A; explicit |
| Owner-side approval | NEP-11 Approve extension (if present) → SetUser honours approved operators | Optional; depends on NEP-11 base |
Pairs Well With
- ERC-6551 — the renter (user) of an NFT could itself control the NFT's TBA, so granting user role grants TBA usage. Useful for "rent a whole character including its inventory".
- ERC-2981 royalty splits — set the renter as a partial royalty recipient during the rental window, owner gets the rest.
Migration Notes
OpenZeppelin's ERC4907 reference contract ports cleanly to Neo via neo-solc: replace block.timestamp with block.timestamp (lowered to Runtime.Time / 1000 by the compiler), replace the _userInfo mapping with NEP-11 storage prefixes, and override the inherited _beforeTokenTransfer hook (or transfer directly) to clear user/expires on movement.
The C# port (next tab) is the idiomatic Neo shape.
