Core Contracts

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

Incorrect Rebalance Logic in LendingPool Prevents Curve Vault Deposits and Causes Denial of Service

Summary:

The LendingPool contract's mechanism for depositing excess liquidity into the Curve crvUSD vault for extra yeild is broken. Due to a mismatch between accounting and actual token holdings, the LendingPool contract attempts to transfer crvUSD to the vault when the crvUSD is held by the RToken contract, resulting in a revert and causing a whole DoS.

Vulnerability Details:

The core issue lies in the inconsistent handling of crvUSD tokens after a user deposit. When a user deposits crvUSD via LendingPool.deposit(), the deposited crvUSD is immediately transferred to the RToken contract using IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amount);. The RToken contract then mints corresponding RTokens to the user. This transfer is crucial for the RToken mechanism, but it creates a problem for the subsequent Curve vault deposit. After the deposit, LendingPool._rebalanceLiquidity() is called. This function calculates the desired liquidity buffer (20% of total liquidity) and determines if there is excess liquidity. If excess liquidity exists, LendingPool._depositIntoVault() is called. LendingPool._depositIntoVault() attempts to transfer crvUSD from the LendingPool contract to the CurveCrvUSDVault using curveVault.deposit(amount, address(this));. However, this transfer fails because the LendingPool contract no longer holds the crvUSD. The funds are now held by the RToken contract. The CurveCrvUSDVault deposit reverts due to insufficient balance in the LendingPool contract.

The list of functions affected by this issue is as follows:

Impact:

The inability to deposit excess liquidity into the Curve vault due to the insufficient balance in the LendingPool contract leads to a total DoS as the LendingPool._depositIntoVault fails, causing no users to deposit their crvUSDC into the LendingPool.

Proof Of Concept:

  1. Alice deposits 1000 crvUSD into the LendingPool by calling LendingPool.deposit(1000).

  2. The LendingPool contract transfers the 1000 crvUSD to the RToken contract.

  3. The RToken contract mints 1000 RTokens to Alice.

  4. LendingPool._rebalanceLiquidity() is called. Assume that after the deposit, the total liquidity is 1000 crvUSD. The desired buffer is 20% of 1000, which is 200 crvUSD. The current buffer in LendingPool is 0. Thus, the function identifies 800 crvUSD as excess liquidity.

  5. LendingPool._depositIntoVault(800) is called.

  6. LendingPool._depositIntoVault() attempts to transfer 800 crvUSD from the LendingPool contract to the CurveCrvUSDVault.

  7. The transfer reverts because the LendingPool contract's crvUSD balance is 0. The 1000 crvUSD is held by the RToken contract. The Curve vault deposit fails. Thus alice is unable to deposit her crvUSD into the LendingPool causing DoS.

Proof Of Code:

  1. Use this guide to intergrate foundry into your project: foundry

  2. Create a new file FortisAudits.t.sol in the test directory.

  3. Add the following gist code to the file: Gist Code

  4. Run the test using forge test --mt test_FortisAudits_IncorrectRebalanceLogic -vvvv.

function test_FortisAudits_IncorrectRebalanceLogic() public {
uint256 amount = 1000e18;
vm.prank(initialOwner);
lendingPool.setCurveVault(address(mockCurveVault));
vm.startPrank(anon);
_reserveAsset.mint(anon, amount);
_reserveAsset.approve(address(lendingPool), amount);
vm.expectRevert();
lendingPool.deposit(amount);
assertEq(_reserveAsset.balanceOf(anon), amount);
vm.stopPrank();
}

Tools Used:

  • Manul code review.

Recommended Mitigation:

The issue can be resolved by first transfering the crvUSD from the RToken contract to the LendingPool contract before attempting to deposit it into the Curve vault. This can be achieved by calling the RToken.transferAsset in the RToken contract that allows the LendingPool contract to transfer crvUSD from the RToken then deposit into the Curve vault.

function _rebalanceLiquidity() internal {
// ... other code
uint256 currentBuffer = IERC20(reserve.reserveAssetAddress).balanceOf(reserve.reserveRTokenAddress);
if (currentBuffer > desiredBuffer) {
uint256 excess = currentBuffer - desiredBuffer;
// Transfer tokens from RToken to LendingPool
+ rToken.transferAsset(address(this), excess);
// 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);
}
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!