DAO V1.1 Reference Auditor: An Independent Assessment

In the public testing thread, a tool satisfying the following 9 requirements was proposed as a compromise path:

This assessment evaluates the released repo against that nine-point standard.

Scope

This is a repo-state audit of the released CCF-DAO1-1/ccfdao-vote-auditor-rfc repository.

It is not an audit of:

  • the whole DAO v1.1 stack across other repositories,
  • a hypothetical future docker-compose,
  • or a future self-hosted stack that does not exist in this repo yet.

The narrower question is: does the released reference implementation, as published, satisfy those 9 requirements?

Release status: Earlier public discussion described the fallback path as community-led in development, an internal version, or absent as a standalone audit tool. The later compromise thread led to this released reference implementation. Publishing it is a step forward for transparency, and this assessment covers this released state.

Verdict table

The table below is the short version. The sections that follow carry the detailed evidence behind each negative or partial verdict.

Requirement Verdict Why
1. Running locally Partial Local CLI, but remote data path remains. See section 1.
2. Easy to set-up No User must provision three indexers. See section 2.
3. Depending exclusively on on-chain data (via local full node) No Still uses public clients and external indexers. See section 1.
4. Not depending on operator controlled services No Still depends on external indexers. See section 1.
5. Allowing Whitelist generation Partial Internal rebuild exists, but it is only logged, not saved as an output artifact. See section 3.
6. Allowing Whitelist audit Partial Root recomputation exists, but mismatch is non-fatal. See section 4.
7. Allowing Whitelist proof of inclusion generation Partial Proof code exists, but no main CLI workflow. See section 3.
8. Allowing Voting No This repo provides no qualifying independent fallback voting path. See section 5.
9. Allow Vote auditing Partial Vote auditing exists, but key gaps remain. See sections 1 and 6.

Findings

The sections below follow the table order: dependencies and setup first, then whitelist workflows, then voting and tally behavior.

1. The released tool still depends on public clients and indexers

Requirements 3 and 4 fail for the same reason: the released tool still depends on remote RPC and indexer infrastructure.

Indexer inputs: The CLI expects three separate indexer endpoints, either through CLI options or the corresponding environment-variable lookup:

  • --did-indexer
  • --address-bind-indexer
  • --dao-indexer

RPC path: The auditor instantiates the public mainnet client or public testnet client directly at the auditor call site instead of exposing a caller-provided local RPC endpoint in the CLI surface:

  • new ccc.ClientPublicMainnet()
  • new ccc.ClientPublicTestnet()

Fetch path: The actual whitelist rebuild path then fetches data from those remote services:

Docs: The basic usage example documents the same external dependency model, the environment-variable section repeats those endpoints, and the notes section explicitly says:

Current implementation depends on 3 indexing services.

2. Setup still leaves indexer provisioning to the user

Requirement 2 fails because setup still assumes indexer infrastructure outside this repo.

Docs: The documented workflow expects the user to provide:

Repo contents: The project structure section documents the auditor CLI and SMT tool, but no bundled indexer stack or self-contained setup path for those dependencies.

A bundled docker-compose, a self-hostable DAO UI, or an equivalent self-hostable voting path would change this section’s result only if it covered the dependent indexers and a usable voting path as well. Neither the released repo tree nor the documented project structure shows such a stack.

3. Whitelist generation and proof generation remain partial user-facing workflows

Requirements 5 and 7 are only partial: the repo contains the underlying logic, but the whitelist is only logged to stdout and proof generation is not exposed as a full end-user workflow.

Whitelist generation: The repo does rebuild a voter list internally:

The code logs voter map as part of the normal output stream, which is useful, but the repo’s own documented output files list vote CSVs and final results, not a saved whitelist artifact.

Proof generation: SMT proof generation also exists in code:

But this is not wired into the main CLI as a clear end-user workflow.

4. Whitelist audit exists, but whitelist failure is not fatal

Requirement 6 is only partial: the tool recomputes the whitelist root, but a mismatch does not stop the run.

5. The released repo does not provide an independent fallback voting path

Requirement 8 fails in this repo state: the released repo is an auditor, not a voting client.

The CLI exposes a single command:

  • audit

This is the entire CLI surface.

There is no command for:

  • building a vote transaction,
  • generating a vote witness for submission,
  • or submitting a vote.

A voting UI exists elsewhere in the DAO v1.1 stack, but it is outside this repo and does not solve the same dependency problems identified in this assessment. That leaves this released repo without a qualifying independent fallback voting path.

6. Final vote weight calculation diverges from the documented DAO v1.1 flow

Requirement 9 is only partial for two reasons. One is the external-service dependency covered in section 1. The other is the end-height weight calculation, which still diverges from the documented DAO v1.1 flow in three places.

Self-exclusion bug

The end-height tally drops the voter’s own address whenever it appears in validVotes.

In the final tally stage, the auditor builds the end-height bind map from validVotes.keys(), queries DAO stake for the voting address plus its bound addresses in the end-height stake pass, then sums returned values only when !validVotes.has(addr).

Because every voteAddress in that second loop originated from validVotes, any stake returned for the voter’s own address is filtered out before accumulation. On successful responses, the current open-source DAO indexer’s /dao-stake-set handler iterates every requested address in ckb_list and inserts that address into the response map, so the queried voteAddress reaches daoResult before the auditor drops it.

This is not just a corner case. When a valid voter has no bound addresses, ckbList = [voteAddress, ...bindAddresses] collapses to the voter’s own address, so any successful end-height stake lookup becomes 0 after the self-address filter.

The bundled sample is consistent with that bug, but it does not isolate it. The only valid vote in c748442f-votes-valid.csv comes from the same address that has 20000000000000 in the earlier whitelist stake pass, but that whitelist pass runs at update height 20550000 while the final tally re-queries stake at end height 20630434. The later end-height stake pass logs 0 for that address, and the final c748442f-vote-results.csv plus console output both report 0 CKB, but those sample artifacts alone cannot distinguish a real stake change from the self-address filter. The static code path above is the direct proof.

Non-empty binding mismatch

The documented and open-source binding path returns objects, but the auditor treats them as strings.

The documented and open-source address-bind path returns /by_to_at_height response elements as {from, height, tx_index} objects, and the docs’ example response shows the same shape: [{"height":18861258, "tx_index":1, "from":"..."}]. The shipped app_view code extracts each record’s from before querying DAO stake.

The auditor instead types data as string[] and again as string[], then posts [voteAddress, ...bindAddresses] directly as ckb_list. The DAO indexer expects ckb_list: Vec<String>, so a non-empty binding result from the released service would be forwarded as objects unless the auditor first extracted each record’s from field. The bundled sample does not exercise this path because the only valid vote’s bind lookup reports 0 bind addresses.

pw lock omission

The released auditor omits pw lock weight even though the documented and shipped tally path includes it.

The docs say the end-height weight calculation includes bound addresses and pw lock. The shipped app_view get_weight() adds pw_lock_addr when present. The auditor has no corresponding step, and its own notes say: Current implementation does not handle pw lock automatic association.

The released auditor does not faithfully implement the documented tally flow. The docs say to aggregate stake to the voting address and then exclude only another address’s weight when that other address also voted. The shipped app_view tally code follows that narrower rule: get_weight() includes the queried address itself, and the final overlap filter preserves voter_ckb_addr == weight_addr.

This directly weakens requirement 9.

What the released repo does provide

The release still provides:

These pieces let a reader parse VoteMeta, rebuild whitelist state, recompute the SMT root, collect votes, and inspect CSV outputs. They do not make this repo the locally runnable, operator-independent fallback stack described by the 9 requirements.

Bottom line

Even with those capabilities, the released reference auditor does not yet satisfy the 9-requirement compromise path.

The released repo still lacks:

A bundled docker-compose, a self-hostable DAO UI, or another self-hostable voting path would change this assessment only if it covered the dependent indexers and a usable voting path.

Good step forward, Phroi

4 Likes

I never thought id say this, because I love the open public discussions.

But maybe this should be taken behind closed doors so you and David can work through the details before giving us a final verdict on whether you’ve reached agreement or not.

In my opinion this is bordering on public disrespect almost in a teacher demeaning a student in front of the class situation.

I’m not saying you are doing this deliberately and I’m definitely not saying that this is the case as far as your and David’s skills and experience, but I just don’t think this is going to play out well publicly.

Of course if David wants to do it this way then go for it.

1 Like

This is just a reference implementation to prove that the voting results in the current dao1.1 technical solution can indeed be independently verified.

Many of the trivial issues you mentioned are easy to fix. I think we can first discuss the most core issue —— the need to rely on three indexers.

  1. Community members can run indexers themselves. They don’t need to run them continuously; they just need to turn them on when verification is required, though they will need to wait a bit for synchronization. The experience is similar to using the Neuron wallet.

  2. The indexer code is very simple, easy to inspect, or even re-implement with AI assistance without much difficulty.

  3. So far, no alternative to off-chain indexers has been seen. Someone suggested MMR technology, but upon careful consideration, it becomes clear that this technology is not applicable in this scenario.

4 Likes

Yes, indeed! A docker compose configuration would fix most of them.

Looking forward to these fixes, Phroi

2 Likes