Core Contracts

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

Double scaling in the `DebtToken::totalSupply` function will lead to wrong `reserve` state updates

Summary

This is due to the double scaling that happens when the DebtToken::totalSupply function is called

Vulnerability Details

Lets track the following scenario:

  1. User borrows money from LendingPool and the pool mints him DebtTokens

  2. In the process of minting tokens we go through the _mint and _update function, which look like this:

function mint(
address user,
address onBehalfOf,
uint256 amount,
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256) {
if (user == address(0) || onBehalfOf == address(0))
revert InvalidAddress();
if (amount == 0) {
return (false, 0, totalSupply());
}
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
uint256 scaledBalance = balanceOf(onBehalfOf);
bool isFirstMint = scaledBalance == 0;
uint256 balanceIncrease = 0;
if (
_userState[onBehalfOf].index != 0 &&
_userState[onBehalfOf].index < index
) {
balanceIncrease =
scaledBalance.rayMul(index) -
scaledBalance.rayMul(_userState[onBehalfOf].index);
}
_userState[onBehalfOf].index = index.toUint128();
uint256 amountToMint = amount + balanceIncrease;
_mint(onBehalfOf, amountToMint.toUint128());
emit Transfer(address(0), onBehalfOf, amountToMint);
emit Mint(user, onBehalfOf, amountToMint, balanceIncrease, index);
return (scaledBalance == 0, amountToMint, totalSupply());
}
function _update(
address from,
address to,
uint256 amount
) internal virtual override {
if (from != address(0) && to != address(0)) {
revert TransfersNotAllowed(); // Only allow minting and burning
}
uint256 scaledAmount = amount.rayDiv(
ILendingPool(_reservePool).getNormalizedDebt()
);
super._update(from, to, scaledAmount);
emit Transfer(from, to, amount);
}

From those 2 we see that the actual place where the tokens are scaled is the _update function, which means that every update on the users' balances are updates with scaled amounts. This goes the same for the ERC20::_totalSupply variable as well

By clearing this out of the way, now we can take a look at the DebtToken::totalSupply function:

function totalSupply()
public
view
override(ERC20, IERC20)
returns (uint256)
{
uint256 scaledSupply = super.totalSupply();
return
scaledSupply.rayDiv(ILendingPool(_reservePool).getNormalizedDebt());
}

As can be seen here the totalSupply function gets the already scaled amount and scales it again.

Impact

This will lead to wrong update of the reserve.totalUsage variable, since its purpose is to track the active DebtToken supply and the totalSupply function doesn't return the true total supply

Tools Used

Manual Review

Recommendations

Don't scale in the totalSupply function. Just return super.totalSupply as the amounts returned from it are already scaled. This will return the actual DebtToken supply that the reserve.totalUsage wants to track.

Updates

Lead Judging Commences

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

DebtToken::totalSupply incorrectly uses rayDiv instead of rayMul, severely under-reporting total debt and causing lending protocol accounting errors

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

DebtToken::totalSupply incorrectly uses rayDiv instead of rayMul, severely under-reporting total debt and causing lending protocol accounting errors

Support

FAQs

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

Give us feedback!