Skip to content
On this page

L2-2: Moloch Rage Quit(DAO退出メカニズム)⭐

1. 問題

「Rage Quit」(レイジクイット)をサポートするDAOコントラクトを作成します。メンバーは提案投票を通じてDAOを管理できます。任意のメンバーは一方的に自身のシェアを焼却し、比例配分でトレジャリーからETHを引き出すことができます。中核的な課題は:新規メンバーは提案実行を通じてのみ参加可能addMemberonlySelf アクセス制御パターン)であることです。

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つの制約を満たす必要があります:

  1. 外部から任意に呼び出せてはいけない(そうでなければ誰でもDAOに参加してシェアを希薄化できる)
  2. 提案実行を通じて呼び出せる必要がある(新規メンバー追加には民主的投票が必要なため)
  3. 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) → 柔軟だがセキュリティ上の懸念あり
二重投票防止hasVotedpropose 時にマーク
O(1) 削除スワップ&ポップによる配列管理
Moloch フレームワークこのレベルの着想源 — MolochDAO v1/v2 の中核メカニズム
業界比較Moloch v2 では Guild Kick、Loot、マルチトークントレジャリーを追加

Built with AiAda