Alert Source Discuss
Standards Track: ERC

ERC-6147: Guard of NFT/SBT, an Extension of ERC-721

A new management role with an expiration date of NFT/SBT is defined, achieving the separation of transfer right and holding right.

Authors 5660-eth (@5660-eth), Wizard Wang
Created 2022-12-07
Requires EIP-165, EIP-721

Abstract

This standard is an extension of ERC-721. It separates the holding right and transfer right of non-fungible tokens (NFTs) and Soulbound Tokens (SBTs) and defines a new role, guard with expires. The flexibility of the guard setting enables the design of NFT anti-theft, NFT lending, NFT leasing, SBT, etc.

Motivation

NFTs are assets that possess both use and financial value.

Many cases of NFT theft currently exist, and current NFT anti-theft schemes, such as transferring NFTs to cold wallets, make NFTs inconvenient to be used.

In current NFT lending, the NFT owner needs to transfer the NFT to the NFT lending contract, and the NFT owner no longer has the right to use the NFT while he has obtained the loan. In the real world, for example, if a person takes out a mortgage on his own house, he still has the right to use that house.

For SBT, the current mainstream view is that an SBT is not transferable, which makes an SBT bound to an Ether address. However, when the private key of the user address is leaked or lost, retrieving SBT will become a complicated task and there is no corresponding standard. The SBTs essentially realizes the separation of NFT holding right and transfer right. When the wallet where SBT is located is stolen or unavailable, SBT should be able to be recoverable.

In addition, SBTs still need to be managed in use. For example, if a university issues diploma-based SBTs to its graduates, and if the university later finds that a graduate has committed academic misconduct or jeopardized the reputation of the university, it should have the ability to retrieve the diploma-based SBTs.

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.

ERC-721 compliant contracts MAY implement this EIP.

A guard Must be valid only before expires.

When a token has no guard or the guard is expired, guardInfo MUST return (address(0), 0).

When a token has no guard or the guard is expired, owner, authorised operators and approved address of the token MUST have permission to set guard and expires.

When a token has a valid guard, owner, authorised operators and approved address of the token MUST NOT be able to change guard and expires, and they MUST NOT be able to transfer the token.

When a token has a valid guard, guardInfo MUST return the address and expires of the guard.

When a token has a valid guard, the guard MUST be able to remove guard and expires, change guard and expires, and transfer the token.

When a token has a valid guard, if the token burns, the guard MUST be deleted.

If issuing or minting SBTs, the guard MAY be uniformly set to the designated address to facilitate management.

Contract Interface

 interface IERC6147 {

    /// Logged when the guard of an NFT is changed or expires is changed
    /// @notice Emitted when the `guard` is changed or the `expires` is changed
    ///         The zero address for `newGuard` indicates that there currently is no guard address
    event UpdateGuardLog(uint256 indexed tokenId, address indexed newGuard, address oldGuard, uint64 expires);
    
    /// @notice Owner, authorised operators and approved address of the NFT can set guard and expires of the NFT and
    ///         valid guard can modifiy guard and expires of the NFT
    ///         If the NFT has a valid guard role, the owner, authorised operators and approved address of the NFT
    ///         cannot modify guard and expires
    /// @dev The `newGuard` can not be zero address
    ///      The `expires` need to be valid
    ///      Throws if `tokenId` is not valid NFT
    /// @param tokenId The NFT to get the guard address for
    /// @param newGuard The new guard address of the NFT
    /// @param expires UNIX timestamp, the guard could manage the token before expires
    function changeGuard(uint256 tokenId, address newGuard, uint64 expires) external;

    /// @notice Remove the guard and expires of the NFT
    ///         Only guard can remove its own guard role and expires
    /// @dev The guard address is set to 0 address
    ///      The expires is set to 0
    ///      Throws if `tokenId` is not valid NFT
    /// @param tokenId The NFT to remove the guard and expires for
    function removeGuard(uint256 tokenId) external;
    
    /// @notice Transfer the NFT and remove its guard and expires
    /// @dev The NFT is transferred to `to` and the guard address is set to 0 address
    ///      Throws if `tokenId` is not valid NFT
    /// @param from The address of the previous owner of the NFT
    /// @param to The address of NFT recipient 
    /// @param tokenId The NFT to get transferred for
    function transferAndRemove(address from, address to, uint256 tokenId) external;

    /// @notice Get the guard address and expires of the NFT
    /// @dev The zero address indicates that there is no guard
    /// @param tokenId The NFT to get the guard address and expires for
    /// @return The guard address and expires for the NFT
   function guardInfo(uint256 tokenId) external view returns (address, uint64);   
}

The changeGuard(uint256 tokenId, address newGuard, uint64 expires) function MAY be implemented as public or external.

The removeGuard(uint256 tokenId) function MAY be implemented as public or external.

The transferAndRemove(address from,address to,uint256 tokenId) function MAY be implemented as public or external.

The guardInfo(uint256 tokenId) function MAY be implemented as pure or view.

The UpdateGuardLog event MUST be emitted when a guard is changed.

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

Rationale

Universality

There are many application scenarios for NFT/SBT, and there is no need to propose a dedicated EIP for each one, which would make the overall number of EIPS inevitably increase and add to the burden of developers. The standard is based on the analysis of the right attached to assets in the real world, and abstracts the right attached to NFT/SBT into holding right and transfer right making the standard more universal.

For example, the standard has more than the following use cases:

SBTs. The SBTs issuer can assign a uniform role of guard to the SBTs before they are minted, so that the SBTs cannot be transferred by the corresponding holders and can be managed by the SBTs issuer through the guard.

NFT anti-theft. If an NFT holder sets a guard address of an NFT as his or her own cold wallet address, the NFT can still be used by the NFT holder, but the risk of theft is greatly reduced.

NFT lending. The borrower sets the guard of his or her own NFT as the lender’s address, the borrower still has the right to use the NFT while obtaining the loan, but at the same time cannot transfer or sell the NFT. If the borrower defaults on the loan, the lender can transfer and sell the NFT.

Additionally, by setting an expires for the guard, the scalability of the protocol is further enhanced, as demonstrated in the following examples:

More flexible NFT issuance. During NFT minting, discounts can be offered for NFTs that are locked for a certain period of time, without affecting the NFTs’ usability.

More secure NFT management. Even if the guard address becomes inaccessible due to lost private keys, the owner can still retrieve the NFT after the guard has expired.

Valid SBTs. Some SBTs have a period of use. More effective management can be achieved through guard and expires.

Extensibility

This standard only defines a guard and its expires. For complex functions needed by NFTs and SBTs, such as social recovery and multi-signature, the guard can be set as a third-party protocol address. Through the third-party protocol, more flexible and diverse functions can be achieved based on specific application scenarios.

Naming

The alternative names are guardian and guard, both of which basically match the permissions corresponding to the role: protection of NFT or necessary management according to its application scenarios. The guard has fewer characters than the guardian and is more concise.

Backwards Compatibility

This standard can be fully ERC-721 compatible by adding an extension function set.

If an NFT issued based on the above standard does not set a guard, then it is no different in the existing functions from the current NFT issued based on the ERC-721 standard.

Reference Implementation


// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.8;

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

abstract contract ERC6147 is ERC721, IERC6147 {

    /// @dev A structure representing a token of guard address and expires
    /// @param guard address of guard role
    /// @param expirs UNIX timestamp, the guard could manage the token before expires
    struct GuardInfo{
        address guard;
        uint64 expires;
    }
    
    mapping(uint256 => GuardInfo) internal _guardInfo;

    /// @notice Owner, authorised operators and approved address of the NFT can set guard and expires of the NFT and
    ///         valid guard can modifiy guard and expires of the NFT
    ///         If the NFT has a valid guard role, the owner, authorised operators and approved address of the NFT
    ///         cannot modify guard and expires
    /// @dev The `newGuard` can not be zero address
    ///      The `expires` need to be valid
    ///      Throws if `tokenId` is not valid NFT
    /// @param tokenId The NFT to get the guard address for
    /// @param newGuard The new guard address of the NFT
    /// @param expires UNIX timestamp, the guard could manage the token before expires
    function changeGuard(uint256 tokenId, address newGuard, uint64 expires) public virtual{
        require(expires > block.timestamp, "ERC6147: invalid expires");
        _updateGuard(tokenId, newGuard, expires, false);
    }

    /// @notice Remove the guard and expires of the NFT
    ///         Only guard can remove its own guard role and expires
    /// @dev The guard address is set to 0 address
    ///      The expires is set to 0
    ///      Throws if `tokenId` is not valid NFT
    /// @param tokenId The NFT to remove the guard and expires for
    function removeGuard(uint256 tokenId) public virtual  {
        _updateGuard(tokenId, address(0), 0, true);
    }
    
    /// @notice Transfer the NFT and remove its guard and expires
    /// @dev The NFT is transferred to `to` and the guard address is set to 0 address
    ///      Throws if `tokenId` is not valid NFT
    /// @param from The address of the previous owner of the NFT
    /// @param to The address of NFT recipient 
    /// @param tokenId The NFT to get transferred for
    function transferAndRemove(address from, address to, uint256 tokenId) public virtual {
        safeTransferFrom(from, to, tokenId);
        removeGuard(tokenId);
    }
    
    /// @notice Get the guard address and expires of the NFT
    /// @dev The zero address indicates that there is no guard
    /// @param tokenId The NFT to get the guard address and expires for
    /// @return The guard address and expires for the NFT
    function guardInfo(uint256 tokenId) public view virtual returns (address, uint64) {
        if(_guardInfo[tokenId].expires >= block.timestamp){
            return (_guardInfo[tokenId].guard, _guardInfo[tokenId].expires);
        }
        else{
            return (address(0), 0);
        }
    }

    /// @notice Update the guard of the NFT
    /// @dev Delete function: set guard to 0 address and set expires to 0; 
    ///      and update function: set guard to new address and set expires
    ///      Throws if `tokenId` is not valid NFT
    /// @param tokenId The NFT to update the guard address for
    /// @param newGuard The newGuard address
    /// @param expires UNIX timestamp, the guard could manage the token before expires
    /// @param allowNull Allow 0 address
    function _updateGuard(uint256 tokenId, address newGuard, uint64 expires, bool allowNull) internal {
        (address guard,) = guardInfo(tokenId);
        if (!allowNull) {
            require(newGuard != address(0), "ERC6147: new guard can not be null");
        }
        if (guard != address(0)) { 
            require(guard == _msgSender(), "ERC6147: only guard can change it self"); 
        } else { 
            require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC6147: caller is not owner nor approved");
        } 

        if (guard != address(0) || newGuard != address(0)) {
            _guardInfo[tokenId] = GuardInfo(newGuard,expires);
            emit UpdateGuardLog(tokenId, newGuard, guard, expires);
        }
    }
    
    /// @notice Check the guard address
    /// @dev The zero address indicates there is no guard
    /// @param tokenId The NFT to check the guard address for
    /// @return The guard address
    function _checkGuard(uint256 tokenId) internal view returns (address) {
        (address guard, ) = guardInfo(tokenId);
        address sender = _msgSender();
        if (guard != address(0)) {
            require(guard == sender, "ERC6147: sender is not guard of the token");
            return guard;
        }else{
            return address(0);
        }
    }
 
    /// @dev Before transferring the NFT, need to check the gurard address
    function transferFrom(address from, address to, uint256 tokenId) public virtual override {
        address guard;
        address new_from = from;
        if (from != address(0)) {
            guard = _checkGuard(tokenId);
            new_from = ownerOf(tokenId);
        }
        if (guard == address(0)) {
            require(
                _isApprovedOrOwner(_msgSender(), tokenId),
                "ERC721: transfer caller is not owner nor approved"
            );
        }
        _transfer(new_from, to, tokenId);
    }

    /// @dev Before safe transferring the NFT, need to check the gurard address
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public virtual override {
        address guard;
        address new_from = from;
        if (from != address(0)) {
            guard = _checkGuard(tokenId);
            new_from = ownerOf(tokenId);
        }
        if (guard == address(0)) {
            require(
                _isApprovedOrOwner(_msgSender(), tokenId),
                "ERC721: transfer caller is not owner nor approved"
            );
        }
        _safeTransfer(from, to, tokenId, _data);
    }

    /// @dev When burning, delete `token_guard_map[tokenId]`
    /// This is an internal function that does not check if the sender is authorized to operate on the token.
    function _burn(uint256 tokenId) internal virtual override {
        (address guard, )=guardInfo(tokenId);
        super._burn(tokenId);
        delete _guardInfo[tokenId];
        emit UpdateGuardLog(tokenId, address(0), guard, 0);
    }

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

Security Considerations

Make sure to set an appropriate expires for the guard, based on the specific application scenario.

When an NFT has a valid guard, even if an address is authorized as an operator through approve or setApprovalForAll, the operator still has no right to transfer the NFT.

When an NFT has a valid guard, the owner cannot sell the NFT. Some trading platforms list NFTs through setApprovalForAll and owners’ signature. It is recommended to prevent listing these NFTs by checking guardInfo.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

5660-eth (@5660-eth), Wizard Wang, "ERC-6147: Guard of NFT/SBT, an Extension of ERC-721," Ethereum Improvement Proposals, no. 6147, December 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6147.