<!-- MIT License. Copyright (c) 2026 Decentralized Science Labs LLC. -->
<!-- summary: How COLLAB is built: the contracts on GenesisL1, the static web app, the server kit with its admin service and prerender job, and the tooling. -->
# 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](SECURITY.md) 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.

```text
                 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.
- `DAOLens` and `MarketLens`: 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 an `eth_call`.

The [Contracts guide](contracts.md) describes every contract and its events and errors, and
[Governance internals](governance-internals.md) 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

1. 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 (`holdingBlocks` earlier, 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.
2. Anyone may add deposits. When the minimum is reached, voting starts; if the deposit period ends
   first, the proposal expires.
3. 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.
4. 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.
5. 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.
6. 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.html` | home: the network of DAOs, featured DAOs, votes under way, tokens |
  | `/daos` | `daos.html` | directory with search and sorting |
  | `/votes` | `votes.html` | open proposals across every DAO |
  | `/dao/<slug>[/<section>/…]` | `dao.html` | a DAO: overview, proposals, propose, treasury, members, tokens, liquidity, sealed data, models, constitution, settings. `/dao/<address>/…` still works and is replaced by the slug URL |
  | `/create` | `create.html` | creation wizard |
  | `/swap`, `/pools`, `/locks` | `descifi.html` | the DeSciFi exchange |
  | `/tokens`, `/tokens/<address>` | `tokens.html` | every token with a DeSciFi pool, and a page per token |
  | `/admin[/<tab>]` | `admin.html` | the operator's admin: deploy, publish to this site, check, site settings, status. `/deploy` redirects to `/admin/deploy` |
  | `/docs`, `/docs/<slug>` | `docs.html`, `docs/<slug>/index.html` | the documentation, generated from `docs/*.md` |
  | `/settings`, `/privacy` | their 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 in `deploy/`; the server kit writes its own nginx
  site. Elsewhere `404.html` forwards 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.js` is 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 `estimateGas` first, 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](gas.md)). 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_call` of
  `governor.execute` with its actions, exactly the transaction anyone may send; before that, an
  `eth_call` of the treasury's `executeBatch` from 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,
  and `js/consent.js` loads 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 in `web/BUILD.json`) and gives every text file of
  1 KB or more a `.gz` sibling. Servers keep the current release's `?v=` URLs for a year as immutable
  and revalidate pages, `config.js` and the `/data/*.json` files on every load, so a new deployment
  or setting takes effect at once.

[The web app](../web/DEVELOPING.md) 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](INSTALL.md)). 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.sh` sets 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 the `collab` helper, and touches no other site on the server. A release is copied to
  `/var/www/collab/releases/<version>/` and `current` points 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`, user `collab`) 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 and `site.json`, from which it writes `config.js`
  (with `tools/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](admin-api.md).
- The prerender job (`tools/prerender.py` with `tools/seo/`, user `collab-prerender`, every 10
  minutes and on request) reads the chain and writes `shared/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`) and `shared/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](data-formats.md) describes each file and
[Static pages, data files and prices](prerender.md) 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/` and `web/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 in `externals/`);
- the site: `tools/write_web_config.py`, `tools/prerender.py` and `tools/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.py` and `tools/docs_figures.py`, and `tools/lint_web.py`;
- the release: `tools/build_release.py` ([Building a release](release-build.md));
- the tests: `tools/e2e/` (Playwright scenarios with an injected test wallet) and `server/tests/`
  ([Testing](testing.md));
- `deploy/`: nginx and Caddy configurations for hosting without the server kit, `deploy/sync_csp.py`
  and `deploy/check_site.py`, which checks a published site.

## Related

- [Developer overview](developers.md): the repository and where to start.
- [Contracts guide](contracts.md): every contract and the main flows.
- [The web app](../web/DEVELOPING.md): the app's modules and rules.
- [Installing COLLAB on a server](INSTALL.md): the server kit from the operator's side.
- [Security](SECURITY.md): the security model, the reviews and the known limitations.
