Core Contracts

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

Reserve Asset Depositors Do Not Earn Interests in their Deposits

Summary

The depositors of reserve assets (crvUSD) in the LendingPool do not earn any interest in their deposits. This is in stark contrast to the documentation which suggests that a "user's deposit in the Reserve Pool accrues interest over time".

Depositor deposits x amount of reserve assets --> Depositor waits till a lot of interest is accrued --> Depositor withdraws the reserve assets but receive only the amount they originally deposited.

Vulnerability Details

Depositors deposit reserve assets (crvUSD) into the LendingPool and receive RTokens. The docs mention the following point:

The RToken is an implementation of the interest-bearing token for the RAAC lending protocol. It represents a user's deposit in the Reserve Pool and accrues interest over time using an index-based system similar to Aave's AToken.

This is not the case because as we will learn, the depositor will only get back the tokens they have deposited without gaining any interest in the deposits.

To understand this, let's look at the sequence of actions a depositor will take.

Firstly, they will deposit their tokens using the deposit function:

function deposit(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
...
uint256 mintedAmount = ReserveLibrary.deposit(reserve, rateData, amount, msg.sender);
...

This function calls the ReserveLibrary::deposit function as follows:

function deposit(ReserveData storage reserve,ReserveRateData storage rateData,uint256 amount,address depositor) internal returns (uint256 amountMinted) {
...
1> IERC20(reserve.reserveAssetAddress).safeTransferFrom(
msg.sender, // from
reserve.reserveRTokenAddress, // to
amount // amount
);
// Mint RToken to the depositor (scaling handled inside RToken)
2> (bool isFirstMint, uint256 amountScaled, uint256 newTotalSupply, uint256 amountUnderlying) = IRToken(reserve.reserveRTokenAddress).mint(
address(this), // caller
depositor, // onBehalfOf
amount, // amount
reserve.liquidityIndex // index
);
amountMinted = amountScaled;
...

Code 1 shown above transfers the reserve asset from the depositor into the contract, and line 2 calls the RToken::mint function. Let's look at the mint function below:

function mint(
address caller,
address onBehalfOf,
uint256 amountToMint,
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256, uint256) {
...
1> uint256 amountScaled = amountToMint.rayDiv(index);
...
2> uint256 scaledBalance = balanceOf(onBehalfOf);
...
3> _mint(onBehalfOf, amountToMint.toUint128());
...

As shown above in line 3, the amount of RTokens minted is the amount variable passed directly into the mint function. The amount in turn is equal to the amount of reserve asset (crvUSD) tokens transferred into the contract.

As time passes, the depositor would want to withdraw their tokens using the withdraw function. The LendingPool::withdraw function calls the ReserveLibrary::withdraw function:

function withdraw(
ReserveData storage reserve,
ReserveRateData storage rateData,
uint256 amount,
address recipient
) internal returns (uint256 amountWithdrawn, uint256 amountScaled, uint256 amountUnderlying) {
...
@> (uint256 burnedScaledAmount, uint256 newTotalSupply, uint256 amountUnderlying) = IRToken(reserve.reserveRTokenAddress).burn(
recipient, // from
recipient, // receiverOfUnderlying
amount, // amount
reserve.liquidityIndex // index
);
amountWithdrawn = burnedScaledAmount;
...

This function calls the RToken::burn function as shown below:

function burn(
address from,
address receiverOfUnderlying,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256) {
...
1> uint256 userBalance = balanceOf(from);
...
2> uint256 amountScaled = amount.rayMul(index);
...
3> _burn(from, amount.toUint128());
if (receiverOfUnderlying != address(this)) {
4> IERC20(_assetAddress).safeTransfer(receiverOfUnderlying, amount);
}
...

Let's look at the burn function above in more detail, we have the following observations:

  • Line 1 gives the total RTokens owned by the user.

  • Line 2 gives the actual amount of reserve assets the depositor needs to receive.

  • Line 3 burns the amount of RTokens of the user.

  • Line 4 transfers the reserve assets to the user. But the amount transferred is the amount and not the amountScaled.

Therefore, the amount of RTokens burned is equal to the amount of reserve assets transferred to the user. This transfer does not include any interest earned by the depositor.

Impact

The major impact of this is that none of the depositors receive any form of interest. The LendingPool only serves as an account with 0% interest but with additional risks of possibly losing all their investments.

Tools Used

Manual Review

Recommendations

The amountScaled should be transferred instead of the amount in the RToken::burn function.

Updates

Lead Judging Commences

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