Description:
In the MarketMakingEngineConfiguration::distributeProtocolAssetReward
function, the code for distributeProtocolAssetReward
explicitly uses integer division in the core reward calculation:
This line calculates each recipient's reward by multiplying the total reward amount (amountX18)
by the recipient's share (ud60x18(shares))
and then dividing by the total shares (totalFeeRecipientsSharesX18)
. The .div()
operation in Solidity performs integer division, truncating any fractional part.
impact:
The code attempts to compensate for rounding loss in the last iteration of the loop:
This logic adds the entire remaining amount (amountX18.sub(ud60x18(totalDistributed))
converted to a uint256
) to the last recipient's reward.
While this attempts to prevent fees from being "stuck," it introduces a systematic bias in favour of the last fee recipient in the protocolFeeRecipients
mapping. The last recipient will always receive any accumulated rounding errors from all previous recipient calculations. This is not a fair or proportional distribution and can lead to significant discrepancies in reward allocation over time, especially if there are many fee recipients and frequent distributions.
Proof of Concept:
Recomended Mitigation:
Distribute Remainder Proportionally. A more equitable and accurate approach is to distribute the "remainder" (lost fractions due to integer division) proportionally across all recipients in subsequent distributions. This requires tracking the accumulated remainder for each recipient. A possible implementation strategy:
Maintain a mapping accumulatedRemainder[address feeRecipient]
of UD60x18
.
In each distribution, calculate the feeRecipientReward
using integer division (for gas efficiency in the main calculation)
Calculate the fractional remainder for each recipient: remainder = amountX18.mul(ud60x18(shares)).div(totalFeeRecipientsSharesX18).sub(ud60x18(feeRecipientReward))
Add this remainder to accumulatedRemainder[feeRecipient]
In each distribution, also distribute any accumulated remainder from previous distributions: feeRecipientReward += accumulatedRemainder[feeRecipient].intoUint256();
(and then reset accumulatedRemainder[feeRecipient] = UD60x18_ZERO
; or implement a more sophisticated remainder carry-forward mechanism if needed). The key is to distribute accumulated fractional remainders across all recipients over time, not just give all rounding errors to the last recipient in each distribution cycle.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.