ethereum / EIPs

The Ethereum Improvement Proposal repository
https://eips.ethereum.org/
Creative Commons Zero v1.0 Universal
12.74k stars 5.18k forks source link

ERC: Renting Standard for Rival, Non-Fungible Tokens #809

Closed slee981 closed 2 years ago

slee981 commented 6 years ago

Preamble

EIP: <to be assigned>
Title: Renting Standard for Rival, Non-Fungible Tokens
Author: Steven Lee <steven@booklocal.in>
Type: Standard
Category: ERC
Status: Draft
Created: 2017-12-26

Summary

A standard interface for renting rival non-fungible tokens.

Definitions

Rival good: a good is rival if its consumption by one individual prevents simultaneous consumption by other individuals. For example, driving a car is rival but watching the sunset is non-rival.

Non-Fungible good: a good is non-fungible if it is not interchangeable. For example, cars are non-fungible but Ether is fungible.

Abstract

The following suggests a standard API for renting access to rival non-fungible tokens within smart contracts.

Motivation

A standard interface would allow for any rival non-fungible token (rival NFT) on Ethereum to be handled by general purpose applications for renting purposes. Specifically, this would allow an owner to rent access to their rival NFTs using a standard set of commands, thus allowing users to view all past and current rental agreements from a single wallet interface.

Specifications

ERC-721 Compatibility

This section taken from ERC721. Follow link for a more detailed description of the ERC721 methods.

name

function name() constant returns (string name)

OPTIONAL - It is recommend that this method is implemented for enhanced usability with wallets and exchanges, but interfaces and other contracts MUST NOT depend on the existence of this method.

symbol

function symbol() constant returns (string symbol)

OPTIONAL - It is recommend that this method is implemented for enhanced usability with wallets and exchanges, but interfaces and other contracts MUST NOT depend on the existence of this method.

totalSupply

function totalSupply() public view returns (uint256 total)

Returns the total number of NFTs currently tracked by this contract.

balanceOf

function balanceOf(address _owner) public view returns (uint256 balance)

Returns the number of NFTs assigned to address _owner.

ownerOf

function ownerOf(uint256 _tokenId) external view returns (address owner);

Returns the address currently marked as the owner of _tokenID.

approve

function approve(address _to, uint256 _tokenId) external

Grants approval for address _to to take possession of the NFT with ID _tokenId.

transferFrom

function transferFrom(address _from, address _to, uint256 _tokenId) external

Assigns ownership of the NFT with ID _tokenId to _to if and only if _from has been previously granted approval

transfer

function transfer(address _to, uint256 _tokenId) external

Assigns the ownership of the NFT with ID _tokenId to _to if and only if msg.sender == ownerOf(_tokenId). A successful transfer MUST fire the Transfer event (defined below).

Basic Renting

reserve

reserve(uint256 _tokenId, uint256 _start, uint256 _stop) external returns (bool success)

Reserve access to token (_tokenId) from time _start to time _stop. A successful reservation must ensure each time slot in the range _start to _stop is not previously reserved (by calling the function checkAvailable() described below) and then emit a Reserve event. For example, this could be implemented through a double mapping given by,

mapping(uint256 -> mapping(uint256 -> address)) reservations

In this case, we could map _tokenId to a range of time slots, and each time slot to an address. If each time in the range _start to _stop returns address(0), then add reservations[_tokenId][_time] = msg.sender for every _time in range _start to _stop.

Further considerations include adding a reservation price, although this may not be necessary in the case of an auction for rental price.

access

access(uint256 _tokenId) external returns (bool success)

If msg.sender == reservations[_tokenId][now] then grant access. Due to the possibility of different units of time preferred in the reservation (i.e. reserving by the second, hour, day, week, and so on), now may need to be checked to the nearest reservation unit. This is to say that access must ensure that msg.sender has a reservation for that time slot.

settle

settle(uint256 _tokenId, address _renter, uint256 _stop) external returns (bool success)

Removes _renter access to _tokenId and transfers any agreed upon funds only if _renter == reservations[_tokenId][_stop]. Analogous to checking out of a hotel room or returning a rental car.

This function should be callable by either the owner of _tokenId or _renter, however, the owner should only be able to call this function if now >= _stop to prevent premature settlement of funds.

checkAvailable

checkAvailable(uint256 _tokenId, uint256 _time) public view returns (bool available)

Returns true if reservations[_tokenId][_time] == address(0) and false otherwise.

cancelReservation

cancelReservation(uint256 _tokenId, uint256 _start, uint256 _stop) external returns (bool success)

Returns true if msg.sender == reservations[_tokenId][_time] for every _time in range _start to _stop and deletes reservation.

Events

Transfer

event Transfer(address indexed _from, address indexed _to, uint256 _tokenId)

Consistent with ERC721.

Approval

event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId)

Consistent with ERC721.

Reserve

event Reserve(address indexed _renter, uint256 _tokenId, uint256 _start, uint256 _stop)

Must trigger on any successful call to reservation.

Cancel

event CancelReservation(address indexed _renter, uint256 _tokenId, uint256 _start, uint256 _stop)

Must trigger on any successful call to cancelReservation.

Rationale

The Ethereum blockchain provides the opportunity for reimagined distribution systems in asset rental markets (i.e. the travel industry). A community standard would help encourage secure rental contracts with common interfaces, thus allowing renters and owners alike to view all of their rental agreements from a single application.

Implementation

https://github.com/BookLocal/EthMemphis

pabloruiz55 commented 6 years ago

Hi Steven, I like the idea. You can check this article/code I wrote a while ago about renting smart contracts (it's specifically about renting smart contracts and not NF tokens), if it helps: https://hackernoon.com/renting-items-on-the-blockchain-de4e2663dfc6

I'm concerned about the way a reservation is made and checked:

In this case, we could map _tokenId to a range of time slots, and each time slot to an address. If each time in the range _start to _stop returns address(0), then add reservations[_tokenId][_time] = msg.sender for every _time in range _start to _stop.

If I understand correctly, you propose storing each time that falls within _start & _stop for the token being rented and then checking with now to see if the caller actually is currently set within the mapping. So, if I rent the item ID1 for a year this smart contract would store my address in the mapping X times as reservations[ID1][X] = msg.sender where x is each second from _start to _stop. This would mean that for a 1 year reservation you would be storing my address 31 million times (3600 seconds 24 hours 365 days).

slee981 commented 6 years ago

Hi Pablo, thanks for the feedback. Just checked out your article and Rentable contract. It looks good. Have you thought of a different way of using your rentable contract to make reservations in the future? For example, with movies this wouldn't matter since many people can rent simultaneously (the good is non-rival), but in the case of renting a seat on an airplane, rental car, or hotel room, you would want to know that you alone have access to that asset at a specific time.

I totally agree with the issue of renting by the second. My idea is use (as in your rentable idea) a minimum unit of rent. For example, to rent by the hour we could say uint MIN_RENT = 3600 and then store time as _time = _start/MIN_RENT and finally store as reservations[_tokenId][_time] = msg.sender where _time is stored in hours.

This does still pose potential rounding issues, but then I think we could find a reasonable tolerance around the rent date (i.e. search the reservations[_tokenId] mapping for _time +/- TOLERANCE).

montsamu commented 6 years ago

I’m also interested in this and would plan to implement it in my next project.

finnious commented 6 years ago

I heard Arthur Camara of CrypoKitties on The Blockchain Guy podcast. Arthur talked about ERC-721. I can't help but think how your functions give more power to the participants. While CrypoKitties maintain ultimate control of the tokens(kitties) via their marketplace.

@slee981 What are your thoughts on how much control over a token should fall under the owner vs fall under some kind of marketplace overseer?

jruffer commented 6 years ago

This is great! Are you thinking this would also work for time sharing for others to be able to re-rent their timeshare? Also, this looks like it could also work for items?

slee981 commented 6 years ago

@finnious I think the main reason that the CryptoKitties creators may have so much control over their tokens is that standard wallets aren't yet compatible with the ERC-721 interface. Thus, they control the interaction. Same applies here.

Wallets will most likely adapt once there are more applications that use non-fungible tokens, and hopefully the same applies for renting.

slee981 commented 6 years ago

@jruffer in principle this should work with timeshares, but depends on how payments are incorporated. Right now I see a few options:

  1. Auctions: reservation goes to highest bidder
  2. Pay-per-time: access cost per unit time. This might make more sense for non-rival goods though, like movie rentals, where it returning earlier or later doesn't impact someone else's ability to rent the item.
  3. Flat-rate: set a specific rate upfront. This option could be implemented a few different ways. Either pay all funds immediately upon making the reservation or transfer funds to a multi-sig wallet (between token owner and renter). The latter option would allow for more complex rules around canceling reservations or dispute settlement (i.e. damages to real-world property).

Timeshares could probably be made feasible by applying a modified version of this rental standard on top of an existing rental agreement. For example, if you rent a beach house from someone, you could open up a second rentable token on top of their token, but only for the dates you've reserved.

Dsummers91 commented 6 years ago

If the minimum times are going to be standard throughout the contract (i.e. daily for hotel, or hourly for bike rental). Instead of having minimum times, you could partition each tokens time by that time block.

For a hotel each individual day would in essence be its own token within each NFT room token. So lets say initialTime is noon (43200) and each _timeBlock is a day (86400), and _startTime is the starting date someone wants to reserve something. Then the mapping would be reservations[_tokenId][_startTime] where (_startTime - initialTime) % _timeBlock = 0

That way the tokens are more uniform and are able to trade timeblocks between people

hyperfekt commented 6 years ago

May I suggest that we work towards making individual reservations NFTs conformant to ERC821/721? This would allow reusing contracts that handle these kinds of NFTs for individual reservations, and reimplementing e.g. transfers, auctions and sales could be avoided.

Also it is not entirely clear to me how this compares to ERC #808, it appears there is some overlap which would allow either a cross-compatible subset or merging?

saurfang commented 6 years ago

This looks really promising not just for accomodation reservation but like laid out in the proposal, it can be applicable to other rival goods. It is helpful to create some toy implementations of different use cases and it can help us decide what is the largest common denominator that should make into the interface specification.

I started a basic implementation of a virtual ads billboard. The idea is to simulate renting out a virtual space on your website for ads. People can bid on ad impression for a future period. It is not intended to be a real product but for exploring flexibility of ERC809 only since BookLocal already demonstrated its use on accomodation.

I intend to make a front-end eventually but for now, you can refer to the test cases on its usage.

A lot of what I have here is a working in progress but I feel there is enough progress to get some feedbacks and I want to incorporate your ideas and suggestions to the iterations.

Availability Storage

We can avoid storing reservation for each time unit by only storing start and stop timestamp in a sorted treemap. It translates the problem into a calendar scheduling problem that can be solved in O(logn) for each insertion and lookup w.r.t number of reservations in the system.

I started a treemap implementation under solidity-treemap (WIP. Again see tests for its usage.) I found some existing solidity AVL tree implementation but they don't appear to be well documented or tested.

I use this treemap to maintain availability here. It is actually pretty simple and sweet with treemap.

ERC809 Interface

I copied Steven's proposal into a contract interface here with some modifications. I changed checkAvailable to take uint256 _tokenId, uint256 _start, uint256 _stop instead since we now operate on a time range instead of a discrete time unit.

@slee981 Can you share some concrete examples on what you had in mind for access function? I understand it should be called by the renter. What exactly should happen? I found it to be more useful to have hasAccess(uint256 _tokenId, uint256 _address, uint256 _time) public view returns(bool) to allow people to check if some _address has access to _tokenId as of _time. I simplified it to function renterOf(uint256 _tokenId, uint256 _time) public view returns (address); which practically does the same thing but limits each token strictly rival.

I have some worry over the usefulness of reserve function. I agree it is a core-functionality but it seems reserve requires a lot more metadata such as price that you already mentioned. Maybe we can include a metadata argument for the reservation request as a catch-all? When exactly do renter pay for the reservation since reserve is not payable?

ERC809 is ERC721?

I think @hyperfekt has a great idea that ERC809 leases themselves can be ERC721 tokens. Currently, the proposal frames that ERC809 enhances an ERC721 token with the ability to lease out a specific token for a period of time to a renter. Meanwhile, renting/leasing is really just "access", a (usually) non-tangible non-fungible tokenizable resource:

  1. Booking an accommodation grants access to space for the agreed upon time period
  2. Reserve an (autonomous) car ride gives access to transportation from A to B
  3. Reserve a group tour gives access to an experience that lasts the duration of the tour
  4. Bid on an advertisement impression gives access to a virtual real estate to place marketing content

While "access"s are tied to a non-fungible resource, they only provide up to the level of rights they promised. If I rent a room from a home-owner, I don't magically own the home and I cannot make modifications to the home. While each "access" can be rival, a tokenized asset might have different kinds of "access" one can derive. It seems sub-optimal to tightly couple a specific type of "access" to the token contract itself.

In a contrived example, if I deploy an ERC721 Car token, I can have two ERC809 access tokens related to this car:

  1. one grants access to car rider
  2. the other grants access to car operator

where car rider reserve the car for a ride and car operator lease the car to drive, and make money by providing rides.

Long story short, I think tokenizing access (reservation/booking), isolated from the underlying token contract, enable a lot more interesting capabilities:

  1. In addition to being non-fungible, an ERC809 access token is ephemeral with a defined living window
  2. Each ERC809 access token could be associated with another ERC721 and access can be divisible or combinable by the time (when permitted)
    1. If I am a travel agent, I can reserve a whole month of accommodation and split them into pieces as I package them for my travel clients (wholesaling)
  3. Access token can be transferred and traded (when permitted)
    1. If the implementation interferes with an identity contract
      1. tokens can be transferred to other accounts owned by the same identity
      2. tokens can be purchased on someone else' behalf (e.g. booking agent)
    2. If resale is permitted, the access can be traded on a secondary marketplace such as OpenSea
saurfang commented 5 years ago

So I took @hyperfekt's idea and @zemingyu's https://github.com/ethereum/EIPs/issues/1201 and ran with it a bit. I think we come to the realization that ownership and rental rights equally deserve to be a token by themselves. Instead of jamming both in a single contract, what do you all think about composing them with two ERC721 contracts instead like so:

pragma solidity ^0.4.23;

import "openzeppelin-solidity/contracts/token/ERC721/ERC721.sol";

/// @title ERC809: a standard interface for rentable rival non-fungible tokens.
contract ERC809 is ERC721 {
  // address of the ERC721 contract tokenizing reseravation/access of this contract's token
  address public reservationContract;

  /// @notice Find the renter of an NFT token as of `_time`
  /// @dev The renter is who made a reservation on `_tokenId` and the reservation spans over `_time`.
  function renterOf(uint256 _tokenId, uint256 _time) public view returns (address);

  /// @notice Query if token `_tokenId` if available to reserve between `_start` and `_stop` time
  function isAvailable(uint256 _tokenId, uint256 _start, uint256 _stop) public view returns (bool);

  /// @notice Cancel reservation for `_tokenId` between `_start` and `_stop`
  /// @dev All reservations between `_start` and `_stop` are cancelled. `_start` and `_stop` do not guarantee
  //   to be the ends for any one of the reservations
  function cancelAll(uint256 _tokenId, uint256 _start, uint256 _stop) public returns (uint256);

  /// @notice Cancel a single reservation for `_tokenId`
  function cancel(uint256 _tokenId, uint256 _reservationId) public;
}

/// @title ERC809Child: an auxiliary ERC809 token representing access to a ERC809.
contract ERC809Child is ERC721 {
  // address of the parent ERC721 contract whose tokens are open for access
  address public owner;

  /// @dev This emits when a successful reservation is made for accessing any NFT.
  event Creation(address indexed _renter, uint256 _calendarId, uint256 _tokenId);

  /// @dev This emits when a successful cancellation is made for a reservation.
  event Cancellation(address indexed _renter, uint256 _calendarId, uint256 _tokenId);
}

By the way, this was motivated by @shrugs's suggestion in ERC1155 of composing ERC721. Before that, I struggled for a long time on the tradeoff between two tokens: putting them in one contract would put one of them as a second-class citizen, but creating separate contracts doesn't seem have benefit because most of the logic is so intertwined and doesn't allow a separation of concerns.

I wrote a Medium post about this exploration and a proof of concept implementation called MeetETH. I appreciate your thoughts and suggestions.

github-actions[bot] commented 2 years ago

There has been no activity on this issue for two months. It will be closed in a week if no further activity occurs. If you would like to move this EIP forward, please respond to any outstanding feedback or add a comment indicating that you have addressed all required feedback and are ready for a review.

github-actions[bot] commented 2 years ago

This issue was closed due to inactivity. If you are still pursuing it, feel free to reopen it and respond to any feedback or request a review in a comment.

shanu12joshi commented 2 years ago

I am not able to find ERC-809, does it not exists anymore?

MicahZoltu commented 2 years ago

It appears this never got turned into an EIP, it was just an idea that no one ran with.

tommed commented 1 year ago

I really like this idea. But it would need to be expanded to cater for rentals where there is no pre-determined end date too. I.e., I want to borrow your car, and when I return it, I will then know how much I owe you (i.e., based on a price per minute).

I agree the mapping for time incrementals is not efficient, but you could track a binary of whether the item was currently in rental to keep this simple - but then use some kind of trigger to set this to false when the end of the contract occurs.

I'm guessing this may be replaced by EIP-4907?