Skip to content
Protocol contracts not yet configured for this build
Enter app

Core concepts

The Core

The Core is the PloutoRevenueRouter contract and the rule it enforces. It has one job: take claimed revenue and divide it three ways, the same way, every time.

Pons creator fees flow into the Plouto Revenue Router, which splits every epoch into 60 percent buyback and retirement, 30 percent staker rewards, and 10 percent protocol reserve.

Source

Pons creator fees

Claimed in ETH from the fee escrow

Router

Plouto Revenue Router

Immutable 60 / 30 / 10 · no admin override

  • 60%

    Buyback & retirement

    Market purchases, permanently removed from circulation.

  • 30%

    Staker rewards

    ETH streamed to locked Gravity positions.

  • 10%

    Plouto Reserve

    Security, infrastructure, automation and future protocol-owned liquidity.

The rule

solidity
uint256 public constant BUYBACK_BPS = 6000;uint256 public constant STAKER_BPS  = 3000;uint256 public constant RESERVE_BPS = 1000;uint256 public constant BPS         = 10_000;

These are constant, which in Solidity means they are compiled into the bytecode. There is no storage slot holding them and therefore no way — for an admin, an owner, a multisig or anyone else — to change them after deployment. Changing the split would require deploying a different contract and persuading Pons to redirect fees to it, which is a visible, timelocked operation on the Pons side.

The constructor asserts the arithmetic as a last line of defence against a bad edit:

solidity
assert(BUYBACK_BPS + STAKER_BPS + RESERVE_BPS == BPS);

What each destination does

60% → BuybackExecutor

Funds open-market purchases of PLOUTO which are then permanently retired. The ETH sits as buybackBudget until a keeper executes, bounded by size, slippage and price-impact limits.

30% → GravityStaking

Delivered by calling notifyRewardETH(), which folds the amount into the reward-per-weight accumulator. If nothing is staked, it is held rather than lost.

10% → PloutoReserve

Delivered by calling depositRevenue(), which records it as protocol revenue rather than a donation and appends to the deposit history.

Ordering

The router follows checks-effects-interactions strictly. All accounting is written and the event is emitted before any ETH leaves:

solidity
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); if (toBuybacks != 0) IBuybackExecutor(buybackExecutor).fundBuyback{value: toBuybacks}();if (toStakers  != 0) IGravityStaking(gravityStaking).notifyRewardETH{value: toStakers}();if (toReserve  != 0) IPloutoReserve(reserve).depositRevenue{value: toReserve}();

Combined with nonReentrant, a downstream contract cannot re-enter mid-split and observe or exploit a partially-updated state.

What the Core deliberately does not have

  • No arbitrary-call function. There is no execute(address,bytes) anywhere in the router. An admin cannot make it call something unexpected.
  • No percentage setter. Not even behind a timelock.
  • No withdrawal function. ETH held by the router can only leave through the split, or — for donations specifically — through sweepDonationsToReserve(), which sends to a fixed address.
  • No pause on claiming. Pausing blocks routing, not claiming, so a pause can never strand fees in the escrow.

Donations are not revenue

ETH sent directly to the router is classified as a donation, tracked in totalDonationsReceived and unallocatedDonations, and excluded from the split entirely.

The one subtlety: ETH arriving from the escrow is the payout leg of a claim, which is already accounted for by measured delta. The receive() function falls through in that case rather than double-counting:

solidity
receive() external payable {    if (msg.sender == address(feeEscrow)) return;    totalDonationsReceived += msg.value;    unallocatedDonations += msg.value;    emit DonationReceived(msg.sender, msg.value);}

This was a real bug found by the test suite, and is described in build status.