EIP-4671: Non-tradable Token

,

That’s an interesting idea! Thank you!
I’m just afraid people would consider this as “transferable”, because you can always sign a message for someone else. But I mean, that would almost be the same as giving your keys away, just one time. So I think it’s a good idea! I’ll see think about how to standardise it!

@tserg I’ve linked your implementation in the EIP :wink:

1 Like

Like this idea. It is definitely useful for many use cases. As the main function of the token is to represent a transfer of trust — by minting and giving it to someone else — have you considered the name “Credential Token”?

1 Like

I’ve updated the standard. Here’s how it is now:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/introspection/IERC165.sol";

interface INTT 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 invalidated
    event Invalidated(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 invalidated
    /// @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);
}

Extensions

Metadata

An interface allowing to add metadata linked to each token, as in ERC721.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./INTT.sol";

interface INTTMetadata is INTT {
    /// @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);
}

Enumerable

An interface allowing to enumerate the tokens of an owner, as in ERC721.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./INTT.sol";

interface INTTEnumerable is INTT {
    /// @return Total number of tokens emitted by the contract
    function total() 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);
}

Delegation

An interface allowing delegation rights of token minting.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./INTT.sol";

interface INTTDelegate is INTT {
    /// @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);
}

Consensus

An interface allowing minting/invalidation of tokens based on a consensus of a predefined set of addresses.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./INTT.sol";

interface INTTConsensus is INTT {
    /// @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 invalidate a specific token
    /// @param tokenId Identifier of the token to invalidate
    function approveInvalidate(uint256 tokenId) external;
}

Vitalik himself wrote a post a few days ago talking about “soulbound NFTs”. Essentially NFTs that you cannot trade and that are assigned to you personally. He mentions POAP and Proof Of Humanity, which are great, but these are specific implementations of a more general concept that is Non-Tradable Tokens! I feel really confident about the fact that we need a standard for this now :slight_smile:

4 Likes

I do think NTTs are a major necessity. The list of applications is endless. A new one: mint a specific NTT to all the Genesis accounts. Any account holding the token can now be identified as genesis by smart contracts. Or mint a warning token to known hacker/scammer accounts.

Does the spec include a “mintTo” type function?

Also, could my idea of “NFT Entanglement” be woven into this EIP? (NTT whose ownership is atomically linked to some other transferrable token.)

1 Like

what about a hash function?

(or a more broader getAttr(attribute_name)

interface INTTMetadata is INTT {
...
    /// @return the hash of the NTT
    function hash() external view returns (string memory);

...}
1 Like

That’s a very interesting idea. So, about the hash, I think that would be great, but the tokenURI could really be pointing to anything, including a normal webpage (not necessarily a JSON file, or image, etc.) and so the hash won’t always make sense. Any thoughts on this ?

Regarding getAttr(attribute_name) , that’s interesting I never thought of that. I initially wanted to make a function store() returns (address) that returns the address of another contract that contains metadata in structs. But since solidity doesn’t support generics or inheritance for structs, I could not make a standard interface for the store. I would have loved something like:

interface IMetadataStore {
    function getData(address contract, uint256 tokenId) returns (Data);
    function setData(address contract, uint256 tokenId, Data data);
}

But I can’t make a struct that inherit from Data to make this work for everything.

So your solution with getAttr(attribute_name) could do the work (although it’s not really cool that the method can fail to retrieve data, if the key doesn’t exist for instance). What would be the return type ? String ?

Haha I really like that application ! :smiley:

The standard itself doesn’t really need to describe how to “mint” a token. You can take a look at ERC20, ERC721 for reference. But implementations of the standard will contain that function. Ideally people don’t implement directly the standard but extend from an existing implementation (like the ones of OpenZeppelin). I made an implementation myself: ERC4671/contracts at master · omaraflak/ERC4671 · GitHub

You can check the EIPCreatorBadge to see what you would actually need to do to create a NTT: ERC4671/contracts/EIPCreatorBadge.sol at master · omaraflak/ERC4671 · GitHub

I’ll read your EIP idea and get back to you, I wanted to answer the first part of your message first :smiley:

I think returning a string should cover the majority of use cases.

Of course, there also should be setAttr(string attr_name, string attr_value)

and maybe getAttrs() (if there are no attributes/metadata it return a empty array)

In case someone need to store the hash of the NTT, he can use this funcionality…

Another use case: Identifying and marking synthetic EOAs

Synthetic EOAs are EOAs whose public address has been derived by computing a single TX and without the private key via the method described here (see proffered solution 1).

statechannels.discourse.group/t/reliable-and-safe-generation-of-synthetic-signatures/165`

And it is also possible, if you know the address, digest and value of “c”, for a smart contract to know if an EOA is synthetic (i.e. if it was created with the above method).

statechannels.discourse.group/t/reliable-and-safe-generation-of-synthetic-signatures/165/4

Without EIP-4671, you’d need to run this computation each time you needed to check if the EOA is synthetic. This is relatively gas-expensive and gas-variable because you may need to cycle through a few values of r and run ecrecover multiple times. Not great.

With EIP-4671, a NTT could be created that checks if an account is synthetic, then issues the EOA a token identifying it as such. Now any smart contract wishing to identify synthetic EOAs and coded to assume the presence (or absence) of this NTT can simply check the address in question for the NTT.

Three benefits:

  1. Synthetic-ness can now be checked w/o the digest and “c”
  2. Less gas usage
  3. Predictable gas usage

As I said before, the uses cases for this EIP will be myriad and impactful. IMO, it should be finalized and adopted ASAP.

This is really cool @omaraflak! I’ve been interested in this idea of NTTs and been working on some proposals for it that I just published: https://github.com/ethereum/EIPs/issues/1238#issuecomment-1029055365

As mentioned, I think this EIP is a duplicate of EIP-1238 but let’s combine our efforts? :slightly_smiling_face:

@omaraflak I love this concept, and find it very relevant.

I see instances where it would make sense to make this pull-oriented and push-oriented. Curious if you’re envisioning the implementation of both scenarios?

Also I see this potentially (eventually) tying into Self Sovereign Identity (SSI) and Decentralized ID (DID) use cases. Not sure if this has been considered.

Hi @saginawj, I’m not sure what you mean by pull/push oriented. Could you expand a bit ? :slight_smile:

What about options for revoking when minting, and a corresponding revoke function.

  • function mint(address owner, bool revokable) 
    
  • function mint(address owner, uint256 expiryDate) 
    
  • function revoke(uint256 tokenId)  external

I think there are no situations where the token should not be revokable. A certificate can always be delivered by mistake, and the authority who delivers should be able to revoke it. Although the record should show that you once owned a token from that contract, so it must not “disappear”.

This is why each token has a isValid(tokenId) returns (bool) property. But I didn’t specify how the token should be invalidated (or revoked) because it seems as an implementation detail. It’s more general to have a boolean that states if the token is valid or not. Different implementation could choose how exactly to invalidate the token, e.g. based on an expiry date as you suggested, or anything else.

1 Like

@calvbore I talked to people from Ethereum about this particular subject. I ended up implementing this idea of pulling the token by providing a signed message from the owner :slight_smile:
https://github.com/ethereum/EIPs/pull/4671#discussion_r818979629

2 Likes

Yes sorry- I envision both self-mints and airdrops being relevant use cases. So I’m just curious if there’s an expectation to assign a token that can’t be traded, but maybe can be burned. For example, maybe a professor assigns a NTT to a student. The student can’t sell/trade it, but can burn.

I feel that it depends how you see these NTT in general.

If you want to burn a token it means you’re not happy that it’s been assigned to you publicly. For instance if it’s a criminal record (even though I don’t think this would ever be on the blockchain). There is no good solution:

1- If you allow people to burn tokens, then it doesn’t fit the use-case of the criminal record example or any other where you’re labeled against your will (which is a good thing I think, we don’t want to end up with a social score on the blockchain…).
2- If you don’t allow to burn tokens, you can be assigned tokens from random contracts you might not want to be associated with.

I think the way to see this is that non-tradable tokens are not meant to represent anything too sensitive that you don’t want to expose. They’re only a proof of (good) achievement(s). Something that you’re willing to display publicly. And the importance tied to a NTT should come from the authority that delivered it. If some random guy gives you a jail-token, it shouldn’t matter because that person is nobody. Also, this is why the standard outlines a NTT Store, which is a way for you to expose in one place a subset (possibly all) of our tokens, and share them with an external service.

The Non-Tradable Token standard has been merged as a draft!

I’m planning on changing a few things. Mainly:

  • Currently the NTT Store allows you to make a display/collection of NTTs coming from different contracts but that only belong to one address. I think in practice people will have their tokens delivered to different addresses. So I want to make it possible to build that collection from multiple sources addresses.

  • I want to add a hash of the data associated to tokenURI

  • (to be discussed) I want to add a key/value store to a token contract. The hash could be one of the keys…