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
| Severity | Count |
|---|---|
| Critical | 0 |
| High | 3 |
| Medium | 7 |
| Low / Informational | 9 |
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-01 —
ForumExecutor.disputeBountydoes not resethasVoted→ voters from a disputed round are permanently locked out of subsequent rounds. Fix: introducevoteRound[bountyId]counter and namespace votes by(bountyId, voteRound, voter). - H-02 — No allowlist of bounty token →
createBountyaccepts any ERC-20, so fee-on-transfer / rebasing / blacklist tokens brickBondingEscrowaccounting. Fix: restrict toChamberRegistry.getIdea(token).exists. - H-03 —
ForumExecutor.finalizeshort-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)).
Recommended remediation order
- H-02 (token whitelist on
createBounty) — single-line fix. - H-01 (
disputeBountyclearshasVotedvia voteRound counter). - H-03 (minimum quorum on
finalizeshort-circuit). - M-02 (
tokenAdmin = owner()notaddress(this)). - M-03 (move
src/mocks/→test/mocks/). - M-07 (CEI:
settlebeforesafeTransferin_approve/_reject). - M-01 (single
mulDivinclaim+ addrescueDust). - M-04 (deploy behind Safe multisig +
TimelockController). - M-06 (FeeRouter recipient blocklist resilience — pull-payment OR admin reconfigure).
- 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
markGraduatedis gated toonlyIdea || onlyFactory(ChamberRegistry.sol:127, testtest_MarkGraduated_RandomCallerRevertspasses). - Confirmed no
uncheckedblocks and no inlineassemblyanywhere insrc/. - Confirmed
BondingEscrowhas nounbondfunction — AGAINST stake = voting power = locked until settlement. - Confirmed
Deploy.s.sol(mainnet) uses real Clanker atvm.envAddress("CLANKER_FACTORY"), not MockClanker. - Confirmed OpenZeppelin version is 5.6.1 (current) and
SafeERC20is used at every token-transfer site. - Confirmed
Math.mulDivis used inBondingEscrowto 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):
- Math precision proofs on
BondingEscrow.claimpayout, including:totalForandtotalAgainstfrom 1 wei up totype(uint128).max - 1.- Single-winner / single-loser / many-winners / one-against / one-for matrix.
protocolSlashBps ∈ {0, 1, 100, 1000, 3000}boundary cases.
- Clanker v4 integration semantics — verify
clanker.deployToken{value: msg.value}produces the expectedtokenAddressdeterministically given salt; confirmtokenAdminpost-deploy behavior on Base mainnet. - Front-running of
finalize— can an MEV bot observesubmitBounty(prId)in mempool and sandwichbondAgainst+vote(approve)+finalizein one bundle to capture the bounty for a colluding agent? (H-03 reduces the surface but does not eliminate it.) - Cross-bounty griefing on a shared idea-token — all bounties on the same idea-token
transit through the same
BondingEscrowandFeeRouter. Can adversarial activity on bounty A starve bounty B (gas, balance accounting, dust)? - 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.markGraduatedaccess control (rejects every caller except idea token + factory)BondingEscrowbond → settle → claim happy pathsForumExecutorcreate → claim → submit → vote → finalize lifecycleFeeRouter.flush6-way split correctnessIdeaFactory.deployIdeaagainstMockClanker(Sepolia) and real Clanker (anvil fork)
Gaps surfaced by the internal audit (to land before external audit):
dispute → re-claim → re-voteintegration test (catches H-01).- Fuzz
claimwith M ∈ [2, 100] winners (catches M-01 dust accumulation at scale). - Bad-actor token suite:
FeeOnTransferMockERC20+BlacklistMockERC20(catches H-02). - Flash-bond attack on
finalize(catches H-03). - Reentrant idea-token tests via
ReentrantMockERC20.transfer. getGraduatedIdeasgas-DoS at 1k / 10k ideas.FeeRouter.flushblocked-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.refundedexactly.