30555 - [SC - Low] Precision loss when calculating the FLUX amount...

Precision loss when calculating the FLUX amount required to ragequit for a token

Submitted on Apr 30th 2024 at 18:42:03 UTC by @MTNether for Boost | Alchemix

Report ID: #30555

Report type: Smart Contract

Report severity: Low

Target: https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol

Impacts:

  • Permanent freezing of funds

  • Temporary freezing of funds for 12 hours

  • Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)

Description

Brief/Intro

Fewer FLUX tokens than actual are calculated and burnt in the cooling down process due to the priority of division over multiplication.

Vulnerability Details

Solidity rounds down the result of an integer division, and because of that, it is always recommended to multiply before dividing to avoid that precision loss. In the case of a prior division over multiplication, the final result may face serious precision loss as the first answer would face truncated precision and then multiplied to another integer.

The problem arises in the VotingEscrow's cooling down part. After performing the necessary checks inside the startCooldown() function, it checks the required FLUX amounts to ragequit:

    function amountToRagequit(uint256 _tokenId) public view returns (uint256) {
        // amount of flux earned in one epoch
        uint256 oneEpochFlux = claimableFlux(_tokenId);

        // total amount of epochs in fluxMultiplier amount of years
        uint256 totalEpochs = fluxMultiplier * ((MAXTIME) / EPOCH);

        // based on one epoch, calculate total amount of flux over fluxMultiplier amount of years
        uint256 ragequitAmount = oneEpochFlux * totalEpochs;

        return ragequitAmount;
    }

It is evident that the ragequitAmount is calculated by the multiplication of claimable flux token amounts and the flux time ratio which is defined as:

    totalEpochs = fluxMultiplier * ((MAXTIME) / EPOCH);

The variable fluxMultiplier is set to be 4, the MAXTIME, and EPOCH are 365 days, and 2 weeks respectively.

The current implementation of the aforementioned definition has a hidden division before a multiplication that rounds down the whole expression.

This is bad as the precision loss can be significant, which leads to the contract calculating and burning less ragequitAmount than the actual.

At the Proof of Concept part, we can check this behavior precisely.

Impact Details

Low ragequitAmount to ragequit inside the amountToRagequit() function is calculated leading to wrongly setting the minimum Flux amounts to ragequit and thus burning fewer tokens than actual.

References

https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol?utm_source=immunefi#L345-L356

Consider modifying the ragequitAmount calculation to prevent such precision loss and prioritize the multiplication over division:

    function amountToRagequit(uint256 _tokenId) public view returns (uint256) {
        // amount of flux earned in one epoch
        uint256 oneEpochFlux = claimableFlux(_tokenId);

        // total amount of epochs in fluxMultiplier amount of years
-       uint256 totalEpochs = fluxMultiplier * ((MAXTIME) / EPOCH);

        // based on one epoch, calculate total amount of flux over fluxMultiplier amount of years
-       uint256 ragequitAmount = oneEpochFlux * totalEpochs;
+       uint256 ragequitAmount = (oneEpochFlux * fluxMultiplier * MAXTIME) / EPOCH;

        return ragequitAmount;
    }

Proof of Concept

You can run this code inside the Forge to see the difference between the results:

    function test_precissionLoss() public {

        uint256 oneEpochFlux = claimableFlux;
        uint256 totalEpochs = fluxMultiplier * ((MAXTIME) / EPOCH);

        uint256 ragequitAmount_actual   = (oneEpochFlux * totalEpochs);
        uint256 ragequitAmount_accurate = (oneEpochFlux * fluxMultiplier * MAXTIME) / EPOCH;
        
        console.log("Current Implementation ", ragequitAmount_actual);
        console.log("Actual Implementation  ", ragequitAmount_accurate);
    }

The result would be: (for a sample and real claimableFlux of 156510000000 just for testing purpose)

     Current Implementation  16277040000000
     Actual Implementation   16321757142857

Thus, we can see that the actual implementation produces less ragequit amount than the precise method. This test shows a big difference between the two calculated ragequit amounts in the cooling down process.

Last updated