Core Contracts

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

Quorum math is wrong

Summary

Quorum math is wrong as user cotes are bias with decay, but quorum is bias without decay. Thus even if 100% of the users voted, their voting power will be way less than the totalSupply of veRAAC, which is used for quorum math.

Vulnerability Details

1 When user cast votes getVotingPower is used to retrieve there voting power

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/governance/proposals/Governance.sol#L181

function castVote(uint256 proposalId, bool support) external override returns (uint256) {
// ...
uint256 weight = _veToken.getVotingPower(msg.sender);

Where user voting power is the current decayed bias. In simple terms if a user has 1000 bias at day 1 of his 1 year stake, on the 6th month he will have 500 bias, as the other 500 has linearly decayed.

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 0 if power has fully decayed
return adjustedBias > 0
? uint256(uint128(adjustedBias))
: 0;
}

2 When users lock their tokens they get minted 100% of their initial bias. The totalSupply also gets increased by that

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

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 veTokens
_mint(msg.sender, newPower);
}

With these 2 in mind we can see an issue appear inside Governance::state, where we are comparing currentQuorum, which is user votes (decayed bias) to quorum, which is a simple math equation multiplying veRAAC totalSupply by quorum percentage.

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/governance/proposals/Governance.sol#L288

function state(uint256 proposalId) public view override returns (ProposalState) {
// ...
ProposalVote storage proposalVote = _proposalVotes[proposalId];
uint256 currentQuorum = proposalVote.forVotes + proposalVote.againstVotes;
uint256 requiredQuorum = quorum();
if (currentQuorum < requiredQuorum || proposalVote.forVotes <= proposalVote.againstVotes) {
return ProposalState.Defeated;
}
function quorum() public view override returns (uint256) {
// veRAAC totalSupply * 4 / 100
return (_veToken.getTotalVotingPower() * quorumNumerator) / QUORUM_DENOMINATOR;
}

Example:

  1. There are 100k tokens minted

  2. The average user has locked for 2 years and it's currently year 1

  3. A proposal is made with 20% quorum

  4. Users vote on it, but the current active voting power that can be used for voting is 50k

  5. 15k of voting power is used, which is 30% of all active one

  6. This is compared against 100k * 20% = 20k and the vote fails to reach quorum

Impact

We are comparing active voting power against originally minted one. The comparison won't be correct, which will result in the failure of many proposals.
Quorums will be hard to reach.
Proposals with voting power more than the current quorum will sill struggle to pass.
Quorum math will always be wrong, this makes the bug more impactful as with time the difference between the 2 will expand even more, to a point where not proposals would be able to meet the quorum.

Tools Used

Manual review

Recommendations

Either have a variable to track decayed voting power, or user balances as voting power.

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!