A miscalculation in the logic that determines the value for totalUnbonded prevents it from ever being different than 0.
This prevents successful withdrawals from VaultDepositController::withdraw, and causes VaultControllerStrategy::getMinDeposits to always return totalDeposits, ultimately preventing WithdrawalPool::checkUpkeep to ever return true.
There are only two places where VaultControllerStrategy::totalUnbonded is updated : VaultDepositController::withdraw and VaultControllerStrategy::updateVaultGroups. As this report will show, both of those cannot update the value of totalUnbonded to another value than 0.
withdraw functionThe function VaultDepositController::withdraw begins by checking whether the withdrawal amount is greater than totalUnbonded.
Since totalUnbonded is always 0 due to a miscalculation, the function reverts whenever it's called, preventing any withdrawal from happening. This also prevents the assignation of totalUnbonded to another value in this function.
updateVaultGroups functionThe function VaultControllerStrategy::updateVaultGroups is called only by FundFlowController, for both operatorVCS and communityVCS. The function takes the parameter _nextGroupTotalUnbonded, which is the value we are setting to totalUnbonded.
For both operatorVCS and communityVCS, the variable passed as _nextGroupTotalUnbonded is calculated by the function FundFlowController::_getVaultUpdateData, which sets the variable nextGroupTotalUnbonded from the result of FundFlowController::_getTotalUnbonded.
The result variable in this function, totalUnbonded, is computed by adding the results of vault.getPrincipalDeposits() for each vault passed in parameter (each vault of the group to unbond) that is not removed and for which the claim period is active. However, since we are trying to get the totalUnbonded value of all vaults that we are about to unbond, none of these vault can have claimPeriodActive() return true, so the returned value of totalUnbonded will always be 0, which will later be set to VaultControllerStrategy::updateVaultGroups from the FundFlowController.
updateVaultGroups :FundFlowController::updateVaultGroups is called. This will start the unbonding for the next group of vaults.
The data for the next group of vaults is computed using _getVaultUpdateData, and returns 0 for the nextGroupOpVaultsTotalUnbonded and nextGroupComVaultsTotalUnbonded.
Vault groups are updated for operatorVCS and communityVCS.
The value of totalUnbonded is set to 0.
The miscalculation of totalUnbonded results in funds being permanently locked within vaults, as VaultDepositController::withdraw can never be executed successfully.
Additionally, StakingPool::canWithdraw will always return 0. Since WithdrawalPool::performUpkeep relies on this value being different to 0 to execute, the execution of the WithdrawalPool::performUpkeep function to always fail. While the WithdrawalPool can still execute withdrawals with deposits from the PriorityPool, the inability to run performUpkeep leads to the protocol not being able to finalize the withdrawals for users from the tokens of the staking pool.
The following PoC can be copy-pasted in test/core/priorityPool/, and run with :
npx hardhat test --grep "totalUnbonded is always 0"
The PoC proves the following points :
VaultControllerStrategy::totalUnbonded is always 0.
VaultDepositController::withdraw always revert.
WithdrawalPool::checkUpkeep returns false due to PriorityPool::canWithdraw consistently returning 0.
Manual review.
If FundFlowController::_getTotalUnbonded is edited to not ignore vaults that aren't in the claim period, this would fix the value of totalUnbonded for this vulnerability, but it may impact other parts of the protocol, such as FundFlowController::updateOperatorVaultGroupAccounting as it loops through all _vaultGroups and not only the next one to unbound.
To mitigate this, consider creating a separate function, _getTotalUnbondedForNextVaultGroup, to handle the unbonding process specifically.
The following code diff illustrates the recommended solution :
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.