Open code423n4 opened 1 year ago
kirk-baird marked the issue as primary issue
This is a valid edge case where calling drip before adding the locked funds will result in these locked funds not being distributed to previous stakers.
I do not believe this meets the criteria for medium severity as there are no requirements for an owner, when adding more funds, to use the new funds to pay previous stakers. While it may be desirable to have locked funds pay previous stakers this is a design decision where it is valid not to action this issue.
kirk-baird changed the severity to QA (Quality Assurance)
kirk-baird marked the issue as grade-a
kirk-baird marked the issue as not confirmed for report
vaporkane marked the issue as sponsor confirmed
Lines of code
https://github.com/code-423n4/2023-05-xeth/blob/main/src/wxETH.sol#L146-L157
Vulnerability details
Adding locked funds may undercut current stakers rewards due to
drip
being called before totalFunds is increasedA call to
addLockedFunds()
can potentially undercut staking rewards since the dripping process is triggered before the actual balance oflockedFunds
is increased.Impact
The wxETH contract works as a vault in which holders of xETH can stake their tokens to earn rewards. These rewards are implemented through a "dripping" mechanism in which amounts of xETH provided by the protocol owners (coming from different sources of revenue) are "dripped" each block.
After the drip process is started, re-adding funds to the vault can lead to an scenario in which current stakers rewards are undercut due to how the implementation of
addLockedFunds()
works.https://github.com/code-423n4/2023-05-xeth/blob/main/src/wxETH.sol#L146-L157
This function implements the
drip
modifier which basically calls_accrueDrip()
before executing the body of the function:https://github.com/code-423n4/2023-05-xeth/blob/main/src/wxETH.sol#L222-L256
As we can see in the previous snippets of code, the dripping mechanism is triggered before the actual locked funds are increased, as the
_accrueDrip()
function is executed before the main body ofaddLockedFunds()
. In particular, this means that line 236, which caps the drip amount to the available locked funds, is executed before line 154, which increases the funds.This can lead to an undesired scenario where current stakers are not rewarded in full. To better visualize the issue, let's say the drip rate is set to 1, and X amount of funds are locked in block
b1
. Given the current conditions, funds will last forb1 + X
blocks. Now suppose we are at blockb2
such thatb2 > b1 + X
, and the protocol owners add funds to the contract by callingaddLockedFunds()
to fulfill the rewards. Because_accrueDrip()
is executed before,dripAmount
will beb2 - b1 = Y + b1 + X - b1 = Y + X
beingY > 0
the difference ofb2
andb1+X
. This means thatdripAmount > lockedFunds
is true, and consequentlydripAmount
gets capped tolockedFunds
. As a conclusion, the rewards for the Y period are not paid, even though that technically funds are being added and balance should be enough to pay these rewards.Note that this can also inadvertently stop the dripping functionality. When
dripAmount
is capped atlockedFunds
, line 241 will setlockedFunds
to zero, which means that lines 248-251 will disable the dripping mechanism.Proof of concept
The following test demonstrates the issue. Here, the drip rate is configured to be 1e18 per block and the initial amount of locked funds is 3e18. Once 5 blocks have passed, the owner refills the contract by calling
addLockedFunds()
. Even though funds are enough, Alice will receive rewards for only 3 blocks when unstaking. Note that the dripping has been disabled too.Note: the snippet shows only the relevant code for the test. Full test file can be found here.
Recommendation
Remove the
drip
modifier from theaddLockedFunds()
function. The call to_accrueDrip()
is not technically needed here, and removing it will solve the issue.Assessed type
Other