Розробка механізму rage quit для вашого DAO
Ми розробляємо механізм rage quit — критично важливий захист міноритарних учасників DAO. Наші рішення інтегруються з Compound Governor, Moloch, Nouns та іншими фреймворками. За тривалий досвід у Web3 ми реалізували rage quit для 15+ DAO із загальним TVL понад $200M. Атака на DAO без такого захисту може коштувати до $10M — це реальні цифри з практики. Кейс: один із наших клієнтів запобіг втратам на $500k завдяки впровадженню rage quit.
Навіщо потрібен rage quit?
Rage quit — це право учасника DAO вийти та забрати пропорційну частку treasury до виконання proposal, з яким він не згоден. Механіка прийшла з Moloch DAO та вирішує фундаментальну проблему governance — захист міноритаріїв. Без rage quit міноритарний учасник, який програв голосування, може або прийняти рішення, або продати частку на ринку (часто з дисконтом). З rage quit — є третій варіант: вийти з чесною часткою активів за справедливою ціною.
Як працює rage quit у різних DAO?
Moloch DAO (shares/loot)
Moloch V2/V3 — референсна реалізація. Учасники тримають shares (внутрішній облік) та loot (shares без voting power). Rage quit конвертує shares/loot у пропорційну частку treasury tokens.
contract MolochDAO {
struct Member {
address delegateKey;
uint256 shares;
uint256 loot;
bool exists;
uint256 highestIndexYesVote;
uint256 jailed;
}
mapping(address => Member) public members;
uint256 public totalShares;
uint256 public totalLoot;
address[] public approvedTokens;
function ragequit(uint256 sharesToBurn, uint256 lootToBurn) public nonReentrant {
require(members[msg.sender].shares >= sharesToBurn, "Insufficient shares");
require(members[msg.sender].loot >= lootToBurn, "Insufficient loot");
require(
canRagequit(members[msg.sender].highestIndexYesVote),
"Cannot ragequit until highest index proposal member voted YES on is processed"
);
_ragequit(msg.sender, sharesToBurn, lootToBurn);
}
function _ragequit(address memberAddress, uint256 sharesToBurn, uint256 lootToBurn) internal {
uint256 initialTotalSharesAndLoot = totalShares + totalLoot;
members[memberAddress].shares -= sharesToBurn;
members[memberAddress].loot -= lootToBurn;
totalShares -= sharesToBurn;
totalLoot -= lootToBurn;
for (uint256 i = 0; i < approvedTokens.length; i++) {
address token = approvedTokens[i];
uint256 treasuryBalance = IERC20(token).balanceOf(address(this));
uint256 amountToRagequit = (treasuryBalance * (sharesToBurn + lootToBurn))
/ initialTotalSharesAndLoot;
if (amountToRagequit > 0) {
IERC20(token).safeTransfer(memberAddress, amountToRagequit);
}
}
emit Ragequit(memberAddress, sharesToBurn, lootToBurn);
}
function canRagequit(uint256 highestIndexYesVote) public view returns (bool) {
require(highestIndexYesVote < proposalQueue.length, "No proposal queue");
return proposals[proposalQueue[highestIndexYesVote]].processed;
}
}
Ключовий захист: highestIndexYesVote lock. Без неї можлива атака: проголосувати YES за proposal, який дає ETH зловмиснику → treasury ще повна → зробити rage quit → зловмисник отримує і частку treasury, і виплату за proposal. Lock блокує rage quit, доки proposal не оброблено.
Nouns DAO (fork)
Nouns V3 реалізує масштабний rage quit — не вихід одного учасника, а fork всього DAO. Якщо 20%+ токен-холдерів поміщають токени в ескроу, створюється новий fork із пропорційною часткою treasury.
contract NounsDAOForkEscrow {
INounsToken public nounsToken;
address public dao;
uint256 public forkId;
mapping(uint256 => address) private escrowedTokens;
uint256 public numTokensInEscrow;
function escrowToFork(
uint256[] calldata tokenIds,
uint256[] calldata proposalIds,
string calldata reason
) external {
for (uint256 i = 0; i < tokenIds.length; i++) {
require(nounsToken.ownerOf(tokenIds[i]) == msg.sender, "Not token owner");
escrowedTokens[tokenIds[i]] = msg.sender;
nounsToken.transferFrom(msg.sender, address(this), tokenIds[i]);
}
numTokensInEscrow += tokenIds.length;
emit TokensEscrowed(msg.sender, tokenIds, proposalIds, reason);
}
function returnTokensToOwner(address owner, uint256[] calldata tokenIds) external {
require(msg.sender == dao, "Only DAO");
for (uint256 i = 0; i < tokenIds.length; i++) {
require(escrowedTokens[tokenIds[i]] == owner, "Not escrow owner");
escrowedTokens[tokenIds[i]] = address(0);
nounsToken.transferFrom(address(this), owner, tokenIds[i]);
}
numTokensInEscrow -= tokenIds.length;
}
}
Після активації fork новий NounsToken деплоїться з копією supply, treasury ділиться пропорційно:
function executeFork() external {
require(msg.sender == address(dao));
uint256 forkSupply = forkEscrow.numTokensInEscrow();
uint256 totalSupply = nounsToken.totalSupply();
uint256 ethToFork = (address(timelock).balance * forkSupply) / totalSupply;
for (uint256 i = 0; i < forkDAOTokens.length; i++) {
uint256 tokenBalance = IERC20(forkDAOTokens[i]).balanceOf(address(timelock));
uint256 tokenAmount = (tokenBalance * forkSupply) / totalSupply;
// transfer в fork treasury
}
payable(forkTreasury).transfer(ethToFork);
emit ForkExecuted(forkId, forkTreasury, ethToFork);
}
ERC-20 DAO (vault-based)
Для DAO на ERC-20 токенах rage quit складніший — токени fungible та freely tradable. Ми використовуємо vault-based підхід: учасники депонують токени в DA vault (отримують receipt tokens), vault бере участь у governance.
contract DAOVaultWithRageQuit {
IERC20 public immutable governanceToken;
IReceiptToken public immutable receiptToken;
address[] public vaultAssets;
mapping(uint256 => uint256) public proposalRageQuitDeadline;
function deposit(uint256 amount) external {
governanceToken.safeTransferFrom(msg.sender, address(this), amount);
receiptToken.mint(msg.sender, amount);
emit Deposited(msg.sender, amount);
}
function rageQuit(uint256 receiptAmount, address[] calldata tokens) external {
_validateNoActiveLocks(msg.sender);
uint256 totalReceipts = receiptToken.totalSupply();
receiptToken.burn(msg.sender, receiptAmount);
for (uint256 i = 0; i < tokens.length; i++) {
uint256 balance;
if (tokens[i] == address(0)) {
balance = address(this).balance;
} else {
balance = IERC20(tokens[i]).balanceOf(address(this));
}
uint256 payout = (balance * receiptAmount) / totalReceipts;
if (payout > 0) {
if (tokens[i] == address(0)) {
payable(msg.sender).transfer(payout);
} else {
IERC20(tokens[i]).safeTransfer(msg.sender, payout);
}
}
}
emit RageQuit(msg.sender, receiptAmount, tokens);
}
function _validateNoActiveLocks(address user) internal view {
uint256[] memory activeProposals = governor.getActiveProposals();
for (uint256 i = 0; i < activeProposals.length; i++) {
uint256 proposalId = activeProposals[i];
if (governor.hasVoted(proposalId, user)) {
uint256 deadline = proposalRageQuitDeadline[proposalId];
require(block.timestamp > deadline, "Active proposal in rage quit window");
}
}
}
}
Порівняння підходів
| Характеристика | Moloch-style | Nouns-style | Vault-based |
|---|---|---|---|
| Тип DAO | Treasury DAO | NFT DAO | ERC-20 DAO |
| Вихід | Індивідуальний | Форк (груповий) | Індивідуальний |
| Gas cost | Низький | Високий | Середній |
| Гнучкість | Середня | Низька | Висока |
| Складність впровадження | Середня | Висока | Середня |
Газова вартість типової транзакції rage quit
| Реалізація | Gas (при 1 токені) | Gas (при 10 токенах) |
|---|---|---|
| Moloch-style | 60k | 120k |
| Nouns-style (fork) | 200k | 800k |
| Vault-based | 80k | 150k |
Ключові параметри та оптимізація
Rage quit window
Rage quit window — період між прийняттям proposal та його виконанням, протягом якого незгодні можуть вийти. Типове значення: 3–7 днів. Timelock має бути >= rage quit window.
Типова помилка: Timelock коротший за rage quit window. Якщо proposal виконується через 24 години, а rage quit window — 3 дні, механіка не працює.
Газова оптимізація
Rage quit з великим списком токенів дорогий. Оптимізації:
- Claim по одному токену: користувач вказує конкретні токени. Економія до 40% газу.
- Merkle distribution: для великої кількості assets — snapshot + merkle tree.
- Pull payment pattern: кошти резервуються, користувач забирає їх окремими транзакціями.
Деталі Merkle distribution
Merkle root фіксується на момент proposal. Користувачі доводять свою частку через merkle proof. Це знижує gas з O(n) до O(log n) за кількістю токенів.Обмеження та альтернативи
Rage quit ефективний для treasury DAO (тримають fungible assets). Для operational DAO — treasury може складатися з illiquid активів (NFT, vesting, LP позиції). У таких випадках ми пропонуємо token buyback або exit fee. Порівняння: vault-based підхід у 2 рази дешевший по газу, ніж fork-style, при однаковій кількості токенів.
Наш досвід та процес впровадження
Ми реалізували rage quit для DAO на Compound Governor, Uniswap та кастомних губернаторах. Кожен проєкт проходить формальну верифікацію (Slither, Mythril) та аудит. Гарантуємо підтримку після деплою.
Процес роботи
- Аналітика: вивчаємо поточну архітектуру governance, визначаємо вимоги до rage quit.
- Проєктування: обираємо підхід (Moloch/Nouns/Vault), проєктуємо контракти.
- Реалізація: пишемо смарт-контракти на Solidity 0.8.x з використанням Foundry/Hardhat.
- Тестування: unit-тести, fuzzing (Echidna), інтеграційні тести, покриття 95%+.
- Аудит: зовнішній аудит (опціонально) та внутрішня перевірка.
- Деплой та підтримка: розгортання на цільовій мережі, моніторинг, документація.
Входить у роботу
Входить у роботу: смарт-контракти з реалізацією rage quit, інтеграція з існуючим Governor (Compound, OZ, кастом), модульні тести з покриттям 95%+, документація та інструкція з експлуатації, розгортання в mainnet/testnet, підтримка 3 місяці після запуску.
Як замовити реалізацію?
Зв'яжіться з нами для оцінки вашого проєкту. Ми проаналізуємо вашу DAO та запропонуємо оптимальне рішення. Терміни — від 2 тижнів до 2 місяців залежно від складності. Вартість розраховується індивідуально.
Оцініть свій проєкт зараз — наші інженери з великим досвідом впровадять механізм, який захистить учасників та підвищить довіру до вашого DAO. Замовте реалізацію та забезпечте on-chain управління з exit-опцією.







