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

FeeRouter

FeeRouter.sol is the immutable per-idea fee router. 131 LOC, Solidity 0.8.26. Trading fees from the Uniswap V4 pool flow through Clanker’s reward distribution into FeeRouter, which splits the balance across six recipients per the snapshotted BPS config.

See Protocol · Ideas for the BPS rationale (DECISIONS.md #005 ). This page is the contract-level reference.

Surface

function configureIdea( address ideaToken, address protocol, address creator, address winnersSplitter, address forPool, address againstPool, address executorPool, uint16 protocolBps, uint16 creatorBps, uint16 winnersBps, uint16 forBps, uint16 againstBps, uint16 executorBps ) external onlyFactory; function flush(address ideaToken) external nonReentrant; function getConfig(address ideaToken) external view returns (Config memory); // Owner-gated function setFactory(address) external onlyOwner;

Storage

struct Config { address protocol; address creator; address winnersSplitter; address forPool; address againstPool; address executorPool; uint16 protocolBps; uint16 creatorBps; uint16 winnersBps; uint16 forBps; uint16 againstBps; uint16 executorBps; bool configured; } mapping(address => Config) private _ideaConfig; address public factory;

The bps fields must sum to exactly 10000. Validation:

uint256 total = uint256(protocolBps) + creatorBps + winnersBps + forBps + againstBps + executorBps; if (total != 10_000) revert BpsMismatch();

configureIdea

Only IdeaFactory can configure. The configuration is one-shot: subsequent calls revert.

function configureIdea(...) external { if (msg.sender != factory) revert NotFactory(); Config storage c = _ideaConfig[ideaToken]; if (c.configured) revert AlreadyConfigured(); // ...validate addresses, bps sum... c.protocol = protocol; c.creator = creator; c.winnersSplitter = winnersSplitter; c.forPool = forPool; c.againstPool = againstPool; c.executorPool = executorPool; c.protocolBps = protocolBps; c.creatorBps = creatorBps; c.winnersBps = winnersBps; c.forBps = forBps; c.againstBps = againstBps; c.executorBps = executorBps; c.configured = true; emit IdeaConfigured(ideaToken, protocol, creator, ...); }

flush

Anyone can flush. Distributes the router’s full balance to the six recipients per BPS, with the last (executorPool) taking the remainder so rounding favors the executor.

function flush(address ideaToken) external nonReentrant { Config memory c = _ideaConfig[ideaToken]; if (!c.configured) revert NotConfigured(); uint256 bal = IERC20(ideaToken).balanceOf(address(this)); if (bal == 0) return; uint256 protocolAmt = (bal * c.protocolBps) / 10_000; uint256 creatorAmt = (bal * c.creatorBps) / 10_000; uint256 winnersAmt = (bal * c.winnersBps) / 10_000; uint256 forAmt = (bal * c.forBps) / 10_000; uint256 againstAmt = (bal * c.againstBps) / 10_000; if (protocolAmt > 0) IERC20(ideaToken).safeTransfer(c.protocol, protocolAmt); if (creatorAmt > 0) IERC20(ideaToken).safeTransfer(c.creator, creatorAmt); if (winnersAmt > 0) IERC20(ideaToken).safeTransfer(c.winnersSplitter, winnersAmt); if (forAmt > 0) IERC20(ideaToken).safeTransfer(c.forPool, forAmt); if (againstAmt > 0) IERC20(ideaToken).safeTransfer(c.againstPool, againstAmt); // remainder → executorPool, absorbs rounding uint256 remainder = IERC20(ideaToken).balanceOf(address(this)); if (remainder > 0) IERC20(ideaToken).safeTransfer(c.executorPool, remainder); emit Flushed(ideaToken, bal); }

Audit M-06: if any single recipient blocks the transfer (token blacklist, reverting contract, gas-griefing receiver), the entire flush reverts and fees accumulate on the router. Pre-mainnet fix options:

  1. Pull-payment pattern — flush records per-recipient credits, recipients call withdraw.
  2. Admin reconfigure(address ideaToken, ...) gated by owner + timelock.
  3. try/catch per-recipient transfer.

Option 1 (pull-payment) is preferred — it eliminates the failure mode entirely.

Why immutable per-idea

The BPS config is snapped at IdeaFactory.deployIdea time and never changes. Rationale:

  • Trust: bonders and creators rely on the split being predictable for the life of the idea.
  • Auditability: a reconfigure path would require time-locks and DAO governance; one-shot is simpler.
  • Bug isolation: a botched config on idea A does not affect idea B.

The audit (M-06) flags that this also means a botched config has no escape hatch. Pre-mainnet remediation balances immutability against the need to fix blocked recipients.

Wiring

IdeaFactory.feeRouter → FeeRouter FeeRouter.factory → IdeaFactory (set by setFactory)

Events

event IdeaConfigured( address indexed ideaToken, address protocol, address creator, address winnersSplitter, address forPool, address againstPool, address executorPool, uint16 protocolBps, uint16 creatorBps, uint16 winnersBps, uint16 forBps, uint16 againstBps, uint16 executorBps ); event Flushed(address indexed ideaToken, uint256 amount); event FactoryUpdated(address indexed previous, address indexed next);

Access control summary

FunctionCaller
configureIdeafactory only
flushanyone
setFactoryowner
getConfiganyone
Last updated on