Summary
Whenever a user increments their borrowed funds the balance increase of the debt is minted to the users account as debt amount. But this is calculated wrongly and will lead to a wrong account of the debt amount interest.
Vulnerability Details
When a user borrows more
* @notice Allows a user to borrow reserve assets using their NFT collateral
* @param amount The amount of reserve assets to borrow
*/
function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
if (isUnderLiquidation[msg.sender]) revert CannotBorrowUnderLiquidation();
UserData storage user = userData[msg.sender];
uint256 collateralValue = getUserCollateralValue(msg.sender);
if (collateralValue == 0) revert NoCollateral();
ReserveLibrary.updateReserveState(reserve, rateData);
_ensureLiquidity(amount);
uint256 userTotalDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex) + amount;
revert NotEnoughCollateralToBorrow();
}
uint256 scaledAmount = amount.rayDiv(reserve.usageIndex);
@audit>> (bool isFirstMint, uint256 amountMinted, uint256 newTotalSupply) = IDebtToken(reserve.reserveDebtTokenAddress).mint(msg.sender, msg.sender, amount, reserve.usageIndex);
IRToken(reserve.reserveRTokenAddress).transferAsset(msg.sender, amount);
@audit>> user.scaledDebtBalance += scaledAmount;
reserve.totalUsage = newTotalSupply;
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, 0, amount);
_rebalanceLiquidity();
emit Borrow(msg.sender, amount);
}
During the minting process we call to obtain the scaled balance of the user to calculate his owed interest
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();
@audit>>> 1. uint256 scaledBalance = balanceOf(onBehalfOf);
bool isFirstMint = scaledBalance == 0;
uint256 balanceIncrease = 0;
@audit>>> if (_userState[onBehalfOf].index != 0 && _userState[onBehalfOf].index < index) {
@audit>>> 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());
}
The actually debt is always calculated by multiplying scaled balance with index
see below
* @notice Returns the scaled debt balance of the user
* @param account The address of the user
* @return The user's debt balance (scaled by the usage index)
*/
1. function balanceOf(address account) public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledBalance = super.balanceOf(account);
return scaledBalance.rayMul(ILendingPool(_reservePool).getNormalizedDebt());
}
The balance of function returns the actual debt that we are owing and not the scaled balance/ debt.
When we try to obtain the balance increase we multiply the actual debt obtained by raymul index again which will now give us a completely wrong value
uint256 balanceIncrease = 0;
if (_userState[onBehalfOf].index != 0 && _userState[onBehalfOf].index < index) {
@audit >>> using balance instead of scaled balance>> balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].index);
}
This will return a wrong amount to mint causing the system to mint the user more debt token than a user should hold, then they owe.
We are miniting a double interest
user balance 1000 USD
index 1
rate now 1.1
user calls to add 500 USD more debt tokens (borrow)
balance of returns 1100 USD .
but during the balance increase
1100(1.1) - 1100(1) = 210 USD instead of the 100 USD actual profit owed by the user.
Using Aave as a reference again , see implementation =>
function mint(
address user,
address onBehalfOf,
uint256 amount,
uint256 rate
) external virtual override onlyPool returns (bool, uint256, uint256) {
MintLocalVars memory vars;
if (user != onBehalfOf) {
_decreaseBorrowAllowance(onBehalfOf, user, amount);
}
(, uint256 currentBalance, uint256 balanceIncrease) = _calculateBalanceIncrease(onBehalfOf);
All the calculations were done once with the scaled balance to obtain the balance change and the new principal
* @notice Calculates the increase in balance since the last user interaction
* @param user The address of the user for which the interest is being accumulated
* @return The previous principal balance
* @return The new principal balance
* @return The balance increase
*/
function _calculateBalanceIncrease(
address user
) internal view returns (uint256, uint256, uint256) {
@audit>>>> uint256 previousPrincipalBalance = super.balanceOf(user);
if (previousPrincipalBalance == 0) {
return (0, 0, 0);
}
@audit>>>> uint256 newPrincipalBalance = balanceOf(user);
return (
previousPrincipalBalance,
@audit>>>> newPrincipalBalance,
@audit>>>> newPrincipalBalance - previousPrincipalBalance
);
}
The double mulray is wrong and will mint users more debt tokens than they owe
Impact
Wrong account and minting a user more debt tokens than they actually owe.
Tools Used
Manual review
Recommendations
Return the scaledbalance of and not the balanceof the user
-- uint256 scaledBalance = balanceOf(onBehalfOf);
++ uint256 scaledBalance = scaledBalanceOf(onBehalfOf);