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.
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).
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.
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; 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 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 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:
| 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:
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
| 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 theircheck…forms, which revertInvalidText(). 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 slots: the length ats, the keccak256 ats + 1, data wordiatkeccak256(s) + i. Data formats has the profile's two base slots.
Main flows
Creating a DAO
- Choose a slug and check
factory.slugAvailable(slug);predictTreasury(you, salt)shows the treasury address. - Store the constitution in TextStore (
store, orstoreChunkthencommit) and pass its pointer and hash. - 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. - Read the result from
DAOCreated, ordaoBySlug(slug)anddaoAt(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
submitProposal(kind, title, description, actions)with at leastceil(minDeposit × minInitialDepositBps / 10,000)as value. The governor checks the proposer (not frozen, voting power at leastproposalThreshold), the title and the size limits, askspolicy.validate(kind, actions)(which decides critical), stores the description in TextStore, stores onlykeccak256(abi.encode(actions)), and emits the actions in full inProposalSubmitted.deposit(id)adds L1 during the deposit or voting period. When the total reachesminDeposit,VotingStartedis emitted.castVote(id, option)with 1 Yes, 2 Abstain, 3 No or 4 NoWithVeto; a voter may change its vote until voting ends (VoteCast(…, changed)).- After the vote a passed proposal with actions is Queued for
executionDelay, then Executable forexecutionWindow. Anyone mayexecute(id, actions): the governor checks the actions' hash (ActionsMismatch), setsexecutingProposal, and the treasury runs them atomically. - Depositors
claimDeposit(id); a forfeited proposal is settled withsettlePenalty(id); a freeze proposal's locks are released withreleaseLocks(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)orprofile.setDescription(text)(standard rules by default). - By a representative with
PERM_LOGOorPERM_DESCRIPTION: a direct call to the profile, which puts the bytes into the chain's history once instead of three times (submit calldata, theProposalSubmittedevent, 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
| 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: every function, event and error.
- Governance internals: the policy, critical kinds and default rules.
- Reading COLLAB from the chain:
castexamples for the lenses and the profile. - Gas reference: measured costs and the gas-limit formula.
- Architecture: how the contracts, the site and the server fit together.