EIP-5006: ERC-1155 Usage Rights Extension
Add a user role with restricted permissions to ERC-1155 tokens.
Author | Lance, Anders, Shrug |
---|---|
Discussions-To | https://ethereum-magicians.org/t/eip5006-erc-1155-usage-rights-extension/8941 |
Status | Draft |
Type | Standards Track |
Category | ERC |
Created | 2022-04-12 |
Requires | 165, 1155 |
Table of Contents
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
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.