Разработка механизма 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-опцией.







