ERC-20の安全策
はじめに
イーサリアムの素晴らしい点の1つとして、トランザクションを変更したり取り消したりできる中央機関が存在しないことがあります。 反対に、イーサリアムでは、ユーザーの間違いや不正なトランザクションを取り消す権限を持つ中央機関が存在しないことがデメリットになりえます。 この記事では、ERC-20トークンでユーザーがしてしまうよくあるミスや、そのミスを防ぐトークンの作成方法について説明します。また、中央機関に権限を与えることについても説明します (例えば、アカウントの凍結など) 。
注意: OpenZeppelin ERC-20トークンコントラクト(opens in a new tab)を使いますが、このコントラクト自体について詳しくは説明しません。 ERC-20トークンコントラクトの詳細については、こちらをご覧ください。
全てのソースコードを表示したい場合は、次のようにします。
- Remix IDE(opens in a new tab)を開きます。
- クローンGitHubアイコン () をクリックします。
- GitHubリポジトリ
https://github.com/qbzzt/20220815-erc20-safety-rails
をクローンします。 - 「contracts > erc20-safety-rails.sol」を開きます。
ERC-20コントラクトの作成
安全策を講じるための機能を追加する前に、ERC-20コントラクトが必要になります。 この記事では、OpenZeppelin Contracts Wizard(opens in a new tab)を使って加えます。 もう一つブラウザで開いて、次の手順に従ってください。
ERC20を選びます。
次の設定値を入力します。
パラメータ 値 名前 SafetyRailsToken Symbol SAFE Premint 1000 機能 なし Access Control Ownable Upgradability なし 上にスクロールして (Remixを使う場合は) Open in Remixをクリックしてください。別の環境を使う場合は、ダウンロードをクリックしてください。 ここでは、Remixを使用していることとします。他の環境を使用する場合は、適宜変更してください。
これで完全なERC-20コントラクトがあります。 「
.deps
>npm
」でインポートしたコードを展開して確認できます。コントラクトをコンパイル、デプロイ、そして操作してERC-20 コントラクトとして機能していることを確認します。 Remixの使用方法を学びたいならば、このチュートリアルが役立ちます(opens in a new tab)。
よくあるミス
ミスのタイプ
ユーザーは、間違ったアドレスへトークンを送信してしまうことがあります。 なぜ間違って送ってしまった理由を知ることはできませんが、よく発生するミスのタイプで頻繁に検出できる次のものがあります。
トークンをコントラクト自身のアドレスに送信する。 例えば、OptimismのOPトークン(opens in a new tab)では、12万(opens in a new tab)を超えるOPトークンが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 virtual3 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");コピー
これが最初の要件です。to
とthis(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つの管理機能を持つものとします。
アカウントの凍結と解凍。 これは、アカウントが侵害された可能性がある場合などに役立ちます。
アセットのクリーンアップ。
時には、詐欺師が正当であると思わせるために、本物のトークンコントラクトに偽物のトークンを送信することがあります。 こちら(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)コントラクトでは、所有者は1人です。onlyOwner
modifier(opens in a new tab)のある関数は、その所有者のみしか呼び出せません。 所有者は、所有権を他の人に譲渡することも、完全に放棄することもできます。 通常、それ以外のアカウントの権限は変わりません。AccessControl
(opens in a new tab)コントラクトでは、ロールベースのアクセス制御 (RBAC)(opens in a new tab)機能があります。
この記事では、簡潔にするために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 unfrozen2 event AccountFrozen(address indexed _addr);3 event AccountThawed(address indexed _addr);コピーアカウントを凍結および解凍するための関数。 これらの2つの関数は、ほぼ同一であるため凍結する関数についてのみ説明します。
1 function freezeAccount(address addr)2 public3 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 dest4 )5 public6 onlyOwner7 {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 }コピー
上記は、すべてのトークンをクリーンアップする関数です。 このプロセスを自動化することで、ユーザーから手動で残高を取得するよりも効率化できます。
まとめ
「ユーザーの間違い」によって発生する問題に完璧な解決策はないため、これらは完全な解決策ではありません。 しかしながら、この記事のようなチェックをすることで、少なくともいくつかのミスを防止できます。 アカウントの凍結機能は危険を伴うものの、ハッカーが資金を盗むことを防ぎ、ハッキングによる被害を特定の範囲内におさめるために使うことができます。
最終編集者: @omahs(opens in a new tab), 2024年2月17日