Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Valid

Excess liquidity used when rebalancing `LendingPool` can never be recovered from vault

Summary

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.

Vulnerability Details

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:

uint256 totalDeposits = reserve.totalLiquidity; // Total liquidity in the system
uint256 desiredBuffer = totalDeposits.percentMul(liquidityBufferRatio);
uint256 currentBuffer = IERC20(reserve.reserveAssetAddress).balanceOf(reserve.reserveRTokenAddress);
if (currentBuffer > desiredBuffer) {
uint256 excess = currentBuffer - desiredBuffer;
// Deposit excess into the Curve vault
_depositIntoVault(excess);
} else if (currentBuffer < desiredBuffer) {
uint256 shortage = desiredBuffer - currentBuffer;
// Withdraw shortage from the Curve vault
_withdrawFromVault(shortage);
}

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:

function _depositIntoVault(uint256 amount) internal {
IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);
curveVault.deposit(amount, address(this));
totalVaultDeposits += amount;
}

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:

function _withdrawFromVault(uint256 amount) internal {
curveVault.withdraw(amount, address(this), msg.sender, 0, new address[](0));
totalVaultDeposits -= amount;
}

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.

Impact

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.

Tools Used

Manual review.

Recommendations

Ensure that the correct owner of shares is set when _withdrawFromVault() is called, which is the LendingPool itself:

function _withdrawFromVault(uint256 amount) internal {
- curveVault.withdraw(amount, address(this), msg.sender, 0, new address[](0));
+ curveVault.withdraw(amount, address(this), address(this), new address[](0));
totalVaultDeposits -= amount;
}

Relevant links

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

LendingPool::_withdrawFromVault incorrectly uses msg.sender instead of address(this) as the owner parameter, causing vault withdrawals to fail

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

LendingPool::_withdrawFromVault incorrectly uses msg.sender instead of address(this) as the owner parameter, causing vault withdrawals to fail

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.