diff --git a/script/EmitDynamicTraitsTestEvents.s.sol b/script/EmitDynamicTraitsTestEvents.s.sol new file mode 100644 index 0000000..9ac521a --- /dev/null +++ b/script/EmitDynamicTraitsTestEvents.s.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Script} from "forge-std/Script.sol"; +import {ERC721DynamicTraitsMultiUpdate} from "src/dynamic-traits/test/ERC721DynamicTraitsMultiUpdate.sol"; +import {Solarray} from "solarray/Solarray.sol"; + +contract EmitDynamicTraitTestEvents is Script { + function run() public { + ERC721DynamicTraitsMultiUpdate token = new ERC721DynamicTraitsMultiUpdate(); + + bytes32 key = bytes32("testKey"); + bytes32 value = bytes32("foo"); + + // Emit TraitUpdated + token.mint(address(this), 0); + token.setTrait(0, key, value); + + // Emit TraitUpdatedRange + uint256 fromTokenId = 1; + uint256 toTokenId = 10; + bytes32[] memory values = new bytes32[](10); + for (uint256 i = 0; i < values.length; i++) { + values[i] = bytes32(i); + } + for (uint256 tokenId = fromTokenId; tokenId <= toTokenId; tokenId++) { + token.mint(address(this), tokenId); + } + token.setTraitsRangeDifferentValues(fromTokenId, toTokenId, key, values); + + // Emit TraitUpdatedRangeUniformValue + token.setTraitsRange(fromTokenId, toTokenId, key, value); + + // Emit TraitUpdatedList + uint256[] memory tokenIds = Solarray.uint256s(100, 75, 20, 50); + values = new bytes32[](tokenIds.length); + for (uint256 i = 0; i < values.length; i++) { + values[i] = bytes32(i * 1000); + } + for (uint256 i = 0; i < tokenIds.length; i++) { + token.mint(address(this), tokenIds[i]); + } + token.setTraitsListDifferentValues(tokenIds, key, values); + + // Emit TraitUpdatedListUniformValue + token.setTraitsList(tokenIds, key, value); + + // Emit TraitMetadataURIUpdated + token.setTraitMetadataURI("http://example.com/1"); + } +} diff --git a/src/dynamic-traits/DynamicTraits.sol b/src/dynamic-traits/DynamicTraits.sol index 26014a0..edeb0a5 100644 --- a/src/dynamic-traits/DynamicTraits.sol +++ b/src/dynamic-traits/DynamicTraits.sol @@ -1,23 +1,38 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.19; -import {EnumerableSet} from "openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol"; import {IERC7496} from "./interfaces/IERC7496.sol"; -contract DynamicTraits is IERC7496 { - using EnumerableSet for EnumerableSet.Bytes32Set; - - /// @notice Thrown when a new trait value is not different from the existing value - error TraitValueUnchanged(); +library DynamicTraitsStorage { + struct Layout { + /// @dev A mapping of token ID to a mapping of trait key to trait value. + mapping(uint256 tokenId => mapping(bytes32 traitKey => bytes32 traitValue)) _traits; + /// @dev An offchain string URI that points to a JSON file containing trait metadata. + string _traitMetadataURI; + } - /// @notice An enumerable set of all trait keys that have been set - EnumerableSet.Bytes32Set internal _traitKeys; + bytes32 internal constant STORAGE_SLOT = keccak256("contracts.storage.erc7496-dynamictraits"); - /// @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; + function layout() internal pure returns (Layout storage l) { + bytes32 slot = STORAGE_SLOT; + assembly { + l.slot := slot + } + } +} - /// @notice An offchain string URI that points to a JSON file containing trait metadata - string internal _traitMetadataURI; +/** + * @title DynamicTraits + * + * @dev Implementation of [ERC-7496](https://eips.ethereum.org/EIPS/eip-7496) Dynamic Traits. + * Uses a storage layout pattern for upgradeable contracts. + * + * Requirements: + * - Overwrite `setTrait` with access role restriction. + * - Expose a function for `setTraitMetadataURI` with access role restriction if desired. + */ +contract DynamicTraits is IERC7496 { + using DynamicTraitsStorage for DynamicTraitsStorage.Layout; /** * @notice Get the value of a trait for a given token ID. @@ -25,7 +40,8 @@ contract DynamicTraits is IERC7496 { * @param traitKey The trait key to get the value of */ function getTraitValue(uint256 tokenId, bytes32 traitKey) public view virtual returns (bytes32 traitValue) { - traitValue = _traits[tokenId][traitKey]; + // Return the trait value. + return DynamicTraitsStorage.layout()._traits[tokenId][traitKey]; } /** @@ -39,8 +55,11 @@ contract DynamicTraits is IERC7496 { virtual returns (bytes32[] memory traitValues) { + // Set the length of the traitValues return array. uint256 length = traitKeys.length; traitValues = new bytes32[](length); + + // Assign each trait value to the corresopnding key. for (uint256 i = 0; i < length;) { bytes32 traitKey = traitKeys[i]; traitValues[i] = getTraitValue(tokenId, traitKey); @@ -54,7 +73,8 @@ contract DynamicTraits is IERC7496 { * @notice Get the URI for the trait metadata */ function getTraitMetadataURI() external view virtual returns (string memory labelsURI) { - return _traitMetadataURI; + // Return the trait metadata URI. + return DynamicTraitsStorage.layout()._traitMetadataURI; } /** @@ -66,29 +86,45 @@ contract DynamicTraits is IERC7496 { * @param newValue The new trait value to set */ function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 newValue) public virtual { - bytes32 existingValue = _traits[tokenId][traitKey]; - + // Revert if the new value is the same as the existing value. + bytes32 existingValue = DynamicTraitsStorage.layout()._traits[tokenId][traitKey]; if (existingValue == newValue) { revert TraitValueUnchanged(); } - // no-op if exists - _traitKeys.add(traitKey); - - _traits[tokenId][traitKey] = newValue; + // Set the new trait value. + _setTrait(tokenId, traitKey, newValue); + // Emit the event noting the update. emit TraitUpdated(traitKey, tokenId, newValue); } /** - * @notice Set the URI for the trait metadata - * @param uri The new URI to set + * @notice Set the trait value (without emitting an event). + * @param tokenId The token ID to set the trait value for + * @param traitKey The trait key to set the value of + * @param newValue The new trait value to set + */ + function _setTrait(uint256 tokenId, bytes32 traitKey, bytes32 newValue) internal virtual { + // Set the new trait value. + DynamicTraitsStorage.layout()._traits[tokenId][traitKey] = newValue; + } + + /** + * @notice Set the URI for the trait metadata. + * @param uri The new URI to set. */ - function _setTraitMetadataURI(string calldata uri) internal virtual { - _traitMetadataURI = uri; + function _setTraitMetadataURI(string memory uri) internal virtual { + // Set the new trait metadata URI. + DynamicTraitsStorage.layout()._traitMetadataURI = uri; + + // Emit the event noting the update. emit TraitMetadataURIUpdated(); } + /** + * @dev See {IERC165-supportsInterface}. + */ function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { return interfaceId == type(IERC7496).interfaceId; } diff --git a/src/dynamic-traits/ERC721DynamicTraits.sol b/src/dynamic-traits/ERC721DynamicTraits.sol index 0a41074..9b22705 100644 --- a/src/dynamic-traits/ERC721DynamicTraits.sol +++ b/src/dynamic-traits/ERC721DynamicTraits.sol @@ -1,19 +1,20 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.19; import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; import {Ownable} from "openzeppelin-contracts/access/Ownable.sol"; -import {DynamicTraits} from "./DynamicTraits.sol"; +import {DynamicTraits} from "src/dynamic-traits/DynamicTraits.sol"; contract ERC721DynamicTraits is DynamicTraits, Ownable, ERC721 { constructor() Ownable(msg.sender) ERC721("ERC721DynamicTraits", "ERC721DT") { - _traitMetadataURI = "https://example.com"; + _setTraitMetadataURI("https://example.com"); } function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 value) public virtual override onlyOwner { // Revert if the token doesn't exist. _requireOwned(tokenId); + // Call the internal function to set the trait. DynamicTraits.setTrait(tokenId, traitKey, value); } @@ -27,6 +28,7 @@ contract ERC721DynamicTraits is DynamicTraits, Ownable, ERC721 { // Revert if the token doesn't exist. _requireOwned(tokenId); + // Call the internal function to get the trait value. return DynamicTraits.getTraitValue(tokenId, traitKey); } @@ -40,10 +42,12 @@ contract ERC721DynamicTraits is DynamicTraits, Ownable, ERC721 { // Revert if the token doesn't exist. _requireOwned(tokenId); + // Call the internal function to get the trait values. return DynamicTraits.getTraitValues(tokenId, traitKeys); } function setTraitMetadataURI(string calldata uri) external onlyOwner { + // Set the new metadata URI. _setTraitMetadataURI(uri); } diff --git a/src/dynamic-traits/ERC721OnchainTraits.sol b/src/dynamic-traits/ERC721OnchainTraits.sol index d4ca9c1..4dc96ab 100644 --- a/src/dynamic-traits/ERC721OnchainTraits.sol +++ b/src/dynamic-traits/ERC721OnchainTraits.sol @@ -7,13 +7,14 @@ import {DynamicTraits} from "./DynamicTraits.sol"; contract ERC721OnchainTraits is OnchainTraits, ERC721 { constructor() ERC721("ERC721DynamicTraits", "ERC721DT") { - _traitMetadataURI = "https://example.com"; + _setTraitMetadataURI("https://example.com"); } function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 value) public virtual override onlyOwner { // Revert if the token doesn't exist. _requireOwned(tokenId); + // Call the internal function to set the trait. DynamicTraits.setTrait(tokenId, traitKey, value); } @@ -27,6 +28,7 @@ contract ERC721OnchainTraits is OnchainTraits, ERC721 { // Revert if the token doesn't exist. _requireOwned(tokenId); + // Call the internal function to get the trait value. return DynamicTraits.getTraitValue(tokenId, traitKey); } @@ -40,10 +42,12 @@ contract ERC721OnchainTraits is OnchainTraits, ERC721 { // Revert if the token doesn't exist. _requireOwned(tokenId); + // Call the internal function to get the trait values. return DynamicTraits.getTraitValues(tokenId, traitKeys); } function _isOwnerOrApproved(uint256 tokenId, address addr) internal view virtual override returns (bool) { + // Return if the address is owner or an approved operator for the token. return addr == ownerOf(tokenId) || isApprovedForAll(ownerOf(tokenId), addr) || getApproved(tokenId) == addr; } diff --git a/src/dynamic-traits/OnchainTraits.sol b/src/dynamic-traits/OnchainTraits.sol index 8fc4b33..d9c82cf 100644 --- a/src/dynamic-traits/OnchainTraits.sol +++ b/src/dynamic-traits/OnchainTraits.sol @@ -18,27 +18,43 @@ import { StoredTraitLabelLib } from "./lib/TraitLabelLib.sol"; +library OnchainTraitsStorage { + struct Layout { + /// @notice An enumerable set of all trait keys that have been set. + EnumerableSet.Bytes32Set _traitKeys; + /// @notice A mapping of traitKey to OnchainTraitsStorage.layout()._traitLabelStorage metadata. + mapping(bytes32 traitKey => TraitLabelStorage traitLabelStorage) _traitLabelStorage; + /// @notice An enumerable set of all accounts allowed to edit traits with a "Custom" editor privilege. + EnumerableSet.AddressSet _customEditors; + } + + bytes32 internal constant STORAGE_SLOT = keccak256("contracts.storage.erc7496-dynamictraits.onchaintraits"); + + function layout() internal pure returns (Layout storage l) { + bytes32 slot = STORAGE_SLOT; + assembly { + l.slot := slot + } + } +} + abstract contract OnchainTraits is Ownable, DynamicTraits { + using OnchainTraitsStorage for OnchainTraitsStorage.Layout; using EnumerableSet for EnumerableSet.Bytes32Set; using EnumerableSet for EnumerableSet.AddressSet; - ///@notice Thrown when the caller does not have the privilege to set a trait + /// @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 + /// @notice Thrown when trying to set a trait that does not exist error TraitDoesNotExist(bytes32 traitKey); - ///@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); } // ABSTRACT - ///@notice helper to determine if a given address has the AllowedEditor.TokenOwner privilege + /// @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 @@ -48,7 +64,7 @@ abstract contract OnchainTraits is Ownable, DynamicTraits { * @param editor The address to check */ function isCustomEditor(address editor) external view returns (bool) { - return _customEditors.contains(editor); + return OnchainTraitsStorage.layout()._customEditors.contains(editor); } /** @@ -58,9 +74,9 @@ abstract contract OnchainTraits is Ownable, DynamicTraits { */ function updateCustomEditor(address editor, bool insert) external onlyOwner { if (insert) { - _customEditors.add(editor); + OnchainTraitsStorage.layout()._customEditors.add(editor); } else { - _customEditors.remove(editor); + OnchainTraitsStorage.layout()._customEditors.remove(editor); } } @@ -68,14 +84,14 @@ 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(); + return OnchainTraitsStorage.layout()._customEditors.values(); } /** * @notice Get the number of custom editors */ function getCustomEditorsLength() external view returns (uint256) { - return _customEditors.length(); + return OnchainTraitsStorage.layout()._customEditors.length(); } /** @@ -83,7 +99,7 @@ abstract contract OnchainTraits is Ownable, DynamicTraits { * @param index The index of the custom editor to get */ function getCustomEditorAt(uint256 index) external view returns (address) { - return _customEditors.at(index); + return OnchainTraitsStorage.layout()._customEditors.at(index); } // LABELS URI @@ -99,8 +115,15 @@ abstract contract OnchainTraits is Ownable, DynamicTraits { * @notice Get the raw JSON for the trait metadata */ function _getTraitMetadataJson() internal view returns (string memory) { - bytes32[] memory keys = _traitKeys.values(); - return TraitLabelStorageLib.toLabelJson(traitLabelStorage, keys); + bytes32[] memory keys = OnchainTraitsStorage.layout()._traitKeys.values(); + return TraitLabelStorageLib.toLabelJson(OnchainTraitsStorage.layout()._traitLabelStorage, keys); + } + + /** + * @notice Return trait label storage information at a given key. + */ + function traitLabelStorage(bytes32 traitKey) external view returns (TraitLabelStorage memory) { + return OnchainTraitsStorage.layout()._traitLabelStorage[traitKey]; } /** @@ -112,7 +135,7 @@ abstract contract OnchainTraits is Ownable, DynamicTraits { * @param newValue The new trait value */ function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 newValue) public virtual override { - TraitLabelStorage memory labelStorage = traitLabelStorage[traitKey]; + TraitLabelStorage memory labelStorage = OnchainTraitsStorage.layout()._traitLabelStorage[traitKey]; StoredTraitLabel storedTraitLabel = labelStorage.storedLabel; if (!StoredTraitLabelLib.exists(storedTraitLabel)) { revert TraitDoesNotExist(traitKey); @@ -136,12 +159,12 @@ abstract contract OnchainTraits is Ownable, DynamicTraits { } /** - * @notice Set the TraitLabelStorage for a traitKey. Packs SSTORE2 value along with allowedEditors, required?, and + * @notice Set the OnchainTraitsStorage.layout()._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({ + OnchainTraitsStorage.layout()._traitKeys.add(traitKey); + OnchainTraitsStorage.layout()._traitLabelStorage[traitKey] = TraitLabelStorage({ allowedEditors: _traitLabel.editors, required: _traitLabel.required, valuesRequireValidation: _traitLabel.acceptableValues.length > 0, @@ -171,7 +194,7 @@ abstract contract OnchainTraits is Ownable, DynamicTraits { } // customEditor if (EditorsLib.contains(editors, AllowedEditor.Custom)) { - if (_customEditors.contains(msg.sender)) { + if (OnchainTraitsStorage.layout()._customEditors.contains(msg.sender)) { // short circuit return; } @@ -195,7 +218,7 @@ abstract contract OnchainTraits is Ownable, DynamicTraits { * @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(); + bytes32[] memory keys = OnchainTraitsStorage.layout()._traitKeys.values(); uint256 keysLength = keys.length; string[] memory attributes = new string[](keysLength); @@ -203,10 +226,11 @@ abstract contract OnchainTraits is Ownable, DynamicTraits { uint256 num; for (uint256 i = 0; i < keysLength;) { bytes32 key = keys[i]; - bytes32 trait = _traits[tokenId][key]; + bytes32 trait = getTraitValue(tokenId, key); // check that the trait is set, otherwise, skip it if (trait != bytes32(0)) { - attributes[num] = TraitLabelStorageLib.toAttributeJson(traitLabelStorage, key, trait); + attributes[num] = + TraitLabelStorageLib.toAttributeJson(OnchainTraitsStorage.layout()._traitLabelStorage, key, trait); unchecked { ++num; } diff --git a/src/dynamic-traits/interfaces/IERC7496.sol b/src/dynamic-traits/interfaces/IERC7496.sol index 130fa92..7e425b0 100644 --- a/src/dynamic-traits/interfaces/IERC7496.sol +++ b/src/dynamic-traits/interfaces/IERC7496.sol @@ -1,13 +1,15 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.19; -import {IERC165} from "forge-std/interfaces/IERC165.sol"; - -interface IERC7496 is IERC165 { +interface IERC7496 { /* Events */ event TraitUpdated(bytes32 indexed traitKey, uint256 tokenId, bytes32 trait); - event TraitUpdatedBulkRange(bytes32 indexed traitKey, uint256 fromTokenId, uint256 toTokenId, bytes32 traitValue); - event TraitUpdatedBulkList(bytes32 indexed traitKey, uint256[] tokenIds, bytes32 traitValue); + event TraitUpdatedRange(bytes32 indexed traitKey, uint256 fromTokenId, uint256 toTokenId); + event TraitUpdatedRangeUniformValue( + bytes32 indexed traitKey, uint256 fromTokenId, uint256 toTokenId, bytes32 traitValue + ); + event TraitUpdatedList(bytes32 indexed traitKey, uint256[] tokenIds); + event TraitUpdatedListUniformValue(bytes32 indexed traitKey, uint256[] tokenIds, bytes32 traitValue); event TraitMetadataURIUpdated(); /* Getters */ @@ -20,4 +22,7 @@ interface IERC7496 is IERC165 { /* Setters */ function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 value) external; + + /* Errors */ + error TraitValueUnchanged(); } diff --git a/src/dynamic-traits/test/ERC721DynamicTraitsMultiUpdate.sol b/src/dynamic-traits/test/ERC721DynamicTraitsMultiUpdate.sol new file mode 100644 index 0000000..180cf4b --- /dev/null +++ b/src/dynamic-traits/test/ERC721DynamicTraitsMultiUpdate.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.19; + +import {ERC721DynamicTraits} from "src/dynamic-traits/ERC721DynamicTraits.sol"; + +contract ERC721DynamicTraitsMultiUpdate is ERC721DynamicTraits { + constructor() ERC721DynamicTraits() {} + + function setTraitsRange(uint256 fromTokenId, uint256 toTokenId, bytes32 traitKey, bytes32 value) + public + virtual + onlyOwner + { + for (uint256 tokenId = fromTokenId; tokenId <= toTokenId;) { + // Revert if the token doesn't exist. + _requireOwned(tokenId); + + // Call the internal function to set the trait. + _setTrait(tokenId, traitKey, value); + + unchecked { + ++tokenId; + } + } + + // Emit the event noting the update. + emit TraitUpdatedRangeUniformValue(traitKey, fromTokenId, toTokenId, value); + } + + function setTraitsRangeDifferentValues( + uint256 fromTokenId, + uint256 toTokenId, + bytes32 traitKey, + bytes32[] calldata values + ) public virtual onlyOwner { + for (uint256 tokenId = fromTokenId; tokenId <= toTokenId;) { + // Revert if the token doesn't exist. + _requireOwned(tokenId); + + // Call the internal function to set the trait. + _setTrait(tokenId, traitKey, values[tokenId - 1]); + + unchecked { + ++tokenId; + } + } + + // Emit the event noting the update. + emit TraitUpdatedRange(traitKey, fromTokenId, toTokenId); + } + + function setTraitsList(uint256[] calldata tokenIds, bytes32 traitKey, bytes32 value) public virtual onlyOwner { + for (uint256 i = 0; i < tokenIds.length;) { + // Revert if the token doesn't exist. + _requireOwned(tokenIds[i]); + + // Call the internal function to set the trait. + _setTrait(tokenIds[i], traitKey, value); + + unchecked { + ++i; + } + } + + // Emit the event noting the update. + emit TraitUpdatedListUniformValue(traitKey, tokenIds, value); + } + + function setTraitsListDifferentValues(uint256[] calldata tokenIds, bytes32 traitKey, bytes32[] calldata values) + public + virtual + onlyOwner + { + for (uint256 i = 0; i < tokenIds.length;) { + // Revert if the token doesn't exist. + _requireOwned(tokenIds[i]); + + // Call the internal function to set the trait. + _setTrait(tokenIds[i], traitKey, values[i]); + + unchecked { + ++i; + } + } + + // Emit the event noting the update. + emit TraitUpdatedList(traitKey, tokenIds); + } + + function mint(address to, uint256 tokenId) public onlyOwner { + _mint(to, tokenId); + } +} diff --git a/test/dynamic-traits/ERC721DynamicTraits.t.sol b/test/dynamic-traits/ERC721DynamicTraits.t.sol index 2857f2f..33febda 100644 --- a/test/dynamic-traits/ERC721DynamicTraits.t.sol +++ b/test/dynamic-traits/ERC721DynamicTraits.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.19; import "forge-std/Test.sol"; @@ -6,7 +6,7 @@ import {IERC721Errors} from "openzeppelin-contracts/contracts/interfaces/draft-I import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; import {IERC7496} from "src/dynamic-traits/interfaces/IERC7496.sol"; -import {ERC721DynamicTraits, DynamicTraits} from "src/dynamic-traits/ERC721DynamicTraits.sol"; +import {ERC721DynamicTraits} from "src/dynamic-traits/ERC721DynamicTraits.sol"; import {Solarray} from "solarray/Solarray.sol"; contract ERC721DynamicTraitsMintable is ERC721DynamicTraits { @@ -21,11 +21,7 @@ contract ERC721DynamicTraitsTest is Test { ERC721DynamicTraitsMintable token; /* Events */ - event TraitUpdated(bytes32 indexed traitKey, uint256 tokenId, bytes32 traitValue); - event TraitUpdatedBulkConsecutive( - bytes32 indexed traitKeyPattern, uint256 fromTokenId, uint256 toTokenId, bytes32 traitValue - ); - event TraitUpdatedBulkList(bytes32 indexed traitKeyPattern, uint256[] tokenIds, bytes32 traitValue); + event TraitUpdated(bytes32 indexed traitKey, uint256 tokenId, bytes32 trait); event TraitMetadataURIUpdated(); function setUp() public { @@ -37,7 +33,7 @@ contract ERC721DynamicTraitsTest is Test { } function testReturnsValueSet() public { - bytes32 key = bytes32("test.key"); + bytes32 key = bytes32("testKey"); bytes32 value = bytes32("foo"); uint256 tokenId = 12345; token.mint(address(this), tokenId); @@ -58,19 +54,19 @@ contract ERC721DynamicTraitsTest is Test { } function testSetTrait_Unchanged() public { - bytes32 key = bytes32("test.key"); + bytes32 key = bytes32("testKey"); bytes32 value = bytes32("foo"); uint256 tokenId = 1; token.mint(address(this), tokenId); token.setTrait(tokenId, key, value); - vm.expectRevert(DynamicTraits.TraitValueUnchanged.selector); + vm.expectRevert(IERC7496.TraitValueUnchanged.selector); token.setTrait(tokenId, key, value); } function testGetTraitValues() public { - bytes32 key1 = bytes32("test.key.one"); - bytes32 key2 = bytes32("test.key.two"); + bytes32 key1 = bytes32("testKeyOne"); + bytes32 key2 = bytes32("testKeyTwo"); bytes32 value1 = bytes32("foo"); bytes32 value2 = bytes32("bar"); uint256 tokenId = 1; @@ -86,7 +82,11 @@ contract ERC721DynamicTraitsTest is Test { function testGetAndSetTraitMetadataURI() public { string memory uri = "https://example.com/labels.json"; + + vm.expectEmit(true, true, true, true); + emit TraitMetadataURIUpdated(); token.setTraitMetadataURI(uri); + assertEq(token.getTraitMetadataURI(), uri); vm.prank(address(0x1234)); @@ -94,8 +94,8 @@ contract ERC721DynamicTraitsTest is Test { token.setTraitMetadataURI(uri); } - function testGetTraitValue_NonexistantToken() public { - bytes32 key = bytes32("test.key"); + function testGetAndSetTraitValue_NonexistantToken() public { + bytes32 key = bytes32("testKey"); bytes32 value = bytes32(uint256(1)); uint256 tokenId = 1; @@ -109,21 +109,15 @@ contract ERC721DynamicTraitsTest is Test { token.getTraitValues(tokenId, Solarray.bytes32s(key)); } - function testGetTraitValue_ZeroValue() public { - bytes32 key = bytes32("test.key"); + function testGetTraitValue_DefaultZeroValue() public { + bytes32 key = bytes32("testKey"); uint256 tokenId = 1; token.mint(address(this), tokenId); - bytes32 result = token.getTraitValue(tokenId, key); - assertEq(result, bytes32(0), "should return bytes32(0)"); - } - - function testGetTraitValues_ZeroValue() public { - bytes32 key = bytes32("test.key"); - uint256 tokenId = 1; - token.mint(address(this), tokenId); + bytes32 value = token.getTraitValue(tokenId, key); + assertEq(value, bytes32(0), "should return bytes32(0)"); - bytes32[] memory result = token.getTraitValues(tokenId, Solarray.bytes32s(key)); - assertEq(result[0], bytes32(0), "should return bytes32(0)"); + bytes32[] memory values = token.getTraitValues(tokenId, Solarray.bytes32s(key)); + assertEq(values[0], bytes32(0), "should return bytes32(0)"); } } diff --git a/test/dynamic-traits/ERC721DynamicTraitsMultiUpdate.t.sol b/test/dynamic-traits/ERC721DynamicTraitsMultiUpdate.t.sol new file mode 100644 index 0000000..8ece7b1 --- /dev/null +++ b/test/dynamic-traits/ERC721DynamicTraitsMultiUpdate.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {ERC721DynamicTraitsMultiUpdate} from "src/dynamic-traits/test/ERC721DynamicTraitsMultiUpdate.sol"; +import {Solarray} from "solarray/Solarray.sol"; + +contract ERC721DynamicTraitsMultiUpdateTest is Test { + ERC721DynamicTraitsMultiUpdate token; + + /* Events */ + event TraitUpdatedRange(bytes32 indexed traitKey, uint256 fromTokenId, uint256 toTokenId); + event TraitUpdatedRangeUniformValue( + bytes32 indexed traitKey, uint256 fromTokenId, uint256 toTokenId, bytes32 traitValue + ); + event TraitUpdatedList(bytes32 indexed traitKey, uint256[] tokenIds); + event TraitUpdatedListUniformValue(bytes32 indexed traitKey, uint256[] tokenIds, bytes32 traitValue); + + function setUp() public { + token = new ERC721DynamicTraitsMultiUpdate(); + } + + function testEmitsTraitUpdatedRange_UniformValue() public { + bytes32 key = bytes32("testKey"); + bytes32 value = bytes32("foo"); + uint256 fromTokenId = 1; + uint256 toTokenId = 100; + + for (uint256 tokenId = fromTokenId; tokenId <= toTokenId; tokenId++) { + token.mint(address(this), tokenId); + } + + vm.expectEmit(true, true, true, true); + emit TraitUpdatedRangeUniformValue(key, fromTokenId, toTokenId, value); + + token.setTraitsRange(fromTokenId, toTokenId, key, value); + + for (uint256 tokenId = fromTokenId; tokenId <= toTokenId; tokenId++) { + assertEq(token.getTraitValue(tokenId, key), value); + } + } + + function testEmitsTraitUpdatedRange_DifferentValues() public { + bytes32 key = bytes32("testKey"); + uint256 fromTokenId = 1; + uint256 toTokenId = 10; + bytes32[] memory values = new bytes32[](10); + for (uint256 i = 0; i < values.length; i++) { + values[i] = bytes32(i); + } + + for (uint256 tokenId = fromTokenId; tokenId <= toTokenId; tokenId++) { + token.mint(address(this), tokenId); + } + + vm.expectEmit(true, true, true, true); + emit TraitUpdatedRange(key, fromTokenId, toTokenId); + + token.setTraitsRangeDifferentValues(fromTokenId, toTokenId, key, values); + + for (uint256 tokenId = fromTokenId; tokenId <= toTokenId; tokenId++) { + assertEq(token.getTraitValue(tokenId, key), values[tokenId - 1]); + } + } + + function testEmitsTraitUpdatedList_UniformValue() public { + bytes32 key = bytes32("testKey"); + bytes32 value = bytes32("foo"); + uint256[] memory tokenIds = Solarray.uint256s(1, 10, 20, 50); + + for (uint256 i = 0; i < tokenIds.length; i++) { + token.mint(address(this), tokenIds[i]); + } + + vm.expectEmit(true, true, true, true); + emit TraitUpdatedListUniformValue(key, tokenIds, value); + + token.setTraitsList(tokenIds, key, value); + + for (uint256 i = 0; i < tokenIds.length; i++) { + assertEq(token.getTraitValue(tokenIds[i], key), value); + } + } + + function testEmitsTraitUpdatedList_DifferentValues() public { + bytes32 key = bytes32("testKey"); + uint256[] memory tokenIds = Solarray.uint256s(1, 10, 20, 50); + bytes32[] memory values = new bytes32[](tokenIds.length); + for (uint256 i = 0; i < values.length; i++) { + values[i] = bytes32(i * 1000); + } + + for (uint256 i = 0; i < tokenIds.length; i++) { + token.mint(address(this), tokenIds[i]); + } + + vm.expectEmit(true, true, true, true); + emit TraitUpdatedList(key, tokenIds); + + token.setTraitsListDifferentValues(tokenIds, key, values); + + for (uint256 i = 0; i < tokenIds.length; i++) { + assertEq(token.getTraitValue(tokenIds[i], key), values[i]); + } + } +} diff --git a/test/dynamic-traits/ERC721OnchainTraits.t.sol b/test/dynamic-traits/ERC721OnchainTraits.t.sol index 1f3c63e..2c36081 100644 --- a/test/dynamic-traits/ERC721OnchainTraits.t.sol +++ b/test/dynamic-traits/ERC721OnchainTraits.t.sol @@ -70,12 +70,11 @@ contract ERC721OnchainTraitsTest is Test { function testGetTraitLabel() public { TraitLabel memory label = _setLabel(); - (Editors editors, bool required, bool shouldValidate, StoredTraitLabel storedlabel) = - token.traitLabelStorage(bytes32("test.key")); - assertEq(Editors.unwrap(editors), Editors.unwrap(label.editors)); - assertEq(required, label.required); - assertEq(shouldValidate, false); - TraitLabel memory retrieved = StoredTraitLabelLib.load(storedlabel); + TraitLabelStorage memory storage_ = token.traitLabelStorage(bytes32("testKey")); + assertEq(Editors.unwrap(storage_.allowedEditors), Editors.unwrap(label.editors)); + assertEq(storage_.required, label.required); + assertEq(storage_.valuesRequireValidation, false); + TraitLabel memory retrieved = StoredTraitLabelLib.load(storage_.storedLabel); assertEq(label, retrieved); } @@ -83,22 +82,22 @@ contract ERC721OnchainTraitsTest is Test { _setLabel(); assertEq( token.getTraitMetadataURI(), - 'data:application/json;[{"traitKey":"test.key","fullTraitKey":"test.key","traitLabel":"Trait Key","acceptableValues":[],"fullTraitValues":[],"displayType":"string","editors":[0]}]' + 'data:application/json;[{"traitKey":"testKey","fullTraitKey":"testKey","traitLabel":"Trait Key","acceptableValues":[],"fullTraitValues":[],"displayType":"string","editors":[0]}]' ); } function testSetTrait() public { _setLabel(); token.mint(address(this)); - token.setTrait(1, bytes32("test.key"), bytes32("foo")); - assertEq(token.getTraitValue(1, bytes32("test.key")), bytes32("foo")); + token.setTrait(1, bytes32("testKey"), bytes32("foo")); + assertEq(token.getTraitValue(1, bytes32("testKey")), bytes32("foo")); } function testStringURI() public { _setLabel(); token.mint(address(this)); - token.setTrait(1, bytes32("test.key"), bytes32("foo")); - assertEq(token.getTraitValue(1, bytes32("test.key")), bytes32("foo")); + token.setTrait(1, bytes32("testKey"), bytes32("foo")); + assertEq(token.getTraitValue(1, bytes32("testKey")), bytes32("foo")); assertEq( token.getStringURI(1), '{"name":"Example NFT #1","description":"This is an example NFT","image":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MDAiIGhlaWdodD0iNTAwIiA+PHJlY3Qgd2lkdGg9IjUwMCIgaGVpZ2h0PSI1MDAiIGZpbGw9ImxpZ2h0Z3JheSIgLz48dGV4dCB4PSI1MCUiIHk9IjUwJSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSI0OCIgZmlsbD0iYmxhY2siID4xPC90ZXh0Pjwvc3ZnPg==","attributes":[{"trait_type":"Example Attribute","value":"Example Value"},{"trait_type":"Number","value":"1","display_type":"number"},{"trait_type":"Parity","value":"Odd"},{"trait_type":"Trait Key","value":"foo","display_type":"string"}]}' @@ -115,7 +114,7 @@ contract ERC721OnchainTraitsTest is Test { editors: Editors.wrap(EditorsLib.toBitMap(AllowedEditor.Anyone)), required: false }); - token.setTraitLabel(bytes32("test.key"), label); + token.setTraitLabel(bytes32("testKey"), label); return label; }