<!-- MIT License. Copyright (c) 2026 Decentralized Science Labs LLC. -->
<!-- summary: Read DAOs, slugs, profiles, proposals, pools and events straight from GenesisL1 with cast, within the nodes' gas and log limits. -->
# Reading COLLAB from the chain

Everything the site shows is public chain state, and you can read it without the site: for an indexer, a bot, a research script or a second interface. This page shows which views to call, how large a page each can safely be, how to read and verify a DAO's logo and description, and which events to follow, with examples in Foundry's `cast`. It is for developers; the [Contracts guide](contracts.md) explains what the contracts do.

## What you need

- A GenesisL1 JSON-RPC endpoint, chain id 29: the default presets are `https://rpc.genesisl1.org` and `https://gl1x.com/api/eth-rpc`.
- The deployment's addresses. A COLLAB site publishes them in its `/config.js` (`addresses`, and `startBlock`); the operator's record `deployments/29.json` has the same ones ([Data formats](data-formats.md#deployment-records)). Most reads need only `factory`, `daoLens` and `marketLens`; a DAO's own six contracts come from the factory.
- `cast` from Foundry, or any client with the ABIs in `abi/` (one JSON file per contract).

```bash
RPC=https://rpc.genesisl1.org
FACTORY=0x…   # addresses.factory
LENS=0x…      # addresses.daoLens
MARKET=0x…    # addresses.marketLens
```

The outputs below come from the local demo chain (`tools/local_chain.py up --seed`), so the addresses differ from GenesisL1's.

## Limits that shape every read

| Limit | Value | What to do |
|---|---|---|
| `eth_call` and `eth_estimateGas` gas cap | 25,000,000 by default on Evmos-based nodes such as GenesisL1's | keep lens pages small (below); a call over the cap fails, whatever the block gas limit |
| `eth_getLogs` block range | at most 10,000 blocks per request | query in ranges of 10,000 blocks |
| `eth_getLogs` results | at most 10,000 logs per answer | narrow the filter (address, topics) or the range; COLLAB's server tools halve a range a node refuses |
| first block | the deployment's `startBlock` | never ask for logs before it; for a DAO's contracts start at its `createdBlock` |

| View | Safe page | COLLAB uses |
|---|---|---|
| `DAOLens.getDAOs(offset, limit, newestFirst)` | at most 100 (200 summaries with the longest fields take 23.6M gas) | 60 in the web app, 50 in the prerender |
| `DAOLens.directory(offset, limit)` | at most 200 (13.1M gas with the longest fields, about 70k per DAO) | 200 in the web app, 100 in the prerender |
| `MarketLens.getPools(offset, limit, user)` | at most 80 pools if every token is hostile | 40 in the web app and in the prerender |
| `MarketLens.tokenInfo(tokens, owner, spender)` | at most 60 tokens if every token is hostile | 40 in the web app and in the prerender |
| `MarketLens.modelsOf`, `cipnftsOf` | `max` at most 50 | |
| `DAOProfile.logoChunk`, `descriptionChunk` | at most 16,384 bytes (about 1.2M gas each) | 16,384, four reads in flight |

## The factory: count, slugs and addresses

```bash
cast call $FACTORY 'VERSION()(string)' --rpc-url $RPC                  # "COLLAB DAO v2.2.0"
cast call $FACTORY 'daoCount()(uint256)' --rpc-url $RPC                # 5
cast call $FACTORY 'daoBySlug(string)(uint256)' open-proteomics-collective --rpc-url $RPC   # 1 (0 = no such DAO)
cast call $FACTORY 'slugAvailable(string)(bool)' my-new-lab --rpc-url $RPC                 # true: well formed and free
cast call $FACTORY 'slugOf(address)(string)' $TREASURY --rpc-url $RPC  # "" when the address is not a DAO
```

`daoAt(id)` (ids start at 1) returns the DAO's six addresses, its creator and when it was created:

```bash
cast call $FACTORY 'daoAt(uint256)((address,address,address,address,address,address,address,uint64,uint64))' 1 --rpc-url $RPC
# (treasury, governor, shares, profile, policy, vault, creator, createdAt, createdBlock)
```

`daoOf(treasury)` and `idOf(treasury)` go the other way (`idOf` is 0 for an address that is not a DAO). A slug resolves to the same DAO forever: slugs are permanent and unique, and never start with a digit. Titles are not unique; when a DAO vouches for something, show its slug.

## The DAO lens

For one DAO, `getDAO(treasury)` or `getDAOBySlug(slug)` returns `(DAOSummary, DAODetails)`, or reverts `"not a DAO"`. The full return types are long; build `cast`'s signature from the ABI file:

```bash
cat > sig.py <<'EOF'
import json, sys
def t(p):
    if p["type"].startswith("tuple"):
        return "(" + ",".join(t(c) for c in p["components"]) + ")" + p["type"][5:]
    return p["type"]
abi, name = json.load(open(sys.argv[1])), sys.argv[2]
f = next(x for x in abi if x.get("type") == "function" and x["name"] == name)
print(f"{name}({','.join(t(i) for i in f['inputs'])})({','.join(t(o) for o in f['outputs'])})")
EOF
cast call $LENS "$(python3 sig.py abi/DAOLens.json getDAOBySlug)" open-proteomics-collective --rpc-url $RPC
```

The summary starts with the id, the six addresses, the creator, `createdAt`, `createdBlock`, the title, the slug and the logo's hash and length; the details start with the description's hash and length. The field order is in the [Contract API reference](CONTRACT_API.md#daolens).

For lists, `directory(offset, limit)` is the light view: one `DirectoryEntry` per DAO in ascending ids, with what changes when a profile changes.

```bash
cast call $LENS 'directory(uint256,uint256)((uint256,address,address,string,string,bytes32,uint32,bytes32,uint32,uint64,uint64)[],uint256)' 0 2 --rpc-url $RPC
# [(1, 0x3189…, 0xeB37…, "open-proteomics-collective", "Open Proteomics Collective",
#   0x3666…e3f6, 4264, 0xecfe…a510, 279, 1793564960, 1793564960), (2, …)]
# 5
```

The fields are id, treasury, profile, slug, title, logo hash and length, description hash and length, `createdAt` and `profileUpdatedAt`; the second value is the total, so you page with `offset += page.length` until you reach it. `getDAOs` returns the heavier `DAOSummary` rows (share supply, members, treasury balance, proposal count) and can page newest first.

Proposals: `getProposals(governor, fromId, count, viewer)` pages newest first (`fromId` 0 = the latest), and `getProposal(governor, id, viewer)` returns one, with the viewer's vote, preview weight and deposit when `viewer` is not the zero address. `getMembers(treasury, accounts)` reads the accounts you pass; to find them, replay the share token's `UnitsChanged` logs.

## Logos and descriptions

A DAO's logo (a 128 × 128 PNG of at most 524,288 bytes) and description (UTF-8 of at most 262,144 bytes) live in its profile's storage. Read them in four steps:

1. `logoInfo()` or `descriptionInfo()` returns `(bytes32 hash, uint32 length)`: keccak-256 of the stored bytes and their length, `(0, 0)` when empty. The lens rows carry the same pair.
2. Read `logoChunk(offset, 16384)` (or `descriptionChunk`) for `offset = 0, 16384, …` below the length. A chunk returns the bytes in `[offset, min(offset + size, length))`, empty past the end; a size above 16,384 reverts `ChunkTooLarge()`.
3. Concatenate the chunks and check that keccak-256 of the result equals the hash and that its length matches.
4. On a mismatch an edit landed between your calls: read the info again and start over.

```bash
read HASH LEN < <(cast call $PROFILE 'logoInfo()(bytes32,uint32)' --rpc-url $RPC | tr '\n' ' ')
: > logo.hex
for ((off = 0; off < LEN; off += 16384)); do
  cast call $PROFILE 'logoChunk(uint256,uint256)(bytes)' $off 16384 --rpc-url $RPC | sed 's/^0x//' >> logo.hex
done
[ "$({ printf 0x; tr -d '\n' < logo.hex; } | cast keccak)" = "$HASH" ] && echo verified
python3 -c "import sys; sys.stdout.buffer.write(bytes.fromhex(open('logo.hex').read()))" > logo.png
```

The hex goes to `cast keccak` on standard input because a 512 KB logo is a megabyte of hex, more than one command-line argument may hold. Use `descriptionChunk(…)(string)` to see the text directly. The contract checks only the PNG's header (signature, IHDR, IEND), never its image data, so show the bytes only as an image, and treat the description as untrusted Markdown.

A COLLAB server also publishes verified copies by hash at `/data/logos/<hash>.png` and `/data/texts/<hash>.txt` (64 lowercase hex digits, no `0x`). They are a cache, not a source: check the hash of what you fetch, and fall back to the chunks ([Data formats](data-formats.md#logos-and-descriptions)).

## Constitutions, announcements and other texts

Constitutions, announcements, proposal descriptions and token logos are TextStore blobs: a pointer and a hash. `read(pointer)` returns the bytes, `hashOf(pointer)` and `lengthOf(pointer)` what was registered:

```bash
cast call $TEXTSTORE 'read(address)(string)' $PTR --rpc-url $RPC
cast call $TEXTSTORE 'read(address)(bytes)' $PTR --rpc-url $RPC | cast keccak    # must equal the record's hash
cast code $PTR --rpc-url $RPC    # 0x00 "CTS1" kind length payload: no gas cap
```

The record that points to a blob (a constitution version, a proposal, an announcement event) carries its hash too; compare it with keccak-256 of what you read before you show it. For large blobs read the manifest and its chunks with `eth_getCode`, which no gas cap limits.

## Pools and tokens

```bash
cast call $MARKET 'getPools(uint256,uint256,address)((address,address,address,string,string,uint8,uint8,uint16,bool,uint112,uint112,uint112,uint112,uint256,uint128,uint128,uint256,uint256,uint256)[],uint256)' 0 40 0x0000000000000000000000000000000000000000 --rpc-url $RPC
# [(pool, token0, token1, "USDC", "wL1", 6, 18, 30, false, reserve0, reserve1, virtual0, virtual1, lpSupply, feeReserve0, feeReserve1, 0, 0, 0), …]
```

A pool row has its tokens, symbols and decimals, the fee in basis points, whether it is a one-sided launch pool, its real and virtual reserves, the LP supply, the fees held for LPs, and, for a non-zero `user`, that account's LP and pending fees. `tokenInfo(tokens, owner, spender)` reads names, symbols, decimals and supply with gas-capped reads (`ok` is false for a token that does not answer like an ERC-20), and `launchedBy` names the issuing DAO only for tokens a factory DAO launched. Names and symbols come from the tokens themselves: clean them before you show them, and never treat a symbol as an identity. The DeSciFi factory lists every pool with `allPairsLength()` and `pairsPage(offset, limit)`.

Prices are not stored anywhere: COLLAB computes them from reserves, along the deepest path of pools to wL1. `tools/seo/tokens.py` does it in Python for the server's `/data/tokens.json`, with the same integer math as `web/js/descifi/data.js`.

## Events

`cast logs` takes the event's signature and, after it, values for its indexed arguments:

```bash
cast logs --from-block $START --address $FACTORY 'DAOCreated(uint256 indexed id, address indexed treasury, address indexed creator, address governor, address shares, address profile, address policy, address vault, string title, string slug)' --rpc-url $RPC

# a proposal's actions: the ProposalSubmitted log of proposal 2, in its createdBlock
cast logs --from-block 48 --to-block 48 --address $GOVERNOR 'ProposalSubmitted(uint256 indexed id, address indexed proposer, uint8 indexed kind, bool critical, string title, address descriptionPtr, bytes32 descriptionHash, (address,uint256,bytes)[] actions, uint48 snapshot, uint64 depositEnd)' 2 --rpc-url $RPC

# when a logo changed (no bytes in the event: read them from storage)
cast logs --from-block $START --address $PROFILE 'LogoChanged(bytes32,uint32)' --rpc-url $RPC
cast decode-event --sig 'LogoChanged(bytes32 hash, uint32 length)' $DATA    # DATA: a log's data field
# 0x3666541155b237c991f35160b8c49908bdfa8e4ee1b9c291742ec56bc24ee3f6
# 4264
```

On GenesisL1 each of these must stay within 10,000 blocks per request: loop over ranges from the start block to the head. The events worth following are listed in the [Contracts guide](contracts.md#events-to-index). Two habits keep an indexer honest:

- Read the actions of a proposal from its `ProposalSubmitted` log and check `keccak256(abi.encode(actions))` against the proposal's `actionsHash` before you show or execute them.
- Members are the accounts with voting units: replay `ShareToken.UnitsChanged(account, previousUnits, newUnits)` and confirm the result against `memberCount()`, as `tools/seo/network.py` does.

## Related

- [Contracts guide](contracts.md): what each contract does and its events and errors.
- [Contract API reference](CONTRACT_API.md): every signature.
- [Data formats](data-formats.md): the server's cache files and the deployment record.
- [Gas reference](gas.md): what reads and writes cost.
- [Architecture](ARCHITECTURE.md): how the site reads the chain.
