Core Contracts

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

Misaligned Buffer Ratio Management Makes Liquidity Rebalancing Impossible

Summary

The LendingPool's liquidity buffer management system has a critical flaw where the buffer calculations and actual token movements are misaligned. The system calculates the buffer based on the RToken contract's balance but attempts to move tokens between the curvePool and the LendingPool.

Vulnerability

The core issue lies in the mismatch between where the buffer ratio is calculated and where the actual token movements occur:

Buffer Calculation (checks RToken balance):

function _rebalanceLiquidity() internal {
uint256 desiredBuffer = totalDeposits.percentMul(liquidityBufferRatio);
uint256 currentBuffer = IERC20(reserve.reserveAssetAddress).balanceOf(reserve.reserveRTokenAddress);
// ... rebalancing logic based on these values
}

Token Movement (to/from LendingPool):

function _depositIntoVault(uint256 amount) internal {
IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);
curveVault.deposit(amount, address(this)); // Deposits from LendingPool
}
function _withdrawFromVault(uint256 amount) internal {
curveVault.withdraw(amount, address(this), msg.sender, 0, new address[](0)); // Withdraws to LendingPool
}



Proof of Concept

  1. A user attempts to borrow an amount more than the availableLiquidity

  2. _ensureLiquiditywould call__ withdrawFromVaultwithdrawing the underlying crvUSD tokens to the LendingPool

  3. Now in the borrowfunction the asset transfer will fail because the RTokendoes not have the amoutbeing requested to borrow

Within withdrawthe token transfer of the underlying out of the RTokencontract will fail after the same flow.

Coded PoC - add this to the protocols-test.js describe('LendingPool'block

it("should fail when attempting to borrow more than available RToken liquidity", async function () {
// 1. Initial deposit to create some liquidity
const depositAmount = ethers.parseEther("100");
await contracts.crvUSD.connect(user1).approve(contracts.lendingPool.target, depositAmount);
await contracts.lendingPool.connect(user1).deposit(depositAmount);
// 2. Set up NFT collateral for borrowing with higher price to allow more borrowing
const tokenId = HOUSE_TOKEN_ID + 5;
const higherPrice = HOUSE_PRICE * 10n; // 10x the normal price to allow more borrowing
await contracts.housePrices.setHousePrice(tokenId, higherPrice);
await contracts.crvUSD.connect(user2).approve(contracts.nft.target, higherPrice);
await contracts.nft.connect(user2).mint(tokenId, higherPrice);
await contracts.nft.connect(user2).approve(contracts.lendingPool.target, tokenId);
await contracts.lendingPool.connect(user2).depositNFT(tokenId);
// Log initial balances
console.log("\nInitial balances:");
const initialRTokenBalance = await contracts.crvUSD.balanceOf(contracts.rToken.target);
const initialLendingPoolBalance = await contracts.crvUSD.balanceOf(contracts.lendingPool.target);
console.log("RToken balance:", ethers.formatEther(initialRTokenBalance));
console.log("LendingPool balance:", ethers.formatEther(initialLendingPoolBalance));
// First borrow will succeed
const borrowAmount = ethers.parseEther("90");
await contracts.lendingPool.connect(user2).borrow(borrowAmount);
// Log intermediate balances
console.log("\nBalances after first borrow:");
const rTokenBalance = await contracts.crvUSD.balanceOf(contracts.rToken.target);
const lendingPoolBalance = await contracts.crvUSD.balanceOf(contracts.lendingPool.target);
console.log("RToken balance:", ethers.formatEther(rTokenBalance));
console.log("LendingPool balance:", ethers.formatEther(lendingPoolBalance));
// Second borrow attempt
const borrowAmount2 = ethers.parseEther("50");
await expect(
contracts.lendingPool.connect(user2).borrow(borrowAmount2)
).to.be.revertedWithCustomError(
contracts.rToken,
"ERC20InsufficientBalance"
);
// Verify final state shows the architectural flaw
const finalRTokenBalance = await contracts.crvUSD.balanceOf(contracts.rToken.target);
const finalLendingPoolBalance = await contracts.crvUSD.balanceOf(contracts.lendingPool.target);
expect(finalRTokenBalance).to.equal(ethers.parseEther("10")); // Only 10 crvUSD left
expect(finalLendingPoolBalance).to.equal(0); // LendingPool has 0 b/c transaction failed
});

Impact

HIGH - The protocol's liquidity management system is fundamentally broken, preventing proper management of the liquidity buffer.

Recommendation

Deposit and withdraw from the curvePool to the RToken contract.

Updates

Lead Judging Commences

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