Core Contracts

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

CrvUSD Permanently Locked in RToken Contract During Liquidations

Summary

During liquidations, crvUSD tokens sent to the RToken contract become permanently locked due to restrictive withdrawal mechanisms.
RToken holds crvUSD _assetAddress, and is called by LendingPool to mint or burn RToken to users when they deposit/withdraw crvUSD into the pool.
There is no other way to withdraw crvUSD from RTokens, except when it's dust amount but that does not apply here.

Also, liquidation is only dependent on the RAAC teams and its ability to provide enough crvUSD in StabilityPool for liquidation. This does not seem sustainable.

Vulnerability Details

During a liquidation, StabilityPool will trigger it and allow LendingPool to transfer the userDebt in crvUSD to RToken.

// LendingPool.sol
function finalizeLiquidation(address userAddress) external nonReentrant onlyStabilityPool {
// ...
// Burn DebtTokens from the user
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) = IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
// Transfer reserve assets from Stability Pool to cover the debt
IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled); //msg.sender = stabilityPool amountScaled = userDebt
// ...
}

The issue here is that there is no way to recover those crvUSD in RToken.sol
The only way to get back crvUSD from this contract is by calling burn(), but RToken are needed.

3 others functions in RTokens interact with crvUSD, but are useless for our case :

function transferAsset(address user, uint256 amount) external override onlyReservePool {
IERC20(_assetAddress).safeTransfer(user, amount);
}

It's called in LendingPool::borrow(), not usable in our case

function rescueToken(address tokenAddress, address recipient, uint256 amount) external onlyReservePool {
if (recipient == address(0)) revert InvalidAddress();
if (tokenAddress == _assetAddress) revert CannotRescueMainAsset();
IERC20(tokenAddress).safeTransfer(recipient, amount);
}

This function cannot be used with crvUSD so it does not work either

function transferAccruedDust(address recipient, uint256 amount) external onlyReservePool {
if (recipient == address(0)) revert InvalidAddress();
uint256 poolDustBalance = calculateDustAmount();
if(poolDustBalance == 0) revert NoDust();
// Cap the transfer amount to the actual dust balance
uint256 transferAmount = (amount < poolDustBalance) ? amount : poolDustBalance;
// Transfer the amount to the recipient
IERC20(_assetAddress).safeTransfer(recipient, transferAmount);
emit DustTransferred(recipient, transferAmount);
}

This functions will only work to retrieve sa mall amount of accruedDust with max poolDustBalance.

Exemple :

  1. userA deposit NFT and borrow 100_000

  2. userA enter liquidation because of a market drop

  3. RAAC funds 100 000 crvUSD in stability pool to cover userDebt

  4. userA is liquidated, 100_000 crvUSD transferred from StabilityPool to RToken

  5. Funds are locked in RToken, as no RToken has been minted for those crvUSD.

Impact

All crvUSD provided by StabilityPool (ie Protocol team) during liquidation will end up lost and locked up in RToken.
It also shows a flaw in design in the liquidation process, as currently, the team needs to input crvUSD to liquidate users in StabilityPool. How sustainable is that?

Tools Used

Manual

Recommendations

Transfer crvUSD used for liquidation to another address with a different logic than RToken.
A good way will be to have part of fees aggregated from the protocol to accrue there and be used to liquidate the user, instead of having to rely on the protocol team to input enough funds in StabilityPool

Updates

Lead Judging Commences

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

StabilityPool design flaw where liquidations will always fail as StabilityPool receives rTokens but LendingPool expects it to provide crvUSD

Support

FAQs

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