ERC20トークンをスマートコントラクトに送金すると失われる…と誤解してしまった話

# このメモについて
スマートコントラクトについて学習中の人間が書いた勉強メモです。初心者が書いたものなので正しくない記述があるかもしれません。古い書籍に基づいて初歩的な技術確認をするような内容に過ぎないので、真新しい情報なども無いですし何かの参考になるということも無いです。突っ込みどころの指摘や情報提供などあればコメント大歓迎です。 `m(_ _)m`

# やりたいこと
ERC20トークンを保有するスマートコントラクトから、別のEOAへ送金させたい。

# 実験
OpenZeppelinのERC20から派生したMyTokenコントラクトを実装し、独自トークンとしてMFTを作成した。これを受け取るスマートコントラクトとしてMyFaucetを用意し、MyTokenからMyFaucetへMFTをtransferした。MyFaucetから適当なEOAにMFTを転送した場合、MyFaucetのMFT残高がどう変化するかを確認した。

## 環境
以下の環境で開発を行う(本稿では各環境名の大文字、小文字表記を特に区別しない)。

- Windows 10 Home
- Visual Studio Code v1.68.1
- Solidityコンパイラ 0.8.15
- Truffle v5.5.20
- Ganache CLI v6.12.2 (ganache-core: v2.13.2)
- Visual Studio Code Extension: solidity v0.0.139
- OpenZeppelin v4.4.1

## 実装
独自ファンジブルトークンMyToken(MFT)の実装は以下のような感じ。
```
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

import '@openzeppelin/contracts/token/ERC20/ERC20.sol';

contract MyToken is ERC20 {
    constructor() ERC20("MyFT", "MFT") {
        _mint(msg.sender, 100);
    }
}
```
次にMFTの引き出しに対応したファウセットコントラクト MyFaucet を作成する。MyFaucetに送金したMFTを、別のEOAに送金したい。果たして可能なのか？そこで以下のようにMyFaucetを実装してみた。マスタリングイーサリアムの例示を参考にした。
```
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

import '@openzeppelin/contracts/token/ERC20/ERC20.sol';

contract MyFaucet
{
    ERC20 public MyToken;
    address public MyTokenOwner;

    constructor(address _MyToken, address _MyTokenOwner) {
        MyToken = ERC20(_MyToken);
        MyTokenOwner = _MyTokenOwner;
    }

    // MyFaucetがMyTokenからapproveされていれば引き出せる
    function withdraw(uint withdraw_amount) public {
        require(withdraw_amount <= 1000);
        // withdraw()の呼び出し元アドレスへ指定額を送金 (approveされた範囲内)
        MyToken.transferFrom(MyTokenOwner, msg.sender, withdraw_amount);
    }

    // このコントラクトへの送金を禁止
    fallback() external { revert(); }
}
```
引き出すための関数としてwithdraw()を実装した。この中でMyToken.transferFrom()を呼び出すことにより出金が可能なはず。ただしtransferFromを行うにはMyToken.approve()によってMyFaucetのアドレスに送金許可を出しておく必要がある。以下、MyTokenとMyFaucetのデプロイを行うデプロイスクリプトを示す。
```
var MyToken = artifacts.require('MyToken');
var MyFaucet = artifacts.require('MyFaucet');

module.exports = async function (deployer, network, accounts) {
  await deployer.deploy(MyToken);
  await deployer.deploy(MyFaucet, MyToken.address, accounts[0]);
};
```

## 確認方法
いつも通り`truffle migrate`コマンドを用いてデプロイしたら、`truffle console`で対話操作を行ってみる。まずはMyTokenからMyFaucetへMFTを送金する。
```
truffle(development)> MyToken.deployed().then((instance) => instance.transfer(MyFaucet.address, 10))
```
実際に送金されているかをbalanceOfで確認する。
```
truffle(development)> MyToken.deployed().then((instance) => instance.balanceOf(MyFaucet.address))
BN { negative: 0, words: [ 10, <1 empty item> ], length: 1, red: null }
```
MyFaucetに他アカウントへのMFT送金を許すため、MyTokenからapproveを行う。
```
truffle(development)> MyToken.deployed().then((instance) => instance.approve(MyFaucet.address, 10))
```
MyFaucetからアカウント1番にMFTを転送する。
```
truffle(development)> web3.eth.getAccounts().then(res => accounts)
truffle(development)> MyFaucet.deployed().then((instance) => instance.withdraw(10, {from:accounts[1]}))
```
これでMyFaucetの保有するMFTは0になったはず？balanceOfで再確認してみる。
```
truffle(development)> MyToken.deployed().then((instance) => instance.balanceOf(MyFaucet.address))
BN { negative: 0, words: [ 10, <1 empty item> ], length: 1, red: null }
```
期待に反してMyFaucetのMFT残高は変わっていない。実際にはMyTokenから直接MFTが移動している。それはMyToken.addressに対するbalanceOfで確認できる。

# 疑問
MyTokenからMyFaucetに預け入れておいたMFTはあくまでMyTokenコントラクトから引き出されているらしい。つまりMyTokenコントラクトがMyFaucet.address宛に全てのMFTを送ってしまうと、それらは永遠に失われるということか？MyTokenコントラクトのMFT残高が0になれば。MyFaucet.withdraw()も行えなくなるはず。MyFaucetの残高からの出金をする手段はないのか？

# 結論
**ERC20トークンを保有するスマートコントラクトの残高から、別のEOAへ `transferFrom` で送金することはできない。今回の目的を果たすには `transfer` を使うべきだった。**

## transferFrom に関する自分の誤解
transferFromとapproveの仕様に関する理解が間違っていた。approve はtransferFromとセットで扱う、ERC20の受信可能アドレスと受信可能量を指定する関数である。approve(MyFaucet.address, 100)とした場合、このapproveの発行元アドレスからMyFaucet.addressが受信できるERC20トークンが最大100になる。したがって下記のようにすることでMyFaucetからaccounts[1]へのトークン転送を許可できるのではないかと考えた。
```
truffle(development)> MyToken.deployed().then(instance => instance.approve(accounts[1], 100, {from:MyFaucet.address}))
```
しかしこれは "'Error: Returned error: sender account not recognized" というエラーで失敗する。
[OpenZeppelinのフォーラムを見るに、トランザクションを初期化できるのはEOAのみに限定されているとのこと](https://forum.openzeppelin.com/t/erc20-transferfrom-returned-error-sender-account-not-recognized/5057)。スマートコントラクトからの発行はできない。実際、下記の場合は成功する。
```
truffle(development)> MyToken.deployed().then(instance => instance.approve(accounts[1], 100, {from:accounts[2]}))
```
つまりスマートコントラクトMyFaucetから別のEOAへのtransferFromは実現できない。~~そもそもトークンをスマートコントラクトへ転送できないほうがいい。ERC223を使ったほうがいい。~~

## transfer について
@blueplanet さんからいただいたコメントにあるように、transfer を使えば MyFaucet の残高からほかのEOAへのトークンの送金が可能だった ([確認用コードはこちら](https://qiita.com/g_fukurowl/items/71e1b12511ea3f322ed7#comment-771c7d403d46242e5b4e))。本記事を書いた当初、transferFrom でやりたいことが実現できなかったために「スマートコントラクトに送られたERC20トークンは二度と引き出せない(失われる)」と思い込んでしまったが、これは誤りだった。

# 新しい疑問
- ERC20を敢えて今使う理由とは？
- ~~NFT(ERC721)でもERC20と同じような問題はあるのか？あるとしたらどう回避する？~~