Voting power decays linearly over time, but balanceOf(user) only updates during explicit interactions (e.g., extend(), withdraw()).
After a lock expires, users retain their full veRAAC balance until they call withdraw(), even though their voting power is zero.
A user with a 1-year lock will see their balanceOf() remain static while their actual voting power decays daily. This breaks governance and boost calculations.
Expected Behavior:
Alice's veRAAC balance should decay to 0 linearly over 4 years.
After 2 years, her balance should be 50 veRAAC.
Actual Behavior:
Alice's veRAAC balance remains 100 until she calls withdraw(), even though her voting power decays.
After 2 Years
Expected Voting Power:
100 * (remaining time) / total lock duration = 100 * 2y / 4y = 50 veRAAC.
Actual State:
balanceOf(alice) remains 100 (no automatic decay).
getVotingPower(alice) returns 50 (correctly calculated via _votingState).
She calls recordVote() for a proposal.
Voting Power Used: 50 (correct).
But: Her veRAAC balance still shows 100 (balanceOf(alice) = 100).
A rewards contract calls calculateBoost(alice, 100 RAAC).
Boost is calculated using 100 veRAAC instead of the true 50, giving Alice an unfair advantage.
Alice does not call withdraw().
State:
balanceOf(alice) = 100 (still!).
getVotingPower(alice) = 0 (correct).
balanceOf() is used externally to display voting power (e.g., in UIs), misleading users. Rewards are overpaid because boosts use stale ERC20 balances. Expired locks still contribute to totalSupply(), distorting system-wide metrics.
Foundry
Implement a _beforeTokenTransfer hook that decays balanceOf(user) to match their real-time voting power on every interaction (e.g., transfers, votes).
Ensure balanceOf(user) always reflects getVotingPower(user) by updating balances during state changes (e.g., block timestamp checks).
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.