Core Contracts

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

Curve vault interaction is wrong resulting in not being able to withdraw from it

Summary

In LendingPool, the interaction with the Curve vault is wrongly implemented and it withdraws assets to the wrong address.

Vulnerability Details

Throughout the protocol, the _ensureLiquidity and _rebalanceLiquidity functions are called at different places, also explained by their natspec:

/**
* @notice Internal function to ensure sufficient liquidity is available for withdrawals or borrowing
* @param amount The amount required
*/
function _ensureLiquidity(uint256 amount) internal {
// if curve vault is not set, do nothing
if (address(curveVault) == address(0)) {
return;
}
uint256 availableLiquidity = IERC20(reserve.reserveAssetAddress).balanceOf(reserve.reserveRTokenAddress);
if (availableLiquidity < amount) {
uint256 requiredAmount = amount - availableLiquidity;
// Withdraw required amount from the Curve vault
// then rToken address sends the whole liquidity
_withdrawFromVault(requiredAmount);
}
}
/**
* @notice Rebalances liquidity between the buffer and the Curve vault to maintain the desired buffer ratio
*/
function _rebalanceLiquidity() internal {
// if curve vault is not set, do nothing
if (address(curveVault) == address(0)) {
return;
}
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);
}
emit LiquidityRebalanced(currentBuffer, totalVaultDeposits);
}

According to the protocol's needs, it deposits or withdraws from the Curve vault using these functions:

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

As we can see by the vault's interface, during deposit the receiver of the shares is address(this):

ICurveCrvUSDVault.sol
/**
* @notice Deposits assets into the vault
* @param assets Amount of assets to deposit
* @param receiver Address to receive the shares
* @return shares Amount of shares minted
*/
function deposit(uint256 assets, address receiver) external returns (uint256 shares);

However, when withdrawing, the protocol enters address(this) as the receiver of the assets and msg.sender as the owner of the assets:

ICurveCrvUSDVault.sol
/**
* @notice Withdraws assets from the vault
* @param assets Amount of assets to withdraw
* @param receiver Address to receive the assets
* @param owner Owner of the shares
* @param maxLoss Maximum acceptable loss in basis points
* @param strategies Optional specific strategies to withdraw from
* @return shares Amount of shares burned
*/
function withdraw(
uint256 assets,
address receiver,
address owner,
uint256 maxLoss,
address[] calldata strategies
) external returns (uint256 shares);

The owner of the shares should be address(this) as this is the address which holds the shares. Also, the receiver should be the rToken address as this is the address which holds all the assets. For example, as we can see in the withdraw function which calls _ensureLiquidity, it calls the ReserveLibrary.withdraw(..) function which in turn the rToken.burn(..) function which transfers the underlying asset amount to the caller:

LendingPool.sol
function withdraw(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
.
.
// Ensure sufficient liquidity is available
// @audit can force withdraw from vault with big amount but then it gets redeposited
_ensureLiquidity(amount);
// Perform the withdrawal through ReserveLibrary
@> (uint256 amountWithdrawn, uint256 amountScaled, uint256 amountUnderlying) = ReserveLibrary.withdraw(
reserve, // ReserveData storage
rateData, // ReserveRateData storage
amount, // Amount to withdraw
msg.sender // Recipient
);
.
.
ReserveLibrary.sol
function withdraw(ReserveData storage reserve, ReserveRateData storage rateData, uint256 amount, address recipient)
internal
returns (uint256 amountWithdrawn, uint256 amountScaled, uint256 amountUnderlying)
{
if (amount < 1) revert InvalidAmount();
// Update the reserve interests
updateReserveInterests(reserve, rateData);
// Burn RToken from the recipient - will send underlying asset to the recipient
@> (uint256 burnedScaledAmount, uint256 newTotalSupply, uint256 amountUnderlying) = IRToken(
reserve.reserveRTokenAddress
).burn(
recipient, // from
recipient, // receiverOfUnderlying
amount, // amount
reserve.liquidityIndex // index
);
.
.
rToken.sol
function burn(address from, address receiverOfUnderlying, uint256 amount, uint256 index)
external
override
onlyReservePool
returns (uint256, uint256, uint256)
{
.
.
_burn(from, amount.toUint128());
if (receiverOfUnderlying != address(this)) {
@> IERC20(_assetAddress).safeTransfer(receiverOfUnderlying, amount);
}

Impact

The protocol cannot withdraw funds from the Curve vault, only deposit, as it is called the withdraw function with wrong owner of the shares and also wrong receiver if the call would go through.

Tools Used

Manual review

Recommendations

Fix the _withdrawFromVault like this:

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

Lead Judging Commences

inallhonesty Lead Judge 4 months 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 4 months 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.