こんにちは!ブロックチェーンの世界へようこそ。
スマートコントラクトの開発にワクワクしている新人エンジニアの皆さん、日々のコーディングお疲れ様です。
「コードは法律(Code is Law)」なんて言葉を聞いたことはありませんか? ブロックチェーン上のプログラム(スマートコントラクト)は、一度デプロイしてしまうと、誰も書き換えることができません。ルール通りに自動で動くからこそ、そこに一つでもバグや設計の抜け穴があると、想像もしなかった大惨事に繋がってしまうんです。
今回は、分散型自律組織(DAO)における「クォーラム操作と提案の乗っ取り」という、ちょっと怖いけれど非常に重要なテーマについてお話しします。
難しそうに聞こえるかもしれませんが、ご安心ください。身近な「町内会の回覧板」や「マンションの管理組合」に例えながら、一歩ずつ優しく紐解いていきましょう!
—
1. DAOの仕組みと「クォーラム(定足数)」を身近な例で理解しよう
まずは、DAO(Decentralized Autonomous Organization)が何なのかを整理しておきますね。
DAOとは、中央管理者がいない、みんなの投票で方針を決める「株主総会のようなオンラインの仕組み」だと思ってください。
ここで登場するのが「クォーラム(Quorum)」という言葉です。日本語にすると「定足数(ていそくすう)」ですね。
マンションの管理組合で考えてみましょう
あなたの住むマンションで、「来年の大規模修繕工事をどうするか?」を決める住民集会が開かれるとします。
全100世帯のうち、たった3人だけが集まって「じゃあ、全員でピンク色の外壁に塗り替えよう!」と決めたら、どう思いますか? 「ちょっと待って! 多くの人が参加していないのに、そんな勝手に決めていいの!?」って怒りますよね。
だからこそ、マンションの規約にはこう書かれているはずです。
> 「重大な決定をするには、全住民の過半数(または最低30世帯以上)が参加していなければならない」
この「会議が成立するために最低限必要な参加人数」こそが、DAOにおける「クォーラム(Quorum)」なんです。
—
2. 攻撃者が狙う盲点:なぜDAOの提案は乗っ取られてしまうのか?
ブロックチェーンの世界では、みんながいつも熱心に参加してくれるとは限りません。むしろ、初期のプロジェクトや知名度が低いDAOでは、「参加率の低さ(低投票率)」が常態化しています。
「どうせ自分の一票なんて変わらないし、ガス代(手数料)もかかるから投票はパスしよう」
みんながそうやってサボってしまうと、何が起きるでしょうか?
真夜中の乗っ取り劇
全メンバーが寝静まった静かな夜、悪意を持った攻撃者(ここでは「泥棒のAさん」と呼びましょう)がやってきました。
Aさんは、DAOのトークンを買い集めて、自分一人で全体の15%分の票を持っています。
もし、DAOのルール(クォーラム)が「全発行量の10%以上の賛成があれば可決する」と設定されていたらどうなるでしょう?
真面目なメンバーの90%が「面倒くさいから投票しない」とサボっている隙に、Aさんはたった一人で「DAOの金庫にある資金をすべて私のアドレスに送金する」という提案を出し、自分で自分に投票しました。
- 全体の参加者:Aさん(15%)のみ
- クォーラム(最低参加枠):10%クリア!
- 結果:提案可決。金庫の資金はすべて盗まれました。
これが、「クォーラム操作と提案の乗っ取り」のメカニズムです。誰も見ていない隙に、ルールギリギリの人数で勝手に組織の財布を持ち去られてしまうわけですね。怖くないですか?
—
3. コードで見てみよう:脆弱なスマートコントラクトの例
では、実際にSolidityというプログラミング言語で書かれた、危なっかしいスマートコントラクトの例を見てみましょう。
新人エンジニアの皆さんは、「どこがダメなのか」を探してみてくださいね。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// 【危険な実装例】固定されたクォーラムと単純すぎる集計ロジック
contract VulnerableDAO {
struct Proposal {
string description; // 提案内容
uint256 yesVotes; // 賛成票の総数
bool executed; // 実行済みかどうか
}
Proposal[] public proposals;
mapping(uint256 => mapping(address => bool)) public hasVoted;
// 全トークンの総供給量(仮に1000トークンとする)
uint256 public constant TOTAL_SUPPLY = 1000;
// クォーラム:総供給量の10%(100票)が集まれば成立とする
uint256 public constant QUORUM_THRESHOLD = 100;
// 提案を作成する関数
function createProposal(string memory _description) external {
proposals.push(Proposal({
description: _description,
yesVotes: 0,
executed: false
}));
}
// 投票する関数(簡易版:1アドレス=1票とする)
function vote(uint256 _proposalId) external {
require(!hasVoted[_proposalId][msg.sender], "すでに投票済みです");
hasVoted[_proposalId][msg.sender] = true;
proposals[_proposalId].yesVotes += 1; // 1票を追加
}
// 提案を実行する関数
function executeProposal(uint256 _proposalId) external {
Proposal storage prop = proposals[_proposalId];
require(!prop.executed, "すでに実行されています");
// 【致命的な脆弱性】
// 参加者の総数ではなく、「賛成票がクォーラムを超えているか」だけで判定している!
// さらに、反対票の概念がないため、誰も反対できずに通ってしまう。
require(prop.yesVotes >= QUORUM_THRESHOLD, "クォーラムに達していません");
prop.executed = true;
// ここに金庫から資金を送金する処理が入る想定
}
}
何が問題だったのでしょうか?
1. 静的なクォーラム:QUORUM_THRESHOLD が常に「100票」と固定されています。もしDAOのメンバーが減ったり、トークンが一部のクジラ(大口保有者)に買い占められたりすると、簡単にこの数字をクリアされてしまいます。
2. 反対票の欠如:賛成しかカウントしないため、攻撃者がこっそり提案を通すのを防ぐブレーキがありません。
3. タイムロックの欠如:「提案が可決された瞬間にすぐ資金が移動する」ため、正当なメンバーが不正に気づいて阻止する時間がありません。
—
4. 一歩ずつ対策を学んでいきましょう!堅牢なスマートコントラクトの設計
この脆弱性を防ぐため、実際の現場やセキュリティ監査ではどのような対策をとるべきでしょうか?
実務でそのまま使える、安全な設計のポイントをコード例とともに見ていきましょう。
対策のポイント
1. 動的クォーラム(Dynamic Quorum)の導入:時間経過や参加状況に応じて、必要なクォーラムの計算方法を工夫する。
2. タイムロック(Timelock)の設置:可決されてから実際に実行されるまでに「数日間の猶予期間」を設け、その間に不正な提案であればメンバーが脱退したり資金を避難させたりできるようにする。
3. 投票期間の十分な確保:週末を挟むなど、多くの人が確認できる時間を確保する。
以下は、タイムロックと厳格なチェックを取り入れた改善版のコードです。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// 【安全な実装例】タイムロックと厳格な検証を取り入れたDAO
contract SecureDAO {
struct Proposal {
string description;
uint256 yesVotes;
uint256 noVotes;
uint256 voteEndTime; // 投票終了時間
uint256 executionTime; // 実行可能になる時間(タイムロック)
bool executed;
}
Proposal[] public proposals;
mapping(uint256 => mapping(address => bool)) public hasVoted;
// ガバナンストークンのコントラクト(実際の保有量に応じて投票権が決まる想定)
// interface等を使って各ユーザーの残高を取得する
// 提案作成(投票期間を3日間に設定)
function createProposal(string memory _description) external {
proposals.push(Proposal({
description: _description,
yesVotes: 0,
noVotes: 0,
voteEndTime: block.timestamp + 3 days, // 3日間の投票期間
executionTime: 0,
executed: false
}));
}
// 投票関数(賛成・反対を選べるようにする)
function vote(uint256 _proposalId, bool _support, uint256 _weight) external {
Proposal storage prop = proposals[_proposalId];
require(block.timestamp < prop.voteEndTime, "投票期間は終了しています");
require(!hasVoted[_proposalId][msg.sender], "すでに投票済みです");
hasVoted[_proposalId][msg.sender] = true;
if (_support) {
prop.yesVotes += _weight; // 持ち分に応じた重み付け
} else {
prop.noVotes += _weight;
}
}
// 提案の承認(タイムロックの予約)
function queueProposal(uint256 _proposalId) external {
Proposal storage prop = proposals[_proposalId];
require(block.timestamp >= prop.voteEndTime, "まだ投票期間中です");
require(!prop.executed, "すでに実行済みです");
// 簡単のため、総投票数が一定以上かつ賛成が反対を上回っていることなどをチェック
require(prop.yesVotes > prop.noVotes, "賛成多数ではありません");
// 【重要】可決されてもすぐには実行させず、2日間の「タイムロック期間」を設ける
// この2日間の間に、不審な提案であればコミュニティが察知して対応できる!
prop.executionTime = block.timestamp + 2 days;
}
// 最終実行関数
function executeProposal(uint256 _proposalId) external {
Proposal storage prop = proposals[_proposalId];
require(!prop.executed, "すでに実行されています");
require(prop.executionTime > 0, "まだキューに入っていません");
require(block.timestamp >= prop.executionTime, "タイムロック期間が経過していません");
prop.executed = true;
// 実際の安全な処理(資金移動など)をここに記述
}
}
—
5. まとめ:現場のセキュリティ担当者・開発者としての心構え
いかがでしたでしょうか?
DAOのクォーラム操作や提案の乗っ取りは、技術的なバグ(例えばオーバーフローなど)というよりも、「経済的なインセンティブや人間のサボりグセを考慮しきれていない設計ミス」から生まれます。
私たちエンジニアが心掛けるべきことは、コードが正しく動くことだけを確認するのではなく、「もし全員がサボったらどうなるか?」「もし悪意ある大富豪が参加してきたらどうなるか?」という意地悪な視点(攻撃者の目線)を持ってシステムを設計することです。
最初は難しく感じるかもしれませんが、こうした防犯の意識を一つずつ積み上げていけば、必ず信頼されるセキュアなWeb3サービスを作れるようになります。
一歩ずつ、確実にスキルアップしていきましょう!次の記事でも、実践的なセキュリティの知見を分かりやすくお届けしますね。
コメント