When a user locks tokens, the contract computes a “point” consisting of a bias (initial voting power) and a slope (the rate at which that power decays). In a correct implementation (like Curve’s veCRV), the voting power at any later time is given by:
voting power = bias − slope × (elapsed time)
However, in this implementation the following happens:
The calculateAndUpdatePower(...)
function correctly computes both the bias and the slope using the RAACVoting library.
Immediately afterward, the contract writes a checkpoint that “freezes” the bias value (converted to a lower‑precision type, e.g. a uint128
) and then mints that many ve‑tokens.
Later, when the contract’s view functions (e.g. getCurrentPower
) are used to calculate the “current” voting power, they recompute decay using the stored slope and the elapsed time. But because:
The decay amount (slope × elapsed time divided by RAY) is very small compared to the precision of the stored bias
The decay may be rounded to zero. Even if some decay is computed, the checkpoint itself is never updated after the lock is created, so the “stored” value never reflects any decay.
In the lock
function the contract calls:
At this point the bias (which represents the full voting power at lock time) is stored, but it never “ticks” down. (The slope is computed and stored inside the user's point, but it isn’t used to update the checkpointed balance)
Later, when someone calls getCurrentPower
(via getVotingPower
), the contract does:
The bias is stored as an int128
even though the calculation uses “ray” precision (1e27). When converting the bias to a lower‑precision type (via uint256(uint128(point.bias))
), any decay less than 1 unit of that 128‑bit value is lost. For example, if the decay per day is only a small fraction of one unit in that 128‑bit space, then after one day the computed decay may round down to zero.
The checkpoint is only written once (in the lock
function) and never updated. Thus, the stored voting power does not “decay” as time passes, even though the view function computes a decayed value using the original timestamp and slope.
Voting power never decays, a user’s voting power remains artificially high throughout the lock duration. In protocols like Curve’s veCRV, voting power decays linearly so that a user’s influence (and boost on rewards) diminishes as the lock approaches its expiry. Here, if decay is not applied correctly, users can benefit from long-lasting, high voting power even when they have locked tokens for only part of the full period. This can be exploited to gain disproportionate influence in governance or reward distribution.
Add this test "BaseGaugeExploit.test.js" to the folder test/unit/core/governance/gauges/
:
Logs:
Manual Review, Hardhat.
Keep the bias and slope in “ray” precision (1e27) without immediately converting them to a low‑precision type when checkpointing.
Implement an automatic “checkpointing” mechanism that updates the stored voting power (by subtracting the decay computed via the slope and elapsed time) as time passes. This can be done on each call to getVotingPower
or via scheduled updates.
Adjust getCurrentPower
to ensure that the decay is computed in full precision before converting the result to the final unit. For example, avoid casting to uint128
too early or use a high‑precision fixed‑point math library throughout.
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.