Summary
Instead of returning the index of the position, the debt value is return leading to an error whenever we call to calculate a users debt
Vulnerability Details
The totalusage and total liquidity are the total asset and total debt respectively.
* @notice Struct to hold reserve data.
* @dev All values are stored in RAY (27 decimal) precision.
*/
struct ReserveData {
address reserveRTokenAddress;
address reserveAssetAddress;
address reserveDebtTokenAddress;
uint256 totalLiquidity;
@audit>> uint256 totalUsage;
uint128 liquidityIndex;
@audit>> uint128 usageIndex;
uint40 lastUpdateTimestamp;
}
Call to get normalised income returns the correct index but debt is flawed
* @notice Gets the normalized income of the reserve.
* @param reserve The reserve data.
* @return The normalized income (in RAY).
*/
function getNormalizedIncome(ReserveData storage reserve, ReserveRateData storage rateData) internal view returns (uint256) {
uint256 timeDelta = block.timestamp - uint256(reserve.lastUpdateTimestamp);
if (timeDelta < 1) {
@audit>> index >> return reserve.liquidityIndex;
}
@audit>> index >> return calculateLinearInterest(rateData.currentLiquidityRate, timeDelta, reserve.liquidityIndex).rayMul(reserve.liquidityIndex);
}
* @notice Gets the normalized debt of the reserve.
* @param reserve The reserve data.
* @return The normalized debt (in underlying asset units).
*/
function getNormalizedDebt(ReserveData storage reserve, ReserveRateData storage rateData) internal view returns (uint256) {
uint256 timeDelta = block.timestamp - uint256(reserve.lastUpdateTimestamp);
if (timeDelta < 1) {
@audit>> returning debt instead of index>>> return reserve.totalUsage;
}
@audit>> returns index >> return calculateCompoundedInterest(rateData.currentUsageRate, timeDelta).rayMul(reserve.usageIndex);
}
In the stability pool when we call to liquidate a position from the lending pool
Stability pool if call after an update
function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
uint256 userDebt = lendingPool.getUserDebt(userAddress);
@audit>> uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
if (userDebt == 0) revert InvalidAmount();
uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();
@audit>> wrong calculation used for approval>> bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
if (!approveSuccess) revert ApprovalFailed();
lendingPool.finalizeLiquidation(userAddress);
emit BorrowerLiquidated(userAddress, scaledUserDebt);
}
the above code multiplies the debt by the total debt divided by ray instead of getting the scaled user debt
Lending pool
* @notice Allows the Stability Pool to finalize the liquidation after the grace period has expired
* @param userAddress The address of the user being liquidated
*/
function finalizeLiquidation(address userAddress) external nonReentrant onlyStabilityPool {
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
ReserveLibrary.updateReserveState(reserve, rateData);
if (block.timestamp <= liquidationStartTime[userAddress] + liquidationGracePeriod) {
revert GracePeriodNotExpired();
}
UserData storage user = userData[userAddress];
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
isUnderLiquidation[userAddress] = false;
liquidationStartTime[userAddress] = 0;
for (uint256 i = 0; i < user.nftTokenIds.length; i++) {
uint256 tokenId = user.nftTokenIds[i];
user.depositedNFTs[tokenId] = false;
raacNFT.transferFrom(address(this), stabilityPool, tokenId);
}
delete user.nftTokenIds;
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) = IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
@audit>> transfer from stability pool >>> IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
user.scaledDebtBalance -= amountBurned;
reserve.totalUsage = newTotalSupply;
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
emit LiquidationFinalized(stabilityPool, userAddress, userDebt, getUserCollateralValue(userAddress));
}
Looking at Aave's implementation also -
* @notice Returns the ongoing normalized income for the reserve.
* @dev A value of 1e27 means there is no income. As time passes, the income is accrued
* @dev A value of 2*1e27 means for each unit of asset one unit of income has been accrued
* @param reserve The reserve object
* @return The normalized income, expressed in ray
*/
function getNormalizedIncome(
DataTypes.ReserveData storage reserve
) internal view returns (uint256) {
uint40 timestamp = reserve.lastUpdateTimestamp;
if (timestamp == block.timestamp) {
return reserve.liquidityIndex;
} else {
return
MathUtils.calculateLinearInterest(reserve.currentLiquidityRate, timestamp).rayMul(
reserve.liquidityIndex
);
}
}
* @notice Returns the ongoing normalized variable debt for the reserve.
* @dev A value of 1e27 means there is no debt. As time passes, the debt is accrued
* @dev A value of 2*1e27 means that for each unit of debt, one unit worth of interest has been accumulated
* @param reserve The reserve object
* @return The normalized variable debt, expressed in ray
*/
function getNormalizedDebt(
DataTypes.ReserveData storage reserve
) internal view returns (uint256) {
uint40 timestamp = reserve.lastUpdateTimestamp;
if (timestamp == block.timestamp) {
@audit>>> return reserve.variableBorrowIndex;
} else {
return
MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp).rayMul(
reserve.variableBorrowIndex
);
}
}
Impact
Wrong borrow rate, utilization rate and liquidity rate calculations.
Tools Used
Manual Review
Recommendations
Return the index of the debt and not the debt amount