<!-- MIT License. Copyright (c) 2026 Decentralized Science Labs LLC. -->
<!-- summary: Every COLLAB contract and how they fit: the factory and its six clones, the singletons, DeSciFi, the lenses, the main flows, events and errors. -->
# Contracts guide

This page explains the Solidity contracts of COLLAB 2.2 for developers who read, test, integrate with or extend them: what each contract is for, who may call what, how the contracts call each other, and the events and errors you will meet. It describes behaviour; every signature is in the [Contract API reference](CONTRACT_API.md), generated from the ABIs. COLLAB 2.2 is an alpha: the contracts had AI-assisted internal reviews and no external audit.

## The contracts at a glance

`script/Deploy.s.sol` deploys 20 contracts, in this order, and the admin page's Deploy tab sends the same 20 creations. Sizes are the deployed bytecode from `web/assets/deploy/collab-protocol.json`; every COLLAB contract fits the EIP-170 limit of 24,576 bytes, which `test/SizeLimits.t.sol` checks.

| # | Contract | Kind | Role | Bytes |
|---:|---|---|---|---:|
| 1 | `TextStore` | singleton | content-addressed blobs (constitutions, announcements, proposal descriptions, token logos) | 3,462 |
| 2 | `CollabKeyring` | singleton | members' public encryption keys | 1,548 |
| 3 | `DeSciFiPair` | implementation | the pool every DeSciFi pair clones | 11,460 |
| 4 | `DeSciFiFactory` | singleton | creates and registers pools | 3,980 |
| 5 | `DeSciFiRouter` | singleton | swaps and liquidity over explicit pool paths | 8,953 |
| 6 | `LockBox` | implementation | the LP lock every lock box clones | 3,205 |
| 7 | `LockFactory` | singleton | creates and indexes lock boxes | 2,844 |
| 8 | `MemberDistributor` | singleton | snapshot airdrops to members | 5,419 |
| 9 | `DAOToken` | implementation | the ERC-20 a DAO launches | 8,061 |
| 10 | `TokenLauncher` | singleton | launches a DAO token with its allocations and pools | 7,589 |
| 11 | `PolicyBuilder` | singleton | the default call policy of new DAOs | 6,144 |
| 12–17 | `ShareToken`, `DAOGovernor`, `DAOTreasury`, `DAOPolicy`, `DAOProfile`, `CipherVault` | implementations | the six contracts every DAO clones | 14,243 / 21,068 / 4,548 / 14,852 / 13,331 / 14,363 |
| 18 | `DAOFactory` | singleton | creates DAOs and keeps the registry and the slugs | 11,946 |
| 19 | `DAOLens` | lens | read-only pages of DAOs, proposals and members | 16,201 |
| 20 | `MarketLens` | lens | read-only pages of tokens, pools, locks, airdrops and NFTs | 16,375 |

wL1, the wrapped L1 coin, is not deployed by COLLAB: the script uses the live GenesisL1 contract (`0x59a153c0fD47C6c1F305Abdb8030A90Aa3001fe2`), or `EXT_WL1` on a test chain. The GL1F collections and CIPNFT contracts that DAOs integrate with are outside contracts too ([Integrations](#integrations)).

No COLLAB contract has a protocol owner, an admin key or an upgrade path. The only owners are part of the design: a DAO token belongs to the DAO that launched it (until a vote hands it on), and a lock box to whoever may withdraw its LP. Implementations call `_disableInitializers()` in their constructors, so only their clones can be initialized, once. The build settings are fixed in `foundry.toml`: solc 0.8.28, via-IR, optimizer 200 runs, EVM `istanbul`, IPFS metadata hash.

## This site's contracts

The figure below shows the network and the contracts this site uses. Their addresses come from the site's `config.js`, which is written when the operator publishes a deployment from the admin page ([Publish, unpublish and check](publish.md#what-publishing-does)), or by hand on a site without the server kit ([Hosting without the server kit](static-hosting.md#generating-configjs)); the implementations behind every DAO are read from the DAO factory on the chain. To check the addresses, open the admin page's **Check** tab, `/admin/check`, which compares every contract with the code of this release and needs no admin token ([Checking any deployment](publish.md#checking-any-deployment)), or read them yourself as [Reading COLLAB from the chain](reading-the-chain.md) shows.

![This site's network and the COLLAB contracts it uses: the protocol contracts and the GenesisL1 contracts it connects to, as its config.js names them, and the code every DAO runs, read from the DAO factory.](live:deployment)

## One DAO, six clones

`DAOFactory.createDAO` deploys six EIP-1167 minimal clones with OpenZeppelin `Clones.cloneDeterministic` and initializes them in the same transaction, so a half-built DAO never exists. Each clone's salt is `keccak256(abi.encode(creator, salt, role))`, with role `"treasury"`, `"governor"`, `"shares"`, `"profile"`, `"policy"` or `"vault"`; `predictTreasury(creator, salt)` gives the treasury address before creation.

The treasury is the DAO. Every governed contract authorizes `msg.sender == treasury` (stored as `dao`), so "the DAO did it" always means "a passed proposal executed it".

| Clone | What it holds | Who may change it |
|---|---|---|
| `DAOTreasury` | L1, ERC-20, ERC-721 and ERC-1155 assets (CIPNFT assets and GL1F Model NFTs included); ERC-1271 approved hashes; module permissions | the governor (`executeBatch`), itself (`setModulePermission`, `signHash`, `revokeHash` inside a proposal), modules for exact (target, selector) pairs |
| `DAOGovernor` | proposals, deposits, votes, the rules | anyone submits, deposits, votes with shares and executes; the DAO changes the rules |
| `ShareToken` | shares, voting units, delegation, freezes, exclusions | the DAO mints, burns, freezes and configures; the governor places freeze locks; holders transfer and delegate |
| `DAOPolicy` | which calls each proposal kind may make, the critical kinds, the integrations | the DAO, by critical PARAMS proposals |
| `DAOProfile` | the permanent title, the logo, the description, links, announcements, constitution versions, representatives | the DAO; representatives within their permissions |
| `CipherVault` | key stewards, key epochs, reveals, declassifications, approved purchases | the DAO; stewards post envelopes and complete approved trades |

### DAOTreasury

`executeBatch(actions)` runs each `Action {address target; uint256 value; bytes data}` with a plain low-level call, in order, and reverts the whole batch if one fails. A failing action's own revert data is bubbled up; an action that fails without data reverts `ActionFailed(index)`. Each action emits `ActionExecuted(index, target, value, selector)`. Only the governor may call it.

`executeFromModule(target, data)` lets a module (a key vault, a representative through the profile's `operatorCall`) make one call the DAO granted: an exact target and selector, never with value, never to the treasury, the governor or the DAO's protected contracts (policy, shares, profile, vault). The factory grants the vault `finalizeOffer` and `deliverOffer` on every CIPNFT contract the DAO lists at creation. `isValidSignature(hash, …)` returns the ERC-1271 magic value for a hash a SIGNATURE proposal approved, until its expiry. The treasury accepts L1 and implements the ERC-721 and ERC-1155 receiver hooks.

### DAOGovernor

The governor runs the proposal lifecycle, modelled on GenesisL1's own governance. The details of rules and counting are in [Governance internals](governance-internals.md); the constants that bound them:

| Constant | Value |
|---|---|
| `MAX_ACTIONS` | 32 per proposal |
| `MAX_TITLE_BYTES`, `MAX_DESCRIPTION_BYTES` | 200 and 65,536 (the description is stored in TextStore) |
| voting period | 10 minutes to 365 days |
| execution delay (timelock) | at most 60 days |
| deposit period | 10 minutes to 365 days |
| execution window | 1 to 365 days |
| `MAX_MIN_DEPOSIT` | 1,000,000 L1 |
| `MAX_MIN_YES_BPS` | 5,000 (50 % of the snapshot's voting units) |
| `MAX_HOLDING_BLOCKS` | 2,250,000 |

Deposit flags (`DepositParams.flags`): `FLAG_BURN_VOTE_QUORUM` 1, `FLAG_BURN_DEPOSIT_PREVOTE` 2, `FLAG_BURN_VOTE_VETO` 4, `FLAG_PENALTY_TO_TREASURY` 8 (forfeits go to the treasury instead of `0x…dEaD`), `FLAG_FREEZE_LOCK` 16, `FLAG_FREEZE_BOND` 32. `state(id)` returns the `State` enum: 0 None, 1 DepositPeriod, 2 VotingPeriod, 3 Passed, 4 Rejected, 5 Vetoed, 6 NoQuorum, 7 DepositExpired, 8 Queued, 9 Executable, 10 Executed, 11 ExecutionExpired, 12 Canceled.

### ShareToken

An OpenZeppelin `ERC20Upgradeable` whose `_update` hook keeps checkpointed voting units per block. An account's units equal its balance unless it is the treasury, frozen or excluded; votes are the units delegated to an account (its own by default, with no activation step). `getPastUnits`, `getPastVotes`, `getPastDelegate`, `getPastTotalUnits` and `getPastMemberCount` read any past block. With `transfersEnabled` false, shares move only to and from the treasury; `setTransfersEnabled(true)` needs a holding period on the governor (`NeedsHoldingPeriod`), and `everTransferable`, once true, never clears. Name and symbol are at most 80 and 32 bytes (`InvalidMetadata`). Every change of units emits `UnitsChanged(account, previousUnits, newUnits)`, which the prerender replays to count members.

### DAOPolicy

The policy is the allowlist behind every proposal kind: `validate(kind, actions)` reverts unless every action is allowed, and returns whether the critical voting rules apply. It also holds the DAO's integrations and the spenders approvals may name. [Governance internals](governance-internals.md) explains rules, precedence, homes, the treasury stance and the 115 default rules.

### DAOProfile

| Field | Rule | Errors |
|---|---|---|
| `title()` | set at creation, never changes: 1 to 80 bytes, well-formed UTF-8, no C0 control character and no DEL | `InvalidTitle`, `InvalidText` |
| logo | empty, or a PNG of at most `MAX_LOGO_BYTES` (524,288) whose header passes `Png.header` and whose IHDR says 128 × 128 | `TooLarge`, `InvalidPng`, `WrongLogoSize(width, height)` |
| description | at most `MAX_DESCRIPTION_BYTES` (262,144) of well-formed UTF-8 without C0 controls other than tab and line feed (send LF, not CRLF) | `TooLarge`, `InvalidText` |
| `links()` | at most 2,048 bytes of UTF-8 | `TooLarge`, `InvalidText` |
| announcements | a heading of 1 to 80 bytes and a TextStore blob of at most 131,072 bytes; emitted as `Announcement`, not stored | `InvalidTitle`, `InvalidBlob`, `TooLarge` |
| constitution | a TextStore blob of at most 1,048,576 bytes; every version kept with the proposal that enacted it | `InvalidBlob`, `TooLarge` |
| representatives | at most 21, each with permission bits and an optional term end | `TooManyReps`, `InvalidRep(rep)`, `DuplicateRep(rep)` |

The logo and the description live in the profile's own storage (the `StoredBytes` library, at ERC-7201 slots) and an edit overwrites them in place: a shorter value zeroes the old tail, so editing never adds state. `setLogo(bytes)` and `setDescription(bytes)` accept empty bytes to remove. Each write emits `LogoChanged(hash, length)` or `DescriptionChanged(hash, length)`, without the bytes, and updates `updatedAt()` and `lastEditor()`. Reads go through `logoInfo()`/`descriptionInfo()` and `logoChunk(offset, size)`/`descriptionChunk(offset, size)`, `size` at most `MAX_CHUNK_BYTES` (16,384, else `ChunkTooLarge`); [Reading COLLAB from the chain](reading-the-chain.md#logos-and-descriptions) shows how.

Representative permissions are bits: `PERM_DESCRIPTION` 2, `PERM_LOGO` 4, `PERM_LINKS` 8, `PERM_ANNOUNCE` 16, `PERM_OPERATOR` 32, `ALL_PERMS` 62. Bit 1 (2.1's title permission) is unassigned and granting it reverts `InvalidRep`. A representative acts only while its term runs and it is not frozen (`isActiveRep`); it may `resign()`. `operatorCall(target, data)` lets a representative with `PERM_OPERATOR` trigger a call the treasury granted the profile as a module, and appointing one is a critical vote.

### CipherVault

The vault is the DAO's seat in CIPNFT, on every CIPNFT contract the DAO lists, with one DAO key. A contract cannot keep a secret, so the private key is held by up to 9 key stewards (`MAX_STEWARDS`) elected by vote. Stewards call `proposeKeyEpoch` (the DAO's X25519 key, an optional ML-KEM-768 key hash and the private key sealed to each steward), `postKeyEnvelopes`, `postRevealEnvelopes`, `finalizePurchase` and `deliverSale`; the DAO calls `setStewards`, `activateKeyEpoch`, `openReveal`, `closeReveal`, `openDeclassification`, `approvePurchase` and `revokePurchase`. Anyone may `publishDataKey` for an open declassification, which is accepted only when the key hashes to CIPNFT's commitment (`WrongDataKey`). Envelope bytes appear only in events (`KeyEnvelope`, `RevealEnvelope`); storage keeps their hash, block, time and author. The [sealed data guide](sealed-data.md) explains the protocol for members.

## The factory

`createDAO(CreateParams p)` takes the whole DAO in one struct, in this order:

| Field | Meaning |
|---|---|
| `title`, `slug` | the permanent name and web address |
| `logo`, `description` | bytes for the profile (empty allowed), checked as `setLogo` and `setDescription` check them |
| `links`, `constitution` | links text; the constitution's TextStore `Blob {ptr, hash}` (required) |
| `shareName`, `shareSymbol`, `transfersEnabled` | the share token |
| `founders`, `founderShares`, `treasuryShares` | initial holders: at most 200 founders (`MAX_FOUNDERS`), plus shares for the treasury |
| `deposit`, `standardRules`, `criticalRules` | `DAOGovernor.DepositParams` and two `VoteRules` |
| `reps`, `repPerms`, `repTermEnds` | the first representatives |
| `stewards` | the first key stewards |
| `extraCriticalKinds` | a bit mask of kinds to make critical on top of the mandatory ones |
| `salt` | for the clone addresses |
| `gl1f`, `cipnfts` | the integrations the DAO starts with (`DAOPolicy.Gl1fCollection[]`, `address[]`) |

The slug is 3 to 48 bytes of `a-z`, `0-9` and `-`, starts with a letter, has no `--` and does not end with `-` (`InvalidSlug()`); it is unique (`SlugTaken(uint256 id)` names the DAO that has it). Since a slug never starts with a digit, `/dao/<slug>` and `/dao/0x…` never collide. The factory also refuses mismatched founder lists (`LengthMismatch`), no founder shares (`NoFounders`) and a proposal threshold above the founders' total (`ThresholdUnreachable`). A DAO created with transferable shares gets a holding period of `DEFAULT_HOLDING_BLOCKS` (7,200 blocks, about 28 hours at 14 s a block). The clones are initialized in this order: shares, policy (with the rules `PolicyBuilder.build` returns), governor, treasury, profile, vault. Then the DAO is registered and the factory emits:

```solidity
event DAOCreated(uint256 indexed id, address indexed treasury, address indexed creator,
    address governor, address shares, address profile, address policy, address vault,
    string title, string slug);
```

Views: `daoCount()`, `daoAt(id)` (1-based), `daoOf(treasury)`, `idOf(treasury)` (0 for no DAO), `isDAO(treasury)`, `daoBySlug(slug)` (0 for none), `slugOf(treasury)` ("" for no DAO), `slugAvailable(slug)` (well formed and free), `predictTreasury(creator, salt)`, `implementations()`, `protocol()` (text store, launcher, distributor, DeSciFi factory and router, lock factory, keyring, wL1), `policyBuilder()`, `defaultGl1f()`, `defaultCipnfts()` and `VERSION` (`"COLLAB DAO v2.2.0"`).

### Integrations

The factory is deployed with default lists of GL1F collections and CIPNFT contracts (`script/integrations.json`, the live GenesisL1 ones). They bind no DAO: interfaces offer them first, and each DAO's policy keeps its own lists, at most 8 of each, changed only by critical PARAMS proposals. Adding a GL1F collection checks on chain that `registry.modelNFT() == nft`, `market.nft() == nft` and `runtime.registry() == registry`, and every listed contract must have code and be none of the DAO's or the protocol's contracts (`InvalidIntegration(reason)`).

## Protocol singletons

### TextStore

Immutable, content-addressed blobs stored as contract bytecode with Solady's SSTORE2. `store(bytes)` writes a blob in one transaction (chunked automatically) and returns `(pointer, hash)`; large blobs go up in several transactions with `storeChunk(bytes)` and then `commit(chunks, expectedHash)`. A chunk holds at most 24,000 bytes (`CHUNK_SIZE`), a blob at most 64 chunks (`MAX_CHUNKS`), and a blob of up to 23,991 bytes (`MAX_INLINE`) sits inline in its manifest. Blobs are deduplicated by keccak256 (`pointerOf(hash)`). Read them with `read(pointer)`, `hashOf(pointer)` and `lengthOf(pointer)`, or off-chain with `eth_getCode`, which has no gas cap: the manifest is a STOP byte, `"CTS1"`, a kind byte (0 inline, 1 a table of 20-byte chunk addresses), a 4-byte length and the payload. Every record that points to a blob also stores its hash, and the interface refuses content whose hash does not match. Since 2.2 a DAO's own logo and description no longer live here.

### CollabKeyring

Members publish an X25519 public key and optionally an ML-KEM-768 key of 1,184 bytes (`PQC_PUBKEY_BYTES`) with `setKey(x25519, pqcPub)`, and remove them with `clearKey()`. `keys(account)` returns `(x25519, pqcHash, updatedAt)`, `pqcKey(account)` the post-quantum key. Stewards seal data keys to these keys when a DAO reveals an asset to its members.

### PolicyBuilder

A pure contract: `build(Targets)` returns the default rules and spenders for a new DAO's policy, written for that DAO's own addresses and the protocol's. Reading `contracts/core/PolicyBuilder.sol` is the quickest way to see what every proposal kind may do out of the box; [Governance internals](governance-internals.md#the-115-default-rules) groups them.

### TokenLauncher and DAOToken

A DAO launches a token from a TOKEN_ISSUANCE proposal: the treasury calls `launch(LaunchParams)`, which deploys a `DAOToken` clone owned by the treasury and splits the entire supply, exactly, between the treasury, at most 256 named recipients, a member airdrop, and at most 8 pools in all: one-sided launch pools (the token against a virtual quote reserve that sets the starting price) and two-sided pools against assets the treasury holds or native L1. LP tokens go to the treasury or into a lock box. The launcher keeps nothing between calls. `launchedBy(token)` names the calling DAO; `tokensOf(dao)` lists its tokens.

`DAOToken` capabilities are chosen at launch and never added later: `FLAG_MINTABLE` 1 (the owner mints, up to `cap()` unless it is 0), `FLAG_BURNABLE` 2 (holders burn; the owner may always burn its own), `FLAG_PAUSABLE` 4 (the owner pauses transfers, but may still move its own). `renounceCapability` gives up minting or pausing for good (burning, once granted, stays); `setMetadata` (the logo pointer, hash and MIME type in TextStore), `transferOwnership` (one step, so a DAO can hand a token to another DAO in one proposal) and `renounceOwnership` (which also ends minting and pausing) complete it.

### MemberDistributor

Airdrops to the members of a share token. `createCampaign(shares, token, amount, mode, deadline, sweepTo)` funds a campaign (token `address(0)` for L1; anyone may create one for any share token). The snapshot is the executing proposal's snapshot when a DAO proposal creates the campaign, else the previous block. `MODE_EQUAL` (0) pays `total / memberCount` to each member and is refused for shares that were ever transferable; `MODE_PRO_RATA` (1) pays `total × units / totalUnits`. Anyone may `claim(id, member)` for a member, except that a contract claims only for itself (`ContractMustClaim`); members frozen at claim time cannot claim, and `sweep(id)` returns what is left to `sweepTo` after the deadline (at least `MIN_DURATION`, 1 day).

## DeSciFi

### DeSciFiFactory and DeSciFiPair

Constant-product pools in the Uniswap V2 lineage, written for COLLAB. Standard pools are unique per (token0, token1, fee) with fees of 10, 30 or 100 basis points; launch pools are one-sided, take 30 or 100, and any number may exist per pair of tokens. Two differences from Uniswap V2: swap fees are held aside and credited to LP holders through a fee-growth index (`pendingFees`, `collectFees(to)`), so liquidity can be locked or burned while its fees still flow; and launch pools price on reserve plus virtual reserve while paying out only real reserves. There are no flash swaps or callbacks, and no protocol fee. Fee-on-transfer and rebasing tokens are not supported. The factory is ownerless; `isPair(pool)`, `allPairsLength()` and `pairsPage(offset, limit)` list the pools, and it emits `PairCreated`.

### DeSciFiRouter

Swaps and liquidity with slippage limits and deadlines (`InsufficientOutput`, `ExcessiveInput`, `Expired`). Routes are explicit lists of pools, not token paths, so standard and launch pools for the same tokens can coexist; every pool must be the factory's (`UnknownPool`). The `…L1` entry points wrap and unwrap native L1 through wL1.

### LockFactory and LockBox

`createLock(pair, amount, owner, feeRecipient, unlockTime)` moves LP tokens into a new lock box. The owner withdraws only after `unlockTime`; `PERMANENT` (`type(uint64).max`) means never. The fee recipient is fixed at creation (the treasury, a collector or the burn address), and anyone may call `collectFees()`, which always pays it. The factory indexes boxes by owner, fee recipient and pair, with paged views (`ownerLockCount`/`ownerLockAt`, `feeLockCount`/`feeLockAt`).

### wL1

wL1 is GenesisL1's wrapped L1 (`deposit`, `withdraw`, WETH-style). Pools hold wL1, never native L1; the policy allows wrapping in TREASURY, TOKEN_ISSUANCE and DESCIFI proposals.

## The lenses

The lenses hold no state, can be redeployed freely and exist so a page loads in one or two `eth_call`s. Keep their pages small: a node caps every `eth_call` at its json-rpc gas cap, 25,000,000 by default.

### DAOLens

| View | Returns | Page size |
|---|---|---|
| `getDAOs(offset, limit, newestFirst)` | `DAOSummary[]` and the total | keep `limit` at 100 or less (200 of the longest summaries take 23.6M gas); the web reads 60, the prerender 50 |
| `directory(offset, limit)` | `DirectoryEntry[]` (id, treasury, profile, slug, title, logo and description hashes and lengths, creation and profile update times) in ascending ids, and the total | at most 200 (13.1M gas with the longest fields) |
| `getDAO(treasury)`, `getDAOBySlug(slug)` | `(DAOSummary, DAODetails)`; reverts `"not a DAO"` | one DAO |
| `getProposals(governor, fromId, count, viewer)`, `getProposal(governor, id, viewer)` | `ProposalSummary`, newest first, with the viewer's vote, preview weight and deposit | a few dozen |
| `getMembers(treasury, accounts)` | `MemberInfo[]` for the accounts you pass | your choice |
| `constitutionHistory(treasury)` | every constitution version | all |

### MarketLens

Token reads are gas-capped at 60,000 each and copy back only the words they decode, so a hostile token costs a page no more than its cap. Worst-case limits with hostile contracts: `getPools` at most 80 pools per call, `tokenInfo` at most 60 tokens, `modelsOf` and `cipnftsOf` `max` at most 50. The DeSciFi pages and the prerender ask for 40 pools or tokens per call, and the prerender halves a page a node refuses. `aggregate(calls)` forwards all gas to its targets: never put an untrusted token in a shared batch. `TokenInfo.launchedBy` is set only when the launcher's caller is a DAO of the factory, so no interface can label an account's own launch "issued by a COLLAB DAO".

## Libraries

- `Utf8`: `isValid`, `isText` (no C0 controls but tab and line feed) and their `check…` forms, which revert `InvalidText()`. Titles, descriptions, share names and lens strings go through it, because apps decode on-chain strings as UTF-8 and one malformed string must not break a page.
- `Png`: `header(bytes)` checks the signature, a 13-byte IHDR first (a valid bit depth for the colour type, compression and filter 0, interlace 0 or 1) and the standard IEND chunk at the end, then returns width and height; `InvalidPng()` otherwise. The image data is not decoded: display these bytes only as an image.
- `StoredBytes`: a byte string at a fixed base slot `s`: the length at `s`, the keccak256 at `s + 1`, data word `i` at `keccak256(s) + i`. [Data formats](data-formats.md#logos-and-descriptions) has the profile's two base slots.

## Main flows

### Creating a DAO

1. Choose a slug and check `factory.slugAvailable(slug)`; `predictTreasury(you, salt)` shows the treasury address.
2. Store the constitution in TextStore (`store`, or `storeChunk` then `commit`) and pass its pointer and hash.
3. Call `createDAO(p)`. Above about 25 KB of logo and description the node cannot estimate the gas: estimate the same call with an empty logo and description and add the formula in the [Gas reference](gas.md#the-gas-limit-formula).
4. Read the result from `DAOCreated`, or `daoBySlug(slug)` and `daoAt(id)`.

Slugs are first come, first served, and a pending creation shows its slug. If another creation takes it first, yours reverts `SlugTaken`, and GenesisL1 still charges half the gas limit it was sent with ([what a failure costs](gas.md#what-a-failure-costs)).

### Proposing, voting and executing

1. `submitProposal(kind, title, description, actions)` with at least `ceil(minDeposit × minInitialDepositBps / 10,000)` as value. The governor checks the proposer (not frozen, voting power at least `proposalThreshold`), the title and the size limits, asks `policy.validate(kind, actions)` (which decides critical), stores the description in TextStore, stores only `keccak256(abi.encode(actions))`, and emits the actions in full in `ProposalSubmitted`.
2. `deposit(id)` adds L1 during the deposit or voting period. When the total reaches `minDeposit`, `VotingStarted` is emitted.
3. `castVote(id, option)` with 1 Yes, 2 Abstain, 3 No or 4 NoWithVeto; a voter may change its vote until voting ends (`VoteCast(…, changed)`).
4. After the vote a passed proposal with actions is Queued for `executionDelay`, then Executable for `executionWindow`. Anyone may `execute(id, actions)`: the governor checks the actions' hash (`ActionsMismatch`), sets `executingProposal`, and the treasury runs them atomically.
5. Depositors `claimDeposit(id)`; a forfeited proposal is settled with `settlePenalty(id)`; a freeze proposal's locks are released with `releaseLocks(id, actions)`.

To execute, read the actions from the `ProposalSubmitted` log in the proposal's `createdBlock` (a one-block log query) and pass them back unchanged. [Governance internals](governance-internals.md#how-proposals-commit-to-their-actions) explains why.

### Editing the logo and description

- By proposal: a PROFILE proposal whose action calls `profile.setLogo(png)` or `profile.setDescription(text)` (standard rules by default).
- By a representative with `PERM_LOGO` or `PERM_DESCRIPTION`: a direct call to the profile, which puts the bytes into the chain's history once instead of three times (submit calldata, the `ProposalSubmitted` event, execute calldata).

Either way the new bytes replace the old ones in place, and `LogoChanged`/`DescriptionChanged` carry the new hash and length. Readers fetch the info, then the chunks, and check the hash; [Reading COLLAB from the chain](reading-the-chain.md#logos-and-descriptions) has the loop. Large writes need explicit gas limits ([Gas reference](gas.md)).

### Launching a token

A TOKEN_ISSUANCE proposal holds the approvals the launch spends (to the launcher only, the approved spender of that kind), optionally a wL1 deposit, and the `launch` call itself, which may carry L1 for pools quoted in native L1. The launcher first checks that the parts add up to the supply exactly (`AllocationMismatch`). Then, in the same transaction, it deploys the token, mints the supply, pays the allocations, creates the member campaign (only for the DAO's own share token, else `SharesNotOfDAO`), seeds the pools and locks their LP as asked, and finally checks that it holds nothing (`Residue`). It emits `TokenLaunched`, one `PoolSeeded` per pool, and the factory's `PairCreated`. Where treasury spends are critical, a launch is a critical vote.

## Events to index

| Event | Contract | When |
|---|---|---|
| `DAOCreated` | DAOFactory | a DAO is created (carries title and slug) |
| `ProposalSubmitted` | DAOGovernor | a proposal is submitted; carries its actions |
| `DepositAdded`, `VotingStarted`, `VoteCast`, `DelegateOverridden` | DAOGovernor | deposits and votes |
| `ProposalExecuted`, `ProposalCanceled`, `DepositRefunded`, `DepositPenalized` | DAOGovernor | outcomes |
| `ParamsUpdated`, `HoldingPeriodSet` | DAOGovernor | the rules change |
| `ActionExecuted`, `ModuleCall`, `HashSigned`, `HashRevoked` | DAOTreasury | what the DAO did |
| `UnitsChanged`, `DelegateChanged`, `Frozen`, `Unfrozen`, `ExcludedSet`, `TransfersEnabledSet` | ShareToken | membership and voting power |
| `LogoChanged`, `DescriptionChanged`, `LinksSet`, `ConstitutionAmended`, `Announcement`, `RepresentativesSet`, `RepresentativeRemoved` | DAOProfile | the profile changes |
| `RuleSet`, `RuleCleared`, `SpenderSet`, `CriticalKindsSet`, `Gl1fCollectionAdded`, `CipnftAdded` (and their removals) | DAOPolicy | the policy changes |
| `Stored`, `ChunkStored` | TextStore | a blob is stored |
| `TokenLaunched`, `PoolSeeded` | TokenLauncher | a token launch |
| `PairCreated`; `Swap`, `Mint`, `Burn`, `Sync`, `FeesCollected` | DeSciFiFactory; DeSciFiPair | pools and trading |
| `LockCreated`, `LockOwnerChanged` | LockFactory | LP locks |
| `CampaignCreated`, `Claimed`, `Swept` | MemberDistributor | airdrops |

Profile events carry hashes and lengths, never the bytes; read the bytes from storage.

## Errors you will meet

| Error | Contract | Cause |
|---|---|---|
| `InvalidSlug()`, `SlugTaken(uint256 id)` | DAOFactory | a malformed slug, or one another DAO holds |
| `ThresholdUnreachable()`, `NoFounders()`, `TooManyFounders()` | DAOFactory | founder shares that cannot propose, none, or more than 200 |
| `InvalidMetadata()` | ShareToken (bubbled up from createDAO) | a share name over 80 or a symbol over 32 bytes |
| `InvalidPng()`, `WrongLogoSize(uint256, uint256)`, `TooLarge()` | DAOProfile | a logo that is not a 128 × 128 PNG of at most 512 KB, or text over its limit |
| `InvalidText()` | Utf8, wherever text is checked | malformed UTF-8 or a forbidden control character |
| `ChunkTooLarge()` | DAOProfile | a chunk read of more than 16,384 bytes |
| `NotAllowed()`, `NotDAO()` | DAOProfile, DAOPolicy, others | a caller without the permission |
| `BelowProposalThreshold(power, required)`, `InitialDepositTooLow(sent, required)` | DAOGovernor | submitting without the voting power or deposit |
| `InvalidState(State current)` | DAOGovernor | the call does not fit the proposal's state |
| `ActionsMismatch()` | DAOGovernor | executing with actions other than those submitted |
| `ActionNotAllowed(index, target, selector)`, `ValueNotAllowed(index)`, `SpenderNotAllowed(index, spender)`, `InvalidTarget(index)` | DAOPolicy | a proposal that does more than its kind allows |
| `ActionFailed(index)` | DAOTreasury | an action failed without revert data |
| `AccountFrozen(address)`, `TransfersDisabled()`, `TransferLocked(address, until)` | ShareToken | a share transfer the rules refuse |
| `AllocationMismatch(allocated, totalSupply)`, `SharesNotOfDAO()`, `Residue()` | TokenLauncher | a launch whose parts do not add up |
| `InsufficientOutput(out, minOut)`, `Expired()`, `UnknownPool(pool)` | DeSciFiRouter | slippage, deadline or an unknown pool |

The web app explains reverts before the wallet opens: `explainError()` in `web/js/core/contracts.js` maps custom errors to plain titles.

## Related

- [Contract API reference](CONTRACT_API.md): every function, event and error.
- [Governance internals](governance-internals.md): the policy, critical kinds and default rules.
- [Reading COLLAB from the chain](reading-the-chain.md): `cast` examples for the lenses and the profile.
- [Gas reference](gas.md): measured costs and the gas-limit formula.
- [Architecture](ARCHITECTURE.md): how the contracts, the site and the server fit together.
