Интеграция с Parcel (мультисиг-платежи DAO)

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Интеграция с Parcel (мультисиг-платежи DAO)
Простая
~2-3 рабочих дня
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • 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

Разработка Nouns-style DAO (auction-based)

Nouns DAO запустился в августе 2021 года и изменил то, как люди думают о NFT и DAO совместно. Механика проста: один Noun (NFT) аукционируется каждые 24 часа, все вырученные средства идут в DAO treasury, каждый Noun = один голос в governance. Нет pre-mine, нет VC allocation, нет whitelist — только открытый аукцион каждый день навсегда. На момент написания treasury превышает 30,000 ETH, и система работает непрерывно более трёх лет.

Это не просто механика, это дизайн-паттерн, который воспроизводится: Lil Nouns, Gnars, Purple (Farcaster DAO), Builder DAO (Zora). Разработка Nouns-style DAO — это хорошо исследованная задача с открытым reference implementation.

Ключевые компоненты системы

Nouns-style DAO состоит из четырёх смарт-контрактов:

  1. NounsToken (ERC-721 + ERC20Votes) — NFT, каждый является voting unit
  2. NounsAuctionHouse — механизм ежедневного аукциона
  3. NounsGovernor — governance с fork механикой (rage quit)
  4. NounsTreasury (Timelock) — treasury под governance контролем

Дополнительно: NounsDescriptor — on-chain генерация SVG artwork, NounsSeeder — Chainlink VRF для random seed при генерации.

NounsToken: NFT как voting unit

contract NounsToken is ERC721Checkpointable, INounsToken {
    // Адрес для Nounder's reward (каждый 10-й Noun идёт основателям)
    address public noundersDAO;
    
    // Только AuctionHouse может минтить
    address public minter;
    
    // Seed для генерации artwork
    mapping(uint256 => INounsSeeder.Seed) public seeds;
    INounsSeeder public seeder;
    INounsDescriptor public descriptor;
    
    uint256 private _currentNounId;
    
    function mint() public override onlyMinter returns (uint256) {
        // Каждый 10-й Noun — основателям (nounders reward)
        if (_currentNounId <= 1820 && _currentNounId % 10 == 0) {
            _mintTo(noundersDAO, _currentNounId++);
        }
        return _mintTo(minter, _currentNounId++);
    }
    
    function _mintTo(address to, uint256 nounId) internal returns (uint256) {
        // Получаем случайный seed через Seeder (Chainlink VRF или block hash)
        INounsSeeder.Seed memory seed = seeds[nounId] = seeder.generateSeed(nounId, descriptor);
        _mint(owner(), to, nounId);
        emit NounCreated(nounId, seed);
        return nounId;
    }
    
    // tokenURI генерируется on-chain — никакого IPFS
    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        require(_exists(tokenId), "URI query for nonexistent token");
        return descriptor.tokenURI(tokenId, seeds[tokenId]);
    }
    
    // dataURI — SVG прямо в base64
    function dataURI(uint256 tokenId) public view override returns (string memory) {
        return descriptor.dataURI(tokenId, seeds[tokenId]);
    }
}

ERC-721 с checkpoint voting (ERC721Checkpointable)

Обычный ERC-721 не имеет voting power механики. Nouns расширяет его через checkpoint систему — аналог ERC20Votes, но для NFT:

abstract contract ERC721Checkpointable is ERC721Enumerable {
    mapping(address => address) private _delegates;
    
    struct Checkpoint {
        uint32 fromBlock;
        uint96 votes;
    }
    
    mapping(address => mapping(uint32 => Checkpoint)) public checkpoints;
    mapping(address => uint32) public numCheckpoints;
    
    function votesToDelegate(address delegator) public view returns (uint96) {
        return safe96(balanceOf(delegator), "ERC721Checkpointable: votes exceed 96 bits");
    }
    
    function delegate(address delegatee) public {
        return _delegate(msg.sender, delegatee);
    }
    
    function getPriorVotes(address account, uint256 blockNumber) public view returns (uint96) {
        require(blockNumber < block.number, "ERC721Checkpointable: not yet determined");
        
        uint32 nCheckpoints = numCheckpoints[account];
        if (nCheckpoints == 0) return 0;
        
        // Binary search через checkpoints
        if (checkpoints[account][nCheckpoints - 1].fromBlock <= blockNumber) {
            return checkpoints[account][nCheckpoints - 1].votes;
        }
        // ...binary search implementation
    }
}

NounsAuctionHouse: механика аукциона

Это сердце системы. Каждые 24 часа — новый аукцион, новый Noun.

contract NounsAuctionHouse is INounsAuctionHouse, PausableUpgradeable, ReentrancyGuardUpgradeable, OwnableUpgradeable {
    INounsToken public nouns;
    address public weth;
    
    uint256 public timeBuffer;      // минимальное время до конца аукциона после bid (15 мин)
    uint256 public reservePrice;    // минимальная ставка
    uint8 public minBidIncrementPercentage;  // минимальный инкремент ставки (%)
    uint256 public duration;        // длительность аукциона (24 часа)
    
    INounsAuctionHouse.Auction public auction;
    
    struct Auction {
        uint256 nounId;
        uint256 amount;       // текущая ставка
        uint256 startTime;
        uint256 endTime;
        address payable bidder;
        bool settled;
    }
    
    function createBid(uint256 nounId) external payable override nonReentrant {
        INounsAuctionHouse.Auction memory _auction = auction;
        
        require(_auction.nounId == nounId, "Noun not up for auction");
        require(block.timestamp < _auction.endTime, "Auction expired");
        require(msg.value >= reservePrice, "Must send at least reservePrice");
        require(
            msg.value >= _auction.amount + ((_auction.amount * minBidIncrementPercentage) / 100),
            "Must send more than last bid by minBidIncrementPercentage amount"
        );
        
        address payable lastBidder = _auction.bidder;
        
        // Возвращаем предыдущему bidder-у
        if (lastBidder != address(0)) {
            _safeTransferETHWithFallback(lastBidder, _auction.amount);
        }
        
        auction.amount = msg.value;
        auction.bidder = payable(msg.sender);
        
        // Если bid пришёл за timeBuffer до конца — продлеваем
        bool extended = _auction.endTime - block.timestamp < timeBuffer;
        if (extended) {
            auction.endTime = block.timestamp + timeBuffer;
        }
        
        emit AuctionBid(_auction.nounId, msg.sender, msg.value, extended);
        
        if (extended) {
            emit AuctionExtended(_auction.nounId, auction.endTime);
        }
    }
    
    function settleCurrentAndCreateNewAuction() external override nonReentrant whenNotPaused {
        _settleAuction();
        _createAuction();
    }
    
    function _settleAuction() internal {
        INounsAuctionHouse.Auction memory _auction = auction;
        
        require(_auction.startTime != 0, "Auction hasn't begun");
        require(!_auction.settled, "Auction has already been settled");
        require(block.timestamp >= _auction.endTime, "Auction hasn't completed");
        
        auction.settled = true;
        
        if (_auction.bidder == address(0)) {
            // Никто не поставил — Noun идёт в treasury
            nouns.transferFrom(address(this), owner(), _auction.nounId);
        } else {
            nouns.transferFrom(address(this), _auction.bidder, _auction.nounId);
        }
        
        if (_auction.amount > 0) {
            // ETH идёт в treasury (Timelock)
            _safeTransferETHWithFallback(owner(), _auction.amount);
        }
        
        emit AuctionSettled(_auction.nounId, _auction.bidder, _auction.amount);
    }
    
    // Fallback: если ETH transfer не прошёл — отправляем WETH
    function _safeTransferETHWithFallback(address to, uint256 amount) internal {
        if (!_safeTransferETH(to, amount)) {
            IWETH(weth).deposit{ value: amount }();
            IERC20(weth).transfer(to, amount);
        }
    }
}

Anti-sniping: time buffer

timeBuffer — критическая защита от last-second sniping. Если bid приходит за 15 минут до конца аукциона — конец сдвигается ещё на 15 минут. Это эффективно убирает incentive делать bid в последние секунды — аукцион будет продолжаться пока есть желающие.

On-chain SVG artwork: NounsDescriptor

Одна из самых инновационных частей Nouns — artwork полностью хранится on-chain. Нет IPFS, нет centralized сервера. Noun генерируется из набора слоёв (backgrounds, bodies, accessories, heads, glasses) хранящихся как RLE-сжатые данные прямо в контракте.

contract NounsDescriptor {
    // Компрессированные слои artwork в байтах (RLE encoding)
    bytes[] public bodies;
    bytes[] public accessories;
    bytes[] public heads;
    bytes[] public glasses;
    
    // Генерация SVG из seed
    function generateSVGImage(INounsSeeder.Seed memory seed) 
        external view returns (string memory svg) 
    {
        ISVGRenderer.SVGParams memory params = ISVGRenderer.SVGParams({
            parts: _getPartsForSeed(seed),
            background: backgrounds[seed.background]
        });
        return renderer.generateSVG(params);
    }
    
    function tokenURI(uint256 tokenId, INounsSeeder.Seed memory seed)
        external view override returns (string memory)
    {
        string memory name = string(abi.encodePacked('Noun ', tokenId.toString()));
        string memory description = string(abi.encodePacked('Noun ', tokenId.toString(), ' is a member of the Nouns DAO'));
        
        return genericDataURI(name, description, seed);
    }
    
    function genericDataURI(
        string memory name,
        string memory description,
        INounsSeeder.Seed memory seed
    ) public view override returns (string memory) {
        NFTDescriptor.TokenURIParams memory params = NFTDescriptor.TokenURIParams({
            name: name,
            description: description,
            parts: _getPartsForSeed(seed),
            background: backgrounds[seed.background]
        });
        return NFTDescriptor.constructTokenURI(renderer, params);
    }
}

Полностью on-chain NFT — это постоянство. Nouns будут существовать пока существует Ethereum.

NounsGovernor: расширенный Governor с fork механизмом

Nouns Governor отличается от стандартного OpenZeppelin Governor двумя ключевыми механиками: objection period и fork.

Objection period

После окончания голосования — дополнительный период (48 часов), в течение которого только голоса Against принимаются. Если proposal прошёл голосование, но в последний момент появилось много против — последние голоса For не помогут принять proposal. Это защита от last-minute vote manipulation.

enum ProposalState {
    Pending,
    Active,
    Canceled,
    Defeated,
    Succeeded,
    Queued,
    Expired,
    Executed,
    Vetoed,
    ObjectionPeriod  // Новое состояние
}

function state(uint256 proposalId) public view override returns (ProposalState) {
    // ...стандартная логика...
    
    // Проверяем нужен ли objection period
    if (isForVotesSucceeded && !isObjectionPeriodOver) {
        // Если в последние X блоков пришли значительные Against голоса
        if (proposal.objectionPeriodEndBlock > block.number) {
            return ProposalState.ObjectionPeriod;
        }
    }
}

Fork mechanism (rage quit on proposal level)

Самая оригинальная часть Nouns v3: если крупный holder не согласен с принятым proposal, он может инициировать fork. DAO разделяется: несогласные забирают пропорциональную долю treasury в новый fork DAO.

function escrowToFork(
    uint256[] calldata tokenIds,
    uint256[] calldata proposalIds,
    string calldata reason
) external onlyNounOwner {
    // Токены эскроуются — их нельзя использовать для голосования пока в escrow
    for (uint256 i = 0; i < tokenIds.length; i++) {
        nouns.safeTransferFrom(msg.sender, address(forkEscrow), tokenIds[i]);
    }
    
    emit NounsEscrowed(msg.sender, tokenIds, proposalIds, reason);
    
    // Если количество заэскроуенных токенов превышает forkThreshold
    // (например 20% от total supply) — fork может быть активирован
    if (_isForkThresholdReached()) {
        _activateFork();
    }
}

function _activateFork() internal {
    // Новый fork DAO создаётся с теми же параметрами
    // Treasury разделяется пропорционально количеству токенов в escrow
    uint256 forkedTreasuryAmount = (address(timelock).balance * escrowedTokens) / totalSupply;
    
    // Деплой нового NounsToken и NounsGovernor для fork DAO
    (address forkToken, address forkTreasury) = forkDAODeployer.deployForkDAO(
        forkEscrow.numTokensInEscrow(),
        forkedTreasuryAmount,
        block.timestamp + FORK_PERIOD  // период когда остальные могут присоединиться к fork
    );
    
    // Transfer ETH в fork treasury
    payable(forkTreasury).transfer(forkedTreasuryAmount);
}

Это принципиально иной подход к governance minority protection по сравнению со стандартным rage quit в Moloch-style DAO.

Кастомизация под конкретный проект

Builder DAO (Zora) создал factory для запуска Nouns-style DAO без кодинга: TokenFactory деплоит кастомные контракты с заданными параметрами. Gnars (скейтбординг), Purple (Farcaster), Federation — все используют Builder DAO framework.

Параметры кастомизации:

Параметр Nouns Типичный форк Builder DAO default
Auction duration 24 часа 12–48 часов 24 часа
Reserve price 1 ETH 0.01–1 ETH 0
Founder allocation 10% (каждый 10-й) 5–15% Настраивается
Voting delay 1 день 1 час – 2 дня 1 день
Voting period 3 дня 2–7 дней 3 дня
Quorum 10% 5–20% 10%

On-chain artwork vs IPFS

Полностью on-chain artwork — дорогой деплой (хранение байтов в EVM стоит gas). Nouns потратили значительные суммы на деплой descriptor с artwork. Для большинства форков оптимальный компромисс: artwork на IPFS с on-chain hash, Descriptor генерирует metadata динамически используя IPFS CID.

Экономика: устойчивость модели

Nouns-style модель создаёт flywheel: аукцион → ETH в treasury → proposals → деятельность DAO → внимание → выше bid на следующий аукцион. Это самофинансирующаяся система без external fundraising.

Ключевой вопрос устойчивости: насколько долго participation остаётся высокой. Nouns решает это через постоянную on-chain активность (ежедневный аукцион — это событие), качество сообщества (каждый Noun дорог, значит владельцы вовлечены) и fork механику (minority protection удерживает участников от "exit by dumping").

Стек для разработки форка

Компонент Референс Альтернатива
Auction contract Nouns AuctionHouse Builder DAO factory
NFT + voting ERC721Checkpointable ERC721Votes (OZ)
Artwork storage NounsDescriptor (on-chain) IPFS + descriptor
Random seed Chainlink VRF v2 Prevrandao (проще, менее надёжно)
Governor NounsGovernor v3 OZ Governor
Frontend nouns.wtf open source Builder DAO UI

Этапы разработки

Фаза Содержание Срок
Дизайн параметров Auction duration, pricing, founder allocation, governance 1–2 нед
NFT контракт + artwork ERC-721 + checkpoint voting + descriptor 3–4 нед
Auction house Bid механика, settlement, anti-snipe 2–3 нед
Governor + Timelock Governance с кастомными параметрами 2–3 нед
Artwork preparation Создание/подготовка слоёв, encoding 2–4 нед (зависит от арта)
Тесты Full coverage, fork simulation, governance attack scenarios 2–3 нед
Frontend Auction UI, governance, NFT gallery 4–6 нед
Аудит 3–4 нед

Первый запуск аукциона — это публичное событие: на него нужно привлечь внимание сообщества до деплоя. Механика прозрачна и понятна даже без технического бэкграунда — это конкурентное преимущество перед сложными DeFi протоколами.