Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Valid

`_calculatePendingRewards` compares `totalsSupply` to decayed bias, lowering rewards significantly

Summary

_calculatePendingRewards compares totalsSupply to decayed bias, lowering rewards significantly

Vulnerability Details

To calculate the rewards _calculatePendingRewards uses users voting power and the total voting power

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/collectors/FeeCollector.sol#L479

function _calculatePendingRewards(address user) internal view returns (uint256) {
// The above is a represnetation of the current time voting power
// where as `getTotalVotingPower` is representing the total as all minted
// meaning that it assumes that all is at max original boos, i.e. not decaied
uint256 userVotingPower = veRAACToken.getVotingPower(user);
if (userVotingPower == 0) return 0;
uint256 totalVotingPower = veRAACToken.getTotalVotingPower();
if (totalVotingPower == 0) return 0;
uint256 share = (totalDistributed * userVotingPower) / totalVotingPower;
return share > userRewards[user]
? share - userRewards[user]
: 0;
}

However the issue is that these 2 are 2 very different variables:

getTotalVotingPower is the total bias without decay, as token are minted based on the initial bias (which doesn't factor the linear decay):

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/tokens/veRAACToken.sol#L212

function getTotalVotingPower() external view override returns (uint256) {
return totalSupply();
}
function lock(uint256 amount, uint256 duration) external nonReentrant whenNotPaused {
// ...
(int128 bias, int128 slope) = _votingState.calculateAndUpdatePower(
msg.sender,
amount,
unlockTime
);
uint256 newPower = uint256(uint128(bias));
_mint(msg.sender, newPower);
}

But getVotingPower accounts for the decay

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/tokens/veRAACToken.sol#L426

function getVotingPower(address account) public view returns (uint256) {
return _votingState.getCurrentPower(account, block.timestamp);
}

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/libraries/governance/VotingPowerLib.sol#L193

function getCurrentPower(
VotingPowerState storage state,
address account,
uint256 timestamp
) internal view returns (uint256) {
RAACVoting.Point memory point = state.points[account];
if (point.timestamp == 0) return 0;
if (timestamp < point.timestamp) {
return uint256(uint128(point.bias));
}
uint256 timeDelta = timestamp - point.timestamp;
// Calculate decay
int128 adjustedBias = point.bias;
if (timeDelta > 0) {
// Calculate decay per second and multiply by time delta
int128 decay = (point.slope * int128(int256(timeDelta))) / int128(int256(1));
adjustedBias = point.bias - decay;
}
return adjustedBias > 0
? uint256(uint128(adjustedBias))
: 0;
}

This means that the 2 are gonna be wrongly compared, which would lead to stuck rewards as the total combined biases factoring decay (getVotingPower for all users) will always be less then the total voting power not factoring decay (getTotalVotingPower).

Example

  1. There are 10k tokens, divided by 2 users where both have minted for 2 years and it's currently year 1

  2. Both have balance of 5k tokens, and the totalSupply is 10k

  3. However they both have voting power of 2.5k as 50% of the lock has already decayed

Only 50% of the rewards will be claimable as

// 1000 tokens are rewards
(totalDistributed * userVotingPower) / totalVotingPower
User1 - 1000 * 2.5k / 10k = 250
User2 - 1000 * 2.5k / 10k = 250
total - 500

Impact

Less rewards will be distributed
Funds will be bricked

Tools Used

Manual review

Recommendations

Use a different approach for reward distribution.

Updates

Lead Judging Commences

inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Validated
Assigned finding tags:

veRAACToken::getTotalVotingPower returns non-decaying totalSupply while individual voting powers decay, causing impossible governance quorums and stuck rewards in FeeCollector

inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Validated
Assigned finding tags:

veRAACToken::getTotalVotingPower returns non-decaying totalSupply while individual voting powers decay, causing impossible governance quorums and stuck rewards in FeeCollector

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.