Liquid Staking

Stakelink
DeFiHardhatOracle
50,000 USDC
View results
Submission Details
Severity: medium
Invalid

Integer Division in _splitRewards Function Causes Precision Loss and Accumulation of Undistributed Tokens

Details

The LSTRewardsSplitter contract distributes LST rewards to multiple recipients based on their fee percentages (basis points). The _splitRewards function uses integer division to calculate the reward amount for each recipient.

Integer division inherently truncates any remainder, leading to potential precision loss. In the context of reward distribution, this means small amounts of LST might remain undistributed after each split. While the loss might be negligible for individual transactions, it can accumulate over time, especially with a large number of splits or with fee percentages that result in repeating decimals.

Code Snippet

function _splitRewards(uint256 _rewardsAmount) private {
for (uint256 i = 0; i < fees.length; ++i) {
Fee memory fee = fees[i];
uint256 amount = (_rewardsAmount * fee.basisPoints) / 10000; // Precision loss due to integer division
// ... (rest of the distribution logic) ...
}
// ...
}

Impact

  • Accumulated Loss: Precision loss can accumulate over time, leading to a significant amount of undistributed rewards trapped in the contract.

  • Unfair Distribution: While the loss might seem minor for individual recipients, it can lead to an unfair distribution over the long term.

Scenario

Consider a scenario with three recipients and 100 tokens to distribute:

Recipient A: 33.33% (3333 basis points)
Recipient B: 33.33% (3333 basis points)
Recipient C: 33.34% (3334 basis points)

Due to integer division:

A receives 33 tokens.
B receives 33 tokens.
C receives 33 tokens.

This leaves 1 token undistributed.

Fix

There are a couple of approaches to mitigate this issue:

  • Distribute Remainder: Track the total distributed amount and distribute any remaining tokens to a designated address (as explained in the previous bug report).

  • Higher Precision Math: Use a higher precision for calculations. This can be achieved by multiplying the _rewardsAmount and fee.basisPoints by a large number (e.g., 10^18) before dividing. Dividing the result by the same large number to get the final amount.

Test

it('should handle precision loss correctly', async () => {
const { accounts, controller, token, splitter0 } = await loadFixture(deployFixture);
// Add fees that could lead to precision loss (e.g., with repeating decimals)
await splitter0.addFee(accounts[7], 3333); // 33.33%
await splitter0.addFee(accounts[8], 3333); // 33.33%
await splitter0.addFee(accounts[9], 3334); // 33.34%
await token.transferAndCall(controller.target, toEther(100), '0x'); // Deposit 100 tokens
await token.transfer(splitter0.target, toEther(100)); // Add 100 tokens as rewards
await splitter0.splitRewards();
// Check the balances of the recipients
const balance7 = fromEther(await token.balanceOf(accounts[7]));
const balance8 = fromEther(await token.balanceOf(accounts[8]));
const balance9 = fromEther(await token.balanceOf(accounts[9]));
// Calculate the total distributed amount
const totalDistributed = balance7 + balance8 + balance9;
// Assert that the total distributed amount is equal to the rewards amount (100)
assert.equal(totalDistributed, 100, 'Total distributed amount should not have precision loss');
// Optionally, assert individual balances if a specific distribution method is expected
// (e.g., rounding up the last recipient's share)
});
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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