Core Contracts

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

Lending Pool's profit is incorrectly calculated

Summary

When calculating RToken's dust amount, contractBalance is deflated and totalRealBalance is inflated. Calculated dust amount will be less than actual accrued amount. The impact of this vulnerability is high because calculateDustAmount represents protocol's profit, and transferAccruedDustis the only way to withdraw protocol's profit from RToken.

Vulnerability Details

Take a look into RToken.calculateDustAmount:

function calculateDustAmount() public view returns (uint256) {
// Calculate the actual balance of the underlying asset held by this contract
@> uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this)).rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
// Calculate the total real obligations to the token holders
uint256 currentTotalSupply = totalSupply();
// Calculate the total real balance equivalent to the total supply
@> uint256 totalRealBalance = currentTotalSupply.rayMul(ILendingPool(_reservePool).getNormalizedIncome()); // @audit should be div not mul
// All balance, that is not tied to rToken are dust (can be donated or is the rest of exponential vs linear)
return contractBalance <= totalRealBalance ? 0 : contractBalance - totalRealBalance;
}

contractBalance is calculated as the following:

contractBalance = assetBalance / liquidityIndex

while totalRealBalance is calculated as the following:

totalRealBalance = totalSupply * liquidityIndex

Since RToken is minted 1:1 to asset deposited amount, and RToken's totalSupply is multiplied by liquidityIndex, we can derive the following:

totalRealBalance = totalSupply * liquidityIndex
= scaledTotalSupply * liquidityIndex * liquidityIndex
= depositedAssetAmount * liquidityIndex * liquidityIndex

We can see that there are two mistakes in calculateDustAmount:

  • contractBalance is deflated by liquidityIndex. There is no need to divide asset balance by liquidityIndex

  • totalRealBalance is inflated by liquidityIndex ^ 2

As a result, calculated dust amount will be deflated by

totalSupply * (1 - 1 / liquidityIndex) + depositedAssetAmount * (liquidityIndex^2 - 1)

As a side note, liquidityIndex will be greater than 1 as long as protocol works.

Impact

calculateDustAmountand transferAccruedDustare very important functions, whatever their names are. Because it's the only way to withdraw protocol's profit from RToken.

Let's see how Lending Pool makes profit.

RAAC protocol implemented (or tried to do) similar math with other lending protocols such as AAVE and Compound

usageRateis guaranteed to be greater than liquidityRate, and usageIndexis guaranteed to be greater than liquidityIndex.

What this means, we can see in the following example:

  • Alice deposits 100K crvUSD and receives 100K RToken

  • Bob borrows 100K crvUSD and receives 100K DebtToken

  • 1 year passes

  • Alice now has 103593 RToken - i.e liquidityIndex = 1.03593

  • Bob now has 107452 DebtToken - i.e. usageIndex = 1.07452

  • Bob clears debt by paying 107452 crvUSD

  • Alice withdraws 103593 RToken and receives 103593 crvUSD

Now RToken has accrued 107452 - 103593 = 3859crvUSD, thanks to gap between usageIndexand liquidityIndex

This 3859 USD is protcol's profit - that's how any other decentralized lending protocols (AAVE, Compound etc) and banks work.

So if there's any gap between contractBalanceand totalRealBalance, it's due to either one of the following:

  • Protocol's profit due to usageIndexand liquidityIndexgap, or

  • Donation

This amount is calculated by calculateDustAmountand can be retrieved by calling transferAccruedDust.

So this vulnerability will lead to underestimation of protocol's profit and stuck of unclaimed profit in the protocol.

Tools Used

Manual Review

Recommendation

contractBalance should be calculated as the following:

- uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this)).rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
+ uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this));

totalRealBalance should be calculated as the following:

- uint256 totalRealBalance = currentTotalSupply.rayMul(ILendingPool(_reservePool).getNormalizedIncome());
+ uint256 totalRealBalance = currentTotalSupply.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
Updates

Lead Judging Commences

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

RToken::calculateDustAmount incorrectly applies liquidity index, severely under-reporting dust amounts and permanently trapping crvUSD in contract

Support

FAQs

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

Give us feedback!