29106 - [SC - High] Insufficient Handling of Partial Failures in Wi...
Submitted on Mar 7th 2024 at 05:22:54 UTC by @cheatcode for Boost | Puffer Finance
Report ID: #29106
Report type: Smart Contract
Report severity: High
Target: https://etherscan.io/address/0xd9a442856c234a39a81a089c06451ebaa4306a72
Impacts:
Permanent freezing of funds
Description
Brief/Intro
The PufferVault
contract orchestrates asset management across different protocols, including interactions with external protocols like Lido and EigenLayer for staking and withdrawals. However, the current implementation lacks handling for partial failures in withdrawal requests, which could lead to inaccurate asset accounting.
Vulnerability Details
The contract facilitates withdrawals from protocols like Lido (claimWithdrawalsFromLido
) and initiates stETH withdrawals from EigenLayer. However, it assumes these operations are atomic—either entirely successful or unsuccessful. This assumption may not hold in all scenarios, especially in decentralized environments where partial failures or discrepancies in expected outcomes can occur (e.g., due to changes in protocol behavior, gas fluctuations, or execution limits).
The claimWithdrawalsFromLido
function iterates through an array of request IDs and claims each withdrawal individually from the Lido withdrawal queue. However, it does not track or handle the scenario where some withdrawals are successfully claimed while others fail. Similarly, when initiating stETH withdrawals from EigenLayer (initiateStETHWithdrawalFromEigenLayer
), the contract assumes that the requested amount will be successfully withdrawn.
Impact Details
If partial failures occur during withdrawal operations, the PufferVault
contract may report an inflated or inaccurate value of assets, as it does not account for the discrepancies between the expected and actual amounts received. This can mislead users and administrators about the true holdings of the vault and lead to financial loss.
Mitigation
Implement detailed tracking and verification of each step in the withdrawal and conversion process. This includes verifying the actual amounts received versus expected and adjusting the vault's internal accounting accordingly.
In this modified version of claimWithdrawalsFromLido
, we introduce two new mappings: claimedWithdrawals
to track claimed withdrawals (preventing duplicates), and withdrawalAmounts
to store the actual amount received for each withdrawal request. The function calculates the total claimed amount and updates the lidoLockedETH
accordingly, ensuring accurate accounting.
Alternatively, introduce logic to handle partial withdrawals gracefully. This could involve proportionally adjusting the user's claim in the case of a shortfall or providing the option to retry the withdrawal for the remaining amount.
In the modified initiateStETHWithdrawalFromEigenLayer
function, we account for the requested shares in the eigenLayerPendingWithdrawalSharesAmount
storage variable.
In the claimWithdrawalFromEigenLayer
function, we introduce logic to handle partial withdrawals. If the actual amount received is less than the expected amount, the function calculates the remaining shares based on the discrepancy and adds them back to the eigenLayerPendingWithdrawalSharesAmount
. This allows users to retry the withdrawal for the remaining amount. Additionally, an event PartialWithdrawal
is emitted to notify users about the partial withdrawal.
Proof of Concept
poc.py
In this example, the PufferVault
class manages the total assets, Lido holdings, and EigenLayer holdings. The withdraw_from_lido
function simulates withdrawing assets from Lido, while the convert_to_eth_in_eigenlayer
function simulates converting stETH to ETH in EigenLayer, with a 30% chance of a partial failure.
When a partial failure occurs, the function simulates receiving only a portion (50-90%) of the expected ETH amount. However, the current implementation does not account for this discrepancy, leading to inaccurate total asset tracking.
You can run this code, and you should see output similar to the following:
If you don't see error run it a couple of times until you hit the partial failure error.
In the example output above, a partial failure occurred during the conversion to ETH in EigenLayer, but the total assets reported by get_total_assets
does not reflect this discrepancy accurately.
Last updated