はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、1つのNFTを複数のアドレスで共同所有する仕組みであるMO-NFを提案しているERC7743についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。

概要

ERC7743は、複数の所有者を持つことができる新しいNFT標準「MO-NFT(Multi-Owner NFT)」を定義する提案です。 従来のNFT(Non-Fungible Token:非代替性トークン)は単一の所有者しか持てませんでしたが、MO-NFTでは1つのNFTを複数人で共有して共同で所有することが可能になります。 この仕組みにより、デジタルアセット(画像・音楽・メタバース内のアイテムなど)の共有的で分散的な特性をより正確に反映できます。

さらに、MO-NFT標準には次のような機能が組み込まれています。

機能 説明
プロバイダー定義の移転手数料 NFTの発行者や提供者が、譲渡時の手数料(transfer fee)を自由に設定できる仕組み。二次流通などでも利益を得られるようになる。
所有権アーカイブ機構 過去の所有者や所有履歴を保存し、NFTの透明性や信頼性を高める。所有履歴が明確になることで、NFTのこれまでの保有履歴を保証できる。

MO-NFTは既存のERC721標準との互換性を維持しており、すでに存在するツールやプラットフォームでも容易に利用できるよう設計されています。 つまり、新しい標準を導入しても、既存のNFTエコシステムとの相互運用性(interoperability)が確保されています。

動機

従来のNFTは「唯一無二のトークン」という特性から、所有者が一人であることを前提としています。 しかし、実際のデジタルコンテンツやサービスは、共同制作・共同利用・共有収益などが一般的であり、単一所有モデルではその実態を十分に反映できません。

  • 複数人で作成したデジタルアート
  • DAO(分散型自律組織)で共同購入したバーチャルアイテム
  • 音楽・動画・ゲームアイテムなどの共同配信収益モデル といったケースでは、「1つのNFTを複数人で持つ」ことが自然です。

MO-NFTの登場により、以下のようなメリットが生まれます。

観点 説明
共有所有(Shared Ownership) 1つのNFTを複数人が分割または共同で所有できる。これにより、作品や資産をチームやコミュニティで保有することが可能になる。
コラボレーション促進 NFTを共同で保有することで、クリエイターや利用者同士の協働が活発化する。特に、共同クリエイティブやDAO運営での利用が期待される。
柔軟な手数料設計 提供者(発行者)がNFTの二次流通時などに手数料を設定できるため、持続的な収益構造を実現できる。
アーカイブによる透明性 所有者の変遷が明確に記録されるため、トークンの信頼性・履歴の正当性を保証できる。

このように、MO-NFTは「NFTを1人が独占的に所有するもの」という従来の考え方を超え、デジタル資産の共有・分散・協働的な利用を可能にする新しい概念を示しています。 また、既存のERC721との互換性を持つことで、従来のNFTインフラをそのまま活用しながらもより柔軟で現実的なデジタル資産管理を実現します。

仕様

トークン生成と所有モデル

トークンの発行(Minting)

MO-NFTでは、新しいトークンを発行する時にmintToken()関数を使用します。 これにより、呼び出し元が最初の所有者(Owner)かつ提供者(Provider)として登録されます。

mintToken
javascript
function mintToken() public onlyOwner returns (uint256);

新しいMO-NFTトークンを発行する関数。

呼び出し元が新しいトークンの提供者(Provider)および最初の所有者(Owner)として登録されます。 トークンID(tokenId)が新たに生成され、呼び出し元が所有者リストに追加されます。 同時に、その所有者のbalanceOfが1増加します。

戻り値

  • uint256
    • 新しく発行されたトークンの識別子(tokenId)。

所有者リスト

MO-NFTでは、各トークンに対して所有者のリストを保持します。 このリストは重複を防ぎ、高速な検索を可能にするためにEnumerable Setとして管理されます。 つまり、1つのNFTを複数人が共有でき、各所有者を効率的に確認できます。

プロバイダーのロール

プロバイダーとは、そのトークンを最初に作成した人物(アドレス)です。 プロバイダーは、transferValue(譲渡手数料)の設定や更新を行う権限を持っています。 また、一部のトークン設定を変更できるのもプロバイダーのみです。

Transferメカニズム

MO-NFTでは、transferFromを使ってトークンを送付します。 ただし、従来のNFTと異なり、送付しても既存の所有者は削除されずに新しい所有者が追加されます。 送付時には、設定されたtransferValueがプロバイダーに支払われます。

transferFrom
javascript
function transferFrom(address from, address to, uint256 tokenId) public;

MO-NFTを別の所有者に送付する関数。

この関数では、既存の所有者を削除せずに新しい所有者を追加します。 また、送付時に設定されたtransferValue手数料をプロバイダーに送金します。 これにより、NFTの所有が共同的に拡大していく仕組みが実現します。

引数

  • address from
    • 現在の所有者のアドレス。
  • address to
    • 新しい所有者のアドレス。
  • uint256 tokenId
    • 送付するトークンの識別子。

所有権の移転

加算的所有

MO-NFTでは、送付によって新しい所有者が追加されるだけで、既存の所有者が消えることはありません。 これにより、デジタル資産の「共有」という性質を正確に表現できます。

プロバイダー手数料の扱い

送付時には、設定されたtransferValueがプロバイダーに支払われます。 そのため、コントラクトには十分な残高が必要です。 これにより、プロバイダーが持続的に収益を得られる仕組みが作られます。

所有者のアーカイブ化

MO-NFTでは、所有者が自らを「アーカイブ状態」に設定することができます。 アーカイブされた所有者は、そのトークンを送付することができなくなります。 一度アーカイブされた状態は取り消しできません。 また、デジタルアセットプラットフォーム上では、アーカイブ状態で初めて実際のデジタルデータをダウンロードできるようになります。

archive
javascript
function archive(uint256 tokenId) external;

特定のトークンについて、自身をアーカイブ状態に変更する関数。

一度アーカイブに設定すると、その所有者はトークンを送付できなくなります。 デジタルアセットの実データをダウンロードするには、まずアーカイブ状態にする必要があります。

引数

  • uint256 tokenId
    • アーカイブ対象のトークンの識別子。

インターフェース

MO-NFTでは、各種操作を行うために以下のインターフェースが定義されています。 ここまでで解説していない関数を取り上げます。

provide

javascript
function provide(string memory assetName, uint256 size, bytes32 fileHash, address provider, uint256 transferValue) external returns (uint256);

デジタルアセット情報を登録して新しいトークンを生成する関数。

資産の名前、サイズ、ファイルハッシュ、提供者情報、手数料を指定してMO-NFTを作成します。 この関数により、資産情報をメタデータとしてブロックチェーン上に記録できます。

引数

  • string memory assetName
    • 資産の名称。
  • uint256 size
    • ファイルサイズなどの資産容量情報。
  • bytes32 fileHash
    • デジタル資産のハッシュ値。
  • address provider
    • 提供者(発行者)のアドレス。
  • uint256 transferValue
    • 送付時に支払われる手数料。

戻り値

  • uint256
    • 新しく生成されたトークンの識別子。

safeTransferFrom

javascript
function safeTransferFrom(address from, address to, uint256 tokenId) public;
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public;

安全な送付機能を提供する関数ですが、MO-NFTでは無効化または上書きされています。

従来のERC721のsafeTransferFromでは、単一所有者を前提とした安全チェックを行いますが、MO-NFTのような加算的所有モデルではその前提が成り立たないため、これらの関数は無効化されています。

所有管理関数

javascript
function isOwner(uint256 tokenId, address account) public view returns (bool);
function getOwnersCount(uint256 tokenId) public view returns (uint256);
function balanceOf(address owner) external view returns (uint256 balance);
function ownerOf(uint256 tokenId) external view returns (address owner);
関数名 説明
isOwner 指定アカウントが特定のトークンの所有者かどうかを確認。
getOwnersCount 特定のトークンの所有者数を取得。
balanceOf 所有者が持つMO-NFTの数を返す。
ownerOf トークンの代表的な所有者を返す(初期提供者など)。

setTransferValue

javascript
function setTransferValue(uint256 tokenId, uint256 newTransferValue) external;

指定したトークンの譲渡手数料(transferValue)を更新する関数。

この関数はプロバイダーのみが実行でき、NFTの送付に必要な手数料を変更します。 手数料を柔軟に設定することで、二次流通での取引を調整できます。

引数

  • uint256 tokenId
    • 対象トークンの識別子。
  • uint256 newTransferValue
    • 新しい手数料。

TokenMinted

javascript
event TokenMinted(uint256 indexed tokenId, address indexed owner);

トークンが発行された時に発行されるイベント。 新しいMO-NFTが生成された時に、そのトークンIDと所有者情報を記録します。

パラメータ

  • tokenId
    • 新しく発行されたトークンの識別子。
  • owner
    • 最初の所有者のアドレス。

TokenTransferred

javascript
event TokenTransferred(uint256 indexed tokenId, address indexed from, address indexed to);

トークンが送付された時に発行されるイベント。 トークンの所有権が追加された時に、送信者・受信者情報を通知します。

パラメータ

  • tokenId
    • 対象のトークンID。
  • from
    • 送信者のアドレス。
  • to
    • 新しい所有者のアドレス。

TransferValueUpdated

javascript
event TransferValueUpdated(uint256 indexed tokenId, uint256 oldTransferValue, uint256 newTransferValue);

トークンの手数料設定が変更された時に発行されるイベント。 手数料が変更されたことを監視ツールや外部システムに通知します。

パラメータ

  • tokenId
    • 対象トークンの識別子。
  • oldTransferValue
    • 以前の手数料。
  • newTransferValue
    • 新しい手数料。

ArchivedStatusUpdated

javascript
event ArchivedStatusUpdated(uint256 indexed tokenId, address indexed owner, bool archived);

所有者がアーカイブ状態に変更された時に発行されるイベント。 アーカイブの設定変更をトラッキングするために利用されます。

パラメータ

  • tokenId
    • 対象トークン。
  • owner
    • 所有者のアドレス。
  • archived
    • アーカイブ状態(true/false)。

ERC721互換性

MO-NFTはERC721の主要関数(balanceOf、ownerOf、transferFrom)を実装し、既存ツールとの互換性を保っています。

ただし、以下の機能は無効化または変更されています:

機能 状態 理由
approve, getApproved, setApprovalForAll, isApprovedForAll 無効化 単一所有モデルに基づく承認機構がMO-NFTには適さないため。
safeTransferFrom 上書き 所有が追加的(Additive)であり、伝統的な安全チェックが不要。

互換性確認のため、以下の関数が定義されています。

supportsInterface

javascript
function supportsInterface(bytes4 interfaceId) public view returns (bool) {
    return interfaceId == type(IERC721).interfaceId || interfaceId == type(IERC165).interfaceId;
}

MO-NFTがERC721およびERC165に準拠していることを示す関数。 この関数により、他のプラットフォームやウォレットがMO-NFTを正しく認識・連携できます。

引数

  • bytes4 interfaceId
    • 確認対象のインターフェースID。

戻り値

  • bool
    • 対応していればtrue、非対応ならfalse。

補足

複数所有モデル(Multi-Ownership Model)

デジタル資産は複製可能であり、品質を損なうことなく共有することができます。 例えば、デジタルアート、音楽、3Dモデルなどは、共有や共同利用が前提となっていることが多いです。 しかし、従来のERC721モデルでは「1つのNFT=1人の所有者」という前提が固定されていました。 これでは、現実のデジタルコラボレーションの仕組みを表現するのに限界があります。

MO-NFTの複数所有モデルは、これを拡張して1つのトークンに複数の所有者を登録できる構造を採用しています。 これにより、以下を実現します。

特徴 説明
デジタルアセットの共有性 同一トークンを複数人で保有することで、デジタル作品やサービスを共同所有できる。
一意性の維持 共有されてもトークン自体は「唯一のもの」として存在し続ける。
コラボレーション支援 チーム制作や分散型所有(DAOなど)において、トークンを分け合うことが可能になる。

このように、MO-NFTはデジタル資産の「複製可能で共有される」性質と、「一意なトークン」という二つの要素を両立しています。

加算的所有

MO-NFTでは、所有権の移転が「置き換え」ではなく「追加」という形で行われます。 つまり、既存の所有者を削除せずに新しい所有者を追加します。

従来のNFTは、Aが持つNFTをBに送付した場合、Aの所有権が消えてBだけが新たな所有者となります。 しかしMO-NFTでは、Aの所有権を残したままBが加わるため、共同所有が成立します。

この設計は、以下のような環境で特に有効です。

使用例 説明
共同制作(Collaborative Creation) アーティストや開発者が共同で作成した作品を、全員が所有者として記録できる。
教育・研究の共有資産 学術・教育用コンテンツを共同で保有し、全員に利用権を与えることができる。
デジタル配信プラットフォーム コンテンツ提供者や配信者が共有収益モデルを持つ場合に、所有関係を明確化できる。

このような「追加的所有」の概念は、デジタル社会における所有権の新しいあり方を象徴しています。

プロバイダー手数料の仕組み

MO-NFTには、プロバイダー手数料(Provider Fee)の概念が組み込まれています。 これは、NFTの送付や売買が行われるたびに、元の提供者(プロバイダー)へ一定の手数料を自動で支払う仕組みです。

この機能は、以下のようなメリットを生みます。

観点 説明
クリエイター支援 制作者が作品の二次流通からも報酬を得られる。
継続的インセンティブ 提供者が長期的に作品を流通させる動機を持つ。
公平な報酬モデル コントラクトにより報酬分配が自動化され、透明性が高い。

これは、音楽やアートの世界で一般的なロイヤリティモデルをブロックチェーン上で実現したものと言えます。 クリエイターに対して継続的な報酬を提供する仕組みとして、MO-NFTの重要な要素です。

所有権アーカイブ

MO-NFTには、所有者が自らを「アーカイブ状態」に設定できる仕組みがあります。 これは、所有者が自分の権利を保持しながらも「そのNFTをこれ以上送付しない」ことを宣言するようなものです。 この設計は、従来のNFTにおけるBurn機能の代替として導入されています。

特徴 説明
柔軟な権利放棄 所有者は自分の意思で送付権を無効化できる。
デジタルアセットの最終受領 アーカイブ状態にすると、実データ(例:画像・音源)を直接ダウンロードできる。
透明性と履歴の保持 アーカイブ設定はブロックチェーンに記録され、後から確認可能。

つまり、「アーカイブ」は所有権の削除ではなく、「保持しながら固定する」行為です。 これにより、NFTのライフサイクル管理がより現実的で柔軟になります。

ERC721互換性

MO-NFTは完全な新規設計ではなく、既存のERC721標準を拡張する形で開発されています。 これにより、既存のNFTインフラやツールとの互換性と相互運用性(interoperability)を維持しています。

要素 説明
互換関数の保持 balanceOf、ownerOf、transferFromなどの主要関数を実装。
非対応関数の除外 approveやsafeTransferFromなど、単一所有前提の関数は無効化。
インターフェース宣言 supportsInterfaceによりERC721およびERC165との互換性を明示。

この設計により、MO-NFTは既存のウォレット、NFTマーケットプレイス、データビューワなどに対応可能です。 つまり、新しいモデルを導入しながらも、既存エコシステムの利用を妨げません。

互換性

MO-NFTはERC721標準との互換性をできる限り維持することを目指して設計されています。

承認機能の無効化

ERC721では、approveやsetApprovalForAllといった関数を通じて、所有者が他者にトークン送付の許可を与えることができます。 これは「1つのトークンに1人の所有者」が前提の設計です。

一方、MO-NFTでは1つのトークンに複数の所有者が存在するため、この仕組みがそのままでは成立しません。 仮に1人の所有者が承認を与えられると、他の共同所有者の意思に反してトークンが送付される可能性があります。 つまり、「共同所有の合意」という概念が存在しないまま承認を許可すると、意図しない取引や不正送付のリスクが生じるのです。

そのため、MO-NFTでは承認関連の関数呼び出しを無効化(revert)する設計が採用されています。 これにより、以下のような特徴があります。

項目 内容
無効化される関数 approve、getApproved、setApprovalForAll、isApprovedForAll
理由 複数所有者がいるため、単独の承認では不整合や不正の原因になるため。
結果 単一所有を前提とするマーケットプレイスでは一部機能が動作しない可能性がある。

この設計によって、既存のNFTマーケットプレイス(例:OpenSeaなど)での互換性が部分的に制限される場合があります。 ただし、将来的には「マルチシグ(multi-signature)」や「共同承認(consensus-based approval)」を導入することで拡張可能です。 ERC7743では、あくまでコア標準をシンプルに保ち、混乱を避けることを重視しています。

安全な送付良い良い機能

ERC721では、safeTransferFrom関数が「NFTを誤って非対応のコントラクトに送ってしまう」ことを防ぐために存在します。 この関数は、送信先がERC721トークンを正しく受け取れるかどうかをonERC721Receivedで確認する仕組みを持っています。

MO-NFTでも、複数所有モデルがこの仕組みを壊すことはないため、同様の安全性チェックを維持できます。 ただし、MO-NFTの送付は「追加的所有」を前提としているため、従来のように「所有権の完全移転」ではなく、「所有者リストへの追加後に安全確認」を行うように動作します。

つまり、safeTransferFromの内部処理は以下のように考えられます。

javascript
function safeTransferFrom(address from, address to, uint256 tokenId) public {
    // 新しい所有者を追加
    _addOwner(tokenId, to);

    // ERC721受取対応のチェック
    if (to.isContract()) {
        require(
            IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, "") ==
            IERC721Receiver.onERC721Received.selector,
            "Recipient contract cannot handle ERC721 tokens"
        );
    }
}

このように、「安全な送付」という性質はそのまま維持しつつ、加算的所有に適した形で再設計されています。 つまり、MO-NFTは安全性と柔軟性の両立を実現しているといえます。

ownerOf関数の取り扱い

ERC721では、ownerOf(tokenId)が「トークンの唯一の所有者」を返すことが仕様として定められています。 しかし、MO-NFTではトークンに複数の所有者が存在するため、この仕様をそのまま適用することはできません。

MO-NFTでは、最初の所有者(リストの先頭に登録されたアドレス)を返すように設計されています。 これは、ERC721との互換性を維持するための設計上の妥協です。

javascript
function ownerOf(uint256 tokenId) external view returns (address owner) {
    return _owners[tokenId].at(0); // 最初の所有者を返す
}

この仕様により、既存のERC721互換ツール(ウォレットやマーケットプレイスなど)はMO-NFTを認識できます。 ただし、開発者は以下の点に注意する必要があります。

注意点 説明
単一所有を前提にした処理は誤動作の可能性 例えば、「ownerOfで返されたアドレス=唯一の所有者」と誤認する設計では問題が起きる。
所有者リストの参照を推奨 実際の所有者情報を取得するには、getOwnersCountやisOwnerを使う必要がある。
コード互換性は維持 最初の所有者を返すことで、既存のERC721ライブラリやUIとの接続が可能。

開発者への注意点

MO-NFTを既存のERC721対応システムに統合する時には、以下の違いを理解しておくことが重要です。

項目 MO-NFTでの挙動 ERC721との差異
承認機能(approve系) 無効化されている 単一所有モデルを前提にしているため使用不可。
安全転送(safeTransferFrom) 追加的所有の概念に合わせて再実装 ロジックは同様だが、転送結果が「追加所有」となる。
所有者取得(ownerOf) 最初の所有者を返す 本来の意味での「唯一の所有者」ではない。

これらの仕様変更は、MO-NFTが複数所有という新しい所有モデルを導入するために必要な変更です。 同時に、可能な限りERC721との互換性を維持する工夫もなされています。

参考実装

コントラクト構成

ファイル名 内容
MONFT.sol MO-NFT標準の基礎となる実装。複数所有モデルやイベントなど、コアロジックを定義。
DigitalAsset.sol デジタルアセット向けの拡張実装。プロバイダー手数料など、商用利用を想定した機能を追加。

この構成により、開発者はまずMONFT.solを利用して基本的なNFTの複数所有機能を利用し、 さらにDigitalAsset.solを組み合わせることで、提供者(Provider)への報酬設計を行うことができます。

インターフェース構成

ファイル名 内容
IDigitalAsset.sol デジタルアセット管理のための関数を定義したインターフェース。資産提供者、ファイルハッシュ、手数料設定などの管理を標準化。

このインターフェースにより、異なるデジタルアセットコントラクト間での共通的な操作が可能になります。

参考実装内の主要関数

mintToken
javascript
function mintToken() public onlyOwner returns (uint256) {
    _nextTokenId++;

    // Add the sender to the set of owners for the new token
    _owners[_nextTokenId].add(msg.sender);

    // Increment the balance of the owner
    _balances[msg.sender] += 1;

    // Set the provider to the caller
    _providers[_nextTokenId] = msg.sender;

    emit TokenMinted(_nextTokenId, msg.sender);
    return _nextTokenId;
}

新しいMO-NFTトークンを発行する関数。 呼び出したアドレスを最初の所有者(Owner)および提供者(Provider)として登録します。

この関数を呼び出すと、新しいトークンIDが生成されます。 呼び出し元(msg.sender)は、そのトークンの所有者集合に追加されると同時に、発行者(Provider)として登録されます。 また、所有者の保有数(balance)が1増加し、イベントTokenMintedが発行されます。

この処理により、MO-NFTの「最初の所有者=提供者」という基本構造が確立されます。

戻り値

  • uint256 新しく発行されたトークンの識別子(tokenId)。
transferFrom
javascript
function transferFrom(address from, address to, uint256 tokenId) public override {
    require(isOwner(tokenId, msg.sender), "MO-NFT: Caller is not an owner");
    require(to != address(0), "MO-NFT: Transfer to zero address");
    require(!isOwner(tokenId, to), "MO-NFT: Recipient is already an owner");

    // Add the new owner to the set
    _owners[tokenId].add(to);
    _balances[to] += 1;

    // Transfer the transferValue to the provider
    uint256 transferValue = _transferValues[tokenId];
    address provider = _providers[tokenId];
    require(address(this).balance >= transferValue, "Insufficient contract balance");

    (bool success, ) = provider.call{value: transferValue}("");
    require(success, "Transfer to provider failed");

    emit TokenTransferred(tokenId, from, to);
}

MO-NFTを他のアドレスに送付する関数。既存の所有者を削除せずに新しい所有者を追加する形式で行われます。

この関数では、まず呼び出し元が該当トークンの所有者であるかを確認します。 次に、送付先が無効アドレス(0アドレス)でないこと、すでに所有者でないことをチェックします。 すべての条件を満たした場合、新しい所有者が所有リストに追加され、所有数(balance)が更新されます。

さらに、トークンごとに設定されているtransferValueがプロバイダーに支払われます。 この支払いにより、トークンの二次流通が発生するたびに提供者に報酬が還元されます。

引数

  • address from 現在の所有者アドレス。
  • address to 新しい所有者アドレス。
  • uint256 tokenId 送付対象トークンの識別子。
archive
javascript
function archive(uint256 tokenId) external {
        require(
            isOwner(tokenId, msg.sender),
            "MO-NFT: Caller is not the owner of this token"
        );
        // Once archived, the status cannot be reversed
        require(
            _archivedStatus[tokenId][msg.sender] == false,
            "MO-NFT: Token can only be archived once for an owner"
        );
        _archivedStatus[tokenId][msg.sender] = true;
        emit ArchivedStatusUpdated(tokenId, msg.sender, archived);
    }

所有者が自分の所有状態を「アーカイブ」に設定する関数。 一度アーカイブ化された状態は元に戻すことができません。

この関数は、MO-NFTの「所有権アーカイブ」機能を実装しています。 呼び出し元がそのトークンの所有者であることを確認したうえで、_archivedStatusマッピングによりアーカイブ状態をtrueに変更します。 この状態では、所有者はそのトークンを転送できなくなりますが、デジタルアセットプラットフォーム上では、アーカイブ状態の所有者が実データ(画像・音源など)を直接ダウンロードできるようになります。

引数

  • uint256 tokenId アーカイブ対象となるトークンの識別子。

実装の特徴

項目 説明
所有管理 各トークンに対して複数の所有者をEnumerableSetとして管理。重複登録を防ぎ、高速検索を実現。
提供者報酬 _providersマッピングで各トークンの提供者を追跡し、transferValueで報酬を支払う。
アーカイブ制御 _archivedStatusを用いて、所有者単位でアーカイブ状態を記録。永続的に変更不可。
イベント通知 各主要操作(発行・転送・アーカイブ)ごとにイベントを発行し、トークンの履歴をオンチェーンで追跡可能。

セキュリティ

再入可能性攻撃(Reentrancy Attacks)

再入可能性は、外部コール(callなど)を行った直後に、相手先から自コントラクトの関数が再度呼ばれて状態が不正に書き換えられる問題です。 MO-NFTではプロバイダーへの手数料送金が発生するため、外部コールを安全に扱う必要があります。

観点 説明
何がリスクか transferFromなどで手数料を送金(外部コール)した後に、再入可能性を悪用して状態を改ざんされる可能性がある。
推奨パターン Checks→Effects→Interactions(CEI)パターンで記述し、状態更新を先に完了してから外部コールを行う。
追加対策 OpenZeppelinのReentrancyGuardを導入し、nonReentrant修飾子で再入をブロックする。
javascript
// ReentrancyGuard の導入例
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract MONFT is ReentrancyGuard {
    // ...
    function transferFrom(address from, address to, uint256 tokenId) public nonReentrant {
        // Checks
        require(isOwner(tokenId, msg.sender), "MO-NFT: not an owner");
        require(to != address(0), "MO-NFT: zero address");
        require(!isOwner(tokenId, to), "MO-NFT: already an owner");

        // Effects(先に状態を確定)
        _owners[tokenId].add(to);
        _balances[to] += 1;

        // Interactions(最後に外部コール)
        uint256 fee = _transferValues[tokenId];
        address provider = _providers[tokenId];
        if (fee > 0) {
            (bool ok, ) = provider.call{value: fee}("");
            require(ok, "MO-NFT: provider payment failed");
        }

        emit TokenTransferred(tokenId, from, to);
    }
}

整数オーバーフロー/アンダーフロー

Solidity 0.8.xは算術演算に自動チェックが入り、オーバーフローやアンダーフローが発生すると例外を投げます。 追加のSafeMathを使わなくても標準で保護されます。

観点 説明
リスク 旧バージョンのSolidityではuint256等の加減算で境界を超えるとラップアラウンドが起きた。
緩和 Solidity 0.8.x以降を使用することで、演算時に自動的に例外が発生して不正状態を防ぐ。

アクセス制御

MO-NFTは「複数所有」を前提とするため、誰が何を呼べるかを明確に制御する必要があります。 典型的には「所有者のみが送付できる」、「プロバイダーのみが手数料を設定できる」といった制約です。

観点 説明
保護対象 transferFromは所有者のみ。setTransferValueはプロバイダーのみ。
手法 requireで前提条件を厳格化、専用の修飾子(modifier)で再利用性を高める。
javascript
modifier onlyOwnerOf(uint256 tokenId) {
    require(isOwner(tokenId, msg.sender), "MO-NFT: caller not owner");
    _;
}

modifier onlyProvider(uint256 tokenId) {
    require(_providers[tokenId] == msg.sender, "MO-NFT: caller not provider");
    _;
}

function setTransferValue(uint256 tokenId, uint256 newValue) external onlyProvider(tokenId) {
    uint256 old = _transferValues[tokenId];
    _transferValues[tokenId] = newValue;
    emit TransferValueUpdated(tokenId, old, newValue);
}

function transferFrom(address from, address to, uint256 tokenId) public onlyOwnerOf(tokenId) {
    // 送付ロジック…
}

DoS(サービス妨害)の可能性(Denial of Service)

所有者が増えるほど、所有者一覧に対する反復処理(全件ループなど)はガスコストが膨らみ、トランザクション失敗を誘発しやすくなります。

観点 説明
リスク 大人数オーナーのトークンで全件列挙や一括処理を行う関数は高ガスでDoSを誘発しうる。
緩和 ループを避ける・ページングを導入する・ビュー関数でcountとownerAt(index)のように分ける・所有者数の設計上限を設ける。
javascript
// O(1) 取得でループ回避
function getOwnersCount(uint256 tokenId) external view returns (uint256) {
    return _owners[tokenId].length();
}

function ownerAt(uint256 tokenId, uint256 index) external view returns (address) {
    return _owners[tokenId].at(index);
}

データ完全性

正しいデータ型と構造(EnumerableSetなど)を用い、状態変更ごとにイベントを発行しておくことで、オフチェーンから履歴を検証できます。

観点 説明
型・構造 所有者集合にEnumerableSetを使い重複を防止、検索を高速化。
監査可能性 ミント・送付・手数料変更・アーカイブなど、あらゆる状態変更でイベントを発行し、オフチェーン(インデクサ等)で追跡可能にする。
javascript
event TokenMinted(uint256 indexed tokenId, address indexed owner);
event TokenTransferred(uint256 indexed tokenId, address indexed from, address indexed to);
event TransferValueUpdated(uint256 indexed tokenId, uint256 oldTransferValue, uint256 newTransferValue);
event ArchivedStatusUpdated(uint256 indexed tokenId, address indexed owner, bool archived);

Etherの取り扱い(Ether Handling)

プロバイダー手数料を支払うため、コントラクトがEtherを受け取れることが必要です。また、受領・送金ロジックは堅牢に保つべきです。

観点 説明
受金 コントラクトが手数料プールを保持できるようにreceive()を実装して入金を受け付ける。
送金 CEIとnonReentrantで保護した上で外部送金。失敗時の扱い(revert)を明確化。必要に応じてPull-Payment(引き出し)方式も検討。
javascript
// Ether を受け取るためのエントリポイント
receive() external payable {}

// 明示的な入金口(任意)
function deposit() external payable {}

// 送金はCEI+nonReentrantで
function _payProvider(uint256 tokenId) internal {
    uint256 fee = _transferValues[tokenId];
    if (fee == 0) return;

    address provider = _providers[tokenId];
    require(address(this).balance >= fee, "MO-NFT: insufficient balance");

    (bool ok, ) = provider.call{value: fee}("");
    require(ok, "MO-NFT: provider payment failed");
}

引用

Cheng Qian (@jamesavechives) james.walstonn@gmail.com, "ERC-7743: Multi-Owner Non-Fungible Tokens (MO-NFT)," Ethereum Improvement Proposals, no. 7743, July 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7743.

最後に

今回は「1つのNFTを複数のアドレスで共同所有する仕組みであるMO-NFを提案しているERC7743」についてまとめてきました! いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!