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

Dynamic Trait updates #16

Merged
merged 3 commits into from
Oct 24, 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
51 changes: 51 additions & 0 deletions script/EmitDynamicTraitsTestEvents.s.sol
Original file line number Diff line number Diff line change
@@ -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");
}
}
86 changes: 61 additions & 25 deletions src/dynamic-traits/DynamicTraits.sol
Original file line number Diff line number Diff line change
@@ -1,31 +1,47 @@
// 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.
* @param tokenId The token ID to get the trait value for
* @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];
}

/**
Expand All @@ -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);
Expand All @@ -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;
}

/**
Expand All @@ -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;
}
Expand Down
10 changes: 7 additions & 3 deletions src/dynamic-traits/ERC721DynamicTraits.sol
Original file line number Diff line number Diff line change
@@ -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);
}

Expand All @@ -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);
}

Expand All @@ -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);
}

Expand Down
6 changes: 5 additions & 1 deletion src/dynamic-traits/ERC721OnchainTraits.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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);
}

Expand All @@ -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;
}

Expand Down
Loading
Loading