メインコンテンツへスキップ

ERC-20の安全策

ERC-20
初級
Ori Pomerantz
2022年8月15日
16 分の読書 minute read

はじめに

イーサリアムの素晴らしい点の1つとして、トランザクションを変更したり取り消したりできる中央機関が存在しないことがあります。 反対に、イーサリアムでは、ユーザーの間違いや不正なトランザクションを取り消す権限を持つ中央機関が存在しないことがデメリットになりえます。 この記事では、ERC-20トークンでユーザーがしてしまうよくあるミスや、そのミスを防ぐトークンの作成方法について説明します。また、中央機関に権限を与えることについても説明します (例えば、アカウントの凍結など) 。

注意: OpenZeppelin ERC-20トークンコントラクト(opens in a new tab)を使いますが、このコントラクト自体について詳しくは説明しません。 ERC-20トークンコントラクトの詳細については、こちらをご覧ください。

全てのソースコードを表示したい場合は、次のようにします。

  1. Remix IDE(opens in a new tab)を開きます。
  2. クローンGitHubアイコン (clone github icon) をクリックします。
  3. GitHubリポジトリhttps://github.com/qbzzt/20220815-erc20-safety-railsをクローンします。
  4. contracts > erc20-safety-rails.sol」を開きます。

ERC-20コントラクトの作成

安全策を講じるための機能を追加する前に、ERC-20コントラクトが必要になります。 この記事では、OpenZeppelin Contracts Wizard(opens in a new tab)を使って加えます。 もう一つブラウザで開いて、次の手順に従ってください。

  1. ERC20を選びます。

  2. 次の設定値を入力します。

    パラメータ
    名前SafetyRailsToken
    SymbolSAFE
    Premint1000
    機能なし
    Access ControlOwnable
    Upgradabilityなし
  3. 上にスクロールして (Remixを使う場合は) Open in Remixをクリックしてください。別の環境を使う場合は、ダウンロードをクリックしてください。 ここでは、Remixを使用していることとします。他の環境を使用する場合は、適宜変更してください。

  4. これで完全なERC-20コントラクトがあります。 「.deps > npm」でインポートしたコードを展開して確認できます。

  5. コントラクトをコンパイル、デプロイ、そして操作してERC-20 コントラクトとして機能していることを確認します。 Remixの使用方法を学びたいならば、このチュートリアルが役立ちます(opens in a new tab)

よくあるミス

ミスのタイプ

ユーザーは、間違ったアドレスへトークンを送信してしまうことがあります。 なぜ間違って送ってしまった理由を知ることはできませんが、よく発生するミスのタイプで頻繁に検出できる次のものがあります。

  1. トークンをコントラクト自身のアドレスに送信する。 例えば、OptimismのOPトークン(opens in a new tab)では、12万(opens in a new tab)を超えるOPトークンが2か月もしないうちに累積していることがわかります。 これは、人々の膨大な資産がただ単に失われていることを表しています。

  2. トークンを外部所有アカウントスマートコントラクトに相当しない空アドレスへ送信する。 このミスがどのくらいの頻度で発生するかについての統計はありません。1件のインシデントで2千万トークンを失っているものもあります(opens in a new tab)

送金を防止する

OpenZeppelinのERC-20コントラクトには、_beforeTokenTransferというフック(opens in a new tab)があり、トークンを送金する前に呼び出されます。 デフォルトでは、このフックは何も行いません。しかし、独自の機能をフックに掛けることで、問題がある場合に元に戻すなどのチェックが可能です。

このフックを使用するには、コンストラクタの後に次の関数を加えます。

1 function _beforeTokenTransfer(address from, address to, uint256 amount)
2 internal virtual
3 override(ERC20)
4 {
5 super._beforeTokenTransfer(from, to, amount);
6 }
コピー

Solidityにあまり詳しくない人ならば、次の関数の箇所は馴染みがないかもしれません。

1 internal virtual
コピー

上記のvirtualキーワードでは、ERC20から機能を継承し、関数をオーバーライドして、他のコントラクトも同様にこのコントラクトの機能を継承して、この関数をオーバーライドできるようにしています。

1 override(ERC20)
コピー

ERC20トークンの_beforeTokenTransferの定義をオーバーライド(opens in a new tab)することを明示的に指定しなければなりません。 一般的なセキュリティの観点において、暗黙的な定義よりも明示的な定義の方がはるかに良いとされています。記述されていれば、それが実行されることを忘れないからです。 オーバーライドするスーパークラスの _beforeTokenTransferを指定しなければならないのも同様の理由です。

1 super._beforeTokenTransfer(from, to, amount);
コピー

上記は、継承しているコントラクトから継承元のコントラクトの _beforeTokenTransfer関数を呼び出しています。 この場合は、ERC20のみであり、Ownableにはこのフックがありません。 現時点では、ERC20._beforeTokenTransferは何も行いません。コントラクトはデプロイ後に変更できないため、再デプロイによって将来に機能が追加された場合に備えて呼び出しています。

要件のコーディング

次の要件を関数に対して加えたいと思います。

  • toアドレスをERC-20コントラクト自体のアドレスであるaddress(this)と等しくできないようにすること。
  • toアドレスを空にすることができないこと。また、次のいずれかであること。
    • 外部所有アカウント (EOA) 。 アドレスがEOAであるかどうかを直接確認することはできませんが、アドレスのETH残高を確認することはできます。 EOAは、たとえ使用されなくなったとしても、ほとんどの場合、残高が残っています。これは、最後のweiまで使うのは困難だからです。
    • スマートコントラクト。 アドレスがスマートコントラクトであるかどうかのテストは少し大変です。 EXTCODESIZE(opens in a new tab)という外部コードの長さをチェックするオペコードがありますが、Solidityでは直接使用することはできません。 これには、Yul(opens in a new tab)というEVMアセンブリを使う必要があります。 Solidityから使用できる他の値 ( <address>.codeおよび <address>.codehash(opens in a new tab)) もありますが、コストがそれよりも高くなります。

新しいコードを1行ずつ見てみましょう。

1 require(to != address(this), "Can't send tokens to the contract address");
コピー

これが最初の要件です。tothis(address)が等しくないことを確認しています。

1 bool isToContract;
2 assembly {
3 isToContract := gt(extcodesize(to), 0)
4 }
コピー

上記は、アドレスがコントラクトかどうかを確認する方法です。 Yulから出力を直接受け取ることはできません。そのため、代わりに結果を保持する変数を定義しています (この場合は isToContract) 。 Yulでは、すべてのオペコードが関数として動作します。 したがって、最初にEXTCODESIZE(opens in a new tab)を呼び出してコントラクトサイズを取得し、次にGT(opens in a new tab)でゼロでないことを確認します (符号なし整数を扱っているため、当然、負の値にすることはできません) 。 その後、結果をisToContractに書き込んでいます。

1 require(to.balance != 0 || isToContract, "Can't send tokens to an empty address");
コピー

最後に、空アドレスのチェックをしています。

管理者アクセス

間違いを取り消せる管理者がいると、便利なことがあります。 悪用される可能性を減らすには、管理者をマルチシグ(opens in a new tab)にして、各アクションに対する複数人の同意を必要にします。 この記事では、次の2つの管理機能を持つものとします。

  1. アカウントの凍結と解凍。 これは、アカウントが侵害された可能性がある場合などに役立ちます。

  2. アセットのクリーンアップ。

    時には、詐欺師が正当であると思わせるために、本物のトークンコントラクトに偽物のトークンを送信することがあります。 こちら(opens in a new tab)に、その例があります。 正当なERC-20コントラクトは、0x4200....0042(opens in a new tab)です。 装っているスキャムは、0x234....bbe(opens in a new tab)です。

    また、正当なERC-20トークンを誤ってコントラクト自体に送信してしまう可能性もあります。これが、アセットのクリーンアップ方法が必要になるもう1つの理由です。

OpenZeppelinでは、管理者アクセスを可能にする次の2つのメカニズムを提供しています。

この記事では、簡潔にするためにOwnableを使っています。

コントラクトの凍結および解凍

コントラクトの凍結と解凍において、次のいくつかの変更が必要になります。

  • どのアドレスが凍結されているかを追跡するために、アドレスをブール値(opens in a new tab)マッピング(opens in a new tab)します。 すべての値の初期値はゼロです。これは、ブール値でfalseとして解釈されます。 デフォルトでは、アカウントを凍結しないため、このようにしています。

    1 mapping(address => bool) public frozenAccounts;
    コピー
  • アカウントが凍結または解除されたときに、関係者に対してイベント(opens in a new tab)で通知します。 技術的観点では、アカウントの凍結および解除におけるアクションでは、イベントは必要ありません。しかし、オフチェーンのコードで、これらのイベントをリッスンして何が起こっているかわかると便利です。 関係者に対して何かが発生したときに、スマートコントラクトでイベントを発行することは、良いマナーであるとされています。

    どのタイミングでアカウントの凍結または解除されたかを検索できるように、イベントにインデックスを付けています。

    1 // When accounts are frozen or unfrozen
    2 event AccountFrozen(address indexed _addr);
    3 event AccountThawed(address indexed _addr);
    コピー
  • アカウントを凍結および解凍するための関数。 これらの2つの関数は、ほぼ同一であるため凍結する関数についてのみ説明します。

    1 function freezeAccount(address addr)
    2 public
    3 onlyOwner
    コピー

    public(opens in a new tab)が付けられた関数では、他のスマートコントラクトまたはトランザクションから直接呼び出すことができます。

    1 {
    2 require(!frozenAccounts[addr], "Account already frozen");
    3 frozenAccounts[addr] = true;
    4 emit AccountFrozen(addr);
    5 } // freezeAccount
    コピー

    アカウントがすでに凍結されている場合は、処理を取消します。 それ以外の場合は、凍結してイベントをemitします。

  • _beforeTokenTransferを凍結されたアカウントから資金が移動されないよう変更します。 凍結されたアカウントへの送金は、引き続き可能であることに注意してください。

    1 require(!frozenAccounts[from], "The account is frozen");
    コピー

アセットのクリーンアップ

コントラクト自体が保持しているERC-20トークンを解放するには、それに属しているトークンコントラクトの関数であるtransfer(opens in a new tab)またはapprove(opens in a new tab)を呼び出す必要があります。 この場合、Allowanceで無駄にガスを消費するのはもったいないため、直接送金 (transfer) の方がよいでしょう。

1 function cleanupERC20(
2 address erc20,
3 address dest
4 )
5 public
6 onlyOwner
7 {
8 IERC20 token = IERC20(erc20);
コピー

これは、アドレスがトークンを受け取った場合に、コントラクトにオブジェクトを作成するための構文です。 ERC20トークンがソースコード (4行目を参照) の一部として定義されており、OpenZeppelinのERC-20コントラクトのインターフェースであるIERC20の定義(opens in a new tab)がそのファイルに含まれているためこれが可能です。

1 uint balance = token.balanceOf(address(this));
2 token.transfer(dest, balance);
3 }
コピー

上記は、すべてのトークンをクリーンアップする関数です。 このプロセスを自動化することで、ユーザーから手動で残高を取得するよりも効率化できます。

まとめ

「ユーザーの間違い」によって発生する問題に完璧な解決策はないため、これらは完全な解決策ではありません。 しかしながら、この記事のようなチェックをすることで、少なくともいくつかのミスを防止できます。 アカウントの凍結機能は危険を伴うものの、ハッカーが資金を盗むことを防ぎ、ハッキングによる被害を特定の範囲内におさめるために使うことができます。

このチュートリアルは役に立ちましたか?