Alert Source Discuss
Standards Track: ERC

ERC-6147: NFT/SBT 的 Guard,ERC-721 的扩展

定义了一个具有 NFT/SBT 过期日期的新管理角色,实现了转移权和持有权的分离。

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

摘要

本标准是 ERC-721 的扩展。它分离了非同质化代币 (NFT) 和 Soulbound 代币 (SBT) 的持有权和转移权,并定义了一个新的角色 guard,具有 expires 属性。guard 设置的灵活性使得 NFT 防盗、NFT 借贷、NFT 租赁、SBT 等的设计成为可能。

动机

NFT 是一种兼具使用价值和金融价值的资产。

目前存在许多 NFT 被盗的案例,而目前的 NFT 防盗方案,例如将 NFT 转移到冷钱包,使得 NFT 的使用不方便。

在目前的 NFT 借贷中,NFT 所有者需要将 NFT 转移到 NFT 借贷合约,NFT 所有者在获得贷款的同时不再拥有 NFT 的使用权。在现实世界中,例如,如果一个人用自己的房子抵押贷款,他仍然有权使用那栋房子。

对于 SBT,目前的主流观点是 SBT 不可转移,这使得 SBT 与以太坊地址绑定。但是,当用户地址的私钥泄露或丢失时,检索 SBT 将成为一项复杂的任务,并且没有相应的标准。SBT 本质上实现了 NFT 持有权和转移权的分离。当 SBT 所在的钱包被盗或不可用时,SBT 应该能够被恢复。

此外,SBT 在使用中仍然需要管理。例如,如果一所大学向其毕业生颁发基于文凭的 SBT,并且如果该大学后来发现一名毕业生犯有学术不端行为或损害了该大学的声誉,它应该有能力检索基于文凭的 SBT。

规范

本文档中的关键词 “MUST”、”MUST NOT”、”REQUIRED”、”SHALL”、”SHALL NOT”、”SHOULD”、”SHOULD NOT”、”RECOMMENDED”、”MAY” 和 “OPTIONAL” 按照 RFC 2119 中的描述进行解释。

符合 ERC-721 的合约可以实现此 EIP。

guard 必须仅在 expires 之前有效。

当一个代币没有 guardguard 已过期时,guardInfo 必须返回 (address(0), 0)

当一个代币没有 guardguard 已过期时,代币的所有者、授权运营商和批准的地址必须有权设置 guardexpires

当一个代币有一个有效的 guard 时,代币的所有者、授权运营商和批准的地址不能更改 guardexpires,并且他们不能转移代币。

当一个代币有一个有效的 guard 时,guardInfo 必须返回 guard 的地址和 expires

当一个代币有一个有效的 guard 时,guard 必须能够移除 guardexpires,更改 guardexpires,以及转移代币。

当一个代币有一个有效的 guard 时,如果该代币被销毁,则必须删除 guard

如果发行或铸造 SBT,guard 可以统一设置为指定地址,以便于管理。

合约接口

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

changeGuard(uint256 tokenId, address newGuard, uint64 expires) 函数可以实现为 publicexternal

removeGuard(uint256 tokenId) 函数可以实现为 publicexternal

transferAndRemove(address from,address to,uint256 tokenId) 函数可以实现为 publicexternal

guardInfo(uint256 tokenId) 函数可以实现为 pureview

guard 发生变化时,必须发出 UpdateGuardLog 事件。

当使用 0xb61d1057 调用时,supportsInterface 方法必须返回 true

理由

普遍性

NFT/SBT 有许多应用场景,没有必要为每一个应用场景都提出一个专门的 EIP,这将不可避免地增加 EIP 的总体数量,并增加开发人员的负担。该标准基于对现实世界中附加于资产的权利的分析,并将附加于 NFT/SBT 的权利抽象为持有权和转移权,从而使该标准更具普遍性。

例如,该标准具有以下多种用途:

SBT。SBT 的发行者可以在 SBT 被铸造之前为 SBT 分配一个统一的 guard 角色,以便 SBT 不能被相应的持有者转移,并且可以通过 guard 由 SBT 的发行者进行管理。

NFT 防盗。如果 NFT 持有者将 NFT 的 guard 地址设置为他或她自己的冷钱包地址,则 NFT 仍然可以被 NFT 持有者使用,但被盗的风险大大降低。

NFT 借贷。借款人将其自己的 NFT 的 guard 设置为贷款人的地址,借款人在获得贷款的同时仍然有权使用该 NFT,但同时不能转移或出售该 NFT。如果借款人拖欠贷款,贷款人可以转移和出售该 NFT。

此外,通过为 guard 设置 expires,协议的可扩展性得到了进一步增强,如下面的例子所示:

更灵活的 NFT 发行。在 NFT 铸造期间,可以为锁定一段时间的 NFT 提供折扣,而不会影响 NFT 的可用性。

更安全的 NFT 管理。即使 guard 地址由于私钥丢失而无法访问,owner 仍然可以在 guard 过期后检索 NFT。

有效的 SBT。有些 SBT 有使用期限。通过 guardexpires 可以实现更有效的管理。

可扩展性

本标准仅定义了一个 guard 及其 expires。对于 NFT 和 SBT 所需的复杂功能,例如社交恢复和多重签名,可以将 guard 设置为第三方协议地址。通过第三方协议,可以基于特定的应用场景实现更灵活和多样化的功能。

命名

备选名称为 guardianguard,两者基本上都与其角色对应的权限相匹配:保护 NFT 或根据其应用场景进行必要的管理。guardguardian 字符更少,也更简洁。

向后兼容性

通过添加一个扩展函数集,本标准可以完全与 ERC-721 兼容。

如果基于上述标准发行的 NFT 没有设置 guard,那么它在现有功能上与当前基于 ERC-721 标准发行的 NFT 没有什么不同。

参考实现


// 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]`
    /// 销毁时,删除`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);
    }
}

安全注意事项

根据具体的应用场景,确保为 guard 设置适当的 expires

当一个 NFT 有一个有效的 guard 时,即使通过 approvesetApprovalForAll 授权一个地址作为运营商,该运营商仍然没有转移该 NFT 的权利。

当一个 NFT 有一个有效的 guard 时,owner 不能出售该 NFT。一些交易平台通过 setApprovalForAll 和所有者的签名来列出 NFT。建议通过检查 guardInfo 来防止列出这些 NFT。

版权

版权和相关权利通过 CC0 放弃。

Citation

Please cite this document as:

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