Core Contracts

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

Incorrect approval source in _depositIntoVault() causes deposit reverts in LendingPool

Summary

The _rebalanceLiquidity() attempts to deposit excess liquidity into the curveVault by approving it to spend tokens from reserve.reserveAssetAddress. However, deposited funds reside in reserve.reserveRTokenAddress, not reserve.reserveAssetAddress. This causes _depositIntoVault() to fail due to insufficient balance when calling approve() and deposit(), leading to deposit transaction reverts and liquidity management failure.

Vulnerability Details

A user deposits reserve asset tokens into the protocol by calling deposit(amount).

Inside deposit(), ReserveLibrary.deposit() transfers funds from msg.sender to reserve.reserveRTokenAddress, not LendingPool.

After the deposit, _rebalanceLiquidity() is triggered to check if excess liquidity should be deposited into the Curve vault if it is not address(0).

If there is excess liquidity, _depositIntoVault(excess) is called:

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

The approve() function is called by LendingPool, but all liquidity is held in reserve.reserveRTokenAddress.

Since LendingPool has no funds, approve() and deposit() fail, reverting the entire transaction.

  • Proof of Concept (PoC)

Make a MockCurveCrvUSDVault

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "../interfaces/curve/ICurveCrvUSDVault.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockCurveCrvUSDVault is ERC20 {
address asset;
constructor(address _asset) ERC20("Mock curveCrvUSD", "curveCrvUSD") {
asset = _asset;
}
function deposit(uint256 assets, address receiver) external returns (uint256 shares) {
IERC20(asset).transferFrom(msg.sender, address(this), assets);
_mint(receiver, assets);
shares = assets;
}
function withdraw(
uint256 assets,
address receiver,
address owner,
uint256 maxLoss,
address[] calldata strategies
) external returns (uint256 shares) {
IERC20(asset).transfer(receiver, assets);
_burn(owner, assets);
shares = assets;
}
}

Update LendingPool.test.js

describe("LendingPool", function () {
...
beforeEach(async function () {
...
const LendingPool = await ethers.getContractFactory("LendingPool");
lendingPool = await LendingPool.deploy(
crvusd.target,
rToken.target,
debtToken.target,
raacNFT.target,
raacHousePrices.target,
initialPrimeRate
);
+ const scrvCurve = await ethers.getContractFactory("MockCurveCrvUSDVault");
+ const curveVault = await scrvCurve.deploy(crvusd.target);
+ await lendingPool.connect(owner).setCurveVault(curveVault);
...
});
});

As seen above, after lendingPool.curveVault is set, deposit transaction reverts in _depositIntoVault().

Impact

Protocol core function(deposit) reverts

Tools Used

manual

Recommendations

Adds transfer asset proccess in _rebalanceLiquidity()

function _rebalanceLiquidity() internal {
...
if (currentBuffer > desiredBuffer) {
uint256 excess = currentBuffer - desiredBuffer;
+ rToken.transferAsset(address(this), excess);
// Deposit excess into the Curve vault
_depositIntoVault(excess);
...
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month 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 about 1 month 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.