51324 sc high rounding in commission accounting burns delegator rewards

Submitted on Aug 1st 2025 at 17:33:58 UTC by @Rhaydden for Attackathon | Plume Network

  • Report ID: #51324

  • Report Type: Smart Contract

  • Report severity: High

  • Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/plume/src/lib/PlumeRewardLogic.sol

  • Impacts: Theft of unclaimed yield

Description

Brief/Intro

The staking reward logic credits validator commission using floor division but debits each delegator using ceiling division. When ≥2 stakers exist, the sum of per-user charges always exceeds the amount credited to the validator, and the excess “dust” is lost forever. Over time this silently steals part of the users’ unclaimed yield and breaks the protocol's accounting.

In the validator path, updateRewardPerTokenForValidator floors the aggregate commission so the validator is never over credited:

commissionDeltaForValidator =
    (grossRewardForValidatorThisSegment * commissionRate)
/* FLOOR */    / REWARD_PRECISION;

In the user path, _calculateRewardsCore ceilings each delegator’s commission so the validator is never underpaid:

commissionForThisSegment =
    _ceilDiv(grossRewardForSegment * commissionRate,
/* CEIL */   REWARD_PRECISION);

These two choices together break conservation of tokens when more than one staker exists:

ceil(user₁) + ceil(user₂) + … + ceil(userₙ) ≥ floor(user₁+user₂+…+userₙ)

The excess created by the per-user ceilings is not recorded anywhere. Comments in the code hint at rounding intent, but the contract never reconciles the difference.

Vulnerability Details

In PlumeRewardLogic.sol:

Mathematically:

Σ ceil(user_i × r / P) ≥ floor(Σ user_i × r / P)

A strict inequality occurs whenever fractions exist, producing up to (n − 1) wei of “dust” per segment (n = #stakers). This dust is:

  • Removed from delegators’ gross rewards;

  • Not added to validatorAccruedCommission;

  • Not reflected in totalClaimableByToken;

Because reward distribution runs every epoch for every validator, the loss compounds and eventually makes:

contract.balance > totalClaimableByToken + Σ validatorAccruedCommission

breaking conservation of tokens.

Impact Details

High (Theft of unclaimed yield)

  • Delegators receive less reward than they earned.

  • Validator cannot withdraw the difference, so funds accumulate as unclaimable dust.

  • Over many epochs with thousands of stakers, the loss can become material, effectively burning user yield.

Fix

  • Simpler fix: use floor everywhere (i.e., floor per-user commission as well).

  • Alternative: keep per-user ceil but credit the rounding dust to the validator (i.e., reconcile per-segment rounding delta and add it to validatorAccruedCommission or totalClaimableByToken).

References

https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/plume/src/lib/PlumeRewardLogic.sol#L185-L187

Proof of Concept

Solidity test demonstrating the rounding mismatch (expand to view)

Logs

Execution logs (expand to view)

Was this helpful?