# [ERC6105] NFTマケプレに頼らずにNFTの取引を実現する仕組みを理解しよう！

> このERC（Ethereum Request for Comments）は、ERC721にマーケットプレイスの機能を追加して、中間取引プラットフォームに頼らずに非代替可能トークン（NFT）の取引を可能にするものです。

- URL: https://cardene.dev/articles/a9f1d3f91867955490a1
- 公開日: 2023-08-20
- 更新日: 2023-08-20
- トピック: Blockchain
- タグ: ethereum, solidity, ブロックチェーン, nft

## はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、NFTマーケットプレイスに依存せずにNFTの取引を実現できる提案である**ERC6105**についてまとめていきます！

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

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

## 概要

このERC（Ethereum Request for Comments）は、**ERC721**にマーケットプレイスの機能を追加して、中間取引プラットフォームに頼らずに非代替可能トークン（NFT）の取引を可能にするものです。同時に、クリエーターはさまざまなロイヤリティスキームを実装できるようになります。

## 動機
現在のほとんどのNFT取引は、中間取引プラットフォームが仲介役として機能することに依存しています。

これには以下の問題があります。

#### setApprovalForAll関数を介したセキュリティ上の懸念

NFT取引において、`setApprovalForAll`関数を使った承認にはセキュリティ上の問題があります。
この関数を使って、NFT所有者は特定のアカウントに対してトークンを操作する権限を与えることができます。
しかし、この方法では不正なアクセスや詐欺のリスクが存在します。
もし取引プラットフォームの契約に問題があれば、多くの人々に影響が及び、業界全体に深刻な損害をもたらす可能性があります。
さらに、ユーザーが取引プラットフォームにNFT操作権限を許可している場合、フィッシング詐欺によってユーザーが詐欺師によって設定された低価格の注文に署名させられ、自分自身を受信者として指定される可能性があります。
これは普通のユーザーにとって防ぎがたい脅威です。

:::message
NFTを売買できるプラットフォームである**Opensea**なども、ログイン時にユーザーが接続したウォレットに対して`setApprovalForAll`関数の実行を求めています。
この関数を実行することで、ユーザーの保有しているNFTをプラットフォームが送付する権限を渡すことができます。

よく問題になっていたのが、**Opensea**にそっくりなサイトに誤ってウォレットを接続して`setApprovalForAll`関数の実行を許可してしまったときです。
この場合、偽のサイトを作った悪意あるアドレスがユーザーのNFTを好きなようにできてしまい、全てのNFTをとられる危険性があります。
:::

#### 高い取引コストの問題

NFT取引のコストは高額であることが課題です。
取引プラットフォームの数が増えるにつれて、NFTの流動性が分散し、ユーザーは複数のプラットフォームで取引を行う必要があります。
これにより、ユーザーは複数のプラットフォームで認可を行い、注文を出す必要があり、それに伴ってリスクも増加します。
例えば、BAYCというNFTを取引対象に考えると、その総供給量は`10,000`枚で保有者は`6,000`人以上ですが、平均的な保有者ごとの`BAYC`の数は2未満です。
`setApprovalForAll`は1つのプラットフォームで保留中の注文のガス費用を節約する効果がありますが、複数のプラットフォームで認可を行うと、ユーザーの総ガス費用が増加します。
加えて、取引プラットフォームが取引手数料を請求することもあり、これが認可にかかるガス費用を上回ることもしばしばあります。

:::message
`setApprovalForAll`関数はスマートコントラクト内のデータを書き換えるため、ガス代がかかります。
そのため、何度も実行するとその度にガス代が多少なりともかかります。
:::

#### アグリゲーターの中央集権的な意思決定

アグリゲーターは、複数の取引プラットフォームからの流動性をまとめて提供する方法を提供します。
これによって、NFTの取引がより効率的に行えるようになります。
ただし、このアグリゲーター自体が取引情報のまとめ役として機能するため、意思決定プロセスが中央集権的になることがあります。
つまり、どの取引を優先的に提示するか、どのプラットフォームから情報を収集するかなどの意思決定は、アグリゲーターの運営者によって行われます。

そのため、取引情報のまとめ方や選択基準などは、アグリゲーター運営者の判断に依存するため、透明性や分散性が損なわれる可能性があります。
ユーザーにとっては、どの取引情報が選ばれるのか、どのプラットフォームが優先されるのかについての詳細な情報が提供されない場合、アグリゲーターの意思決定プロセスが不透明であると感じる可能性があります。

さらに、取引プラットフォームの注文情報は通常オフチェーンで管理されています。
アグリゲーターが取引プラットフォームから情報を取得する際には、取引プラットフォームのAPI（Application Programming Interface）の使用が必要です。
しかし、これらのAPIは頻繁にアクセスすることが制限されたり、一時的に停止されたりすることがあります。
その結果、アグリゲーターが取引情報を正確に取得できないことがあり、ユーザー体験や情報の正確性に影響を及ぼす可能性があります。

要するに、アグリゲーターは流動性を向上させる一方で、その意思決定プロセスが中央集権的であったり、取引情報の取得に制約があることがあるため、透明性や分散性の課題が生じる可能性があると言えます。

#### ロイヤルティ収入の中央集権化

NFTプロジェクト関係者のロイヤルティ収入は、取引プラットフォームの中央集権的な意思決定に依存しています。
一部の取引プラットフォームはプロジェクト関係者の同意なしにオプションのロイヤルティを実装することがあり、これはプロジェクト関係者の利益を損なう可能性があります。

:::message
NFTのロイヤリティについては以下を参考にしてください。
:::

[\[ERC2981\]NFTのロイヤリティ強制化!? ERC2981の仕組みについて理解しよう！](/articles/456ccacca080fd72e3ec)

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

#### 検閲への耐性の不足

一部のNFT取引プラットフォームは検閲に対して耐性がありません。
これによって、プラットフォームが特定のNFTをリストから削除することがあり、その削除のルールや実施方法が中央集権的で透明性に欠けることがあります。
過去には、誤って特定のNFTを削除することで市場が混乱することがありました。

:::message
またまた、**Opensea**の例になりますが、今でもNFTを新規発行するプロジェクトの公式のコレクションに混じって公式と同じ画像、説明文などの偽物のコレクションがわらわらと出てくることがあります。
このとき、ユーザーは間違って偽物のコレクションからNFTを購入してしまう危険性があります。
そのため、**Opensea**側で偽物のコレクションを削除していたのですが、過去に誤って公式のコレクションが削除されてしまったことがありました。
このようにプラットフォームの権限で削除できるメリット・デメリットが存在します。
:::

## 仕様

この提案に準拠するコントラクトは、以下のインターフェースを実装する必要があります。

```javascript
interface IERC6105 {
royalties
  event UpdateListing(
    uint256 indexed tokenId,
    address indexed from,
    uint256 salePrice,
    uint64 expires,
    address supportedToken,
    uint256 benchmarkPrice
    );

  event Purchased(
    uint256 indexed tokenId,
    address indexed from,
    address indexed to,
    uint256 salePrice,
    address supportedToken,
    uint256 royalties
    );

  function listItem(
    uint256 tokenId,
    uint256 salePrice,
    uint64 expires,
    address supportedToken
    ) external;

  function listItem(
    uint256 tokenId,
    uint256 salePrice,
    uint64 expires,
    address supportedToken,
    uint256 benchmarkPrice
    ) external;
 
  function delistItem(uint256 tokenId) external;
 
  function buyItem(uint256 tokenId, uint256 salePrice, address supportedToken) external payable;

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

#### UpdateListing

- **Event**

トークンが売り出し中または売り出し解除された時に発行されるイベント。

**パラメータ**

- `tokenId`
    - 売り出されるトークンのID。
- `from`
    - トークンを出品するアドレス。
- `salePrice`
    - トークンの売価。
- `expires`
    - 購入期限のUNIXタイムスタンプ。
- `supportedToken`
    - サポートされたトークンのコントラクトアドレス（ETHの場合は`0`アドレス）。
- `benchmarkPrice`
    - ロイヤリティ計算時に使用される追加の価格パラメータ。

#### Purchased

- **Event**

トークンが購入された時に発行されるイベント。

**パラメータ**

- `tokenId`
    - 購入されたトークンのID。
- `from`
    - トークンを出品したアドレス。
- `to`
    - トークンを購入したアドレス。
- `salePrice`
    - トークンの購入価格。
- `supportedToken`
    - サポートされたトークンのコントラクトアドレス（ETHの場合は`0`アドレス）。
- `royalties`
    - この購入に支払われるロイヤリティの量。

#### listItem

- **Function**

トークンを売り出すためのリストアップを行う関数。

**パラメータ**

- `tokenId`
    - 売り出すトークンのID。
- `salePrice`
    - トークンの売価。
- `expires`
    - 購入期限のUNIXタイムスタンプ。
- `supportedToken`
    - サポートされたトークンのコントラクトアドレス（ETHの場合は`0`アドレス）。

#### delistItem

- **Function**

トークンのリストアップを解除する関数。

**パラメータ**

- `tokenId`
    - リストアップを解除するトークンのID。

#### buyItem

- **Function**

トークンを購入して取引を実行する関数。
購入対象のトークンがリストアップされており、価格とサポートされたトークンが正しく一致しているかチェックします。

**パラメータ**

- `tokenId`
    - 購入するトークンのID。
- `salePrice`
    - トークンの購入価格。
- `supportedToken`
    - サポートされたトークンのコントラクトアドレス（ETHの場合は`0`アドレス）。

#### getListing

- **Function**

特定のトークンのリストアップ情報を取得する関数。

**パラメータ**

- `tokenId`
    - 情報を取得するトークンのID。

**戻り値**

リストアップ情報（売価、購入期限、サポートされたトークン、ベンチマーク価格）。

### コレクションのオファー拡張機能

```javascript
interface IERC6105CollectionOffer {

  event UpdateCollectionOffer(address indexed from, uint256 amount, uint256 salePrice ,uint64 expires, address supportedToken);

  function makeCollectionOffer(uint256 amount, uint256 salePrice, uint64 expires, address supportedToken) external;

  function acceptCollectionOffer(uint256 tokenId, uint256 salePrice, address supportedToken, address buyer) external;

  function acceptCollectionOffer(uint256 tokenId, uint256 salePrice, address supportedToken, address buyer, uint256 benchmarkPrice) external;

  function cancelCollectionOffer() external;

  function getCollectionOffer(address buyer) external view returns (uint256, uint256, uint64, address);
}
```

#### UpdateCollectionOffer

- **Event**

コレクションがオファーを受けたか、オファーがキャンセルされた時に発行されるイベント。

**パラメータ**

- `from`
    - コレクションオファーを行ったアドレス。
- `amount`
    - オファーを出した人が`salePrice`ごとに購入したい数量。
- `salePrice`
    - コレクション内の各トークンのオファー価格。
- `expires`
    - オファーが受け付けられる期限のUNIXタイムスタンプ。
- `supportedToken`
    - サポートされたトークンのコントラクトアドレス。

#### makeCollectionOffer

- **Function**

コレクションに対してオファーを作成または更新する関数。

**チェック項目**

- コントラクトを十分な量のサポートトークンで承認し、所有している。
- `salePrice`が`0`でない。
- `amount`が`0`でない。
- `expires`が有効である。
- `UpdateCollectionOffer`イベントを発行する。

**パラメータ**

- `amount`
    - オファーを出した人が`salePrice`ごとに購入したい数量。
- `salePrice`
    - コレクション内の各トークンのオファー価格。
- `expires`
    - オファーが受け付けられる期限のUNIXタイムスタンプ。
- `supportedToken`
    - サポートされたトークンのコントラクトアドレス。

#### acceptCollectionOffer

- **Function**

コレクションオファーを受け入れてトークンを購入者に送付する関数。

**チェック項目**

- `tokenId`が存在し、オファーされている。
- 呼び出し元が所有者、承認されたオペレータ、またはトークンの承認済みアドレスである。
- `Purchased`イベントを発行する。

**パラメータ**

- `tokenId`
    - オファーされているトークンのID。
- `salePrice`
    - トークンのオファー価格。
- `supportedToken`
    - サポートされたトークンのコントラクトアドレス。
- `buyer`
    - トークンを購入したいアドレス。

#### cancelCollectionOffer

- **Function**

コレクションオファーをキャンセルする関数。

**パラメータ**

- `buyer`
    - トークンを購入したいアドレス。

**戻り値**

オファー情報（数量、オファー価格、期限、サポートトークンのアドレス）。

### アイテムのオファー拡張機能

```javascript
interface IERC6105ItemOffer {

  event UpdateItemOffer(
    uint256 indexed tokenId,
    address indexed from,
    uint256 salePrice,
    uint64 expires,
    address supportedToken
    );

  function makeItemOffer(uint256 tokenId, uint256 salePrice, uint64 expires, address supportedToken) external;

  function cancelItemOffer(uint256 tokenId) external;

  function acceptItemOffer(uint256 tokenId, uint256 salePrice, address supportedToken, address buyer) external;

  function acceptItemOffer(uint256 tokenId, uint256 salePrice, address supportedToken, address buyer, uint256 benchmarkPrice) external;

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

#### UpdateItemOffer

- **Event**

トークンがオファーを受けたか、オファーがキャンセルされた時に発行されるイベント。

**パラメータ**

- `tokenId`
    - オファーされているトークンのID。
- `from`
    - トークンを購入したいアドレス。
- `salePrice`
    - トークンのオファー価格。
- `expires`
    - オファーが受け付けられる期限のUNIXタイムスタンプ。
- `supportedToken`
    - サポートされたトークンのコントラクトアドレス。

#### makeItemOffer

- **Function**

トークンに対してオファーを作成または更新する関数。

**チェック項目**

- `tokenId`が存在する。
- コントラクトを十分な量のサポートトークンで承認し、所有している。
- `salePrice`が`0`でない。
- `expires`が有効である。
- `UpdateItemOffer`イベントを発行する。

**パラメータ**

- `tokenId`
    - オファーされているトークンのID。
- `salePrice`
    - トークンのオファー価格。
- `expires`
    - オファーが受け付けられる期限のUNIXタイムスタンプ。
- `supportedToken`
    - サポートされたトークンのコントラクトアドレス。

#### cancelItemOffer

- **Function**

トークンのオファーをキャンセルする関数。

**チェック項目**

- `tokenId`が存在し、オファーされている。
- 呼び出し元がオファーを出したアドレス。
- `UpdateItemOffer`イベントを発行する。

**パラメータ**

- `tokenId`
    - オファーをキャンセルするトークンのID。

#### acceptItemOffer

- **Function**

トークンのオファーを受け入れてトークンを購入者に送付する関数。

**チェック項目**

- `tokenId`が存在し、オファーされている。
- 呼び出し元が所有者、承認されたオペレータ、またはトークンの承認済みアドレスである。
- `Purchased`イベントを発行する。

**パラメータ**

- `tokenId`
    - オファーされているトークンのID。
- `salePrice`
    - トークンのオファー価格。
- `supportedToken`
    - サポートされたトークンのコントラクトアドレス。
- `buyer`
    - トークンを購入したいアドレス。

#### getItemOffer

- **Function**

特定のトークンと`buyer`によるトークンのオファー情報を取得する関数。

**パラメータ**

- `tokenId`
    - オファー情報を取得するトークンのID。
- `buyer`
    - トークンを購入したいアドレス。

**戻り値**

オファー情報（オファー価格、期限、サポートトークンのアドレス）。

## 補足

### ローカル変数に関する考慮事項

`listItem`関数内の`salePrice`は`0`に設定できません。
- 呼び出し側が価格を`0`に設定することは稀であり、その際は通常運用上のエラーが原因であり、資産の損失を引き起こす可能性があります。
- 呼び出し側はこの関数を呼び出すためにガスを消費する必要があるため、トークン価格を`0`に設定できると、その時点で実際の収益がマイナスになることになり、経済学的観点から合理的ではありません。
- トークン価格が`0`の場合、アイテムが販売対象でないことが示され、参考実装がより簡潔になります。

:::message
もしトークン価格を`0`に設定できる場合、呼び出し側はガスを消費して関数を実行しても、収益が発生しない状況が生まれます。
具体的には、トークンの価値が`0`なのに、関数の実行にはガスが必要なため、ガスコストが支払われることになります。
この状況は、経済学的な観点から考えると合理的でないとされています。
経済学では、経済的な判断をする個人は、自身の利益を最大化しようとする合理的な行動者として捉えられています。
しかし、トークン価格が`0`である場合、ガスコストを支払って関数を呼び出すことで、呼び出し側の収益がマイナスになってしまうため、この行動は合理的とは言えません。

さらに、トークン価格が`0`の場合、それはアイテムが現在販売されていないことを示します。
この情報をトークン自体に組み込むことで、トークンが販売中であるかどうかを簡潔に示すことができます。
これにより、他の関数やプロセスがアイテムの販売ステータスを確認しやすくなり、トークンの状態を理解するためのコードがよりわかりやすくなります。
:::

`listItem`関数内で`expires`を設定することで、呼び出し側がリスティングをより良く管理できるようになります。
リスティングが自動的に期限切れになる場合、トークンの所有者はもはや手動で`delistItem`を行う必要がなく、それによってガスが節約されます。

`listItem`関数内で`supportedToken`を設定することで、呼び出し側またはコントラクトオーナーは受け入れるトークンを単一のトークンに限定されることなく選択できるようになります。

`acceptCollectionOffer`および`acceptItemOffer`関数内の変数設定の理由は、上記で説明したものと同じです。

より多様なロイヤリティスキーム
`listItem`、`acceptCollectionOffer`、`acceptItemOffer`関数内で`benchmarkPrice`パラメータを導入することにより、**ERC2981**インターフェースの `royaltyInfo(uint256 _tokenId, uint256 _salePrice)`関数内の`_salePrice`を`taxablePrice`に変更でき、**ERC2981**ロイヤリティスキームをより多様にすることができます。
以下にいくつかのロイヤリティスキームの例を示します。

```javascript
（address royaltyRecipient、uint256 royalties）= royaltyInfo(tokenId、taxablePrice)
```

1. 付加価値ロイヤリティ（VAR、ロイヤリティは売り手の利益の一部のみが課金されます）
    - `taxablePrice = max（salePrice - historicalPrice、0）`
2. セールロイヤリティ（SR）
    - `taxablePrice = salePrice`
3. 上限ロイヤリティ（CR）
    - `taxablePrice = min（salePrice、constant）`
4. 数量ロイヤリティ（QR、各トークン取引が固定のロイヤリティを支払う）
    - `taxablePrice = constant`

### オプションのブロックリスト
一部の観点では、ロイヤリティスキームに準拠しない中間市場でのトークン取引を防止するべきであるとされていますが、この標準は非中間NFT取引の機能を提供するだけであり、これらの市場でトークンの取引を防止するための標準化されたインターフェースは提供していません。
プロジェクトチームとコミュニティの利益をよりよく保護するために、プロジェクトのロイヤリティスキームに準拠していないプラットフォームでのNFT取引を防止するためのブロックリストを実装こんとらくとに追加することを検討することがあります。

:::message
一部の人々は、NFT（非代替可能トークン）の取引において、プロジェクトのロイヤリティスキーム（販売手数料や収益の一部を支払う仕組み）に準拠していない中間市場での取引を防ぐ必要性があると考えています。
つまり、プロジェクトが設定した取引ルールやロイヤリティを尊重していない取引所やプラットフォームでの取引を防ぐべきだという意見です。

しかし、現行のNFT標準では、非中間市場（直接的なトークンの取引）に関する機能を提供していますが、中間市場における取引に対する防止策や規制を提供する標準化された方法は提供していません。
したがって、プロジェクトが自身のロイヤリティスキームに準拠していない市場でのNFT取引をどのように制御するかについては、明確なガイドラインや標準が存在しない状況です。

このため、プロジェクトチームやコミュニティは、自身の利益や価値を保護するために、プロジェクトのロイヤリティスキームに準拠しないプラットフォームでのNFT取引を防止するための方法を検討しています。
一つの提案として、ブロックリスト（特定のアドレスやプラットフォームをリストアップする）を導入して、プロジェクトの方針に合致しない取引が行われないようにする方法が考えられています。
これにより、プロジェクトが望むロイヤリティスキームを尊重し、コミュニティの信頼を保ちつつ、健全な取引環境を維持することを目指しています。
:::

## 後方互換性
この規格は**ERC721**および**ERC2981**と互換性がある。

## 参考実装

以下に参考実装を置いておきました。

https://github.com/cardene777/base-code/blob/develop/contract/contracts/ERC6105/ERC6105.sol

引用: https://eips.ethereum.org/EIPS/eip-6105

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

セキュリティの観点から考えると、`buyItem`関数、および`acceptCollectionOffer`と`acceptItemOffer`関数には、フロントランニングのリスクが潜在しています。
このため、フロントランニング攻撃を防ぐためには、`salePrice`と`supportedToken`が予想される価格とトークンに一致するかを確認する必要があります。

:::message
フロントランニング攻撃（Front-Running Attack）は、ブロックチェーンや暗号通貨取引において発生するセキュリティ上の脆弱性の一つです。
この攻撃では、攻撃者が他のユーザーのトランザクションを事前に検知し、そのトランザクションの内容をコピーして、自分のトランザクションを同じ内容で送信することで、不正な利益を得ることを試みます。

具体的には、攻撃者は他のユーザーが送信しようとしているトランザクションを観察し、そのトランザクションの内容を自分のトランザクションとしてブロックチェーンに送信します。
攻撃者のトランザクションが最初に採用されると、他のユーザーのトランザクションは取り消され、攻撃者が利益を得ることができます。
これは、取引の順序やタイミングを悪用して、価格の変動などを利用する攻撃方法です。

フロントランニング攻撃は、特にディセントラライズドな環境で発生しやすく、暗号通貨取引や分散型アプリケーションにおいて注意が必要なセキュリティリスクの一つです。
プロトコルやスマートコントラクトの設計段階で対策を考慮し、セキュリティを確保する必要があります。
:::

また、`acceptCollectionOffer`と`acceptItemOffer`関数には再入可能性のリスクがあります。
これに対処するためには、チェック、エフェクト、インタラクション（CEIパターン）のパターンに従うか、再入可能性ガードを使用する必要があります。

:::message
再入可能（リエントランシー）攻撃については以下を参照してください。
:::

https://chaldene.net/solidity-reentrancy-attack

NFTを購入する際に**ERC20**トークンを使用する場合、購入者はまず**ERC20**トークンの`approve(address spender, uint256 amount)`関数を呼び出して、NFTコントラクトに一定量のトークンへのアクセス権限を付与する必要があります。
適切な量を承認することを確認してください。
さらに、非監査済みのコントラクトと取引する際には注意が必要です。
これにより、セキュリティ上のリスクを最小限に抑えながら取引を行うことが重要です。


## 引用

5660-eth (@5660-eth), Silvere Heraudeau (@lambdalf-dev), Martin McConnell (@offgridgecko), Abu <team10kuni@gmail.com>, Wizard Wang, "ERC-6105: No Intermediary NFT Trading Protocol," Ethereum Improvement Proposals, no. 6105, December 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6105.

## 考察
マーケットプレイスに依存しない方法での売買を可能にする仕様は面白いと思いました。
ただ、あくまでこの仕様に沿っていてもマーケットプレイスは無視して売買できてしまうので、その点は注意が必要です。
拡張機能でNFTコレクションやNFTごとにオファーなどを管理できるので、複数プラットフォームでこの規格を参照することで一貫した処理ができるのは特に興味深いです。
この規格を使用して何かを作る場合は、NFTマーケットプレイス関連になるため、NFTマーケットプレイスを作成するかNFTマーケットプレイスに情報を提供するコントラクトを作成するかの２択になるのかなと考えています。
どちらも挑戦するには良いレベルな気がするのでちょっとやってみたさが出てきました。

## 最後に

今回は「商品を購入したレシートをNFTのメタデータに書きこんで、ブロックチェーン上に書き込むことができる**ERC5570**」についてまとめてきました！
いかがだったでしょうか？

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

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