Sablier

Sablier
DeFiFoundry
53,440 USDC
View results
Submission Details
Severity: medium
Invalid

`_calculateTranches` rounding errors will disturb the intended amounts to distribute

Summary

Rounding errors will assign smaller amount values in all tranches except in the last one where all owed tokens would be added up. A 3x100 tranche distribution [100, 100, 100] will look like [99, 99, 102]. This could lead to unintended behaviour and varying impact based on tranches.length and the value of the streamed token.

Vulnerability Details

Whenever a MerkleLT recipient calls claim, _calculateTranches is invoked. Amount per tranche owed is calculated based on the total amount owed to the recipient multiplied by each tranche's assigned percentage. Tranche percentages are checked beforehand to amount to 100%.

for (uint256 i = 0; i < trancheCount; ++i) {
// Convert the tranche's percentage from the `UD2x18` to the `UD60x18` type.
UD60x18 percentage = (tranchesWithPercentages[i].unlockPercentage).intoUD60x18(); // each tranche percentage, sum of all = 100%
// Calculate the tranche's amount by multiplying the claim amount by the unlock percentage. //
uint128 calculatedAmount = claimAmountUD.mul(percentage).intoUint128(); // total owed claim amount multiplied by each percentage, @audit prone to rounding down
// Create the tranche with duration.
tranches[i] = LockupTranched.TrancheWithDuration({
amount: calculatedAmount, // amounts stored for each tranche
duration: tranchesWithPercentages[i].duration
});
}

Further down the code the devs have made sure that the recipient ends up receiving the entire owed amount, despite rounding errors, which are stated as possible in the natspec.

// Since there can be rounding errors, the last tranche amount needs to be adjusted to ensure the sum of all
// tranche amounts equals the claim amount.
if (calculatedAmountsSum < claimAmount) { // if the sum of all amounts does not add up to the total owed
unchecked {
tranches[trancheCount - 1].amount += claimAmount - calculatedAmountsSum; // add the difference in the last tranche
}
}

This mitigation, however, would disturb the inteded distribution by the sender. Given the freedom of working with any ERC20 token and to set tranche timestamps far in the future, a merkle stream recipient will be forced to wait for the tokens owed until the last tranche has passed. As per Sablier's website, Sablier intends to cater to businesses and assist in payroll handling. One could expect a scenario with an annual salary via MerkleLT (e.g 1200 tokens, 12x100 per month). A recipient would expect 12 equal payments looking as [100, 100, 100, ..., 100] but end up receiving it as [99, 99, 99, ..., 111]. Recipient would have to wait a year to receive their originally owed funds.

Despite the example being an abstract one, it points to a key issue - recipients will receive less than expected/owed and the difference is softlocked until the end of the stream.

Impact

Unexpected behaviour

Tools Used

Manual review

Recommendations

Add the option in createMerkleLT to input tranche amounts manually in cases where working with percentages is not optimal.

Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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