Core Contracts

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

Liquidity Providers Not Receiving Accrued Interest During Withdrawals

Summary

In the current implementation of the lending pool contract, when a liquidity provider withdraws funds, the protocol fails to account for accrued interest based on the liquidityIndex. The issue stems from the incorrect transfer logic within the burn() function of the RToken contract, where the underlying asset is transferred without scaling the amount by the liquidityIndex.
As per the docs here: [Lending](https://docs.raac.io/quickstart/about-raac?id=lending)

The user depositing the curvUsd receives Rtokens which represents the amount he has deposited along with accured interest so after particular amount of time after which he has deposited the curvUsd if he withdraws by burning all Rtokens he should receive the curvUsd which he had deposited along with any interests which are accumalated.But here due to the issue mentioned only he receives the amount he had deposited and not any accured interest.

Vulnerability Details

Let us consider the scenario where he wants to burn all the rtokens he received after depositing the curvUsd. He calls function LendingPool::withdraw , as highlighted below here it uses reserve library to call withdraw.

function withdraw(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
if (withdrawalsPaused) revert WithdrawalsArePaused();
// Update the reserve state before the withdrawal
ReserveLibrary.updateReserveState(reserve, rateData);
// Ensure sufficient liquidity is available
_ensureLiquidity(amount);
// Perform the withdrawal through ReserveLibrary
@> (uint256 amountWithdrawn, uint256 amountScaled, uint256 amountUnderlying) = ReserveLibrary.withdraw(
@> reserve, // ReserveData storage
@> rateData, // ReserveRateData storage
@> amount, // Amount to withdraw
@> msg.sender // Recipient
@> );
// Rebalance liquidity after withdrawal
_rebalanceLiquidity();
emit Withdraw(msg.sender, amountWithdrawn);
}

Now the withdraw function in reserve library calls the burn function of rtoken address which also transfers the underlying asset to the liquidity provider.

ReserveLibrary::withdraw:

function withdraw(
ReserveData storage reserve,
ReserveRateData storage rateData,
uint256 amount,
address recipient
) internal returns (uint256 amountWithdrawn, uint256 amountScaled, uint256 amountUnderlying) {
if (amount < 1) revert InvalidAmount();
// Update the reserve interests
updateReserveInterests(reserve, rateData);
// Burn RToken from the recipient - will send underlying asset to the recipient
(uint256 burnedScaledAmount, uint256 newTotalSupply, uint256 amountUnderlying) = IRToken(reserve.reserveRTokenAddress).burn(
recipient, // from
recipient, // receiverOfUnderlying
amount, // amount
reserve.liquidityIndex // index
);
amountWithdrawn = burnedScaledAmount;
// Update the total liquidity and interest rates
updateInterestRatesAndLiquidity(reserve, rateData, 0, amountUnderlying);
emit Withdraw(recipient, amountUnderlying, burnedScaledAmount);
return (amountUnderlying, burnedScaledAmount, amountUnderlying);
}

now in the below rtoken contract where the rtokens are burnt, as the ratio of initally deposited amount of liquidity provider and the rtokens he got is in the ratio 1:1. As we considered that he is burning all rtokens he as here the amountScaled is calculated in the below function which is never used meaning the amount actually user should get is the amountScaled but he is just receiving the amount he had trasnferred and not receiving any interest.

RToken::burn:

function burn(
address from,
address receiverOfUnderlying,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256) {
if (amount == 0) {
return (0, totalSupply(), 0);
}
uint256 userBalance = balanceOf(from);
_userState[from].index = index.toUint128();
if(amount > userBalance){
amount = userBalance;
}
@> uint256 amountScaled = amount.rayMul(index);
_userState[from].index = index.toUint128();
_burn(from, amount.toUint128());
if (receiverOfUnderlying != address(this)) {
@> IERC20(_assetAddress).safeTransfer(receiverOfUnderlying, amount);
}
emit Burn(from, receiverOfUnderlying, amount, index);
return (amount, totalSupply(), amount);
}

Impact

Financial Loss: Liquidity providers do not receive their accrued interest when withdrawing their funds.

Tools Used

Manual

Recommendations

function burn(
address from,
address receiverOfUnderlying,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256) {
if (amount == 0) {
return (0, totalSupply(), 0);
}
uint256 userBalance = balanceOf(from);
_userState[from].index = index.toUint128();
if(amount > userBalance){
amount = userBalance;
}
uint256 amountScaled = amount.rayMul(index);
_userState[from].index = index.toUint128();
_burn(from, amount.toUint128());
if (receiverOfUnderlying != address(this)) {
- IERC20(_assetAddress).safeTransfer(receiverOfUnderlying, amount);
+ IERC20(_assetAddress).safeTransfer(receiverOfUnderlying, amountScaled);
}
emit Burn(from, receiverOfUnderlying, amount, index);
return (amount, totalSupply(), amount);
}
Updates

Lead Judging Commences

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

RToken::burn transfers original deposit amount (amount) to users instead of amount plus interest (amountScaled), causing loss of all accrued interest on withdrawals

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

RToken::burn transfers original deposit amount (amount) to users instead of amount plus interest (amountScaled), causing loss of all accrued interest on withdrawals

Support

FAQs

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