31076 - [SC - Critical] checkpointTotalSupply can checkpoint before a t...
Submitted on May 12th 2024 at 10:57:42 UTC by @Holterhus for Boost | Alchemix
Report ID: #31076
Report type: Smart Contract
Report severity: Critical
Target: https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/RevenueHandler.sol
Impacts:
Permanent freezing of funds
Description
Brief/Intro
The checkpointTotalSupply()
function is in the RewardsDistributor
and is callable by anyone. The function has an incorrect comparison of >
instead of >=
, and this can lead to a checkpoint being recorded when a timestamp is not yet complete. This leads to mistakes in the internal accounting. In the worst case, a user can never successfully claim()
again, which permanently freezes their BPT
tokens that have been deposited into veALCX
.
Vulnerability Details
The implementation of _checkpointTotalSupply()
is as follows:
Notice that whenever veSupply[t]
is assigned to a value, the t += WEEK
increment happens immediately after, which permanently progresses the cursor so that veSupply[t]
will never be assigned to again. Also, notice that the veSupply[t]
assignment is only skipped if t > roundedTimestamp
. This is incorrect. It should also be skipped if t == roundedTimestamp
, otherwise this code can record the totalSupplyAtT()
value before the timestamp itself is complete. This means the check should actually be: if (t >= roundedTimestamp)
.
Impact Details
In the scenario when t == roundedTimestamp
, the code will incorrectly cache the totalSupplyAtT()
of the current timestamp early. Any actions taken in veALCX
after this (but on the same timestamp) will not be reflected in the veSupply[]
value. On the other hand, the _claimable()
function will correctly account for these last-second actions in each individual tokenId
. As a result, it is possible for the balanceOf
value below to contain deposits that did not contribute to the veSupply[weekCursor]
value:
In the worst-case scenario, the first user of VotingEscrow
can end up in a situation where veSupply[weekCursor] == 0
and balanceOf > 0
. In this case, the claim()
function will revert due to a division by zero, and it will permanently fail to progress past the broken week.
Since the withdraw()
function in the veALCX
contract has the following code:
it will always revert on this line, and users will be in a state where they are permanently unable to withdraw their BPT
tokens. See the PoC for an example.
References
See the PoC below.
Proof of Concept
I have created the following test file and added it to the tests/
directory:
Running the command forge test -vvv --match-test testCheckpointTotalSupplyBugFreezing --rpc-url $ETH_RPC_URL
gives the following result:
which shows that the user's attempt to call withdraw()
indeed reverts and their deposited BPT
tokens are permanently frozen.
Last updated