⚠️ This EIP is not recommended for general use or implementation as it is likely to change.

EIP-5006: ERC-1155 Usage Rights Extension Source

Add a user role with restricted permissions to ERC-1155 tokens.

AuthorLance, Anders, Shrug
Discussions-Tohttps://ethereum-magicians.org/t/eip5006-erc-1155-usage-rights-extension/8941
StatusDraft
TypeStandards Track
CategoryERC
Created2022-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 that represent a user of the assets rather than an owner.

Motivation

Like ERC-721, ERC-1155 tokens may have utility of some kind. The people who “use” the token may be different than the people who own it (such as in a rental). Thus, it would be useful to have separate roles for the “owner” and the “user” so that the “user” would not be able to take actions that the owner could (for example, transferring ownership).

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/utils/introspection/IERC165.sol";

interface IERC5006 is IERC165 {
    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;
}

The supportsInterface method MUST return true when called with 0x17fa3123.

Rationale

This model is intended to facilitate easy implementation. Here are some of the problems that are solved by this standard.

Clear Rights Assignment

With Dual “owner” and “user” roles, it becomes significantly easier to manage what lenders and borrowers can and cannot do with the NFT (in other words, their rights).  For example, for the right to transfer ownership,  the project simply needs to check whether the address taking the action represents the owner or the user and prevent the transaction if it’s the user.  Additionally, owners can control who the user is and it’s easy for other projects to assign their own rights to either the owners or the users.

Easy Third-Party Integration

In the spirit of permissionless interoperability, this standard makes it easier for third-party protocols to manage NFT usage rights without permission from the NFT issuer or the NFT application. Once a project has adopted the additional user role and expires, any other project can directly interact with these features and implement their own type of transaction. For example, a PFP NFT using this standard can be integrated into both a rental platform where users can rent the NFT for 30 days AND, at the same time, a mortgage platform where users can use the NFT while eventually buying ownership of the NFT with installment payments. This would all be done without needing the permission of the original PFP project.

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 ERC-1155. This allows developers to easily adopt the standard quickly.

Test Cases

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);
    }

    function getInterface() public pure  returns (bytes4) {
        return  type(IERC5006).interfaceId;
    }
    
}

Test Code

Run in terminal: npm hardhat test

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);
        });

    });
});

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(IERC165, ERC1155) 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.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Lance, Anders, Shrug, "EIP-5006: ERC-1155 Usage Rights Extension [DRAFT]," Ethereum Improvement Proposals, no. 5006, April 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5006.