Core Contracts

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

`LendingPool` can't transfer accrued dust due to a scaling issue.

Summary

Admin can't transfer accrued dust amounts from rToken contract because the asset and rToken are incorrectly scaled. Dust is permanently locked in rToken contract.

Vulnerability Details

Admin can call LendignPool::transferAccruedDust to transfer accrued dust to a specified recipient. rToken::transferAccruedDust is invoked and dust amount is calculated.

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

i. asset.balanceOf() returns the amount of assets the rToken contract holds. contractBalance is calculated as assetBalance / normalizedIncome.

ii. totalSupply() is overridden and returns the amounts deposited including the accrued interest:

// RToken.sol
function totalSupply() public view override(ERC20, IERC20) returns (uint256) {
return super.totalSupply().rayMul(ILendingPool(_reservePool).getNormalizedIncome());
}

iii. totalRealBalance is calculated as rToken::totalSupply * normalizedIncome

On one hand contractBalance is wrongly divided by normalizedIncome and on the other hand currentTotalSupply is wrongly multiplied by normalizedIncome

Let's take the following example:

  • normalized income = 1.1

  • assets held by rToken contract = 1210

  • total supply of rToken = 1000

Both assets and rToken totalSupply are expressed in non-scaled amounts (actual value which can be withdrawed).
calculateDustAmount() compares the following values:

  • contractBalance = 1210 / 1.1 = 1100

  • totalRealBalance = 1000 * 1.1 = 1100

The values are equal, calculateDustAmount returns 0 and no dust is collected.

As the normalizedIncome increases with accrued interest, the contractBalance will become progressively smaller than the totalRealBalance, permanently locking the accumulated dust.

Impact

Dust is permanently locked in contract even if protocol implemented the function to collect it.

Tools Used

Recommendations

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());
+ uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this));
// 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;
+ return contractBalance <= currentTotalSupply ? 0 : contractBalance - currentTotalSupply ;
}
Updates

Lead Judging Commences

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