Symbolブロックチェーンでオンチェーンロイヤリティを実現する

NFTマーケットプレイス等で実装されているロイヤリティ（転売されるたびにクリエイターに一定の金額が支払われる仕組み）ですが、その仕組みをスキップするようなサービスも増え始めていると聞きます。

「そもそもクリエイターに払い続ける仕組みはそれほど重要か？」
そんな声も聞かれるようになりました。

https://www.coindeskjapan.com/164942/


しかしながら「出来るけどしない」のと「できないから必要ない」のは大きく違います。

将来、NFTはDIDなどと組み合わせて土地の権利など様々な裏付けを持った証書として活用される可能性があります。そういった用途での活用を検討する場合、「場合によってはロイヤリティの支払いを回避することも可能」などと言ってられません。

そこで今日はSymbolブロックチェーンでオンチェーン上のロイヤリティを実現してみます。

### 他チェーンでのオンチェーンロイヤリティについて

ERC2981がオンチェーンロイヤリティについて最も進んだ議論でしょうか。このあたり、詳しい方がいらっしゃいましたらコメントお願いします。

https://blockchain-insight.ch/jp/2021/09/18/2262

> NFTは通常ERC721またはERC1155に準拠して設計されているが、この規格にロイヤリティを回収するための仕組みはない。これにロイヤリティ回収のための独自の実装をいれるか、普及の可能性を持ったERC2981 に準拠したNFTだけが売買に際してロイヤリティの回収ができる可能性がある。
可能性があると言ったのは、たとえ対応しているNFTであっても、Metamaskなどのウォレットを使って個人間で直接NFTのやりとりをした場合には、NFT自身が売買金額を知る方法はなく、ロイヤリティの回収はできない。

https://metaipandlaw.com/nft-royalty-on-chain-difficult-why/

> 誰かがERC-2981トークンをNFTの通常規格であるERC-721ボックスで包み、ERC-721スマートコントラクトを通して取引することができます。そうすることによってERC-721スマートコントラクトはその取引を肯定し、ロイヤリティなしで成立させることができるのです。 

要するに、個人間のやり取りにERC2981の出て来る幕は無く、マーケットプレイスが真摯に準拠する姿勢を見せなければ解決できないようです。



# 前提条件
さて、前書きで大きく広げた風呂敷ですが、早速縮小します(汗
現在マーケットプレイス上で実現しているロイヤリティをそのまま再現することはできません。

実現するのは、**クリエイターの署名無しでは転送できないNFT**の作成です。
つまり、マーケットプレイスはクリエイターと事前に取り決めた内容で買い手を探し出す必要があり、クリエイターは満足するロイヤリティ支払いの提示がなければトランザクションの実行を拒否することができます。

NFT転送のたびにクリエイターによる承認が必要にはなりますが、一方で都度トレンドに応じた柔軟なロイヤリティの設定が可能になり、「契約は固定すべきでない」という発想から見れば柔軟さ(オフチェーン)と厳密さ(オンチェーン)を兼ね備えたフレキシブルコントラクトの実現が可能になる、と考えることもできます。保有しているNFTを他人ではなく、自分のサブアカウントに転送したいだけの場合などもクリエイターを説得させることができればロイヤリティを払うことなく実現可能になります。

なお、Symbolではトークンをアカウント間で移転させず、マルチシグの組み換えによってアカウントの操作権を移転させる方法があります。今回はアカウント操作権の移転についてのロイヤリティには言及しません。

# 活用する機能

グローバルモザイク制限を使用します。内容をご存じない方は速習Symbolで予習お願いします。


https://github.com/xembook/quick_learning_symbol/blob/main/11_restriction.md#112-%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%90%E3%83%AB%E3%83%A2%E3%82%B6%E3%82%A4%E3%82%AF%E5%88%B6%E9%99%90

本来グローバルモザイク制限は、KYCの済んだアカウントのみが所属する経済圏内で流通が可能なトークンを定義することができるため、CBDCやSTOなどへの活用が期待される機能です。


今回はこのグローバルモザイク制限で**購入者だけが所属する経済圏**を作成します。この経済圏を解除しない限り購入者は転売をすることができません。
購入者が転売したい場合は、いったん経済圏を解除し、転送した後即座（同じブロック内）に**受領者のみが所属する経済圏を再構築**させます。この経済圏の解除・再構築する権限をクリエイターに**委任**することで「クリエイターが承諾したロイヤリティが支払われる場合のみ転売を成立させる」ことが可能になります。（速習Symbolは初学者向けドキュメントのため、委任については言及しておりません）

このアイデアはちくぽかさんのツイートからひらめきました。ありがとうございます。

https://twitter.com/7338tkpk/status/1576736124348686339


# アカウント定義

今回の記事で使用するアカウントについて説明します。

## Carol(Creater)

クリエイター：NFTに紐づけるデータの著作者です。
グローバルモザイク制限の権限の委任を受け、ロイヤリティを受け取るアカウントです。

## Alice（NFT作成アカウント）

NFTを生成するアカウントです。
Symbolでは一般的にNFT作成アカウントとクリエイターアカウントは分けておきます。
（NFT生成アカウントはデータ追加や設定変更などの権限が大きいため）
グローバルモザイク制限を定義するのはAliceですが、その権限をCarolに委任します。
Aliceアカウントはデータの追記や数量やメタデータの変更が簡単にできないようにマルチシグ化あるいは封滅・破棄しておくのが理想です。

## Bob(Holder）
NFT購入者です。今回の検証ではDaveに転売を試みます。


# 処理概要

事前にサンプルプログラムを動かすために以下のページで検証環境を整えておいてください。

https://qiita.com/nem_takanobu/items/e88d5ea76cd609e76bbf

## シーケンス図

```mermaid
sequenceDiagram
    participant Carol
    participant Alice
    participant Bob
    participant Dave


    Carol->>Carol: 制御用モザイク定義
    Carol->>Carol: モザイク制限定義
    Alice->>Alice: NFTモザイク定義
    Alice->>Alice: モザイク数量変更(NFT生成)
    Alice->>Bob: NFT転送
    Alice->>Carol: モザイク制限定義（Carolに委任）
    opt ロイヤリティが正しく設定されている場合のみバッチ処理
        Carol->>Bob: モザイク制限解除
        Carol->>Dave: モザイク制限解除
        Bob->>Dave: NFT転送
        Dave->>Carol: ロイヤリティ
        Carol->>Bob: モザイク制限設定
        Carol->>Dave: モザイク制限設定
    end
```
- Carolに制御用モザイクを定義します
- AliceがNFTモザイクを定義します
- AliceからBobにモザイクを転送します
- AliceがNFTにモザイク制限をかけ、Carolに制御を委任します
- CarolがBobとDaveのモザイク制限を解除します
- BobからDaveにNFTを転送します
- CarolがBobとDaveに再びモザイク制限をかけます

### 共通定義

```js
supplyMutable = false; //供給量変更の可否
transferable = true; //第三者への譲渡可否
restrictable = true; //グローバル制限設定の可否
revokable = false; //発行者からの還収可否
key = sym.KeyGenerator.generateUInt64Key("KYC") // restrictionKey 

```


### グローバル制限制御用モザイク生成

クリエイターCarolがグローバル制限の制御用に数量を持たないモザイクを定義します。

```js
carol = sym.Account.generateNewAccount(networkType);
console.log(carol.address);

//ここで作成したCarolアカウント入金
```

```js
//クリエイターCarolが制限委任を受けるためのモザイク定義
nonce = sym.MosaicNonce.createRandom();
carolMosaicDefTx = sym.MosaicDefinitionTransaction.create(
    undefined,
    nonce,
    sym.MosaicId.createFromNonce(nonce, carol.address),
    sym.MosaicFlags.create(supplyMutable, transferable, restrictable, revokable),
    0,//divisibility
    sym.UInt64.fromUint(0), //duration
    networkType
);

//作成したモザイク定義にグローバル制限情報を追加
carolMosaicGlobalResTx = sym.MosaicGlobalRestrictionTransaction.create(
    undefined,
    carolMosaicDefTx.mosaicId, // mosaicId
    key, // restictionKey
    sym.UInt64.fromUint(0), // previousRestrictionValue
    sym.MosaicRestrictionType.NONE, // previousRestrictionType
    sym.UInt64.fromUint(1), // newRestrictionValue
    sym.MosaicRestrictionType.EQ, // newRestrictionType
    networkType,
);

//トランザクションを集約してネットワークにアナウンス
aggregateTx = sym.AggregateTransaction.createComplete(
    sym.Deadline.create(epochAdjustment),
    [
      carolMosaicDefTx.toAggregate(carol.publicAccount),
      carolMosaicGlobalResTx.toAggregate(carol.publicAccount),
    ],
    networkType,[],
).setMaxFeeForAggregate(100, 0);
signedTx = carol.sign(aggregateTx,generationHash);
await txRepo.announce(signedTx).toPromise();
clog(signedTx);

```
### NFTモザイク生成・転送　

AliceアカウントでNFTモザイクを定義してBobに転送した後、グローバルモザイク制限をCarolで生成したモザイクに委任します。


```js
alice = sym.Account.generateNewAccount(networkType);
console.log(alice.address);

bob = sym.Account.generateNewAccount(networkType);
console.log(bob.address);
```

```js
//NFTモザイク定義
nonce = sym.MosaicNonce.createRandom();
aliceMosaicDefTx = sym.MosaicDefinitionTransaction.create(
    undefined,
    nonce,
    sym.MosaicId.createFromNonce(nonce, alice.address),
    sym.MosaicFlags.create(supplyMutable, transferable, restrictable, revokable),
    0,//divisibility
    sym.UInt64.fromUint(0), //duration
    networkType
);

//NFTモザイク数量定義
aliceMosaicChangeTx = sym.MosaicSupplyChangeTransaction.create(
    undefined,
    aliceMosaicDefTx.mosaicId,
    sym.MosaicSupplyChangeAction.Increase,
    sym.UInt64.fromUint(1),
    networkType
);

//Bobに転送（売却）
transferTx = sym.TransferTransaction.create(
  undefined,
  bob.address, 
  [new sym.Mosaic(aliceMosaicDefTx.mosaicId,sym.UInt64.fromUint(1))],
  sym.EmptyMessage,
  networkType
);

//Bobに転送後グローバルモザイク制限設定（Carolに委任)
aliceMosaicGlobalResTx = sym.MosaicGlobalRestrictionTransaction.create(
    undefined,
    aliceMosaicDefTx.mosaicId, // mosaicId
    key, // restictionKey
    sym.UInt64.fromUint(0), // previousRestrictionValue
    sym.MosaicRestrictionType.NONE, // previousRestrictionType
    sym.UInt64.fromUint(2), // newRestrictionValue
    sym.MosaicRestrictionType.EQ, // newRestrictionType
    networkType,
    carolMosaicDefTx.mosaicId, //ref    
);

aggregateTx = sym.AggregateTransaction.createComplete(
    sym.Deadline.create(epochAdjustment),
    [
      aliceMosaicDefTx.toAggregate(alice.publicAccount),
      aliceMosaicChangeTx.toAggregate(alice.publicAccount),
      transferTx.toAggregate(alice.publicAccount),
      aliceMosaicGlobalResTx.toAggregate(alice.publicAccount),
    ],
    networkType,[],
).setMaxFeeForAggregate(100, 0);

signedTx = alice.sign(aggregateTx,generationHash);
await txRepo.announce(signedTx).toPromise();
clog(signedTx);
```


#### 確認
Bobが他人にNFTを転送できないことを確認します。

```js
dave = sym.Account.generateNewAccount(networkType);
console.log(dave.address);

//daveに手数料分のXYMを入金しておきます。
```

```js
transferTx = sym.TransferTransaction.create(
  sym.Deadline.create(epochAdjustment),
  dave.address, 
  [new sym.Mosaic(aliceMosaicDefTx.mosaicId,sym.UInt64.fromUint(1))],
  sym.EmptyMessage,
  networkType
).setMaxFee(100);
signedTx = bob.sign(transferTx,generationHash);
await txRepo.announce(signedTx).toPromise();
clog(signedTx);

```

`Failure_RestrictionMosaic_Account_Unauthorized`

### 譲渡
BobからDaveへの転送を設定します。
Carolによるモザイク制限の解除、再設定を確認します。
Carolは設定を確認し、妥当な場合のみ署名を行います。

```js
//bobの制限解除
bobUnlockMosaicAddressResTx =  sym.MosaicAddressRestrictionTransaction.create(
    undefined,
    carolMosaicDefTx.mosaicId, // mosaicId
    sym.KeyGenerator.generateUInt64Key("KYC"), // restrictionKey
    bob.address, // address
    sym.UInt64.fromUint(2), // newRestrictionValue
    networkType,
);

//daveの制限解除
daveUnlockMosaicAddressResTx =  sym.MosaicAddressRestrictionTransaction.create(
    undefined,
    carolMosaicDefTx.mosaicId, // mosaicId
    sym.KeyGenerator.generateUInt64Key("KYC"), // restrictionKey
    dave.address, // address
    sym.UInt64.fromUint(2), // newRestrictionValue
    networkType,
);

//daveへ譲渡
transferTx = sym.TransferTransaction.create(
  undefined,
  dave.address, 
  [new sym.Mosaic(aliceMosaicDefTx.mosaicId,sym.UInt64.fromUint(1))],
  sym.EmptyMessage,
  networkType
);

loyaltyTx = sym.TransferTransaction.create(
  undefined,
  carol.address, 
  [new sym.Mosaic(new sym.NamespaceId("symbol.xym"),sym.UInt64.fromUint(100))],
  sym.EmptyMessage,
  networkType
);

//bobの制限
bobLockMosaicAddressResTx =  sym.MosaicAddressRestrictionTransaction.create(
    undefined,
    carolMosaicDefTx.mosaicId, // mosaicId
    sym.KeyGenerator.generateUInt64Key("KYC"), // restrictionKey
    bob.address, // address
    sym.UInt64.fromUint(0), // newRestrictionValue
    networkType,
    sym.UInt64.fromUint(2) //previousRestrictionValue
);

//daveの制限
daveLockMosaicAddressResTx =  sym.MosaicAddressRestrictionTransaction.create(
    undefined,
    carolMosaicDefTx.mosaicId, // mosaicId
    sym.KeyGenerator.generateUInt64Key("KYC"), // restrictionKey
    dave.address, // address
    sym.UInt64.fromUint(0), // newRestrictionValue
    networkType,
    sym.UInt64.fromUint(2) //previousRestrictionValue
);

aggregateTx = sym.AggregateTransaction.createComplete(
    sym.Deadline.create(epochAdjustment),
    [
      bobUnlockMosaicAddressResTx.toAggregate(carol.publicAccount),
      daveUnlockMosaicAddressResTx.toAggregate(carol.publicAccount),
      transferTx.toAggregate(bob.publicAccount),
      loyaltyTx.toAggregate(dave.publicAccount),
      bobLockMosaicAddressResTx.toAggregate(carol.publicAccount),
      daveLockMosaicAddressResTx.toAggregate(carol.publicAccount),
    ],
    networkType,[],
).setMaxFeeForAggregate(100, 2);


signedTx =  aggregateTx.signTransactionWithCosignatories(
    bob,
    [carol,dave],
    generationHash,
);
await txRepo.announce(signedTx).toPromise();
clog(signedTx);

```
今回は説明のために全員の秘密鍵をあらかじめ知っているアグリゲートコンプリートトランザクションを使用していますが、実運用ではオフライン署名あるいはアグリゲートボンデッドトランザクションなどを使用してください。


#### 確認
Daveは譲渡されたNFTが転送できないことを確認します。

```js
transferTx = sym.TransferTransaction.create(
  sym.Deadline.create(epochAdjustment),
  bob.address, 
  [new sym.Mosaic(aliceMosaicDefTx.mosaicId,sym.UInt64.fromUint(1))],
  sym.EmptyMessage,
  networkType
).setMaxFee(100);
signedTx = dave.sign(transferTx,generationHash);
await txRepo.announce(signedTx).toPromise();
clog(signedTx);
```

`Failure_RestrictionMosaic_Account_Unauthorized`

# 結論

これで、クリエイターCarolが承認しなければ転送できないNFTを実現することができました。
この仕組みはCarolの承認が必ず必要なため以下のようなトラブルが発生してしまう可能性があることは留意しておく必要があります。

- Carolの気まぐれあるいは、有効期限内にトランザクションの存在に気付かず譲渡が成立しなかった。
- Carolが鍵を紛失したため、永久にNFTを転送することができなくなった。

また、同様の仕組みとしてmonakaさんが、マルチシグの共有を活用した擬似levyの仕組みを検討されていますのでこちらもご参考ください。

https://qiita.com/monakaJP/items/8682639d68bd86e22936#%E7%96%91%E4%BC%BC%E7%9A%84%E3%81%ABlevy%E3%82%92%E5%86%8D%E7%8F%BE%E3%81%99%E3%82%8B


今回の記事で、プラグインを組み合わせてスマコンを実現するSymbolブロックチェーンの表現力を感じていただければ幸いです。
また、ソースコードを読んでいただけば分かる通り、ロイヤリティの実施は少し複雑です。これらの複雑なトランザクションをボタン一つで確認・完了できるUXを提供することが今後のマーケットプレイスに求められる価値になるのではないかと思います。


# 応用

この1blockで状況復帰させるトランザクションの書き方はさまざまな用途に応用できます。

### 排他制御

NFTを処理実施できるセマフォとみなし、巡回させることで処理の順序づけを制御します。

- Aliceが処理を行い、その作業証明を記録しNFTをBobに転送します。
- Bobが処理を行い、その作業証明を記録しNFTをCarolに転送します。
    - このとき、BobがNFTを所有していない場合、トランザクションが失敗します。
- Carolが処理を行い、その作業証明を記録し、NFTをAliceに転送します。
    - このとき、CarolがNFTを所有していない場合、トランザクションが失敗します。

### フラッシュローン

たとえば以下のような交換を必要とするユーザーや取引所が存在したとします。

- 100XYMと1000xembook.tomatoを交換できる取引所A
- 1000xembook.tomatoと500toshi.tomatoを交換したいユーザーAlice
- 500toshi.tomatoと110XYMを交換できる取引所B

もしあなたが1XYMも持っていなかったとしても、
1ブロックで返却できれば100XYMを貸してくれるサービスが存在した場合、
アグリゲートトランザクションで以下のようなフラッシュローンを組むことができます。

100XYMを借りて取引所Aで1000xembook.tomatoを購入
ユーザーAと交渉し500toshi.tomatoを入手
500toshi.tomatoを元手に取引所Bで110XYMを入手
サービスBに100XYMを返済。差分10XYMを利益として得る。


他にもいろいろな仕組みが考えられると思いますので、何か思いついたかたはぜひ教えてください。