Core Contracts

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

`calculateDustAmount` function in RToken contract is incorrect, leading to wrong amount calculated and then transferred when owner calls `transferAccruedDust` in LendingPool contract.

Summary

calculateDustAmount function in RToken contract is defined as follows:

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());
// 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;
}

Multiple issues are combined, leading to a wrong calculation:

  • contracBalance is supposed to represent the actual balance of the underlying asset, but represents the scaled amount (i.e, the Rtoken amount). Indeed, balanceOf is overridden by RToken contract and returns the amount of underlying asset by calling super.balanceOf and multiplying by the liquidity index:

    function balanceOf(address account) public view override(ERC20, IERC20) returns (uint256) {
    uint256 scaledBalance = super.balanceOf(account);
    return scaledBalance.rayMul(ILendingPool(_reservePool).getNormalizedIncome());
    }

    This is why calling balanceOf(address(this))and then dividing by the liquidity index is incorrect, as it sequentially unscales and scales, returning the RToken contract balance (not underlying asset).

    uint256 contractBalance =
    IERC20(_assetAddress).balanceOf(address(this)).rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
  • currentTotalSupply returns the total supply of underlying asset. Indeed, totalSupply is also overridden and returns the total supply of RToken (scaled token), multiplied by the liquidity index to get the total supply of underlying asset (reserve asset). But calculateDustAmount assumes it returns the total supply of scaled token, as it then multiples currentTotalSupply by the liquidity index. The is incorrect as it leads to a double multiplication by the liquidity index.

uint256 currentTotalSupply = totalSupply();
uint256 totalRealBalance = currentTotalSupply.rayMul(ILendingPool(_reservePool).getNormalizedIncome());

In consequence, its highly probable that the following code will mostly return 0, given that contractBalance is scaled (Rtoken contract balance) and totalRealBalance has been multiplied by the liquidity index twice:

return contractBalance <= totalRealBalance ? 0 : contractBalance - totalRealBalance;

Impact

The impact of this issue is medium, as it leads to incorrect amounts of token transferred when removing accrued dust from RToken contract. This issue will lead to impossibility to remove the dust amount by the owner, especially when the liquidity index has grown over time (because totalRealBalance will be double scaled up with an index greater than 1, while contractBalance will be scaled down).

Tools Used

Manual review

Recommendations

Rectify the implementation of calculateDustAmount to make sure it computes token balances and supply with the right scale:

function calculateDustAmount() public view returns (uint256) {
// Calculate the actual balance of the underlying asset held by this contract
// `balanceOf` overriden function multiplies by liquidity index to return underlying asset
uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this));
// Calculate the total real balance in underlying asset equivalent to the total supply of RToken scaled token
uint256 totalRealBalance = totalSupply();
// 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;
}
Updates

Lead Judging Commences

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