NOTICE: this proposal has been abandoned after trying to use it in a couple of projects because in almost every scenario uint8 values are not enough for what it is needed and when working with conversions, the cost to save the changes becomes higher because of the calculation compared with using custom struct in the contract. This kills the advantage coming for using a common structure amongst games.
A new proposal replacing this one can be found at https://github.com/ndujaLabs/attributable
A proposal to extend the ERC721 standard with a struct and a few functions to allow players, for example games, to use NFTs storing data on the NFT itself.
This repo implements the MCIP-1 proposal mantaining full compatibility with the ERC721 standard.
The standard ERC721 works very well for collectibles, despite being introduced by a game. Decentralized player, for example games, need attributes and other information on chain to be able to play with it. For example, the original EverDragons factory was implementing the following
struct Dragon {
bytes32 name;
uint24 attributes;
uint32 experience;
uint32 prestige;
uint16 state;
mapping(bytes32 => uint32) items;
}
to manage mutable and immutable properties of a dragon. The limit of this model is that the properties were predefined and a different game could not reuse the same approach.
Projects like EverDragons2, which does not have a ready game right now, need a generic solution that establish basic rules and is flexible enough to manage most scenarios and make NFT really movable among games. The same rules can be helpful in any other game, as well as in any platform that can take advantage for managing an array of values in the NFT.
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.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/// @title IERC721Playable Cross-player On-chain Attributes
/// Version: 0.1.1
/// ERC165 interfaceId is 0xac517b2e
interface IERC721Playable /* is IERC165 */ {
/// @dev Emitted when the attributes for a token id and a player is set.
event AttributesSet(uint256 indexed _tokenId, address indexed _player, Attributes _attributes);
/// @dev This struct saves info about the token.
struct Attributes {
// A player can change the way it manages the data, updating the version
// If 8 bits are not enough, one attribute can be used to form a uint16
// Alternatively, we could use the first attribute as a version, but it
// would use the same space, making the concept less clear
uint8 version;
// Attributes can be immutable (for example because taken from the attributes.json)
// or mutable, because they depends only on the player itself.
// If a field requires more than 256 possible value, two bytes can be used for it.
uint8[31] attributes;
}
/// @dev It returns the on-chain attributes of a specific token
/// @param _tokenId The id of the token for whom to query the on-chain attributes
/// @param _player The address of the player's contract
/// @return The attributes of the token
function attributesOf(uint256 _tokenId, address _player) external view returns (Attributes memory);
/// @notice Initialize the attributes of a token
/// @dev It must be called by the nft's owner to approve the player.
/// To avoid that nft owners give themselves arbitrary values, they can
/// only set an empty uint8[], with version = 1
/// Later, the player will be able to update array and version, with the right values
/// @param _tokenId The id of the token for whom to change the attributes
/// @param _player The version of the attributes
/// @return true if the initialization is successful
function initAttributes(
uint256 _tokenId,
address _player
) external returns (bool);
/// @notice Sets the attributes of a token after first set up
/// @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.
/// @param _tokenId The id of the token for whom to change the attributes
/// @param _newVersion It should be changed only when introducing a new set
/// of attributes. If set to zero, the update should be ignored.
/// @param _indexes The indexes of the attributes to be changed
/// @param _attributes The values of the attributes to be changed
/// @return true if the change is successful
function updateAttributes(
uint256 _tokenId,
uint8 _newVersion,
uint256[] memory _indexes,
uint8[] memory _attributes
) external returns (bool);
}
The reason why ERC721 metadata are off-chain makes perfect sense in general, in particular for immutable collectibles, but it does not allow pure on-chain games to interact with the NFT because they cannot access the metadata. This proposal adds a relatively inexpensive solution to it.
Imagine that there are two Bored Ape with the same rarity, but the two apes are used in a game. If one of the two gets very good attributes, whoever will by the ape will also buy levels in the game where the ape played. So, the value on the market of the good ape will be higher that the value of the ape who is a bad player.
Of course, to work as expected, OpenSea and other marketplace should have a list of games and be able to retrieve the attributes on game of any NFT.
If NFT owners are allowed to update the attributes of their tokens, they could set whichever value the want, so the only the Player must be able to update the attributes. It is important that the owner cannot revoke the approval because if not, owners could play a game and, when they realized they are decreasing their score, before that the game updates the attributes, they revoke the approval.
On the other hand, if the Player initializes the token, spam will florish. For example, a bad game could initialize tokens pretending that their owner played the game. Think of a porn game that sets your NFT and pretends that you have high scores. That can be a problem, right? The solution is that the Owner only can initialize the attribute for a specific game.
The on-chain Metadata must be used only for important informations, like, for example, level ups. If used for frequent changing values, too many changes will congest the blockchain.
As long as 31 bytes are enough, numbers can be spread on the array. For that you can use the library Uint8Lib
in the libraries
folder. It manages conversions between uint8, uint16, uint32 and uint64.
But, this proposal works well only as long as the data to be saved are uint8 — which is what in theory we should do to save on chain the minimum possible. However, in the initial cases where this proposal was tested, it was necessary to save larger information. Then, it had become clear that the conversion consumes too much gas, making the usage of a uint8[31]
in a struct not convenient.
A game just need to be able to set attributes in the NFT to manage it. Marketplace, however, need more information to show their users the attributes in a way that makes sense and helps users to value a token. It is unrealistic to expect that a marketplace understands what an attribute is used for in any game in the crypto metaverse.
The solution is an interface that any game can implement to expose a map that shows what any attribute is for.
We will cover this in a future proposal, but the idea is to expose a function like
function attributesURI(uint8 _version) external returns(string memory);
which should return a JSON file allowing the marketplace to interpretate any attribute for a specific version, and return the full set of information if the passed version is 0
.
This is totally compatible with the ERC721 standard.
This repo implements a full working contract at https://github.com/ndujaLabs/erc721playable/blob/main/contracts/ERC721Playable.sol
In the implementation there is the internal function _initAttributesAndSafeMint
which mints a token and set its attributes. It can be used to mint pre-configured tokens.
Install with
npm install @ndujalabs/erc721playable
and use as
import "@ndujalabs/erc721playable/contracts/ERC721Playable.sol";
contract YourNFT is ERC721Playable {
...
MIT
(c) 2021 Francesco Sullo & Nduja Labs