Component: ghost-dao-contracts @ b87dff4, src/StakingDistributor.sol (GhostDistributor), retrieveBounty() lines 58-62.
Description: retrieveBounty() mints bounty to the staking contract every time rebase() runs but never resets bounty to zero:
function retrieveBounty() external override returns (uint256) {
if (msg.sender != staking) revert OnlyStaking();
if (bounty > 0) ITreasury(treasury).mint(staking, bounty);
return bounty;
}
The function is named “retrieve”, and no code path resets bounty except a manual setBounty() by the governor. So one setBounty(X) call prints X FTSO EVERY epoch until the governor notices and sets it back to 0. This is also not a keeper incentive: the mint goes to the staking contract, not to the rebase() caller, so economically this is a recurring emission stream to stakers, not a one-time bounty. Additionally, if the treasury’s excessReserves() falls below bounty, treasury.mint() reverts (InsufficientReserves), and since GhostStaking.rebase() calls retrieveBounty() unconditionally, the whole rebase() reverts: stake()/unstake() are stuck until the governor calls setBounty(0).
Impact: Unbounded per-epoch FTSO emission (drains excess reserves, dilutes backing); worst case: rebase bricks (staking functions DoS) until the governor acts.
Reproduction: Local forge PoC (PASS). test_poc_bountyIsMintedEveryEpochAndNeverReset: deploy GhostDistributor with a mock treasury, governor calls setBounty(1000e9), call distribute() + retrieveBounty() twice (simulating two epochs). Result: distributor.bounty() still 1000e9; mock treasury recorded 2 * bounty minted. test_poc_persistentBountyBricksRebaseWhenReservesDepleted proves retrieveBounty() reverts when the treasury mint fails (simulated depleted excess reserves); the revert propagation into rebase() reads directly from the code.
Severity: Medium. Reason: real economic impact (recurring emission plus potential rebase DoS), but the trigger is governor configuration, not a permissionless attacker path. A governor who believes this is a one-time bounty would unknowingly drain reserves.
Recommendation: Add bounty = 0; after the mint in retrieveBounty() (one-time semantics matching the name). If recurring emission is intended, document it explicitly, rename to e.g. recurringEmission, and make rebase() resilient to a failing bounty mint (try/catch, or skip when bounty > excessReserves()).
Verification: VERIFIED (forge PoC passed; 209 existing tests passed before adding PoCs, 214 after, no regressions).