Appearance
L2-2: Moloch Rage Quit(DAO退出メカニズム)⭐
1. 問題
「Rage Quit」(レイジクイット)をサポートするDAOコントラクトを作成します。メンバーは提案投票を通じてDAOを管理できます。任意のメンバーは一方的に自身のシェアを焼却し、比例配分でトレジャリーからETHを引き出すことができます。中核的な課題は:新規メンバーは提案実行を通じてのみ参加可能(addMember の onlySelf アクセス制御パターン)であることです。
2. 理由
従来の企業で株主が退出するには、買い手を見つけるか複雑な法的手続きを経る必要があります。DAOにおいて、多数派があなたの同意しない提案(例:トレジャリー資金を詐欺だと思うプロジェクトに投資する)を可決した場合、あなたに何ができるでしょうか?
Moloch DAO の解決策は Rage Quit です:
- 任意のメンバーが一方的に退出可能(誰の承認も不要)
- 自身のシェアを焼却 →
shares / totalShares * treasuryBalanceで比例的にETHを引き出す - これは経済的な「足による投票」——少数派が多数派に略奪されるのを防ぐ
これはDAOガバナンスにおける「少数派保護」の基礎メカニズムであり、DAOの経済的安全性を理解する上で重要な概念です。
onlySelf パターンはこのレベルの最も巧妙な設計です:addMember 関数は public ですが(提案実行を通じて呼び出される必要があるため)、外部から直接呼び出すことはできません。解決策は onlySelf モディファイアです:
solidity
modifier onlySelf() {
require(msg.sender == address(this), "Not proposal execution");
_;
}
提案が可決されて target.call(data) を実行 → DAOコントラクトが自身の addMember を呼び出す → msg.sender == address(this) → 通過。
3. 解決策
アーキテクチャ設計
MolochDAO
├── メンバー制 (1メンバー1票、シェア数ではなくメンバー数に基づく)
├── シェア (ETH拠出と引き換えに取得、rageQuit 時に焼却)
│
├── propose(target, data, deadline)
│ └── 提案者が自動的に1票を投じる
├── vote(proposalId)
│ └── 各メンバーは1票のみ投票可能
├── executeProposal(proposalId)
│ └── votes * 2 > memberCount → target.call(data)
│ └── 例:DAOが自身の addMember(newMember, shares) を呼び出す
│
├── addMember(newMember, shares) [onlySelf]
│ └── 提案実行を通じてのみ呼び出し可能
│
└── rageQuit()
└── (shares * ethBalance) / totalShares → ETH返還
└── シェア焼却 → memberList から削除
コアコード
メンバー追加 — onlySelf パターン:
solidity
function addMember(address newMember, uint256 shares) external onlySelf {
_addMember(newMember, shares);
}
function _addMember(address newMember, uint256 shares) internal {
require(!isMember[newMember], "Already member");
isMember[newMember] = true;
memberShares[newMember] = shares;
totalShares += shares;
memberList.push(newMember);
}
提案 — 投票 — 実行フロー:
solidity
function propose(address target, bytes calldata data, uint256 deadline)
external onlyMember returns (uint256)
{
// ...
p.votes = 1; // 提案者が自動投票
p.hasVoted[msg.sender] = true; // vote() での再投票を防止
}
solidity
function executeProposal(uint256 proposalId) external {
// 過半数のメンバー投票が必要(厳格な多数決)
if (p.votes * 2 <= memberList.length) revert NotEnoughVotes();
(bool success, ) = p.contractToCall.call(p.data);
}
Rage Quit — 少数派保護:
solidity
function rageQuit() external onlyMember {
uint256 shares = memberShares[msg.sender];
uint256 returnAmount = (shares * address(this).balance) / totalShares;
// 1. メンバー資格を削除
isMember[msg.sender] = false;
totalShares -= shares;
_removeFromMemberList(msg.sender);
// 2. ETHを返還
(bool success, ) = msg.sender.call{value: returnAmount}("");
}
4. 遭遇した落とし穴
4.1 addMember のアクセス制御 — onlySelf の巧妙さ
これはこのレベルで最も中心的な設計問題です。addMember は以下の3つの制約を満たす必要があります:
- 外部から任意に呼び出せてはいけない(そうでなければ誰でもDAOに参加してシェアを希薄化できる)
- 提案実行を通じて呼び出せる必要がある(新規メンバー追加には民主的投票が必要なため)
- Solidity には「提案実行を通じてのみ呼び出し可能」なネイティブモディファイアが存在しない
解決策は onlySelf です:
solidity
modifier onlySelf() {
require(msg.sender == address(this), "Not proposal execution");
_;
}
提案実行フロー executeProposal → target.call(data) がDAO自身の addMember を呼び出す場合:
target.call(data)はDAOコントラクトのexecuteProposalコンテキスト内で実行されるaddMember内のmsg.sender= DAOコントラクトアドレス =address(this)onlySelf通過
外部から直接呼び出された場合:
msg.sender= 呼び出し元のEOAアドレス ≠address(this)onlySelfが revert
4.2 シェア数ではなくメンバー数に基づく多数決
solidity
if (p.votes * 2 <= memberList.length) revert NotEnoughVotes();
通過条件:メンバーの過半数が賛成票を投じること(人数ベースで計算)、シェア数ベースではありません。これは以下を意味します:
- 1%のシェアを持つメンバー = 1票
- 99%のシェアを持つメンバー = 1票
- 大口保有者が一方的にすべての提案を通すのを防止(1人1票の民主制)
注意: この設計はトークン重み付け投票とも対照的です——ほとんどのDeFiガバナンスはトークン重み付け(他のレベルの OZ Governor のように)を使用しますが、Moloch フレームワークはメンバー制を選択しました。
4.3 提案者の自動投票 + 二重投票防止
propose() 内で提案者が自動的に1票を投じ、hasVoted[proposer] が即座に true にマークされます:
solidity
p.votes = 1;
p.hasVoted[msg.sender] = true;
この行がないと、提案者は vote() で再度投票可能になり → p.votes += 1 → 1人で2票 → 1メンバー1票の設計に違反します。
4.4 rageQuit が提案投票に与える影響
メンバーが投票後に rageQuit した場合:
- メンバー資格は削除される(
isMember = false) - しかし既に投じられた票は提案の
p.votesに残る - つまり退出メンバーの過去の投票はロールバックされない
これは微妙な影響を及ぼす可能性があります——退出メンバーが決定的な票を投じていた場合、退出後に memberCount が減少 → 通過条件 votes * 2 > memberCount がより緩和されます。
例: 元々5人のメンバー、3票賛成(ちょうど過半数:32=6 > 5)。反対者が1人 rageQuit すると、memberCount は4になるが p.votes は3のまま → 32=6 > 4 → 提案は依然として可決(実際にはより可決しやすくなる)。
5. 落とし穴の理由
5.1 onlySelf vs onlyOwner vs onlyMember の比較
| モディファイア | 呼び出し可能な主体 | 適用シーン |
|---|---|---|
onlyOwner | 単一の管理者 | 中央集権的管理(アップグレード可能コントラクト、緊急停止) |
onlyMember | 任意のDAOメンバー | 悪用の可能性:メンバーが無制限に新規メンバーを追加可能 |
onlySelf | コントラクト自身のみ | 民主的投票の後、コントラクトが自己呼び出し |
onlySelf は「外部アクション」を「DAO自律アクション」に変える鍵です——メンバー追加は提案プロセスを経る必要がありますが、提案実行時にはDAOコントラクトが「自分自身を呼び出す」のです。
5.2 メンバー制 vs トークン重み付け制
| 次元 | メンバー制 (Moloch) | トークン重み付け制 (OZ Governor) |
|---|---|---|
| 投票単位 | 1メンバー = 1票 | 1トークン = 1重み |
| 参加閾値 | 投票による加入が必要 | トークン保有のみ |
| 大口保有者の影響力 | 制限される | 投票を支配可能 |
| 退出保護 | rageQuit(経済的退出) | トークン売却で退出 |
5.3 rageQuit の数理公式
returnAmount = (shares * ethBalance) / totalShares
この公式の暗黙の前提:
totalSharesは退出するメンバーのシェアを含む(減算前に計算)- これにより退出メンバーは「事後的な totalShares」で計算した場合よりもわずかに少ないETHを受け取る(自身のシェアが先に分母に含まれるため)
例: totalShares=100, トレジャリー=100 ETH, メンバーが10シェア保有
returnAmount = (10 * 100) / 100 = 10 ETH- 退出後: totalShares=90, トレジャリー=90 ETH
- 残存メンバー: 90シェア → 90 ETH → 比率は変わらない
6. 落とし穴の解決方法
solidity
// 正解:onlySelf が民主的プロセスを通じたメンバー追加を保証
function addMember(address newMember, uint256 shares) external onlySelf {
_addMember(newMember, shares);
}
// 正解:提案時に投票済みとマークし、二重投票を防止
function propose(...) external onlyMember returns (uint256) {
proposalId = ++proposalCount;
Proposal storage p = proposals[proposalId];
p.votes = 1; // 自動投票
p.hasVoted[msg.sender] = true; // 二重投票防止
// ...
}
// 正解:厳格な多数決 — メンバーの過半数
function executeProposal(uint256 proposalId) external {
if (p.votes * 2 <= memberList.length) revert NotEnoughVotes();
// ...
}
// 正解:スワップ&ポップ O(1) メンバー削除
function _removeFromMemberList(address member) internal {
for (uint256 i; i < list.length; ++i) {
if (list[i] == member) {
list[i] = list[list.length - 1];
list.pop();
break;
}
}
}
7. 技術的ポイント
| ポイント | 説明 |
|---|---|
| Rage Quit | (shares * ethBalance) / totalShares — 比例退出 |
| onlySelf パターン | msg.sender == address(this) — 自己呼び出しアクセス制御 |
| メンバー制投票 | 1人1票、シェアサイズに影響されない |
| 提案実行の任意呼び出し | target.call(data) → 柔軟だがセキュリティ上の懸念あり |
| 二重投票防止 | hasVoted は propose 時にマーク |
| O(1) 削除 | スワップ&ポップによる配列管理 |
| Moloch フレームワーク | このレベルの着想源 — MolochDAO v1/v2 の中核メカニズム |
| 業界比較 | Moloch v2 では Guild Kick、Loot、マルチトークントレジャリーを追加 |