Closed sherlock-admin4 closed 1 month ago
This issue doesn't make any sense, there are not two unpaid
functions. It is just the dssvest one that is called from distribute
to save the amount that the dssvest will pay. The reason to have to do this is because the vest
call doesn't return the vested value.
Mansa11
High
distribute
can be DOSed by an attackerSummary
The
VestedRewardsDistribution::distribute
can be bricked leading to users not able to earn rewards. https://github.com/sherlock-audit/2024-06-makerdao-endgame/blob/main/endgame-toolkit/src/VestedRewardsDistribution.sol#L152-L166 https://github.com/makerdao/dss-vest/blob/19a9d663bb3a2737f1f0c763365f1dfc6788aad2/src/DssVest.sol#L232-L241Description
Anyone can call
VestedRewardsDistribution.distribute
which callsStakingReward.notifyRewardAmount
and distributes the accumulated rewards to the users and sets a new periodFinish.When a user calls
distribute
, theunpaid
returns the amount of vested which is claimable and it makes a call to thedssVest.vest
with the said amount in order to mint the required gem tokens needed. ThedssVest.vest
also has an implementation that gets the currentunpaid
amount and compares it with the unpaid amount from thedistribute
function in order to get the minimum value.The vulnerability lies in the fact that a malicious user can advantage of this by DOSing the distribute function making it difficult or impossible for legit users to earn rewards.
The
unpaid
gotten indistribute
might be different from theunpaid
from thevest
, and in this case the minimum is being selected according to this line from thedssVest.vest
function ->amt = min(amt, _maxAmt);
From the above, the amount to be minted to the the
VestedRewardsDistribution
contract is based on theamt = min(amt, _maxAmt);
which selects the min between theamt
and_maxAmt
. In the case whereby the selected amount is theamt
, it mints a lesser amount to theVestedRewardsDistribution
when this happens, thedistribute()
tries to the initial amount to thestakingRewards
contract. However, the transaction is going to fail because the specified amount will be insufficient.Attack scenario
stake
some amountsdistribute
calls.distribute
distribute
calculates amount as -> amount = dssVest.unpaid(vestId);withdraw
his staked amountdssVest.vest
function is being calleddssVest.vest
=> amt = min(amt, _maxAmt); which is now lesser(minus malicious actors amount)dssVest.vest
mintsamt
tokens to theVestedRewardsDistribution
contractdistribution
fails due to contract not having enough tokens as the amount minted fromdssVest.vest
is lesser than what he is trying to sendImpact
distribute
can be DOSed making it very difficult or impossible for legit users to earn rewardsPOC
Recommendation
Remove the
unpaid
logic from thedistribute()
function and only use the one from thedssVest.vest