31079 - [SC - Critical] Claiming bribes for epochs you didnt vote for l...
Submitted on May 12th 2024 at 11:10:23 UTC by @infosec_us_team for Boost | Alchemix
Report ID: #31079
Report type: Smart Contract
Report severity: Critical
Target: https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol
Impacts:
Protocol insolvency
Description
This is a text-dense report we apologize in advance.
Summary
This report demonstrates how a user can claim rewards for an epoch he didn't vote for, causing solvency issues as the users who voted cannot receive their share of bribes.
A coded proof of concept can be found in the POC section.
Detailed Description
To understand the root issue, let's start by breaking down with bullet points an example:
"Alice" votes in epoch
1
."Bob" votes in epoch
1
.
The second epoch (
2
) starts.
"Alice" claims bribes from epoch
1
."Bob" votes in epoch
2
."Bob" claims bribes from epoch
1
.
A third epoch (
3
) starts.
Alice claims bribes from epoch
2
. (This shouldn't happen)"Bob" votes in epoch
2
."Bob" attempts to claim bribes from epoch
2
but fails with "ERC20: transfer amount exceeds balance" because Alice stole rewards from epoch2
and the Bribe became insolvent.
Why is this happening?
When claiming Bribe rewards the code does the following:
Reads the balance of "Alice" in the previously recorded checkpoint.
It doesn't check if "Alice" voted in that epoch.
Sends rewards to "Alice".
Creates a new checkpoint and stores "Alice" 's balance.
Then it all repeats in the next epoch.
Whether it is a veACLX
position with "maxLock" enabled (where voting power never decays) or a position that expires within a couple of epochs, everyone can claim rewards for epochs they haven't voted for as long as they have at least 1 checkpoint.
This leads to solvency issues as the users who voted cannot receive their share of bribes.
How to solve this?
The best solution is to keep track inside the Bribe
smart contract of every epoch that a veACLX
lock votes for, fortunately, this is easy to implement.
A new mapping must be added to the Bribe
smart contract like this:
We only update it when a user votes/pokes like this:
Then in the getRewardForOwner(uint256 tokenId, address[] memory tokens)
function of the Bribe
smart contract before sending rewards to the user, we check if the tokenId voted in the epoch that we are giving him rewards for.
Why couldn't the testGetRewardForOwner()
test detect this bug?
testGetRewardForOwner()
test detect this bug?This test was introduced on April 1 to detect if users can claim rewards in past epochs they didn't vote for causing solvency issues:
Log the block.timestamp using console2.log(block.timestamp);
after every time the test "moves the time to the next epoch".
You will realize, that it always prints the same timestamp. Time is not moving forward at all, the test always warps to the present.
But why?
Because the definition of newEpoch()
is:
If we never update the active period of the minter
, then "newEpoch()
" will always return the same value.
To fix this test and actually move forward to the next timestamp, add voter.distribute();
after the start of the second epoch like this:
The distribution mechanism updates the minter's active period.
Try to rerun the test. It will fail indicating that rewards can be claimed for epochs that a user has not voted for.
Impact Details
Solvency issues as the users that voted cannot receive their share of bribes.
Proof of Concept
This test reproduces the example given about "Bob" and "Alice", proving the solvency issues and how "Bob" can't receive his share of bribes.
Last updated