Architecture
This page is the architecture reference of COLLAB 2.2, for developers who need the whole picture before they change or review a part: the contracts on GenesisL1, the static web interface, the server kit that hosts it, and the tooling around them. COLLAB 2.2 is an alpha: its contracts and server kit had AI-assisted internal reviews and no external audit, and Security describes the model and its limits.
COLLAB has two halves: a set of Solidity contracts on GenesisL1 and a static web interface. Since 2.2 a COLLAB server has the admin service (an API for the operator only), the prerender job and the /data files. Users' reads still go straight to the chain, with no indexer or database. Everything the interface shows is read from the chain when it is shown, or from a file the server published and the browser checks against the chain, and every change is a transaction the user signs.
eth_call, eth_getLogs
browser ─────────────────────────────▶ GenesisL1 RPC
│ └── signs with ──▶ wallet ───────▶ GenesisL1
│
└── GET ──▶ COLLAB server (nginx): the release's files, /config.js, /data/, /og/
▲ ▲
│ writes config.js, site.json │ writes pages, /data/, /og/
admin service (/admin/api/, prerender job (every 10 minutes
the operator only) and on request; reads the chain)Contracts
One DAO = six owner-less clones
DAOFactory.createDAO deploys six EIP-1167 minimal clones and initializes them in the same
transaction, so a half-initialized DAO never exists. Clone addresses are deterministic (each
clone's CREATE2 salt is keccak256(abi.encode(creator, salt, role))), which lets the interface
show the treasury address before creation. Implementations lock their own initializer in their
constructor. The factory also gives every DAO a permanent, unique slug, its web address
/dao/<slug>, first come, first served, and keeps the registry (daoAt, idOf, daoBySlug,
slugOf).
The contracts are written for COLLAB under the MIT License and build on audited libraries:
OpenZeppelin Contracts Upgradeable 5.1 for initializers, the ERC-20 core of the share, DAO and LP
tokens, single-step ownership and reentrancy guards (all with ERC-7201 namespaced storage, which suits
clones), OpenZeppelin Contracts 5.1 for clones, checkpoints, Math.mulDiv, SafeCast and
SafeERC20, and Solady's SSTORE2 for TextStore. Rules that differ from the library defaults stay in
COLLAB's own code: share voting units and the DAO token's owner-exempt pause in each token's
_update hook, the DAO token's optional cap in its mint.
| Contract | Role |
|---|---|
DAOTreasury | The DAO's identity and vault. Holds L1, ERC-20, ERC-721, ERC-1155 (including CIPNFT and GL1F Model NFTs). Acts only through executeBatch, callable only by the governor, or executeFromModule for exact (target, selector) pairs that governance granted to a module (no value, never the treasury or the governor). Implements ERC-1271 with hashes approved by vote. Every governed contract authorizes msg.sender == treasury, so "the DAO did it" always means "a proposal passed". |
DAOGovernor | Proposal lifecycle: submission with an initial deposit, deposit period, voting, tally, timelock, permissionless execution within a window, deposit refunds or penalties. Stores keccak256(abi.encode(actions)); execution requires the exact same actions. |
ShareToken | The share token. Checkpointed voting units per block (balance for eligible holders: not frozen, not the treasury, not excluded), delegation with history, member count, optional non-transferability between members, freezes and provisional transfer locks. |
DAOPolicy | The call policy: which (kind, target, selector) each proposal kind may call, whether a call may carry value or makes the proposal critical, and which spenders approvals may name. It also keeps the DAO's integrations, the GL1F collections and CIPNFT contracts it works with, and gives each of their contracts a role. Defaults come from PolicyBuilder (115 rules); changes need a critical PARAMS proposal. |
DAOProfile | Title (fixed at creation), description and logo (kept in the profile's own storage, rewritten in place and read in chunks of at most 16 KB), links, announcements, the versioned constitution, and representatives with limited, expiring permissions. |
CipherVault | The DAO's seat in CIPNFT, across every CIPNFT contract the DAO lists: key stewards, key epochs (one DAO public key + sealed private-key envelopes), reveals to members, declassification and governance-approved purchases and sales completed by stewards, each naming its contract. |
Protocol singletons (shared by all DAOs)
TextStore: content-addressed on-chain storage (Solady SSTORE2 data contracts, up to 64 chunks of 24,000 bytes each, deduplicated by keccak256). Constitutions, announcements, proposal descriptions and token logos live here; a DAO's own logo and description live in its DAOProfile since 2.2. Every record that points to a blob also stores its hash, and the interface refuses to display content whose hash does not match.TokenLauncher+DAOToken: ERC-20 issuance by vote with exact allocation to the treasury, addresses, a member airdrop and liquidity pools; capabilities (mint up to a cap, burn, pause) are chosen at launch and never added later, and the DAO can give up minting and pausing.MemberDistributor: airdrops to members, equal or pro rata to shares at a snapshot, with a claim deadline and sweep. Campaigns are isolated from each other.- DeSciFi (
DeSciFiFactory,DeSciFiPair,DeSciFiRouter,LockFactory/LockBox): constant-product pools whose swap fees accrue to a separate fee reserve claimed by LP holders, one-sided launch pools with virtual quote reserves, a router with explicit pool paths and native L1 support through wL1, and LP locks with an immutable fee recipient. CollabKeyring: members' public encryption keys (X25519 and optionally ML-KEM-768) used for reveals.DAOLensandMarketLens: read-only aggregators, so a page loads in one or two calls. DAOLens returns DAO summaries and details by address or slug, the directory (slugs, titles and the hashes and lengths of logos and descriptions), proposals and members; MarketLens returns tokens, pools, locks, airdrops and NFTs, reading untrusted contracts with capped gas and bounded copies. Their pages are sized to stay below the 25,000,000 gas that nodes allow aneth_call.
The Contracts guide describes every contract and its events and errors, and Governance internals the policy.
Integrations: GL1F collections and CIPNFT contracts
COLLAB works with two families of GenesisL1 contracts it does not own: GL1F collections (a model registry, Model NFT, market and runtime that point at each other, such as GL1F and GL1F Crypto) and CIPNFT contracts for sealed data. The factory records the ones the deployer wired as its defaults, but they bind no DAO: each DAO's policy keeps its own lists, chosen at creation (the interface preselects the defaults) and changed only by critical PARAMS votes, so a DAO can add a collection or contract it built itself, or remove any. Adding a GL1F collection checks on chain that its four contracts point at each other, and refuses the DAO's own contracts, the protocol's contracts and anything already listed; each list holds at most eight entries.
Every listed contract gets a role (CIPNFT, GL1F registry, NFT, market or runtime). Default rules are
written for roles, not addresses: a GL1F proposal may call updateModelSettings on any registry the
DAO lists, a CIPNFT proposal may list or deliver on any CIPNFT contract it lists. An explicit rule
for one address still beats the role rule, which beats the wildcard. The treasury stance keys on roles
as well, so listing on any listed market or paying model revenue elsewhere is critical wherever
treasury spends are. The vault serves all listed CIPNFT contracts with one DAO key: activating a key
epoch checks the key registered on each of them, and the vault's module permissions exist per
contract. A contract that no longer answers can be unlisted and the key rotated in one Governance
rules proposal.
Removing an integration leaves the DAO's assets where they are. A contract the DAO no longer lists has no role, so only the generic safeguards (value, approvals, transfers, burns) cover calls to it. Revoking an approval needs no approved spender, so a treasury proposal can still revoke approvals there, per token or collection-wide, but cancelling a listing on a removed market takes a raw transaction; the interface asks the DAO to cancel its listings and offers before it removes a collection or contract.
A proposal from start to finish
- The proposer calls
submitProposal(kind, title, description, actions)with at least the initial deposit. The governor records two readings: the snapshot (the previous block) and the holding snapshot (holdingBlocksearlier, never before the DAO's creation). It checks the proposer's voting power, the lower of the two readings, against the proposal threshold. It asks the policy to validate the actions for that kind (which also returns whether the proposal is critical), stores the description in TextStore and copies the applicable rules into the proposal. The actions themselves are emitted in the event and only their hash is stored. - Anyone may add deposits. When the minimum is reached, voting starts; if the deposit period ends first, the proposal expires.
- Members vote Yes, Abstain, No or No-with-veto and may change their vote. Own units count at the lower of the two readings. A delegate votes with the units lent to it, again the lower of its two readings. A delegator who votes directly takes its own units back from whichever delegate held them at each reading, before or after the delegate votes, so the result does not depend on the order of votes.
- Tally: no quorum if votes are below the quorum share of total voting units at the snapshot; vetoed if No-with-veto exceeds the veto threshold of all votes; passed if Yes exceeds the threshold of non-abstain votes (or all non-abstain votes are Yes when the threshold is 100%) and, when the rules set a minimum Yes, Yes reaches that share of all voting units. Abstain counts towards quorum only, so without a minimum Yes a small Yes can pass when most voters abstain.
- A passed proposal with actions waits for the timelock, then anyone can execute it during the execution window by supplying the actions; the governor checks their hash and the treasury runs them atomically.
- Depositors reclaim deposits, or the deposits are forfeited to the treasury or the burn address when the DAO's flags say so (deposit never reached, no quorum, vetoed, or a freeze proposal that failed under the freeze-bond flag).
A membership proposal that freezes someone records its targets at submission and, when the DAO's flag is on, locks their shares from the moment voting starts, so a member cannot move shares to escape the vote. Each proposal holds its own lock. Locks expire on their own, and anyone can release them once the proposal can no longer be executed (it failed, was executed or its window closed). With the freeze-bond flag, a freeze proposal that is rejected or misses quorum forfeits its deposit, so every vote set up to fail costs its proposer.
Sealed data custody
A smart contract cannot keep a secret. A DAO's CIPNFT identity is therefore a keypair whose public half is registered on CIPNFT by vote and whose private half is held by key stewards elected by vote: for every key epoch a steward posts the private key sealed to each steward's registered key. To reveal an asset to members, the DAO votes, then a steward posts the asset's data key sealed to each eligible member's key; members verify it against the asset's on-chain commitment before use. Declassification publishes the data key on-chain, where the vault accepts it only if it matches the commitment. Envelopes are emitted in events; storage keeps their hash, block, time and author. A posted envelope can be replaced by its author; by any steward once the author has stopped being an active steward; or by any steward once the recipient changed their key after the post.
Purchases through the vault complete only against the delivery a steward verified, identified by its hashes, and only when the DAO's offer is sealed to the active key epoch. A purchase proposal therefore creates the offer first and approves the purchase second, and a key rotation between the approval and the delivery stops the purchase. Completing a purchase directly on CIPNFT skips these checks, so it is a critical vote in every DAO. Once the DAO has registered a post-quantum key on a contract, every later key epoch needs one too, since CIPNFT can't clear it.
Web interface
Static files, ES modules, no bundler, no framework. Each page is one HTML entry served at clean URLs by
js/core/router.js:Path Entry Page /index.htmlhome: the network of DAOs, featured DAOs, votes under way, tokens /daosdaos.htmldirectory with search and sorting /votesvotes.htmlopen proposals across every DAO /dao/<slug>[/<section>/…]dao.htmla DAO: overview, proposals, propose, treasury, members, tokens, liquidity, sealed data, models, constitution, settings. /dao/<address>/…still works and is replaced by the slug URL/createcreate.htmlcreation wizard /swap,/pools,/locksdescifi.htmlthe DeSciFi exchange /tokens,/tokens/<address>tokens.htmlevery token with a DeSciFi pool, and a page per token /admin[/<tab>]admin.htmlthe operator's admin: deploy, publish to this site, check, site settings, status. /deployredirects to/admin/deploy/docs,/docs/<slug>docs.html,docs/<slug>/index.htmlthe documentation, generated from docs/*.md/settings,/privacytheir own entries user settings, privacy Hosts map the paths to the entries with
web/_redirects(Netlify, Cloudflare Pages, IPFS gateways) or the nginx and Caddy configurations indeploy/; the server kit writes its own nginx site. Elsewhere404.htmlforwards to the right entry and the router restores the clean address. Version 1 links are redirected.Every entry carries the portal shell (alpha strip, sidebar with the network card, top bar with the global search, footer) as static markup written by
tools/shell_html.py, so the first paint shows the whole frame and the links are crawlable; the page module fills in what is live.config.jsis the only deployment-specific file: chain id, RPC endpoints, explorer, contract addresses, the deployment's integrations, optional analytics, whether COLLAB is deployed on this site and the public site settings, validated on load. Before the operator deploys, every page runs in an undeployed mode that says so and reads no contract. A user's settings (theme, RPC endpoint, explorer, display currency) stay in their browser.Reads go to the RPC endpoints with failover, are batched through the lens contracts and cached briefly; event history is fetched in block ranges of at most 10,000 blocks starting at each DAO's creation block. The endpoint's chain id is checked when it is chosen and every five minutes after.
On the site's own server, pages also read the files the prerender publishes, and check each one before use:
/data/price.json(the L1 price for the default display in US dollars, so no visitor's browser asks a price service unless they choose a currency),/data/tokens.json(holders, the 24-hour change and the 7-day line of each token; prices and liquidity are recomputed live), and the DAOs' logos and descriptions under/data/logos/and/data/texts/, named by the keccak-256 the chain reports and verified against it, with the chunked chain reads as the fallback.Writes go through the user's wallet (EIP-6963 discovery). Every transaction carries the chain id and is simulated with
estimateGasfirst, so failures are explained before the wallet opens. Writes too large for a node to estimate (large logos and descriptions) get an explicit gas limit from measured figures (Gas reference). Before a transaction to a DAO's contracts, the DAO's addresses are read again through the wallet's own network, so a dishonest RPC endpoint cannot redirect it.Proposal pages decode every action into plain language, verify the decoded actions against the on-chain hash, and can simulate them: once a proposal is ready to execute, an
eth_callofgovernor.executewith its actions, exactly the transaction anyone may send; before that, aneth_callof the treasury'sexecuteBatchfrom the governor.Untrusted content is never parsed as HTML: the DOM is built node by node, Markdown is parsed by markdown-it with raw HTML off and its tokens are turned into DOM nodes by COLLAB's own renderer, images from the chain are shown only as hash-verified
data:URLs, and a strict Content-Security-Policy forbids inline scripts, inline styles and third-party code. The one exception is Google Analytics: its script is allowed only on a site whose operator sets an ID, andjs/consent.jsloads it only after the visitor accepts.Encryption for sealed data runs in the browser (libsodium and ML-KEM-768 in WebAssembly); keys are derived from wallet signatures and kept in memory only.
A release versions every script, stylesheet, font and icon URL with one
?v=for the whole release (tools/version_assets.py, recorded inweb/BUILD.json) and gives every text file of 1 KB or more a.gzsibling. Servers keep the current release's?v=URLs for a year as immutable and revalidate pages,config.jsand the/data/*.jsonfiles on every load, so a new deployment or setting takes effect at once.
The web app is the reference for its modules and rules.
Server kit
server/ installs and runs a COLLAB site on a Debian or Ubuntu server (Installing COLLAB on a
server). The public site stays static: the server kit serves the files and keeps three
sets of them current: config.js, the prerender's output and the /data/ files.
server/install.shsets up nginx with HTTPS (Let's Encrypt certificates for every name whose DNS points at the server, the aliases answering 301 to the main host), the system users, the systemd units and thecollabhelper, and touches no other site on the server. A release is copied to/var/www/collab/releases/<version>/andcurrentpoints to it; what the services write lives in/var/www/collab/shared/, the settings and secrets in/etc/collab/collab.env.- The admin service (
server/admin/collab_admin.py, usercollab) listens on 127.0.0.1 behind nginx, which adds a secret header. It signs the operator in with the admin token and stores what the admin page sends: the deployment record andsite.json, from which it writesconfig.js(withtools/write_web_config.py). It then asks for a prerender, and for the policy step when the pages' Content-Security-Policy must change. Its API is in the Admin API reference. - The prerender job (
tools/prerender.pywithtools/seo/, usercollab-prerender, every 10 minutes and on request) reads the chain and writesshared/prerender/(a static page for every DAO and token, Open Graph images for every DAO, the entries with the site settings baked in, the sitemap,og/network.json) andshared/data/(the logos and descriptions by hash,tokens.json,price.json). nginx serves the prerendered pages before the release's own files for the page routes only, and never serves scripts from them. - The policy step (
collab apply, run by root through a path unit) writes the policy that config.js needs into the release's pages and COLLAB's nginx file, checks nginx and reloads it, and puts everything back when nginx refuses.
When the admin service or the prerender job is down, the site keeps working: pages read the chain and fall back from every missing file. Data formats describes each file and Static pages, data files and prices the job.
Tooling
Python only, no Node.js:
- the chain and the dev server:
tools/local_chain.py(a local chain with the GenesisL1 contracts COLLAB integrates with, the deployment and demo seeding),tools/dev_sandbox.py(isolated chain and server copies from a snapshot),tools/serve_local.py(the dev server with production's routes and headers); - the contracts' outputs:
tools/export_abis.py(abi/andweb/js/core/abi.js),tools/export_deploy_artifacts.py(the bundle the Deploy tab sends),tools/abi_summary.py(docs/CONTRACT_API.md),tools/build_externals.py(the upstream contracts inexternals/); - the site:
tools/write_web_config.py,tools/prerender.pyandtools/seo/,tools/shell_html.py,tools/version_assets.py,tools/merge_css.py,tools/make_assets.py,tools/make_demo_logos.py,tools/build_docs.pyandtools/docs_figures.py, andtools/lint_web.py; - the release:
tools/build_release.py(Building a release); - the tests:
tools/e2e/(Playwright scenarios with an injected test wallet) andserver/tests/(Testing); deploy/: nginx and Caddy configurations for hosting without the server kit,deploy/sync_csp.pyanddeploy/check_site.py, which checks a published site.
Related
- Developer overview: the repository and where to start.
- Contracts guide: every contract and the main flows.
- The web app: the app's modules and rules.
- Installing COLLAB on a server: the server kit from the operator's side.
- Security: the security model, the reviews and the known limitations.