Core Contracts

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

Incorrect voting power calculation using undecayed balance enables unfair gauge voting

Incorrect voting power calculation using undecayed balance enables unfair gauge voting

Description

The BaseGauge::voteDirection function calculates voting power using veRAACToken.balanceOf() which returns the raw token balance without considering time-based decay. This allows users to maintain full voting power indefinitely, bypassing the intended linear decay mechanism implemented in veRAACToken::getVotingPower. The discrepancy creates an unfair advantage for voters who should have reduced influence over time.

Proof of Concept

Add to test file BaseGauge.test.js:

it("uses undecayed voting power for gauge votes", async () => {
// Deploy RAAC token with constructor parameters
const RAACToken = await ethers.getContractFactory("RAACToken");
const raacToken = await RAACToken.deploy(owner.address, 0, 0);
await raacToken.setMinter(owner.address);
// Deploy veRAACToken with RAAC address
const VeRAACToken = await ethers.getContractFactory("veRAACToken");
const veRAACToken = await VeRAACToken.deploy(raacToken.target);
// Mint RAAC to user1 and approve veRAACToken
await raacToken.mint(user1.address, ethers.parseEther("1000"));
await raacToken
.connect(user1)
.approve(veRAACToken.target, ethers.parseEther("1000"));
// Lock RAAC with valid duration (1 year minimum)
const MIN_LOCK_DURATION = 365 * 24 * 3600; // 1 year in seconds
await veRAACToken.connect(user1).lock(
ethers.parseEther("1000"),
MIN_LOCK_DURATION // Use minimum required duration
);
// Initial vote with full voting power
await baseGauge.connect(user1).voteDirection(5000);
const firstVote = await baseGauge.userVotes(user1.address);
expect(firstVote.weight).to.equal(ethers.parseEther("1000"));
const MONTH = 30 * 24 * 3600;
// Advance 3 months (voting power should decay)
await time.increase(3 * MONTH);
// Subsequent vote - should use undecayed balance
await baseGauge.connect(user1).voteDirection(6000);
const secondVote = await baseGauge.userVotes(user1.address);
// But test shows it remains 1000 due to using balanceOf()
expect(secondVote.weight).to.equal(ethers.parseEther("1000"));
});

Impact

Medium severity - Distorts governance outcomes by allowing voters to maintain inflated influence beyond intended decay schedule. While not directly risking fund loss, it undermines protocol's weighted voting mechanics and long-term incentive alignment.

Recommendation

  • Primary Fix: Replace balance check with time-decayed voting power:

- uint256 votingPower = IERC20(IGaugeController(controller).veRAACToken()).balanceOf(msg.sender);
+ uint256 votingPower = IVeRAACToken(IGaugeController(controller).veRAACToken()).getVotingPower(msg.sender, block.timestamp);
Updates

Lead Judging Commences

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

BaseGauge::_applyBoost, GaugeController::vote, BoostController::calculateBoost use balanceOf() instead of getVotingPower() for vote-escrow tokens, negating time-decay mechanism

Support

FAQs

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