31163 - [SC - Critical] Malicious actor can acquire bribe rewards by bl...
Submitted on May 13th 2024 at 20:01:40 UTC by @DuckAstronomer for Boost | Alchemix
Report ID: #31163
Report type: Smart Contract
Report severity: Critical
Target: https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol
Impacts:
Permanent freezing of unclaimed yield
Theft of unclaimed yield
Description
Vulnerability Details
The Voter
contract uses the onlyNewEpoch
modifier for the reset()
and vote()
external functions, which prevents users from voting multiple times or revoting in the same epoch.
However, the poke()
function is missing the onlyNewEpoch
modifier, allowing the user to call it multiple times during an epoch. When poke()
is invoked, it internally calls _vote()
which resets previous votes first (https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol#L413) and then proceeds to apply the same amount of votes.
The _reset()
function perform the withdrawal of votes from a Bribe
contract (https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol#L396), while the _vote()
function deposit votes back (https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol#L441).
The Bribe
contract utilizes the deposit()
function to checkpoint the voting amount for the current Epoch by invoking _writeVotingCheckpoint()
(https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol#L313). However, in the withdraw()
function, it does not checkpoint the voting amount for the current Epoch. This is acceptable since users are required to vote or reset once in an Epoch. However, this assumption is invalid because of the poke()
function. Users can call it any number of times in an epoch.
A malicious user can call poke()
multiple times, inflating the value of votes made for an Epoch. Consequently, when calculating the amount of bribes a benign user should receive, the amount will be significantly lower.
Since the value of _prevSupply
will be quite big due to multiple calls to poke()
, the reward (prevRewards.balanceOf
) will be small. Consequently, reward tokens become trapped in the Bribe contract, causing users to miss out on their rewards.
This way an attacker can discourage users with significant voting power from participating in voting for a specific gauge and then later acquire trapped bribes in the following epochs.
Impact Details
Theft of unclaimed yield.
Permanent freezing of unclaimed yield.
Proof of Concept
POC scenario:
The bad guy has 99 times less voting power than the good guy.
The good guy normally should get 99K of BAL bribes.
However, the Bad guys call
poke()
2000 times.As a result of the attack, the good guy receives 15 times less reward.
Instructions:
Put Poc's code from below into the file
src/test/Voting.t.sol
- https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/test/Voting.t.sol.Run the Poc as follows:
forge test --mp src/test/Voting.t.sol --fork-url URL --fork-block-number BLOCK