Skip to content

Commit d29904c

Browse files
committed
Update IERC7496 and DynamicTraits based on ethereum/ERCs PR 1400
- Add TraitDoesNotExist error to IERC7496 interface - Add _validTraitKeys mapping to DynamicTraitsStorage - Require trait keys to be registered before get/set operations - Add _registerTraitKey and _isTraitKeyRegistered internal functions - Add registerTraitKey public function to ERC721DynamicTraits - Update OnchainTraits to register trait keys in _setTraitLabel - Remove duplicate TraitDoesNotExist error from OnchainTraits - Update tests to register trait keys before use - Add tests for unregistered trait key behavior
1 parent 704f9db commit d29904c

6 files changed

Lines changed: 102 additions & 5 deletions

File tree

src/dynamic-traits/DynamicTraits.sol

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ library DynamicTraitsStorage {
99
mapping(uint256 tokenId => mapping(bytes32 traitKey => bytes32 traitValue)) _traits;
1010
/// @dev An offchain string URI that points to a JSON file containing trait metadata.
1111
string _traitMetadataURI;
12+
/// @dev A mapping of valid trait keys.
13+
mapping(bytes32 traitKey => bool isValid) _validTraitKeys;
1214
}
1315

1416
bytes32 internal constant STORAGE_SLOT = keccak256("contracts.storage.erc7496-dynamictraits");
@@ -40,8 +42,14 @@ contract DynamicTraits is IERC7496 {
4042
* @param traitKey The trait key to get the value of
4143
*/
4244
function getTraitValue(uint256 tokenId, bytes32 traitKey) public view virtual returns (bytes32 traitValue) {
45+
// Revert if the trait key does not exist.
46+
DynamicTraitsStorage.Layout storage layout = DynamicTraitsStorage.layout();
47+
if (!layout._validTraitKeys[traitKey]) {
48+
revert TraitDoesNotExist(traitKey);
49+
}
50+
4351
// Return the trait value.
44-
return DynamicTraitsStorage.layout()._traits[tokenId][traitKey];
52+
return layout._traits[tokenId][traitKey];
4553
}
4654

4755
/**
@@ -86,8 +94,15 @@ contract DynamicTraits is IERC7496 {
8694
* @param newValue The new trait value to set
8795
*/
8896
function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 newValue) public virtual {
97+
DynamicTraitsStorage.Layout storage layout = DynamicTraitsStorage.layout();
98+
99+
// Revert if the trait key does not exist.
100+
if (!layout._validTraitKeys[traitKey]) {
101+
revert TraitDoesNotExist(traitKey);
102+
}
103+
89104
// Revert if the new value is the same as the existing value.
90-
bytes32 existingValue = DynamicTraitsStorage.layout()._traits[tokenId][traitKey];
105+
bytes32 existingValue = layout._traits[tokenId][traitKey];
91106
if (existingValue == newValue) {
92107
revert TraitValueUnchanged();
93108
}
@@ -122,6 +137,22 @@ contract DynamicTraits is IERC7496 {
122137
emit TraitMetadataURIUpdated();
123138
}
124139

140+
/**
141+
* @notice Register a trait key as valid.
142+
* @param traitKey The trait key to register.
143+
*/
144+
function _registerTraitKey(bytes32 traitKey) internal virtual {
145+
DynamicTraitsStorage.layout()._validTraitKeys[traitKey] = true;
146+
}
147+
148+
/**
149+
* @notice Check if a trait key is registered.
150+
* @param traitKey The trait key to check.
151+
*/
152+
function _isTraitKeyRegistered(bytes32 traitKey) internal view virtual returns (bool) {
153+
return DynamicTraitsStorage.layout()._validTraitKeys[traitKey];
154+
}
155+
125156
/**
126157
* @dev See {IERC165-supportsInterface}.
127158
*/

src/dynamic-traits/ERC721DynamicTraits.sol

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ contract ERC721DynamicTraits is DynamicTraits, Ownable, ERC721 {
5151
_setTraitMetadataURI(uri);
5252
}
5353

54+
function registerTraitKey(bytes32 traitKey) external onlyOwner {
55+
// Register the trait key as valid.
56+
_registerTraitKey(traitKey);
57+
}
58+
5459
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, DynamicTraits) returns (bool) {
5560
return ERC721.supportsInterface(interfaceId) || DynamicTraits.supportsInterface(interfaceId);
5661
}

src/dynamic-traits/OnchainTraits.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@ abstract contract OnchainTraits is Ownable, DynamicTraits {
4545

4646
/// @notice Thrown when the caller does not have the privilege to set a trait
4747
error InsufficientPrivilege();
48-
/// @notice Thrown when trying to set a trait that does not exist
49-
error TraitDoesNotExist(bytes32 traitKey);
5048

5149
constructor() {
5250
_initializeOwner(msg.sender);
@@ -170,6 +168,8 @@ abstract contract OnchainTraits is Ownable, DynamicTraits {
170168
valuesRequireValidation: _traitLabel.acceptableValues.length > 0,
171169
storedLabel: TraitLabelLib.store(_traitLabel)
172170
});
171+
// Register the trait key in the base DynamicTraits contract.
172+
_registerTraitKey(traitKey);
173173
}
174174

175175
/**

src/dynamic-traits/interfaces/IERC7496.sol

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
pragma solidity ^0.8.19;
33

44
interface IERC7496 {
5+
/* Errors */
6+
/// @notice Thrown when trying to set a trait that does not exist.
7+
error TraitDoesNotExist(bytes32 traitKey);
8+
// Note: TokenDoesNotExist(uint256 tokenId) is specified in ERC-7496 but omitted here
9+
// to avoid conflicts with token contracts that define their own error (e.g., Solady ERC721).
10+
// Implementations MAY use their underlying token's error for non-existent tokens.
11+
512
/* Events */
613
event TraitUpdated(bytes32 indexed traitKey, uint256 tokenId, bytes32 traitValue);
714
event TraitUpdatedRange(bytes32 indexed traitKey, uint256 fromTokenId, uint256 toTokenId);

test/dynamic-traits/ERC721DynamicTraits.t.sol

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ contract ERC721DynamicTraitsTest is Test {
3636
bytes32 key = bytes32("testKey");
3737
bytes32 value = bytes32("foo");
3838
uint256 tokenId = 12345;
39+
40+
// Register the trait key before using it.
41+
token.registerTraitKey(key);
3942
token.mint(address(this), tokenId);
4043

4144
vm.expectEmit(true, true, true, true);
@@ -47,16 +50,23 @@ contract ERC721DynamicTraitsTest is Test {
4750
}
4851

4952
function testOnlyOwnerCanSetValues() public {
53+
bytes32 key = bytes32("test");
54+
// Register the trait key before testing access control.
55+
token.registerTraitKey(key);
56+
5057
address alice = makeAddr("alice");
5158
vm.prank(alice);
5259
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice));
53-
token.setTrait(0, bytes32("test"), bytes32("test"));
60+
token.setTrait(0, key, bytes32("test"));
5461
}
5562

5663
function testSetTrait_Unchanged() public {
5764
bytes32 key = bytes32("testKey");
5865
bytes32 value = bytes32("foo");
5966
uint256 tokenId = 1;
67+
68+
// Register the trait key before using it.
69+
token.registerTraitKey(key);
6070
token.mint(address(this), tokenId);
6171

6272
token.setTrait(tokenId, key, value);
@@ -70,6 +80,10 @@ contract ERC721DynamicTraitsTest is Test {
7080
bytes32 value1 = bytes32("foo");
7181
bytes32 value2 = bytes32("bar");
7282
uint256 tokenId = 1;
83+
84+
// Register the trait keys before using them.
85+
token.registerTraitKey(key1);
86+
token.registerTraitKey(key2);
7387
token.mint(address(this), tokenId);
7488

7589
token.setTrait(tokenId, key1, value1);
@@ -99,6 +113,9 @@ contract ERC721DynamicTraitsTest is Test {
99113
bytes32 value = bytes32(uint256(1));
100114
uint256 tokenId = 1;
101115

116+
// Register the trait key so we can test token non-existence.
117+
token.registerTraitKey(key);
118+
102119
vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, tokenId));
103120
token.setTrait(tokenId, key, value);
104121

@@ -112,6 +129,9 @@ contract ERC721DynamicTraitsTest is Test {
112129
function testGetTraitValue_DefaultZeroValue() public {
113130
bytes32 key = bytes32("testKey");
114131
uint256 tokenId = 1;
132+
133+
// Register the trait key before using it.
134+
token.registerTraitKey(key);
115135
token.mint(address(this), tokenId);
116136

117137
bytes32 value = token.getTraitValue(tokenId, key);
@@ -120,4 +140,26 @@ contract ERC721DynamicTraitsTest is Test {
120140
bytes32[] memory values = token.getTraitValues(tokenId, Solarray.bytes32s(key));
121141
assertEq(values[0], bytes32(0), "should return bytes32(0)");
122142
}
143+
144+
function testGetTraitValue_UnregisteredTraitKey() public {
145+
bytes32 key = bytes32("unregisteredKey");
146+
uint256 tokenId = 1;
147+
token.mint(address(this), tokenId);
148+
149+
vm.expectRevert(abi.encodeWithSelector(IERC7496.TraitDoesNotExist.selector, key));
150+
token.getTraitValue(tokenId, key);
151+
152+
vm.expectRevert(abi.encodeWithSelector(IERC7496.TraitDoesNotExist.selector, key));
153+
token.getTraitValues(tokenId, Solarray.bytes32s(key));
154+
}
155+
156+
function testSetTrait_UnregisteredTraitKey() public {
157+
bytes32 key = bytes32("unregisteredKey");
158+
bytes32 value = bytes32("foo");
159+
uint256 tokenId = 1;
160+
token.mint(address(this), tokenId);
161+
162+
vm.expectRevert(abi.encodeWithSelector(IERC7496.TraitDoesNotExist.selector, key));
163+
token.setTrait(tokenId, key, value);
164+
}
123165
}

test/dynamic-traits/ERC721DynamicTraitsMultiUpdate.t.sol

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ contract ERC721DynamicTraitsMultiUpdateTest is Test {
2626
uint256 fromTokenId = 1;
2727
uint256 toTokenId = 100;
2828

29+
// Register the trait key before using it.
30+
token.registerTraitKey(key);
31+
2932
for (uint256 tokenId = fromTokenId; tokenId <= toTokenId; tokenId++) {
3033
token.mint(address(this), tokenId);
3134
}
@@ -49,6 +52,9 @@ contract ERC721DynamicTraitsMultiUpdateTest is Test {
4952
values[i] = bytes32(i);
5053
}
5154

55+
// Register the trait key before using it.
56+
token.registerTraitKey(key);
57+
5258
for (uint256 tokenId = fromTokenId; tokenId <= toTokenId; tokenId++) {
5359
token.mint(address(this), tokenId);
5460
}
@@ -68,6 +74,9 @@ contract ERC721DynamicTraitsMultiUpdateTest is Test {
6874
bytes32 value = bytes32("foo");
6975
uint256[] memory tokenIds = Solarray.uint256s(1, 10, 20, 50);
7076

77+
// Register the trait key before using it.
78+
token.registerTraitKey(key);
79+
7180
for (uint256 i = 0; i < tokenIds.length; i++) {
7281
token.mint(address(this), tokenIds[i]);
7382
}
@@ -90,6 +99,9 @@ contract ERC721DynamicTraitsMultiUpdateTest is Test {
9099
values[i] = bytes32(i * 1000);
91100
}
92101

102+
// Register the trait key before using it.
103+
token.registerTraitKey(key);
104+
93105
for (uint256 i = 0; i < tokenIds.length; i++) {
94106
token.mint(address(this), tokenIds[i]);
95107
}

0 commit comments

Comments
 (0)