Summary
Wrong returned values order of the DebtToken::burn function causes the LendingPool::_repay function to transfer the amount to repay without interest accrued on borrowing, making the the protocol losing funds.
Vulnerability Details
The LendingPool::_repay function is used to repay or repay on behalf of another address in the lending pool. It invokes the DebtToken::burn function to burn the equivalent of debt tokens of the amount of assets the user want to repay. However, this function inverts the values of the amount of scaled tokens burned and the amount of underlying burned tokens.
File: contracts/core/tokens/DebtToken.sol#L170-L214
* @notice Burns debt tokens from a user
* @param from The address from which tokens are burned
* @param amount The amount to burn (in underlying asset units)
* @param index The usage index at the time of burning
* @return A tuple containing:
* - uint256: The amount of scaled tokens burned
* - uint256: The new total supply after burning
* - uint256: The amount of underlying tokens burned
* - uint256: The balance increase due to interest
*/
function burn(
address from,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256, uint256) {
if (from == address(0)) revert InvalidAddress();
if (amount == 0) {
return (0, totalSupply(), 0, 0);
}
uint256 userBalance = balanceOf(from);
uint256 balanceIncrease = 0;
if (_userState[from].index != 0 && _userState[from].index < index) {
uint256 borrowIndex = ILendingPool(_reservePool).getNormalizedDebt();
balanceIncrease = userBalance.rayMul(borrowIndex) - userBalance.rayMul(_userState[from].index);
amount = amount;
}
_userState[from].index = index.toUint128();
if(amount > userBalance){
amount = userBalance;
}
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
_burn(from, amount.toUint128());
emit Burn(from, amountScaled, index);
return (amount, totalSupply(), amountScaled, balanceIncrease);
}
It makes the function transfer amount from the caller to reserve instead of the real amountScaled.
File: contracts/core/pools/LendingPool/LendingPool.sol#L398-L431
function _repay(uint256 amount, address onBehalfOf) internal {
if (amount == 0) revert InvalidAmount();
if (onBehalfOf == address(0)) revert AddressCannotBeZero();
UserData storage user = userData[onBehalfOf];
ReserveLibrary.updateReserveState(reserve, rateData);
uint256 userDebt = IDebtToken(reserve.reserveDebtTokenAddress).balanceOf(onBehalfOf);
uint256 userScaledDebt = userDebt.rayDiv(reserve.usageIndex);
uint256 actualRepayAmount = amount > userScaledDebt ? userScaledDebt : amount;
uint256 scaledAmount = actualRepayAmount.rayDiv(reserve.usageIndex);
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) =
IDebtToken(reserve.reserveDebtTokenAddress).burn(onBehalfOf, amount, reserve.usageIndex);
IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
reserve.totalUsage = newTotalSupply;
user.scaledDebtBalance -= amountBurned;
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
emit Repay(msg.sender, onBehalfOf, actualRepayAmount);
}
amount is the amount the user want to repay and the amountScaled is the amount the user want to repay plus the interest accrued on borrowing.
Impact
This leads to the protocol losing the interests accrued on borrowing since the value of amountScaled transfered from the re-payer (the caller) to the reserve is in fact the value of amount.
Tools Used
Manual review.
Recommendations
Place amount and amountScaled in their right place when returning values in the DebtToken::burn function.
-- return (amount, totalSupply(), amountScaled, balanceIncrease);
++ return (amountScaled, totalSupply(), amount, balanceIncrease);