Alert Source Discuss
🚧 Stagnant Standards Track: ERC

ERC-4671: 不可交易代币标准

不可交易代币的标准接口,也称为徽章或 Soulbound NFT。

Authors Omar Aflak (@omaraflak), Pol-Malo Le Bris, Marvin Martin (@MarvinMartin24)
Created 2022-01-13
Discussion Link https://ethereum-magicians.org/t/eip-4671-non-tradable-token/7976
Requires EIP-165

摘要

不可交易代币,或 NTT,代表着本质上属于个人的财产(物质或非物质的),例如大学文凭、在线培训证书、政府颁发的文件(国民身份证、驾驶执照、签证、结婚证等)、标签等等。

顾名思义,不可交易代币的目的是不能被交易或转让,它们是“灵魂绑定”的。它们没有货币价值,它们是亲自交付给的,它们只作为拥有/成就的证明

换句话说,拥有一个代币本身就具有很强的意义,这取决于它被交付的原因

动机

我们过去已经看到智能合约被用于颁发大学文凭或驾驶执照,用于食品标签或参加活动等等。所有这些实现方式都不同,但它们有一个共同点:这些代币都是不可交易的

区块链作为一种投机手段已经被使用了太长时间,而不可交易代币希望成为通过区块链提供实用性的一般努力的一部分。

通过为不可交易代币提供一个通用的接口,我们允许开发更多的应用程序,并将区块链技术定位为验证个人财产和成就的标准网关。

规范

不可交易代币

一个 NTT 合约被视为代表一种类型的证书,由一个机构颁发。例如,一个 NTT 合约用于法国国民身份证,另一个用于 Ethereum EIP 创建者,等等…

  • 一个地址可能拥有多个代币。每个代币都有一个唯一的标识符:tokenId
  • 颁发证书的机构应该有权撤销它。可以想想驾驶执照或结婚证。但是,它不能删除你的代币,也就是说,记录会显示你曾经拥有过该合约的一个代币。
  • 第三方最典型的用法是验证用户是否在给定的合约中拥有有效的代币。
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC165.sol";

interface IERC4671 is IERC165 {
    /// Event emitted when a token `tokenId` is minted for `owner`
    event Minted(address owner, uint256 tokenId);

    /// Event emitted when token `tokenId` of `owner` is revoked
    event Revoked(address owner, uint256 tokenId);

    /// @notice Count all tokens assigned to an owner
    /// @param owner Address for whom to query the balance
    /// @return Number of tokens owned by `owner`
    function balanceOf(address owner) external view returns (uint256);

    /// @notice Get owner of a token
    /// @param tokenId Identifier of the token
    /// @return Address of the owner of `tokenId`
    function ownerOf(uint256 tokenId) external view returns (address);

    /// @notice Check if a token hasn't been revoked
    /// @param tokenId Identifier of the token
    /// @return True if the token is valid, false otherwise
    function isValid(uint256 tokenId) external view returns (bool);

    /// @notice Check if an address owns a valid token in the contract
    /// @param owner Address for whom to check the ownership
    /// @return True if `owner` has a valid token, false otherwise
    function hasValid(address owner) external view returns (bool);
}

扩展

元数据

一个允许添加与每个代币相关联的元数据的接口。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC4671.sol";

interface IERC4671Metadata is IERC4671 {
    /// @return Descriptive name of the tokens in this contract
    function name() external view returns (string memory);

    /// @return An abbreviated name of the tokens in this contract
    function symbol() external view returns (string memory);

    /// @notice URI to query to get the token's metadata
    /// @param tokenId Identifier of the token
    /// @return URI for the token
    function tokenURI(uint256 tokenId) external view returns (string memory);
}
枚举

一个允许枚举所有者的代币的接口。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC4671.sol";

interface IERC4671Enumerable is IERC4671 {
    /// @return emittedCount Number of tokens emitted
    function emittedCount() external view returns (uint256);

    /// @return holdersCount Number of token holders  
    function holdersCount() external view returns (uint256);

    /// @notice Get the tokenId of a token using its position in the owner's list
    /// @param owner Address for whom to get the token
    /// @param index Index of the token
    /// @return tokenId of the token
    function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256);

    /// @notice Get a tokenId by it's index, where 0 <= index < total()
    /// @param index Index of the token
    /// @return tokenId of the token
    function tokenByIndex(uint256 index) external view returns (uint256);
}
委托

一个允许代币铸造的委托权限的接口。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC4671.sol";

interface IERC4671Delegate is IERC4671 {
    /// @notice Grant one-time minting right to `operator` for `owner`
    /// An allowed operator can call the function to transfer rights.
    /// @param operator Address allowed to mint a token
    /// @param owner Address for whom `operator` is allowed to mint a token
    function delegate(address operator, address owner) external;

    /// @notice Grant one-time minting right to a list of `operators` for a corresponding list of `owners`
    /// An allowed operator can call the function to transfer rights.
    /// @param operators Addresses allowed to mint
    /// @param owners Addresses for whom `operators` are allowed to mint a token
    function delegateBatch(address[] memory operators, address[] memory owners) external;

    /// @notice Mint a token. Caller must have the right to mint for the owner.
    /// @param owner Address for whom the token is minted
    function mint(address owner) external;

    /// @notice Mint tokens to multiple addresses. Caller must have the right to mint for all owners.
    /// @param owners Addresses for whom the tokens are minted
    function mintBatch(address[] memory owners) external;

    /// @notice Get the issuer of a token
    /// @param tokenId Identifier of the token
    /// @return Address who minted `tokenId`
    function issuerOf(uint256 tokenId) external view returns (address);
}
共识

一个允许基于预定义地址集的共识来铸造/撤销代币的接口。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC4671.sol";

interface IERC4671Consensus is IERC4671 {
    /// @notice Get voters addresses for this consensus contract
    /// @return Addresses of the voters
    function voters() external view returns (address[] memory);

    /// @notice Cast a vote to mint a token for a specific address
    /// @param owner Address for whom to mint the token
    function approveMint(address owner) external;

    /// @notice Cast a vote to revoke a specific token
    /// @param tokenId Identifier of the token to revoke
    function approveRevoke(uint256 tokenId) external;
}
拉取

一个允许代币所有者将其代币拉取到另一个他的钱包(这里是 recipient)的接口。调用者必须使用 owner 钱包提供元组 (tokenId, owner, recipient) 的签名。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC4671.sol";

interface IERC4671Pull is IERC4671 {
    /// @notice Pull a token from the owner wallet to the caller's wallet
    /// @param tokenId Identifier of the token to transfer
    /// @param owner Address that owns tokenId
    /// @param signature Signed data (tokenId, owner, recipient) by the owner of the token
    function pull(uint256 tokenId, address owner, bytes memory signature) external;
}

NTT 商店

不可交易代币旨在被第三方获取,这就是为什么需要一种方便的方式让用户公开他们的一些或所有代币。我们使用一个必须实现以下接口的商店来实现此结果。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC165.sol";

interface IERC4671Store is IERC165 {
    // Event emitted when a IERC4671Enumerable contract is added to the owner's records
    event Added(address owner, address token);

    // Event emitted when a IERC4671Enumerable contract is removed from the owner's records
    event Removed(address owner, address token);

    /// @notice Add a IERC4671Enumerable contract address to the caller's record
    /// @param token Address of the IERC4671Enumerable contract to add
    function add(address token) external;

    /// @notice Remove a IERC4671Enumerable contract from the caller's record
    /// @param token Address of the IERC4671Enumerable contract to remove
    function remove(address token) external;

    /// @notice Get all the IERC4671Enumerable contracts for a given owner
    /// @param owner Address for which to retrieve the IERC4671Enumerable contracts
    function get(address owner) external view returns (address[] memory);
}

理由

链上 vs 链下

做出将数据保存在链下的决定 (通过 tokenURI()),主要有两个原因:

  • 不可交易代币代表个人财产。因此,在某些情况下,数据可能需要加密。该标准不应概述有关加密的决策,因为有很多方法可以做到这一点,并且每种可能性都特定于用例。
  • 不可交易代币必须保持通用性。本来有可能创建一个 MetadataStore 以优雅的方式保存代币的数据,但不幸的是,我们需要 solidity 中的泛型支持(或结构体继承),而这在今天是不适用的。

参考实现

你可以在 ../assets/eip-4671 中找到此标准的实现。

使用此实现,以下是如何创建一个代币:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./ERC4671.sol";

contract EIPCreatorBadge is ERC4671 {
    constructor() ERC4671("EIP Creator Badge", "EIP") {}

    function giveThatManABadge(address owner) external {
        require(_isCreator(), "You must be the contract creator");
        _mint(owner);
    }

    function _baseURI() internal pure override returns (string memory) {
        return "https://eips.ethereum.org/ntt/";
    }
}

这可能是一个由 Ethereum 基金会管理的合约,它允许他们向 EIP 创建者交付代币。

安全注意事项

一个安全方面与 tokenURI 方法有关,该方法返回链接到代币的元数据。由于该标准代表着本质上属于个人的财产,因此用户可能希望在某些情况下对数据进行加密,例如国民身份证。此外,合约创建者有责任确保此方法返回的 URI 始终可用。

该标准没有定义任何将代币从一个钱包转移到另一个钱包的方式。因此,用户必须非常谨慎地使用他们用来接收这些代币的钱包。如果钱包丢失,取回代币的唯一方法是让发行机构再次交付代币,类似于现实生活。

版权

CC0 下放弃版权和相关权利。

Citation

Please cite this document as:

Omar Aflak (@omaraflak), Pol-Malo Le Bris, Marvin Martin (@MarvinMartin24), "ERC-4671: 不可交易代币标准 [DRAFT]," Ethereum Improvement Proposals, no. 4671, January 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4671.