Skip to content

Latest commit

 

History

History
118 lines (80 loc) · 4.61 KB

File metadata and controls

118 lines (80 loc) · 4.61 KB

Low Sangria Cricket

Medium

Insiders rewards distribution will take 2 months longer than expected due to double division and precision loss

Summary

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.

Root Cause

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

Internal pre-conditions

insiderCliffDuration % ONE_MONTH != 0

External pre-conditions

The insider attempts a claim anytime after their cliff duration.

Attack Path

Here’s a step-by-step example using insiderCliffDuration = 89 days and attempting a claim 118 days after startDate:

Inputs:

  • 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)

Calculation Steps:

  • 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;

Impact of Precision Loss:

  • 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.

Impact

Insiders receive rewards up to 2 months later than expected due to precision loss in the calculation.

PoC

No response

Mitigation

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;