スマートコントラクトとは?
Ethereumには2種類のアカウントがあります。すなわち外部所有アカウント(EOA)とスマートコントラクトアカウント(SCA)です。
EOAは私たちが資金を保管したりアプリケーションとやり取りしたりするために一般的に使用している電子金融口座に非常によく似ています。例えば、ユーザーはPayPalを通じてフィアットを入金しさまざまなウェブサイト、店舗、アプリとやり取りして支払いを行います。
DeFiマイナーは通常、EOAに暗号資産を保管しDeFi dAppsとやり取り、利益を得るためにdAppsに資金を入金します。しかしEOAには電子金融口座にはない特徴があります。
ユーザーは秘密鍵の所有権を通じてEOAに対するコントロールを検証してもらう必要があります。
SCAもまた基本的に実行可能なバイトコードのセグメント(スマートコントラクトとも呼ばれる)に関連付けられたアカウントの一種です。スマートコントラクトは様々なビジネスロジックを記述しdAppsのバックエンドとして機能します。
しかし、従来のチューリング完全開発言語と比較してより多くの制約があるにもかかわらず、準チューリング完全スマートコントラクトは依然として数多くの攻撃に対して脆弱であり、ブロックチェーン業界に数え切れないほどの打撃を与えています。
一般的なスマートコントラクトの攻撃
- Reentrancy Attack
- tx.originの誤用
- tx.originの代わりにmsg.senderを使用
- tx.origin == msg.sender を検証
- 乱数発生器(RNG)攻撃
- リプレイ攻撃
- checkSig(): ECDSA検証関数で、検証結果が当初設定された署名者であることを保証
- getMsgHash(): ハッシュを生成する関数
- transfer(): 流動性プールから資金を引き出すための送金関数。署名に制限がないため、同じ署名を再利用することができ、ハッカーは継続的に資金を盗むことが可能
- DoS攻撃
- deposit():入金関数で入金者のアドレスと入金額を記録
- refund():プロジェクトチームが投資家に資金を返す返金関数
- 外部コントラクトを呼び出す際に重要な機能がスタックしないようにする
- デカップリング
- permit Attack
- approve(): アカウント A がアカウント B に一定額の資金を承認する標準的な承認関数。
- permit(): アカウントBがアカウントAから承認額を得るために署名検証を提出し完了する署名承認関数。パラメータは、認可を許可するowner、認可されるspender、認可amount、署名deadline、所有者の署名データv、r、s。
- オンチェーン取引における全ての署名に注意を払う
- 通常のやり取りを行うウォレットと資産を保管するウォレットを分離
- ハニーポット攻撃
- フロントランニング攻撃
最も一般的で悪名高い攻撃はこの攻撃で、Ethereum Classicの誕生につながったEthereumフォークの原因となりました。2016年、ハッカーはThe DAOコントラクトに対してこの攻撃を実行し、当時1億5,000万ドル以上の価値があった3,600,000ETHを盗みました。
この攻撃はEthereumの初期段階で発生し、エコシステムを荒廃させ投資家の信頼を打ち砕き、最終的にフォークに至りました。
具体的なロジック
Reentrancy Attackの原理をより理解するための例を示します。ある日、銀行Bは銀行Aに対しすべての資金を銀行Bに送金するよう要求しました。
ステップ1: 銀行Bが資金の引き出しを依頼
ステップ2: A銀行がB銀行に資金を送金
ステップ3: 銀行Aは銀行Bに送金が成功したことを確認
ステップ4: 銀行Aは銀行Bの口座残高を更新
しかしB銀行がステップ2の後に抜け穴を作り、ステップ3で確認することなくA銀行にすべての資金を要求し続けた場合、B銀行におけるA銀行の口座残高は変更されません。この再帰的な呼び出しによって、銀行Aの資産はすべて空になります。
関連するスマートコントラクト
銀行Aのコントラクトには2つの関数があります。
deposit(): 銀行 A に入金し、ユーザーの残高を更新する入金関数
withdraw():銀行Aに入金し、残高を更新する入金関数
銀行Bの攻撃コントラクトは、主にreceive()callback関数をトリガーとするループを含み、そのコールバック関数が銀行コントラクトのwithdraw()関数を呼び出し、1回の入金、1回の出金、receive()コールバック関数の呼び出しのシーケンスを通じて銀行Aの資産を流出させ、最終的に銀行AのBの残高を更新します。
・receive(): ETHを受け取ったときにトリガーされるcallback関数で、出金を行うためにBankコントラクトのwithdraw()関数を再帰的に呼び出す
attack(): 最初にBankコントラクトのdeposit()関数を呼び出して残高をリフレッシュし、次にwithdraw()関数を呼び出して最初の引き出しを開始し、receive()callback関数をトリガーにしてwithdraw()を再帰的に呼び出してBankコントラクトの資産を流出させる
解決方法
Reentrancy lockの実装
Reentrancy lockはReentrancyを防ぐために使用される修飾語で、呼び出しが再び呼び出される前にその呼び出しの実行が完了しなければならないことを保証します。例えば、銀行Bによる攻撃は、銀行コントラクトのwithdraw()関数を複数回呼び出す必要があるため、Reentrancy lockの実装により失敗します。
使用方法
スマートコントラクトにおけるtx.originの主な機能は、取引を開始した元のアカウントを取得することです。ここではスマートコントラクトでよく使われる2つの変数、msg.senderとtx.originについて説明します。
msg.senderはスマートコントラクトを直接呼び出してアカウントを取得しますが、ブロックチェーンの世界では(DeFi Legoのように)異なるスマートコントラクトが入れ子になって相互に呼び出されるため、取引を開始した元のアカウントを取得するにはtx.originが必要になります。dAppの開発者がコード内でtx.originのセキュリティのみを検証し、tx.originを迂回して攻撃を仕掛けるために中間的なコントラクトを展開する攻撃者のセキュリティ検証を怠ると脆弱性が生じます。
具体的なロジック
一般的な攻撃シナリオを深く理解するための例を示します。Billはスマートウォレットを持っておりBillが送金のイニシエータであるかどうかを検証します。
ある時、BillはフィッシングサイトでNFTをミントしました。そのため、ウェブサイトはビルのIDを入手し彼のIDを使用して彼のスマートウォレットから送金を開始し、その結果、資産が失われました。
通常であれば、ユーザーはこのような罠に引っかかる可能性は低いのですが、ウォレットを使用してdAppsとやり取りをする場合、やり取りを促すプロンプトを確認するのを忘れてしまうことがよくあります。例えば、両方ともMint()関数を含む場合、不注意なユーザーは簡単にフィッシングの罠にはまる可能性があります。
フィッシング・ウェブサイト内のビジネスロジックはトラップだらけなので、定期的な対話の際に対話プロンプトにエラーがないかチェックすることが重要です。
スマートウォレットコントラクト
スマートウォレットのコントラクトには1つの関数があります。
・transfer(): ウォレットの所有者(この場合はビル)のみが開始できる引き出し機能
フィッシング攻撃コントラクト
フィッシング攻撃コントラクトでは、Mint()はユーザーにハッカーのアドレスに送金するように仕向けます。これには一つの関数が含まれています。
・Mint(): Mint():呼び出されると、フィッシング関数は内部的にWalletコントラクトのtransfer()を実行します。元の送金者はユーザー(この例ではBill)自身なのでrequire(tx.origin == owner, “Not owner”); という検証は問題になりません。しかし、送金先のアドレスはすでにハッカーのアドレスに改ざんされており、結果的に資金が盗まれてしまいます。
解決策
コントラクトの呼び出しがいくつあっても(コントラクト A → コントラクト B →…→ ターゲット・コントラクト)、悪意のある中間コントラクトによる攻撃を避けるためmsg.sender、つまり直接の呼び出し元だけを検証します。
この方法は悪意のあるコントラクトを排除することができますが、他の外部コント ラクト呼び出しを効果的に隔離するため、開発者は自分のビジネスの現実を考慮する必要があります。
これは2018年と2019年ごろのギャンブルやベッティングのdAppトレンドにさかのぼります。通常、開発者はスマートコントラクトで特定のシードを使用して乱数を生成し、抽選時に勝者を選択します。
一般的なシードにはblock.number、block.timestamp、blockhash、keccak256などがあります。しかしマイナーはこれらのシードを完全にコントロールできるため、場合によっては悪意のあるマイナーが変数を操作して利益を得ることもあります。
一般的なDice Contracts
Dice Contractsには1つの関数があります。
・Bet(): ユーザーがベット番号を入力し、ETHを支払うベット機能。複数のシードで乱数が生成され、ベット番号が乱数と一致した場合ユーザーは賞金プール全体を獲得する
マイナーのAttack Contract
マイナーは当選乱数を事前に計算し、同じブロック内で実行する限り、勝利することができます。これには1つの関数が含まれます。
・attack(): ベッティングアタック関数で、マイナーが勝利乱数を事前に計算します。同じブロック内で実行されるため、同じブロック内の blockhash(block.number – 1)と block.timestampは同じになります。
その後、マイナーはDice ContractのBet()を呼び出して攻撃を完了します。
解決方法
オラクルプロジェクトが提供するオフチェーン乱数を使用
Chainlink などのオラクルプロジェクトが提供するサービスを通じて、オンチェーン乱数をオンチェーンコントラクトに注入しランダム性と安全性を確保します。ただし、オラクルプロジェクトには中央集権化のリスクもあるため、より成熟したオラクル・サービスが必要です。
リプレイ攻撃は、以前に使用された署名を使用して取引を再開し資金を盗むものです。近年で最も有名なリプレイ攻撃の1つは、OptimismのマーケットメーカーであるWintermuteから2,000万ドルのOPトークンが盗まれた事件で、これはクロスチェーンのリプレイ攻撃でした。
Wintermuteのマルチシグネチャウォレットアカウントは一時的にEthereumのメインネット上にのみ展開されていたため、ハッカーはWintermuteがEthereum上に展開したマルチシグネチャアドレスのトランザクションの署名を使用して、Optimismチェーン上で同じトランザクションを再実行し、それによってOptimism上のマルチシグネチャウォレットアカウントのコントロールを得ました。マルチシグネチャーのウォレットアカウントは基本的にスマートコントラクトのアカウントであり、SCAとEOAの大きな違いを示しています。
EOAの場合、通常のユーザーは1つの秘密鍵でEthereumとEVM互換チェーン上のすべてのアドレスを制御することができます(アドレス文字列は全く同じです)。
具体的なロジック
ここで、典型的なリプレイ攻撃(同一チェーンリプレイ攻撃)の例を示します。Billはスマートウォレットを持っており、各取引を実行する前に電子署名を入力する必要があります。
ハッカーLucyはBillの電子署名を盗んだので、Billのスマートウォレットから無制限に取引を開始することができます。
例
脆弱性を持つコントラクトは3つの関数で構成されます。
解決策
リプレイ攻撃を防ぐために署名の組み合わせにnonceを含めます。パラメータの原理は以下の通りです。
・nonce: ブロックチェーン・ネットワークにおける EOA のトランザクション数の変数。これは順序性と一意性を持っています。
ブロックチェーン・ネットワークはトランザクションの nonce がアカウントの現在の nonce と一致しているかどうかをチェックします。したがってハッカーが使用済みの署名を使用した場合、その署名の組み合わせの nonce 値が EOA の現在の nonce 値より小さいためハッカーは失敗することになります。
Denial of Service(DoS)攻撃は従来のWeb2の世界では目新しいものではありません。これは大量のジャンクや破壊的な情報を送信し、可用性を妨げたり完全に破壊したりするような、サーバーへのあらゆる干渉を指します。
同様にスマートコントラクトもこのような攻撃に悩まされており、基本的にスマートコントラクトを機能不全に陥らせることを目的としています。
具体的なロジック
例を見てみましょう。プロジェクトAはプロトコルトークンの公募を行っており、全ユーザーが流動性プール(スマートコントラクト)に資金を拠出して先着順でクォータを購入し、余剰資金は参加者に還元されます。ハッカー・アリスは攻撃コントラクトを悪用して公募に参加します。流動性プールがアリスの攻撃コントラクトに資金を返却しようとすると、DoS攻撃が発動し返却行為が実現しないようにします。
その結果、大量の資金がスマートコントラクトにロックされます。
例
公募コントラクトには2つの関数があります。
DoS攻撃コントラクト
DoS攻撃コントラクトには1つの関数があります。
・attack(): 攻撃関数であるにもかかわらず、ほかには何の問題もありません。主な問題は、Hacker コントラクトに組み込まれた receive() 支払いコールバック関数にあり、これには例外の判定が含まれています。
外部コントラクトがハッカーコントラクトに資金を送金すると、revert()によって例外が発生し操作が完了しなくなります。
解決方法
PublicSaleコントラクトの上記のrefund()関数からrequire(success, “Refund Fail!”);を削除し、1つのアドレスへの返金が失敗しても返金操作が継続できるようにします。
上記のPublicSaleコントラクトのrefund()関数において、払い戻しを分配するのではなく、利用者が自分で払い戻しを請求できるようにすることで、外部コントラクトとの不要なやりとりを最小限に抑えます。
Permit Attackとは、アカウントAが指定された相手に対して事前に署名を提供し、その署名を入手したアカウントBが許可されたトークン送金を実行することで、一定量のトークンを盗み出す攻撃です。ここでは主にスマートコントラクトにおけるトークン認可のための2つの一般的な関数、approve()とpermit()について説明します。
一般的なERC20コントラクトでは、アカウントAがapprove()を呼び出すことで、アカウントBに対して一定量のトークンを認可し、アカウントBが前者からトークンを移転できるようにします。さらに、permit()はEIP-2612でERC20コントラクトに導入され、Uniswapは2022年11月に新しいトークン認可標準であるPermit2をリリースしました。
具体的なロジック
以下に例を示します。ある日、Billがブロックチェーンニュースのウェブサイトを見ていると、突然Metamask署名のポップアップが表示されました。
多くのブロックチェーンウェブサイトやアプリケーションはユーザーのログインを確認するために署名を使用するため、Billはあまり深く考えずそのまま署名を完了しました。5分後、彼のMetamask資産は流出しました。
Billはブロックチェーンエクスプローラーで、未知のアドレスがpermit()トランザクションを開始し、続いてtransferFrom()トランザクションが彼のウォレットを空にしたことを発見しました。
例
2つの関数は以下の通りです。
解決策
いくつかのウォレットはapprove()の署名情報をデコードして表示する対策をとっていますが、permit()署名のフィッシングに対する警告をほとんど提供しておらず、攻撃のリスクを高めています。したがって、permit()関数を狙ったものであるかどうかを確認するために、すべての未知のシグネチャを厳密に検査することが強く推奨されます。
これは暗号ユーザー、特にairdropハンターにとって非常に重要です。彼らは毎日数え切れないほどのdAppやウェブサイトとやり取りしておりトラップに引っかかりやすいからです。
定期的なやり取り用のウォレットには少額の資金のみを保管することで、損失を管理可能な範囲内に抑えることができます。
ブロックチェーン業界ではハニーポット攻撃はプロジェクトチームによって展開される悪意のあるトークンコントラクトの一種を指します。この契約はプロジェクトチームにのみ売却許可を与え、一般ユーザーは売却する代わりに購入することしかできないため損失を被ることになります。
具体的なロジック
例を挙げましょう。プロジェクトAはTelegram上のアナウンスで、トークンがメインネットにデプロイされ、取引が可能になったことをユーザーに知らせます。
トークンは買うことしかできず売ることはできないため、最初のうちは価格が高騰し続け損失を恐れたユーザーが買い続けます。しばらくして、ユーザーが売却できないことに気づくと、プロジェクトチームはその機会を捉えてトークンを投棄し、価格が急落します。
例
コア関数
・_beforeTokenTransfer(): トークン転送時に呼び出される内部関数で、オーナーが呼び出した場合のみ成功する。他のアカウントからの呼び出しは失敗する。
解決方法
セキュリティスキャンツールの使用
a. Ethereumトークン用のToken Sniffer
b. 他のチェーン上のトークンのAve Check
c. Dextools のような検出ツールを組み込んだマーケットウェブサイト
スコアの低いトークンの取引は避けてください。
フロントランニングはもともと伝統的な金融市場で発生し、情報の非対称性によって金融仲介者が特定の業界情報に基づいて迅速な行動を取ることで利益を得ることができました。ブロックチェーン業界では、フロントランニングは主にオンチェーン・フロントランニングに起因するもので、これは利益を得るために自分のトランザクションをチェーンに優先的に詰め込むようマイナーを操作することを含みます。
ブロックチェーン分野では、マイナーはブロックにパックするトランザクションを操作することで利益を得ることができます。このような利益はMiner Extractable value(MEV)で測定することができます。
ユーザーのトランザクションがEthereumのメインネットに追加される前に、トランザクションの大部分はmempoolに集約されます。マイナーはこのmempoolでガス価格の高いトランザクションを探し、それらを優先的にパックして利益を最大化します。
一般に、ガス価格の高い取引はマイナーによってより簡単に梱包されます。一方、MEVボットの中にも収益性の高いトランザクションを探すためにmempoolを探し回るものがあります。
具体的なロジック
以下はその例です。Billが価格変動の大きい新しいホットトークンを発見します。
Uniswapでのトークン取引を確実に成功させるため、Billは非常に広いスリッページ幅を設定します。不運なことに、アリスのMEVボットがmempoolでこのトランザクションを検出し、即座にガス代を増やし、同じブロック内でBillのトランザクションの前に買いトランザクションを開始し、Billのトランザクションの後に売りトランザクションを挿入します。
ブロックの確認後、これによってBillは大きなスリッページ損失を被る一方、Aliceは安く買って高く売る裁定取引で利益を得ます。
例
関数は以下です。
・solve(): 誰でも答えを提出することができ、提出された答えが目標とする答えと一致すれば、提出者は10ETHを受け取ることができる推測関数。
プロセス
- ビルが正解を見つけます。
- アリスがmempoolを監視し、誰かが正解を投稿するのを待ちます。
- ビルはsolve()を呼び出して答えを提出し、ガス価格を100Gweiに設定します。
- アリスはビルが送信したトランザクションを見て答えを発見します。彼女はビルの200Gweiより高いガス価格を設定しsolve()を呼び出します。
- アリスのトランザクションはビルより先にマイナーによってパックされます。
- アリスは10ETHの報酬を獲得します。
解決方法
主な関数は以下の3つです。
- commitSolution(): 結果を提出する関数で、ユーザーの提出した解答solutionHash、提出時間commitTime、および明らかになった状態をCommit structureに配置します。
- getMySolution(): 結果を取得する関数で、ユーザーが提出した解答と関連情報 (提出した解答 solutionHash、提出時刻 commitTime、および明らかにされた状態など) を表示できます。
- revealSolution(): パズルを推測して報酬を請求する関数で、ユーザーは答えと設定したパスワードを提供した後に報酬を請求できます。
プロセス
- ビルが正解を見つけます。
- ビルがcommitSolution() を呼び出して正解を送信します。
- 次のブロックでは彼は revealSolution() を呼び出し、答えと報酬を要求するために設定したパスワードを提供します。
commitSolution()では、ビルは暗号化された文字列を提出し提出された平文データは自分だけに保持されます。このステップでは、提出ブロック時間 commitTime も記録されます。
次に、revealSolution() で同じブロック内でのフロントランを防ぐためにブロックタイムをチェックします。revealSolution() を呼び出すには平文の答えを提出する必要があるため、このステップの目的は、他の人が commitSolution() をバイパスして revealSolution() を直接呼び出すのを防ぐことです。
検証に成功した後、答えが正しいことが確認されると報酬が配布されます。
結論
スマートコントラクトはブロックチェーン技術において重要な役割を果たし、多くの利点を提供します。第一にスマートコントラクトは分散型の自動実行を可能にし、第三者なしで取引の安全性と信頼性を確保します。
第二にスマートコントラクトは仲介ステップとコストを削減し、取引効率を高めます。
これほど多くの利点があるにもかかわらず、スマートコントラクトはユーザーに金銭的損失をもたらす攻撃のリスクにも直面しています。そのため、オンチェーンユーザーにはいくつかの習慣が不可欠です。
まずユーザーは常に交流するdAppsを慎重に選択し、コントラクトコードと関連ルールを徹底的に確認する必要があります。さらに、ハッカー攻撃のリスクを軽減するために安全なウォレットとコントラクト対話ツールを定期的に更新し、使用する必要があります。
さらに、コントラクト攻撃による潜在的な損失を最小限に抑えるために、資金を複数のアドレスに保管することをお勧めします。
業界プレイヤーにとって、スマートコントラクトのセキュリティと安定性を確保することは、同様に重要です。まず優先すべきは、潜在的な脆弱性とセキュリティリスクを特定し是正するために、スマートコントラクトの監査を強化することです。
次に業界関係者は、コントラクト攻撃に関するブロックチェーンの最新動向を常に把握し、それに応じたセキュリティ対策を講じる必要があります。最後にスマートコントラクトの正しい使用に関して、ユーザー教育とセキュリティ意識を高めることです。
結論として、ユーザーと業界関係者の双方が協調して努力すれば、スマートコントラクトがもたらすセキュリティリスクを大幅に軽減することができます。ユーザーは常に慎重にコントラクトを選択し個人資産を保護する必要があり、業界関係者はコントラクト監査を強化し、技術の進歩に後れを取らずユーザー教育とセキュリティ意識を高める必要があります。
私たちは共に、スマートコントラクトの安全で信頼できる開発を推進していきます。
参考文献
実例によるソリディティ
https://solidity-by-example.org/
SlowMistのブロックチェーンノウハウ
Chainlink – DeFiセキュリティのベストプラクティス トップ10
https://blog.chain.link/defi-security-best-practices/#post-title
WTF – Solidity 104 コントラクトセキュリティ
https://www.wtf.academy/solidity-104/
DeFiスマートコントラクトの脆弱性を4つのカテゴリーに分けて38のシナリオで解説
https://www.weiyangx.com/381670.html
OpenZeppelin
https://github.com/OpenZeppelin/
サービスのご利用・お問い合わせに関しては、直接サービス提供会社へご連絡ください。CoinPostは本稿で言及されたあらゆる内容について、またそれを参考・利用したことにより生じたいかなる損害や損失において、直接的・間接的な責任を負わないものとします。ユーザーの皆さまが本稿に関連した行動をとる際には、必ず事前にご自身で調査を行い、ご自身の責任において行動されるようお願いいたします。