Skip to content

Commit

Permalink
Merge pull request #17 from ProjectOpenSea/ryan/add-redeemables-sips
Browse files Browse the repository at this point in the history
Add SIP-14: Redeemable Contract Offerer and SIP-15: Interface for Dynamic Traits Enforcement
  • Loading branch information
ryanio authored Aug 18, 2023
2 parents 143d7e1 + ce34be3 commit f1d4b5d
Show file tree
Hide file tree
Showing 2 changed files with 364 additions and 0 deletions.
268 changes: 268 additions & 0 deletions SIPS/sip-14.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
---
sip: 14
title: Redeemable Contract Offerer
description: A Seaport Contract Offerer that uses ERC-7500 Dynamic Traits to enable onchain redemptions
author: Ryan Ghods (@ryanio), 0age (@0age)
discussions-to: https://github.com/ProjectOpenSea/SIPs/discussions/18
status: Draft
type: Standards Track
category: ERC
created: 2023-07-28
requires: 5
---

## Abstract

This specification proposes a standard for a Seaport Contract Offerer that uses ERC-7500 Dynamic Traits to enable onchain redemptions. This also allows a Seaport zone to use the Dynamic Traits standard to ensure redemptions cannot be frontrun when NFTs are listed to be sold with a guarantee of certain traits.

## Motivation

Traits and metadata for non-fungible and semi-fungible tokens are often stored offchain. This makes it difficult to query and mutate these values in contract code. ERC-7500 Dynamic Traits brings certain traits onchain, to enable contracts like this Redeemable Contract Offerer to use them to create onchain token and offchain product redemptions.

## Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.

The contract offerer MUST have the following interface and MUST return `true` for EIP-165 supportsInterface for `0x12345678`, the 4 byte interfaceId of the below.

```solidity
interface IRedeemableContractOfferer {
/* Events */
event CampaignUpdated(uint256 indexed campaignId, CampaignParams params, string URI);
event Redemption(uint256 indexed campaignId, bytes32 redemptionHash);
/* Structs */
struct CampaignParams {
uint32 startTime;
uint32 endTime;
uint32 maxCampaignRedemptions;
address manager; // the address that can modify the campaign
address signer; // null address means no EIP-712 signature required
OfferItem[] offer; // items to be minted, can be empty for offchain redeemable
ConsiderationItem[] consideration; // the items you are transferring to recipient
}
struct TraitRedemption {
uint8 substandard;
address token;
uint256 identifier;
bytes32 traitKey;
bytes32 traitValue;
bytes substandardValue;
}
/* Getters */
function getCampaign(uint256 campaignId) external view returns (CampaignParams memory params, string memory uri, uint256 totalRedemptions);
/* Setters */
function createCampaign(CampaignParams calldata params, string calldata uri) external returns (uint256 campaignId);
function updateCampaign(uint256 campaignId, CampaignParams calldata params, string calldata uri) external;
}
```

### Creating campaigns

When creating a new campaign, `createCampaign` MUST be used and MUST return the newly created `campaignId` along with the `CampaignUpdated` event. The `campaignId` MUST be an incrementing counter starting at `1`.

### Updating campaigns

Updates to campaigns MUST use `updateCampaign` and MUST emit the `CampaignUpdated` event. If an address other than the `manager` tries to update the campaign, it MUST revert with `NotManager()`. If the manager wishes to make the campaign immutable, the `manager` MAY be set to the null address.

### Offer

If tokens are set in the params `offer`, the tokens MUST implement the `IRedemptionMintable` interface in order to support minting new items. The implementation SHOULD be however the token mechanics are desired. The implementing token MUST return true for EIP-165 `supportsInterface` for the interfaceIds of: `IERC721RedemptionMintable: 0x12345678` or `IERC1155RedemptionMintable: 0x12345678`

```solidify
interface IERC721RedemptionMintable {
function mintRedemption(address to, SpentItem[] calldata spent) external returns (uint256[] memory tokenIds);
}
interface IERC1155RedemptionMintable {
function mintRedemption(address to, SpentItem[] calldata spent) external returns (uint256[] memory tokenIds, uint256[] amounts);
}
```

The array length return values of `tokenIds` and `amounts` for `IERC1155RedemptionMintable` MUST equal each other.

### Consideration

Any token may be used in the RedeemableParams `consideration`. This will ensure the token is transferred to the `recipient`. If the token is meant to be burned the recipient SHOULD be `0x000000000000000000000000000000000000dEaD`.

### Dynamic traits

The contract offerer MUST include the ERC-7500 Dynamic Traits interface itself in case the campaign is a trait redemption and the specified token does not implement Dynamic Traits itself. According to the Dynamic Traits specification for "registry" functionality, the first 20 bytes of the `traitKey` should be the contract address to associate the proper `tokenId`.

### Signer

A signer MAY be specified to provide a signature to process the redemption. If the signer is NOT the null address, the signature MUST recover to the signer address via EIP-712 or EIP-1271.

The EIP-712 struct for signing MUST be as follows: `SignedRedeem(address owner,address redeemedToken, uint256[] tokenIds,bytes32 redemptionHash, uint256 salt)"`

### AdvancedOrder extraData

When interacting with the contract offerer via Seaport, the extraData/context layout for the AdvancedOrder MUST follow:

| bytes | value | description / notes |
| -------- | ----------------- | ---------------------------------------------------------------- |
| 0-32 | campaignId | |
| 32-64 | redemptionHash | hash of offchain order ids |
| 64-\* | TraitRedemption[] | see TraitRedemption struct. empty array for no trait redemptions |
| \*-(+32) | salt | if signer != address(0) |
| \*-(+\*) | signature | if signer != address(0). can be for EIP-712 or EIP-1271 |

Upon redemption, the contract offerer MUST check that the campaign is still active (using the same boundary check as Seaport, `startTime <= block.timestamp < endTime`). If it is, it MUST revert with `NotActive()`.

### Redemptions

The `Redemption` event MUST be emitted when any valid redemptions occur. The `Redemption` event is simplified to deduplicate information that is present in the `OrderFulfilled` event from Seaport.

### Trait redemptions

The contract offerer MUST respect the TraitRedemption substandards as follows:

| substandard ID | description | substandard value |
| -------------- | ------------------------------- | ------------------------------------------------------------------ |
| 1 | set value to `traitValue` | prior required value. if blank, cannot be the `traitValue` already |
| 2 | increment trait by `traitValue` | max value |
| 3 | decrement trait by `traitValue` | min value |

Additional substandards MAY be specified in subsequent SIPs that inherit this SIP.

### Max campaign redemptions

The contract offerer MUST check that the `maxCampaignRedemptions` is not exceeded. If the redemption does exceed `maxCampaignRedemptions`, it MUST revert with `MaxCampaignRedemptionsReached(uint256 total, uint256 max)`

### Metadata URI

The metadata URI MUST follow the following JSON schema:

```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string",
"description": "A one-line summary of the redeemable. Markdown is not supported."
},
"details": {
"type": "string",
"description": "A multi-line or multi-paragraph description of the details of the redeemable. Markdown is supported."
},
"imageUrls": {
"type": "string",
"description": "A list of image URLs for the redeemable. The first image will be used as the thumbnail. Will rotate in a carousel if multiple images are provided. Maximum 5 images."
},
"bannerUrl": {
"type": "string",
"description": "The banner image for the redeemable."
},
"faq": {
"type": "array",
"items": {
"type": "object",
"properties": {
"question": {
"type": "string"
},
"answer": {
"type": "string"
},
"required": ["question", "answer"]
}
}
},
"contentLocale": {
"type": "string",
"description": "The language tag for the content provided by this metadata. https://www.rfc-editor.org/rfc/rfc9110.html#name-language-tags"
},
"maxRedemptionsPerToken": {
"type": "string",
"description": "The maximum number of redemptions per token. When isBurn is true should be 1, else can be a number based on the trait redemptions limit."
},
"isBurn": {
"type": "string",
"description": "If the redemption burns the token."
},
"uuid": {
"type": "string",
"description": "A unique identifier for the campaign, for backends to identify when draft campaigns are published onchain."
},
"productLimitForRedemption": {
"type": "number",
"description": "The number of products which are able to be chosen from the products array for a single redemption."
},
"products": {
"type": "object",
"properties": "https://schema.org/Product",
"required": ["name", "url", "description"]
},
"traitRedemptions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"substandard": {
"type": "number"
},
"token": {
"type": "string",
"description": "The token address"
},
"traitKey": {
"type": "string"
},
"traitValue": {
"type": "string"
},
"substandardValue": {
"type": "string"
}
},
"required": [
"substandard",
"token",
"traitKey",
"traitValue",
"substandardValue"
]
}
}
},
"required": ["name", "description", "isBurn"]
}
```

Future SIPs MAY inherit this one and add to the above metadata to add more features and functionality.

### ERC-1155 (Semi-fungibles)

This standard MAY be applied to ERC-1155 but the redemptions would apply to all token amounts for specific token identifiers. If the ERC-1155 contract only has tokens with amount of 1, then this specification MAY be used as written.

## Rationale

The intention of this SIP is to define a consistent standard to enable redeemable entitlements for tokens and onchain traits. This contract offerer pattern allows for websites to discover, display, and interact with redeemable campaigns.

## Backwards Compatibility

As a new SIP, no backwards compatibility issues are present.

## Test Cases

Test cases can be found in [https://github.com/ProjectOpenSea/redeemables/tree/main/test](https://github.com/ProjectOpenSea/redeemables/tree/main/test)

## Reference Implementation

The reference implementation for the Redeemable Contract Offerer can be found at [https://github.com/ProjectOpenSea/redeemables/blob/main/src/RedeemableContractOfferer.sol](https://github.com/ProjectOpenSea/redeemables/blob/main/src/RedeemableContractOfferer.sol)

## Security Considerations

Tokens must properly implement EIP-7500 Dynamic Traits and allow the Redeemable Contract Offerer to use the `setTrait` method to enforce the full security of the standard.

For tokens to be minted as part of the params `offer`, the `mintRedemption` function contained as part of `IRedemptionMintable` MUST be permissioned and ONLY allowed to be called by redeemable contract offerers specified by address.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
96 changes: 96 additions & 0 deletions SIPS/sip-15.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
sip: 15
title: Interface for Dynamic Traits Enforcement
description: A Seaport interface for specifying and enforcing values of ERC-7500 Dynamic Traits.
author: Ryan Ghods (@ryanio), James Wenzel (emo.eth)
discussions-to: https://github.com/ProjectOpenSea/SIPs/discussions/19
status: Draft
type: Standards
category (*only required for Standards Track): Interface
created: 2023-08-14
requires (*optional): 5, 6
---

## Abstract

This SIP defines a standard interface for Seaport zones and contract offerers to specify and enforce ERC-7500 Dynamic Traits during order fulfillment.

## Motivation

Token metadata is typically specified at offchain locations but with ERC-7500 Dynamic Traits certain metadata values can be queried onchain. This can help protect NFTs against frontrunning during purchase where the value of traits changes the expected value of the NFT. With this SIP, zones and contract offerers can enforce the value of certain traits at time of order fulfillment.

## Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.

### Requirements

This SIP requires a single variable data array as part of supplied `extraData`.

Zones or contract offerers that do not implement additional SIPs must support extraData version byte `0x00` in accordance with SIP-6, while zones or contract offerers that implement additional SIPs with their own data requirements will require other version bytes.

Zones or contract offerers implementing this SIP MUST return a schema with an `id` of X as part of the `schemas` array returned by `getSeaportMetadata()` in accordance with SIP-5. They also MUST return an associated `metadata` parameter on the returned schema, decoded as `(bytes32 domainSeparator, string memory apiEndpoint, uint256[] memory substandards, string memory documentationURI)`.

### Metadata Verification

The data for verifying a protected order is sent as part of the order's `extraData` and must contain at least XX bytes. The `extraData` MUST be formatted according to [SIP-6](./sip-6.md) based on the other SIPs returned in accordance with SIP-5.

| field | bytes |
| ---------------------------------------------------- | ----- |
| variable context data (format based on substandards) | 0-end |

If the order does not conform to the expected metadata values, the zone or contract offerer MUST revert with `error InvalidDynamicTraitValue(address token, bytes32 traitKey, bytes32 expectedTraitValue, bytes32 actualTraitValue);`

### Interface

If the implementing contract is a zone, it MUST provide a `validateOrder()` function that adheres to the Seaport zone interface to decode the extra data component.

If the implementing contract is a contract offerer, it MUST provide `previewOrder()`, `generateOrder()`, and `ratifyOrder()` functions that adheres to the Seaport contract offerer interface that decodes the extra data.

### Substandards

The `context` argument MUST be populated based on the "substandards" specified by the zone or contract offerer; these substandards will be encoded in accordance with SIP-6 versioning with the assumption that all necessary data is to be treated as "variable" data arrays.

The `context` MUST start with a byte identifying the substandard ID below. The byte SHOULD be 1-indexed, but for gas efficiency reasons, 00 MAY also be used as an alias to reference substandard ID 1.

Initial substandards include:

| substandard ID | description | decoding scheme |
| -------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
| 1 | required traitValue for traitKey (single) | `(uint8 comparisonEnum, address token, bytes32 traitValue, bytes32 traitKey)` |
| 2 | required traitValues for traitKeys (multiple) | `(uint8 comparisonEnum, address token, bytes32 traitValue, bytes32 traitKey)[]` |
| 3 | required hash of values of all traitKeys (up to traitKeysLength, use uint256 max for all possible traitKeys) | `(address token, uint256 traitKeysLength, bytes32 expectedHash)` |

| comparison enum | behavior |
| --------------- | ------------------------ |
| 0 | equal to |
| 1 | less than |
| 2 | less than or equal to |
| 3 | greater than |
| 4 | greater than or equal to |

Additional substandards MAY be specified in subsequent SIPs that inherit this SIP.

## Rationale

This specification was developed to ensure a consistent experience to protect orders against frontrunning valuable traits.

## Backwards Compatibility

As a newly proposed standard there is no issue with backwards compatibility.

## Test Cases

Test cases are located in the [Dynamic Traits repository](https://github.com/ProjectOpenSea/dynamic-traits/blob/main/src/lib/DynamicTraits.sol).

## Reference Implementation

The reference implementation can be found in the [Dynamic Traits repository](https://github.com/ProjectOpenSea/dynamic-traits/blob/main/test/ERC721DynamicTraits.t.sol).

## Security Considerations

- For the zone to work as intended tokens must properly implement ERC-7500 Dynamic Traits.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).

0 comments on commit f1d4b5d

Please sign in to comment.