Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update dynamic traits lib to latest spec #14

Merged
merged 1 commit into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 36 additions & 93 deletions src/dynamic-traits/DynamicTraits.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,142 +7,85 @@ import {IERC7496} from "./interfaces/IERC7496.sol";
abstract contract DynamicTraits is IERC7496 {
using EnumerableSet for EnumerableSet.Bytes32Set;

///@notice Thrown when trying to delete a trait that has not been set
error TraitNotSet(uint256 tokenId, bytes32 traitKey);
///@notice Thrown when trying to set a trait explicitly to the zero value hash
error TraitCannotBeZeroValueHash();
///@notice Thrown when a new trait value is not different from the existing value
/// @notice Thrown when a new trait value is not different from the existing value
error TraitValueUnchanged();

bytes32 constant ZERO_VALUE = keccak256("DYNAMIC_TRAITS_ZERO_VALUE");
///@notice An enumerable set of all trait keys that have been set
/// @notice An enumerable set of all trait keys that have been set
EnumerableSet.Bytes32Set internal _traitKeys;
///@notice A mapping of token ID to a mapping of trait key to trait value

/// @notice A mapping of token ID to a mapping of trait key to trait value
mapping(uint256 tokenId => mapping(bytes32 traitKey => bytes32 traitValue)) internal _traits;
///@notice An offchain string URI that points to a JSON file containing trait labels
string internal _traitLabelsURI;

function setTrait(bytes32 traitKey, uint256 tokenId, bytes32 trait) external virtual;
function deleteTrait(bytes32 traitKey, uint256 tokenId) external virtual;
/// @notice An offchain string URI that points to a JSON file containing trait metadata
string internal _traitMetadataURI;

/**
* @notice Get the value of a trait for a given token ID. Reverts if the trait is not set.
* @param traitKey The trait key to get the value of
* @notice Get the value of a trait for a given token ID.
* @param tokenId The token ID to get the trait value for
* @param traitKey The trait key to get the value of
*/
function getTraitValue(bytes32 traitKey, uint256 tokenId) public view virtual returns (bytes32) {
bytes32 value = _traits[tokenId][traitKey];
// Revert if the trait is not set
if (value == bytes32(0)) {
revert TraitNotSet(tokenId, traitKey);
} else if (value == ZERO_VALUE) {
// check for zero value hash; return 0 if so
return bytes32(0);
} else {
// otherwise return normal value
return value;
}
function getTraitValue(uint256 tokenId, bytes32 traitKey) public view virtual returns (bytes32 traitValue) {
traitValue = _traits[tokenId][traitKey];
}

/**
* @notice Get the values of a trait for a given list of token IDs. Reverts if the trait is not set on any single token.
* @param traitKey The trait key to get the value of
* @param tokenIds The token IDs to get the trait values for
* @notice Get the values of traits for a given token ID.
* @param tokenId The token ID to get the trait values for
* @param traitKeys The trait keys to get the values of
*/
function getTraitValues(bytes32 traitKey, uint256[] calldata tokenIds)
external
function getTraitValues(uint256 tokenId, bytes32[] calldata traitKeys)
public
view
virtual
returns (bytes32[] memory traitValues)
{
uint256 length = tokenIds.length;
bytes32[] memory result = new bytes32[](length);
for (uint256 i = 0; i < length; i++) {
uint256 tokenId = tokenIds[i];
result[i] = getTraitValue(traitKey, tokenId);
uint256 length = traitKeys.length;
traitValues = new bytes32[](length);
for (uint256 i = 0; i < length;) {
bytes32 traitKey = traitKeys[i];
traitValues[i] = getTraitValue(tokenId, traitKey);
unchecked {
++i;
}
}
return result;
}

/**
* @notice Get the total number of trait keys that have been set
* @notice Get the URI for the trait metadata
*/
function getTotalTraitKeys() external view virtual returns (uint256) {
return _traitKeys.length();
}

/**
* @notice Get the trait key at a given index
* @param index The index of the trait key to get
*/
function getTraitKeyAt(uint256 index) external view virtual returns (bytes32 traitKey) {
return _traitKeys.at(index);
}

/**
* @notice Get the trait keys that have been set. May revert if there are too many trait keys.
*/
function getTraitKeys() external view virtual returns (bytes32[] memory traitKeys) {
return _traitKeys._inner._values;
}

/**
* @notice Get the URI for the trait labels
*/
function getTraitLabelsURI() external view virtual returns (string memory labelsURI) {
return _traitLabelsURI;
function getTraitMetadataURI() external view virtual returns (string memory labelsURI) {
return _traitMetadataURI;
}

/**
* @notice Set the value of a trait for a given token ID. If newTrait is bytes32(0), sets the zero value hash.
* Reverts if the trait value is the zero value hash.
* @param traitKey The trait key to set the value of
* @param tokenId The token ID to set the trait value for
* @param newTrait The new trait value to set
* @param traitKey The trait key to set the value of
* @param newValue The new trait value to set
*/
function _setTrait(bytes32 traitKey, uint256 tokenId, bytes32 newTrait) internal {
function _setTrait(uint256 tokenId, bytes32 traitKey, bytes32 newValue) internal {
bytes32 existingValue = _traits[tokenId][traitKey];

if (newTrait == bytes32(0)) {
newTrait = ZERO_VALUE;
} else if (newTrait == ZERO_VALUE) {
revert InvalidTraitValue(traitKey, newTrait);
}

if (existingValue == newTrait) {
if (existingValue == newValue) {
revert TraitValueUnchanged();
}

// no-op if exists
_traitKeys.add(traitKey);

_traits[tokenId][traitKey] = newTrait;

emit TraitUpdated(traitKey, tokenId, newTrait);
}

/**
* @notice Delete the value of a trait for a given token ID.
* @param traitKey The trait key to delete the value of
* @param tokenId The token ID to delete the trait value for
*/
function _deleteTrait(bytes32 traitKey, uint256 tokenId) internal {
bytes32 existingValue = _traits[tokenId][traitKey];
if (existingValue == bytes32(0)) {
revert TraitValueUnchanged();
}
_traits[tokenId][traitKey] = newValue;

_traits[tokenId][traitKey] = bytes32(0);
emit TraitUpdated(traitKey, tokenId, bytes32(0));
emit TraitUpdated(traitKey, tokenId, newValue);
}

/**
* @notice Set the URI for the trait labels
* @notice Set the URI for the trait metadata
* @param uri The new URI to set
*/
function _setTraitLabelsURI(string calldata uri) internal virtual {
_traitLabelsURI = uri;
emit TraitLabelsURIUpdated(uri);
function _setTraitMetadataURI(string calldata uri) internal virtual {
_traitMetadataURI = uri;
emit TraitMetadataURIUpdated();
}

function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
Expand Down
39 changes: 32 additions & 7 deletions src/dynamic-traits/ERC721DynamicTraits.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,44 @@ import {DynamicTraits} from "./DynamicTraits.sol";

contract ERC721DynamicTraits is DynamicTraits, Ownable, ERC721 {
constructor() Ownable(msg.sender) ERC721("ERC721DynamicTraits", "ERC721DT") {
_traitLabelsURI = "https://example.com";
_traitMetadataURI = "https://example.com";
}

function setTrait(bytes32 traitKey, uint256 tokenId, bytes32 value) external virtual override onlyOwner {
_setTrait(traitKey, tokenId, value);
function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 value) external virtual override onlyOwner {
// Revert if the token doesn't exist.
_requireOwned(tokenId);

_setTrait(tokenId, traitKey, value);
}

function deleteTrait(bytes32 traitKey, uint256 tokenId) external virtual override onlyOwner {
_deleteTrait(traitKey, tokenId);
function getTraitValue(uint256 tokenId, bytes32 traitKey)
public
view
virtual
override
returns (bytes32 traitValue)
{
// Revert if the token doesn't exist.
_requireOwned(tokenId);

return DynamicTraits.getTraitValue(tokenId, traitKey);
}

function getTraitValues(uint256 tokenId, bytes32[] calldata traitKeys)
public
view
virtual
override
returns (bytes32[] memory traitValues)
{
// Revert if the token doesn't exist.
_requireOwned(tokenId);

return DynamicTraits.getTraitValues(tokenId, traitKeys);
}

function setTraitLabelsURI(string calldata uri) external onlyOwner {
_setTraitLabelsURI(uri);
function setTraitMetadataURI(string calldata uri) external onlyOwner {
_setTraitMetadataURI(uri);
}

function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, DynamicTraits) returns (bool) {
Expand Down
35 changes: 34 additions & 1 deletion src/dynamic-traits/ERC721OnchainTraits.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,40 @@ import {DynamicTraits} from "./DynamicTraits.sol";

contract ERC721OnchainTraits is OnchainTraits, ERC721 {
constructor() ERC721("ERC721DynamicTraits", "ERC721DT") {
_traitLabelsURI = "https://example.com";
_traitMetadataURI = "https://example.com";
}

function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 value) external virtual override onlyOwner {
// Revert if the token doesn't exist.
_requireOwned(tokenId);

_setTrait(tokenId, traitKey, value);
}

function getTraitValue(uint256 tokenId, bytes32 traitKey)
public
view
virtual
override
returns (bytes32 traitValue)
{
// Revert if the token doesn't exist.
_requireOwned(tokenId);

return DynamicTraits.getTraitValue(tokenId, traitKey);
}

function getTraitValues(uint256 tokenId, bytes32[] calldata traitKeys)
public
view
virtual
override
returns (bytes32[] memory traitValues)
{
// Revert if the token doesn't exist.
_requireOwned(tokenId);

return DynamicTraits.getTraitValues(tokenId, traitKeys);
}

function _isOwnerOrApproved(uint256 tokenId, address addr) internal view virtual override returns (bool) {
Expand Down
44 changes: 10 additions & 34 deletions src/dynamic-traits/OnchainTraits.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ abstract contract OnchainTraits is Ownable, DynamicTraits {
error InsufficientPrivilege();
///@notice Thrown when trying to set a trait that does not exist
error TraitDoesNotExist(bytes32 traitKey);
///@notice Thrown when trying to delete a trait that is required to have a value.
error TraitIsRequired();

///@notice a mapping of traitKey to TraitLabelStorage metadata
mapping(bytes32 traitKey => TraitLabelStorage traitLabelStorage) public traitLabelStorage;
Expand Down Expand Up @@ -91,16 +89,16 @@ abstract contract OnchainTraits is Ownable, DynamicTraits {
// LABELS URI

/**
* @notice Get the onchain URI for the trait labels, encoded as a JSON data URI
* @notice Get the onchain URI for the trait metadata, encoded as a JSON data URI
*/
function getTraitLabelsURI() external view virtual override returns (string memory) {
return Metadata.jsonDataURI(_getTraitLabelsJson());
function getTraitMetadataURI() external view virtual override returns (string memory) {
return Metadata.jsonDataURI(_getTraitMetadataJson());
}

/**
* @notice Get the raw JSON for the trait labels
* @notice Get the raw JSON for the trait metadata
*/
function _getTraitLabelsJson() internal view returns (string memory) {
function _getTraitMetadataJson() internal view returns (string memory) {
bytes32[] memory keys = _traitKeys.values();
return TraitLabelStorageLib.toLabelJson(traitLabelStorage, keys);
}
Expand All @@ -109,11 +107,11 @@ abstract contract OnchainTraits is Ownable, DynamicTraits {
* @notice Set a trait for a given traitKey and tokenId. Checks that the caller has permission to set the trait,
* and, if the TraitLabel specifies that the trait value must be validated, checks that the trait value
* is valid.
* @param traitKey The trait key to get the value of
* @param tokenId The token ID to get the trait value for
* @param trait The trait value
* @param traitKey The trait key to get the value of
* @param newValue The new trait value
*/
function setTrait(bytes32 traitKey, uint256 tokenId, bytes32 trait) external virtual override {
function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 newValue) external virtual override {
TraitLabelStorage memory labelStorage = traitLabelStorage[traitKey];
StoredTraitLabel storedTraitLabel = labelStorage.storedLabel;
if (!StoredTraitLabelLib.exists(storedTraitLabel)) {
Expand All @@ -122,28 +120,9 @@ abstract contract OnchainTraits is Ownable, DynamicTraits {
_verifySetterPrivilege(labelStorage.allowedEditors, tokenId);

if (labelStorage.valuesRequireValidation) {
TraitLabelLib.validateAcceptableValue(StoredTraitLabelLib.load(storedTraitLabel), traitKey, trait);
}
_setTrait(traitKey, tokenId, trait);
}

/**
* @notice Delete a trait for a given traitKey and tokenId. Checks that the caller has permission to delete the trait,
* and that the trait is not required to have a value.
* @param traitKey The trait key to delete the value of
* @param tokenId The token ID to delete the trait value for
*/
function deleteTrait(bytes32 traitKey, uint256 tokenId) external virtual override {
TraitLabelStorage memory labelStorage = traitLabelStorage[traitKey];
StoredTraitLabel storedTraitLabel = labelStorage.storedLabel;
if (!StoredTraitLabelLib.exists(storedTraitLabel)) {
revert TraitDoesNotExist(traitKey);
TraitLabelLib.validateAcceptableValue(StoredTraitLabelLib.load(storedTraitLabel), traitKey, newValue);
}
_verifySetterPrivilege(labelStorage.allowedEditors, tokenId);
if (labelStorage.required) {
revert TraitIsRequired();
}
_deleteTrait(traitKey, tokenId);
_setTrait(tokenId, traitKey, newValue);
}

/**
Expand Down Expand Up @@ -227,9 +206,6 @@ abstract contract OnchainTraits is Ownable, DynamicTraits {
bytes32 trait = _traits[tokenId][key];
// check that the trait is set, otherwise, skip it
if (trait != bytes32(0)) {
if (trait == ZERO_VALUE) {
trait = bytes32(0);
}
attributes[num] = TraitLabelStorageLib.toAttributeJson(traitLabelStorage, key, trait);
unchecked {
++num;
Expand Down
18 changes: 6 additions & 12 deletions src/dynamic-traits/interfaces/IERC7496.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,20 @@ pragma solidity ^0.8.19;
import {IERC165} from "forge-std/interfaces/IERC165.sol";

interface IERC7496 is IERC165 {
error InvalidTraitValue(bytes32 traitKey, bytes32 traitValue);

/* Events */
event TraitUpdated(bytes32 indexed traitKey, uint256 indexed tokenId, bytes32 trait);
event TraitUpdatedBulkConsecutive(bytes32 indexed traitKeyPattern, uint256 fromTokenId, uint256 toTokenId);
event TraitUpdatedBulkRange(bytes32 indexed traitKeyPattern, uint256 fromTokenId, uint256 toTokenId);
event TraitUpdatedBulkList(bytes32 indexed traitKeyPattern, uint256[] tokenIds);
event TraitLabelsURIUpdated(string uri);
event TraitMetadataURIUpdated();

/* Getters */
function getTraitValue(bytes32 traitKey, uint256 tokenId) external view returns (bytes32 traitValue);
function getTraitValues(bytes32 traitKey, uint256[] calldata tokenIds)
function getTraitValue(uint256 tokenId, bytes32 traitKey) external view returns (bytes32 traitValue);
function getTraitValues(uint256 tokenId, bytes32[] calldata traitKeys)
external
view
returns (bytes32[] memory traitValues);
function getTotalTraitKeys() external view returns (uint256);
function getTraitKeys() external view returns (bytes32[] memory traitKeys);
function getTraitKeyAt(uint256 index) external view returns (bytes32 traitKey);
function getTraitLabelsURI() external view returns (string memory labelsURI);
function getTraitMetadataURI() external view returns (string memory uri);

/* Setters */
function setTrait(bytes32 traitKey, uint256 tokenId, bytes32 value) external;
function deleteTrait(bytes32 traitKey, uint256 tokenId) external;
function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 value) external;
}
Loading
Loading