Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Rentable NFT (EIP-4907) + Rental Market

An ERC-721 collection implementing EIP-4907 — the "rentable NFT" standard that adds a time-limited user role alongside the owner — plus a self-contained rental marketplace that lets an owner rent out USE of a token per second without ever handing over ownership.

Contracts

Contract Purpose Runtime size
src/RentableNft.sol ERC-721 + EIP-4907 rentable NFT 4,329 bytes*
src/RentalMarket.sol Escrow marketplace over any EIP-4907 collection 4,652 bytes
src/IERC4907.sol The canonical EIP-4907 interface

* Both are far under the EIP-170 24,576-byte limit.

The EIP-4907 user-vs-owner model

A rentable NFT carries two distinct roles:

  • Owner — holds the token, can transfer / sell / burn it, and is the only party (with an approved operator) allowed to grant the user role via setUser.
  • User — an address granted the time-limited right to USE the token until an expires timestamp. The user may drive whatever off-chain or on-chain utility gates on userOf (game access, metadata, membership) but never owns the token and can never transfer, sell, or re-delegate it.

RentableNft implements the standard surface:

  • setUser(uint256 tokenId, address user, uint64 expires) — owner/approved only.
  • userOf(uint256) returns (address) — the current user, or address(0) once expired.
  • userExpires(uint256) returns (uint256) — the raw expiry timestamp.
  • UpdateUser(tokenId, user, expires) event on every change.

Expiry boundary decision. userOf returns the user while expires >= block.timestamp — the token is still usable at exactly block.timestamp == expires, and lapses one second later. This matches the EIP-4907 reference implementation and is fuzzed at the boundary.

Transfer clears the rental. Per the spec, selling a token ends any active rental. _update (the OZ v5 transfer hook) wipes the UserInfo and emits UpdateUser(id, 0, 0) on every ownership change, so a buyer never inherits someone else's user right. Mints are exempt (no prior user to clear).

Rental market flow

The market works with any IERC4907 + IERC721 collection and settles rent in a single ERC-20 payment token fixed at construction.

owner ──approve──▶ list(collection, tokenId, pricePerSecond, minDuration, maxDuration)
                     └─ NFT is escrowed in the market; owner recorded as `lister`
renter ─pay ppS·d─▶ rent(collection, tokenId, duration)
                     ├─ pulls pricePerSecond·duration from the renter
                     ├─ retains a bounded protocol fee, forwards the rest to the lister
                     └─ sets the renter as EIP-4907 user until block.timestamp + duration
lister ──────────▶ delist(collection, tokenId)   (only when no active rental)
                     └─ NFT returned to the lister

Views: listing(collection, tokenId), isRented(collection, tokenId), quote(collection, tokenId, duration) → (cost, fee, payout).

Escrow / settlement model

Escrow-the-NFT. To list, the owner deposits the token into the market, which becomes the on-chain owner while it is listed. This is exactly what lets the market grant EIP-4907 user rights — only the owner/approved may call setUser, and escrow makes the market that owner. It is the simplest and safest model for a self-contained demo: there is no standing setUser approval dangling on the owner's wallet, and the market's authority is scoped to tokens actually deposited with it. The original owner is recorded as the lister and is the only party who can reclaim the token (delist), and only while no rental is active.

Rent is settled atomically inside one nonReentrant call: pull the renter's payment, retain the fee, forward the remainder to the lister — all in the same transaction. The market never parks renter funds; the only balance it ever accrues is protocol fees.

Renter-can't-own security model

  • The renter buys USE, never ownership. On rent the market sets the renter as the token's EIP-4907 user; the NFT itself is never transferred to the renter.
  • The renter has no path to transfer, sell, re-list, or re-delegate the token — setUser and transfers both require owner/approval, which the renter never holds. (Tested directly.)
  • An escrowed NFT is only ever returnable to its recorded lister, never to a renter.
  • A raw safeTransferFrom into the market (not via list) is rejected, so no NFT can be stranded without a listing (onERC721Received accepts only market-initiated deposits).
  • The owner cannot yank an NFT out from under an active renter: delist reverts while userOf != 0.

Bounded protocol fee

Each rent pays a protocol fee of feeBps / 10_000, capped at MAX_FEE_BPS = 1000 (10%) — the owner can never set a higher fee, at construction or via setFeeBps. Fees accrue in the contract and are withdrawable only by the feeRecipient (withdrawFees). Conservation holds exactly: Σ rent payments == Σ lister payouts + fees held + fees withdrawn.

Tests

67 tests, all green (forge test): 21 EIP-4907 unit/fuzz tests, 42 market unit/fuzz tests, and 4 stateful invariants.

The invariant suite runs 12,800 calls with 0 reverts per invariant under fail_on_revert = true (pinned inline per invariant, proving the run is non-hollow):

  • invariant_feeConservationΣ rent payments == Σ payouts + fees held + fees withdrawn.
  • invariant_marketSolvency — the market's payment-token balance equals exactly the accrued (not yet withdrawn) fees; renter funds are never parked.
  • invariant_nftCustody — every listed token is escrowed by the market; no tracked token is ever owned by a renter.
  • invariant_activeRentalOwnedByMarket — a token with a live rental is always owned by the market, never its renter; user and owner stay strictly separated.

Reentrancy is covered from both hostile directions: a malicious payment token that re-enters rent during the pull, and a malicious EIP-4907 collection that re-enters rent from inside setUser — both blocked by nonReentrant (SafeERC20 + IERC721Receiver on the market).

Payment assumes a standard (non-fee-on-transfer) ERC-20; fee-on-transfer payment tokens are out of scope, as documented in the contract.

Deep dive (v2)

A second adversarial pass targeted the renter/owner boundary, custody stranding, fee accounting, and reentrancy. No exploitable bug was found — the market holds. The audit confirmed and added tests for:

  • Renter can never own or control. A renter gets only the EIP-4907 user role; they cannot transfer the NFT, re-list it, delist it, or setUser themselves (all covered).
  • Owner can't yank a rented NFT, and can always reclaim afterwards. delist reverts during an active rental and only ever returns the token to the recorded lister. While escrowed, even the original lister cannot setUser (can't kick the renter) nor transfer the NFT out — the market is the sole authority (test_ListerCannotSetUserWhileEscrowed, test_ListerCannotTransferEscrowedNft).
  • Boundary re-rent. A token is not re-rentable at the exact expiry second (userOf valid while expires >= now), only strictly past it (test_Rent_BlockedAtExactExpiryBoundary).
  • Reentrancy from three hostile directions — malicious payment token (pull), malicious EIP-4907 collection (setUser), and a contract lister re-entering delist from onERC721Received — all blocked by nonReentrant (test_Delist_ReentrantListerBlocked).
  • Fee-on-transfer stance made concrete. A FoT payment token breaks the balance == accruedFees solvency identity; test_FeeOnTransferBreaksSolvencyStance pins this so integrators know not to configure one.

Usage

forge build --sizes     # compile, check EIP-170
forge test              # 67 tests
forge fmt --check       # formatting
forge snapshot          # gas snapshot (.gas-snapshot)

Deploy with script/Deploy.s.sol (reads config from env; see the script header).

License

MIT

About

Rentable NFT (ERC-4907) + rental market: time-limited user role separate from ownership, escrow-based rentals, renter can never own/transfer, fee + custody invariants

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages