The RAAC protocol implements a voting escrow mechanism where users lock RAAC tokens to receive veRAAC, which grants them voting power. However, the contract does not correctly account for the gradual decrease in voting power due to the slope mechanism, which causes an overestimation of the total supply of veRAAC. This leads to an inaccurate distribution of rewards, where users receive fewer rewards than they are entitled to. The vulnerability arises because the contract uses a standard totalSupply() function to determine the total voting power, instead of dynamically adjusting it based on each user's decaying bias over time.
To understand this, we need to go over how totalSupply works in voting escrows. These are the formulae for calculating a user's bias and slope. The bias is the user's balance at a point in time. A user's bias goes down over time which means their voting power goes down over time. See below:
bias = (amountLocked / MAX_TIME ) times (lockEndTime - block.timestamp)
slope = amountLocked / MAX_TIME
The slope can also be described as the rate of decay of tokens per second. It signifies how much each token should drop per second. So if a user has a bias when they lock tokens with a timestamp recorded, we can calculate the the user's bias at any given point by comparing how much the token has decayed and subtracting it from their bias at the time tokens were initially locked.
currentBias= point.bias - (point.slope times (block.timestamp - point.timestamp))
This is what determines the user's bias. So all is well and good but now we know that the user's bias (balance) always goes down as time passes, the total supply of all veTokens obviously can't be gotten using the usual totalSupply function that an ERC20 contract uses. This is actually a problem that happened right here with RAAC and I will use their protocol to show why.
So in veRAACToken, this is how users lock RAAC to get veRAAC:
The main part we are focusing on is where users are minted veRAAC in the function. You can see that when a user creates a lock, their bias is calculated and then they are minted that bias which represents their balance. This works well but that voting power is only valid until 1 second has passed because of the slope. The slope implies that every second, the user's balance decreases so for example, say the bias was 100 and the slope (rate of decay) was 1. So once the user is minted veRAAC tokens, which are 100, once a second passes, that balance should be reduced to 99 to reflect their new bias. the totalsupply should also be updates to reflect this change which is very important because if the users lock duration is over but they haven't withdraw yet, their bias is 0 yet the totalSupply will still be 100 since they havent withdrawn which burns the tokens. This is where the exploit occurs. As a result, the user's tokens are still taken into consideration in totalSupply calculations when the tokens hold no power, they should no longer be considered in totalSupply calculations
RAAC did not implement voting escrow total supply mechanics and what this meant was that their totalSupply was inaccurate. This is how voting power was calculated in veRAACToken.sol.
which as we discussed doesn't factor slope changes in the calculation.
As a result, when user's are to claim rewards via FeeCollector::claimRewards, the rewards are first calculated via FeeCollector::_calculatePendingRewards:
As seen above, veRAACToken::getTotalVotingPower is called which we referenced above, simply returns the totalSupply of veRAAC tokens which is wrongly implemented as it doesn't account for each user's bias changes over time. As a result, the totalSupply value is inflated which leads to user's getting less rewards that intended.
This test was run in the FeeCollector.test.js file in the "Fee Collection and Distribution" describe block. To run this test, import the assert statement from chai at the top of the file for the test to run as expected
Users Receive Fewer Rewards Than They Deserve: Since the total supply of veRAAC is incorrectly high, each user's reward allocation is diluted. Users who have locked tokens for a long time may see their expected rewards significantly reduced.
Manual Review, Hardhat
Implement proper totalSupply mechanics that follow voting escrow methodology. See an example solution below:
The contract allows lock ends to be only on the exact timestamp a week ends (timestamp % 604800 == 0)
Upon a user creating a lock, the contract adds their bias and slope to the global variables.
Then it adds the user's slope to a mapping slopeChanges(uint256 timestamp => int256 slopeChange)
Any time a week's end is crossed, it applies all the necessary slope changes (decreases the global slope)
By doing all of the above, the contract makes sure that at all times the sum of all user balance equals exactly the total supply.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.