Excess liquidity that is deposited into the CrvUsdVault
by the LendingPool
during rebalancing, cannot be withdrawn again due to an incorrect shareholder value in the withdraw()
function.
The LendingPool
in the RAAC protocol comes with a liquidity rebalancing feature to ensure that protocol's liquidity is utilized optimally. The idea is that, when the overall protocol liquidity exceeds the liquidityBufferRatio
(20%), any excess liquidity should be deposited into a CrvUsdVault
to generate additional yield for the protocol.
If, however, the available liquidity is below the liquidityBufferRatio
, then the protocol withdraws the necessary liquidity from the CrvUsdVault
to restore a healthy liquidity level.
This happens whenever users of the protocol deposit()
or borrow()
money via the _ensureLiquidity
and _rebalanceLiquidity()
functions.
Below is an excerpt of the _rebalanceLiquidity()
function that shows the crucial part:
This looks all well and good, but if we take a look at how _depositIntoVault()
and _withdrawFromVault()
are implemented, we'll see that there's an issue with the shareholder ownership:
Notice that curveVault.deposit()
expects the amount
to deposit and an owner
address. This address is the account that will receive the underlying shares of the vault, which are later burned when withdrawing the funds again.
What this means, is that address(this)
, which is the LendingPool
, becomes the owner of the underlying shares. This makes sense, because it's the LendingPool
that performs the deposit.
Let's take a look at _withdrawFromVault()
next:
In case of missing liquidity, the LendingPool
aims to draw liquidity from the CrvUsdVault
, hence, it calls curveVault.withdraw()
.
This function expects the caller to specify the amount
to withdraw, the receiver
of the funds, and most importantly, the owner
of the shares that will be burned. We can verify this by checking the source of the Vault3.vy
, which is the one that the protocol plans to use.
This is crucial, because the vault needs to ensure that one can only withdraw funds for which one has the underlying shares.
The problem here is that the protocol specifies msg.sender
as the owner of the shares. The msg.sender
is never going to be the account deposited into the vault in the first place, meaning it will never have any shares that can be burned by the vault, which in turn means, withdrawing any funds from that vault via this function will fail.
Given that _withdrawFromVault()
is called via _ensureLiquidity()
as well as _rebalanceLiquidity()
, which are used in LendingPool#deposit()
and LendingPool#borrow()
, this can lead to user funds becoming unaccessible by the protocol.
Manual review.
Ensure that the correct owner of shares is set when _withdrawFromVault()
is called, which is the LendingPool
itself:
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.