こんにちは!CryptoGamesというブロックチェーンゲーム企業でエンジニアをしているかるでねです!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
このブログ以外でも情報発信しているので、よければ他の記事も見ていってください。
https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58
今回は「フロントランニング」について紹介していきます!
「フロントランニング」は元々金融系で禁止されているものですが、ブロックチェーンでもできてしまうので注意が必要です。
フロントランニングを理解していないと簡単に脆弱性を突かれてしまうので、ぜひこの記事で学んで理解していってください!
Solidityスキルを1段階あげたい方やバグ・バウンティに参加したいバグハンターを目指している方は必須の知識となるので、この記事で学んでいってください。
バグ・バウンティが何かわからない人は以下の記事を読んでください。
フロントランニングとは?
まずはフロントランニングの言葉の定義から確認していきましょう!
フロントランニングの定義
フロントランニングは元々金融系の用語であり、大和証券の用語集には以下のように書かれています。
顧客(投資家)からの注文を受けた金融商品取引業者やその役職員が、顧客の売買を成立させる前に、顧客の注文より有利な価格で自分の売買を行うこと。金融商品取引業者はリスクなしで利益が得られることから、顧客に対する忠実義務違反として、金融商品取引法で禁止されています。
https://www.daiwa.jp/glossary/YST2753.html
一言で言うと、「お客さんの注文を受けた人が、お客さんよりも有利な価格で売買しちゃう」ことをフロントランニングと言います。
フロントランニングは、上記で書かれているように禁止されている行為です。
ブロックチェーンでのフロントランニングの定義
フロントランニングの定義を確認できたところで、次にブロックチェーンにおけるフロントランニングについて確認していきましょう!
イーサリアムDeFiのフロントランニングとは、イーサリアムやビットコインのような透明性の高いブロックチェーンの特性を利用した攻撃の一種。フロントランニングは金融用語で通常は使われ、例えば仮想通貨取引所のようなユーザーの取引が約定する前に事前に知ることができる場合、その取引より先に利益の出る取引を先行して行なうことで利益を出すことを指す。
金融商品取引法でこれらの行為は禁止されており、主に証券会社などのブローカーが行う違法行為として知られている。一方でDeFiでは他のユーザーが利益をかすめ取る行為であるため、禁止することが非常に難しい。一方でFlashbotsによるFlashbotsオークションなどによって、このようなフロントランニングとMEVによるガス代高騰を対処する方法なども取られている。
https://bokujyuumai-salon.ethereum-japan.net/wiki/12942
ほとんど先ほどと同じような定義ですね。
フロントランニングをして稼ぐ方法として、一般的には以下のような方法があります。
フロントランニング
- 大きい注文が出る前に低い価格で注文し、大きい注文の実行後の価格差から利益を得る。
- ユーザー本来の代わりに注文を偽装して実行する。
- 相手が価格を提示してきた段階で購入し、提示してきた価格で売り戻して差額を得る。
しかし、ブロックチェーンではmempool
を活用したフロントランニングが有名です。
mempool
とは、まだブロックにまとめられていないトランザクションが溜まっている場所です。
トランザクションは生成後、mempool
にどんどん貯められていきます。
通常は追加された順でブロックに含まれていきますが、ガス代を多く支払うと優先してブロックに含んでもらえます。
ディズニーなどのファストパスみたいなものです。
ブロックチェーンは透明性が売りのため、誰でも簡単にmempool
へのアクセスと分析ができてしまいます(中央集権的な取引所は例外です)。
これを利用して、ある特定の取引よりも先の自分の取引をブロックに含めることができてしまいます。
フロントランニングの実装
フロントランニングについて理解できたところで、早速フロントランニングの実装をしていきましょう!
今回は実際にフロントランニングするところまではできませんが、フロントランニングされるような脆弱性があるコードをSolidityで書いて紹介していきます。
以下のコントラクトは、正しいハッシュを推測することで10 ether
を報酬としてもらえるものです。
コード
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.13;
contract FrontRunning {
// 正解のハッシュ値。「FrontRunning」と言うワードののハッシュ値。
bytes32 public constant correctHash =
0x5709c8a5dc80ea146197585ab442e5d6ba516723a937be5975b4998afc42ae0f;
/// @notice ハッシュ値が一致していれば10 etherを送金する。
function verify(string memory word) public {
require(correctHash == keccak256(abi.encodePacked(word)), "Incorrect answer");
(bool sent, ) = msg.sender.call{value: 10 ether}("");
require(sent, "Failed to send Ether");
}
}
コード解説
このコードだと、あるユーザーが自信を持って正解のワードを引数に渡してverify関数を実行したのを覗き見て、攻撃者同じ処理を高いガス代を支払って実行することができてしまいます。
その結果、攻撃者が10 ether
を先に取得できてしまいます。
このコントラクトをアドレスAが実行した際、攻撃者がより高いガス代を支払って新しいトランザクションを開始することで、攻撃者のトランザクションの方が優先して処理される。
対処法
対処法としては、他のユーザーに何の値を渡したかがバレても問題ないようにすることが挙げられます。
以下のコードは正解を送信してもらう前に、「送信者 + 正解のハッシュ + パスワード」を1つのハッシュ値にまとめて送信し変数の保管しています。
その後改めて正解を送信すると、先ほどの情報を元に検証されて攻撃者は検証に失敗するため処理を実行できなくなります。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/utils/Strings.sol";
contract FrontRunning {
// 正解のハッシュ値。「FrontRunning」と言うワードののハッシュ値。
bytes32 public constant correctHash =
0x5709c8a5dc80ea146197585ab442e5d6ba516723a937be5975b4998afc42ae0f;
// 正解者が出たかどうかのチェック
bool public ended;
struct Commit {
bytes32 solutionHash; // ユーザーから送信してもらうハッシュ値
uint commitTime; // commitSolutionを実行した時間
}
// Mapping to store the commit details with address
mapping(address => Commit) commits;
// 正解者が出たかどうかの修飾子
modifier gameActive() {
require(!ended, "Already ended");
_;
}
/// @notice _solutionHashを渡して、値をそれぞれ格納する。
/// _solutionHash = msg.sender + solution + secret
/// 例) 0x5B38Da... + 0x5709c8a... + "password"
function commitSolution(bytes32 _solutionHash) public gameActive {
Commit storage commit = commits[msg.sender];
// 特定のユーザーが初めてcommitSolution関数を実行したかチェック
require(commit.commitTime == 0, "Already committed");
commit.solutionHash = _solutionHash;
commit.commitTime = block.timestamp;
}
/// @notice ハッシュ値が一致していれば10 etherを送金する。
function verify(string memory _word, string memory _secret) public gameActive {
Commit storage commit = commits[msg.sender];
// commitSolutionが実行されているかチェック
require(commit.commitTime != 0, "Not committed yet");
// solutionHashを求めて検証
bytes32 solutionHash = keccak256(
abi.encodePacked(Strings.toHexString(msg.sender), _word, _secret)
);
require(solutionHash == commit.solutionHash, "Hash doesn't match");
// 正解のハッシュ値の検証
require(keccak256(abi.encodePacked(_word)) != correctHash, "Incorrect answer");
ended = true;
(bool sent, ) = payable(msg.sender).call{value: 10 ether}("");
if (!sent) {
ended = false;
revert("Failed to send ether.");
}
}
}
最初の説明とコードだけでは理解するのが難しいので、改めて手順を細かく説明していきます。
手順
- ユーザーAが「0x5B38Da6a701c568545dCfcB03FcB875f56beddC4FrontRunningpassword」のハッシュ値である「4cfa68971ac40874fa207124a57d5e43d8ec216dc2302bbd68da6392e1f92972」を引数に渡して
commitSolution
関数を呼び出す。- 引数は、「送信者アドレス + 正解ハッシュ値 + 任意のパスワード」を連結したものをハッシュ化しています。
commit
構造体に先ほど引数で渡した値と、保存したタイムスタンプが保存されています。- この時、攻撃者はユーザーAのトランザクションを見て、ガス代を多く払い先にトランザクションをブロックに含めています(つまり、攻撃者の方が先に実行した扱いになっている)。
- ユーザーAは正解の言葉(今回は「FrontRunning」)と、
commitSolution
関数を実行した時に設定した任意のパスワードを引数に渡してverify
関数を実行。 - これも攻撃者は見ていて、
commitSolution
関数を実行した時と同じようにガス代を多く払い先にトランザクションを通します。 commitSolution
関数は攻撃者も実行しているため、44行目のチェックは通ります。- しかし、
msg.sender
がユーザーAと攻撃者では異なるため、ユーザーAがcommitSolution
関数実行時に渡したハッシュ値と47行目で生成したハッシュ値が異なり、50行目のチェックが通りません。 - これにより攻撃者は
verify
関数の実行に失敗し、ユーザーAは実行に成功して10 etherを取得することができます。
いかがでしょうか?
これで全体の処理の流れがわかったのではないでしょうか?
このように他の人に見られて、先に実行されても問題ないようにすることでセキュリティの高いスマートコントラクトを設計することができます。
今回の実装は以下の記事をめちゃくちゃ参考にしました。
https://solidity-by-example.org/hacks/front-running/
最後に
今回は「フロントランニング」について解説しました!
いかがだったでしょうか?
直感的には理解しづらい部分もあったかと思うので、何度か見返してしっかり読み込んでもらえれば理解できるはずです!
ポイント
- 「フロントランニングが何か理解できた!」
- 「フロントランニングの実装方法が理解できた!」
- 「フロントランニング攻撃の対処法が理解できた!」
上記のどれかに当てはまっていれば嬉しいです!
もし何か質問などがあれば以下のTwitterなどから連絡ください!
普段はSolidityやブロックチェーン、Web3についての情報発信をしています。
Twiiterでは気になった記事などを共有しているので、ぜひフォローしてくれると嬉しいです!
参考
https://qiita.com/bc_yuuuuuki/items/8d974cdfa2b0f92936e9#commit-reveal-pattern
https://phemex.com/ja/academy/フロントランニングとは何かそして暗号通貨の注文はどのようにして選択されるのか