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:
- Pull-payment pattern —
flushrecords per-recipient credits, recipients callwithdraw. - Admin
reconfigure(address ideaToken, ...)gated by owner + timelock. try/catchper-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
reconfigurepath 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
| Function | Caller |
|---|---|
configureIdea | factory only |
flush | anyone |
setFactory | owner |
getConfig | anyone |