はじめに
初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
代表的なゲームはクリプトスペルズというブロックチェーンゲームです。
今回は、Openseaが新たに提案している、NFTにオンチェーン・オフチェーン問わずさまざまなデジタルアセットを紐付ける仕組みであるERC7498についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
ERC7498は現在(2023年9月7日)では「Idea」段階です。
概要
この新しい仕様は、既存のNFT(非代替性トークン)規格であるERC721とERC1155を拡張するためのものです。
その目的は、NFT(トークン)と一緒に、追加のデジタルアセット(コンテンツや特権など)をブロックチェーン上とブロックチェーン外から紐づけて利用できるようにすることです。
この仕組みを具体的に説明すると、NFTオーナーはNFTを持っているだけでなく、NFTに関連する他のデジタルアセットを受け取ることができます。
これらのデジタルアセットは、ブロックチェーン上に保存されている場合もあれば、ブロックチェーン外の場所に存在している場合もあります。
例えば、NFTがあるゲームの特別なアイテムを示している場合、そのアイテム自体はゲームのサーバーに存在し、NFTオーナーはNFTを持っていることでその特別なアイテムを利用できるようになることが考えられます。
この新しい仕様は、NFTの所有者に追加の価値を提供し、NFTエコシステムをより魅力的にします。
つまり、NFTが単なるデジタルアートやコレクションではなく、それを持つことでさまざまなデジタルアクティビティや特典にアクセスできるようになります。
これにより、NFTの使い道が広がり、NFT市場が成長することが期待されています。
動機
アーティストやクリエイターは、NFT(非代替性トークン)を使って、デジタルまたは物理的なアイテムにアクセスできる特典を提供できます。
しかし、問題は、このような特典を持つNFTを見つけたり、使ったりするための標準的な方法が不足していることです。
この新しい標準規格は、以下の目標を達成するために作成されました。
発見性の向上(Discovery)
特典を提供するための条件や詳細情報を提供する仕組みを作ります。
これにより、ユーザーやアプリがどのNFTが特典を提供しているのかを容易に見つけることができます。
ブロックチェーン上の情報(Onchain)
特典を受けるためにどのNFTが使用されたかをブロックチェーン上で追跡できるようにします。
これにより、トランザクションの透明性が向上し、信頼性が高まります。
ブロックチェーン外の情報(Offchain)
特典と実際の商品注文を関連付ける仕組みを提供します。
これにより、実際の商品と特典の関係を明確にします。
特性の引き換えの向上(Trait Redemptions)
特性を持つNFTを燃やすことで特典を受けるプロセスを改善します。
これにより、ユーザーエクスペリエンスが向上し、特性の引き換えがスムーズに行えます。
リデンプション(Redemption)
特典や権利を実際に利用したり、交換したりするプロセスを指します。
一般的に、特典や権利はある条件を満たすことで提供され、リデンプションはその条件を満たした際に特典や権利を実現する行為です。
ブロックチェーンや仮想通貨のコンテキストでは、NFT(非代替性トークン)やトークンなどのデジタルアセットがリデンプト(実現)されることがあります。
例えば、NFTを所有しているユーザーが特定の条件を満たすと、そのNFTに関連するデジタルコンテンツや特典を受け取ることができる場合があります。
この場合、特典を受け取る行為がリデンプションと呼ばれます。
リデンプションは、デジタルアセットの所有者がそれらのアセットを有効に活用する重要なプロセスであり、ブロックチェーン技術を使用することで透明かつ信頼性のある方法で実現できるようになります。
特にNFTエコシステムでは、アーティストやクリエイターがファンやコレクターに対して特別な特典を提供し、それをリデンプトすることでファンとの関係を強化するために活用されています。
仕様
トークンは以下のインタフェースを持たなければならず、ERC165のsupportsInterface
(0x12345678
、以下の4バイトのinterfaceId
)に対してtrue
を返します。
interface IERC7501 {
/* Events */
event CampaignUpdated(uint256 indexed campaignId, CampaignParams params, string URI);
event Redemption(uint256 indexed campaignId, bytes32 redemptionHash, uint256[] tokenIds, address redeemedBy);
/* 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;
bytes32 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;
function redeem(uint256[] calldata tokenIds, bytes calldata extraData) external;
}
---
/* Seaport structs, for reference, used in offer/consideration above */
enum ItemType {
NATIVE,
ERC20,
ERC721,
ERC1155
}
struct OfferItem {
ItemType itemType;
address token;
uint256 identifierOrCriteria;
uint256 startAmount;
uint256 endAmount;
}
struct ConsiderationItem extends OfferItem {
address payable recipient;
// (note: psuedocode above, can't currently extend structs in solidity)
}
struct SpentItem {
ItemType itemType;
address token;
uint256 identifier;
uint256 amount;
}
キャンペーンの作成
新しいキャンペーンを作成する場合、必ずcreateCampaign
関数を使用する必要があります。
この関数は、新しく作成されたcampaignId
(キャンペーンID)を返し、同時にCampaignUpdated
イベントを発行します。
campaignId
は1
から始まるインクリメントされる値です。
キャンペーンの更新
キャンペーンを更新する際には、必ずupdateCampaign
関数を使用する必要があります。
この関数は、更新後の情報を反映するためにCampaignUpdated
イベントを発行します。
もし、manager
以外のアドレスがキャンペーンを更新しようとした場合、トランザクションはNotManager()
というエラーで中断されます。
また、もしマネージャーがキャンペーンを変更不可能にしたい場合、manager
はnull
アドレスに設定することができます。
オファー
もしパラメーターの中にoffer
としてトークンが設定されている場合、それらのトークンは新しいアイテムを作成するためにIRedemptionMintable
インターフェースを実装する必要があります。
このインターフェースの実装は、トークンの特定の動作に合わせて設計されるべきです。
また、実装されたトークンは、IERC721RedemptionMintable: 0x12345678
またはIERC1155RedemptionMintable: 0x12345678
というインターフェースIDに対して、ERC165のsupportsInterface
関数を呼び出す時にtrue
を返す必要があります。
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);
}
IERC1155RedemptionMintable
インターフェースにおいて、tokenIds
(トークンの識別子)とamounts
(数量)という2つの配列は、それぞれの要素数が必ず同じ必要があります。
検討事項
RedeemableParams
のconsideration
には、どんなトークンでも使用できます。
これにより、そのトークンはrecipient
(受信アドレス)に転送されます。
トークンを焼却(破棄)する場合、通常recipient
は0x000000000000000000000000000000000000dEaD
に設定されます。
ダイナミックトレイト(Dynamic Traits)
トークンがトレイト引き換えを有効にしたい場合、そのトークンはEIP-7496 Dynamic Traitsインターフェースを含める必要があります。
署名者
リデンプション(特典の利用)プロセスに署名を提供するために、署名者を指定することができます。
署名者がnull
アドレスでない場合、署名はEIP712またはERC1271を介して署名します。
EIP712の構造体(struct
)は次のようになります
SignedRedeem(address owner,address redeemedToken, uint256[] tokenIds,bytes32 redemptionHash, uint256 salt)"
。
これは、リデンプションを行うための署名情報を含んでいます。
リデンプションの追加データ
redeem
関数を呼び出す際、追加のデータは以下に従う必要があります。
バイト範囲 | 値 | 説明 / 注意事項 |
---|---|---|
0-32 | campaignId(キャンペーンID) | |
32-64 | redemptionHash(リデンプションハッシュ) | オフチェーンの注文IDのハッシュ |
64-* | TraitRedemption[](トレイトリデンプション) | トレイトリデンプションがない場合は空の配列 |
*-(+32) | salt(ソルト) | サイン者がaddress(0) でない場合のみ |
*-(+*) | signature(署名) | サイン者がaddress(0) でない場合のみ。EIP-712またはERC-1271の署名が可能 |
リデンプション時に、コントラクトはキャンペーンがまだ有効であることを確認する必要があります(Seaportと同じ境界チェックを使用、startTime <= block.timestamp < endTime
)。
もし有効でない場合、NotActive()
で失敗しなければなりません。
リデンプション
redeem
関数は、consideration
に指定されたトークンの転送を実行する必要があります。
また、offer
で指定されたトークン上でmintRedemption
関数を呼び出す必要があります。
redeem
で提供されたトークンIDのうち、バリデーションに失敗したものがある場合、関数はバリデーションに合格したリデンプションのみを実行し、失敗したリデンプションは無視してもかまいません。
また、有効なリデンプションが発生した場合、Redemption
イベントを発行します。
トレイトリデンプション
トークンは以下のように、TraitRedemption
のサブスタンダードに従います。
サブスタンダードID | 説明 | サブスタンダードの値 |
---|---|---|
1 |
traitValue に値を設定 |
以前の必要な値。空白の場合、traitValue になれません。 |
2 |
traitValue を増やす |
最大値 |
3 |
traitValue を減らす |
最小値 |
最大キャンペーンリデンプション数
トークンはmaxCampaignRedemptions
(最大キャンペーンリデンプション数)が超過しないことを確認する必要があります。
もしリデンプションがmaxCampaignRedemptions
を超える場合、MaxCampaignRedemptionsReached(uint256 total, uint256 max)
で失敗しなければなりません。
メタデータURI
メタデータURIは、以下のJSONスキーマに従う必要があります。
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string",
"description": "リデンプトの要約文。Markdownはサポートされていません。"
},
"details": {
"type": "string",
"description": "リデンプトの詳細な説明。複数行または複数段落の説明が可能で、Markdownがサポートされています。"
},
"imageUrls": {
"type": "string",
"description": "リデンプトの画像URLのリスト。最初の画像はサムネイルとして使用されます。複数の画像が提供された場合、カルーセルで表示されます。最大5つの画像まで。"
},
"bannerUrl": {
"type": "string",
"description": "リデンプトのバナー画像。"
},
"faq": {
"type": "array",
"items": {
"type": "object",
"properties": {
"question": {
"type": "string"
},
"answer": {
"type": "string"
},
"required": ["question", "answer"]
}
}
},
"contentLocale": {
"type": "string",
"description": "このメタデータで提供されるコンテンツの言語タグ。"
},
"maxRedemptionsPerToken": {
"type": "string",
"description": "トークンごとの最大リデンプション数。isBurnがtrueの場合は1である必要があり、それ以外の場合はトレイトリデンプションの制限に基づく数値である可能性があります。"
},
"isBurn": {
"type": "string",
"description": "リデンプションがトークンを焼却するかどうか。"
},
"uuid": {
"type": "string",
"description": "キャンペーンの一意の識別子。バックエンドがドラフトのキャンペーンがチェーン上で公開された際に識別するために使用されます。"
},
"productLimitForRedemption": {
"type": "number",
"description": "単一のリデンプションで選択できる製品の数。"
},
"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": "トークンのアドレス"
},
"traitKey": {
"type": "string"
},
"traitValue": {
"type": "string"
},
"substandardValue": {
"type": "string"
}
},
"required": [
"substandard",
"token",
"traitKey",
"traitValue",
"substandardValue"
]
}
}
},
"required": ["name", "description", "isBurn"]
}
将来のSIPs(Standard Improvement Proposals)では、このスキーマを継承し、さらなる機能と機能を追加する可能性があります。
ERC-1155(セミファンジブル)
この標準はERC1155に適用することも可能ですが、リデンプションは特定のトークン識別子のすべてのトークン数量に適用されます。
ERC1155コントラクトに数量1
のトークンしか含まれていない場合、この仕様はそのまま使用できます。
補足
このEIP(Ethereum Improvement Proposal)の目的は、トークンとオンチェーン追跡の特権を有効にするための一貫した規格を定義することです。
具体的には、トークン所有者に対して特典を提供し、それをブロックチェーン上で管理できる方法を提供します。
この規格を採用することで、ウェブサイトなどのアプリケーションが、リデンプト特典のキャンペーンを見つけ、表示し、ユーザーと対話することが容易になります。
後方互換性
このEIPが新しく導入されるものであるため、既存のシステムやプロトコルに対する変更や影響はありません。
まり、既存のトークンやプロジェクトはこの新しい規格を導入する際に大きな問題を抱えること#なく、スムーズに移行できます。
このEIPは、トークンエコシステムをより多彩で特典豊かにするための革新的なステップとして考えられます。
テスト
以下のGithubに格納されています。
参考実装
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {ContractOffererInterface} from "seaport-types/src/interfaces/ContractOffererInterface.sol";
import {SeaportInterface} from "seaport-types/src/interfaces/SeaportInterface.sol";
import {ItemType, OrderType} from "seaport-types/src/lib/ConsiderationEnums.sol";
import {
AdvancedOrder,
CriteriaResolver,
OrderParameters,
OfferItem,
ConsiderationItem,
ReceivedItem,
Schema,
SpentItem
} from "seaport-types/src/lib/ConsiderationStructs.sol";
import {ERC20} from "solady/src/tokens/ERC20.sol";
import {ERC721} from "solady/src/tokens/ERC721.sol";
import {ERC1155} from "solady/src/tokens/ERC1155.sol";
import {IERC721Receiver} from "seaport-types/src/interfaces/IERC721Receiver.sol";
import {IERC1155Receiver} from "./interfaces/IERC1155Receiver.sol";
import {IERC721RedemptionMintable} from "./interfaces/IERC721RedemptionMintable.sol";
import {IERC1155RedemptionMintable} from "./interfaces/IERC1155RedemptionMintable.sol";
import {SignedRedeemContractOfferer} from "./lib/SignedRedeemContractOfferer.sol";
import {RedeemableErrorsAndEvents} from "./lib/RedeemableErrorsAndEvents.sol";
import {CampaignParams} from "./lib/RedeemableStructs.sol";
/**
* @title RedeemablesContractOfferer
* @author ryanio, stephankmin
* @notice A Seaport contract offerer that allows users to burn to redeem off chain redeemables.
*/
contract RedeemableContractOfferer is
ContractOffererInterface,
RedeemableErrorsAndEvents,
SignedRedeemContractOfferer
{
/// @dev The Seaport address allowed to interact with this contract offerer.
address internal immutable _SEAPORT;
/// @dev The conduit address to allow as an operator for this contract for newly minted tokens.
address internal immutable _CONDUIT;
bytes32 internal immutable _CONDUIT_KEY;
/// @dev Counter for next campaign id.
uint256 private _nextCampaignId = 1;
/// @dev The campaign parameters by campaign id.
mapping(uint256 campaignId => CampaignParams params) private _campaignParams;
/// @dev The campaign URIs by campaign id.
mapping(uint256 campaignId => string campaignURI) private _campaignURIs;
/// @dev The total current redemptions by campaign id.
mapping(uint256 campaignId => uint256 count) private _totalRedemptions;
constructor(address conduit, bytes32 conduitKey, address seaport) {
_CONDUIT = conduit;
_CONDUIT_KEY = conduitKey;
_SEAPORT = seaport;
}
function createCampaign(CampaignParams calldata params, string calldata uri)
external
returns (uint256 campaignId)
{
// Revert if there are no consideration items, since the redemption should require at least something.
if (params.consideration.length == 0) revert NoConsiderationItems();
// Revert if startTime is past endTime.
if (params.startTime > params.endTime) revert InvalidTime();
// Revert if any of the consideration item recipients is the zero address. The 0xdead address should be used instead.
for (uint256 i = 0; i < params.consideration.length;) {
if (params.consideration[i].recipient == address(0)) {
revert ConsiderationItemRecipientCannotBeZeroAddress();
}
unchecked {
++i;
}
}
// Check for and set token approvals for the campaign.
_setTokenApprovals(params);
// Set the campaign params for the next campaignId.
_campaignParams[_nextCampaignId] = params;
// Set the campaign URI for the next campaignId.
_campaignURIs[_nextCampaignId] = uri;
// Set the correct current campaignId to return before incrementing
// the next campaignId.
campaignId = _nextCampaignId;
// Increment the next campaignId.
_nextCampaignId++;
emit CampaignUpdated(campaignId, params, _campaignURIs[campaignId]);
}
function updateCampaign(uint256 campaignId, CampaignParams calldata params, string calldata uri) external {
if (campaignId == 0 || campaignId >= _nextCampaignId) {
revert InvalidCampaignId();
}
// Revert if there are no consideration items, since the redemption should require at least something.
if (params.consideration.length == 0) revert NoConsiderationItems();
// Revert if startTime is past endTime.
if (params.startTime > params.endTime) revert InvalidTime();
// Revert if msg.sender is not the manager.
address existingManager = _campaignParams[campaignId].manager;
if (params.manager != msg.sender && (existingManager != address(0) && existingManager != params.manager)) {
revert NotManager();
}
// Revert if any of the consideration item recipients is the zero address. The 0xdead address should be used instead.
for (uint256 i = 0; i < params.consideration.length;) {
if (params.consideration[i].recipient == address(0)) {
revert ConsiderationItemRecipientCannotBeZeroAddress();
}
unchecked {
++i;
}
}
// Check for and set token approvals for the campaign.
_setTokenApprovals(params);
// Set the campaign params for the given campaignId.
_campaignParams[campaignId] = params;
// Update campaign uri if it was provided.
if (bytes(uri).length != 0) {
_campaignURIs[campaignId] = uri;
}
emit CampaignUpdated(campaignId, params, _campaignURIs[campaignId]);
}
function _setTokenApprovals(CampaignParams memory params) internal {
// Allow Seaport and the conduit as operators on behalf of this contract for offer items to be minted and transferred.
for (uint256 i = 0; i < params.offer.length;) {
// Native items do not need to be approved.
if (params.offer[i].itemType == ItemType.NATIVE) {
revert InvalidNativeOfferItem();
}
// ERC721 and ERC1155 have the same function signatures for isApprovedForAll and setApprovalForAll.
else if (params.offer[i].itemType >= ItemType.ERC721) {
if (!ERC721(params.offer[i].token).isApprovedForAll(_CONDUIT, address(this))) {
ERC721(params.offer[i].token).setApprovalForAll(_CONDUIT, true);
}
// Set the maximum approval amount for ERC20 tokens.
} else {
ERC20(params.offer[i].token).approve(_CONDUIT, type(uint256).max);
}
unchecked {
++i;
}
}
// Allow Seaport and the conduit as operators on behalf of this contract for consideration items to be transferred in the onReceived hooks.
for (uint256 i = 0; i < params.consideration.length;) {
// ERC721 and ERC1155 have the same function signatures for isApprovedForAll and setApprovalForAll.
if (params.consideration[i].itemType >= ItemType.ERC721) {
if (!ERC721(params.consideration[i].token).isApprovedForAll(_CONDUIT, address(this))) {
ERC721(params.consideration[i].token).setApprovalForAll(_CONDUIT, true);
}
// Set the maximum approval amount for ERC20 tokens.
} else {
ERC20(params.consideration[i].token).approve(_CONDUIT, type(uint256).max);
}
unchecked {
++i;
}
}
}
function updateCampaignURI(uint256 campaignId, string calldata uri) external {
CampaignParams storage params = _campaignParams[campaignId];
if (params.manager != msg.sender) revert NotManager();
_campaignURIs[campaignId] = uri;
emit CampaignUpdated(campaignId, params, uri);
}
/**
* @dev Generates an order with the specified minimum and maximum spent
* items, and optional context (supplied as extraData).
*
* @param fulfiller The address of the fulfiller.
* @param minimumReceived The minimum items that the caller must receive.
* @param maximumSpent The maximum items the caller is willing to spend.
* @param context Additional context of the order.
*
* @return offer A tuple containing the offer items.
* @return consideration An array containing the consideration items.
*/
function generateOrder(
address fulfiller,
SpentItem[] calldata minimumReceived,
SpentItem[] calldata maximumSpent,
bytes calldata context // encoded based on the schemaID
) external override returns (SpentItem[] memory offer, ReceivedItem[] memory consideration) {
// Derive the offer and consideration with effects.
(offer, consideration) = _createOrder(fulfiller, minimumReceived, maximumSpent, context, true);
}
/**
* @dev Ratifies an order with the specified offer, consideration, and
* optional context (supplied as extraData).
*
* @custom:param offer The offer items.
* @custom:param consideration The consideration items.
* @custom:param context Additional context of the order.
* @custom:param orderHashes The hashes to ratify.
* @custom:param contractNonce The nonce of the contract.
*
* @return ratifyOrderMagicValue The magic value returned by the contract
* offerer.
*/
function ratifyOrder(
SpentItem[] calldata, /* offer */
ReceivedItem[] calldata, /* consideration */
bytes calldata, /* context */ // encoded based on the schemaID
bytes32[] calldata, /* orderHashes */
uint256 /* contractNonce */
) external pure override returns (bytes4) {
assembly {
// Return the RatifyOrder magic value.
mstore(0, 0xf4dd92ce)
return(0x1c, 32)
}
}
/**
* @dev View function to preview an order generated in response to a minimum
* set of received items, maximum set of spent items, and context
* (supplied as extraData).
*
* @custom:param caller The address of the caller (e.g. Seaport).
* @param fulfiller The address of the fulfiller (e.g. the account
* calling Seaport).
* @param minimumReceived The minimum items that the caller is willing to
* receive.
* @param maximumSpent The maximum items caller is willing to spend.
* @param context Additional context of the order.
*
* @return offer A tuple containing the offer items.
* @return consideration A tuple containing the consideration items.
*/
function previewOrder(
address, /* caller */
address fulfiller,
SpentItem[] calldata minimumReceived,
SpentItem[] calldata maximumSpent,
bytes calldata context // encoded based on the schemaID
) external view override returns (SpentItem[] memory offer, ReceivedItem[] memory consideration) {
// To avoid the solidity compiler complaining about calling a non-view
// function here (_createOrder), we will cast it as a view and use it.
// This is okay because we are not modifying any state when passing
// withEffects=false.
function(
address,
SpentItem[] memory,
SpentItem[] memory,
bytes calldata,
bool
) internal view returns (SpentItem[] memory, ReceivedItem[] memory) fn;
function(
address,
SpentItem[] memory,
SpentItem[] memory,
bytes calldata,
bool
)
internal
returns (
SpentItem[] memory,
ReceivedItem[] memory
) fn2 = _createOrder;
assembly {
fn := fn2
}
// Derive the offer and consideration without effects.
(offer, consideration) = fn(fulfiller, minimumReceived, maximumSpent, context, false);
}
/**
* @dev Gets the metadata for this contract offerer.
*
* @return name The name of the contract offerer.
* @return schemas The schemas supported by the contract offerer.
*/
function getSeaportMetadata()
external
pure
override
returns (
string memory name,
Schema[] memory schemas // map to Seaport Improvement Proposal IDs
)
{
schemas = new Schema[](0);
return ("RedeemablesContractOfferer", schemas);
}
function supportsInterface(bytes4 interfaceId) external view virtual returns (bool) {
return interfaceId == type(ContractOffererInterface).interfaceId
|| interfaceId == type(IERC721Receiver).interfaceId || interfaceId == type(IERC1155Receiver).interfaceId;
}
function _createOrder(
address fulfiller,
SpentItem[] memory minimumReceived,
SpentItem[] memory maximumSpent,
bytes calldata context,
bool withEffects
) internal returns (SpentItem[] memory offer, ReceivedItem[] memory consideration) {
// Get the campaign.
uint256 campaignId = uint256(bytes32(context[0:32]));
CampaignParams storage params = _campaignParams[campaignId];
// Declare an error buffer; first check is that caller is Seaport or the token contract.
uint256 errorBuffer = _cast(msg.sender != _SEAPORT && msg.sender != params.consideration[0].token);
// Check the redemption is active.
errorBuffer |= _cast(_isInactive(params.startTime, params.endTime)) << 1;
// Check max total redemptions would not be exceeded.
errorBuffer |= _cast(_totalRedemptions[campaignId] + maximumSpent.length > params.maxCampaignRedemptions) << 2;
// Get the redemption hash.
bytes32 redemptionHash = bytes32(context[32:64]);
// Check the signature is valid if required.
if (params.signer != address(0)) {
uint256 salt = uint256(bytes32(context[64:96]));
bytes memory signature = context[96:];
// _verifySignature will revert if the signature is invalid or digest is already used.
_verifySignature(params.signer, fulfiller, maximumSpent, redemptionHash, salt, signature, withEffects);
}
if (errorBuffer > 0) {
if (errorBuffer << 255 != 0) {
revert InvalidCaller(msg.sender);
} else if (errorBuffer << 254 != 0) {
revert NotActive(block.timestamp, params.startTime, params.endTime);
} else if (errorBuffer << 253 != 0) {
revert MaxCampaignRedemptionsReached(
_totalRedemptions[campaignId] + maximumSpent.length, params.maxCampaignRedemptions
);
// TODO: do we need this error?
// } else if (errorBuffer << 252 != 0) {
// revert InvalidConsiderationLength(
// maximumSpent.length,
// params.consideration.length
// );
} else if (errorBuffer << 252 != 0) {
revert InvalidConsiderationItem(maximumSpent[0].token, params.consideration[0].token);
} else {
// todo more validation errors
}
}
// Set the offer from the params.
offer = new SpentItem[](params.offer.length);
for (uint256 i = 0; i < params.offer.length;) {
OfferItem memory offerItem = params.offer[i];
uint256 tokenId = IERC721RedemptionMintable(offerItem.token).mintRedemption(address(this), maximumSpent);
// Set the itemType without criteria.
ItemType itemType = offerItem.itemType == ItemType.ERC721_WITH_CRITERIA
? ItemType.ERC721
: offerItem.itemType == ItemType.ERC1155_WITH_CRITERIA ? ItemType.ERC1155 : offerItem.itemType;
offer[i] = SpentItem({
itemType: itemType,
token: offerItem.token,
identifier: tokenId,
amount: offerItem.startAmount // TODO: do we need to calculate amount based on timestamp?
});
unchecked {
++i;
}
}
// Set the consideration from the params.
consideration = new ReceivedItem[](params.consideration.length);
for (uint256 i = 0; i < params.consideration.length;) {
ConsiderationItem memory considerationItem = params.consideration[i];
// TODO: make helper getItemTypeWithoutCriteria
ItemType itemType;
uint256 identifier;
// If consideration item is wildcard criteria item, set itemType to ERC721
// and identifier to the maximumSpent item identifier.
if (
(considerationItem.itemType == ItemType.ERC721_WITH_CRITERIA)
&& (considerationItem.identifierOrCriteria == 0)
) {
itemType = ItemType.ERC721;
identifier = maximumSpent[i].identifier;
} else if (
(considerationItem.itemType == ItemType.ERC1155_WITH_CRITERIA)
&& (considerationItem.identifierOrCriteria == 0)
) {
itemType = ItemType.ERC1155;
identifier = maximumSpent[i].identifier;
} else {
itemType = considerationItem.itemType;
identifier = considerationItem.identifierOrCriteria;
}
consideration[i] = ReceivedItem({
itemType: itemType,
token: considerationItem.token,
identifier: identifier,
amount: considerationItem.startAmount,
recipient: considerationItem.recipient
});
unchecked {
++i;
}
}
// If withEffects is true then make state changes.
if (withEffects) {
// Increment total redemptions.
_totalRedemptions[campaignId] += maximumSpent.length;
SpentItem[] memory spent = new SpentItem[](consideration.length);
for (uint256 i = 0; i < consideration.length;) {
spent[i] = SpentItem({
itemType: consideration[i].itemType,
token: consideration[i].token,
identifier: consideration[i].identifier,
amount: consideration[i].amount
});
unchecked {
++i;
}
}
// Emit Redemption event.
emit Redemption(campaignId, redemptionHash);
}
}
function onERC721Received(
address,
/* operator */
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4) {
if (from == address(0)) {
return IERC721Receiver.onERC721Received.selector;
}
// Get the campaign.
uint256 campaignId = uint256(bytes32(data[0:32]));
CampaignParams storage params = _campaignParams[campaignId];
OfferItem[] memory offer = new OfferItem[](1);
offer[0] = OfferItem({
itemType: ItemType.ERC721_WITH_CRITERIA,
token: params.offer[0].token,
identifierOrCriteria: 0,
startAmount: 1,
endAmount: 1
});
ConsiderationItem[] memory consideration = new ConsiderationItem[](1);
consideration[0] = ConsiderationItem({
itemType: ItemType.ERC721,
token: msg.sender,
identifierOrCriteria: tokenId,
startAmount: 1,
endAmount: 1,
recipient: payable(address(0x000000000000000000000000000000000000dEaD))
});
OrderParameters memory parameters = OrderParameters({
offerer: address(this),
zone: address(0),
offer: offer,
consideration: consideration,
orderType: OrderType.CONTRACT,
startTime: block.timestamp,
endTime: block.timestamp + 10, // TODO: fix
zoneHash: bytes32(0), // TODO: fix
salt: uint256(0), // TODO: fix
conduitKey: _CONDUIT_KEY,
totalOriginalConsiderationItems: consideration.length
});
AdvancedOrder memory order =
AdvancedOrder({parameters: parameters, numerator: 1, denominator: 1, signature: "", extraData: data});
SeaportInterface(_SEAPORT).fulfillAdvancedOrder(order, new CriteriaResolver[](0), _CONDUIT_KEY, from);
return IERC721Receiver.onERC721Received.selector;
}
function onERC1155Received(
address,
/* operator */
address from,
uint256 id,
uint256 value,
bytes calldata data
) external returns (bytes4) {
if (from == address(0)) {
return IERC1155Receiver.onERC1155Received.selector;
}
// Get the campaign.
uint256 campaignId = uint256(bytes32(data[0:32]));
CampaignParams storage params = _campaignParams[campaignId];
SpentItem[] memory minimumReceived = new SpentItem[](1);
minimumReceived[0] = SpentItem({
itemType: ItemType.ERC721,
token: params.offer[0].token,
identifier: params.offer[0].identifierOrCriteria,
amount: params.offer[0].startAmount
});
SpentItem[] memory maximumSpent = new SpentItem[](1);
maximumSpent[0] = SpentItem({itemType: ItemType.ERC1155, token: msg.sender, identifier: id, amount: value});
// _createOrder will revert if any validations fail.
_createOrder(from, minimumReceived, maximumSpent, data, true);
// Transfer the token to the consideration item recipient.
address recipient = _getConsiderationRecipient(params.consideration, msg.sender);
ERC1155(msg.sender).safeTransferFrom(address(this), recipient, id, value, "");
// Transfer the newly minted token to the fulfiller.
ERC721(params.offer[0].token).safeTransferFrom(address(this), from, id, "");
return IERC1155Receiver.onERC1155Received.selector;
}
function onERC1155BatchReceived(
address, /* operator */
address from,
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
) external returns (bytes4) {
if (from == address(0)) {
return IERC1155Receiver.onERC1155BatchReceived.selector;
}
if (ids.length != values.length) revert RedeemMismatchedLengths();
// Get the campaign.
uint256 campaignId = uint256(bytes32(data[0:32]));
CampaignParams storage params = _campaignParams[campaignId];
SpentItem[] memory minimumReceived = new SpentItem[](1);
minimumReceived[0] = SpentItem({
itemType: ItemType.ERC721,
token: params.offer[0].token,
identifier: params.offer[0].identifierOrCriteria,
amount: params.offer[0].startAmount
});
SpentItem[] memory maximumSpent = new SpentItem[](ids.length);
for (uint256 i = 0; i < ids.length;) {
maximumSpent[i] =
SpentItem({itemType: ItemType.ERC1155, token: msg.sender, identifier: ids[i], amount: values[i]});
unchecked {
++i;
}
}
// _createOrder will revert if any validations fail.
_createOrder(from, minimumReceived, maximumSpent, data, true);
// Transfer the tokens to the consideration item recipient.
address recipient = _getConsiderationRecipient(params.consideration, msg.sender);
ERC1155(msg.sender).safeBatchTransferFrom(address(this), recipient, ids, values, "");
// Transfer the newly minted token to the fulfiller.
ERC721(params.offer[0].token).safeTransferFrom(address(this), from, ids[0]);
return IERC1155Receiver.onERC1155BatchReceived.selector;
}
function getCampaign(uint256 campaignId)
external
view
returns (CampaignParams memory params, string memory uri, uint256 totalRedemptions)
{
if (campaignId >= _nextCampaignId) revert InvalidCampaignId();
params = _campaignParams[campaignId];
uri = _campaignURIs[campaignId];
totalRedemptions = _totalRedemptions[campaignId];
}
function _getConsiderationRecipient(ConsiderationItem[] storage consideration, address token)
internal
view
returns (address)
{
for (uint256 i = 0; i < consideration.length;) {
if (consideration[i].token == token) {
return consideration[i].recipient;
}
unchecked {
++i;
}
}
revert ConsiderationRecipientNotFound(token);
}
function _isInactive(uint256 startTime, uint256 endTime) internal view returns (bool inactive) {
// Using the same check for time boundary from Seaport.
// startTime <= block.timestamp < endTime
assembly {
inactive := or(iszero(gt(endTime, timestamp())), gt(startTime, timestamp()))
}
}
function _isValidTokenAddress(CampaignParams memory params, address token) internal pure returns (bool valid) {
for (uint256 i = 0; i < params.consideration.length;) {
if (params.consideration[i].token == token) {
valid = true;
break;
}
unchecked {
++i;
}
}
}
/**
* @notice Internal utility function to remove a uint from a supplied
* enumeration.
*
* @param toRemove The uint to remove.
* @param enumeration The enumerated uints to parse.
*/
function _removeFromEnumeration(uint256 toRemove, uint256[] storage enumeration) internal {
// Cache the length.
uint256 enumerationLength = enumeration.length;
for (uint256 i = 0; i < enumerationLength;) {
// Check if the enumerated element is the one we are deleting.
if (enumeration[i] == toRemove) {
// Swap with the last element.
enumeration[i] = enumeration[enumerationLength - 1];
// Delete the (now duplicated) last element.
enumeration.pop();
// Exit the loop.
break;
}
unchecked {
++i;
}
}
}
/**
* @notice Internal utility function to cast uint types to address
* to dedupe the need for multiple implementations of
* `_removeFromEnumeration`.
*
* @param fnIn The fn with uint input.
*
* @return fnOut The fn with address input.
*/
function _asAddressArray(function(uint256, uint256[] storage) internal fnIn)
internal
pure
returns (function(address, address[] storage) internal fnOut)
{
assembly {
fnOut := fnIn
}
}
/**
* @dev Internal pure function to cast a `bool` value to a `uint256` value.
*
* @param b The `bool` value to cast.
*
* @return u The `uint256` value.
*/
function _cast(bool b) internal pure returns (uint256 u) {
assembly {
u := b
}
}
}
_SEAPORT
address internal immutable _SEAPORT;
概要
Seaportアドレス。
このコントラクトとのインタラクションを許可するためのSeaportのアドレスです。
詳細
Seaportと呼ばれる別のコントラクトとの通信を許可します。
Seaportはこのコントラクトとやり取りし、特典の提供などの操作を実行できます。
_CONDUIT
address internal immutable _CONDUIT;
概要
Conduitアドレス。
このコントラクトの新しくミントされたトークンのオペレーターとして許可されるアドレスです。
詳細
コントラクト内で新しくミントされたトークンに対するオペレーターとして許可されるアドレスを指定します。
Conduitはトークンの操作を行う権限を持ち、特典の提供に関連するトークンを制御できます。
_CONDUIT_KEY
bytes32 internal immutable _CONDUIT_KEY;
概要
このコントラクトで使用されるConduitのキー。
詳細
Conduitの操作を識別するためのキーです。
Conduitはトークンの操作を行うためにこのキーを使用します。
_nextCampaignId
uint256 private _nextCampaignId = 1;
概要
次に作成されるキャンペーンに割り当てるための次の利用可能なキャンペーンIDのカウンター。
詳細
次に作成されるキャンペーンに自動的に割り当てるためのキャンペーンIDを追跡します。
新しいキャンペーンが作成されるたびに、このカウンターは1ずつ増加します。
_campaignParams
mapping(uint256 campaignId => CampaignParams params) private _campaignParams;
概要
キャンペーンIDに対するキャンペーンパラメータを格納するマッピング配列。
詳細
キャンペーンIDをキーとし、関連するキャンペーンのパラメータを値として持ちます。
_campaignURIs
mapping(uint256 campaignId => string campaignURI) private _campaignURIs;
概要
キャンペーンIDに対するキャンペーンのURIを格納するマッピング配列。
詳細
キャンペーンIDをキーとし、URI文字列を値として持ちます。
_totalRedemptions
mapping(uint256 campaignId => uint256 count) private _totalRedemptions;
概要
キャンペーンIDに対する合計のリデンプション数を格納するマッピング配列。
詳細
キャンペーンIDをキーとし、リデンプション数を値として持ちます。
constructor
constructor(address conduit, bytes32 conduitKey, address seaport) {
_CONDUIT = conduit;
_CONDUIT_KEY = conduitKey;
_SEAPORT = seaport;
}
概要
コントラクトのコンストラクタ。
コントラクトを初期化し、必要なアドレス情報を設定します。
引数
-
conduit
- コントラクトが連携する別のコントラクトのアドレス。
-
conduitKey
- コントラクトで使用されるキー情報。
-
seaport
- インターフェースとして使用されるコントラクトのアドレス。
createCampaign
function createCampaign(CampaignParams calldata params, string calldata uri)
external
returns (uint256 campaignId)
{
// Revert if there are no consideration items, since the redemption should require at least something.
if (params.consideration.length == 0) revert NoConsiderationItems();
// Revert if startTime is past endTime.
if (params.startTime > params.endTime) revert InvalidTime();
// Revert if any of the consideration item recipients is the zero address. The 0xdead address should be used instead.
for (uint256 i = 0; i < params.consideration.length;) {
if (params.consideration[i].recipient == address(0)) {
revert ConsiderationItemRecipientCannotBeZeroAddress();
}
unchecked {
++i;
}
}
// Check for and set token approvals for the campaign.
_setTokenApprovals(params);
// Set the campaign params for the next campaignId.
_campaignParams[_nextCampaignId] = params;
// Set the campaign URI for the next campaignId.
_campaignURIs[_nextCampaignId] = uri;
// Set the correct current campaignId to return before incrementing
// the next campaignId.
campaignId = _nextCampaignId;
// Increment the next campaignId.
_nextCampaignId++;
emit CampaignUpdated(campaignId, params, _campaignURIs[campaignId]);
}
概要
新しいキャンペーンを作成する関数。
詳細
キャンペーンのパラメータとURIを指定し、キャンペーンを作成します。
引数
-
params
- キャンペーンのパラメータを指定するデータ構造。
-
uri
- キャンペーンに関連付けるURI情報。
戻り値
作成されたキャンペーンの一意のIDであるcampaignId
を返します。
updateCampaign
function updateCampaign(uint256 campaignId, CampaignParams calldata params, string calldata uri) external {
if (campaignId == 0 || campaignId >= _nextCampaignId) {
revert InvalidCampaignId();
}
// Revert if there are no consideration items, since the redemption should require at least something.
if (params.consideration.length == 0) revert NoConsiderationItems();
// Revert if startTime is past endTime.
if (params.startTime > params.endTime) revert InvalidTime();
// Revert if msg.sender is not the manager.
address existingManager = _campaignParams[campaignId].manager;
if (params.manager != msg.sender && (existingManager != address(0) && existingManager != params.manager)) {
revert NotManager();
}
// Revert if any of the consideration item recipients is the zero address. The 0xdead address should be used instead.
for (uint256 i = 0; i < params.consideration.length;) {
if (params.consideration[i].recipient == address(0)) {
revert ConsiderationItemRecipientCannotBeZeroAddress();
}
unchecked {
++i;
}
}
// Check for and set token approvals for the campaign.
_setTokenApprovals(params);
// Set the campaign params for the given campaignId.
_campaignParams[campaignId] = params;
// Update campaign uri if it was provided.
if (bytes(uri).length != 0) {
_campaignURIs[campaignId] = uri;
}
emit CampaignUpdated(campaignId, params, _campaignURIs[campaignId]);
}
概要
既存のキャンペーンを更新する関数。
詳細
指定したキャンペーンのパラメータとURIを変更できます。
引数
-
campaignId
- 更新対象のキャンペーンの一意のID。
-
params
- 新しいキャンペーンのパラメータを指定するデータ構造。
-
uri
- キャンペーンに関連付ける新しいURI情報。
_setTokenApprovals
function _setTokenApprovals(CampaignParams memory params) internal {
// Allow Seaport and the conduit as operators on behalf of this contract for offer items to be minted and transferred.
for (uint256 i = 0; i < params.offer.length;) {
// Native items do not need to be approved.
if (params.offer[i].itemType == ItemType.NATIVE) {
revert InvalidNativeOfferItem();
}
// ERC721 and ERC1155 have the same function signatures for isApprovedForAll and setApprovalForAll.
else if (params.offer[i].itemType >= ItemType.ERC721) {
if (!ERC721(params.offer[i].token).isApprovedForAll(_CONDUIT, address(this))) {
ERC721(params.offer[i].token).setApprovalForAll(_CONDUIT, true);
}
// Set the maximum approval amount for ERC20 tokens.
} else {
ERC20(params.offer[i].token).approve(_CONDUIT, type(uint256).max);
}
unchecked {
++i;
}
}
// Allow Seaport and the conduit as operators on behalf of this contract for consideration items to be transferred in the onReceived hooks.
for (uint256 i = 0; i < params.consideration.length;) {
// ERC721 and ERC1155 have the same function signatures for isApprovedForAll and setApprovalForAll.
if (params.consideration[i].itemType >= ItemType.ERC721) {
if (!ERC721(params.consideration[i].token).isApprovedForAll(_CONDUIT, address(this))) {
ERC721(params.consideration[i].token).setApprovalForAll(_CONDUIT, true);
}
// Set the maximum approval amount for ERC20 tokens.
} else {
ERC20(params.consideration[i].token).approve(_CONDUIT, type(uint256).max);
}
unchecked {
++i;
}
}
}
概要
キャンペーンに関連するトークンの承認を設定する関数。
詳細
オファーアイテムとコンシダレーションアイテムのトークン承認を処理します。
引数
-
params
- キャンペーンのパラメータを指定するデータ構造。
updateCampaignURI
function updateCampaignURI(uint256 campaignId, string calldata uri) external {
CampaignParams storage params = _campaignParams[campaignId];
if (params.manager != msg.sender) revert NotManager();
_campaignURIs[campaignId] = uri;
emit CampaignUpdated(campaignId, params, uri);
}
概要
指定したキャンペーンのURI情報を更新する関数。
引数
-
campaignId
- 更新対象のキャンペーンの一意のID。
-
uri
- キャンペーンに関連付ける新しいURI情報。
generateOrder
function generateOrder(
address fulfiller,
SpentItem[] calldata minimumReceived,
SpentItem[] calldata maximumSpent,
bytes calldata context // encoded based on the schemaID
) external override returns (SpentItem[] memory offer, ReceivedItem[] memory consideration) {
// Derive the offer and consideration with effects.
(offer, consideration) = _createOrder(fulfiller, minimumReceived, maximumSpent, context, true);
}
概要
注文を生成する関数。
詳細
最小と最大のスペントアイテムを指定し、オプションのコンテキスト情報を提供します。
引数
-
fulfiller
- 注文を履行するアドレス。
-
minimumReceived
- 最小で受け取るべきアイテムを指定するデータ構造。
-
maximumSpent
- 最大で消費できるアイテムを指定するデータ構造。
-
context
- 追加のコンテキスト情報(スキーマIDに基づいてエンコードされたデータ)。
戻り値
-
offer
- 注文のオファーアイテムを含む配列。
-
consideration
- 注文のコンシダレーションアイテムを含む配列。
ratifyOrder
function ratifyOrder(
SpentItem[] calldata, /* offer */
ReceivedItem[] calldata, /* consideration */
bytes calldata, /* context */ // encoded based on the schemaID
bytes32[] calldata, /* orderHashes */
uint256 /* contractNonce */
) external pure override returns (bytes4) {
assembly {
// Return the RatifyOrder magic value.
mstore(0, 0xf4dd92ce)
return(0x1c, 32)
}
}
概要
指定されたオファー(offer
)、対価(consideration
)、およびオプションのコンテキスト(context
)を使用して注文を承認する関数。
引数
-
offer
- オファーされたアイテムを格納したデータの配列。
-
consideration
- 対価のアイテムを格納したデータの配列。
-
context
- 注文の追加情報をエンコードしたデータ。
-
orderHashes
- 承認する注文のハッシュの配列。
-
contractNonce
- コントラクトのノンス(一意の識別子)。
戻り値
-
ratifyOrderMagicValue
- コントラクト提供者から返されるマジック値。
previewOrder
function previewOrder(
address, /* caller */
address fulfiller,
SpentItem[] calldata minimumReceived,
SpentItem[] calldata maximumSpent,
bytes calldata context // encoded based on the schemaID
) external view override returns (SpentItem[] memory offer, ReceivedItem[] memory consideration) {
// To avoid the solidity compiler complaining about calling a non-view
// function here (_createOrder), we will cast it as a view and use it.
// This is okay because we are not modifying any state when passing
// withEffects=false.
function(
address,
SpentItem[] memory,
SpentItem[] memory,
bytes calldata,
bool
) internal view returns (SpentItem[] memory, ReceivedItem[] memory) fn;
function(
address,
SpentItem[] memory,
SpentItem[] memory,
bytes calldata,
bool
)
internal
returns (
SpentItem[] memory,
ReceivedItem[] memory
) fn2 = _createOrder;
assembly {
fn := fn2
}
// Derive the offer and consideration without effects.
(offer, consideration) = fn(fulfiller, minimumReceived, maximumSpent, context, false);
}
概要
受け取る最小のアイテム配列、渡す最大のアイテム配列、およびコンテキストを使用して生成された注文をプレビューする関数。
引数
-
caller
- コールしたアドレス(例:Seaportのアドレス)。
-
fulfiller
- 実行者のアドレス(例:Seaportを呼び出すアカウントのアドレス)。
-
minimumReceived
- 受け取る最小のアイテム配列。
-
maximumSpent
- 渡す最大のアイテム配列。
-
context
- 注文の追加情報をエンコードしたデータ。
戻り値
-
offer
- オファーを受けたアイテムを格納したデータの配列。
-
consideration
- 対価のアイテムを格納したデータの配列。
getSeaportMetadata
function getSeaportMetadata()
external
pure
override
returns (
string memory name,
Schema[] memory schemas // map to Seaport Improvement Proposal IDs
)
{
schemas = new Schema[](0);
return ("RedeemablesContractOfferer", schemas);
}
概要
コントラクト提供者のメタデータを取得する関数。
引数
-
name
- コントラクト提供者の名前。
-
schemas
- コントラクト提供者がサポートするスキーマの配列。
戻り値
-
name
- コントラクト提供者の名前。
-
schemas
- コントラクト提供者がサポートするスキーマの配列。
supportsInterface
function supportsInterface(bytes4 interfaceId) external view virtual returns (bool) {
return interfaceId == type(ContractOffererInterface).interfaceId
|| interfaceId == type(IERC721Receiver).interfaceId || interfaceId == type(IERC1155Receiver).interfaceId;
}
概要
指定されたインターフェースがサポートされているかどうかを確認する関数。
詳細
- インターフェースIDには、コントラクト提供者インターフェース、IERC721Receiver、IERC1155Receiverが含まれます。
引数
-
interfaceId
- 確認するインターフェースのID。
戻り値
- インターフェースがサポートされている場合は
true
、それ以外の場合はfalse
を返します。
_createOrder
function _createOrder(
address fulfiller,
SpentItem[] memory minimumReceived,
SpentItem[] memory maximumSpent,
bytes calldata context,
bool withEffects
) internal returns (SpentItem[] memory offer, ReceivedItem[] memory consideration) {
// キャンペーンを取得します。
uint256 campaignId = uint256(bytes32(context[0:32]));
CampaignParams storage params = _campaignParams[campaignId];
// エラーバッファを宣言します。最初のチェックは、呼び出し元がSeaportまたはトークン契約であることを確認します。
uint256 errorBuffer = _cast(msg.sender != _SEAPORT && msg.sender != params.consideration[0].token);
// 償還がアクティブであることを確認します。
errorBuffer |= _cast(_isInactive(params.startTime, params.endTime)) << 1;
// 最大の合計償還数が超過しないことを確認します。
errorBuffer |= _cast(_totalRedemptions[campaignId] + maximumSpent.length > params.maxCampaignRedemptions) << 2;
// 償還ハッシュを取得します。
bytes32 redemptionHash = bytes32(context[32:64]);
// 必要に応じて署名が有効か確認します。
if (params.signer != address(0)) {
uint256 salt = uint256(bytes32(context[64:96]));
bytes memory signature = context[96:];
// _verifySignatureは、署名が無効であるか、ダイジェストが既に使用されている場合にはリバートします。
_verifySignature(params.signer, fulfiller, maximumSpent, redemptionHash, salt, signature, withEffects);
}
if (errorBuffer > 0) {
if (errorBuffer << 255 != 0) {
revert InvalidCaller(msg.sender);
} else if (errorBuffer << 254 != 0) {
revert NotActive(block.timestamp, params.startTime, params.endTime);
} else if (errorBuffer << 253 != 0) {
revert MaxCampaignRedemptionsReached(
_totalRedemptions[campaignId] + maximumSpent.length, params.maxCampaignRedemptions
);
// TODO: このエラーは必要ですか?
// } else if (errorBuffer << 252 != 0) {
// revert InvalidConsiderationLength(
// maximumSpent.length,
// params.consideration.length
// );
} else if (errorBuffer << 252 != 0) {
revert InvalidConsiderationItem(maximumSpent[0].token, params.consideration[0].token);
} else {
// その他のバリデーションエラーを追加する予定
}
}
// パラメータからオファーを設定します。
offer = new SpentItem[](params.offer.length);
for (uint256 i = 0; i < params.offer.length;) {
OfferItem memory offerItem = params.offer[i];
uint256 tokenId = IERC721RedemptionMintable(offerItem.token).mintRedemption(address(this), maximumSpent);
// 基準を持たないアイテムの場合、アイテムタイプを設定します。
ItemType itemType = offerItem.itemType == ItemType.ERC721_WITH_CRITERIA
? ItemType.ERC721
: offerItem.itemType == ItemType.ERC1155_WITH_CRITERIA ? ItemType.ERC1155 : offerItem.itemType;
offer[i] = SpentItem({
itemType: itemType,
token: offerItem.token,
identifier: tokenId,
amount: offerItem.startAmount // TODO: タイムスタンプに基づいて金額を計算する必要がありますか?
});
unchecked {
++i;
}
}
// パラメータからコンシデレーションを設定します。
consideration = new ReceivedItem[](params.consideration.length);
for (uint256 i = 0; i < params.consideration.length;) {
ConsiderationItem memory considerationItem = params.consideration[i];
// TODO: getItemTypeWithoutCriteriaヘルパーを作成する
ItemType itemType;
uint256 identifier;
// コンシデレーションアイテムがワイルドカード基準アイテムの場合、アイテムタイプをERC721に設定し、
// 識別子をmaximumSpentアイテムの識別子に設定します。
if (
(considerationItem.itemType == ItemType.ERC721_WITH_CRITERIA)
&& (considerationItem.identifierOrCriteria == 0)
) {
itemType = ItemType.ERC721;
identifier = maximumSpent[i].identifier;
} else if (
(considerationItem.itemType == ItemType.ERC1155_WITH_CRITERIA)
&& (considerationItem.identifierOrCriteria == 0)
) {
itemType = ItemType.ERC1155;
identifier = maximumSpent[i].identifier;
} else {
itemType = considerationItem.itemType;
identifier = considerationItem.identifierOrCriteria;
}
consideration[i] = ReceivedItem({
itemType: itemType,
token: considerationItem.token,
identifier: identifier,
amount: considerationItem.startAmount,
recipient: considerationItem.recipient
});
unchecked {
++i;
}
}
// withEffectsがtrueの場合、ステート変更を行います。
if (withEffects) {
// 合計償還数を増やします。
_totalRedemptions[campaignId] += maximumSpent.length;
SpentItem[] memory spent = new SpentItem[](consideration.length);
for (uint256 i = 0; i < consideration.length;) {
spent[i] = SpentItem({
itemType: consideration[i].itemType,
token: consideration[i].token,
identifier: consideration[i].identifier,
amount: consideration[i].amount
});
unchecked {
++i;
}
}
// 償還イベントを発行します。
emit Redemption(campaignId, redemptionHash);
}
}
概要
償還オーダーを作成し、検証および処理を行う関数。
詳細
- キャンペーンおよびパラメータを取得します。
- エラーバッファを使用して検証を行い、エラーがある場合はリバートします。
- オファーアイテムとコンシデレーションアイテムを設定します。
-
withEffects
がtrue
の場合、ステート変更を行います。 - 償還イベントを発行します。
引数
-
fulfiller
- 償還を実行するアドレス。
-
minimumReceived
- 最小償還アイテムの配列。
-
maximumSpent
- 最大償還アイテムの配列。
-
context
- コンテキストデータ。
-
withEffects
- ステート変更を行うかどうかを示すブール値。
戻り値
-
offer
- オファーアイテムの配列。
-
consideration
- コンシデレーションアイテムの配列。
コンシデレーションアイテム
コントラクトや取引において提供される対価や報酬としてのアイテムやサービス。
コントラクトにおいて、各当事者は何かしらの価値あるもの(アイテム、サービス、お金など)を提供し、それに対して相手方からも同様に何かしらの価値あるものを提供する必要があります。
償還(Redemption)
特典や権利を受け取ることを指します。
具体的には、NFT(非代替性トークン)やトークンを所有しているユーザーが、その所有物に関連する特典や報酬を受け取るプロセスを指しています。
NFTやトークンに関連する特典や報酬が提供される特別なキャンペーンや仕組みが考えられており、ユーザーが特典を受け取るために特定の条件を満たす必要があります。
例えば、特定のNFTを所有しているユーザーが、そのNFTに関連するデジタルコンテンツやアイテムを償還するために特別な手順を実行することが考えられます。
onERC721Received
function onERC721Received(
address,
/* operator */
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4) {
if (from == address(0)) {
return IERC721Receiver.onERC721Received.selector;
}
// キャンペーンを取得します。
uint256 campaignId = uint256(bytes32(data[0:32]));
CampaignParams storage params = _campaignParams[campaignId];
OfferItem[] memory offer = new OfferItem[](1);
offer[0] = OfferItem({
itemType: ItemType.ERC721_WITH_CRITERIA,
token: params.offer[0].token,
identifierOrCriteria: 0,
startAmount: 1,
endAmount: 1
});
ConsiderationItem[] memory consideration = new ConsiderationItem[](1);
consideration[0] = ConsiderationItem({
itemType: ItemType.ERC721,
token: msg.sender,
identifierOrCriteria: tokenId,
startAmount: 1,
endAmount: 1,
recipient: payable(address(0x000000000000000000000000000000000000dEaD))
});
OrderParameters memory parameters = OrderParameters({
offerer: address(this),
zone: address(0),
offer: offer,
consideration: consideration,
orderType: OrderType.CONTRACT,
startTime: block.timestamp,
endTime: block.timestamp + 10, // TODO: 修正が必要
zoneHash: bytes32(0), // TODO: 修正が必要
salt: uint256(0), // TODO: 修正が必要
conduitKey: _CONDUIT_KEY,
totalOriginalConsiderationItems: consideration.length
});
AdvancedOrder memory order =
AdvancedOrder({parameters: parameters, numerator: 1, denominator: 1, signature: "", extraData: data});
SeaportInterface(_SEAPORT).fulfillAdvancedOrder(order, new CriteriaResolver[](0), _CONDUIT_KEY, from);
return IERC721Receiver.onERC721Received.selector;
}
概要
ERC721トークンを受け取ったときに呼び出され、償還オーダーを作成して処理する関数。
詳細
-
from
アドレスがゼロでない場合、ERC721トークンを受け取ります。 - キャンペーンおよびパラメータを取得します。
- オファーアイテムとコンシデレーションアイテムを設定し、オーダーパラメータを構築します。
-
Seaportを介して
AdvancedOrder
を実行します。
引数
-
from
- 送信元アドレス。
-
tokenId
- ERC721トークンのID。
-
data
- コンテキストデータ。
戻り値
-
bytes4
d-
ERC721Received
関数のセレクター。
-
onERC1155Received
function onERC1155Received(
address,
/* operator */
address from,
uint256 id,
uint256 value,
bytes calldata data
) external returns (bytes4) {
if (from == address(0)) {
return IERC1155Receiver.onERC1155Received.selector;
}
// キャンペーンを取得します。
uint256 campaignId = uint256(bytes32(data[0:32]));
CampaignParams storage params = _campaignParams[campaignId];
SpentItem[] memory minimumReceived = new SpentItem[](1);
minimumReceived[0] = SpentItem({
itemType: ItemType.ERC721,
token: params.offer[0].token,
identifier: params.offer[0].identifierOrCriteria,
amount: params.offer[0].startAmount
});
SpentItem[] memory maximumSpent = new SpentItem[](1);
maximumSpent[0] = SpentItem({itemType: ItemType.ERC1155, token: msg.sender, identifier: id, amount: value});
// _createOrderが失敗した場合はリバートします。
_createOrder(from, minimumReceived, maximumSpent, data, true);
// トークンをコンシデレーションアイテムの受取人に転送します。
address recipient = _getConsiderationRecipient(params.consideration, msg.sender);
ERC1155(msg.sender).safeTransferFrom(address(this), recipient, id, value, "");
// 新しく作成されたトークンを履行者に転送します。
ERC721(params.offer[0].token).safeTransferFrom(address(this), from, id, "");
return IERC1155Receiver.onERC1155Received.selector
;
}
概要
ERC1155トークンを受け取ったときに呼び出され、償還オーダーを作成して処理する関数。
詳細
-
from
アドレスがゼロでない場合、ERC1155トークンを受け取ります。 - キャンペーンおよびパラメータを取得します。
- 最小償還アイテムと最大償還アイテムを設定し、
_createOrder
で検証と処理を行います。 - トークンをコンシデレーションアイテムの受取アドレスに転送し、新しく作成されたトークンを実行者に転送します。
引数
-
from
- 送信元アドレス。
-
id
- ERC1155トークンのID。
-
value
- トークンの数量。
-
data
- コンテキストデータ。
戻り値
-
bytes4
-
ERC1155Received
関数のセレクター。
-
onERC1155BatchReceived
function onERC1155BatchReceived(
address, /* operator */
address from,
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
) external returns (bytes4) {
if (from == address(0)) {
return IERC1155Receiver.onERC1155BatchReceived.selector;
}
if (ids.length != values.length) revert RedeemMismatchedLengths();
// キャンペーンを取得します。
uint256 campaignId = uint256(bytes32(data[0:32]));
CampaignParams storage params = _campaignParams[campaignId];
SpentItem[] memory minimumReceived = new SpentItem[](1);
minimumReceived[0] = SpentItem({
itemType: ItemType.ERC721,
token: params.offer[0].token,
identifier: params.offer[0].identifierOrCriteria,
amount: params.offer[0].startAmount
});
SpentItem[] memory maximumSpent = new SpentItem[](ids.length);
for (uint256 i = 0; i < ids.length;) {
maximumSpent[i] =
SpentItem({itemType: ItemType.ERC1155, token: msg.sender, identifier: ids[i], amount: values[i]});
unchecked {
++i;
}
}
// _createOrderが失敗した場合はリバートします。
_createOrder(from, minimumReceived, maximumSpent, data, true);
// トークンをコンシデレーションアイテムの受取人に転送します。
address recipient = _getConsiderationRecipient(params.consideration, msg.sender);
ERC1155(msg.sender).safeBatchTransferFrom(address(this), recipient, ids, values, "");
// 新しく作成されたトークンを履行者に転送します。
ERC721(params.offer[0].token).safeTransferFrom(address(this), from, ids[0]);
return IERC1155Receiver.onERC1155BatchReceived.selector;
}
概要
ERC1155トークンのバッチを受け取ったときに呼び出され、償還オーダーを作成して処理する関数。
詳細
-
from
アドレスがゼロでない場合、ERC1155トークンのバッチを受け取ります。 -
ids
とvalues
の長さが一致しない場合はエラーとしてRedeemMismatchedLengths
を呼び出して処理がリバートされます。 - キャンペーンおよびパラメータを取得します。
- 最小償還アイテムと最大償還アイテムを設定し、
_createOrder
で検証と処理を行います。 - トークンをコンシデレーションアイテムの受取アドレスに転送し、新しく作成されたトークンを履行者に転送します。
引数
-
from
- 送信元アドレス。
-
ids
- ERC1155トークンのIDの配列。
-
values
- 各トークンの数量の配列。
-
data
- コンテキストデータ。
戻り値
-
bytes4
-
ERC1155BatchReceived
関数のセレクター。
-
getCampaign
function getCampaign(uint256 campaignId)
external
view
returns (CampaignParams memory params, string memory uri, uint256 totalRedemptions)
{
if (campaignId >= _nextCampaignId) revert InvalidCampaignId();
params = _campaignParams[campaignId];
uri = _campaignURIs[campaignId];
totalRedemptions = _totalRedemptions[campaignId];
}
概要
指定されたキャンペーンに関する情報を取得する関数。
詳細
-
campaignId
に基づいてキャンペーンのパラメータ、URI、および合計償還数を取得します。
引数
-
campaignId
- キャンペーンのID。
戻り値
-
params
- キャンペーンのパラメータ。
-
uri
- キャンペーンのURI。
-
totalRedemptions
- 合計償還数。
_getConsiderationRecipient
function _getConsiderationRecipient(ConsiderationItem[] storage consideration, address token)
internal
view
returns (address)
{
for (uint256 i = 0; i < consideration.length;) {
if (consideration[i].token == token) {
return consideration[i].recipient;
}
unchecked {
++i;
}
}
revert ConsiderationRecipientNotFound(token);
}
概要
指定されたトークンに関連付けられた受取アドレスを取得する関数。
詳細
-
consideration
配列内の各アイテムを確認し、指定されたトークンに対応する受取アドレスを検索します。 - 見つかればそのアドレスを返します。
- 見つからない場合は
ConsiderationRecipientNotFound
エラーを投げて処理をリバートします。
引数
-
consideration
- コンシデレーションアイテムの配列。
-
token
- トークンアドレス。
戻り値
-
address
- トークンに対応する受取アドレス。
_isInactive
function _isInactive(uint256 startTime, uint256 endTime) internal view returns (bool inactive) {
// Seaportと同じ時間境界のチェックを使用します。
// startTime <= block.timestamp < endTime
assembly {
inactive := or(iszero(gt(endTime, timestamp())), gt(startTime, timestamp()))
}
}
概要
キャンペーンが非アクティブかを確認する関数。
詳細
- キャンペーンの開始時間
startTime
および終了時間endTime
を比較し、現在のタイムスタンプがその範囲内にあるかどうかをチェックします。 - 現在がキャンペーンの範囲外にある場合、キャンペーンは非アクティブと判定されます。
引数
-
startTime
- キャンペーンの開始時間。
-
endTime
- キャンペーンの終了時間。
戻り値
-
inactive
- キャンペーンが非アクティブである場合に
true
、アクティブである場合にfalse
。
- キャンペーンが非アクティブである場合に
_isValidTokenAddress
function _isValidTokenAddress(CampaignParams memory params, address token) internal pure returns (bool valid) {
for (uint256 i = 0; i < params.consideration.length;) {
if (params.consideration[i].token == token) {
valid = true;
break;
}
unchecked {
++i;
}
}
}
概要
指定されたトークンアドレスが有効かどうか判定する関数。
詳細
- キャンペーンのパラメータ
params
から、指定されたトークンアドレスtoken
がコンシデレーションアイテムの中に存在するかどうかをチェックします。 - トークンが有効であれば
valid
をtrue
に設定します。
引数
-
params
- キャンペーンのパラメータ。
-
token
- チェック対象のトークンアドレス。
戻り値
-
valid
- トークンが有効である場合に
true
、無効である場合にfalse
。
- トークンが有効である場合に
_removeFromEnumeration
function _removeFromEnumeration(uint256 toRemove, uint256[] storage enumeration) internal {
// 長さをキャッシュします。
uint256 enumerationLength = enumeration.length;
for (uint256 i = 0; i < enumerationLength;) {
// 削除対象の要素が見つかった場合
if (enumeration[i] == toRemove) {
// 最後の要素と交換します。
enumeration[i] = enumeration[enumerationLength - 1];
// (現在は重複している)最後の要素を削除します。
enumeration.pop();
// ループを終了します。
break;
}
unchecked {
++i;
}
}
}
概要
指定された値を配列から削除する関数。
詳細
-
enumeration
配列内の各要素を確認し、指定された値toRemove
と一致する要素を見つけた場合、その要素を最後の要素と交換し、配列から削除します。
引数
-
toRemove
- 削除する値。
-
enumeration
- 値の配列。
_asAddressArray
function _asAddressArray(function(uint256, uint256[] storage) internal fnIn)
internal
pure
returns (function(address, address[] storage) internal fnOut)
{
assembly {
fnOut := fnIn
}
}
概要
uint256
型の引数を受け取る関数をaddress
型の引数を受け取る関数に変換する関数。
詳細
-
fnIn
としてuint256
型の引数を受け取る内部関数を受け取り、同じ機能を持つがaddress
型の引数を受け取る関数fnOut
を返します。
引数
-
fnIn
-
uint256
型の引数を受け取る関数。
-
戻り値
-
fnOut
-
address
型の引数を受け取る内部関数。
-
_cast
function _cast(bool b) internal pure returns (uint256 u) {
assembly {
u := b
}
}
概要
bool
型の値をuint256
型の値にキャストする関数。
詳細
-
bool
型の値b
をuint256
型の値u
にキャストします。
引数
-
b
- キャストする
bool
値。
- キャストする
戻り値
-
u
- キャストされた
uint256
値。
- キャストされた
セキュリティ考慮事項
-
トークンはEIP7496 Dynamic Traitsを正しく実装する必要があります。
これにより、特性の引き換え(Trait Redemptions)が可能になります。
特性の引き換えは、トークンが持つ特定の特性(たとえば、特定のレベルやキャラクターの特性)を利用して、特典を受け取る機能を指します。
トークンがこの仕組みを正しく実装していない場合、特性の引き換えが適切に動作しない可能性があります。
つまり、トークンは特性を認識し、それを利用して特典を提供できるように設計されている必要があります。 -
パラメーターの
offer
に含まれるトークンをミント(発行)するために、IRedemptionMintable
の一部として含まれるmintRedemption
関数は、適切な権限設定が必要です。
どのアドレスからこの関数を呼び出すことができるかを制御する必要があります。
特典を提供するために新しいトークンを作成する場合、誰でもがこの関数を呼び出すことを制限し、特定のアドレスからのみ呼び出しを許可する必要があります。
権限の不適切な設定はセキュリティリスクを引き起こす可能性があり、不正なトークンの発行を防ぐために慎重に設定する必要があります。
最後に
今回は「Openseaが新たに提案している、NFTにオンチェーン・オフチェーン問わずさまざまなデジタルアセットを紐付ける仕組みであるERC7498」についてまとめてきました!
いかがだったでしょうか?
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!