Summary
Users can repay their debt by calling the LendingPool::repay function. At the end of the function, the Repay event is emitted. However, the amount reported in the event is not the actual repaid amount returned by the burn function, which results in a wrong value emitted.
Vulnerability Details
The Repay event definition is the following:
> ILendingPool.sol
* @notice Emitted when a user repays borrowed assets
* @param user The address of the user calling the repay function
* @param onBehalfOf The address of the user being repaid on behalf of
* @param amount The amount repaid
*/
event Repay(address indexed user, address indexed onBehalfOf, uint256 amount);
However, when emitted at the end of the LendingPool::_repay function, the amount reported is the actualRepayAmount and in most cases it is different than the real repaid amount, making the event not reliable.
This can be seen when an user decide to repay all of his DebtToken balance. actualRepayAmount is then capped at userScaledDebt = userDebt/reserve.usageIndex, which is a lower value than the input amount. Nevertheless, the actual amount repaid is contained in the amountScaled variable returned by the DebtToken.burn function, that coincides with the original amount provided by the user and is different than the actualRepayAmount value.
> LendingPool.sol
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);
}
Impact
Impact: Low
Multiple associated services may display incorrect values, potentially misleading protocol administrators and consumers.
Likelihood: Medium
Tools Used
Manual Review
Recommendations
It is recommended to emit the correct value in the event.
> LendingPool.sol
function _repay(uint256 amount, address onBehalfOf) internal {
... snip
// Burn DebtTokens from the user whose debt is being repaid (onBehalfOf)
// is not actualRepayAmount because we want to allow paying extra dust and we will then cap there
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) =
IDebtToken(reserve.reserveDebtTokenAddress).burn(onBehalfOf, amount, reserve.usageIndex);
// Transfer reserve assets from the caller (msg.sender) to the reserve
IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
reserve.totalUsage = newTotalSupply;
user.scaledDebtBalance -= amountBurned;
// Update liquidity and interest rates
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
- emit Repay(msg.sender, onBehalfOf, actualRepayAmount);
+ emit Repay(msg.sender, onBehalfOf, amountScaled);
}