Интеграция с Compound Governor

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Интеграция с Compound Governor
Средняя
~3-5 рабочих дней
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1258
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1170
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    873
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1092
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    563
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    830

Разработка механизма rage quit

Rage quit — это право участника DAO выйти из организации и забрать пропорциональную долю treasury до исполнения proposal, с которым он не согласен. Механика пришла из Moloch DAO (2019) и решает фундаментальную проблему governance: minority protection. Без rage quit миноритарный участник, проигравший голосование, может либо принять решение, либо продать долю на рынке. С rage quit — есть третий вариант: выйти с честной долей активов.

Moloch-style rage quit: классическая реализация

Moloch V2/V3 — референсная реализация. Участники держат shares (не ERC-20, а внутренний учёт) и loot (shares без voting power). Rage quit конвертирует shares/loot в пропорциональную долю treasury tokens.

contract MolochDAO {
    struct Member {
        address delegateKey;
        uint256 shares;      // голосующие доли
        uint256 loot;        // неголосующие доли (для rage quit без потери voting power)
        bool exists;
        uint256 highestIndexYesVote;  // для rage quit lock
        uint256 jailed;
    }
    
    mapping(address => Member) public members;
    uint256 public totalShares;
    uint256 public totalLoot;
    
    // Whitelist approved tokens для rage quit
    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");
        
        // Нельзя rage quit если проголосовал YES за proposal в очереди
        // (защита от "vote yes, drain treasury, rage quit" атаки)
        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;
        
        // Пропорциональная выплата по всем approved tokens
        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) {
        // Proposal с этим индексом должен быть обработан
        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-style fork: rage quit на уровне протокола

Nouns V3 реализует более масштабный rage quit — не выход одного участника, а fork всего DAO. Если достаточный процент токен-холдеров в эскроу (например 20%) — создаётся новый fork DAO с пропорциональной долей treasury.

contract NounsDAOForkEscrow {
    INounsToken public nounsToken;
    address public dao;
    uint256 public forkId;
    
    // tokenId => владелец (для возврата если fork не активирован)
    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);
    }
    
    // Вернуть токены если передумал до активации fork
    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 {
    // Только DAO может активировать fork
    require(msg.sender == address(dao));
    
    uint256 forkSupply = forkEscrow.numTokensInEscrow();
    uint256 totalSupply = nounsToken.totalSupply();
    
    // Пропорциональная доля treasury
    uint256 ethToFork = (address(timelock).balance * forkSupply) / totalSupply;
    
    // USDC и другие токены тоже делятся
    for (uint256 i = 0; i < forkDAOTokens.length; i++) {
        uint256 tokenBalance = IERC20(forkDAOTokens[i]).balanceOf(address(timelock));
        uint256 tokenAmount = (tokenBalance * forkSupply) / totalSupply;
        // transfer в fork treasury
    }
    
    // Transfer ETH
    payable(forkTreasury).transfer(ethToFork);
    
    emit ForkExecuted(forkId, forkTreasury, ethToFork);
}

ERC-20 DAO с rage quit (Governor + Rage Quit)

Для DAO на ERC-20 токенах rage quit сложнее — токены fungible и freely tradable. Механика должна решать: пользователь должен lock токены для участия в rage quit или может выйти с любым балансом?

Vault-based rage quit

Участники депонируют токены в vault (получают receipt tokens), vault участвует в governance. Rage quit — burn receipt tokens, получить пропорцию vault assets.

contract DAOVaultWithRageQuit {
    IERC20 public immutable governanceToken;
    // Receipt token — 1:1 с депозитом
    IReceiptToken public immutable receiptToken;
    
    // Approved assets в vault (ETH + ERC-20)
    address[] public vaultAssets;
    
    // Pending proposals (для rage quit window)
    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 {
        // Проверяем что нет активных proposals в grace period
        // за которые пользователь голосовал
        _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 {
        // Проверяем что нет proposals в rage quit window
        // за которые голосовал пользователь
        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");
            }
        }
    }
}

Rage quit window: критический параметр

Rage quit window — период между принятием proposal и его исполнением, в течение которого несогласные могут выйти. По сути это Timelock с rage quit функциональностью.

Параметр Типичное значение Соображения
Rage quit window 3–7 дней Должен быть достаточен для реакции
Voting period 3–7 дней Информирует о принятых решениях
Execution delay Rage quit window + buffer Timelock >= rage quit window
Min stake для участия 0 (любой holder) Или threshold для предотвращения spam

Типичная ошибка: Timelock короче rage quit window. Если proposal исполняется через 24 часа, а rage quit window — 3 дня, механика не работает.

Газовая оптимизация

Rage quit с large token list дорог — перебор всех assets в цикле. Оптимизации:

  • Claim по одному токену: пользователь указывает конкретные токены для получения, не все
  • Merkle distribution: для большого количества assets — snapshot + merkle tree
  • Pull payment pattern: средства резервируются, пользователь забирает их отдельными транзакциями

Когда rage quit не подходит

Rage quit эффективен для treasury DAO (держат fungible assets). Для operational DAO (исполняют работу) — сложнее: treasury может состоять из illiquid активов (NFT, vesting токены, LP позиции). Rage quit на illiquid assets требует либо немедленной ликвидации (потери), либо отсроченных выплат.

В таких случаях рассматривают token buyback механизм или exit fee как альтернативу прямому rage quit.