Разработка 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 состоит из четырёх смарт-контрактов:
- NounsToken (ERC-721 + ERC20Votes) — NFT, каждый является voting unit
- NounsAuctionHouse — механизм ежедневного аукциона
- NounsGovernor — governance с fork механикой (rage quit)
- 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 протоколами.







