Repay With aTokens Flow¶
End-to-end execution flow for repaying debt using aTokens instead of underlying Tokens in Aave V3.
Quick Reference¶
Aspect |
Details |
|---|---|
Entry Point |
|
Key Transformations |
|
State Changes |
|
Events Emitted |
|
Flow Diagram¶
flowchart TD
%% Styling definitions
classDef validation fill:#ffcccc,stroke:#ff0000,stroke-width:2px
classDef transformation fill:#ccffcc,stroke:#00aa00,stroke-width:2px
classDef storage fill:#ccccff,stroke:#0000ff,stroke-width:2px
classDef event fill:#ffffcc,stroke:#aaaa00,stroke-width:2px
classDef error fill:#ff0000,stroke:#000000,color:#fff
classDef critical stroke:#ff0000,stroke-width:3px
%% Entry point
Entry["Pool.repayWithATokens<br/>asset, amount,<br/>interestRateMode"] --> Execute["BorrowLogic<br/>executeRepay<br/>useATokens: true"]
subgraph StateUpdate ["1. State Updates"]
direction TB
UpdateState["ReserveLogic<br/>updateState"] --> GetDebt["Helpers<br/>getUserCurrentDebt<br/>Gets stable + variable debt"]
end
subgraph Validation ["2. Validation"]
direction TB
GetDebt --> Validate["ValidationLogic<br/>validateRepay<br/>Checks: amount > 0, debt exists"]
class Validate validation
end
subgraph AmountCalc ["3. Amount Calculation"]
direction TB
Validate --> MaxCheck{"amount ==<br/>MAX_UINT?"}
MaxCheck -->|Yes| UseATokenBalance["TRANSFORMATION<br/>paybackAmount =<br/>aToken.balanceOf<br/>msg.sender"]
class UseATokenBalance transformation
MaxCheck -->|No| UseProvided["Use provided amount"]
UseATokenBalance --> CapCheck["Cap at<br/>current debt"]
UseProvided --> CapCheck
end
subgraph TokenBurn ["4. Debt Token Burn"]
direction TB
CapCheck --> ModeCheck{"Interest<br/>Rate Mode"}
ModeCheck -->|STABLE| StableBurn["StableDebtToken<br/>burn"]
ModeCheck -->|VARIABLE| VariableBurn["VariableDebtToken<br/>burn"]
StableBurn --> StableTransform["TRANSFORMATION<br/>principal -= amount<br/>+ accrued interest"]
class StableTransform transformation
VariableBurn --> VariableTransform["TRANSFORMATION<br/>scaledAmountToBurn =<br/>amount.rayDiv(index)"]
class VariableTransform transformation
StableTransform --> StoreStable["STORAGE UPDATE<br/>_balances.principal -= amount"]
class StoreStable storage
VariableTransform --> StoreVariable["STORAGE UPDATE<br/>_scaledBalance -=<br/>scaledAmountToBurn"]
class StoreVariable storage
end
Execute --> StateUpdate
StateUpdate --> Validation
Validation --> AmountCalc
StoreStable --> UpdateRates["ReserveLogic<br/>updateInterestRates<br/>liquidityAdded: 0"]
StoreVariable --> UpdateRates
subgraph CollateralMgmt ["5. Collateral Management"]
direction TB
UpdateRates --> IsoLogic["IsolationModeLogic<br/>updateIsolatedDebtIfIsolated"]
class IsoLogic storage
IsoLogic --> BurnAToken["AToken<br/>burn<br/>from: msg.sender"]
class BurnAToken storage
end
BurnAToken --> FinalEvent["EMIT<br/>Repay<br/>useATokens: true"]
class FinalEvent event
%% Error annotations
%% CRITICAL: Debt token burn is irreversible
%% CRITICAL: Isolation mode debt ceiling must be respected
%% CRITICAL: Repayment caps at current debt balance
%% Link styles for critical paths
linkStyle 18 stroke:#ff0000,stroke-width:3px
linkStyle 22 stroke:#ff0000,stroke-width:3px
Step-by-Step Execution¶
1. Entry Point¶
File: contracts/protocol/pool/Pool.sol
function repayWithATokens(
address asset,
uint256 amount,
uint256 interestRateMode
) public virtual override returns (uint256) {
return
BorrowLogic.executeRepay(
_reserves,
_reservesList,
_usersConfig[msg.sender],
DataTypes.ExecuteRepayParams({
asset: asset,
amount: amount,
interestRateMode: DataTypes.InterestRateMode(interestRateMode),
onBehalfOf: msg.sender,
useATokens: true
})
);
}
Key Differences from repay():
onBehalfOf: Always
msg.sender(cannot repay on behalf of others)useATokens: Set to
true(uses aTokens instead of underlying)
2. Execute Repay¶
File: contracts/protocol/libraries/logic/BorrowLogic.sol
function executeRepay(
mapping(address => DataTypes.ReserveData) storage reservesData,
mapping(uint256 => address) storage reservesList,
DataTypes.UserConfigurationMap storage userConfig,
DataTypes.ExecuteRepayParams memory params
) external returns (uint256) {
DataTypes.ReserveData storage reserve = reservesData[params.asset];
DataTypes.ReserveCache memory reserveCache = reserve.cache();
reserve.updateState(reserveCache);
(uint256 stableDebt, uint256 variableDebt) = Helpers.getUserCurrentDebt(
params.onBehalfOf,
reserveCache
);
ValidationLogic.validateRepay(
reserveCache,
params.amount,
params.interestRateMode,
params.onBehalfOf,
stableDebt,
variableDebt
);
uint256 paybackAmount = params.interestRateMode == DataTypes.InterestRateMode.STABLE
? stableDebt
: variableDebt;
// Allows a user to repay with aTokens without leaving dust from interest.
if (params.useATokens && params.amount == type(uint256).max) {
params.amount = IAToken(reserveCache.aTokenAddress).balanceOf(msg.sender);
}
if (params.amount < paybackAmount) {
paybackAmount = params.amount;
}
// Burn debt tokens
if (params.interestRateMode == DataTypes.InterestRateMode.STABLE) {
IStableDebtToken(reserveCache.stableDebtTokenAddress).burn(
params.onBehalfOf,
paybackAmount
);
} else {
IVariableDebtToken(reserveCache.variableDebtTokenAddress).burn(
params.onBehalfOf,
paybackAmount,
reserveCache.nextVariableBorrowIndex
);
}
// Update interest rates - note: liquidityAdded is 0 when using aTokens
reserve.updateInterestRates(
reserveCache,
params.asset,
params.useATokens ? 0 : paybackAmount,
0
);
// Clear borrowing flag if all debt repaid
if (stableDebt + variableDebt - paybackAmount == 0) {
userConfig.setBorrowing(reserve.id, false);
}
// Update isolation mode debt if applicable
IsolationModeLogic.updateIsolatedDebtIfIsolated(
reservesData,
reservesList,
userConfig,
reserveCache,
paybackAmount
);
// Burn aTokens from caller (instead of transferring underlying)
if (params.useATokens) {
IAToken(reserveCache.aTokenAddress).burn(
msg.sender,
reserveCache.aTokenAddress,
paybackAmount,
reserveCache.nextLiquidityIndex
);
} else {
IERC20(params.asset).safeTransferFrom(
msg.sender,
reserveCache.aTokenAddress,
paybackAmount
);
IAToken(reserveCache.aTokenAddress).handleRepayment(
msg.sender,
params.onBehalfOf,
paybackAmount
);
}
emit Repay(
params.asset,
params.onBehalfOf,
msg.sender,
paybackAmount,
params.useATokens
);
return paybackAmount;
}
3. Variable Debt Token Burn¶
File: contracts/protocol/tokenization/VariableDebtToken.sol
function burn(
address from,
uint256 amount,
uint256 index
) external override onlyPool {
_burnScaled(from, amount, index);
}
function _burnScaled(address from, uint256 amount, uint256 index) internal {
uint256 scaledAmount = amount.rayDiv(index);
uint256 scaledBalanceBefore = _scaledBalance[from];
// Cap at balance
if (scaledAmount > scaledBalanceBefore) {
scaledAmount = scaledBalanceBefore;
amount = scaledAmount.rayMul(index);
}
_scaledBalance[from] -= scaledAmount;
}
[TRANSFORMATION]: See Debt Token Transformations for details on amount.rayDiv(index)
4. Stable Debt Token Burn¶
File: contracts/protocol/tokenization/StableDebtToken.sol
function burn(address from, uint256 amount) external override onlyPool {
_burn(from, amount);
}
function _burn(address from, uint256 amount) internal {
uint256 accountBalance = _balances[from].principal;
uint256 balanceIncrease = accountBalance.rayMul(
MathUtils.calculateCompoundedInterest(
_balances[from].stableRate,
_balances[from].lastUpdateTimestamp
)
) - accountBalance;
uint256 amountToBurn = amount;
if (amount > accountBalance + balanceIncrease) {
amountToBurn = accountBalance + balanceIncrease;
}
_balances[from].principal = accountBalance + balanceIncrease - amountToBurn;
_balances[from].lastUpdateTimestamp = block.timestamp;
_totalSupply.principal -= amountToBurn;
_totalSupply.lastUpdateTimestamp = block.timestamp;
}
5. AToken Burn¶
File: contracts/protocol/tokenization/AToken.sol
function burn(
address from,
address receiverOfUnderlying,
uint256 amount,
uint256 index
) external virtual override onlyPool {
_burnScaled(from, receiverOfUnderlying, amount, index);
}
function _burnScaled(
address from,
address receiverOfUnderlying,
uint256 amount,
uint256 index
) internal {
uint256 amountScaled = amount.rayDiv(index);
require(amountScaled != 0, Errors.INVALID_BURN_AMOUNT);
_scaledBalance[from] -= amountScaled;
}
[TRANSFORMATION]: See Collateral Token Transformations for details on amount.rayDiv(index)
6. Validation Checks¶
File: contracts/protocol/libraries/logic/ValidationLogic.sol
function validateRepay(
DataTypes.ReserveCache memory reserveCache,
uint256 amountSent,
DataTypes.InterestRateMode interestRateMode,
address onBehalfOf,
uint256 stableDebt,
uint256 variableDebt
) internal view {
require(amountSent != 0, Errors.INVALID_AMOUNT);
require(
amountSent != type(uint256).max || msg.sender == onBehalfOf,
Errors.NO_EXPLICIT_AMOUNT_TO_REPAY_ON_BEHALF
);
(bool isActive, , , , bool isPaused) = reserveCache.reserveConfiguration.getFlags();
require(isActive, Errors.RESERVE_INACTIVE);
require(!isPaused, Errors.RESERVE_PAUSED);
require(
(stableDebt != 0 && interestRateMode == DataTypes.InterestRateMode.STABLE) ||
(variableDebt != 0 && interestRateMode == DataTypes.InterestRateMode.VARIABLE),
Errors.NO_DEBT_OF_SELECTED_TYPE
);
}
7. Isolation Mode Debt Update¶
File: contracts/protocol/libraries/logic/IsolationModeLogic.sol
function updateIsolatedDebtIfIsolated(
mapping(address => DataTypes.ReserveData) storage reservesData,
mapping(uint256 => address) storage reservesList,
DataTypes.UserConfigurationMap storage userConfig,
DataTypes.ReserveCache memory reserveCache,
uint256 repayAmount
) internal {
(bool isolationModeActive, address isolationModeCollateralAddress, ) = userConfig
.getIsolationModeState(reservesData, reservesList);
if (isolationModeActive) {
uint128 isolationModeTotalDebt = reservesData[isolationModeCollateralAddress]
.isolationModeTotalDebt;
uint128 isolatedDebtRepaid = (repayAmount /
10 **
(reserveCache.reserveConfiguration.getDecimals() -
ReserveConfiguration.DEBT_CEILING_DECIMALS)).toUint128();
if (isolationModeTotalDebt <= isolatedDebtRepaid) {
reservesData[isolationModeCollateralAddress].isolationModeTotalDebt = 0;
emit IsolationModeTotalDebtUpdated(isolationModeCollateralAddress, 0);
} else {
uint256 nextIsolationModeTotalDebt = reservesData[isolationModeCollateralAddress]
.isolationModeTotalDebt = isolationModeTotalDebt - isolatedDebtRepaid;
emit IsolationModeTotalDebtUpdated(
isolationModeCollateralAddress,
nextIsolationModeTotalDebt
);
}
}
}
Amount Transformations¶
Variable Rate Repay With aTokens¶
_scaledBalance[msg.sender].rayMul(index) = currentDebt (WAD)
|
aTokenBalance = _scaledBalance[msg.sender].rayMul(liquidityIndex) (WAD)
|
Validation: currentDebt > 0
|
IF amount == MAX_UINT:
paybackAmount = aTokenBalance
ELSE:
paybackAmount = min(requested, currentDebt, aTokenBalance)
|
scaledAmountToBurn = paybackAmount.rayDiv(index)
|
aTokenScaledAmount = paybackAmount.rayDiv(liquidityIndex)
|
_scaledBalanceDebt[msg.sender] -= scaledAmountToBurn
_scaledBalanceAToken[msg.sender] -= aTokenScaledAmount
Stable Rate Repay With aTokens¶
_principal = _balances[msg.sender].principal
_accruedInterest = _principal.rayMul(compoundedInterest) - _principal
currentDebt = _principal + _accruedInterest (WAD)
|
aTokenBalance = _scaledBalance[msg.sender].rayMul(liquidityIndex) (WAD)
|
IF amount == MAX_UINT:
paybackAmount = min(aTokenBalance, currentDebt)
ELSE:
paybackAmount = min(requested, currentDebt, aTokenBalance)
|
_balances[msg.sender].principal = currentDebt - paybackAmount
_balances[msg.sender].lastUpdateTimestamp = block.timestamp
_scaledBalanceAToken[msg.sender] -= paybackAmount.rayDiv(liquidityIndex)
Key Differences from Regular Repay:
No underlying token transfer - aTokens are burned directly
Interest rate update uses
liquidityAdded: 0since no new liquidity entersSelf-only repayment - can only repay own debt
MAX_UINT handling - uses aToken balance instead of debt amount
Event Details¶
Repay Event¶
event Repay(
address indexed reserve,
address indexed user,
address indexed repayer,
uint256 amount,
bool useATokens
);
IsolationModeTotalDebtUpdated Event¶
event IsolationModeTotalDebtUpdated(
address indexed asset,
uint256 totalDebt
);
Error Conditions¶
Error |
Condition |
File |
|---|---|---|
|
|
ValidationLogic.sol |
|
|
ValidationLogic.sol |
|
Reserve is not active |
ValidationLogic.sol |
|
Reserve is paused |
ValidationLogic.sol |
|
Repaying stable but only has variable debt (or vice versa) |
ValidationLogic.sol |
Source File Locations¶
contracts/protocol/pool/Pool.sol
contracts/protocol/libraries/logic/BorrowLogic.sol
contracts/protocol/libraries/logic/ValidationLogic.sol
contracts/protocol/libraries/logic/IsolationModeLogic.sol
contracts/protocol/libraries/helpers/Helpers.sol
contracts/protocol/tokenization/VariableDebtToken.sol
contracts/protocol/tokenization/StableDebtToken.sol
contracts/protocol/tokenization/AToken.sol
contracts/protocol/libraries/logic/ReserveLogic.sol