Developing Nouns-style DAO (auction-based)
Nouns DAO launched in August 2021 and changed how people think about NFTs and DAOs together. The mechanic is simple: one Noun (NFT) is auctioned every 24 hours, all proceeds go to the DAO treasury, each Noun = one vote in governance. No pre-mine, no VC allocation, no whitelist — just an open auction every day forever. As of writing, the treasury exceeds 30,000 ETH, and the system has been running continuously for over three years.
This is not just mechanics, it's a design pattern that gets reproduced: Lil Nouns, Gnars, Purple (Farcaster DAO), Builder DAO (Zora). Developing a Nouns-style DAO is a well-researched task with an open reference implementation.
Key System Components
A Nouns-style DAO consists of four smart contracts:
- NounsToken (ERC-721 + ERC20Votes) — NFT, each one is a voting unit
- NounsAuctionHouse — daily auction mechanism
- NounsGovernor — governance with fork mechanism (rage quit)
- NounsTreasury (Timelock) — treasury under governance control
Additionally: NounsDescriptor — on-chain SVG artwork generation, NounsSeeder — Chainlink VRF for random seed during generation.
NounsToken: NFT as voting unit
contract NounsToken is ERC721Checkpointable, INounsToken {
// Address for Nounder's reward (every 10th Noun goes to founders)
address public noundersDAO;
// Only AuctionHouse can mint
address public minter;
// Seed for artwork generation
mapping(uint256 => INounsSeeder.Seed) public seeds;
INounsSeeder public seeder;
INounsDescriptor public descriptor;
uint256 private _currentNounId;
function mint() public override onlyMinter returns (uint256) {
// Every 10th Noun goes to founders (nounders reward)
if (_currentNounId <= 1820 && _currentNounId % 10 == 0) {
_mintTo(noundersDAO, _currentNounId++);
}
return _mintTo(minter, _currentNounId++);
}
function _mintTo(address to, uint256 nounId) internal returns (uint256) {
// Get random seed from Seeder (Chainlink VRF or block hash)
INounsSeeder.Seed memory seed = seeds[nounId] = seeder.generateSeed(nounId, descriptor);
_mint(owner(), to, nounId);
emit NounCreated(nounId, seed);
return nounId;
}
// tokenURI is generated on-chain — no 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 directly in base64
function dataURI(uint256 tokenId) public view override returns (string memory) {
return descriptor.dataURI(tokenId, seeds[tokenId]);
}
}
ERC-721 with checkpoint voting (ERC721Checkpointable)
Regular ERC-721 doesn't have voting power mechanics. Nouns extends it through a checkpoint system — analogous to ERC20Votes, but for NFTs:
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 through checkpoints
if (checkpoints[account][nCheckpoints - 1].fromBlock <= blockNumber) {
return checkpoints[account][nCheckpoints - 1].votes;
}
// ...binary search implementation
}
}
NounsAuctionHouse: auction mechanics
This is the heart of the system. Every 24 hours — a new auction, a new Noun.
contract NounsAuctionHouse is INounsAuctionHouse, PausableUpgradeable, ReentrancyGuardUpgradeable, OwnableUpgradeable {
INounsToken public nouns;
address public weth;
uint256 public timeBuffer; // minimum time until auction end after bid (15 min)
uint256 public reservePrice; // minimum bid
uint8 public minBidIncrementPercentage; // minimum bid increment (%)
uint256 public duration; // auction duration (24 hours)
INounsAuctionHouse.Auction public auction;
struct Auction {
uint256 nounId;
uint256 amount; // current bid
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;
// Return to previous bidder
if (lastBidder != address(0)) {
_safeTransferETHWithFallback(lastBidder, _auction.amount);
}
auction.amount = msg.value;
auction.bidder = payable(msg.sender);
// If bid came within timeBuffer of end — extend
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)) {
// No one bid — Noun goes to treasury
nouns.transferFrom(address(this), owner(), _auction.nounId);
} else {
nouns.transferFrom(address(this), _auction.bidder, _auction.nounId);
}
if (_auction.amount > 0) {
// ETH goes to treasury (Timelock)
_safeTransferETHWithFallback(owner(), _auction.amount);
}
emit AuctionSettled(_auction.nounId, _auction.bidder, _auction.amount);
}
// Fallback: if ETH transfer fails — send 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 is critical protection against last-second sniping. If a bid comes within 15 minutes of auction end — the end shifts another 15 minutes. This effectively removes the incentive to bid in the last seconds — the auction will continue as long as people want to bid.
On-chain SVG artwork: NounsDescriptor
One of the most innovative parts of Nouns — artwork is entirely stored on-chain. No IPFS, no centralized server. A Noun is generated from a set of layers (backgrounds, bodies, accessories, heads, glasses) stored as RLE-compressed data right in the contract.
contract NounsDescriptor {
// Compressed artwork layers in bytes (RLE encoding)
bytes[] public bodies;
bytes[] public accessories;
bytes[] public heads;
bytes[] public glasses;
// Generate SVG from 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);
}
}
Completely on-chain NFT means permanence. Nouns will exist as long as Ethereum exists.
NounsGovernor: extended Governor with fork mechanism
Nouns Governor differs from standard OpenZeppelin Governor through two key mechanics: objection period and fork.
Objection period
After voting ends — an additional period (48 hours) during which only Against votes are accepted. If a proposal passed the vote but in the final moment many votes against appeared — last-minute For votes won't help pass the proposal. This protects against last-minute vote manipulation.
enum ProposalState {
Pending,
Active,
Canceled,
Defeated,
Succeeded,
Queued,
Expired,
Executed,
Vetoed,
ObjectionPeriod // New state
}
function state(uint256 proposalId) public view override returns (ProposalState) {
// ...standard logic...
// Check if objection period is needed
if (isForVotesSucceeded && !isObjectionPeriodOver) {
// If significant Against votes came in recent blocks
if (proposal.objectionPeriodEndBlock > block.number) {
return ProposalState.ObjectionPeriod;
}
}
}
Fork mechanism (rage quit at proposal level)
The most original part of Nouns v3: if a major holder disagrees with an accepted proposal, they can initiate a fork. The DAO splits: dissenters take their proportional treasury share to a new fork DAO.
function escrowToFork(
uint256[] calldata tokenIds,
uint256[] calldata proposalIds,
string calldata reason
) external onlyNounOwner {
// Tokens are escrowed — can't be used for voting while in escrow
for (uint256 i = 0; i < tokenIds.length; i++) {
nouns.safeTransferFrom(msg.sender, address(forkEscrow), tokenIds[i]);
}
emit NounsEscrowed(msg.sender, tokenIds, proposalIds, reason);
// If escrow token count exceeds forkThreshold
// (e.g. 20% of total supply) — fork can be activated
if (_isForkThresholdReached()) {
_activateFork();
}
}
function _activateFork() internal {
// New fork DAO is created with same parameters
// Treasury is split proportionally by tokens in escrow
uint256 forkedTreasuryAmount = (address(timelock).balance * escrowedTokens) / totalSupply;
// Deploy new NounsToken and NounsGovernor for fork DAO
(address forkToken, address forkTreasury) = forkDAODeployer.deployForkDAO(
forkEscrow.numTokensInEscrow(),
forkedTreasuryAmount,
block.timestamp + FORK_PERIOD // period when others can join fork
);
// Transfer ETH to fork treasury
payable(forkTreasury).transfer(forkedTreasuryAmount);
}
This is a fundamentally different approach to governance minority protection compared to standard rage quit in Moloch-style DAOs.
Customization for a specific project
Builder DAO (Zora) created a factory for launching Nouns-style DAOs without coding: TokenFactory deploys custom contracts with specified parameters. Gnars (skateboarding), Purple (Farcaster), Federation — all use Builder DAO framework.
Customization parameters:
| Parameter | Nouns | Typical fork | Builder DAO default |
|---|---|---|---|
| Auction duration | 24 hours | 12–48 hours | 24 hours |
| Reserve price | 1 ETH | 0.01–1 ETH | 0 |
| Founder allocation | 10% (every 10th) | 5–15% | Configurable |
| Voting delay | 1 day | 1 hour – 2 days | 1 day |
| Voting period | 3 days | 2–7 days | 3 days |
| Quorum | 10% | 5–20% | 10% |
On-chain artwork vs IPFS
Completely on-chain artwork is expensive deployment (storing bytes in EVM costs gas). Nouns spent significant sums deploying descriptor with artwork. For most forks, the optimal compromise: artwork on IPFS with on-chain hash, Descriptor generates metadata dynamically using IPFS CID.
Economics: model sustainability
Nouns-style model creates a flywheel: auction → ETH in treasury → proposals → DAO activity → attention → higher bid on next auction. This is a self-financing system without external fundraising.
The key sustainability question: how long does participation stay high. Nouns solves this through constant on-chain activity (daily auction is an event), community quality (each Noun is expensive, so owners are engaged), and fork mechanism (minority protection prevents participants from "exit by dumping").
Development stack for a fork
| Component | Reference | Alternative |
|---|---|---|
| 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 (simpler, less reliable) |
| Governor | NounsGovernor v3 | OZ Governor |
| Frontend | nouns.wtf open source | Builder DAO UI |
Development stages
| Phase | Content | Timeline |
|---|---|---|
| Parameter design | Auction duration, pricing, founder allocation, governance | 1–2 weeks |
| NFT contract + artwork | ERC-721 + checkpoint voting + descriptor | 3–4 weeks |
| Auction house | Bid mechanics, settlement, anti-snipe | 2–3 weeks |
| Governor + Timelock | Governance with custom parameters | 2–3 weeks |
| Artwork preparation | Creating/preparing layers, encoding | 2–4 weeks (depends on art) |
| Tests | Full coverage, fork simulation, governance attack scenarios | 2–3 weeks |
| Frontend | Auction UI, governance, NFT gallery | 4–6 weeks |
| Audit | 3–4 weeks |
The first auction launch is a public event: it needs community attention before deployment. The mechanic is transparent and understandable even without technical background — competitive advantage over complex DeFi protocols.







