EIP-4671: Non-tradable Token

Discussion thread for: EIP-4671: Non-tradable Token by OmarAflak · Pull Request #4671 · ethereum/EIPs · GitHub

Abstract

NTTs represent inherently personal possessions (material or immaterial), such as university diplomas, online training certificates, government issued documents (national id, driving licence, visa, wedding, etc.), badges, labels, and so on.

As the name implies, NTTs are not made to be traded or sold. They don’t have monetary value. They only serve as a proof of possession.

Motivation

US, 2017, MIT published 111 diplomas on a blockchain. France, 2018, Carrefour multinational retail corporation used blockchain technology to certify the provenance of its chickens. South Korea, 2019, the state published 1 million driving licences on a blockchain-powered platform.

Each of them made their own smart contracts, with different implementations. We think diplomas, food labels, or driving licences are just a subset of a more general type of tokens: non-tradable tokens. Tokens that represent certificates or labels that were granted to you by some authority.

By providing a common interface for this type of tokens, we allow more applications to be developed and we position blockchain technology as a standard gateway for verification of personal possessions.

Specification

A single NTT contract, is seen as representing one type of badge by one authority. For instance, one NTT contract for MIT diplomas, one NTT contract for the state driving licences, and so on…

  • An address might possess multiple tokens, which are indexed.
  • An authority who delivers a certificate should be in position to invalidate it. Think of driving licences or weddings. However, it cannot delete your token.
  • The issuer of a token might be someone else than the contract creator.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

interface INTT is IERC165 {
    /// @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 Check if a token hasn't been invalidated
    /// @param owner Address for whom to check the token validity
    /// @param index Index of the token
    /// @return True if the token is valid, False otherwise
    function isValid(address owner, uint256 index) external view returns (bool);

    /// @notice Get the issuer of a token
    /// @param owner Address for whom to check the token issuer
    /// @param owner Index of the token
    /// @return Address of the issuer
    function issuerOf(address owner, uint256 index) external view returns (address);
}

Extensions

Metadata

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

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface INTTMetadata {
    /// @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 owner Address of the token's owner
    /// @param index Index of the token
    /// @return URI for the token
    function tokenURI(address owner, uint256 index) external view returns (string memory);
}

Delegation

An interface to standardize delegation rights of token minting.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

Implementation

The implementation is a bit long. You’ll find it in the PR.

NTT for EIP ?

As a first NTT, why not create the EIP Creator Badge ? An NTT created by the Ethereum foundation, and attributed to EIP-standard creators ? :slightly_smiling_face:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./NTT.sol";

contract EIPCreatorBadge is NTT {
    constructor() NTT("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/";
    }
}

Any thoughts or comments are greatly appreciated!

1 Like

In one sentence, an NTT is an NFT that can only be minted but not transferred, correct?

As a first NTT, why not create the EIP Creator Badge ? An NTT created by the Ethereum foundation, and attributed to EIP-standard creators ?

Curious about what does this sentence mean?

This is an interesting proposal, i think there are a lot of cases where tokens are informal and effectively non-transferable.

I assume that there is no need for a kind of mandated transfer in cases when a recipient address is compromised (private key inaccessible, stolen or lost)? Tokens have to be invalidated and re-issued? I assume implementations might want to combine this into one call to save on gas.

In terms of what you can do with them technically, yes. But the purpose is different.

I really like the fact that there’s no speculation involved for once. NTTs, at their core, are just a proof of possession. But that hits so many use cases! You can imagine NTTs for all sorts of achievements
(a bit like the playstation online). You can imagine a particular implementation where the tokens can be minted only if there is a consensus of a predefined set of addresses => that could be for scholarships for instance, or something less serious :slight_smile:.

You can imagine that the Ethereum foundation would give a badge to all the people who contributed to create one of their standards. They would send them over mail, you would receive one, clip it on your jacket, and brag about it because only Ethereum contributors have that badge :slight_smile:

Since NTTs can be badges (or anything non-tradable that was given to you personally), you can make a NTT for that. If the Ethereum foundation deploys the contract I showed at the end of the post, they can do so. They’ll be able to give a badge by calling giveThatManABadge() (or giveThatGirlABadge() of course, there was a meme intended :stuck_out_tongue:). The address of their contract would be well-known and anyone can verify that you personally had a badge delivered by the foundation.

1 Like

So, actually it would be nice to be able to transfer your token to another of your wallets, however it’s a bit hard to achieve since the whole point is to have non-transferable tokens ^^ Maybe if you can prove you own the other address by signing messages on something…

But then, I try to make the bridge with real world applications and it doesn’t necessarily make sense. When an authority delivers a certificate (say a diploma or driving licence) you can imagine that they wouldn’t want you to be able to change your name, or your address on the official paper.

That’s one of the reasons there are isValid()and invalidate() methods. In case:
1- Your certificate expires
2- The authority wants to take it back because of something you did

Implementations could vary a lot I guess. But the standard has minimal required methods to work (at least I tried to make it that way). I might add a total() method to get the total number of issued tokens.

I suppose ERC721 can achieve the same effect by locking transfer and transferFrom functions.

How is this different than EIP-1238?

@maxareo Yes indeed it could if you modify a bit (NTT has an issuer for instance). But I don’t know if it makes sense to implement ERC721 and override transfer methods to do nothing… Seems weird.

@numtel Thank you for pointing that to me! never saw that since I was searching for existing standards on https://eips.ethereum.org
It’s basically the same idea but it seems the author never made a proper implementation / PR. I wonder why.