Users will steal excess funds from the Vault due to VaultPoolLib::redeem() not always decreasing self.withdrawalPool.raBalance and self.withdrawalPool.paBalance
Summary
Vault::redeemExpiredLv() calls VaultLib::redeemExpired(), which allows users to withdraw funds after expiry, even if they have not requested a redemption. This redemption happens in VaultPoolLib::redeem(), when userEligible < amount, it calls internally __redeemExcessFromAmmPool(), where only self.ammLiquidityPool.balance is reduced, but not self.withdrawalPool.raBalance and self.withdrawalPool.paBalance. As such, when calculing the withdrawal pool balance in the next issuance on VaultPoolLibrary::reserve(), it will double count all the already withdraw self.withdrawalPool.raBalance and self.withdrawalPool.paBalance, allowing users to withdraw the same funds twice.
Root Cause
In VaultPoolLib::__redeemExcessFromAmmPool(), self.withdrawalPool.raBalance and self.withdrawalPool.paBalance are not decreased, but ra and pa are also withdrawn from the withdrawal pool when the user has partially requested redemption.
Internal pre-conditions
- User requests redemption of an amount smaller than the total withdrawn in
VaultLib::redeemExpired(), that is,userEligible < amount.
External pre-conditions
None.
Attack Path
- User calls
Vault::redeemExpiredLv(), withdrawing from the withdrawal pool, butself.withdrawalPool.raBalanceandself.withdrawalPool.paBalanceare not decreased. - A new issuance starts, and in
VaultPoolLib::reserve(), the funds are double counted as not all withdrawals were reduced. - As such,
self.withdrawalPool.raExchangeRateandself.withdrawalPool.paExchangeRatewill be inflated by double the funds and users will redeem more funds than they should, leading to the insolvency of the Vault.
Impact
Users steal funds while unaware users will not be able to withdraw.
PoC
__tryRedeemExcessFromAmmPool() does not decrease the withdrawn self.withdrawalPool.raBalance and self.withdrawalPool.paBalance.
function __tryRedeemExcessFromAmmPool(VaultPool storage self, uint256 amountAttributed, uint256 excessAmount)
internal
view
returns (uint256 ra, uint256 pa, uint256 withdrawnFromAmm)
{
(ra, pa) = __tryRedeemfromWithdrawalPool(self, amountAttributed);
withdrawnFromAmm =
MathHelper.calculateRedeemAmountWithExchangeRate(excessAmount, self.withdrawalPool.raExchangeRate); //@audit PA is ignored here
ra += withdrawnFromAmm;
}Mitigation
Replace tryRedeemfromWithdrawalPool() with redeemfromWithdrawalPool().
function __tryRedeemExcessFromAmmPool(VaultPool storage self, uint256 amountAttributed, uint256 excessAmount)
internal
view
returns (uint256 ra, uint256 pa, uint256 withdrawnFromAmm)
{
(ra, pa) = __redeemfromWithdrawalPool(self, amountAttributed);
withdrawnFromAmm =
MathHelper.calculateRedeemAmountWithExchangeRate(excessAmount, self.withdrawalPool.raExchangeRate); //@audit PA is ignored here
ra += withdrawnFromAmm;
}