# Repay With Permit Flow
End-to-end execution flow for repaying debt in Aave V3 using EIP-2612 permit for gasless approvals.
## Quick Reference
| Aspect | Details |
|--------|---------|
| **Entry Point** | `Pool.repayWithPermit(asset, amount, interestRateMode, onBehalfOf, deadline, permitV, permitR, permitS)` |
| **Key Transformations** | [Scaled Debt → Amount](../transformations/index.md#debt-token-transformations) |
| **State Changes** | `_scaledBalance[onBehalfOf] -= scaledAmount` |
| **Events Emitted** | `Repay`, `IsolationModeTotalDebtUpdated` (conditional) |
| **Key Difference** | Executes `permit()` before repay for gasless token approval |
---
## Flow Diagram
```mermaid
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.repayWithPermit
asset, amount,
interestRateMode,
onBehalfOf,
deadline, permitV,
permitR, permitS"] --> Permit["Try-Catch
IERC20.permit
approve spending"]
Permit --> Execute["RepayLogic
executeRepay"]
subgraph StateUpdate ["1. State Updates"]
direction TB
UpdateState["ReserveLogic
updateState"] --> GetDebt["Helpers
getUserCurrentDebt
Gets variable debt"]
end
subgraph Validation ["2. Validation"]
direction TB
GetDebt --> Validate["ValidationLogic
validateRepay
Checks: amount > 0"]
class Validate validation
end
subgraph Decision ["3. Repay Method Decision"]
direction TB
Validate --> ATokensCheck{"Use aTokens?"}
ATokensCheck -->|Yes| CalcFromAToken["TRANSFORMATION
paybackAmount =
aToken.balanceOf"]
class CalcFromAToken transformation
ATokensCheck -->|No| UseProvided["Use provided amount
or MAX_UINT for full"]
CalcFromAToken --> ModeCheck{"Interest
Rate Mode"}
UseProvided --> ModeCheck
end
subgraph TokenBurn ["4. Debt Token Burn"]
direction TB
ModeCheck -->|VARIABLE| VariableBurn["VariableDebtToken
burn"]
VariableBurn --> VariableTransform["TRANSFORMATION
scaledAmountToBurn =
amount.rayDiv(index)"]
class VariableTransform transformation
VariableTransform --> StoreVariable["STORAGE UPDATE
_scaledBalance -=
scaledAmountToBurn"]
class StoreVariable storage
end
Execute --> StateUpdate
StateUpdate --> Validation
Validation --> Decision
StoreVariable --> UpdateRates["ReserveLogic
updateInterestRates"]
subgraph CollateralMgmt ["5. Collateral Management"]
direction TB
UpdateRates --> IsoLogic["IsolationModeLogic
updateIsolatedDebtIfIsolated"]
class IsoLogic storage
IsoLogic --> RepayTokenCheck{"Use aTokens?"}
RepayTokenCheck -->|Yes| BurnAToken["AToken
burn"]
RepayTokenCheck -->|No| TransferUnderlying["IERC20
safeTransferFrom"]
class TransferUnderlying storage
end
BurnAToken --> FinalEvent["EMIT
Repay"]
class FinalEvent event
TransferUnderlying --> FinalEvent
%% Error annotations
%% CRITICAL: Permit failure is silently caught (try-catch)
%% CRITICAL: Debt token burn is irreversible
%% CRITICAL: Isolation mode debt ceiling must be respected
%% Link styles for critical paths
linkStyle 18 stroke:#ff0000,stroke-width:3px
```
---
## Step-by-Step Execution
### 1. Entry Point
**File:** `contracts/protocol/pool/Pool.sol`
```solidity
function repayWithPermit(
address asset,
uint256 amount,
uint256 interestRateMode,
address onBehalfOf,
uint256 deadline,
uint8 permitV,
bytes32 permitR,
bytes32 permitS
) external virtual override returns (uint256) {
try
IERC20WithPermit(asset).permit(
_msgSender(),
address(this),
amount,
deadline,
permitV,
permitR,
permitS
)
{} catch {}
{
DataTypes.ExecuteRepayParams memory params = DataTypes.ExecuteRepayParams({
asset: asset,
user: _msgSender(),
interestRateStrategyAddress: RESERVE_INTEREST_RATE_STRATEGY,
amount: amount,
interestRateMode: DataTypes.InterestRateMode(interestRateMode),
onBehalfOf: onBehalfOf,
useATokens: false,
oracle: ADDRESSES_PROVIDER.getPriceOracle(),
userEModeCategory: _usersEModeCategory[onBehalfOf]
});
return
BorrowLogic.executeRepay(
_reserves,
_reservesList,
_eModeCategories,
_usersConfig[onBehalfOf],
params
);
}
}
```
**Key Differences from `repay()`:**
- **Additional Parameters:** `deadline`, `permitV`, `permitR`, `permitS` for EIP-2612 permit signature
- **Permit Call:** Executes `IERC20WithPermit(asset).permit()` to approve token spending via signature
- **Try-Catch:** The permit is wrapped in try-catch to handle cases where permit might fail (e.g., if approval already exists)
- **Same Execution:** After permit, delegates to `BorrowLogic.executeRepay` identical to regular `repay()`
### 2. Permit Execution
**File:** `contracts/protocol/pool/Pool.sol`
```solidity
try
IERC20WithPermit(asset).permit(
_msgSender(),
address(this),
amount,
deadline,
permitV,
permitR,
permitS
)
{} catch {}
```
The permit function:
- Approves the Pool contract to spend `amount` of `asset` tokens on behalf of `msg.sender`
- Uses EIP-2612 signature validation (`v`, `r`, `s` components)
- Validates the `deadline` timestamp has not expired
- Wrapped in try-catch to prevent revert if:
- The permit has already been executed (replay protection)
- The user already has sufficient allowance
- The token doesn't support permit (fallback behavior)
### 3. Execute Repay
**File:** `contracts/protocol/libraries/logic/BorrowLogic.sol`
```solidity
function executeRepay(
mapping(address => DataTypes.ReserveData) storage reservesData,
mapping(uint256 => address) storage reservesList,
mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories,
DataTypes.UserConfigurationMap storage onBehalfOfConfig,
DataTypes.ExecuteRepayParams memory params
) external returns (uint256) {
DataTypes.ReserveData storage reserve = reservesData[params.asset];
DataTypes.ReserveCache memory reserveCache = reserve.cache();
reserve.updateState(reserveCache);
uint256 userDebtScaled = IVariableDebtToken(reserveCache.variableDebtTokenAddress)
.scaledBalanceOf(params.onBehalfOf);
uint256 userDebt = userDebtScaled.getVTokenBalance(reserveCache.nextVariableBorrowIndex);
ValidationLogic.validateRepay(
params.user,
reserveCache,
params.amount,
params.interestRateMode,
params.onBehalfOf,
userDebtScaled
);
uint256 paybackAmount = params.amount;
if (params.useATokens && params.amount == type(uint256).max) {
// Allows a user to repay with aTokens without leaving dust from interest.
paybackAmount = IAToken(reserveCache.aTokenAddress)
.scaledBalanceOf(params.user)
.getATokenBalance(reserveCache.nextLiquidityIndex);
}
if (paybackAmount > userDebt) {
paybackAmount = userDebt;
}
bool noMoreDebt;
(noMoreDebt, reserveCache.nextScaledVariableDebt) = IVariableDebtToken(
reserveCache.variableDebtTokenAddress
).burn({
from: params.onBehalfOf,
scaledAmount: paybackAmount.getVTokenBurnScaledAmount(reserveCache.nextVariableBorrowIndex),
index: reserveCache.nextVariableBorrowIndex
});
reserve.updateInterestRatesAndVirtualBalance(
reserveCache,
params.asset,
params.useATokens ? 0 : paybackAmount,
0,
params.interestRateStrategyAddress
);
if (noMoreDebt) {
onBehalfOfConfig.setBorrowing(reserve.id, false);
}
IsolationModeLogic.reduceIsolatedDebtIfIsolated(
reservesData,
reservesList,
onBehalfOfConfig,
reserveCache,
paybackAmount
);
// Transfer repayment (underlying tokens)
IERC20(params.asset).safeTransferFrom(params.user, reserveCache.aTokenAddress, paybackAmount);
emit IPool.Repay(
params.asset,
params.onBehalfOf,
params.user,
paybackAmount,
params.useATokens
);
return paybackAmount;
}
```
### 4. Variable Debt Token Burn
**File:** `contracts/protocol/tokenization/VariableDebtToken.sol`
```solidity
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); // [TRANSFORMATION]
uint256 scaledBalanceBefore = _scaledBalance[from];
// Cap at balance
if (scaledAmount > scaledBalanceBefore) {
scaledAmount = scaledBalanceBefore;
amount = scaledAmount.rayMul(index);
}
_scaledBalance[from] -= scaledAmount;
}
```
**[TRANSFORMATION]:** See [Debt Token Transformations](../transformations/index.md#debt-token-transformations) for details on `amount.rayDiv(index)`
### 5. Validation Checks
**File:** `contracts/protocol/libraries/logic/ValidationLogic.sol`
```solidity
function validateRepay(
address user,
DataTypes.ReserveCache memory reserveCache,
uint256 amountSent,
DataTypes.InterestRateMode interestRateMode,
address onBehalfOf,
uint256 debtScaled
) internal pure {
require(amountSent != 0, Errors.InvalidAmount());
require(
interestRateMode == DataTypes.InterestRateMode.VARIABLE,
Errors.InvalidInterestRateModeSelected()
);
require(
amountSent != type(uint256).max || user == onBehalfOf,
Errors.NoExplicitAmountToRepayOnBehalf()
);
(bool isActive, , , bool isPaused) = reserveCache.reserveConfiguration.getFlags();
require(isActive, Errors.ReserveInactive());
require(!isPaused, Errors.ReservePaused());
require(debtScaled != 0, Errors.NoDebtOfSelectedType());
}
```
### 6. Isolation Mode Debt Update
**File:** `contracts/protocol/libraries/logic/IsolationModeLogic.sol`
```solidity
function reduceIsolatedDebtIfIsolated(
mapping(address => DataTypes.ReserveData) storage reservesData,
mapping(uint256 => address) storage reservesList,
DataTypes.UserConfigurationMap storage userConfig,
DataTypes.ReserveCache memory reserveCache,
uint256 repayAmount
) internal {
// Check if reserve is in isolation mode
if (reserveCache.reserveConfiguration.getDebtCeiling() != 0) {
DataTypes.ReserveData storage reserve = reserves[
reservesList[reserveCache.reserveConfiguration.getId()]
];
// Reduce isolation mode total debt
uint128 isolatedDebt = reserve.isolationModeTotalDebt;
uint256 repayAmountUint = repayAmount / 10**
(reserveCache.reserveConfiguration.getDecimals() -
ReserveConfiguration.DEBT_CEILING_DECIMALS);
if (repayAmountUint > isolatedDebt) {
reserve.isolationModeTotalDebt = 0;
} else {
reserve.isolationModeTotalDebt -= uint128(repayAmountUint);
}
emit IsolationModeTotalDebtUpdated(
reserveCache.aTokenAddress,
reserve.isolationModeTotalDebt
);
}
}
```
---
## Amount Transformations
### Variable Rate Repay
```
_scaledBalance[onBehalfOf].rayMul(index) = currentDebt (WAD)
↓
Validation: currentDebt > 0
↓
Calculate payback = min(requested, currentDebt)
↓
scaledAmountToBurn = payback.rayDiv(index) // v4+: uses rayDivFloor
↓
_scaledBalance[onBehalfOf] -= scaledAmountToBurn
```
### Permit Integration
```
User signs permit message (off-chain)
↓
Signature parameters: v, r, s, deadline
↓
Contract calls: token.permit(owner, spender, amount, deadline, v, r, s)
↓
Token contract validates:
- Signature is valid
- Deadline not expired
- Nonce not reused
↓
Allowance[owner][spender] += amount
↓
Repay proceeds with transferFrom
```
**Key Differences:**
- **Variable Rate:** Scaled balance approach, interest in index
- **Gasless Approval:** Permit allows approval without separate transaction
- **Same Repay Logic:** After permit, execution is identical to regular `repay()`
- **Stable Rate Deprecated:** v3.2.0+ only supports variable rate borrowing
- **Permit Failure Safe:** Try-catch ensures repay continues even if permit fails
---
## Event Details
### Repay Event
```solidity
event Repay(
address indexed reserve, // Asset address
address indexed user, // Debt owner (onBehalfOf)
address indexed repayer, // msg.sender
uint256 amount, // Amount repaid
bool useATokens // True if repaid using aTokens (false for permit)
);
```
### IsolationModeTotalDebtUpdated Event
Emitted when repaying isolated debt.
```solidity
event IsolationModeTotalDebtUpdated(
address indexed asset,
uint256 totalDebt
);
```
---
## Error Conditions
| Error | Condition | File |
|-------|-----------|------|
| `INVALID_AMOUNT` | `amount == 0` | ValidationLogic.sol |
| `NO_DEBT_OF_SELECTED_TYPE` | User has no variable debt | ValidationLogic.sol |
| `INVALID_INTEREST_RATE_MODE_SELECTED` | Interest rate mode != VARIABLE | ValidationLogic.sol |
| `NO_EXPLICIT_AMOUNT_TO_REPAY_ON_BEHALF` | amount=MAX but user != onBehalfOf | ValidationLogic.sol |
| `RESERVE_INACTIVE` | Reserve is not active | ValidationLogic.sol |
| `RESERVE_PAUSED` | Reserve is paused | ValidationLogic.sol |
**Permit-Specific Considerations:**
- **Invalid Signature:** If `permitV/R/S` don't form a valid signature, the try-catch silently ignores it
- **Expired Deadline:** If `block.timestamp > deadline`, permit reverts (caught by try-catch)
- **Replay:** If nonce already used, permit reverts (caught by try-catch)
- **Insufficient Allowance:** Will revert during `safeTransferFrom` if permit didn't grant enough allowance
---
## Related Flows
- [Repay Flow](./repay.md) - Standard repay without permit
- [Borrow Flow](./borrow.md) - Taking out debt
- [Liquidation Flow](./liquidation.md) - Debt repayment via liquidation
---
## 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/AToken.sol
contracts/protocol/libraries/logic/ReserveLogic.sol
```