Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

attributable

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

attributable

A proposal for on-chain attributes for NFT and other assets

  • 0.0.3
  • latest
  • npm
  • Socket score

Version published
Weekly downloads
1
Maintainers
1
Weekly downloads
 
Created
Source

Attributable

A proposal for a standard approach to attributes on chain

THIS IS A WORK IN PROGRESS

Premise

In 2021, I proposed a standard for on-chain attributes for NFT at https://github.com/ndujaLabs/erc721playable

It was using an array of uint8 to store generic attributes.

After a few iterations and attempts to implement it, I realized that it is unlikely that a player, for example, a game, can be okay with just storing uint8 values. It will most likely need multiple types that defy the advantages of that approach.

Investigating the possible alternatives, I concluded that the best way to have generic values is to encode them in an array of uint256, asking the player to translate them into parameters that can be understood, for example, by a marketplace.

Let's say you have an NFT that starts in a game at level 2 but later can level up. Where do you store the info about the level? If you put it in the JSON metadata, you break one of the rules of the NFT, the immutability of the attributes (essential for collectors). The solution is to split the attributes into two categories: mutable and immutable attributes.

There are a few proposals to extend the metadata provided by JSON files (like https://eips.ethereum.org/EIPS/eip-4906). The problem is that smart contracts can't read dynamic parameters off-chain, which is the problem I am trying to solve here.

Why do we need a common standard for on-chain metadata?

People talks every day about having NFTs that can be moved around games. The problem is that, despite the good intention, that is not possible in most cases. A standard NFT is not intended for it. What it misses is the flexibility necessary to allow any player out there (a game, a metaverse, whatever) to use the NFT inside a game, in total transparency, and in a shared way. The idea behind Attributable is that your NFT:

  1. can be used by any game, i.e., any game access the data in the same format, encoding/decoding them for its purposes
  2. only the NFT owner can authorize the game
  3. only the game can modify its attributes

Point 2 is necessary because you don't want any player adds data to your NFT. For example, a porn game can add you PfP. Maybe you don't like it. For sure, you don't want it if the player is involved in some criminal activity.

Point 3 is necessary because you can cheat after you authorize a game if you alter the data that the game sets. For example, in Mobland, a character can be wounded and go into a coma. If that character is not cured in the maximum allowed time, the character will die. On the market, a dead character will probably have a much lower value than a character in good health. But, right now, there is no way to get it. If the character is Attributable, that value can be stored in the NFT and be visible to anyone.

How? A marketplace like OpenSea can listen to the emitted event and record any new authorization. Then, it can query the authorized game to get the on-chain attributes of that NFT. (Of course, the game can also set off-chain dynamic attributes to make its data more broadly available.)

Point 1 is essential. The data must be stored most generically to allow cross-game maneuverability. The choice then is between uint256 and bytes32. My preference is for using big integers because it looks to be easier to play with.

When you have big integers that encode information, you just need a map based on tokenId and game address. A basic approach would be setting the data as:

mapping(uint256 => mapping(address => uint256)) internal _tokenAttributes;

The problem is that sometimes, a single integer is not enough. So, a better solution is to have an "array" of big integers. My preferred map would be

mapping(uint256 => mapping(address => mapping(uint8 => uint256))) internal _tokenAttributes;

This way, you can have a maximum of 256 values which should cover the 99.9% of the use cases.

Regardless, the optimal data format is not central. What is more important here is to define how the NFT (or any other asset with an ID) interfaces with the game.

Another advantage of this approach is that it allows upgrading a contract keeping the storage compatible with previous versions. Look at this map:

The map above is supposed to be used as

_tokenAttributes[tokenId][playerAddress][index]

The interfaces

IAttributable - the NFT should extend it

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

// Author:
// Francesco Sullo <francesco@sullo.co>

/**
   @title IAttributable Cross-player On-chain Attributes
    Version: 0.0.1
   ERC165 interfaceId is
   */
/* is IERC165 */
interface IAttributable {
  /**
     @dev Emitted when the attributes for an id and a player is set.
          The function must be called by the owner of the asset to authorize a player to set
          attributes on it. The rules for that are left to the asset.

          This even is important because allows a marketplace to know that there are
          dynamic attributes set on the NFT by a specific contract (the player) so that
          the marketplace can query the player to get the attributes of the NFT in within
          the game.
   */
  event AttributesInitializedFor(uint256 indexed _id, address indexed _player);

  /**
     @dev It returns the on-chain attributes of a specific id
       This function is called by the player, which is able to decode the uint and
       transform them in whatever is necessary for the game.
     @param _id The id of the token for whom to query the on-chain attributes
     @param _player The address of the player's contract
     @param _index The index in the array of attributes
     @return The encoded attributes of the token
   */
  function attributesOf(
    uint256 _id,
    address _player,
    uint8 _index
  ) external view returns (uint256);

  /**
     @notice Authorize a player initializing the attributes of a token to 1
     @dev It must be called by the nft's owner to approve the player.

       To avoid that nft owners give themselves arbitrary values, they must not
       be able to set up the values, but only to create the array that later
       will be filled by the player.

       Since by default the value in the array would
       be zero, the initial value must be a uint8 representing the version of the data,
       starting with the value 1. This way the player can see if the data are initialized
       checking that the attributesOf a certain id is 1.

       The function must emit the AttributesInitiated event

     @param _id The id of the token for whom to change the attributes
     @param _player The version of the attributes
   */
  function authorizePlayer(uint256 _id, address _player) external;

  /**
     @notice Sets the attributes of a token after the initialization
     @dev It modifies attributes by id for a specific player. It must
       be called by the player's contract, after an NFT has been initialized.

       The owner of the NFT must not be able to update the attributes.

       It must revert if the asset is not initialized for that player (the msg.sender), i.e., if
       the value returned by attributesOf is 0.

     @param _id The id of the token for whom to change the attributes
     @param _index The index of the array where the attribute is updated
     @param _attributes The encoded attributes
   */
  function updateAttributes(
    uint256 _id,
    uint8 _index,
    uint256 _attributes
  ) external;
}

IPlayer - the player should extend it

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

// Author:
// Francesco Sullo <francesco@sullo.co>

/**
   @title IPlayer Player of an attributable asset
    Version: 0.0.1
   ERC165 interfaceId is
   */
/* is IERC165 */
interface IPlayer {
  /**
    @dev returns the attributes in a readable way
    @param _asset The address of the asset played by the game
    @param _id The id of the asset
    @return A string with type of the attribute, name and value
  */
  function attributesOf(address _asset, uint256 _id) external view returns (string memory);
}


Examples

In /contracts/examples there is an example of a token and a player.

Let's show here just the function attributesOf in the player:

function attributesOf(address _nft, uint256 tokenId) external override view returns (string memory) {
    uint256 _attributes = IAttributable(_nft).attributesOf(tokenId, address(this), 0);
    if (_attributes != 0) {
      return
        string(
          abi.encodePacked(
            "uint8 version:",
            Strings.toString(uint8(_attributes)),
            ";uint8 level:",
            Strings.toString(uint16(_attributes >> 8)),
            ";uint32 stamina:",
            Strings.toString(uint32(_attributes >> 16)),
            ";address winner:",
            Strings.toHexString(uint160(_attributes >> 48), 20)
          )
        );
    } else {
      return "";
    }
  }

Calling it, a marketplace can get something like:

uint8 version:1;uint8 level:2;uint32 stamina:2436233;address winner:0x426eb88af949cd5bd8a272031badc2f80330e766

that can be easily transformed in a JSON like:

{
  "version": {
    "type": "uint8",
    "value": 1    
  },
  "level": {
    "type": "uint8",
    "value": 2
  },
  "stamina": {
    "type": "uint32",
    "value": 2436233
  },
  "winner": {
    "type": "address",
    "value": "0x426eb88af949cd5bd8a272031badc2f80330e766"
  }
}

of something like:

{
  "attributes": [
    {
      "trait_type": "version",
      "value": 1
    },
    {
      "trait_type": "level",
      "value": 2
    },
    {
      "trait_type": "stamina",
      "value": 2436233
    },
    {
      "trait_type": "winner",
      "value": "0x426eb88af949cd5bd8a272031badc2f80330e766"
    }
  ]
}

Notice that the NFT does not encode anything, it is the player who knows what the data means, that encodes it. Look at the following function in MyPlayer.sol:

  function updateAttributesOf(
    address _nft,
    uint256 tokenId,
    TokenData memory data
  ) external {

    require(_operator != address(0) && _operator == _msgSender(), 
            "Not the operator");

    uint256 attributes = uint256(data.version) | 
                         (uint256(data.level) << 8) | 
                         (uint256(data.stamina) << 16) | 
                         (uint256(uint160(data.winner)) << 48);

    IAttributable(_nft).updateAttributes(tokenId, 0, attributes);
  }

Install and usage

To install it, launch

npm i -d attributable

You may need to install the peer dependencies too, i.e., the OpenZeppelin contracts.

To use it, in your smart contract import it as


import "attributable/IAttributable.sol";
import "attributable/IPlayer.sol";

Feel free to make a PR to add your contract in the next section:

Implementations

None, right now. Some coming soon.

(c) 2022, Francesco Sullo francesco@sullo.co

License

MIT

FAQs

Package last updated on 24 Jul 2022

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc