31562 - [SC - Medium] Every consecutive epoch will have same number o...

Submitted on May 21st 2024 at 11:06:37 UTC by @SAAJ for Boost | Alchemix

Report ID: #31562

Report type: Smart Contract

Report severity: Medium

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

Impacts:

  • Contract fails to deliver promised returns, but doesn't lose value

Description

Brief/Intro

Minter contract have function updatePeriod where it has a condition where it reset stepdown to update rewards for next epoch.

Vulnerability Details

The description for condition to reset stepdown in updatePeriod function clearly mentions only when the rewards level reached the TAIL_EMISSIONS_RATE.

// Once we reach the emissions tail stepdown is 0

if (rewards <= TAIL_EMISSIONS_RATE) {
                stepdown = 0;
            }

However, the condition logic is flawed as it is designed to reset stepdown even when rewards level is lower than the TAIL_EMISSIONS_RATE for the current epoch. This will lead to resetting of stepdown in every coming epoch that will have less or equal rewards with comparison to TAIL_EMISSIONS_RATE.

// Set rewards for next epoch
            rewards -= stepdown;

Impact Details

When the updatePeriod is called at first epoch and if rewards level is lower /equal to TAIL_EMISSIONS_RATE it will reset stepdown.

This will impact 3rd epoch, as when updatePeriod is called in 2nd epoch it will have stepdown value equal to 0.

            rewards -= stepdown;

rewards value for 3rd epoch will be same as the previous 2nd epoch value, as the call in 2nd epoch will have no impact on value of rewards based on condition of being subtracted with zero.

References

https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Minter.sol#L160

Recommendation

Recommendation is made to change the logic from <= to >= to clearly and truely meet the condition of resetting stepdown, only when rewards amount meets or surpassed the value of TAIL_EMISSIONS_RATE.

    /// @inheritdoc IMinter
    function updatePeriod() external returns (uint256) {
        require(msg.sender == address(voter), "not voter");

        uint256 period = activePeriod;

        if (block.timestamp >= period + DURATION && initializer == address(0)) {
            // Only trigger if new epoch
            period = (block.timestamp / DURATION) * DURATION;
            activePeriod = period;
            epochEmissions = epochEmission();

            uint256 veAlcxEmissions = calculateEmissions(epochEmissions, veAlcxEmissionsRate);
            uint256 timeEmissions = calculateEmissions(epochEmissions, timeEmissionsRate);
            uint256 treasuryEmissions = calculateEmissions(epochEmissions, treasuryEmissionsRate);
            uint256 gaugeEmissions = epochEmissions.sub(veAlcxEmissions).sub(timeEmissions).sub(treasuryEmissions);
            uint256 balanceOf = alcx.balanceOf(address(this));

            if (balanceOf < epochEmissions) alcx.mint(address(this), epochEmissions - balanceOf);

            // Set rewards for next epoch
            rewards -= stepdown;

            // Adjust updated emissions total
            supply += rewards;

            // Once we reach the emissions tail stepdown is 0
-           if (rewards <= TAIL_EMISSIONS_RATE) {
+           if (rewards >= TAIL_EMISSIONS_RATE) {
                stepdown = 0;
            }

            // If there are no votes, send emissions to veALCX holders
            if (voter.totalWeight() > 0) {
                alcx.approve(address(voter), gaugeEmissions);
                voter.notifyRewardAmount(gaugeEmissions);
            } else {
                veAlcxEmissions += gaugeEmissions;
            }

            // Logic to distrubte minted tokens
            IERC20(address(alcx)).safeTransfer(address(rewardsDistributor), veAlcxEmissions);
            rewardsDistributor.checkpointToken(); // Checkpoint token balance that was just minted in rewards distributor
            rewardsDistributor.checkpointTotalSupply(); // Checkpoint supply

            IERC20(address(alcx)).safeTransfer(address(timeGauge), timeEmissions);
            timeGauge.notifyRewardAmount(timeEmissions);

            IERC20(address(alcx)).safeTransfer(treasury, treasuryEmissions);

            revenueHandler.checkpoint();

            emit Mint(msg.sender, epochEmissions, supply);
        }
        return period;
    }
}

Proof of Concept

This test demonstrate the no change in impact in value of rewards for next epoch if its value is less or equal to TAIL_EMISSIONS_RATE in current one. For this test values for variables are assumed to have clear idea on the outcome generated when the updatePeriod is called in 2nd epoch.

    uint256 TAIL_EMISSIONS_RATE = 2194; // ALCX tail emissions rate sans 18

    uint256 supply = 100; // assumed value of stepdown sans 18 decimals
    uint256 stepdown = 100; // assumed value of stepdown sans 18 decimals
    uint256 rewards = 2190; // assumed value of rewards sans 18 decimals

    // forge t --mt test_newEpoch -vv
    function test_newEpoch() external {
        console.log("Reward at 1st Epoch:", rewards);

        test_UpdatePeriod(); // call made for 1st epoch
        console.log("Stepdown:", stepdown);
console.log("Reward set for 2nd Epoch:", rewards);
        assertEq(rewards, 2090); // asserting reward set after function called

        uint256 next_EPOCH = 2 weeks; // assume update is called on 2nd week
        vm.warp(next_EPOCH); // making call to the function at new epoch i.e. 2nd week

        test_UpdatePeriod(); // call made for 2nd epoch
        console.log("Reward after UpdatePeriod() called on 2nd Epoch:", rewards); // Reward set for 3rd Epoch
        assertEq(rewards, 2090); // asserting reward set at 2nd Epoch

    }

The test passed when updatePeriod is called during 1st and 2nd epoch with same value of rewards generated each time.

[PASS] test_newEpoch() (gas: 23198)
Logs:
  Reward at 1st Epoch: 2190
  Stepdown: 0
  Reward set for 2nd Epoch: 2090
  Reward after UpdatePeriod() called on 2nd Epoch: 2090

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.75ms (228.17µs CPU time)

Ran 1 test suite in 86.64ms (1.75ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

The value shown for rewards is same for 3rd epoch as it is subtracted to zero in the 2nd epoch one by meeting the condition of <= to TAIL_EMISSIONS_RATE.

Last updated