Skip to content

Latest commit

 

History

History
80 lines (50 loc) · 3.88 KB

005.md

File metadata and controls

80 lines (50 loc) · 3.88 KB

Curved Inky Ram

Medium

A user believes they have fully repaid their debt but still has residual debt due to interest accrual

Summary

When a user repays their variable debt using aTokens and specifies the maximum amount (type(uint256).max), the code attempts to repay the full debt by using the user's entire aToken balance. However, due to the way interest accrues on the variable debt, there can be a residual debt remaining even after the repayment. This residual debt is not adequately handled in the code, leading to where the system assumes the debt has been fully repaid when it hasn't.

https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L60C3-L142C1 https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L154C3-L229C6

Root Cause

https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L60C3-L142C1 https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/protocol/libraries/logic/BorrowLogic.sol#L154C3-L229C6

In the executeRepay function, when a user chooses to repay using aTokens (useATokens is true) and specifies the maximum amount (params.amount == type(uint256).max), the following code is executed:

if (params.useATokens && params.amount == type(uint256).max) { params.amount = IAToken(reserveCache.aTokenAddress).balanceOf(msg.sender); } Here, params.amount is set to the user's aToken balance. The code then determines the paybackAmount:

uint256 paybackAmount = variableDebt;

if (params.amount < paybackAmount) { paybackAmount = params.amount; } This means the paybackAmount is the lesser of the user's variableDebt and their aToken balance.

However, between the time the variableDebt is fetched and the burn operation occurs, interest continues to accrue on the variable debt. This interest accrual can cause the actual debt to be slightly higher than the variableDebt value read earlier. As a result, even after burning the maximum possible amount of debt tokens, a small residual debt remains.

After the repayment, the code checks if the user's debt position should be considered closed:

if (variableDebt - paybackAmount == 0) { userConfig.setBorrowing(reserve.id, false); } Due to the residual debt, variableDebt - paybackAmount is not zero, so the user's borrowing flag remains true. However, since the user has repaid using all their aTokens, their aToken balance becomes zero. The code then checks if the user should continue using the reserve as collateral:

bool isCollateral = userConfig.isUsingAsCollateral(reserve.id); if (isCollateral && IAToken(reserveCache.aTokenAddress).scaledBalanceOf(msg.sender) == 0) { userConfig.setUsingAsCollateral(reserve.id, false); emit ReserveUsedAsCollateralDisabled(params.asset, msg.sender); } Since the user's aToken balance is zero, the system disables the reserve as collateral for the user. The net effect is that the user now has a small residual debt but no collateral supporting it, leaving their position undercollateralized and susceptible to liquidation.

Internal Pre-conditions

No response

External Pre-conditions

No response

Attack Path

No response

Impact

Users may believe they have fully repaid their debt but still have a residual amount due to interest accrual.

The system automatically disables the reserve as collateral when the aToken balance reaches zero, even if residual debt remains.

An undercollateralized position can lead to liquidation, resulting in losses for the user.

PoC

No response

Mitigation

// Recalculate the variableDebt after burn uint256 remainingDebt = IERC20(reserveCache.variableDebtTokenAddress).balanceOf(params.onBehalfOf);

if (remainingDebt == 0) { userConfig.setBorrowing(reserve.id, false); }