The voting power tracking in GaugeController fails to properly update user votes when weights change. This compromises the integrity of the gauge voting system and could lead to incorrect reward distributions. The issue occurs in the vote() function where a user's voting power is not being properly validated against their current veToken balance. When a user votes for a gauge weight, their vote gets recorded but the contract fails to ensure the voting power remains consistent with their token holdings.
When we verifies that user votes are properly tracked here:
Here's how this plays out
The issue is that the contract records the new vote weight without checking if the user still has sufficient voting power to support that weight.
This could allow users to maintain voting influence even after their veToken balance has decreased. In a real scenario, this is like being able to keep voting rights after selling your shares.
The code assumed veToken balances would only decrease through time-decay or explicit unlocking. However, the veToken contract allows transfers in certain conditions, creating a disconnect between voting power and vote weight.
This built on Curve's gauge voting system but missed a key difference. RAAC's veToken has more flexible transfer conditions than veCRV. This creates an assumption gap similar to the Compound governance attack of 2021, but with gauge-specific implications.
Looking at the GaugeController acts as the democratic heart of RAAC's real estate price oracle system. When users lock RAAC tokens in the veRAACToken contract, they gain voting power to influence gauge weights, which directly impact real estate valuations and lending parameters.
The core issue emerges in the vote() function's interaction with veToken balances. Think of a voter who locks 1000 RAAC tokens to gain influence over the RWA gauge. They cast their vote, directing price oracle weights. However, even after their lock expires and their veToken balance drops to zero, their vote continues to shape protocol decisions. This creates a dangerous disconnect between actual stake and voting power.
Looking at the technical implementation, the GaugeController tracks votes through userGaugeVotes[user][gauge], but crucially fails to validate this against current veToken.getVotingPower(user). This allows "ghost votes" to persist, potentially directing millions in real estate valuations without any actual stake at risk.
The vote() function from GaugeController.sol.
The fact that while it checks current votingPower > 0, it doesn't validate that the new weight is <= votingPower, allowing votes to potentially exceed actual voting power.
Imagine a real estate voting system where property valuations are determined by stakeholder influence. The _updateGaugeWeight function acts as the vote tallying mechanism, calculating how much influence each voter has over property prices based on their locked tokens.
The current implementation has a flaw in how it handles vote weight calculations. When a user changes their vote, the function recalculates the gauge's total weight using this formula:
Think of this like a ballot box where votes aren't properly validated. A malicious voter could cast votes with artificially inflated votingPower, causing the newGaugeWeight to exceed realistic bounds. This directly impacts RAAC's real estate price oracle system, as gauge weights determine how much influence different price feeds have.
An attacker could manipulate property valuations by inflating their gauge weight, potentially affecting millions in collateralized loans. This isn't just about numbers, it's about the integrity of real estate price discovery in the protocol.
In the _updateGaugeWeight function where the weight delta calculation occurs.
Should be:
This ensures the gauge's total weight stays within safe bounds when aggregating voting power influence.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.