Skip to Content
Quorum contracts are live on Base Sepolia. Mainnet ships after external audit. Do not send real funds.

Audit

Internal audit — 2026-05-18

Auditor: Claude (Opus 4.7), internal review. Scope: packages/contracts/src/** (5 contracts + interfaces + MockClanker). Status: READ-ONLY — no contract source modified.

Full report: our internal security audit notes.

Severity counts

SeverityCount
Critical0
High3
Medium7
Low / Informational9

No critical findings. The most adversarial paths analyzed (reentrancy via malicious idea-token transfer, Math.mulDiv overflow at uint128 max, markGraduated impersonation, settlement front-run, payout drain) all resolve to either no exploit or HIGH-severity-only impact (DoS / dust lock, not theft).

High findings (must fix before mainnet)

  • H-01ForumExecutor.disputeBounty does not reset hasVoted → voters from a disputed round are permanently locked out of subsequent rounds. Fix: introduce voteRound[bountyId] counter and namespace votes by (bountyId, voteRound, voter).
  • H-02 — No allowlist of bounty token → createBounty accepts any ERC-20, so fee-on-transfer / rebasing / blacklist tokens brick BondingEscrow accounting. Fix: restrict to ChamberRegistry.getIdea(token).exists.
  • H-03ForumExecutor.finalize short-circuit on 1 wei of AGAINST stake → a single bonder with dust can deterministically decide every bounty pre-deadline. Fix: minimum quorum threshold (e.g. max(bounty.amount / 100, 1 ether)).
  1. H-02 (token whitelist on createBounty) — single-line fix.
  2. H-01 (disputeBounty clears hasVoted via voteRound counter).
  3. H-03 (minimum quorum on finalize short-circuit).
  4. M-02 (tokenAdmin = owner() not address(this)).
  5. M-03 (move src/mocks/test/mocks/).
  6. M-07 (CEI: settle before safeTransfer in _approve / _reject).
  7. M-01 (single mulDiv in claim + add rescueDust).
  8. M-04 (deploy behind Safe multisig + TimelockController).
  9. M-06 (FeeRouter recipient blocklist resilience — pull-payment OR admin reconfigure).
  10. L-* / docs.

What was reviewed

src/ChamberRegistry.sol 183 LOC src/FeeRouter.sol 131 LOC src/BondingEscrow.sol 196 LOC src/ForumExecutor.sol 357 LOC src/IdeaFactory.sol 302 LOC src/mocks/MockClanker.sol 52 LOC (Sepolia-only) src/interfaces/IBondingEscrow.sol src/interfaces/IChamberRegistry.sol src/interfaces/IClanker.sol src/interfaces/IFeeRouter.sol src/interfaces/IGitlawbDID.sol test/*.t.sol full suite (6 test files) script/Deploy.s.sol mainnet deploy script/DeploySepolia.s.sol Sepolia deploy (uses MockClanker, correctly)

Verifications performed

  • Re-asserted markGraduated is gated to onlyIdea || onlyFactory (ChamberRegistry.sol:127, test test_MarkGraduated_RandomCallerReverts passes).
  • Confirmed no unchecked blocks and no inline assembly anywhere in src/.
  • Confirmed BondingEscrow has no unbond function — AGAINST stake = voting power = locked until settlement.
  • Confirmed Deploy.s.sol (mainnet) uses real Clanker at vm.envAddress("CLANKER_FACTORY"), not MockClanker.
  • Confirmed OpenZeppelin version is 5.6.1 (current) and SafeERC20 is used at every token-transfer site.
  • Confirmed Math.mulDiv is used in BondingEscrow to handle 512-bit intermediates (prevents uint256 overflow at uint128 boundary inputs).

External audit — pending

Mainnet deployment is gated on an external audit. Required scope (per the internal review):

  1. Math precision proofs on BondingEscrow.claim payout, including:
    • totalFor and totalAgainst from 1 wei up to type(uint128).max - 1.
    • Single-winner / single-loser / many-winners / one-against / one-for matrix.
    • protocolSlashBps ∈ {0, 1, 100, 1000, 3000} boundary cases.
  2. Clanker v4 integration semantics — verify clanker.deployToken{value: msg.value} produces the expected tokenAddress deterministically given salt; confirm tokenAdmin post-deploy behavior on Base mainnet.
  3. Front-running of finalize — can an MEV bot observe submitBounty(prId) in mempool and sandwich bondAgainst + vote(approve) + finalize in one bundle to capture the bounty for a colluding agent? (H-03 reduces the surface but does not eliminate it.)
  4. Cross-bounty griefing on a shared idea-token — all bounties on the same idea-token transit through the same BondingEscrow and FeeRouter. Can adversarial activity on bounty A starve bounty B (gas, balance accounting, dust)?
  5. Replay protection on dispute and storage packing of BountyState.

A list of recommended auditors (in alphabetical order): Cantina, Code4rena, OpenZeppelin, Spearbit, Trail of Bits. Engagement gate: a remediation pass on all HIGH findings landing first.

Bug bounty programs (Immunefi or equivalent) launch concurrently with the external audit. See Bug Bounty.

Test coverage

The existing Foundry suite (packages/contracts/test/*.t.sol) covers:

  • ChamberRegistry.markGraduated access control (rejects every caller except idea token + factory)
  • BondingEscrow bond → settle → claim happy paths
  • ForumExecutor create → claim → submit → vote → finalize lifecycle
  • FeeRouter.flush 6-way split correctness
  • IdeaFactory.deployIdea against MockClanker (Sepolia) and real Clanker (anvil fork)

Gaps surfaced by the internal audit (to land before external audit):

  1. dispute → re-claim → re-vote integration test (catches H-01).
  2. Fuzz claim with M ∈ [2, 100] winners (catches M-01 dust accumulation at scale).
  3. Bad-actor token suite: FeeOnTransferMockERC20 + BlacklistMockERC20 (catches H-02).
  4. Flash-bond attack on finalize (catches H-03).
  5. Reentrant idea-token tests via ReentrantMockERC20.transfer.
  6. getGraduatedIdeas gas-DoS at 1k / 10k ideas.
  7. FeeRouter.flush blocked-recipient test.

CI invariants to add (Foundry invariant tests):

  • INV-1 (BondingEscrow): For every settled bounty, Σ payouts_to_winners + protocolCut + dust == loserPool + Σ winner.stake (conservation modulo bounded dust).
  • INV-2 (FeeRouter): For every flushed idea, Σ recipient.balanceOf == prior router balance (full distribution, no funds lost).
  • INV-3 (ForumExecutor): For every bounty in a terminal state, bounty.amount == claimant.received + treasury.received + creator.refunded exactly.
Last updated on