Core Contracts

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

Undecayed getTotalVotingPower leads to incorrect reward distribution in FeeCollector and inaccurate quorum

Description

The veRAACToken::getTotalVotingPower function returns the ERC20's totalSupply() which represents initial minted amounts without accounting for voting power decay over time. This creates a discrepancy between individual decayed voting powers (getVotingPower) and the undecayed total supply used in reward calculations.

The FeeCollector contract uses this inflated total to calculate reward shares, causing all users to receive smaller reward shares than deserved as their decayed voting powers are divided by an artificially high total as their own voting powers decay while the total remains constant.

The Governance contract uses this inflated total to calculate the quorum needed to decide on the proposal state, which would affect the outcome.

Proof of Concept

  1. User locks 1000 RAAC for 1 year (365 days)

  2. Initial voting power = 1000 * (365/1460) = 250 veRAAC

  3. TotalSupply = 250 veRAAC

  4. After 6 months:

    • Individual voting power decays to 125 veRAAC

    • TotalSupply remains 250 veRAAC

  5. FeeCollector calculates rewards using 125/250 = 50% share instead of 125/125 = 100% valid share

Add this test to test/unit/core/tokens/veRAACToken.test.js:

describe("Voting Power Calculations", () => {
it("shows undecayed getTotalVotingPower while individual voting power decays", async () => {
const amount = ethers.parseEther("1000");
const duration = 365 * 24 * 3600;
// Create initial lock
await veRAACToken.connect(users[0]).lock(amount, duration);
const initialTotal = await veRAACToken.getTotalVotingPower();
const initialPower = await veRAACToken.getVotingPower(users[0].address);
// Advance half the lock duration
await time.increase(duration / 2);
const midPower = await veRAACToken.getVotingPower(users[0].address);
const midTotal = await veRAACToken.getTotalVotingPower();
// Individual power decayed by 50%
expect(midPower).to.be.lt(initialPower);
// Total supply remains unchanged
expect(midTotal).to.equal(initialTotal);
// Simulate FeeCollector reward calculation (refer to https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/collectors/FeeCollector.sol#L486)
const totalDistributed = ethers.parseEther("100");
const badShare = (totalDistributed * midPower) / midTotal;
const correctShare = totalDistributed; // Should be 100% as only user
// Shows incorrect share due to inflated totalSupply
expect(badShare).to.be.lt(correctShare);
});
});

Impact

High Severity

  • Causes systematic reward distribution errors where early users receive smaller rewards than deserved as total supply inflates

  • Results of quorum to decide on the state of the governance proposal would be inaccurate, leading to undesired outcomes

Recommendation

  • Track decayed total supply

For example:

+ uint256 private _decayedTotalSupply;
function _updateVotingPower(address user, int128 powerDelta) internal {
+ _decayedTotalSupply = uint256(int256(int128(_decayedTotalSupply) + powerDelta));
}
function getTotalVotingPower() external view override returns (uint256) {
- return totalSupply();
+ return _decayedTotalSupply;
}
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.