Summary
This is a sophisticated attack scenario, exploiting the rounding issues and the interplay between staking, withdrawal, and priority pools.
Attackers can exploit rounding errors in share and token amount calculations in the withdrawal process (withdrawal pool), creating a Denial of Service (DOS) condition.
The performUpkeep
function, responsible for processing withdrawals, can be manipulated when attackers donate small, dust amounts of tokens, causing discrepancies between the system’s token balance and share calculations
Vulnerability Details
An attacker can exploit rounding down issues in share and token amount calculations during the `performUpkeep` process. By donating dust amounts of tokens and manipulating withdrawals, the attacker can cause discrepancies between the actual token balance, TOTAL QUEUED witdrawals and the system’s calculated shares . This manipulation will result in failed or reverted withdrawals due to mismatches between requested and available token amounts. Furthermore, the attacker can block the system’s ability to process legitimate withdrawals from the staking pool, creating a DOS (Denial of Service) condition.
-
Attacker calls deposit with dust amount when total queued Withdrawal is available and Total Queued deposit is 0.
function _deposit(
address _account,
uint256 _amount,
bool _shouldQueue,
bytes[] memory _data
) internal {
if (poolStatus != PoolStatus.OPEN) revert DepositsDisabled();
uint256 toDeposit = _amount;
@audit>> 1. >> if (totalQueued == 0) {
uint256 queuedWithdrawals = withdrawalPool.getTotalQueuedWithdrawals();
if (queuedWithdrawals != 0) {
uint256 toDepositIntoQueue = toDeposit <= queuedWithdrawals
? toDeposit
: queuedWithdrawals;
@audit>> 1. >> withdrawalPool.deposit(toDepositIntoQueue);
toDeposit -= toDepositIntoQueue;
IERC20Upgradeable(address(stakingPool)).safeTransfer(_account, toDepositIntoQueue);
}
-
Deposit in Withdrawal Pool - disrupt total queued shares and lst balance in pool
* @notice Deposits asset tokens in exchange for liquid staking tokens, finalizing withdrawals
* starting from the front of the queue
* @param _amount amount of tokens to deposit
*/
function deposit(uint256 _amount) external onlyPriorityPool {
@audit>> 2. >>> token.safeTransferFrom(msg.sender, address(this), _amount);
@audit>> 2. attacker gets token >>> lst.safeTransfer(msg.sender, _amount);
@audit>> 2. note>>> _finalizeWithdrawals(_amount);
}
3. Finalze withdrawal will not reduce total shares since towithdraw is rounded down to 0.
* @notice Finalizes withdrawal accounting after withdrawals have been executed
* @param _amount amount to finalize
*/
function _finalizeWithdrawals(uint256 _amount) internal {
@audit>> 3. note>>> uint256 sharesToWithdraw = _getSharesByStake(_amount);
uint256 numWithdrawals = queuedWithdrawals.length;
@audit>> 3. no reduction>>> totalQueuedShareWithdrawals -= sharesToWithdraw;
Total shares is intact but we have successfully reduced the amount of LST tokens in the POOL. This has 2 different scenarios but same impact from here.
A
The process continues in the pool and call is made to performUPKEEP. Total Queued is withdrawable so we can to withdrawal totala queued
* @notice Executes withdrawals if there is sufficient available withdrawal space
* @param _performData encoded list of withdrawal data passed to staking pool strategies
*/
function performUpkeep(bytes calldata _performData) external {
uint256 canWithdraw = priorityPool.canWithdraw(address(this), 0);
@audit>> 1. shares is not changed amount is higher than actual needed amount>> uint256 totalQueued = _getStakeByShares(totalQueuedShareWithdrawals);
if (
totalQueued == 0 ||
canWithdraw == 0 ||
block.timestamp <= timeOfLastWithdrawal + minTimeBetweenWithdrawals
) revert NoUpkeepNeeded();
timeOfLastWithdrawal = uint64(block.timestamp);
@audit>> 1. higher than actual needed amount>> uint256 toWithdraw = totalQueued > canWithdraw ? canWithdraw : totalQueued;
bytes[] memory data = abi.decode(_performData, (bytes[]));
@audit>> 1. higher amount>> priorityPool.executeQueuedWithdrawals(toWithdraw, data);
_finalizeWithdrawals(toWithdraw);
}
2.Reverts
* @notice Executes a batch of withdrawals that have been queued in the withdrawal pool
* @dev withdraws tokens from the staking pool and sends them to the withdrawal pool
* @param _amount total amount to withdraw
* @param _data list of withdrawal data passed to staking pool strategies
*/
function executeQueuedWithdrawals(
uint256 _amount,
bytes[] calldata _data
) external onlyWithdrawalPool {
@audit>> 2. higher LST amount passed REVERT>> IERC20Upgradeable(address(stakingPool)).safeTransferFrom(
msg.sender,
address(this),
_amount
);
stakingPool.withdraw(address(this), address(this), _amount, _data);
token.safeTransfer(msg.sender, _amount);
}
B.
Scenario 2,
since attacker has successfully caused an imbalance betweeen the pool LST Value and the value tracked via the sharestostake value in the staking pool,
The Last depositor's call will revert, breaking the deposit function
function _deposit(
address _account,
uint256 _amount,
bool _shouldQueue,
bytes[] memory _data
) internal {
if (poolStatus != PoolStatus.OPEN) revert DepositsDisabled();
uint256 toDeposit = _amount;
if (totalQueued == 0) {
@audit>> 1. Returns a higher amount after attack/rounding expliot>> uint256 queuedWithdrawals = withdrawalPool.getTotalQueuedWithdrawals();
if (queuedWithdrawals != 0) {
uint256 toDepositIntoQueue = toDeposit <= queuedWithdrawals
? toDeposit
: queuedWithdrawals;
@audit>> 1. request for more lst than availble>> withdrawalPool.deposit(toDepositIntoQueue);
toDeposit -= toDepositIntoQueue;
IERC20Upgradeable(address(stakingPool)).safeTransfer(_account, toDepositIntoQueue);
}
function deposit(uint256 _amount) external onlyPriorityPool {
token.safeTransferFrom(msg.sender, address(this), _amount);
@audit>> Reverts amount higher than available >> lst.safeTransfer(msg.sender, _amount);
_finalizeWithdrawals(_amount);
}
Impact
- **Denial of Service**: The upkeep function will be DOSed and funds will not be retrieved from the Staking pool.The entire withdrawal process can be halted, preventing legitimate users from withdrawing their funds.
Tools Used
Manual Review
Recommendations
1. **Manual Admin Correction of Batch Withdrawals**:
- Introduce a manual intervention mechanism allowing administrators to adjust discrepancies when automated withdrawal processes fail due to token balance in the withdrawal pool mismatches with totalstaked.
2. **Ensure Withdrawals Consider Token Balance**:
- Modify the `performUpkeep` function to always check the actual token balance(lst and token) before attempting withdrawals, adjusting the withdrawal amount if the calculated amount exceeds the available balance.
3. **Handle Rounding Issues in Shares Calculations**:
- Implement safeguards to prevent rounding down of shares and token amounts. This could include rounding up share calculations or introducing a minimum threshold for share transfers to avoid manipulation through dust donations.
4. **Finalize Withdrawals Based on Exact Balances**:
- Ensure that the `_finalizeWithdrawals` function uses the exact token balance rather than calculated shares to determine the amount to withdraw. This prevents discrepancies from affecting the withdrawal process and reduces the likelihood of reverts due to insufficient tokens.
---