Open code423n4 opened 1 year ago
Picodes marked the issue as primary issue
We have a limit on maxNonRedeemedUserRequestCount
, which makes sure that attacker can't request more than the limit
https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/UserWithdrawalManager.sol#LL101C58-L101C88
manoj9april marked the issue as sponsor disputed
@manoj9april to me the issue is still valid as DoS can still happen at the level of maxNonRedeemedUserRequestCount
. See for example the duplicates https://github.com/code-423n4/2022-06-stader-findings/issues/224 or https://github.com/code-423n4/2022-06-stader-findings/issues/140.
Picodes marked the issue as satisfactory
Picodes marked the issue as partial-50
Picodes marked issue #140 as primary and marked this issue as a duplicate of 140
I believe this is not a duplicate of #140 (note, I've submitted both issues, see #224)
Let me explain:
maxNonRedeemedUserRequestCount
check not useful. They can easily circumvent it by splitting their deposits across multiple accounts they control, and making small withdrawals in each of them. This strategy will ultimately clog the system, if they really want tomaxNonRedeemedUserRequestCount
(1000) is much greater than finalizationBatchLimit
(50), making the attack easier to perform@DadeKuma thanks for your comment. My initial reasoning was that both issues show that the whole idea of using a queue is very prone to griefing attacks, on both a user level as shown by #140, and at a protocol level as shown by #224.
Furthermore, I believe the fix you are proposing here is basically to stop using a FIFO queue to treat withdrawal requests. As long as there is no FIFO, there is no need to limit the number of requests per user, so the other issue is also fixed.
@Picodes The main issue here is using a global/shared queue instead of a user-based one (the queue should not be removed, it can make sense, for example, to avoid bank runs), so the solution here was to have a queue for each user instead.
This was the solution I proposed:
Consider refactoring the system from a push to a pull system, where each user can finalize only their own requests.
This eliminates the problem of anyone being able to clog the global queue.
The issues are essentially different between the two cases because, even with the previous solution, an attacker can still clog the withdrawal queue of another user, as there are no restrictions on who can request a withdrawal (described in #140).
Furthermore, fixing #140 does not resolve this issue, as it remains vulnerable to the attack described in my previous comment.
@DadeKuma sorry I am not sure to understand. How is a user-based queue useful to protect from bank runs? Unless I am missing something a user-based queue is like no queue at all. It seems to me that either you use a global queue, either it's useless. This being said, if I were the sponsor, I'd keep the current system and acknowledge the fact that a global queue can be clogged
@Picodes
How is a user-based queue useful to protect from bank runs? Unless I am missing something a user-based queue is like no queue at all.
By limiting the amount of how much a user can withdraw at once. When requesting a withdrawal, there is a max amount:
if (assets < staderConfig.getMinWithdrawAmount() || assets > staderConfig.getMaxWithdrawAmount()) {
revert InvalidWithdrawAmount();
}
https://github.com/code-423n4/2023-06-stader/blob/main/contracts/UserWithdrawalManager.sol#L98-L100
The user can't finalize their requests until a minimum delay has passed:
if (
(ethToSendToFinalizeRequest + minEThRequiredToFinalizeRequest > pooledETH) ||
(userWithdrawInfo.requestBlock + staderConfig.getMinBlockDelayToFinalizeWithdrawRequest() >
block.number)
) {
break;
}
https://github.com/code-423n4/2023-06-stader/blob/main/contracts/UserWithdrawalManager.sol#L137-L143
So in this specific period of time, the user is limited to withdrawing a maximum of maxNonRedeemedUserRequestCount * staderConfig.getMaxWithdrawAmount()
, instead of everything.
The sponsor can monitor the number of withdrawal requests, and may decide to pause the contract before they are finalized, to prevent bank runs:
function finalizeUserWithdrawalRequest() external override nonReentrant whenNotPaused {
https://github.com/code-423n4/2023-06-stader/blob/main/contracts/UserWithdrawalManager.sol#L117
This won't be possible without the queue, as a max amount would not make sense (a user could simply send a single transaction with N withdrawal requests), so users would be able to withdraw the funds instantly, potentially causing a bank run.
So here you're saying that the bank-run mitigation is the delay + the limit, not really the "queue", right? And you're saying that the bank-run scenario can be mitigated as the admin can pause.
I disagree with this take and the fact that this is a better solution. Here, the global queue is made to preserve the fairness of the system: the first to request a withdrawal will be processed first.
As long as you remove this global queue and you allow individual users to finalize their own requests, then you will for sure have front-running / bank-run issues because when the withdrawal flow is higher than what the eth exit queue can process, then non-advanced users will always be front-ran by more advanced players and they will effectively be facing a DoS has there will never be enough funds in the system for them to finalize their withdrawals.
So in my opinion all the possible safeguards (min / max withdrawal size, max withdrawal queue per user, max batch size) have already been implemented by stader's team and this issue is of NC severity
Picodes marked the issue as not a duplicate
@sanjay-staderlabs tagging your for visibility
Picodes marked the issue as primary issue
Picodes changed the severity to QA (Quality Assurance)
Lines of code
https://github.com/code-423n4/2023-06-stader/blob/main/contracts/UserWithdrawalManager.sol#L132-L151
Vulnerability details
Impact
An attacker can create a large number of small withdrawal requests in
UserWithdrawalManager
, which implements a push architecture for withdrawals.This can cause a loss for other users, especially if the attacker doesn't finalize their withdrawals, as they will be forced to pay for the finalization if they want to receive their own funds.
Proof of Concept
The withdrawal system works in the following way:
requestWithdraw
finalizeUserWithdrawalRequest
finalizationBatchLimit
requests that can be finalized with each request:An attacker can simply split their withdrawal into a lot of small
requestWithdraw
that will clog this system, resulting in losses for other users, as they are forced to finalize the attacker's withdrawal requests.Tools Used
Manual review
Recommended Mitigation Steps
Consider refactoring the system from a push to a pull system, where each user can finalize only their own requests.
Assessed type
Timing