AlphaCOLLAB runs on GenesisL1 mainnet and has had no external audit. Use small amounts.Security notes

COLLAB

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, generated from the ABIs. COLLAB 2.2 is an alpha: the contracts had AI-assisted internal reviews and no external audit.

Part
Developers
Version
COLLAB 2.2.0
Updated
Reading time
20 min

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.

#ContractKindRoleBytes
1TextStoresingletoncontent-addressed blobs (constitutions, announcements, proposal descriptions, token logos)3,462
2CollabKeyringsingletonmembers' public encryption keys1,548
3DeSciFiPairimplementationthe pool every DeSciFi pair clones11,460
4DeSciFiFactorysingletoncreates and registers pools3,980
5DeSciFiRoutersingletonswaps and liquidity over explicit pool paths8,953
6LockBoximplementationthe LP lock every lock box clones3,205
7LockFactorysingletoncreates and indexes lock boxes2,844
8MemberDistributorsingletonsnapshot airdrops to members5,419
9DAOTokenimplementationthe ERC-20 a DAO launches8,061
10TokenLaunchersingletonlaunches a DAO token with its allocations and pools7,589
11PolicyBuildersingletonthe default call policy of new DAOs6,144
12–17ShareToken, DAOGovernor, DAOTreasury, DAOPolicy, DAOProfile, CipherVaultimplementationsthe six contracts every DAO clones14,243 / 21,068 / 4,548 / 14,852 / 13,331 / 14,363
18DAOFactorysingletoncreates DAOs and keeps the registry and the slugs11,946
19DAOLenslensread-only pages of DAOs, proposals and members16,201
20MarketLenslensread-only pages of tokens, pools, locks, airdrops and NFTs16,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).

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), or by hand on a site without the server kit (Hosting without the server kit); 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), or read them yourself as Reading COLLAB from the chain 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.

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".

CloneWhat it holdsWho may change it
DAOTreasuryL1, ERC-20, ERC-721 and ERC-1155 assets (CIPNFT assets and GL1F Model NFTs included); ERC-1271 approved hashes; module permissionsthe governor (executeBatch), itself (setModulePermission, signHash, revokeHash inside a proposal), modules for exact (target, selector) pairs
DAOGovernorproposals, deposits, votes, the rulesanyone submits, deposits, votes with shares and executes; the DAO changes the rules
ShareTokenshares, voting units, delegation, freezes, exclusionsthe DAO mints, burns, freezes and configures; the governor places freeze locks; holders transfer and delegate
DAOPolicywhich calls each proposal kind may make, the critical kinds, the integrationsthe DAO, by critical PARAMS proposals
DAOProfilethe permanent title, the logo, the description, links, announcements, constitution versions, representativesthe DAO; representatives within their permissions
CipherVaultkey stewards, key epochs, reveals, declassifications, approved purchasesthe 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; the constants that bound them:

ConstantValue
MAX_ACTIONS32 per proposal
MAX_TITLE_BYTES, MAX_DESCRIPTION_BYTES200 and 65,536 (the description is stored in TextStore)
voting period10 minutes to 365 days
execution delay (timelock)at most 60 days
deposit period10 minutes to 365 days
execution window1 to 365 days
MAX_MIN_DEPOSIT1,000,000 L1
MAX_MIN_YES_BPS5,000 (50 % of the snapshot's voting units)
MAX_HOLDING_BLOCKS2,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 explains rules, precedence, homes, the treasury stance and the 115 default rules.

DAOProfile

FieldRuleErrors
title()set at creation, never changes: 1 to 80 bytes, well-formed UTF-8, no C0 control character and no DELInvalidTitle, InvalidText
logoempty, or a PNG of at most MAX_LOGO_BYTES (524,288) whose header passes Png.header and whose IHDR says 128 × 128TooLarge, InvalidPng, WrongLogoSize(width, height)
descriptionat 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-8TooLarge, InvalidText
announcementsa heading of 1 to 80 bytes and a TextStore blob of at most 131,072 bytes; emitted as Announcement, not storedInvalidTitle, InvalidBlob, TooLarge
constitutiona TextStore blob of at most 1,048,576 bytes; every version kept with the proposal that enacted itInvalidBlob, TooLarge
representativesat most 21, each with permission bits and an optional term endTooManyReps, 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 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 explains the protocol for members.

The factory

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

FieldMeaning
title, slugthe permanent name and web address
logo, descriptionbytes for the profile (empty allowed), checked as setLogo and setDescription check them
links, constitutionlinks text; the constitution's TextStore Blob {ptr, hash} (required)
shareName, shareSymbol, transfersEnabledthe share token
founders, founderShares, treasurySharesinitial holders: at most 200 founders (MAX_FOUNDERS), plus shares for the treasury
deposit, standardRules, criticalRulesDAOGovernor.DepositParams and two VoteRules
reps, repPerms, repTermEndsthe first representatives
stewardsthe first key stewards
extraCriticalKindsa bit mask of kinds to make critical on top of the mandatory ones
saltfor the clone addresses
gl1f, cipnftsthe 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:

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 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_calls. Keep their pages small: a node caps every eth_call at its json-rpc gas cap, 25,000,000 by default.

DAOLens

ViewReturnsPage size
getDAOs(offset, limit, newestFirst)DAOSummary[] and the totalkeep 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 totalat 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 deposita few dozen
getMembers(treasury, accounts)MemberInfo[] for the accounts you passyour choice
constitutionHistory(treasury)every constitution versionall

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 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.
  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).

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 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 has the loop. Large writes need explicit gas limits (Gas reference).

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

EventContractWhen
DAOCreatedDAOFactorya DAO is created (carries title and slug)
ProposalSubmittedDAOGovernora proposal is submitted; carries its actions
DepositAdded, VotingStarted, VoteCast, DelegateOverriddenDAOGovernordeposits and votes
ProposalExecuted, ProposalCanceled, DepositRefunded, DepositPenalizedDAOGovernoroutcomes
ParamsUpdated, HoldingPeriodSetDAOGovernorthe rules change
ActionExecuted, ModuleCall, HashSigned, HashRevokedDAOTreasurywhat the DAO did
UnitsChanged, DelegateChanged, Frozen, Unfrozen, ExcludedSet, TransfersEnabledSetShareTokenmembership and voting power
LogoChanged, DescriptionChanged, LinksSet, ConstitutionAmended, Announcement, RepresentativesSet, RepresentativeRemovedDAOProfilethe profile changes
RuleSet, RuleCleared, SpenderSet, CriticalKindsSet, Gl1fCollectionAdded, CipnftAdded (and their removals)DAOPolicythe policy changes
Stored, ChunkStoredTextStorea blob is stored
TokenLaunched, PoolSeededTokenLaunchera token launch
PairCreated; Swap, Mint, Burn, Sync, FeesCollectedDeSciFiFactory; DeSciFiPairpools and trading
LockCreated, LockOwnerChangedLockFactoryLP locks
CampaignCreated, Claimed, SweptMemberDistributorairdrops

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

Errors you will meet

ErrorContractCause
InvalidSlug(), SlugTaken(uint256 id)DAOFactorya malformed slug, or one another DAO holds
ThresholdUnreachable(), NoFounders(), TooManyFounders()DAOFactoryfounder 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()DAOProfilea logo that is not a 128 × 128 PNG of at most 512 KB, or text over its limit
InvalidText()Utf8, wherever text is checkedmalformed UTF-8 or a forbidden control character
ChunkTooLarge()DAOProfilea chunk read of more than 16,384 bytes
NotAllowed(), NotDAO()DAOProfile, DAOPolicy, othersa caller without the permission
BelowProposalThreshold(power, required), InitialDepositTooLow(sent, required)DAOGovernorsubmitting without the voting power or deposit
InvalidState(State current)DAOGovernorthe call does not fit the proposal's state
ActionsMismatch()DAOGovernorexecuting with actions other than those submitted
ActionNotAllowed(index, target, selector), ValueNotAllowed(index), SpenderNotAllowed(index, spender), InvalidTarget(index)DAOPolicya proposal that does more than its kind allows
ActionFailed(index)DAOTreasuryan action failed without revert data
AccountFrozen(address), TransfersDisabled(), TransferLocked(address, until)ShareTokena share transfer the rules refuse
AllocationMismatch(allocated, totalSupply), SharesNotOfDAO(), Residue()TokenLaunchera launch whose parts do not add up
InsufficientOutput(out, minOut), Expired(), UnknownPool(pool)DeSciFiRouterslippage, 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.