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

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Інтеграція з Parcel (мультисиг-платежі DAO)
Простий
~2-3 дні
Часті запитання

Напрямки блокчейн-розробки

Етапи блокчейн-розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1306
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1218
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    920
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1147
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    610
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    885

Розробка 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;
        
        // Бінарний пошук через checkpoints
        if (checkpoints[account][nCheckpoints - 1].fromBlock <= blockNumber) {
            return checkpoints[account][nCheckpoints - 1].votes;
        }
        // ...реалізація бінарного пошуку
    }
}

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 на рівні proposal)

Найоригінальніша частина 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 (skateboarding), Purple (Farcaster), Federation — всі використовують Builder DAO framework.

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

Параметр Nouns Типовий fork 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 → увага → вища ставка на наступний аукціон. Це самофінансуюча система без 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 тижні

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