Skip to content

Commit

Permalink
Merge pull request #16 from ProjectOpenSea/ryan/dt-updates
Browse files Browse the repository at this point in the history
Dynamic Trait updates
  • Loading branch information
ryanio authored Oct 24, 2023
2 parents fa907cb + e9d0ecd commit fc2255f
Show file tree
Hide file tree
Showing 10 changed files with 413 additions and 97 deletions.
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

0 comments on commit fc2255f

Please sign in to comment.