EIP5006: ERC-1155 Usage Rights Extension


eip: 5006
title: ERC-1155U, ERC-1155 Usage Rights Extension
description: Add a user role with restricted permissions to ERC-1155 tokens.
author: Lance (@LanceSnow), Anders (@0xanders), Shrug shrug@emojidao.org
discussions-to:
status: Draft
type: Standards Track
category: ERC
created: 2022-04-12
requires: 165, 1155

Abstract

This standard is an extension of ERC-1155. It proposes an additional role (user) which can be granted to addresses. The user role represents permission to “use” the NFT, but not be able to transfer it or set operators.

Motivation

Some NFTs have certain utilities. For example: in-game NFTs can be “used” to play, virtual land can be “used” to build scenes, and music NFTs can be “used” while listening. In some cases, the owner and user may not be the same account. Someone may purchase an NFT with utility, but they may not have time or ability to use it, so separating the “use” right from ownership makes a lot of sense.

Nowadays, many NFTs are managed by adding the role of controller/operator. Accounts in these roles can perform specific usage actions but can’t approve or transfer the NFT like an owner.

It is conceivable that with the further expansion of NFT application, the problem of usage rights management will become more common, so it is necessary to establish a unified standard to facilitate collaboration among all applications.

By adding user, it enables multiple protocols to integrate and build on top of usage rights.

Specification

The keywords “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.

// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";

interface IERC5006 is IERC1155 {
    event UpdateUser(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256 id,
        uint256 value
    );

    /**
     * @dev Returns the amount of tokens of token type `id` used by `user`.
     *
     * Requirements:
     *
     * - `user` cannot be the zero address.
     */
    function balanceOfUser(address user, uint256 id)
        external
        view
        returns (uint256);

    /**
     * @dev Returns the amount of frozen tokens of token type `id` by `owner`.
     *
     * Requirements:
     *
     * - `owner` cannot be the zero address.
     */
    function frozenOfOwner(address owner, uint256 id)
        external
        view
        returns (uint256);

    /**
     * @dev Returns the amount of tokens of token type `id` used by `user`.
     *
     * Requirements:
     *
     * - `user` cannot be the zero address.
     * - `owner` cannot be the zero address.
     */
    function balanceOfUserFromOwner(
        address user,
        address owner,
        uint256 id
    ) external view returns (uint256);

    /// @notice set the user of a NFT
    /// @dev The zero address indicates there is no user
    /// Throws if `tokenId` is not valid NFT
    /// @param user  The new user of the NFT
    /// @param amount  The new user could use
    function setUser(
        address owner,
        address user,
        uint256 id,
        uint256 amount
    ) external;
}

Rationale

Many developers are trying to develop based on the NFT utility, and some of them have added roles already, but there are some key problems need to be solved. The advantages of this standard are below.

Clear Permissions Management

Usage rights are part of ownership, so owner can modify user at any time, while user is only granted some specific permissions, such as user usually does not have permission to make permanent changes to NFT’s Metadata.

NFTs may be used in multiple applications, and adding the user role to NFTs makes it easier for the application to make special grants of rights.

Easy Third-Party Integration

The standard makes it easier for third-party protocols to manage NFT usage rights without permission from the NFT issuer or the NFT application.

Backwards Compatibility

As mentioned in the specifications section, this standard can be fully ERC compatible by adding an extension function set.

In addition, new functions introduced in this standard have many similarities with the existing functions in ERC1155. This allows developers to easily adopt the standard quickly.

Test Cases

run in Terminal:

npm hardhat test

Test Code

import { expect } from "chai";
import { ethers } from "hardhat";

describe("Test ERC5006", function () {
    let alice, bob, carl;
    let contract;

    beforeEach(async function () {
        [alice, bob, carl] = await ethers.getSigners();

        const ERC5006Demo = await ethers.getContractFactory("ERC5006Demo");

        contract = await ERC5006Demo.deploy();
    });

    describe("", function () {
        it("Should set user to bob", async function () {

            await contract.mint(alice.address, 1, 100);

            await contract.setUser(alice.address, bob.address, 1, 10);

            expect(await contract.balanceOfUser(bob.address, 1)).equals(10);

            expect(await contract.balanceOfUserFromOwner(bob.address, alice.address, 1)).equals(10);

            expect(await contract.frozenOfOwner(alice.address, 1)).equals(10);

            await contract.setUser(alice.address, bob.address, 1, 80);

            expect(await contract.balanceOfUser(bob.address, 1)).equals(80);

            expect(await contract.balanceOfUserFromOwner(bob.address, alice.address, 1)).equals(80);

            expect(await contract.frozenOfOwner(alice.address, 1)).equals(80);

            await contract.setUser(alice.address, bob.address, 1, 0);

            expect(await contract.balanceOfUser(bob.address, 1)).equals(0);

            expect(await contract.balanceOfUserFromOwner(bob.address, alice.address, 1)).equals(0);

            expect(await contract.frozenOfOwner(alice.address, 1)).equals(0);

        });

        it("Should transfer success", async function () {

            await contract.mint(alice.address, 1, 100);

            await contract.setUser(alice.address, bob.address, 1, 10);

            await contract.safeTransferFrom(alice.address, carl.address, 1, 90, "0x");

            expect(await contract.balanceOfUser(bob.address, 1)).equals(10);

            expect(await contract.balanceOfUserFromOwner(bob.address, alice.address, 1)).equals(10);

            expect(await contract.frozenOfOwner(alice.address, 1)).equals(10);

            expect(await contract.balanceOf(alice.address, 1)).equals(10);

        });

        it("Should burn success", async function () {

            await contract.mint(alice.address, 1, 100);

            await contract.setUser(alice.address, bob.address, 1, 10);

            await contract.burn(alice.address, 1, 90);

            expect(await contract.balanceOfUser(bob.address, 1)).equals(10);

            expect(await contract.balanceOfUserFromOwner(bob.address, alice.address, 1)).equals(10);

            expect(await contract.frozenOfOwner(alice.address, 1)).equals(10);

            expect(await contract.balanceOf(alice.address, 1)).equals(10);
        });

    });

});

Test contract:

// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.0;

import "./ERC5006Demo.sol";

contract ERC5006Demo is ERC5006 {
    function mint(
        address to,
        uint256 id,
        uint256 amount
    ) public {
        _mint(to, id, amount, "");
    }

    function burn(
        address from,
        uint256 id,
        uint256 amount
    ) public {
        _burn(from, id, amount);
    }
    
}

Reference Implementation

// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "./IERC5006.sol";

contract ERC5006 is ERC1155, IERC5006 {
    /**mapping(tokenId=>mapping(user=>amount)) */
    mapping(uint256 => mapping(address => uint256)) private _userAllowances;

    /**mapping(tokenId=>mapping(owner=>amount)) */
    mapping(uint256 => mapping(address => uint256)) private _frozen;

    /** mapping(tokenId=>mapping(owner=>mapping(user=>amount))) */
    mapping(uint256 => mapping(address => mapping(address => uint256)))
        private _allowances;

    constructor() ERC1155("") {}

    function balanceOfUser(address user, uint256 id)
        public
        view
        returns (uint256)
    {
        return _userAllowances[id][user];
    }

    function balanceOfUserFromOwner(
        address user,
        address owner,
        uint256 id
    ) public view returns (uint256) {
        return _allowances[id][owner][user];
    }

    function frozenOfOwner(address owner, uint256 id)
        external
        view
        returns (uint256)
    {
        return _frozen[id][owner];
    }

    function setUser(
        address owner,
        address user,
        uint256 id,
        uint256 amount
    ) public virtual {
        require(user != address(0), "ERROR: transfer to the zero address");
        address operator = msg.sender;
        uint256 fromBalance = balanceOf(owner, id);
        _frozen[id][owner] -= _allowances[id][owner][user];
        uint256 frozen = _frozen[id][owner];
        require(
            fromBalance - frozen >= amount,
            "ERROR: insufficient balance for setUser"
        );
        unchecked {
            _frozen[id][owner] = frozen + amount;
        }
        _userAllowances[id][user] -= _allowances[id][owner][user];
        _userAllowances[id][user] += amount;
        _allowances[id][owner][user] = amount;

        emit UpdateUser(operator, owner, user, id, amount);
    }

    function _beforeTokenTransfer(
        address operator,
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) internal virtual override {
        for (uint256 i = 0; i < ids.length; i++) {
            if (from != address(0)) {
                uint256 id = ids[i];
                uint256 fromBalance = balanceOf(from, id);
                uint256 frozen = _frozen[id][from];
                require(
                    fromBalance - frozen >= amounts[i],
                    "ERROR: insufficient balance for transfer"
                );
            }
        }
    }

    /// @dev See {IERC165-supportsInterface}.
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return interfaceId == type(IERC5006).interfaceId || super.supportsInterface(interfaceId);
    }
}


Security Considerations

This EIP standard can completely protect the rights of the owner, the owner can change the NFT user.

2 Likes

Hi, I saw the code here is different with code in: EIP-5006: Rental NFT, NFT User Extension
Which one is the latest?

The code in github is the latest.

This is very interesting. I was wondering how you feel about the main advantage of setting users like in 4907 - which is not necessarily locking the NFT while also “renting” it out - not being present in the github version of this (as it is necessary I presume) would lead to a net increase in gas costs or at least complexity vs locking + handling the rental in a custom rental contract, the latter being a requirement either way for most applications that would make use of the user features (say, to pay out rewards etc).

Standards are likely a better way to go but it just got me wondering as 4907 adds the transferability feature vs a typical lending and locking contract and potentially less costs, but adoption for 5006 with the necessary locking etc may not be as incentivized when the same results can be achieved by a third party contract alongside its distribution or other mechanics,

I would like to hear ideas on how adopting this standard vs 1155 could be pitched as it does add a 1155 wrapping necessity for any typical lending contracts that want to migrate supported 1155 collections to only use 5006 for example, without as many benefits as its 721 counterpart?

Genuinely curious as to whether I missed some key features here. I guess it would be that other apps can read from the nft directly and use it that way? Maybe my point of view is too narrow and focused from a lending protocol perspective. Thanks for reading if you do!