AttributeLib Library¶
Overview¶
The AttributeLib library provides core utilities for managing token attributes within the Gemforce platform. This library implements a flexible attribute system that allows tokens to have dynamic, typed metadata stored on-chain, supporting various data types and efficient key-value storage.
Key Features¶
- Typed Attributes: Support for multiple data types (String, Bytes32, Uint256, Arrays)
- Dynamic Metadata: Runtime attribute assignment and modification
- Efficient Storage: Optimized key-value storage with indexing
- Burn Protection: Prevents attribute operations on burned tokens
- Batch Operations: Support for setting multiple attributes simultaneously
- Event Tracking: Comprehensive event emission for attribute changes
Library Definition¶
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.6;
library AttributeLib {
// Storage management
function attributeStorage() internal pure returns (AttributeStorage storage);
// Attribute retrieval
function _getAttribute(AttributeContract storage self, uint256 tokenId, string memory key) internal view returns (Attribute memory);
function _getAttributeValues(uint256 id) internal view returns (string[] memory);
function _getAttributeKeys(AttributeContract storage self, uint256 tokenId) internal view returns (string[] memory);
// Attribute modification
function _setAttribute(AttributeContract storage self, uint256 tokenId, Attribute memory attribute) internal;
function _setAttributes(AttributeContract storage self, uint256 tokenId, Attribute[] memory _attributes) internal;
function _removeAttribute(AttributeContract storage self, uint256 tokenId, string memory key) internal;
// Token lifecycle
function _burn(AttributeContract storage self, uint256 tokenId) internal;
}
Data Structures¶
AttributeType Enum¶
enum AttributeType {
Unknown, // Undefined type
String, // String value
Bytes32, // 32-byte value
Uint256, // 256-bit unsigned integer
Uint8, // 8-bit unsigned integer
Uint256Array, // Array of 256-bit unsigned integers
Uint8Array // Array of 8-bit unsigned integers
}
Purpose: Defines supported attribute data types for type-safe operations.
Attribute Struct¶
struct Attribute {
string key; // Attribute identifier
AttributeType attributeType; // Type of the attribute value
string value; // String representation of the value
}
Purpose: Core attribute data structure containing key, type, and value.
Design Notes: - All values stored as strings for simplicity and gas efficiency - Type information preserved for proper interpretation - Key serves as unique identifier within token scope
AttributeContract Struct¶
struct AttributeContract {
mapping(uint256 => bool) burnedIds; // Burned token tracking
mapping(uint256 => mapping(string => Attribute)) attributes; // Token attributes
mapping(uint256 => string[]) attributeKeys; // Token attribute keys
mapping(uint256 => mapping(string => uint256)) attributeKeysIndexes; // Key index mapping
}
Purpose: Complete storage structure for token attribute management.
Components: - burnedIds: Tracks burned tokens to prevent attribute operations - attributes: Nested mapping for token-specific attribute storage - attributeKeys: Ordered list of attribute keys per token - attributeKeysIndexes: Efficient key lookup and removal indexing
AttributeStorage Struct¶
struct AttributeStorage {
AttributeContract attributes;
}
Purpose: Diamond storage wrapper for attribute data.
Storage Position:
bytes32 internal constant DIAMOND_STORAGE_POSITION =
keccak256("diamond.nextblock.bitgem.app.AttributeStorage.storage");
Core Functions¶
Storage Management¶
attributeStorage()¶
function attributeStorage() internal pure returns (AttributeStorage storage ds)
Purpose: Access Diamond storage for attribute data using assembly.
Implementation:
function attributeStorage() internal pure returns (AttributeStorage storage ds) {
bytes32 position = DIAMOND_STORAGE_POSITION;
assembly {
ds.slot := position
}
}
Returns: Storage reference to attribute data
Usage: All attribute functions use this to access persistent storage.
Attribute Retrieval¶
_getAttribute()¶
function _getAttribute(
AttributeContract storage self,
uint256 tokenId,
string memory key
) internal view returns (Attribute memory)
Purpose: Retrieve a specific attribute for a token by key.
Parameters:
- self: Storage reference to attribute contract
- tokenId: Token identifier
- key: Attribute key to retrieve
Returns: Attribute struct containing key, type, and value
Security: Prevents access to burned token attributes
Example Usage:
// Get token rarity attribute
Attribute memory rarityAttr = Attribute({
key: "rarity",
attributeType: AttributeType.String,
value: "Legendary"
});
AttributeLib._getAttribute(
attributeStorage.attributes,
tokenId,
"rarity"
);
console.log("Token rarity:", rarityAttr.value);
_getAttributeValues()¶
function _getAttributeValues(uint256 id) internal view returns (string[] memory)
Purpose: Retrieve all attribute values for a token in key order.
Parameters:
- id: Token identifier
Returns: Array of attribute values as strings
Implementation:
function _getAttributeValues(uint256 id) internal view returns (string[] memory) {
AttributeContract storage ct = AttributeLib.attributeStorage().attributes;
string[] memory keys = ct.attributeKeys[id];
string[] memory values = new string[](keys.length);
uint256 keysLength = keys.length;
for (uint256 i = 0; i < keysLength; i++) {
values[i] = ct.attributes[id][keys[i]].value;
}
return values;
}
Use Cases: - Metadata generation for NFTs - Bulk attribute export - Attribute comparison operations
_getAttributeKeys()¶
function _getAttributeKeys(
AttributeContract storage self,
uint256 tokenId
) internal view returns (string[] memory)
Purpose: Retrieve all attribute keys for a token.
Parameters:
- self: Storage reference to attribute contract
- tokenId: Token identifier
Returns: Array of attribute keys
Security: Prevents access to burned token attributes
Example Usage:
// Get all attribute keys for a token
string[] memory keys = AttributeLib._getAttributeKeys(
attributeStorage.attributes,
tokenId
);
for (uint256 i = 0; i < keys.length; i++) {
console.log("Attribute key:", keys[i]);
}
Attribute Modification¶
_setAttribute()¶
function _setAttribute(
AttributeContract storage self,
uint256 tokenId,
Attribute memory attribute
) internal
Purpose: Set or update a single attribute for a token.
Parameters:
- self: Storage reference to attribute contract
- tokenId: Token identifier
- attribute: Attribute data to set
Process: 1. Verify token is not burned 2. Check if attribute key is new 3. Add key to keys array if new 4. Update key index mapping 5. Store attribute data
Security: Prevents modification of burned token attributes
Example Usage:
// Set token rarity attribute
Attribute memory rarityAttr = Attribute({
key: "rarity",
attributeType: AttributeType.String,
value: "Legendary"
});
AttributeLib._setAttribute(
attributeStorage.attributes,
tokenId,
rarityAttr
);
_setAttributes()¶
function _setAttributes(
AttributeContract storage self,
uint256 tokenId,
Attribute[] memory _attributes
) internal
Purpose: Set multiple attributes for a token in a single operation.
Parameters:
- self: Storage reference to attribute contract
- tokenId: Token identifier
- _attributes: Array of attributes to set
Implementation:
function _setAttributes(
AttributeContract storage self,
uint256 tokenId,
Attribute[] memory _attributes
) internal {
require(self.burnedIds[tokenId] == false, "Token has been burned");
uint256 attributesLength = _attributes.length;
for (uint256 i = 0; i < attributesLength; i++) {
_setAttribute(self, tokenId, _attributes[i]);
}
}
Benefits: - Gas efficient for multiple attributes - Atomic operation for consistency - Simplified batch updates
Example Usage:
// Set multiple attributes at once
Attribute[] memory attributes = new Attribute[](3);
attributes[0] = Attribute("rarity", AttributeType.String, "Epic");
attributes[1] = Attribute("level", AttributeType.Uint256, "25");
attributes[2] = Attribute("element", AttributeType.String, "Fire");
AttributeLib._setAttributes(
attributeStorage.attributes,
tokenId,
attributes
);
_removeAttribute()¶
function _removeAttribute(
AttributeContract storage self,
uint256 tokenId,
string memory key
) internal
Purpose: Remove a specific attribute from a token.
Parameters:
- self: Storage reference to attribute contract
- tokenId: Token identifier
- key: Attribute key to remove
Process: 1. Verify token is not burned 2. Delete attribute data 3. Find key index in keys array 4. Shift remaining keys to fill gap 5. Update key indexes 6. Remove last element from array 7. Emit removal event
Security: Prevents modification of burned token attributes
Example Usage:
// Remove temporary attribute
AttributeLib._removeAttribute(
attributeStorage.attributes,
tokenId,
"temporary_boost"
);
Token Lifecycle¶
_burn()¶
function _burn(
AttributeContract storage self,
uint256 tokenId
) internal
Purpose: Mark a token as burned to prevent future attribute operations.
Parameters:
- self: Storage reference to attribute contract
- tokenId: Token identifier to burn
Implementation:
function _burn(
AttributeContract storage self,
uint256 tokenId
) internal {
self.burnedIds[tokenId] = true;
}
Effects: - Prevents all future attribute operations on the token - Preserves existing attribute data for historical purposes - Enables gas-efficient burn protection checks
Integration Examples¶
NFT Collection with Dynamic Attributes¶
// NFT collection with evolving attributes
contract EvolvingNFT {
using AttributeLib for AttributeLib.AttributeStorage;
struct Evolution {
uint256 requiredLevel;
string[] newAttributes;
string[] attributeValues;
}
mapping(uint256 => uint256) public tokenLevels;
mapping(uint256 => uint256) public tokenExperience;
mapping(uint256 => Evolution[]) public evolutionPaths;
event TokenEvolved(uint256 indexed tokenId, uint256 newLevel);
event ExperienceGained(uint256 indexed tokenId, uint256 experience);
event AttributeEvolved(uint256 indexed tokenId, string attribute, string newValue);
function initializeToken(uint256 tokenId, string[] memory initialAttributes, string[] memory initialValues) external {
require(_exists(tokenId), "Token does not exist");
require(initialAttributes.length == initialValues.length, "Arrays length mismatch");
AttributeLib.AttributeStorage storage attrStorage = AttributeLib.attributeStorage();
// Set initial attributes
Attribute[] memory attributes = new Attribute[](initialAttributes.length + 2);
for (uint256 i = 0; i < initialAttributes.length; i++) {
attributes[i] = Attribute({
key: initialAttributes[i],
attributeType: AttributeType.String,
value: initialValues[i]
});
}
// Add level and experience
attributes[initialAttributes.length] = Attribute({
key: "level",
attributeType: AttributeType.Uint256,
value: Strings.toString(tokenLevels[tokenId])
});
attributes[initialAttributes.length + 1] = Attribute({
key: "experience",
attributeType: AttributeType.Uint256,
value: Strings.toString(tokenExperience[tokenId])
});
AttributeLib._setAttributes(attrStorage.attributes, tokenId, attributes);
tokenLevels[tokenId] = 1;
tokenExperience[tokenId] = 0;
}
function gainExperience(uint256 tokenId, uint256 experience) external {
require(_exists(tokenId), "Token does not exist");
require(experience > 0, "Experience must be positive");
AttributeLib.AttributeStorage storage attrStorage = AttributeLib.attributeStorage();
// Update experience
tokenExperience[tokenId] += experience;
// Update experience attribute
Attribute memory expAttr = Attribute({
key: "experience",
attributeType: AttributeType.Uint256,
value: Strings.toString(tokenExperience[tokenId])
});
AttributeLib._setAttribute(attrStorage.attributes, tokenId, expAttr);
emit ExperienceGained(tokenId, experience);
// Check for level up
_checkLevelUp(tokenId);
}
function _checkLevelUp(uint256 tokenId) internal {
uint256 currentLevel = tokenLevels[tokenId];
uint256 currentExp = tokenExperience[tokenId];
uint256 requiredExp = currentLevel * 100; // Simple formula
if (currentExp >= requiredExp) {
uint256 newLevel = currentLevel + 1;
tokenLevels[tokenId] = newLevel;
AttributeLib.AttributeStorage storage attrStorage = AttributeLib.attributeStorage();
// Update level attribute
Attribute memory levelAttr = Attribute({
key: "level",
attributeType: AttributeType.Uint256,
value: Strings.toString(newLevel)
});
AttributeLib._setAttribute(attrStorage.attributes, tokenId, levelAttr);
emit TokenEvolved(tokenId, newLevel);
// Apply evolution changes
_applyEvolution(tokenId, newLevel);
}
}
function _applyEvolution(uint256 tokenId, uint256 newLevel) internal {
Evolution[] memory evolutions = evolutionPaths[tokenId];
for (uint256 i = 0; i < evolutions.length; i++) {
if (evolutions[i].requiredLevel == newLevel) {
AttributeLib.AttributeStorage storage attrStorage = AttributeLib.attributeStorage();
for (uint256 j = 0; j < evolutions[i].newAttributes.length; j++) {
Attribute memory newAttr = Attribute({
key: evolutions[i].newAttributes[j],
attributeType: AttributeType.String, // Assuming string for simplicity
value: evolutions[i].attributeValues[j]
});
AttributeLib._setAttribute(attrStorage.attributes, tokenId, newAttr);
emit AttributeEvolved(tokenId, newAttr.key, newAttr.value);
}
break;
}
}
}
function _exists(uint256 tokenId) internal view returns (bool) {
// Placeholder for actual NFT existence check
return true;
}
}
Gaming Item System¶
// Gaming platform with customizable items
contract GameItems {
using AttributeLib for AttributeLib.AttributeStorage;
struct ItemStats {
uint256 attack;
uint256 defense;
uint256 manaCost;
string itemType;
}
mapping(uint256 => ItemStats) public baseItemStats;
event ItemCreated(uint256 indexed itemId, string name, string itemType);
event StatsUpdated(uint256 indexed itemId, uint256 attack, uint256 defense);
function createItem(uint256 itemId, string memory name, string memory itemType, uint256 attack, uint256 defense, uint256 manaCost) external {
AttributeLib.AttributeStorage storage attrStorage = AttributeLib.attributeStorage();
// Set basic attributes
Attribute[] memory attributes = new Attribute[](5);
attributes[0] = Attribute({key: "name", attributeType: AttributeType.String, value: name});
attributes[1] = Attribute({key: "itemType", attributeType: AttributeType.String, value: itemType});
attributes[2] = Attribute({key: "attack", attributeType: AttributeType.Uint256, value: Strings.toString(attack)});
attributes[3] = Attribute({key: "defense", attributeType: AttributeType.Uint256, value: Strings.toString(defense)});
attributes[4] = Attribute({key: "manaCost", attributeType: AttributeType.Uint256, value: Strings.toString(manaCost)});
AttributeLib._setAttributes(attrStorage.attributes, itemId, attributes);
baseItemStats[itemId] = ItemStats(attack, defense, manaCost, itemType);
emit ItemCreated(itemId, name, itemType);
}
function updateItemStats(uint256 itemId, uint256 newAttack, uint256 newDefense) external {
AttributeLib.AttributeStorage storage attrStorage = AttributeLib.attributeStorage();
Attribute memory attackAttr = Attribute({key: "attack", attributeType: AttributeType.Uint256, value: Strings.toString(newAttack)});
Attribute memory defenseAttr = Attribute({key: "defense", attributeType: AttributeType.Uint256, value: Strings.toString(newDefense)});
AttributeLib._setAttribute(attrStorage.attributes, itemId, attackAttr);
AttributeLib._setAttribute(attrStorage.attributes, itemId, defenseAttr);
emit StatsUpdated(itemId, newAttack, newDefense);
}
function getItemAttack(uint256 itemId) external view returns (uint256) {
AttributeLib.AttributeStorage storage attrStorage = AttributeLib.attributeStorage();
Attribute memory attr = AttributeLib._getAttribute(attrStorage.attributes, itemId, "attack");
return Strings.toUint(attr.value);
}
}
Certificate System with Verifiable Attributes¶
// Certificate system for verifiable credentials
contract CertificateSystem {
using AttributeLib for AttributeLib.AttributeStorage;
struct Certificate {
uint256 certificateId;
address holder;
uint256 issueDate;
uint256 expiryDate;
bool revoked;
string certificateType;
}
mapping(uint256 => Certificate) public certificates;
event CertificateIssued(uint256 indexed certificateId, address indexed holder, string certificateType);
event AttributeVerified(uint256 indexed certificateId, string indexed key, string value);
function issueCertificate(uint256 certificateId, address holder, string memory certificateType, uint256 expiryDate, Attribute[] memory attributes) external onlyOwner {
AttributeLib.AttributeStorage storage attrStorage = AttributeLib.attributeStorage();
certificates[certificateId] = Certificate({
certificateId: certificateId,
holder: holder,
issueDate: block.timestamp,
expiryDate: expiryDate,
revoked: false,
certificateType: certificateType
});
AttributeLib._setAttributes(attrStorage.attributes, certificateId, attributes);
emit CertificateIssued(certificateId, holder, certificateType);
}
function updateCertificateAttributes(uint256 certificateId, Attribute[] memory attributes) external onlyOwner {
AttributeLib.AttributeStorage storage attrStorage = AttributeLib.attributeStorage();
AttributeLib._setAttributes(attrStorage.attributes, certificateId, attributes);
}
function verifyCertificateAttribute(uint256 certificateId, string memory key, string memory expectedValue) external view returns (bool) {
AttributeLib.AttributeStorage storage attrStorage = AttributeLib.attributeStorage();
Attribute memory attr = AttributeLib._getAttribute(attrStorage.attributes, certificateId, key);
bool verified = keccak256(abi.encodePacked(attr.value)) == keccak256(abi.encodePacked(expectedValue));
emit AttributeVerified(certificateId, key, attr.value);
return verified;
}
}
Events¶
Attribute Events¶
AttributeSet(uint256 indexed tokenId, string indexed key, AttributeType attributeType, string value): Emitted when an attribute is set or updated.AttributeRemoved(uint256 indexed tokenId, string indexed key): Emitted when an attribute is removed.AttributesSet(uint256 indexed tokenId, uint256 count): Emitted when multiple attributes are set for a token.
Security Considerations¶
Burn Protection¶
- Prevents attribute operations on burned tokens, ensuring data integrity.
Access Control¶
- Attributes can be modified only by authorized entities (e.g., contract owner, designated roles).
Data Integrity¶
- Type checking for attributes helps ensure data consistency.
- String conversion for storage prevents unexpected behavior.
Gas Optimization¶
Storage Efficiency¶
- The
AttributeContractstruct and its internal mappings are designed for efficient storage. - Storage is minimized by packing related data within structs.
Function Efficiency¶
_setAttributesallows batch updates, reducing transaction costs for multiple attribute changes.- Direct storage access in library functions minimizes overhead.
Error Handling¶
Common Errors¶
"Token has been burned": Attempting to modify attributes of a burned token."Attribute not found": Attempting to retrieve or remove a non-existent attribute."Arrays length mismatch": Provided arrays for attributes or values have different lengths.
Best Practices¶
Attribute Design¶
- Define clear attribute schemas and types for your tokens.
- Use meaningful keys for attributes to improve readability and maintainability.
- Consider off-chain storage for large or highly dynamic metadata to save gas.
Development Guidelines¶
- Always implement access control for functions that modify attributes.
- Use comprehensive unit and integration tests to ensure attribute logic is correct.
- Monitor attribute-related events for auditing and analytics.