Core Contracts

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

Misconfigured Liquidity Management in the `LendingPool` Contract’s Curve Vault Integration

Summary

The LendingPool contract contains incorrect logic in its liquidity management , specifically in the _depositIntoVault function. The issue arises because the contract attempts to deposit funds into the Curve Vault directly from address(this), which does not hold any crvUSD tokens. Instead, all crvUSD tokens are held in the reserve.reserveRTokenAddress. This mismatch in token ownership can lead to failed transactions, incorrect accounting, and potential loss of funds.

Vulnerability Details

In the _depositIntoVault function of the LendingPool contract:

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

The contract assumes that address(this) holds the crvUSD tokens, but in reality, all crvUSD tokens are stored in reserve.reserveRTokenAddress. Since address(this) does not hold the tokens, the deposit call to the Curve Vault will fail, leading to transaction reverts and potential disruptions in the protocol's liquidity management.

  1. Initial State:

    • reserve.reserveRTokenAddress holds 1000 crvUSD.

    • address(this) holds 0 crvUSD.

    • totalVaultDeposits is 0.

  2. Action:

    • A user calls deposit with 500 crvUSD.

    • The _rebalanceLiquidity function is triggered, which calls _depositIntoVault to deposit 500 crvUSD into the Curve Vault.

  3. Expected Behavior:

    • The Curve Vault should receive 500 crvUSD from reserve.reserveRTokenAddress.

    • totalVaultDeposits should be updated to 500.

  4. Actual Behavior:

    • The deposit call fails, and the transaction reverts.

Impact

User deposits crvUSD into the lending pool to mint rTokens.

function deposit(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
// Update the reserve state before the deposit
ReserveLibrary.updateReserveState(reserve, rateData);
// Perform the deposit through ReserveLibrary
uint256 mintedAmount = ReserveLibrary.deposit(reserve, rateData, amount, msg.sender);
// Rebalance liquidity after deposit
_rebalanceLiquidity();
emit Deposit(msg.sender, amount, mintedAmount);
}

In the deposit, transfer all assets from the caller to the RToken contract and mint RToken for the caller.

function deposit(ReserveData storage reserve,ReserveRateData storage rateData,uint256 amount,address depositor) internal returns (uint256 amountMinted) {
if (amount < 1) revert InvalidAmount();
// Update reserve interests
updateReserveInterests(reserve, rateData);
// Transfer asset from caller to the RToken contract
IERC20(reserve.reserveAssetAddress).safeTransferFrom(
msg.sender, // from
reserve.reserveRTokenAddress, // to
amount // amount
);
// Mint RToken to the depositor (scaling handled inside RToken)
(bool isFirstMint, uint256 amountScaled, uint256 newTotalSupply, uint256 amountUnderlying) = IRToken(reserve.reserveRTokenAddress).mint(
address(this), // caller
depositor, // onBehalfOf
amount, // amount
reserve.liquidityIndex // index
);
amountMinted = amountScaled;
// Update the total liquidity and interest rates
updateInterestRatesAndLiquidity(reserve, rateData, amount, 0);
emit Deposit(depositor, amount, amountMinted);
return amountMinted;
}

After that, _rebalanceLiquidity() is called. 20% will remain in the reserve.reserveAssetAddress, and the excess will be deposited into the Curve vault, but this function reverts due to incorrect logic in _depositIntoVault, which assumes that crvUSD is held by the address this.

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);

All deposits are getting blocked, which means the _rebalanceLiquidity() function is reverting the transaction.

Tools Used

Manual Review

Recommendations

To fix the issue, the _depositIntoVault function should be modified to transfer crvUSD from reserve.reserveRTokenAddress to the Curve Vault.

function _depositIntoVault(uint256 amount) internal {
// Transfer crvUSD from reserveRTokenAddress to address(this)
IERC20(reserve.reserveAssetAddress).safeTransferFrom(reserve.reserveRTokenAddress, address(this), amount);
// Approve and deposit into Curve Vault
IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);
curveVault.deposit(amount, address(this));
// Update totalVaultDeposits
totalVaultDeposits += amount;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Validated
Assigned finding tags:

LendingPool::_depositIntoVault and _withdrawFromVault don't transfer tokens between RToken and LendingPool, breaking Curve vault interactions

inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Validated
Assigned finding tags:

LendingPool::_depositIntoVault and _withdrawFromVault don't transfer tokens between RToken and LendingPool, breaking Curve vault interactions

Support

FAQs

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