Skip to content

Commit

Permalink
add natspec
Browse files Browse the repository at this point in the history
  • Loading branch information
jameswenzel authored and GitHub Actions Bot committed Sep 1, 2023
1 parent ef03a75 commit 9340e12
Show file tree
Hide file tree
Showing 4 changed files with 255 additions and 48 deletions.
64 changes: 52 additions & 12 deletions src/dynamic-traits/DynamicTraits.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,48 @@ 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
error TraitValueUnchanged();

bytes32 constant ZERO_VALUE = keccak256("DYNAMIC_TRAITS_ZERO_VALUE");
///@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
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;
bytes32 constant ZERO_VALUE = keccak256("DYNAMIC_TRAITS_ZERO_VALUE");

error TraitValueUnchanged();

function setTrait(bytes32 traitKey, uint256 tokenId, bytes32 trait) external virtual;
function deleteTrait(bytes32 traitKey, uint256 tokenId) external virtual;

function getTraitValue(bytes32 traitKey, uint256 tokenId) external view virtual returns (bytes32) {
/**
* @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
* @param tokenId The token ID to get the trait value for
*/
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;
}
}

/**
* @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
*/
function getTraitValues(bytes32 traitKey, uint256[] calldata tokenIds)
external
view
Expand All @@ -41,34 +59,47 @@ abstract contract DynamicTraits is IERC7496 {
bytes32[] memory result = new bytes32[](length);
for (uint256 i = 0; i < length; i++) {
uint256 tokenId = tokenIds[i];
bytes32 value = _traits[tokenId][traitKey];
if (value == bytes32(0)) {
revert TraitNotSet(tokenId, traitKey);
} else if (value == ZERO_VALUE) {
value = bytes32(0);
} else {
result[i] = value;
}
result[i] = getTraitValue(traitKey, tokenId);
}
return result;
}

/**
* @notice Get the total number of trait keys that have been set
*/
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;
}

/**
* @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
*/
function _setTrait(bytes32 traitKey, uint256 tokenId, bytes32 newTrait) internal {
bytes32 existingValue = _traits[tokenId][traitKey];

Expand All @@ -90,6 +121,11 @@ abstract contract DynamicTraits is IERC7496 {
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)) {
Expand All @@ -100,6 +136,10 @@ abstract contract DynamicTraits is IERC7496 {
emit TraitUpdated(traitKey, tokenId, bytes32(0));
}

/**
* @notice Set the URI for the trait labels
* @param uri The new URI to set
*/
function _setTraitLabelsURI(string calldata uri) internal virtual {
_traitLabelsURI = uri;
emit TraitLabelsURIUpdated(uri);
Expand Down
78 changes: 73 additions & 5 deletions src/dynamic-traits/OnchainTraits.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,42 @@ abstract contract OnchainTraits is Ownable, DynamicTraits {
using EnumerableSet for EnumerableSet.Bytes32Set;
using EnumerableSet for EnumerableSet.AddressSet;

///@notice Thrown when the caller does not have the privilege to set a trait
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;
///@notice an enumerable set of all accounts allowed to edit traits with a "Custom" editor privilege
EnumerableSet.AddressSet internal _customEditors;

constructor() {
_initializeOwner(msg.sender);
}

///@notice a mapping of traitKey to SSTORE2 storage addresses
mapping(bytes32 traitKey => TraitLabelStorage traitLabelStorage) public traitLabelStorage;
EnumerableSet.AddressSet internal _customEditors;

// ABSTRACT

///@notice helper to determine if a given address has the AllowedEditor.TokenOwner privilege
function isOwnerOrApproved(uint256 tokenId, address addr) internal view virtual returns (bool);

// CUSTOM EDITORS

/**
* @notice Check if an address is a custom editor
* @param editor The address to check
*/
function isCustomEditor(address editor) external view returns (bool) {
return _customEditors.contains(editor);
}

/**
* @notice Add or remove an address as a custom editor
* @param editor The address to add or remove
* @param insert Whether to add or remove the address
*/
function updateCustomEditor(address editor, bool insert) external onlyOwner {
if (insert) {
_customEditors.add(editor);
Expand All @@ -53,29 +66,53 @@ abstract contract OnchainTraits is Ownable, DynamicTraits {
}
}

/**
* @notice Get the list of custom editors. This may revert if there are too many editors.
*/
function getCustomEditors() external view returns (address[] memory) {
return _customEditors.values();
}

/**
* @notice Get the number of custom editors
*/
function getCustomEditorsLength() external view returns (uint256) {
return _customEditors.length();
}

/**
* @notice Get the custom editor at a given index
* @param index The index of the custom editor to get
*/
function getCustomEditorAt(uint256 index) external view returns (address) {
return _customEditors.at(index);
}

// LABELS URI

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

/**
* @notice Get the raw JSON for the trait labels
*/
function getTraitLabelsJson() internal view returns (string memory) {
bytes32[] memory keys = _traitKeys.values();
return TraitLabelStorageLib.toLabelJson(traitLabelStorage, keys);
}

/**
* @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
*/
function setTrait(bytes32 traitKey, uint256 tokenId, bytes32 trait) external virtual override {
TraitLabelStorage memory labelStorage = traitLabelStorage[traitKey];
StoredTraitLabel storedTraitLabel = labelStorage.storedLabel;
Expand All @@ -90,6 +127,12 @@ abstract contract OnchainTraits is Ownable, DynamicTraits {
_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;
Expand All @@ -103,10 +146,20 @@ abstract contract OnchainTraits is Ownable, DynamicTraits {
_deleteTrait(traitKey, tokenId);
}

/**
* @notice Set the TraitLabel for a given traitKey. This will overwrite any existing TraitLabel for the traitKey.
* Traits may not be set without a corresponding TraitLabel. OnlyOwner.
* @param traitKey The trait key to set the value of
* @param _traitLabel The trait label to set
*/
function setTraitLabel(bytes32 traitKey, TraitLabel calldata _traitLabel) external virtual onlyOwner {
_setTraitLabel(traitKey, _traitLabel);
}

/**
* @notice Set the TraitLabelStorage for a traitKey. Packs SSTORE2 value along with allowedEditors, required?, and
* valuesRequireValidation? into a single storage slot for more efficient validation when setting trait values.
*/
function _setTraitLabel(bytes32 traitKey, TraitLabel memory _traitLabel) internal virtual {
_traitKeys.add(traitKey);
traitLabelStorage[traitKey] = TraitLabelStorage({
Expand All @@ -117,6 +170,12 @@ abstract contract OnchainTraits is Ownable, DynamicTraits {
});
}

/**
* @notice Checks that the caller has permission to set a trait for a given allowed Editors set and token ID.
* Reverts with InsufficientPrivilege if the caller does not have permission.
* @param editors The allowed editors for this trait
* @param tokenId The token ID the trait is being set for
*/
function _verifySetterPrivilege(Editors editors, uint256 tokenId) internal view {
// anyone
if (EditorsLib.contains(editors, AllowedEditor.Anyone)) {
Expand Down Expand Up @@ -149,15 +208,24 @@ abstract contract OnchainTraits is Ownable, DynamicTraits {
revert InsufficientPrivilege();
}

function _dynamicAttributes(uint256 tokenId) internal view returns (string[] memory) {
/**
* @notice Gets the individual JSON objects for each dynamic trait set on this token by iterating over all
* possible traitKeys and checking if the trait is set on the token. This is extremely inefficient
* and should only be called offchain when rendering metadata.
* @param tokenId The token ID to get the dynamic trait attributes for
* @return An array of JSON objects, each representing a dynamic trait set on the token
*/
function _dynamicAttributes(uint256 tokenId) internal view virtual returns (string[] memory) {
bytes32[] memory keys = _traitKeys.values();
uint256 keysLength = keys.length;

string[] memory attributes = new string[](keysLength);
// keep track of how many traits are actually set
uint256 num;
for (uint256 i = 0; i < keysLength;) {
bytes32 key = keys[i];
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);
Expand Down
Loading

0 comments on commit 9340e12

Please sign in to comment.