The GaugeController
contract facilitates voting on gauges by users with sufficient voting power—derived from their veRAAC token holdings. When a user votes, the contract calls the internal function _updateGaugeWeight
to adjust the gauge's weight using the formula:
uint256 newGaugeWeight = oldGaugeWeight - (oldWeight * votingPower / WEIGHT_PRECISION) + (newWeight * votingPower / WEIGHT_PRECISION);
This formula is designed to subtract the user's previous vote contribution and add the new contribution based on their current voting power. Under the assumption that the user's voting power remains unchanged, the math works correctly. However, if the user’s voting power increases—due either to additional tokens locked (which increases veRAAC) or due to decaying weight not being recalculated properly, the formula does not account for the delta (i.e., the change in voting power).
For example, consider the following scenario:
Initially, a user votes with weight = 100
and a voting power of 100000e18
, resulting in a gauge weight of:
newGaugeWeight = 0 - (0) + (100 * 100000e18 / 10000) = 1000e18.
Later, the user increases their voting power to 10_000_000e18
and changes their vote weight to 5000
. The intended calculation is:
newGaugeWeight = 1000e18 - (100 * 10_000_000e18 / 10000) + (5000 * 10_000_000e18 / 10000).
However, due to left-to-right evaluation in Solidity, the subtraction 1000e18 - (100 * 10_000_000e18 / 10000)
underflows before the addition is applied. Moreover, the formula does not incorporate the change in voting power (delta), leading to a gauge weight that does not correctly represent the user's effective voting contribution.
In addition, the contract retrieves voting power via veRAACToken.balanceOf(msg.sender)
, which may return a stale value that does not account for decay in voting power. This further compounds the problem by using an inaccurate measure in the weight update.
GaugeController::vote
This function allows a user to cast a vote for a gauge:
GaugeController::_updateGaugeWeight
This internal function updates the gauge’s weight using the following formula:
Highlighted Issue:
Formula Limitation:
The formula subtracts the prior vote contribution (oldWeight * votingPower / WEIGHT_PRECISION
) and adds the new vote contribution (newWeight * votingPower / WEIGHT_PRECISION
). If the user’s voting power has increased between votes, the subtraction is performed using the current voting power, causing an arithmetic underflow due to left-to-right evaluation in Solidity.
Stale Voting Power:
The function uses veRAACToken.balanceOf(msg.sender)
to determine voting power. This method may not factor in decay (since decay is handled via a separate mechanism) and thus can yield a stale value.
Lack of Delta Handling:
The formula does not calculate the delta in voting power—that is, the difference between the previous and current voting power is not accounted for, leading to an inaccurate gauge weight update.
Arithmetic Underflow:
With an increase in voting power, the subtraction part of the expression may underflow, causing the transaction to revert and leading to a denial of service for users attempting to update their votes.
Inaccurate Gauge Weight:
Even if underflow did not occur, the gauge weight would be calculated incorrectly. This misrepresentation affects reward distribution and governance decisions since gauge weights determine the allocation of rewards.
Stale Data Impact:
Relying on balanceOf
rather than a dynamic measure like getVotingPower
can further distort the gauge weight, leading to long-term inconsistencies in the system's state.
Initial Vote:
A user (e.g., PATRICK) locks tokens and obtains an initial voting power of 100000e18
.
PATRICK votes with a weight of 100
.
The gauge weight is updated as:
newGaugeWeight = 0 - (0 * 100000e18 / 10000) + (100 * 100000e18 / 10000) = 1000e18.
Voting Power Increase and Vote Change:
PATRICK increases his locked tokens, boosting his voting power to 10_000_000e18
.
He now changes his vote weight to 5000
.
The gauge weight update is computed as:
newGaugeWeight = 1000e18 - (100 * 10000000e18 / 10000) + (5000 * 10000000e18 / 10000).
Due to left-to-right evaluation, the subtraction 1000e18 - (100 * 10_000_000e18 / 10000)
underflows before the addition occurs.
Resulting Denial of Service:
PATRICK’s transaction reverts due to an arithmetic underflow error.
Consequently, every subsequent vote update that involves a change in voting power will fail, potentially leading to a denial of service for users.
Below is a complete Foundry test suite that demonstrates the issue:
Step 1: Create a Foundry project:
Step 2: Remove any unnecessary files.
Step 3: Convert your Hardhat project to a Foundry project by placing your contracts in the src
directory.
Step 4: Create a test
directory adjacent to your src
folder and add all necessary contract files and mocks.
Step 5: In the test
directory, create a test file (e.g., GaugeTest.t.sol
) and paste the above test suite.
Step 6: Run the test:
Expected Output:
You might notice that patrickVotingPower
is way much high than expected, why it happened? It happened due to a bug inside increase function...
veRAACToken::increase
The amount added twice to calculate new voting power. However, whatever a DoS still happens.
Short-Term Impact:
Arithmetic Underflow: Vote updates involving increased voting power trigger an underflow error, causing the vote transaction to revert.
Vote Failure: Users cannot update their vote if their voting power increases, leading to a denial of service on vote changes.
Long-Term Impact:
Reward Distribution Distortion: Gauge weights directly affect reward allocations; incorrect weights can lead to unfair distribution.
Governance Instability: The gauge system is used for governance; persistent inaccuracies can undermine trust and fairness.
User Frustration and Abuse: Users may be prevented from properly casting or updating votes, opening the door for potential manipulative or abusive behaviors.
To address these issues, the gauge weight update mechanism must account for changes (delta) in voting power. One solution is to store the user's last recorded voting power and compute the change, then update the gauge weight accordingly. For example:
_updateGaugeWeight
or if we want to have voting power impact in calculation then...
_updateGaugeWeight
Use Dynamic Voting Power:
Replace calls to veRAACToken.balanceOf(msg.sender)
with a function such as getVotingPower(msg.sender, block.timestamp)
that factors in decay.
Implement SafeMath:
Ensure that all arithmetic operations use safe math (or Solidity 0.8+ built-in overflow checks) to prevent underflow/overflow errors.
By incorporating these changes, the gauge weight update will correctly reflect both the change in vote weight and the delta in voting power, ensuring accurate reward distribution and robust governance functionality.
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.