31385 - [SC - Low] RewardsDistributortokensPerWeek might be zero i...

Submitted on May 17th 2024 at 22:21:00 UTC by @jasonxiale for Boost | Alchemix

Report ID: #31385

Report type: Smart Contract

Report severity: Low

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

Impacts:

  • Protocol insolvency

Description

Brief/Intro

RewardsDistributor.tokensPerWeek is used to record the amount of alcx to distribute per week, if its value is zero, it means there is no alcx will be distributed. In current implementation, there will be an extreme case that if the RewardsDistributor.checkpointToken isn't called for more than 20 weeks, some weeks in the past will has empty RewardsDistributor.tokensPerWeek

Vulnerability Details

In RewardsDistributor._checkpointToken, the function will update lastTokenTime to block.timestamp first, and then loop 20 WEEK in for-loop

227     function _checkpointToken() internal {
...
232         uint256 t = lastTokenTime;
233         uint256 sinceLast = block.timestamp - t;
234         lastTokenTime = block.timestamp; <<<--- lastTokenTime is update to block.timestamp
235         uint256 thisWeek = (t / WEEK) * WEEK;
236         uint256 nextWeek = 0;
237 
238         for (uint256 i = 0; i < 20; i++) { <<<--- here 20 is used, it means that the function will loop 20 weeks at most
...
252         }
254     }

And next time when RewardsDistributor.checkpointToken is called, the function record RewardsDistributor.tokensPerWeek from the timestamp the function is called instead of the RewardsDistributor.tokensPerWeek hasn't been recordeded. So if the RewardsDistributor.checkpointToken hasn't been called in more than 20 weeks, the RewardsDistributor.tokensPerWeek will be like: RewardsDistributor.tokensPerWeek[week_00] -> value_00 RewardsDistributor.tokensPerWeek[week_01] -> value_01 RewardsDistributor.tokensPerWeek[week_02] -> value_02 ... RewardsDistributor.tokensPerWeek[week_19] -> value_19 RewardsDistributor.tokensPerWeek[week_20] -> 0 RewardsDistributor.tokensPerWeek[week_21] -> 0 RewardsDistributor.tokensPerWeek[week_22] -> 0 ... RewardsDistributor.tokensPerWeek[week_nn] -> value_nn <<<<--- next RewardsDistributor.checkpointToken is called, the RewardsDistributor.tokensPerWeek will be updated from here RewardsDistributor.tokensPerWeek[week_nm] -> value_nm

Impact Details

Because RewardsDistributor.tokensPerWeek is used to calcuate the amount of alcx a user can claim, if its value is 0, it means there will be no alcx can be claim.

References

Add any relevant links to documentation or code

Proof of Concept

Put the following code in src/test/Minter.t.sol, and run

FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/0TbY2mhyGA4gLPShfh-PwBlQ3PDNUdL1 --fork-block-number 17133822 --mc MinterTest --mt testNoEmissions -vv
[⠊] Compiling...
No files changed, compilation skipped

Ran 1 test for src/test/Minter.t.sol:MinterTest
[PASS] testNoEmissions() (gas: 5267453)
Logs:
  block.timestamp    0      : 1684972800
  rd.lastTokenTime()        : 1682553600
  block.timestamp    1      : 1700092801
  rd.lastTokenTime()        : 1684972800
  ===========================
  rd.tokensPerWeek          : 0
  rd.tokensPerWeek          : 0
  rd.tokensPerWeek          : 0
  rd.tokensPerWeek          : 0
  rd.tokensPerWeek          : 0
  rd.tokensPerWeek          : 0
  rd.tokensPerWeek          : 324063978567197184709
  rd.tokensPerWeek          : 324063978567197184709
  rd.tokensPerWeek          : 324063978567197184709
  rd.tokensPerWeek          : 324063978567197184709
  ===========================
  block.timestamp           : 1701302402
  rd.tokensPerWeek          : 4008540058167941329413
  rd.tokensPerWeek          : 0
  rd.tokensPerWeek          : 0
  rd.tokensPerWeek          : 0
  rd.tokensPerWeek          : 0
  rd.tokensPerWeek          : 0
  rd.tokensPerWeek          : 324063978567197184709
  rd.tokensPerWeek          : 324063978567197184709
  rd.tokensPerWeek          : 324063978567197184709
  rd.tokensPerWeek          : 324063978567197184709

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.80ms (1.44ms CPU time)

Ran 1 test suite in 1.37s (6.80ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

As we can see from the above output, after two voter.distribute(); calls, there still some rd.tokensPerWeek contains 0

    function testNoEmissions() external {
        // Mint emissions for the amount of epochs until tail emissions target
        uint WEEK = 1 weeks;
        RewardsDistributor rd = RewardsDistributor(payable(address(minter.rewardsDistributor())));
        // console2.log("block.timestamp           :", block.timestamp);
        uint startTime = IMinter(minter).activePeriod();

        hevm.warp(startTime + IMinter(minter).DURATION());
        console2.log("block.timestamp    0      :", block.timestamp);
        console2.log("rd.lastTokenTime()        :", rd.lastTokenTime());
        voter.distribute();

        hevm.warp(block.timestamp + 25 * WEEK + 1);
        console2.log("block.timestamp    1      :", block.timestamp);
        console2.log("rd.lastTokenTime()        :", rd.lastTokenTime());

        voter.distribute();
        uint index = block.timestamp / WEEK * WEEK;
        console2.log("===========================");
        console2.log("rd.tokensPerWeek          :", rd.tokensPerWeek(index - 0 * WEEK));
        console2.log("rd.tokensPerWeek          :", rd.tokensPerWeek(index - 1 * WEEK));
        console2.log("rd.tokensPerWeek          :", rd.tokensPerWeek(index - 2 * WEEK));
        console2.log("rd.tokensPerWeek          :", rd.tokensPerWeek(index - 3 * WEEK));
        console2.log("rd.tokensPerWeek          :", rd.tokensPerWeek(index - 4 * WEEK));
        console2.log("rd.tokensPerWeek          :", rd.tokensPerWeek(index - 5 * WEEK));
        console2.log("rd.tokensPerWeek          :", rd.tokensPerWeek(index - 6 * WEEK));
        console2.log("rd.tokensPerWeek          :", rd.tokensPerWeek(index - 7 * WEEK));
        console2.log("rd.tokensPerWeek          :", rd.tokensPerWeek(index - 8 * WEEK));
        console2.log("rd.tokensPerWeek          :", rd.tokensPerWeek(index - 9 * WEEK));

        console2.log("===========================");
        hevm.warp(block.timestamp + 2 * WEEK + 1);
        voter.distribute();
        console2.log("block.timestamp           :", block.timestamp);
        console2.log("rd.tokensPerWeek          :", rd.tokensPerWeek(index - 0 * WEEK));
        console2.log("rd.tokensPerWeek          :", rd.tokensPerWeek(index - 1 * WEEK));
        console2.log("rd.tokensPerWeek          :", rd.tokensPerWeek(index - 2 * WEEK));
        console2.log("rd.tokensPerWeek          :", rd.tokensPerWeek(index - 3 * WEEK));
        console2.log("rd.tokensPerWeek          :", rd.tokensPerWeek(index - 4 * WEEK));
        console2.log("rd.tokensPerWeek          :", rd.tokensPerWeek(index - 5 * WEEK));
        console2.log("rd.tokensPerWeek          :", rd.tokensPerWeek(index - 6 * WEEK));
        console2.log("rd.tokensPerWeek          :", rd.tokensPerWeek(index - 7 * WEEK));
        console2.log("rd.tokensPerWeek          :", rd.tokensPerWeek(index - 8 * WEEK));
        console2.log("rd.tokensPerWeek          :", rd.tokensPerWeek(index - 9 * WEEK));
    }

Last updated