Low Sangria Cricket
Medium
Insiders rewards distribution will take 2 months longer than expected due to double division and precision loss
When an insider claims their original allocation via UsualSP::claimOriginalAllocation()
, the underlying _released()
function calculates the amount that the insider is entitled to based on the elapsed full months since the distribution started.
However, due to the structure of this function, the calculation underestimates the elapsed time. The function divides the elapsed time into two segments - before and after the cliff -, divides each by the number of days in a month (rounding down), and then adds the two segments together. As a result, rewards will be almost two months delayed due to this precision loss.
The issue lies in the _released() function, which performs two separate divisions for calculating the elapsed time in months. Both divisions round down due to Solidity’s integer arithmetic, resulting in an inaccurate calculation for partial months:
function _released(UsualSPStorageV0 storage $, address insider)
internal
view
returns (uint256)
{
uint256 insiderCliffDuration = $.cliffDuration[insider];
@1> uint256 totalMonthsInCliffDuration = insiderCliffDuration / ONE_MONTH;
uint256 totalAllocation = $.originalAllocation[insider];
if (block.timestamp < $.startDate + insiderCliffDuration) {
// No tokens can be claimed before the cliff duration
revert NotClaimableYet();
} else if (block.timestamp >= $.startDate + $.duration) {
// All tokens can be claimed after the duration
return totalAllocation;
} else {
// Calculate the number of months passed since the cliff duration
@2> uint256 monthsPassed =
(block.timestamp - $.startDate - insiderCliffDuration) / ONE_MONTH;
// Calculate the vested amount based on the number of months passed
uint256 vestedAmount = totalAllocation.mulDiv(
@3> totalMonthsInCliffDuration + monthsPassed,
NUMBER_OF_MONTHS_IN_THREE_YEARS,
Math.Rounding.Floor
);
// Ensure we don't release more than the total allocation due to rounding
return Math.min(vestedAmount, totalAllocation);
}
}
@1: insiderCliffDuration
is divided by ONE_MONTH
@2: monthsPassed
(time elapsed after the cliff) is divided by ONE_MONTH
@3: The sum of insiderCliffDuration
and monthsPassed
is then used to calculate the vested amount, resulting in an underestimation of the time elapsed
insiderCliffDuration % ONE_MONTH != 0
The insider attempts a claim anytime after their cliff duration.
Here’s a step-by-step example using insiderCliffDuration = 89 days
and attempting a claim 118 days after startDate
:
- insiderCliffDuration: 89 days
- current time: 118 days after startDate
- totalAllocation: assumed as 1000 tokens
- NUMBER_OF_MONTHS_IN_THREE_YEARS: 36 months (for three-year vesting)
- First, calculate totalMonthsInCliffDuration:
totalMonthsInCliffDuration = insiderCliffDuration / ONE_MONTH; totalMonthsInCliffDuration = 89 / 30; totalMonthsInCliffDuration = 2;
- Next, calculate monthsPassed after the cliff duration:
monthsPassed = (current time - startDate - insiderCliffDuration) / ONE_MONTH; monthsPassed = (118 - 0 - 89) / 30; monthsPassed = (29) / 30; monthsPassed = 0;
- Now, compute vestedAmount:
vestedAmount = totalAllocation * (totalMonthsInCliffDuration + monthsPassed) / NUMBER_OF_MONTHS_IN_THREE_YEARS; vestedAmount = 1_000 * (2 + 0) / 36; vestedAmount = 1_000 * (2) / 36; vestedAmount = 2_000 / 36; vestedAmount = 55;
- Despite the insider having waited 118 days (almost four months), the vested amount only reflects two months (due to both divisions rounding down).
- Consequently, the insider would need to wait approximately two additional months to accrue the correct vested amount based on actual time elapsed.
Insiders receive rewards up to 2 months later than expected due to precision loss in the calculation.
No response
Avoid splitting insiderCliffDuration and elapsed time into separate chunks. Instead, calculate the entire elapsed time in one division:
uint256 totalMonthsElapsed = (block.timestamp - $.startDate) / ONE_MONTH;