Skip to content

Commit

Permalink
updates from eip editor review:
Browse files Browse the repository at this point in the history
- remove IERC165 inheritance from interface for easier dev
- add to motivation section. improve rationale section
- add test case file, link to assets folder directly
  • Loading branch information
ryanio committed Oct 23, 2023
1 parent 26ea0d7 commit 9e2770a
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 6 deletions.
18 changes: 12 additions & 6 deletions EIPS/eip-7496.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ This specification introduces a new interface that extends [ERC-721](./eip-721.m

## Motivation

Trait values for non-fungible tokens are often stored offchain. This makes it difficult to query and mutate these values in contract code. Specifying the ability to set and get traits onchain allows for new use cases like transacting based on a token's traits or redeeming onchain entitlements.
Trait values for non-fungible tokens are often stored offchain. This makes it difficult to query and mutate these values in contract code. Specifying the ability to set and get traits onchain allows for new use cases like redeeming onchain entitlements and transacting based on a token's traits.

Onchain traits can be used by contracts in a variety of different scenarios. For example, a contract that wants to entitle a token to a consumable benefit (e.g. a redeemable) can robustly reflect that onchain. Marketplaces can allow bidding on these tokens based on the trait value without having to rely on offchain state and exposing users to frontrunning attacks. The motivating use case behind this proposal is to protect users from frontrunning attacks on marketplaces where users can list NFTs with certain traits where they are expected to be upheld during fulfillment.

This interface for traits was chosen instead of relying on using regular `getFoo()` and `setFoo()` style functions to allow for brevity in defining, setting, and getting traits. Otherwise, contracts would need to know both the getter and setter function selectors including the parameters that go along with it. In defining general but explicit get and set functions, the function signatures are known and only the trait key and values are needed to query and set the values. Contracts can also add new traits in the future without needing to modify contract code.

## Specification

Expand All @@ -26,7 +30,7 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S
Contracts implementing this EIP MUST include the events, getters, and setters as defined below, and MUST return `true` for [ERC-165](./eip-165.md) `supportsInterface` for `0xaf332f3e`, the 4 byte `interfaceId` for this ERC.

```solidity
interface IERC7496 is IERC165 {
interface IERC7496 {
/* Events */
event TraitUpdated(bytes32 indexed traitKey, uint256 tokenId, bytes32 traitValue);
event TraitUpdatedRange(bytes32 indexed traitKey, uint256 fromTokenId, uint256 toTokenId);
Expand Down Expand Up @@ -179,7 +183,7 @@ For the `List` events, the `tokenIds` MAY be in any order.

It is RECOMMENDED to use the `UniformValue` events when the trait value is uniform across all token ids, so offchain indexers can more quickly process bulk updates rather than fetching each trait value individually.

Updating the trait metadata URI MUST emit the event `TraitMetadataURIUpdated` so offchain indexers can be notified to query the contract for the latest changes via `getTraitMetadataURI()`.
Updating the trait metadata MUST emit the event `TraitMetadataURIUpdated` so offchain indexers can be notified to query the contract for the latest changes via `getTraitMetadataURI()`.

### `setTrait`

Expand All @@ -191,19 +195,21 @@ If the trait has a `valueMappings` entry defined for the desired value being set

## Rationale

Onchain traits can be used by contracts to get and mutate traits in a variety of different scenarios. For example, a contract that wants to entitle a token to a consumable benefit (e.g. a redeemable) can robustly reflect that onchain. Marketplaces can allow bidding on these tokens based on the trait value without having to rely on offchain state and exposing users to frontrunning attacks.
The design of this specification is primarily a key-value mapping for maximum flexibility.

The traits metadata allows for customizability of both display and behavior. The `valueMappings` property can define human-readable values to enhance the traits, for example, the default label of the `0` value (e.g. if the key was "redeemed", "0" could be mapped to "No", and "1" to "Yes"). The `validateOnSale` property lets the token creator define which traits should be protected on order creation and fulfillment, to protect end users against frontrunning.

## Backwards Compatibility

As a new EIP, no backwards compatibility issues are present, except for the point in the specification above that it is explicitly required that the onchain traits MUST override any conflicting values specified by the ERC-721 or ERC-1155 metadata URIs.

## Test Cases

Authors have included Foundry tests covering functionality of the specification in the assets folder.
Authors have included Foundry tests covering functionality of the specification in the [assets folder](../assets/eip-7496/DynamicTraits.t.sol).

## Reference Implementation

Authors have included reference implementations of the specification in the assets folder.
Authors have included reference implementations of the specification in the [assets folder](../assets/eip-7496/DynamicTraits.sol).

## Security Considerations

Expand Down
131 changes: 131 additions & 0 deletions assets/eip-7496/ERC721DynamicTraits.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.19;

import "forge-std/Test.sol";
import {IERC721Errors} from "openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol";
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 {Solarray} from "solarray/Solarray.sol";

contract ERC721DynamicTraitsMintable is ERC721DynamicTraits {
constructor() ERC721DynamicTraits() {}

function mint(address to, uint256 tokenId) public onlyOwner {
_mint(to, tokenId);
}
}

contract ERC721DynamicTraitsTest is Test {
ERC721DynamicTraitsMintable token;

/* Events */
event TraitUpdated(bytes32 indexed traitKey, uint256 tokenId, bytes32 trait);
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();

function setUp() public {
token = new ERC721DynamicTraitsMintable();
}

function testSupportsInterfaceId() public {
assertTrue(token.supportsInterface(type(IERC7496).interfaceId));
}

function testReturnsValueSet() public {
bytes32 key = bytes32("test.key");
bytes32 value = bytes32("foo");
uint256 tokenId = 12345;
token.mint(address(this), tokenId);

vm.expectEmit(true, true, true, true);
emit TraitUpdated(key, tokenId, value);

token.setTrait(tokenId, key, value);

assertEq(token.getTraitValue(tokenId, key), value);
}

function testOnlyOwnerCanSetValues() public {
address alice = makeAddr("alice");
vm.prank(alice);
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice));
token.setTrait(0, bytes32("test"), bytes32("test"));
}

function testSetTrait_Unchanged() public {
bytes32 key = bytes32("test.key");
bytes32 value = bytes32("foo");
uint256 tokenId = 1;
token.mint(address(this), tokenId);

token.setTrait(tokenId, key, value);
vm.expectRevert(DynamicTraits.TraitValueUnchanged.selector);
token.setTrait(tokenId, key, value);
}

function testGetTraitValues() public {
bytes32 key1 = bytes32("test.key.one");
bytes32 key2 = bytes32("test.key.two");
bytes32 value1 = bytes32("foo");
bytes32 value2 = bytes32("bar");
uint256 tokenId = 1;
token.mint(address(this), tokenId);

token.setTrait(tokenId, key1, value1);
token.setTrait(tokenId, key2, value2);

bytes32[] memory values = token.getTraitValues(tokenId, Solarray.bytes32s(key1, key2));
assertEq(values[0], value1);
assertEq(values[1], value2);
}

function testGetAndSetTraitMetadataURI() public {
string memory uri = "https://example.com/labels.json";
token.setTraitMetadataURI(uri);
assertEq(token.getTraitMetadataURI(), uri);

vm.prank(address(0x1234));
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(0x1234)));
token.setTraitMetadataURI(uri);
}

function testGetTraitValue_NonexistantToken() public {
bytes32 key = bytes32("test.key");
bytes32 value = bytes32(uint256(1));
uint256 tokenId = 1;

vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, tokenId));
token.setTrait(tokenId, key, value);

vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, tokenId));
token.getTraitValue(tokenId, key);

vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, tokenId));
token.getTraitValues(tokenId, Solarray.bytes32s(key));
}

function testGetTraitValue_ZeroValue() public {
bytes32 key = bytes32("test.key");
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[] memory result = token.getTraitValues(tokenId, Solarray.bytes32s(key));
assertEq(result[0], bytes32(0), "should return bytes32(0)");
}
}

0 comments on commit 9e2770a

Please sign in to comment.