安全策を備えたERC-20
はじめに
イーサリアムの素晴らしい点の1つとして、トランザクションを変更したり取り消したりできる中央機関が存在しないことがあります。 反対に、イーサリアムでは、ユーザーの間違いや不正なトランザクションを取り消す権限を持つ中央機関が存在しないことがデメリットになりえます。 この記事では、ERC-20トークンでユーザーがしてしまうよくあるミスや、そのミスを防ぐトークンの作成方法について説明します。また、中央機関に権限を与えることについても説明します (例えば、アカウントの凍結など) 。
この記事ではOpenZeppelin ERC-20トークンコントラクト (opens in a new tab)を使用しますが、その詳細については説明しませんのでご注意ください。 この情報については、こちらをご覧ください。
完全なソースコードを確認したい場合は、以下を参照してください。
- 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 記号 SAFE Premint 1000 機能 なし Access Control Ownable Upgradability なし -
上にスクロールして、(Remixの場合は) Open in Remix を、別の環境を使用する場合は Download をクリックします。 ここでは、Remixを使用していることとします。他の環境を使用する場合は、適宜変更してください。
-
これで完全に機能するERC-20コントラクトができました。
.deps>npmを展開すると、インポートされたコードを確認できます。 -
コントラクトをコンパイル、デプロイ、そして操作して、ERC-20コントラクトとして機能していることを確認します。 Remixの使い方を学ぶ必要がある場合は、このチュートリアル (opens in a new tab)を使用してください。
よくある間違い
間違い
ユーザーは、間違ったアドレスへトークンを送信してしまうことがあります。 ユーザーが何を意図していたかを知ることはできませんが、頻繁に発生し、かつ検出しやすいエラーには2つのタイプがあります。
-
トークンをコントラクト自身のアドレスに送信する。 例えば、OptimismのOPトークン (opens in a new tab)は、2か月足らずで120,000 (opens in a new tab)以上のOPトークンを蓄積してしまいました。 これは、おそらく人々が失ってしまったであろう、かなりの額の資産に相当します。
-
トークンを空のアドレス、つまり外部所有アカウントやスマートコントラクトではないアドレスに送信してしまうこと。 これがどのくらいの頻度で発生するかについての統計はありませんが、あるインシデントでは20,000,000トークンが失われる可能性がありました (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 virtualvirtualキーワードは、私たちがERC20から機能を継承してこの関数をオーバーライドしたのと同じように、他のコントラクトが私たちのコントラクトから継承してこの関数をオーバーライドできることを意味します。
1 override(ERC20)_beforeTokenTransferのERC20トークン定義をオーバーライド (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で直接利用することはできません。 そのためには、EVMアセンブリであるYul (opens in a new tab)を使用する必要があります。 Solidityから使用できる他の値 (<address>.codeや<address>.codehash(opens in a new tab)) もありますが、それらはより多くのコストがかかります。
新しいコードを1行ずつ見ていきましょう。
1 require(to != address(this), "コントラクトアドレスにトークンを送信することはできません");これが最初の要件で、toとaddress(this)が同じでないことを確認します。
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, "空のアドレスにトークンを送信することはできません");そして最後に、空のアドレスに対する実際のチェックを行います。
管理者アクセス
間違いを取り消せる管理者がいると、便利なことがあります。 悪用の可能性を減らすため、この管理者をマルチシグ (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トークンを誤って私たちのコントラクトに送ってしまう可能性もあります。これも、それらを取り出す方法が必要となるもう一つの理由です。
OpenZeppelinは、管理者アクセスを可能にする2つのメカニズムを提供しています。
Ownable(opens in a new tab)コントラクトには、単一の所有者がいます。onlyOwner修飾子 (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 // アカウントが凍結または凍結解除されたとき2 event AccountFrozen(address indexed _addr);3 event AccountThawed(address indexed _addr); -
アカウントを凍結および凍結解除するための関数。 これらの2つの関数はほぼ同一なので、ここでは凍結関数についてのみ説明します。
1 function freezeAccount(address addr)2 public3 onlyOwnerpublic(opens in a new tab)とマークされた関数は、他のスマートコントラクトから、またはトランザクションによって直接呼び出すことができます。1 {2 require(!frozenAccounts[addr], "アカウントはすでに凍結されています");3 frozenAccounts[addr] = true;4 emit AccountFrozen(addr);5 } // freezeAccountアカウントがすでに凍結されている場合は、リバートします。 そうでなければ、アカウントを凍結し、イベントを
emitします。 -
凍結されたアカウントから資金が移動されないように
_beforeTokenTransferを変更します。 凍結されたアカウントへの送金は、引き続き可能であることに注意してください。1 require(!frozenAccounts[from], "アカウントは凍結されています");
アセットのクリーンアップ
このコントラクトが保有するERC-20トークンを解放するには、それらが属するトークンコントラクトのtransfer (opens in a new tab)またはapprove (opens in a new tab)のいずれかの関数を呼び出す必要があります。 この場合、Allowanceでガスを無駄にする意味はないので、直接送金した方が良いでしょう。
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 }これはクリーンアップ関数なので、トークンを残さないようにします。 ユーザーから手動で残高を取得する代わりに、プロセスを自動化した方が良いでしょう。
結論
これは完璧な解決策ではありません。「ユーザーの間違い」によって発生する問題に完璧な解決策はないのです。 しかし、この種のチェックを使用することで、少なくともいくつかの間違いを防ぐことができます。 アカウントを凍結する機能は危険を伴いますが、ハッカーから盗まれた資金を奪うことで、特定のハッキングによる被害を限定するために使用できます。
私の他の作品はこちらでご覧いただけます (opens in a new tab).
最終更新: 2025年9月4日