HomeEIPs
EIPsERC-7303
ERC-7303

Token-Controlled Token Circulation

Access control scheme based on token ownership.
DraftStandards Track: ERC
Created: 2023-07-09
Requires: EIP-721, EIP-1155, EIP-5679
Ko Fujimura (@kofujimura)
DiscussionsOriginal linkEdit
1 min read

Video
Anyone may contribute to propose contents.
Go propose
Original

Abstract

This ERC introduces an access control scheme termed Token-Controlled Token Circulation (TCTC). By representing the privileges associated with a role as an ERC-721 or ERC-1155 token (referred to as a control token), the processes of granting or revoking a role can be facilitated through the minting or burning of the corresponding control token.

Motivation

There are numerous methods to implement access control for privileged actions. A commonly utilized pattern is "role-based" access control as specified in ERC-5982. This method, however, necessitates the use of an off-chain management tool to grant or revoke required roles through its interface. Additionally, as many wallets lack a user interface that displays the privileges granted by a role, users are often unable to comprehend the status of their privileges through the wallet.

Use Cases

This ERC is applicable in many scenarios where role-based access control as described in ERC-5982 is used. Specific use cases include:

Mint/Burn Permission: In applications that circulate items such as tickets, coupons, membership cards, and site access rights as tokens, it is necessary to provide the system administrator with the authority to mint or burn these tokens. These permissions can be realized as control tokens in this scheme.

Transfer Permission: In some situations within these applications, it may be desirable to limit the ability to transfer tokens to specific agencies. In these cases, an agency certificate is issued as a control token. The ownership of this control token then provides the means to regulate token transfers.

Address Verification: Many applications require address verification to prevent errors in the recipient's address when minting or transferring target tokens. A control token is issued as proof of address verification to users, which is required by the recipient when a mint or transfer transaction is executed, thus preventing misdeliveries. In some instances, this control token for address verification may be issued by a government agency or specific company after an identity verification process.

Specification

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

  1. Smart contracts implementing the ERC-7303 standard MUST represent the privilege required by the role as an ERC-721 token or ERC-1155 token. The tokens that represent privileges are called control tokens in this ERC. The control token can be any type of token, and its transactions may be recursively controlled by another control token.
  2. To associate the required control token with the role, the address of the previously deployed contract for the control token MUST be used.
  3. To ascertain whether an account possesses the necessary role, it SHOULD be confirmed that the balance of the control token exceeds 0, utilizing the balanceOf method defined in ERC-721 or ERC-1155. Note that the typeId must be specified if an ERC-1155 token is used for the balanceOf method.
  4. To grant a role to an account, a control token representing the privilege SHOULD be minted to the account using safeMint method defined in ERC-5679.
  5. To revoke a role from an account, the control token representing the privilege SHOULD be burned using the burn method defined in ERC-5679.
  6. A role in a compliant smart contract is represented in the format of bytes32. It's RECOMMENDED the value of such role is computed as a keccak256 hash of a string of the role name, in this format: bytes32 role = keccak256("<role_name>") such as bytes32 role = keccak256("MINTER").

Rationale

The choice to utilize ERC-721 or ERC-1155 token as the control token for privileges enhances visibility of such privileges within wallets, thus simplifying privilege management for users.

Generally, when realizing privileges as tokens, specifications like Soulbound Token (e.g., ERC-5192) are used. Given that ERC-5192 inherits from ERC-721, this ERC has choiced ERC-721 as the requirement for the control token.

Employing a transferable control token can cater to scenarios where role delegation is necessary. For example, when an authority within an organization is replaced or on vacation, the ability to transfer their privileges to another member becomes possible. The decision to designate the control token as transferable will depend on the specific needs of the application.

Backwards Compatibility

This ERC is designed to be compatible for ERC-721, ERC-1155, and ERC-5679 respectively.

Reference Implementation

ERC-7303 provides a modifier to facilitate the implementation of TCTC access control in applications. This modifier checks if an account possesses the necessary role. ERC-7303 also includes a function that grants a specific role to a designated account.

// SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.9; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; abstract contract ERC7303 { struct ERC721Token { address contractId; } struct ERC1155Token { address contractId; uint256 typeId; } mapping (bytes32 => ERC721Token[]) private _ERC721_Contracts; mapping (bytes32 => ERC1155Token[]) private _ERC1155_Contracts; modifier onlyHasToken(bytes32 role, address account) { require(_checkHasToken(role, account), "ERC7303: not has a required token"); _; } /** * @notice Grant a role to user who owns a control token specified by the ERC-721 contractId. * Multiple calls are allowed, in this case the user must own at least one of the specified token. * @param role byte32 The role which you want to grant. * @param contractId address The address of contractId of which token the user required to own. */ function _grantRoleByERC721(bytes32 role, address contractId) internal { require( IERC165(contractId).supportsInterface(type(IERC721).interfaceId), "ERC7303: provided contract does not support ERC721 interface" ); _ERC721_Contracts[role].push(ERC721Token(contractId)); } /** * @notice Grant a role to user who owns a control token specified by the ERC-1155 contractId. * Multiple calls are allowed, in this case the user must own at least one of the specified token. * @param role byte32 The role which you want to grant. * @param contractId address The address of contractId of which token the user required to own. * @param typeId uint256 The token type id that the user required to own. */ function _grantRoleByERC1155(bytes32 role, address contractId, uint256 typeId) internal { require( IERC165(contractId).supportsInterface(type(IERC1155).interfaceId), "ERC7303: provided contract does not support ERC1155 interface" ); _ERC1155_Contracts[role].push(ERC1155Token(contractId, typeId)); } function _checkHasToken(bytes32 role, address account) internal view returns (bool) { ERC721Token[] memory ERC721Tokens = _ERC721_Contracts[role]; for (uint i = 0; i < ERC721Tokens.length; i++) { if (IERC721(ERC721Tokens[i].contractId).balanceOf(account) > 0) return true; } ERC1155Token[] memory ERC1155Tokens = _ERC1155_Contracts[role]; for (uint i = 0; i < ERC1155Tokens.length; i++) { if (IERC1155(ERC1155Tokens[i].contractId).balanceOf(account, ERC1155Tokens[i].typeId) > 0) return true; } return false; } }

The following is a simple example of utilizing ERC7303 within an ERC-721 token to define "minter" and "burner" roles. Accounts possessing these roles are allowed to create new tokens and destroy existing tokens, facilitated by specifying ERC-721 or ERC-1155 control tokens:

// SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.9; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import "./ERC7303.sol"; contract MyToken is ERC721, ERC7303 { bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); constructor() ERC721("MyToken", "MTK") { // Specifies the deployed contractId of ERC721 control token. _grantRoleByERC721(MINTER_ROLE, 0x...); _grantRoleByERC721(BURNER_ROLE, 0x...); // Specifies the deployed contractId and typeId of ERC1155 control token. _grantRoleByERC1155(MINTER_ROLE, 0x..., ...); _grantRoleByERC1155(BURNER_ROLE, 0x..., ...); } function safeMint(address to, uint256 tokenId, string memory uri) public onlyHasToken(MINTER_ROLE, msg.sender) { _safeMint(to, tokenId); } function burn(uint256 tokenId) public onlyHasToken(BURNER_ROLE, msg.sender) { _burn(tokenId); } }

Security Considerations

Needs discussion.

Copyright and related rights waived via CC0.

Further reading
Anyone may contribute to propose contents.
Go propose
Adopted by projects
Anyone may contribute to propose contents.
Go propose

Not miss a beat of EIPs' update?

Subscribe EIPs Fun to receive the latest updates of EIPs Good for Buidlers to follow up.

View all
Serve EIP builders, scale Ethereum.
Resources
GitHub
Supported by