Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 262 additions & 0 deletions ERCS/erc-6884.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
---
eip: 6884
title: Delegatable Utility Tokens for NFTs
description: Delegate an NFT's utility without transferring ownership.
author: Geonwoo Shin (@blgamann), Jongbeom Won (@enginism)
discussions-to: https://ethereum-magicians.org/t/eip-6884-delegatable-utility-tokens-derived-from-origin-nfts/13837
status: Draft
type: Standards Track
category: ERC
created: 2023-04-10
requires: 165, 712
---

## Abstract

This ERC specifies Delegatable Utility Tokens (DUT), which allow utility rights to be associated with [ERC-721](./eip-721.md) origin tokens. Ownership of each utility remains tied to the origin token owner, while usage rights can be delegated to other addresses without transferring ownership.

## Motivation

Most [ERC-721](./eip-721.md) NFTs provide limited or underutilized utility. These limitations become more apparent as NFTs accumulate multiple utilities and can be categorized as follows:

1. **Supply-side limitation**: Utilities are typically provided only by the project team or a small subset of holders, restricting broader ecosystem participation.
2. **Demand-side limitation**: A single owner cannot fully utilize all available utilities, leaving potential value unrealized.
3. **Time limitation**: Even if many utilities are attached to an NFT, one holder has limited time and therefore cannot make use of them all.

This standard addresses these limitations by defining Delegatable Utility Tokens (DUT). A DUT binds utility rights to an [ERC-721](./eip-721.md) origin token while allowing usage rights to be delegated independently from ownership. DUTs remain tied to the current owner of the origin token, and when ownership of the origin token changes, all associated DUTs automatically transfer to the new owner without additional transactions.

## Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

Every contract compliant with this standard contract MUST implement the `IERC6884` and `IERC165` interfaces:

```solidity
pragma solidity ^0.8.0;

interface IERC6884 /* is IERC165 */ {
/// Event emitted when the approved address for a token usage right is
/// changed or reaffirmed. The zero address indicates that there is no
/// approved address. When a Transfer event is emitted, this also
/// signifies that the approved address for that token usage right
/// (if any) is reset to none.
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);

/// Event emitted when an operator is enabled or disabled. The operator
/// has the authority to delegate or restore the token usage rights.
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

/// Event emitted when the owner delegates a token usage right to a user
/// for a specific period of time. At the end of the delegation period,
/// the owner can regain the token usage right, or the user can restore
/// it to the owner before the delegation period ends.
event Delegated(address indexed owner, address indexed user, uint256 indexed tokenId, uint256 duration);

/// Event emitted when the owner regains a token usage right that was
/// delegated to a user.
event Regained(uint256 indexed tokenId);

/// Event emitted when the user restores a token usage right that was
/// delegated.
event Restored(uint256 indexed tokenId);

/// @notice Returns the address of the ERC-721 contract that this contract
/// is based on.
/// @return The address of the ERC-721 contract.
function origin() external view returns (address);

/// @notice Returns the expiration date of a token usage right.
/// @dev The token id matches the id of the origin token.
/// @param tokenId The identifier for a token usage right.
/// @return The expiration date of the token usage right.
function expiration(uint256 tokenId) external view returns (uint256);

/// @notice Returns the number of token usage rights owned by `owner`.
/// @dev Returns the balance of the origin token. Whether the token usage
/// right is delegated or not does not affect the balance at all.
/// @param owner The address of the owner.
/// @return The number of token usage rights that can be delegated, owned
/// by `owner`.
function balanceOf(address owner) external view returns (uint256);

/// @notice Returns the address of the owner of the token usage right.
/// @dev Always the same as the owner of the origin token.
/// @param tokenId The identifier for a token usage right.
/// @return The address of the owner of the token usage right.
function ownerOf(uint256 tokenId) external view returns (address);

/// @notice Returns the address of the user of the token usage right.
/// @dev The usage rights are non-transferable and can only be delegated.
/// @param tokenId The identifier for a token usage right.
/// @return The address of the user of the token usage right.
function userOf(uint256 tokenId) external view returns (address);

/// @notice Returns the address of the approved user for this token
/// usage right.
/// @param tokenId The identifier for a token usage right.
/// @return The address of the approved user for this token usage right.
function getApproved(uint256 tokenId) external view returns (address);

/// @notice Returns true if the operator is approved by the owner.
/// @param owner The address of the owner.
/// @param operator The address of the operator.
/// @return True if the operator is approved by the owner.
function isApprovedForAll(address owner, address operator) external view returns (bool);

/// @notice Approves another address to use the token usage right
/// on behalf of the caller.
/// @param spender The address to be approved.
/// @param tokenId The identifier for a token usage right.
function approve(address spender, uint256 tokenId) external;

/// @notice Approves or disapproves the operator.
/// @param operator The address of the operator.
/// @param approved True if the operator is approved, false to revoke
/// approval.
function setApprovalForAll(address operator, bool approved) external;

/// @notice Delegates a token usage right from owner to new user.
/// @dev Only the owner of the token usage right can delegate it.
/// @param user The address of the new user.
/// @param tokenId The identifier for a token usage right.
/// @param duration The duration of the delegation in seconds.
function delegate(address user, uint256 tokenId, uint256 duration) external;

/// @notice Regains a token usage right from user to owner.
/// @dev The token usage right can only be regained if the delegation
/// period has ended.
/// @param tokenId The identifier for a token usage right.
function regain(uint256 tokenId) external;

/// @notice Restores a token usage right from user to owner.
/// @dev User can restore the token usage right before the delegation
/// period ends.
/// @param tokenId The identifier for a token usage right.
function restore(uint256 tokenId) external;
}

interface IERC165 {
/// @notice Query if a contract implements an interface
/// @param interfaceID The interface identifier, as specified in ERC-165
/// @dev Interface identification is specified in ERC-165. This function
/// uses less than 30,000 gas.
/// @return `true` if the contract implements `interfaceID` and
/// `interfaceID` is not 0xffffffff, `false` otherwise
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
```

### Metadata

The metadata extension is OPTIONAL for contracts implementing this standard. This allows your smart contract to be interrogated for its name and for details about the assets which your utility represent.

```solidity
interface IERC6884Metadata /* is IERC6884 */ {
/// @notice A descriptive name for a collection of Tokens in this contract
function name() external view returns (string memory _name);

/// @notice An abbreviated name for Tokens in this contract
function symbol() external view returns (string memory _symbol);

/// @notice A distinct Uniform Resource Identifier (URI) for a given asset.
/// @dev Throws if `_tokenId` is not a valid NFT. URIs are defined in RFC
/// 3986. The URI may point to a JSON file that conforms to the "ERC-721
/// Metadata JSON Schema".
function tokenURI(uint256 _tokenId) external view returns (string memory);
}
```

The metadata JSON schema follows the same specifications as the metadata schema used in [ERC-721](./eip-721.md).

```json
{
"title": "Asset Metadata",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Identifies the asset to which this Token represents"
},
"description": {
"type": "string",
"description": "Describes the asset to which this Token represents"
},
"image": {
"type": "string",
"description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
}
}
}
```

## Rationale

This ERC is designed to enhance NFT utility while preserving the integrity of the origin asset.

NFTs are valuable assets, and requiring them to be transferred or locked into additional contracts introduces risk. By separating usage rights from ownership, this standard enables utility expansion without moving the origin NFT.

### Why Delegatable but not Transferable

Utility rights are **delegable** but not **transferable**. Allowing utilities to be freely transferred or sold would dilute the value of the origin NFT, since its worth is defined by the sum of its associated utilities. By limiting utilities to temporary delegation, this standard ensures that:

- The aggregate value of an NFT remains tied to its ownership.
- Multiple users can benefit from utilities without fragmenting or redistributing ownership.
- Utility can be shared over time while ownership remains intact.

### Relationship to [ERC-721](./eip-721.md)

This standard does not extend [ERC-721](./eip-721.md). [ERC-721](./eip-721.md) defines transferable ownership semantics, while Delegatable Utility Tokens are explicitly non-transferable and only delegable. Rather than inheriting [ERC-721](./eip-721.md) and reverting transfer functions, a separate standard provides a clearer and simpler design.

### Owners and Users

Two roles are distinguished:

- **Owner**: the holder of the origin [ERC-721](./eip-721.md) token, and therefore the holder of the corresponding DUT.
- **User**: the account delegated to exercise the usage right.

`ownerOf(tokenId)` always returns the current owner of the origin token.
`userOf(tokenId)` returns the delegated user if an active delegation exists; otherwise it returns the owner.

### Balance

`balanceOf(owner)` reflects the balance of the origin [ERC-721](./eip-721.md) contract. Delegation state does not affect balances. This preserves compatibility with other ERC standards and makes explicit that ownership of DUTs is derived directly from ownership of the origin NFTs.

### No Minting Process

DUTs are not minted. Their existence is implicit in the existence of the origin NFT. Ownership is automatically derived from the origin contract, and delegation state is tracked separately. As a result, no additional minting or burning operations are required, and DUT ownership automatically follows transfers of the origin NFT without additional transactions or gas costs.

### Delegation Lifecycle

Delegation is managed through three functions:

- `delegate`: assigns usage rights to a user for a defined duration.
- `regain`: allows the owner to recover usage rights after expiration.
- `restore`: allows the user to return usage rights to the owner before expiration.

### Approval

An approval system, modeled on [ERC-721](./eip-721.md), enables delegation to be mediated by approved operators. This allows external applications to build markets and coordination mechanisms for usage rights without altering the underlying ownership of the NFT.

## Test Cases

Test cases are included in `test.js`.

To run:

```bash
cd assets/erc-6884
yarn install
yarn hardhat test test/test.js
```

## Reference Implementation

See [reference implementation](../assets/eip-6884/contracts/ERC6884.sol).

## Security Considerations

Delegation state may persist across NFT transfers. For example, a seller could delegate usage rights just before selling, leaving the buyer with impaired utility until expiration. Implementations SHOULD enforce proper expiration checks, restrict approvals to the owner or approved operators, and disallow sub-delegation. Marketplaces and wallets SHOULD surface delegation state (`userOf`, `expiration`) to ensure transparency during trades.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
3 changes: 3 additions & 0 deletions assets/erc-6884/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
artifacts
cache
node_modules
130 changes: 130 additions & 0 deletions assets/erc-6884/contracts/ERC6884.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract ERC6884 {
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

event Delegated(address indexed from, address indexed to, uint256 indexed tokenId, uint256 duration);
event Regained(uint256 indexed tokenId);
event Restored(uint256 indexed tokenId);

mapping(uint256 => address) private _users;

mapping(uint256 => address) private _tokenApprovals;
mapping(address => mapping(address => bool)) private _operatorApprovals;

address private _origin;
mapping(uint256 => uint256) private _expirations;

function origin() public view returns (address) {
return _origin;
}

function expiration(uint256 tokenId) public view returns (uint256) {
return _expirations[tokenId];
}

function balanceOf(address owner) public view virtual returns (uint256) {
return IERC721(_origin).balanceOf(owner);
}

function ownerOf(uint256 tokenId) public view virtual returns (address) {
return IERC721(_origin).ownerOf(tokenId);
}

function userOf(uint256 tokenId) public view virtual returns (address) {
return _expirations[tokenId] == 0 ? ownerOf(tokenId) : _users[tokenId];
}

constructor(address origin_) {
require(
IERC721(origin_).supportsInterface(type(IERC721).interfaceId),
"ERC6884: INVALID_ERC721"
);
_origin = origin_;
}

function approve(address spender, uint256 tokenId) public virtual {
address user = userOf(tokenId);
require(
msg.sender == user || _operatorApprovals[user][msg.sender],
"ERC6884: NOT_AUTHORIZED"
);

_tokenApprovals[tokenId] = spender;
emit Approval(user, spender, tokenId);
}

function setApprovalForAll(address operator, bool approved) public virtual {
_operatorApprovals[msg.sender][operator] = approved;

emit ApprovalForAll(msg.sender, operator, approved);
}

function getApproved(uint256 tokenId) public view virtual returns (address) {
return _tokenApprovals[tokenId];
}

function isApprovedForAll(address owner, address operator) public view virtual returns (bool) {
return _operatorApprovals[owner][operator];
}

function delegate(address to, uint256 tokenId, uint256 duration) public virtual {
address owner = ownerOf(tokenId);
require(
msg.sender == owner ||
_operatorApprovals[owner][msg.sender] ||
_tokenApprovals[tokenId] == msg.sender,
"ERC6884: NOT_AUTHORIZED"
);

_transfer(owner, to, tokenId);

uint256 expiration_ = block.timestamp + duration;
_expirations[tokenId] = expiration_;
emit Delegated(owner, to, tokenId, duration);
}

function regain(uint256 tokenId) public virtual {
require(_expirations[tokenId] != 0, "ERC6884: NOT_REGAINABLE");
require(_expirations[tokenId] < block.timestamp, "ERC6884: NOT_EXPIRED");

address user = userOf(tokenId);
address owner = ownerOf(tokenId);
_transfer(user, owner, tokenId);

delete _expirations[tokenId];
emit Regained(tokenId);
}

function restore(uint256 tokenId) public virtual {
address user = userOf(tokenId);
require(
msg.sender == user ||
_operatorApprovals[user][msg.sender] ||
_tokenApprovals[tokenId] == msg.sender,
"ERC6884: NOT_AUTHORIZED"
);

address owner = ownerOf(tokenId);
_transfer(user, owner, tokenId);

delete _expirations[tokenId];
emit Restored(tokenId);
}

function _transfer(
address from,
address to,
uint256 tokenId
) internal virtual {
require(userOf(tokenId) == from, "ERC6884: INVALID_SENDER");
require(to != address(0), "ERC6884: ZERO_ADDRESS");

delete _tokenApprovals[tokenId];
_users[tokenId] = to;
}
}
Loading
Loading