FoundrySolidityLayer 2
7.25 ETH
Submission Details
Impact: low
Likelihood: low

Low-decimal Sybil rounding can hostage the bonus pool indefinitely

Author Revealed upon completion

Low-decimal Sybil rounding can hostage the bonus pool indefinitely

Description

Unclaimed bonus reserves should approximate the remaining payable bonus liability. For low-decimal or zero-decimal tokens, individual bonus shares can round to zero while sweepUnclaimedBonus() still reserves the entire aggregate unclaimed bonus. A Sybil can leave one tiny principal claim outstanding and keep the bonus unavailable to recovery.

Vulnerability Details

Individual bonus shares floor down:

return Math.mulDiv(userEligible, snapshotTotalBonus, snapshotTotalStaked);

The sweep path reserves the aggregate bonus that has not been claimed:

reserved = totalEligibleStake;
if (riskWindowStart != 0) {
reserved += snapshotTotalBonus - claimedBonus;
}

For a zero-decimal token with many one-unit stakers, every individual bonus share can round to zero. After all but one Sybil claims, claimedBonus remains zero, so the sweep still reserves the entire snapshotTotalBonus even though the last staker's rounded bonus entitlement is also zero.

Impact

The bonus remains unavailable to recovery while one tiny principal claim remains outstanding.

The attacker can keep the bonus hostage indefinitely by never claiming the final one-unit principal.

Proof of Concept

function testRoundingHostage() external {
for (uint256 i; i < 101; ++i) {
_stake(pool, token, address(uint160(0xBEEF0000 + i)), 1);
}
_contributeBonus(pool, token, carol, 100);
pool.flagOutcome(PoolStates.Outcome.SURVIVED, false, address(0));
for (uint256 i; i < 100; ++i) {
pool.claimSurvived();
}
assertEq(pool.claimedBonus(), 0);
assertEq(pool.totalEligibleStake(), 1);
vm.expectRevert();
pool.sweepUnclaimedBonus();
}

Recommended Mitigation

Reserve the exact remaining rounded bonus liability instead of the aggregate unclaimed bonus.

-reserved += snapshotTotalBonus - claimedBonus;
+reserved += _remainingRoundedBonusLiability();

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!