PloutoRevenueRouter
The address registered with Pons as creatorFeeRecipient. It pulls fees from the escrow and splits them, and it does nothing else.
Immutable wiring
IPloutoRegistry public immutable registry;IPonsV2FeeEscrow public immutable feeEscrow;address public immutable gravityStaking;address public immutable buybackExecutor;address public immutable reserve;All five are constructor arguments and none can be changed. There is no admin function that redirects any part of the split.
Claiming
function claimPonsFees() public nonReentrant returns (uint256 received) { uint256 balanceBefore = address(this).balance; uint256 reported = feeEscrow.claim(); received = address(this).balance - balanceBefore; if (received == 0) revert NothingClaimed(); totalRevenueClaimed += received; unallocatedRevenue += received; emit PonsFeesClaimed(reported, received, unallocatedRevenue);}Permissionless, and not gated on initialization — fees are never stranded in escrow waiting for a launch.
Routing
function routeUnallocatedRevenue() public nonReentrant whenNotPaused returns (uint256 toBuybacks, uint256 toStakers, uint256 toReserve){ if (!registry.initialized()) revert NotInitialized(); uint256 amount = unallocatedRevenue; if (amount == 0) revert NothingToRoute(); toBuybacks = (amount * BUYBACK_BPS) / BPS; toStakers = (amount * STAKER_BPS) / BPS; toReserve = amount - toBuybacks - toStakers; // dust falls here if (toBuybacks + toStakers + toReserve != amount) revert SplitMismatch(amount, 0); // effects unallocatedRevenue = 0; totalRevenueRouted += amount; totalSentToBuybacks += toBuybacks; totalSentToStakers += toStakers; totalSentToReserve += toReserve; _routes.push(RouteRecord(uint64(block.timestamp), amount, toBuybacks, toStakers, toReserve)); emit RevenueRouted(amount, toBuybacks, toStakers, toReserve, totalRevenueRouted, _routes.length - 1); // interactions if (toBuybacks != 0) IBuybackExecutor(buybackExecutor).fundBuyback{value: toBuybacks}(); if (toStakers != 0) IGravityStaking(gravityStaking).notifyRewardETH{value: toStakers}(); if (toReserve != 0) IPloutoReserve(reserve).depositRevenue{value: toReserve}();}Strict checks-effects-interactions plus nonReentrant. A downstream contract cannot re-enter and observe a partially-written split.
Donation classification
receive() external payable { if (msg.sender == address(feeEscrow)) return; // claim payout, already measured totalDonationsReceived += msg.value; unallocatedDonations += msg.value; emit DonationReceived(msg.sender, msg.value);}Donations are excluded from the split entirely and can only be moved to the reserve:
function sweepDonationsToReserve() external onlyRole(KEEPER_ROLE) nonReentrant returns (uint256 amount);A fixed destination, so even a compromised keeper cannot redirect them.
Accounting surface
totalRevenueClaimed totalRevenueRouted unallocatedRevenuetotalSentToBuybacks totalSentToStakers totalSentToReservetotalDonationsReceived unallocatedDonationspendingInEscrow() localBalances() routesLength() / getRoute()Two invariants hold at all times:
totalSentToBuybacks + totalSentToStakers + totalSentToReserve == totalRevenueRoutedtotalRevenueClaimed - totalRevenueRouted == unallocatedRevenueWhat it deliberately lacks
- No arbitrary-call function.
- No percentage setter, even behind a timelock.
- No withdrawal function.
- No pause on claiming — a pause can never strand fees in escrow.