EIP-6381: Emotable Extension for Non-Fungible Tokens


eip: 6381
title: Emotable Extension for Non-Fungible Tokens
description: An interface for Non-Fungible Tokens extension allowing for reacting to them using Unicode emojis.
author: Bruno Škvorc, Steven Pineda, Stevan Bogosavljevic, Jan Turk
discussions-to: here
status: Draft
type: Standards Track
category: ERC
created: 2023-01-22
requires: 165, 721

Abstract

The Emotable Extension for Non-Fungible Tokens standard extends EIP-721 by allowing NFTs to be emoted at.

This proposal introduces the ability to react to NFTs using Unicode standardized emoji.

Motivation

With NFTs being a widespread form of tokens in the Ethereum ecosystem and being used for a variety of use cases, it is time to standardize additional utility for them. Having the ability for anyone to interact with an NFT introduces an interactive aspect to owning an NFT and unlocks feedback-based NFT mechanics.

This EIP introduces new utilities for EIP-721 based tokens in the following areas:

Interactivity

The ability to emote on an NFT introduces the aspect of interactivity to owning an NFT. This can either reflect the admiration for the emoter (person emoting to an NFT) or can be a result of a certain action performed by the token’s owner. Accumulating emotes on a token can increase its uniqueness and/or value.

Feedback based evolution

Standardized on-chain reactions to NFTs allow for feedback based evolution.

Current solutions are either proprietary or off-chain and therefore subject to manipulation and distrust. Having the ability to track the interaction on-chain allows for trust and objective evaluation of a given token. Designing the tokens to evolve when certain emote thresholds are met incentivizes interaction with the token collection.

Valuation

Current NFT market heavily relies on previous values the token has been sold for, the lowest price of the listed token and the scarcity data provided by the marketplace. There is no real time indication of admiration or desirability of a specific token. Having the ability for users to emote to the tokens adds the possibility of potential buyers and sellers gageing the value of the token based on the impressions the token has collected.

Specification

The key words “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.

/// @title EIP-6381 Emotable Extension for Non-Fungible Tokens
/// @dev See https://eips.ethereum.org/EIPS/eip-5773
/// @dev Note: the ERC-165 identifier for this interface is 0xf8d6854d.

pragma solidity ^0.8.16;

interface IRMRKEmotable is IERC165 {
    /**
     * @notice Used to notify listeners that the token with the specified ID has been emoted to or that the reaction has been revoked.
     * @dev The event SHOULD only be emitted if the state of the emote is changed.
     * @param emoter Address of the account that emoted or revoked the reaction to the token
     * @param tokenId ID of the token
     * @param emoji Unicode identifier of the emoji
     * @param on Boolean value signifying whether the token was emoted to (`true`) or if the reaction has been revoked (`false`)
     */
    event Emoted(
        address indexed emoter,
        uint256 indexed tokenId,
        bytes4 emoji,
        bool on
    );

    /**
     * @notice Used to get the number of emotes for a specific emoji on a token.
     * @param tokenId ID of the token to check for emoji count
     * @param emoji Unicode identifier of the emoji
     * @return Number of emotes with the emoji on the token
     */
    function getEmoteCount(
        uint256 tokenId,
        bytes4 emoji
    ) external view returns (uint256);

    /**
     * @notice Used to emote or undo an emote on a token.
     * @dev Does nothing if attempting to set a pre-existent state.
     * @dev When the state is being changed, the Emoted event MUST be emitted.
     * @param tokenId ID of the token being emoted
     * @param emoji Unicode identifier of the emoji
     * @param state Boolean value signifying whether to emote (`true`) or undo (`false`) emote
     */
    function emote(uint256 tokenId, bytes4 emoji, bool state) external;
}

Rationale

Designing the proposal, we considered the following questions:

  1. Does the proposal support custom emotes or only the Unicode specified ones?
    The proposal only accepts the Unicode identifier which is a bytes4 value. This means that while we encourage implementers to add the reactions using standardized emojis, the values not covered by the Unicode standard can be used for custom emotes. The only drawback being that the interface displaying the reactions will have to know what kind of image to render and such additions will probably be limited to the interface or marketplace in which they were made.
  2. Should the proposal use emojis to relay the impressions of NFTs or some other method?
    The impressions could have been done using user-supplied strings or numeric values, yet we decided to use emojis since they are a well established mean of relaying impressions and emotions.

Backwards Compatibility

The Emotable token standard is fully compatible with EIP-721 and with the robust tooling available for implementations of EIP-721 as well as with the existing EIP-721 infrastructure.

Test Cases

Tests are included in emotable.ts.

To run them in terminal, you can use the following commands:

cd ../assets/eip-6381
npm install
npx hardhat test

Reference Implementation

See Emotable.sol.

Security Considerations

The same security considerations as with EIP-721 apply: hidden logic may be present in any of the functions, including burn, add asset, accept asset, and more.

Caution is advised when dealing with non-audited contracts.

Copyright

Copyright and related rights waived via CC0.

1 Like

What do you think about maybe holding arbitrary data about token on a separate contract?

Just a simple contract that holds mapping, e.g. [account][contract][tokenId]=>emoji

I think that since smart contracts are supposed to function like micro services, it might make more sense to create a contract as a public service for that.

This could potentially allow you to add emojis or any other arbitrary data about any NFT, even for immutable contracts that were already deployed.

Hey @toledoroy, thanks for participating!
Internally, we actually created 2 versions, an extension (the one we’re presenting) and a stand alone which can track emojis for any contract.
We don’t see it as valuable to create an EIP for the second case, since you don’t really need an interface, ideally you would need a single contract deployed which tracks them all. Hard to achieve.

Apologies if I’ve mentioned this before, but what if you track reactions as ERC-5114 badges? Note that these soulbound badges aren’t removable, which would be different from your current proposal.

That’s just too different honestly. Besides not being removable, you may have thousands of reactions using the same emoji on a token, which can be tracked by a simple counter in this proposal. With the badges approach it would be too expensive to do this.

What if I suggest allowing burning badges to @MicahZoltu? It’s a small amount of mutability, in exchange for use cases like these.

If the emoji are tracked as a simple counter, how do you control who can remove one? Couldn’t anyone remove the reaction I added?

We use two simple mappings to track the emotes given by the address and the number of emotes the token has received: EIPs/Emotable.sol at 2bdcdb61baf8ba6210096c06f0e265d8622d4d7f · ethereum/EIPs · GitHub

This ensures that the emoter can emote and undo emote and that the change is reflected on the token.

That seems roughly equivalent to what you’d need to do to keep track of badges. You could use keccak256(abi.encode(msg.sender, tokenId, emoji)) as the badge’s tokenId.

Here’s an untested implementation:

// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.17;

interface IERC5114 {
	event Mint(uint256 indexed badgeId, address indexed nftAddress, uint256 indexed nftTokenId);

	function ownerOf(uint256 badgeId) external view returns (address nftAddress, uint256 nftTokenId);

	function collectionUri() external pure returns (string memory collectionUri);

	function badgeUri(uint256 badgeId) external view returns (string memory badgeUri);

	function metadataFormat() external pure returns (string memory format);
}

contract Reactions is IERC5114 {
    event Reacted(
        address indexed emoter,
        address indexed nftAddress,
        uint256 indexed nftTokenId,
        bytes4 emoji,
        bool enabled
    );

    struct Reaction {
        address operator;
        address nftAddress;
        uint256 nftTokenId;
        bytes4 codepoint;
        bool enabled;
    }

    mapping (uint256 => Reaction) private _reactions;

    //      (nftAddress =>         (nftTokenId =>        (codepoint => count  )))
    mapping (address    => mapping (uint256    => mapping(bytes4    => uint256))) private _counts;

    /*
     * IERC5114 Implementation
     */
    string constant public metadataFormat = "TODO";
    string constant public collectionUri = "TODO";

    function ownerOf(uint256 badgeId) external view returns (address nftAddress, uint256 nftTokenId) {
        Reaction storage reaction = _reactions[badgeId];
        require(address(0) != reaction.operator);
        return (reaction.nftAddress, reaction.nftTokenId);
    }

    function badgeUri(uint256 badgeId) external view returns (string memory) {
        require(address(0) != _reactions[badgeId].operator);
        return "TODO";
    }

    /*
     * IToggleable Implementation
     */
    function isEnabled(uint256 badgeId) external view returns (bool) {
        Reaction storage reaction = _reactions[badgeId];
        require(address(0) != reaction.operator);
        return reaction.enabled;
    }

    /*
     * Reactions Implementation
     */

    function id(
        address operator,
        address nftAddress,
        uint256 nftTokenId,
        bytes4 codepoint
    ) public pure returns (uint256) {
        return uint256(keccak256(abi.encode(operator, nftAddress, nftTokenId, codepoint)));
    }

    function react(
        address nftAddress,
        uint256 nftTokenId,
        bytes4 codepoint,
        bool enabled
    ) external returns (uint256) {
        uint256 badgeId = id(msg.sender, nftAddress, nftTokenId, codepoint);
        Reaction storage reaction = _reactions[badgeId];

        if (reaction.enabled == enabled) {
            return badgeId;
        }

        if (address(0) == reaction.operator) {
            reaction.operator = msg.sender;
            reaction.nftAddress = nftAddress;
            reaction.nftTokenId = nftTokenId;
            reaction.codepoint = codepoint;

            emit Mint(badgeId, nftAddress, nftTokenId);
        }

        if (enabled) {
            _counts[nftAddress][nftTokenId][codepoint] += 1;
        } else {
            _counts[nftAddress][nftTokenId][codepoint] -= 1;
        }

        reaction.enabled = enabled;
        emit Reacted(
            msg.sender,
            nftAddress,
            nftTokenId,
            codepoint,
            enabled
        );

        return badgeId;
    }

    function reactionCountOf(
        address nftAddress,
        uint256 nftTokenId,
        bytes4 codepoint
    ) public view returns (uint256) {
        return _counts[nftAddress][nftTokenId][codepoint];
    }
}

Any mutability (including burning) makes it so you can not aggressively cache badges, which was a design goal of 5114. That being said, one could create another EIP for mutable badges where the owner can burn them. This EIP could share essentially the same interface as 5114, so anyone supporting one would likely support the other as well. I think there is value in calling those two types of badges different things however, which is why I think they should be different EIPs (with different numbers and titles).

There are so many differences:

  1. They are meant to be non removable, confirmed by the author.
  2. Badges are sent to any contract, which can be a nice feature, but our proposal is meant to be an extension, so badges are tracked within the contract itself.
  3. We don’t need collectionUri nor metadataFormat. For badgeUri we could force it to return the id of the emoji I guess, but it feels really hacky IMO.
  4. There’s no way to get the count of reactions for a specific emoji on a token. It is reasonable in general since an indexer could take care of this. But it is important on our EIP since we intend implementations to vary the behavior based on this. e.g:
  • An NFT of a plant returns a dying plant image unless it has a certain number of :cloud_with_rain: reactions. This plays really well with ERC-5773.
  • An NFT of a person returns a blushed version after it gets 100 :heart: emojis. Goes also well with 5773.
  • An NFT can be transferable only after receiving 10 :unlock: reactions, playing nice with ERC-6454

Finally, when building extensions of ERC721, we’ve found contract size to be a huge limitation. You hit the 24kb limit easily when you stack many together. So we try to keep them as minimal as possible. I understand your purpose with this and it’s really valuable, but in this case forcing it into use a somehow related one would be harmful for us.

Very true. The example I posted uses non-removable badges. I’d be very open to making a burnable badge standard if necessary.

What benefits come with putting the reactions in the same contract?

Assuming someone made a generic badge viewing dapp, or wallets build in native support for badges, then the reactions/emoji badges would just show up (and collectionUri could have a nice description of what a reaction is for.) metadataFormat is just an implementation detail.

See reactionCountOf from my earlier example.

Ha, I love it! That’ll be quite fun. I think you could still implement this by querying the badge contract in your tokenURI?

Isn’t this a great argument against your point 2? If the reactions/emoji live in their own contract, there’s no code size increase to the ERC-721 contract.

It seems like such a close fit to me, but it’s totally fine if you don’t agree!

Thanks again, I really enjoy this discussions :grin:

What benefits come with putting the reactions in the same contract?

I don’t have to keep track of an external contract where emojis would be stored. My main problem with tracking them in an external contract is that there could actually be many! Each marketplace might have their own for instance, so I’d have to either choose or aggregate, which feels less usable.

Assuming someone made a generic badge viewing dapp, or wallets build in native support for badges, then the reactions/emoji badges would just show up (and collectionUri could have a nice description of what a reaction is for.) metadataFormat is just an implementation detail.

It still feels forced and extra unneeded effort IMO. Emojis are powerful, widely used and can be achieved with as little as what we’re proposing.

See reactionCountOf from my earlier example.

The thing is, that’s not part of the 5114’s interface, and we do need it to be there if we want to use it as intended. It would be a show stopper.

Isn’t this a great argument against your point 2? If the reactions/emoji live in their own contract, there’s no code size increase to the ERC-721 contract.

It is :sweat_smile: I realized as I was writing it. But weighing pros and cons we find local storage better for this use case, mostly for the reason pointed in the answer about benefits.

That’s a great point :thinking: What if the standard includes the deployment transaction so there can be exactly one emoji contract at a well-known address?

For sure! I’m more suggesting we define IEmotable as:

interface IEmotable is IERC5114 { ... }

Or in other words, add whatever extra methods/events you need on top of IERC5114.

I would use something like GitHub - Zoltu/deterministic-deployment-proxy: An Ethereum proxy contract that can be used for deploying contracts to a deterministic address on any chain. personally, which is a simplification around keyless execution. I agree with this general idea though. Have a single “reaction registry” (this would be standardized) that allows anyone to react to any token (5114 or 721 or other, anything with the form (address, id)). This means you don’t need to build tokens with support for reactions built-in, it can be added on to any existing token. For example, people could react to CryptoKitties or Bored Apes once the standard is finished.

We will review your suggestions @SamWilsn and @MicahZoltu, they look promising.
Thanks!

We decided to switch for the recommended approach of a single contract with a predictable address across all chains. :partying_face: but not reusing 5114. We’ll need a few days to adapt, will keep you posted.
Thanks!

1 Like