[ERC6147] NFTに『guard』ロールを付与して機能拡張しよう！

# はじめに

初めまして。
**CryptoGames**というブロックチェーンゲーム企業でエンジニアをしている **cardene（かるでね）** です！
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

https://cryptogames.co.jp/

代表的なゲームは**クリプトスペルズ**というブロックチェーンゲームです。

https://cryptospells.jp/

今回は、NFTやSBTの保有権と譲渡権を分離し、有効期限を持たせる新しいロールである「**guard**」を付与する**ERC6147**について解説していきます。

::: note info
主に以下の記事をもとに、ChatGPTも使用しながら解説していきます。
:::

https://eips.ethereum.org/EIPS/eip-6147

# 要約
ERC721の拡張であり、NFTやSBTの保有権と譲渡権を分離し、有効期限（**expires**）を持たせる新しいロールである「**guard**」を付与するします。
この新たなロールにより、NFTの盗難防止、NFTの貸し出し、NFTのリース、SBT化などの機能を実現できます。

![盗難防止.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570269/3e7a0902-a542-a0d9-2126-dd856b80a423.png)

:::note info
SBTとは「**Soulbound Token**」の略で譲渡不可能なNFTのことを指します。
まさに、文字通り魂に紐づいたトークンですね。
:::

:::note info
NFTのリースとは、NFTを一定期間他の個人や組織に貸し出す取引のことです。

NFTの貸し出しとの違いとしては以下が挙げられます。
- 条件が細かく、比較的長期的な貸し出しになる。
- 貸し手はリース期間中に収益を得ることができ、借り手は料金を支払う。

貸出の条件としては、以下が挙げられます。
- 貸し出し期間
- 使用料金
- 使用条件
- 更新オプション

NFTの貸し出しは、一定期間借りてが対象のNFTを使用でき、期間が過ぎると所有者に戻るものになります。
そのため、料金発生などは前提にされておらず、短期的な貸し出しになることが多いです。
:::

![盗難防止 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570269/12e34c72-6e8d-05bb-271f-dd8100332cf2.png)

# 動機
NFTは使用価値と金融価値を兼ね備えています。
現在、多くのNFTで盗難の事例が存在します。
NFTの盗難防止策としては、NFTをコールドウォレットで管理することが挙げられるが、これはNFTの使用を不便にします。

:::note info
例えば、自分の家に抵当をつけても、家の使用権は所有者にあります。
抵当とは、不動産などの財産を借り入れたお金の担保として設定する担保権のことです。
住宅ローンを組む場合、金融機関は借り手に対して融資を際に不動産を担保として抵当権を設定します。
借りては住宅ローンを返済しない場合、金融機関は抵当権を行使して不動産を差し押さえて売却し、融資額を回収することができます。
:::

譲渡不可能なNFTであるSBTについては、アドレスの秘密鍵が漏洩したり紛失した場合、SBTを取り戻すことが困難になります。
SBTはNFTの保有権と譲渡権の分離して、ウォレットが盗まれたり使用できなくなっても回収可能であるべきです。
さらに、例えば大学が卒業生に対して卒業証書をSBTとして発行し、後に卒業生が大学の評判を損なったりした場合に大学側はSBT回収する権限を持つべきです。

![盗難防止 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570269/25434751-dac6-dc6c-ecd9-302b755539a0.png)

この規格は、これらの問題に対する解決策として提案されており、NFTおよびSBTの保有権と譲渡権を分離することで、より柔軟な管理が可能となります。
「**guard**」の有効期限（**expires**）を設定することにより、さまざまなアプリケーションシナリオに合わせた管理が可能となります。
例えば、NFTの発行時に一定期間販売や送付ができなくなるロックアップ期間を設けることで、購入者に対して割引を提供することができます。
また、NFTを借りているアドレス自体を使用できなくなった場合でも、有効期限が切れれば所有者はNFTを回収できるようになります。

この規格は、NFTおよびSBTの保有権と譲渡権を分離することで、さまざまなアプリケーションシナリオに対応する柔軟な管理手段を提供し、セキュリティと利便性の向上を目指しています。

# 仕様

この章では「**guard**」を実装する際の仕様についてまとめられています。

## 「**guard**」が有効
- 「**guard**」が有効の時、トークンの所有者や譲渡権限を持つアドレス、承認されたアドレスは、「**guard**」と有効期限（**expires**）を変更したりトークンを送付できない。
- 「**guard**」が有効の時、`guardInfo`は「**guard**」ロールが付与されたアドレスと有効期限（**expires**）を返す。
- 「**guard**」が有効の時、「**guard**」ロールが付与されたアドレスは有効期限（**expires**）の削除・変更・トークンの送付ができる。
- 「**guard**」が有効の時、NFT`burn`（焼却）されたら「**guard**」は削除される。

## 「**guard**」がない or 有効期限切れの場合
- 「**guard**」の有効期限（**expires**）が切れた場合、「**guard**」は無効になる。
- トークンに「**guard**」がない場合や有効期限（**expires**）が切れた場合は、`guardInfoは(address(0), 0)`を返す。
- トークンに「**guard**」がない場合や有効期限（**expires**）が切れた場合は、トークンの所有者や譲渡権限を持つアドレス、承認されたアドレスは「**guard**」と有効期限（**expires**）を設定する権限を持つ。

## その他
- SBTを発行またはMintする場合、管理を容易にするために「**guard**」を指定したアドレスに統一できる。

# コントラクトのInterface

`Interface`については以下の記事を参考にしてください。

https://chaldene.net/erc721#st-toc-h-5

```javascript
 interface IERC6147 {
    event UpdateGuardLog(uint256 indexed tokenId, address indexed newGuard, address oldGuard, uint64 expires);

    function changeGuard(uint256 tokenId, address newGuard, uint64 expires) external;

    function removeGuard(uint256 tokenId) external;
    
    function transferAndRemove(address from, address to, uint256 tokenId) external;

    function guardInfo(uint256 tokenId) external view returns (address, uint64);   
}
```

## UpdateGuardLog

NFTの「**guard**」が変更されるか、有効期限（**expires**）が変更されたときにログを残す。
`newGuard`が0アドレスの場合、現在「**guard**」アドレスがないことを表す。

## changeGuard
NFTの「**guard**」と有効期限（**expires**）を設定できる関数。
NFTの所有者や譲渡権限を持つアドレス、承認されたアドレスはNFTの「**guard**」と有効期限（**expires**）を設定できます。
ただ、NFTに有効な「**guard**」がある場合、NFTの所有者や譲渡権限を持つアドレス、承認されたアドレスは、「**guard**」と有効期限（**expires**）を変更できません。
`public`または`external`として実装できる。

#### 注意点

- `newGuard`に0アドレスを指定できない。
- `expires`は有効である必要がある。
- `tokenId`が有効なNFTでない場合はエラーになる。

#### 引数

- `tokenId`
    - 「**guard**」アドレスを取得するNFTのトークンID。
- `newGuard`
    - NFTの新しい「**guard**」アドレス。
- `expires`
    - UNIXタイムスタンプ。
    - 「**guard**」は有効期限（**expires**）までトークンを管理できる。

## removeGuard

NFTの「**guard**」と有効期限（**expires**）を削除する関数。
「**guard**」が自身の「**guard**」と有効期限（**expires**）を削除できます。
`public`または`external`として実装できる。

#### 注意点
- 「**guard**」アドレスは0アドレスに設定される。
- 有効期限（**expires**）は0に設定される。
- `tokenId`が有効なNFTでない場合はエラーになる。

#### 引数
ー `tokenId` 
    - 「**guard**」と有効期限（**expires**）を削除するNFTのトークンID。

## transferAndRemove

NFTを送付し、その「**guard**」と有効期限（**expires**）を削除する関数。
`public`または`external`として実装できる。

#### 注意点
- NFTは`to`に送付され、「**guard**」アドレスは0アドレスに設定される。
- `tokenId`が有効なNFTでない場合はエラーになる。

#### 引数
- `from`
    -  NFTの送付元のアドレス。
- `to`
    - NFTの送付先のアドレス。
- `tokenId`
    - 送付されるNFTのトークンID。

## guardInfo

NFTの「**guard**」アドレスと有効期限（**expires**）を取得する関数。
`pure`または`view`として実装できる。

#### 注意点
- 0アドレスは「**guard**」がないことを示す。

#### 引数
- `tokenId`
    - 「**guard**」アドレスと有効期限（**expires**）を取得するNFTのトークンID。

#### 戻り値
- NFTの「**guard**」アドレスと有効期限（**expires**）。

## その他

`supportsInterface`関数は、`0xb61d1057`を呼び出したときにtrueを返す。

# 補足

## 汎用性
**ERC6147**は、NFTとSBTに関連する権利を「**所有権**」と「**譲渡権**」に分け、それらをより普遍的な基準に基づいて定義しています。
これによりNFT/SBTは様々なシナリオに対応することができ、それぞれに専用のEIPを提案すると開発者の負担を軽減することができます。

例として、以下のようなユースケースが規格に含まれる。

### SBT

SBTの発行者は、`Mint`（発行）前にSBTに「**guard**」ロールを割り当て、NFTの送付を管理できます。

### NFT盗難防止

NFT保有者が、自身のコールドウォレットアドレスをNFTの「**guard**」アドレスに設定することで、盗難のリスクは大幅に減少します。

### NFTの貸出

借り手が自身のNFTの「**guard**」を貸し手のアドレスに設定すれば、借り手はNFTを使用する権利を維持でき、NFTの送付や売却はできなくなります。
また、借り手がNFTを返さなければ、貸し手はNFTを送付したり売却できる。

## 有効期限の設定
さらに、「**guard**」の有効期限（**expires**）を設定すると以下のようなメリットがある。

#### より柔軟なNFT発行

NFTの`Mint`（発行）時に、NFTを一定期間ロックすることで割引した価格で販売することができます。

#### より安全なNFT管理

プライベートキーの紛失により「**guard**」アドレスが利用不能になっても、「**guard**」の有効期限（**expires**）が切れれば、所有者はNFTを取り戻せる。

#### 有効なSBT

SBTには有効期限を設けてより効果的な管理が可能になる。

## 拡張性
この規格では「**guard**」とその有効期限（**expires**）のみを定義している
NFTやSBTに必要な複雑な機能（ソーシャルリカバリーやマルチシグネチャなど）については、「**guard**」をサードパーティのプロトコルアドレスとして設定でき、より柔軟で多様な機能を実現できる。

:::note info
ソーシャルリカバリー（**Social Recovery**）とは、ウォレットやアカウントのアクセスを失った場合に、友人や家族などの「**信頼できる人**」によってアカウントのアクセスを回復する仕組みです。
具体的な手順としては以下になります。
1. ウォレットの所有者がアカウントにアクセスできなくなり、信頼された人々に対してリカバリーのリクエストを送信する。
2. 信頼された人々はそれぞれが一定の情報や秘密のフレーズを提供し、ウォレットの所有者のアクセスを回復するために必要なマルチシグネチャトランザクションに署名。
3. 署名の検証を行い、正しければアカウントを回復させる。
:::

:::note info
マルチシグネチャ（**Multisig**）は、複数の署名が必要なトランザクションの仕組みです。
企業やプロジェクトの資金管理などにおいて特定のアクションを実行するために、複数のアドレスによる署名が必要とされる場合があります。

マルチシグネチャウォレットでは、事前に指定された複数の公開鍵による署名がトランザクションに必要となります。
例えば、`2-of-3`のマルチシグネチャでは、3つの公開鍵がありますが、そのうち2つの署名が必要となります。
これにより、資金の移動や重要なトランザクションが行われる際に、複数の関係者の同意が必要となり、セキュリティを向上させることができます。

マルチシグネチャは特に、取引所や企業のウォレットなど、大きな金額や責任がかかるアカウントのセキュリティを強化するために利用されます。
複数の関係者の協力によって、不正な取引やハッキングからの防御を強化することができます。
:::

## 名前
`guardian`よりも「**guard**」の方がより文字数が少なく、より簡潔なため「**guard**」にしました。

# 後方互換性
この規格は、**ERC721**と互換性がある。
NFTが「**guard**」を設定していない場合、ERC721で発行されたNFTと同じです。

# 実装

以下にコントラクトの実装コードを格納しています。

https://github.com/cardene777/base-code/tree/develop/contract/contracts/ERC6147

```javascript
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.8;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "./IERC6147.sol";

abstract contract ERC6147 is ERC721, IERC6147 {

    struct GuardInfo{
        address guard;
        uint64 expires;
    }
    
    mapping(uint256 => GuardInfo) internal _guardInfo;

    function changeGuard(uint256 tokenId, address newGuard, uint64 expires) public virtual{
        require(expires > block.timestamp, "ERC6147: invalid expires");
        _updateGuard(tokenId, newGuard, expires, false);
    }

    function removeGuard(uint256 tokenId) public virtual  {
        _updateGuard(tokenId, address(0), 0, true);
    }
    
    function transferAndRemove(address from, address to, uint256 tokenId) public virtual {
        safeTransferFrom(from, to, tokenId);
        removeGuard(tokenId);
    }
    
    function guardInfo(uint256 tokenId) public view virtual returns (address, uint64) {
        if(_guardInfo[tokenId].expires >= block.timestamp){
            return (_guardInfo[tokenId].guard, _guardInfo[tokenId].expires);
        }
        else{
            return (address(0), 0);
        }
    }

    function _updateGuard(uint256 tokenId, address newGuard, uint64 expires, bool allowNull) internal {
        (address guard,) = guardInfo(tokenId);
        if (!allowNull) {
            require(newGuard != address(0), "ERC6147: new guard can not be null");
        }
        if (guard != address(0)) { 
            require(guard == _msgSender(), "ERC6147: only guard can change it self"); 
        } else { 
            require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC6147: caller is not owner nor approved");
        } 

        if (guard != address(0) || newGuard != address(0)) {
            _guardInfo[tokenId] = GuardInfo(newGuard,expires);
            emit UpdateGuardLog(tokenId, newGuard, guard, expires);
        }
    }
    
    function _checkGuard(uint256 tokenId) internal view returns (address) {
        (address guard, ) = guardInfo(tokenId);
        address sender = _msgSender();
        if (guard != address(0)) {
            require(guard == sender, "ERC6147: sender is not guard of the token");
            return guard;
        }else{
            return address(0);
        }
    }
 
    function transferFrom(address from, address to, uint256 tokenId) public virtual override {
        address guard;
        address new_from = from;
        if (from != address(0)) {
            guard = _checkGuard(tokenId);
            new_from = ownerOf(tokenId);
        }
        if (guard == address(0)) {
            require(
                _isApprovedOrOwner(_msgSender(), tokenId),
                "ERC721: transfer caller is not owner nor approved"
            );
        }
        _transfer(new_from, to, tokenId);
    }

    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public virtual override {
        address guard;
        address new_from = from;
        if (from != address(0)) {
            guard = _checkGuard(tokenId);
            new_from = ownerOf(tokenId);
        }
        if (guard == address(0)) {
            require(
                _isApprovedOrOwner(_msgSender(), tokenId),
                "ERC721: transfer caller is not owner nor approved"
            );
        }
        _safeTransfer(from, to, tokenId, _data);
    }

    function _burn(uint256 tokenId) internal virtual override {
        (address guard, )=guardInfo(tokenId);
        super._burn(tokenId);
        delete _guardInfo[tokenId];
        emit UpdateGuardLog(tokenId, address(0), guard, 0);
    }

    /// @dev See {IERC165-supportsInterface}.
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return interfaceId == type(IERC6147).interfaceId || super.supportsInterface(interfaceId);
    }
}
```

#### GuardInfo

「**guard**」アドレスと有効期限の構造体。

#### _guardInfo

NFTのトークンIDと`GuardInfo`構造体の配列。

#### changeGuard

NFTの「**guard**」アドレスと有効期限を変更する関数。

#### removeGuard

NFTの「**guard**」アドレスと有効期限を削除する関数。

#### transferAndRemove

NFTを送付し「**guard**」アドレスと有効期限を削除する関数。

#### guardInfo

NFTの「**guard**」アドレスと有効期限を取得する関数。

#### _updateGuard

NFTの「**guard**」を更新する関数。

**削除する時**
- 「**guard**」アドレスを０アドレスに設定し、有効期限を0にする。

**更新する時**
- 「**guard**」を新しいアドレスに設定し、有効期限を設定する。

#### _checkGuard

「**guard**」アドレスを調べる関数。

#### その他

これ以外の関数はERC721に実装されているものです。

`ERC721`については以下の記事を参考にしてください。

https://chaldene.net/erc721

# セキュリティ上の考慮事項

アプリケーションに応じて、「**guard**」の有効期限を適切に設定する必要がある。

NFTが有効な「**guard**」を持つ場合、アドレスが承認や`setApprovalForAll`を通じて送付できる状態でも、NFTを転送する権利を持っていない。

NFTが有効な「**guard**」を持つ場合、所有者はNFTを売却できない。
一部の取引プラットフォームでは、`setApprovalForAll`と所有者の署名を通じてNFTを販売できてしまうため、「**guard**」情報をチェックすることが推奨される。

# テストケース

以下にテストコードを格納しています。

https://github.com/cardene777/base-code/blob/develop/contract/test/ERC6147/ERC6147.test.ts

```sh
$ cd contract
$ npm install
$ npx hardhat test test/ERC6147/ERC6147.test.ts
```
上記コマンドでテストの実行ができます。

# 引用

5660-eth (@5660-eth), Wizard Wang, "ERC-6147: Guard of NFT/SBT, an Extension of ERC-721," Ethereum Improvement Proposals, no. 6147, December 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6147.

# 考察

今回解説してきた**ERC6147**は活用できる場面が多そうな印象を受けました。
**ERC6147**を使用した実装次第では、NFTの貸し出し料金を発生させたり、特定の条件においては貸し出し期間を延長させたりすることができます。
また、使用期限が来たら自動で所有者の元に戻ってくるため、その点も便利です。
ただし、実装する上では以下のような部分に注意する必要があります。

- 貸し出し期限は期限が切れていない間、借り手が変更できてしまうため実行できる条件を設ける。
- 貸出の期限の変更などは`Approve`されているアドレスか、NFTの所有者しか実行できないため、有効期限が切れていない状態の時は「**guard**」アドレスも実行できるようにする。

あくまで標準実装を提案しているため、この**ERC6147**を使用したコントラクトでどのような実装にするのかによって予想外の挙動をする恐れがあります。
ただ、その分できることの幅が広がるのため、そのメリットを享受しつつ**ERC6147**の仕様をしっかり理解した上で使用する必要があります。

# 最後に

今回の記事では、NFTやSBTの保有権と譲渡権を分離し、有効期限を持たせる新しいロールである「**guard**」を付与する**ERC6147**について解説してきました。
いかがだったでしょうか？

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

[Twitter @cardene777](https://twitter.com/cardene777)

# 採用強化中！

CryptoGamesでは一緒に働く仲間を大募集中です。

https://www.wantedly.com/companies/cryptogames

この記事で書いた自分の経験からもわかるように、裁量権を持って働くことができて一気に成長できる環境です。
「ブロックチェーンやWeb3、NFTに興味がある」、「スマートコントラクトの開発に携わりたい」など、少しでも興味を持っている方はまずはお話ししましょう！