Core Contracts

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

calculateBoost will calculate boost wrong

Summary

calculateBoost calculates boos wrongly as it compares 2 different variables

Vulnerability Details

Before moving into the bug I need to explain away 2 variables first:

1 userVotingPower is the current bias the user has. It would further imply that userVotingPower decays with the bias. If a user has 1000 bias for 1 year lock, after the 6th months his bias (and userVotingPower) would be 500

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/governance/boost/BoostController.sol#L151

uint256 userVotingPower = veToken.getVotingPower(user, block.timestamp);

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);
}
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) {
int128 decay = (point.slope * int128(int256(timeDelta))) / int128(int256(1));
adjustedBias = point.bias - decay;
}
return adjustedBias > 0
? uint256(uint128(adjustedBias))
: 0;
}

2 totalVotingPower is the important one here, where we can see that it's just the veToken totalSupply.

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/governance/boost/BoostController.sol#L97

(uint256 totalWeight, uint256 totalVotingPower, uint256 votingPower) = updateTotalWeight();

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/governance/boost/BoostController.sol#L385

function updateTotalWeight() internal view returns (
uint256 totalWeight,
uint256 totalVotingPower,
uint256 votingPower
) {
return (
veToken.getLockPosition(address(this)).amount,
veToken.getTotalVotingPower(), // totalSupply()
veToken.getVotingPower(address(this), block.timestamp)
);
}

Simply put when tokens are locked we mint veTokens representing the initial bias (the full one without factoring the decay).

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

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);

With the above 2 in mind we come to the conclusion that userVotingPower is a decaying variable, while totalVotingPower is all of the biases combined without factoring the decay.

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/governance/boost/BoostController.sol#L141

function calculateBoost( ... ) external view override returns (uint256 boostBasisPoints, uint256 boostedAmount) {
(uint256 totalWeight, uint256 totalVotingPower, uint256 votingPower) = updateTotalWeight();
uint256 userVotingPower = veToken.getVotingPower(user, block.timestamp);
// ...
// totalVotingPower is totalSuppy, while userVotingPower is the current bias ?
// there doesn't actually exist totalVotingPower as most of it has decayed
// boost - 10k + (veBalance * 1e18 / totalVeSupply) * 15k / 1e18
// boosted amount - boost * amount / 10k
return BoostCalculator.calculateTimeWeightedBoost(
params,
userVotingPower,
totalVotingPower,
amount
);
}

After knowing this we could see that the above function makes an error when it uses these 2 different variables to calculate the boost. This will lead to all users having way less boost, as all combined userVotingPower will be less than totalVotingPower and by a huge margin.

Impact

Boost is calculated wrong
Users will always have way less boost
total userVotingPower < totalVotingPower - invariant broken
Boost is used in number of instances to calculate rewards, which would mean that user will receive less in general and some rewards may end up stuck.

Tools Used

Manual review

Recommendations

Either don't use current bias and use user balance or have a variable to track global decay (really hard) and use it instead of totalVotingPower

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 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 7 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.

Give us feedback!