Research
Security News
Malicious npm Package Targets Solana Developers and Hijacks Funds
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
attributable
Advanced tools
A proposal for a standard approach to attributes on chain
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.
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:
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]
// 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;
}
// 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);
}
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);
}
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:
None, right now. Some coming soon.
(c) 2022, Francesco Sullo francesco@sullo.co
MIT
FAQs
A proposal for on-chain attributes for NFT and other assets
The npm package attributable receives a total of 1 weekly downloads. As such, attributable popularity was classified as not popular.
We found that attributable demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
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.
Research
Security News
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
Security News
Research
Socket researchers have discovered malicious npm packages targeting crypto developers, stealing credentials and wallet data using spyware delivered through typosquats of popular cryptographic libraries.
Security News
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.