31184 - [SC - Critical] Deflating the total amount of votes in a checkp...
Deflating the total amount of votes in a checkpoint, to steal bribes and create solvency issues
Submitted on May 14th 2024 at 10:19:20 UTC by @infosec_us_team for Boost | Alchemix
Report ID: #31184
Report type: Smart Contract
Report severity: Critical
Target: https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol
Impacts:
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Protocol insolvency
Description
Brief/Intro
This report demonstrates how an attacker can deflate the accounting of total votes in a Bribe to claim more tokens than he should, causing solvency issues as other users can't claim their share of bribes.
A coded PoC is included in the Proof of Concept section.
Vulnerability Details
Before diving deep let's recap what Bribe.deposit(...)
and Bribe.withdraw(...)
do.
The Bribe.deposit(...)
function increases the balanceOf[tokenId], totalVoting and creates a new voting checkpoint.
https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol#L303-L316
The function meant to have the opposite effect, Bribe.withdraw(...)
, decreases the balanceOf[tokenId] but doesn't decrease the totalVoting nor creates a voting checkpoint.
https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol#L319-L329
The only way to "decrease" the totalVoting is by resetting it to 0, calling Voter.distribute()
once every epoch.
Vote checkpoints are crucial. The total amount of votes is used to distribute rewards proportional to a user's balance.
The higher the number of total votes recorded in an epoch, not belonging to a specific user, the fewer rewards that user receives. The highly simplified pseudo-code is:
The happy path (where everything goes well) is:
But due to how Bribe.deposit(...)
, Bribe.withdraw(...)
and Voter.distribute(...)
work, the following attack vector is possible:
Attack vector
Description
If Alice front-runs the voter.distribute()
in a new epoch, and votes by calling Voter.vote(..)
, the following two functions are executed in Bribe
in this same order:
First,
Bribe.withdraw(..)
decreases Alice's balance, but does not decrease the value oftotalVoting
and does not create a voting checkpoint representing that there are fewer votes now.Then,
Bribe.deposit(..)
increases Alice's balance for the same number that was decreased, then increases thetotalVoting
(now is an inflated number), and creates a voting checkpoint.
Finally, the voter.distribute()
call is executed and resets the totalVoting
of this epoch to 0
.
Alice now has a deposit and balance in the current epoch, but the amount of totalVoting
for this epoch is 0
, as if no one has voted.
Quick recap: The higher the value of
totalVoting
relative to Alice's balance the less rewards Alice receive, the lower the amount oftotalVoting
relative to Alice's balance the more rewards Alice can claim.
Diagram of the attack
We think this diagram helps to understand:
Step-by-step description of the attack
Step 1- Alice front-runs the voter.distribute(..)
in a new epoch and votes again with X balance, inflating the value of totalVoting
for this epoch by X.
Step 2- voter.distribute()
resets to 0
the totalVoting
of this epoch.
Step-3 Bob votes with Y balance, and a new voting checkpoint is created, increasing the totalVoting
from 0 to Y.
The total amount of balance voted in this epoch is "X + Y" but the value of the voting checkpoint is Y, instead of "X + Y".
If Alice (or Bob) claims rewards, an inflated share of tokens is received, and the Bribe becomes insolvent.
Impact
Deflating the total amount of votes in a checkpoint, to steal bribes and create solvency issues
Proof of Concept
We are going to share 2 foundry tests, the first one is for the "happy path" and in the second one Alice front-runs the voter.distribute()
, then claim bribes, making the system insolvent and preventing Bob from claiming his shares.
Happy path PoC
Add this test to src/test/Voting.t.sol