Core Contracts

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

No assets will be deposit on crvVault

Summary

The LendingPool attempts to deposit assets into curveVault, but the assets are held in the RToken contract, preventing successful deposits.

Vulnerability Details

When a user deposits assets, they are transferred to the RToken contract:

/contracts/core/pools/LendingPool/LendingPool.sol:233
233: function deposit(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
234: // Update the reserve state before the deposit
235: ReserveLibrary.updateReserveState(reserve, rateData);
236:
237: // Perform the deposit through ReserveLibrary
238: uint256 mintedAmount = ReserveLibrary.deposit(reserve, rateData, amount, msg.sender);
239:
240: // Rebalance liquidity after deposit
241: _rebalanceLiquidity();
242:
243: emit Deposit(msg.sender, amount, mintedAmount);
244: }

The assets are transferred to RToken contract. which can be seen in below code:

/contracts/libraries/pools/ReserveLibrary.sol:329
329: // Transfer asset from caller to the RToken contract
330: IERC20(reserve.reserveAssetAddress).safeTransferFrom(
331: msg.sender, // from
332: reserve.reserveRTokenAddress, // to
333: amount // amount
334: );

During _rebalanceLiquidity, the LendingPool determines whether to deposit in to or withdraw from curveVault:

/contracts/core/pools/LendingPool/LendingPool.sol:823
823: uint256 totalDeposits = reserve.totalLiquidity; // Total liquidity in the system
824: uint256 desiredBuffer = totalDeposits.percentMul(liquidityBufferRatio); // 20%
825: uint256 currentBuffer = IERC20(reserve.reserveAssetAddress).balanceOf(reserve.reserveRTokenAddress);
826:
827: if (currentBuffer > desiredBuffer) {
828: uint256 excess = currentBuffer - desiredBuffer;
829: // Deposit excess into the Curve vault
830: _depositIntoVault(excess);
831: } else if (currentBuffer < desiredBuffer) {
832: uint256 shortage = desiredBuffer - currentBuffer;
833: // Withdraw shortage from the Curve vault
834: _withdrawFromVault(shortage);
835: }
836:

However, in _depositIntoVault, the LendingPool grants approval for the reserve asset but lacks ownership of the funds:

/contracts/core/pools/LendingPool/LendingPool.sol:858
858: function _depositIntoVault(uint256 amount) internal {
860: IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);
861: curveVault.deposit(amount, address(this));
862: totalVaultDeposits += amount;
863: }

Since assets reside in the RToken contract, not the LendingPool, the deposit fails.

POC

Add this mock CurveCrvUSDVault:

/contracts/mocks/crvVaultMock.sol
import {ICurveCrvUSDVault} from "../interfaces/curve/ICurveCrvUSDVault.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract MockUsdVault is ICurveCrvUSDVault {
address assetAddress;
constructor(address _assetAddress) {
assetAddress = _assetAddress;
}
function deposit(
uint256 assets,
address receiver
) external returns (uint256 shares) {
IERC20(assetAddress).transferFrom(msg.sender, address(this), assets);
return shares;
}
function withdraw(
uint256 assets,
address receiver,
address owner,
uint256 maxLoss,
address[] calldata strategies
) external returns (uint256 shares) {
IERC20(assetAddress).transfer(msg.sender, assets);
return shares;
}
function asset() external view returns (address) {
return address(this);
}
function totalAssets() external view returns (uint256) {
return 0;
}
function pricePerShare() external view returns (uint256) {
return 1e18;
}
function totalIdle() external view returns (uint256) {
return 0;
}
function totalDebt() external view returns (uint256) {
return 0;
}
function isShutdown() external view returns (bool) {
return false;
}
}

Add this POC to LendingPool.test.js in describe("Borrow and Repay", section:

/test/unit/core/pools/LendingPool/LendingPool.test.js:356
356: it.only("POC: No assets will be deposit on crvVault",async function(){
357: const borrowAmount = ethers.parseEther("100");
358: const MockVault = await ethers.getContractFactory("MockUsdVault");
359: let vault = await MockVault.deploy(crvusd.target);
360:
361:
362: await lendingPool.connect(owner).setCurveVault(vault.target);
363: const depositAmount = ethers.parseEther("1000");
364: await crvusd.connect(user2).approve(lendingPool.target, depositAmount);
365: await lendingPool.connect(user2).deposit(depositAmount);
366:
367: })

Run the command npx hardhat test

Impact

LendingPool cannot deposit assets into curveVault, leading to liquidity mismanagement.

Tools Used

Manual Review, Unit Testing

Recommendations

Modify _depositIntoVault to transfer assets from the RToken to LendingPool and then perform deposit to curveVault.

/contracts/core/pools/LendingPool/LendingPool.sol:858
function _depositIntoVault(uint256 amount) internal {
+ IRToken(reserve.reserveRTokenAddress).transferAsset(address(this), amount);
IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);
Updates

Lead Judging Commences

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

Give us feedback!