Skip to content

Commit 10ddd1a

Browse files
authored
fix: better allowance mechanism + informal issue fixes
1 parent 4cc61ba commit 10ddd1a

13 files changed

Lines changed: 115 additions & 56 deletions

File tree

docs/3.5/Aave-v3.5-features.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ Aave historically has never accurately tracked allowance. The reason for this is
8080
For allowance / approval, this means that the consumed allowance is not always equal to the amount transferred. While this problem is not perfectly solvable without breaking changes, in v3.5 the protocol ensures that the exact consumed allowance is burned if available.
8181

8282
Example: When a user calls `transferFrom(sender, recipient, 100)` in most cases the transfer will transfer slightly more than `100` tokens (e.g `101`). This is due to precision loss between assets/shares conversion.
83-
In Aave versions `< 3.5` this action would always result in burning `100` allowance. On Aave v3.5, the transfer will either burn `101` allowance or burn `100` allowance dependent on the users available allowance. This prevents undesired double spending of allowance, while maintaining backwards compatibility.
83+
In Aave versions `< 3.5` this action would always result in burning `100` allowance. On Aave v3.5, the transfer will check the balance difference on the sender and discount up to the difference from the allowance.
84+
Example: The user transfers `100`, but due to rounding he loses `102` balance. The allowance is reduced by up to `102`. If the original allowance was only `100`, the transaction will still pass for backwards compatibility.
8485

8586
### Misc changes
8687

snapshots/Pool.Operations.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
2-
"borrow: first borrow->borrowingEnabled": "250129",
3-
"borrow: recurrent borrow": "215047",
2+
"borrow: first borrow->borrowingEnabled": "250372",
3+
"borrow: recurrent borrow": "215290",
44
"flashLoan: flash loan for one asset": "162245",
5-
"flashLoan: flash loan for one asset and borrow": "267279",
5+
"flashLoan: flash loan for one asset and borrow": "267522",
66
"flashLoan: flash loan for two assets": "257244",
7-
"flashLoan: flash loan for two assets and borrow": "461912",
7+
"flashLoan: flash loan for two assets and borrow": "462398",
88
"flashLoanSimple: simple flash loan": "138143",
99
"liquidationCall: deficit on liquidated asset": "352397",
1010
"liquidationCall: deficit on liquidated asset + other asset": "438234",
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"batchLiquidate: liquidate 2 users": "486305",
33
"repayAndWithdraw: borrow disabled, collateral disabled": "290170",
4-
"supplyAndBorrow: first supply->collateralEnabled, first borrow": "377351"
4+
"supplyAndBorrow: first supply->collateralEnabled, first borrow": "377594"
55
}

snapshots/StataTokenV2.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"claimRewards": "359625",
33
"deposit": "274833",
4-
"depositATokens": "220899",
4+
"depositATokens": "221434",
55
"redeem": "197171",
66
"redeemAToken": "147141"
77
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"borrowETH": "237215",
2+
"borrowETH": "239507",
33
"depositETH": "195025",
44
"repayETH": "179129",
5-
"withdrawETH": "247524"
5+
"withdrawETH": "248059"
66
}

src/contracts/instances/VariableDebtTokenMainnetInstanceGHO.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,6 @@ contract VariableDebtTokenMainnetInstanceGHO is VariableDebtToken {
4848
);
4949
}
5050

51-
// @note deprecated discount hook being called by stkAAVE
51+
// @note deprecated discount hook being called by stkAAVE, not used since v3.4
5252
function updateDiscountDistribution(address, address, uint256, uint256, uint256) external {}
5353
}

src/contracts/interfaces/IAToken.sol

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,14 @@ interface IAToken is IERC20, IScaledBalanceToken, IInitializableAToken {
3636
) external returns (bool);
3737

3838
/**
39-
* @notice Burns aTokens from `user` and sends the equivalent amount of underlying to `receiverOfUnderlying`
40-
* @dev In some instances, the mint event could be emitted from a burn transaction
41-
* if the amount to burn is less than the interest that the user accrued
39+
* @notice Burns aTokens from `user` and sends the equivalent amount of underlying to `receiverOfUnderlying`.
40+
* @dev Passing both the unscaled and scaled amounts enhances precision. The `scaledAmount` is used for precise balance updates,
41+
* while the `amount` is used for the underlying asset transfer, preventing cumulative rounding errors.
42+
* @dev In some instances, a mint event may be emitted from a burn transaction if the amount to burn is less than the interest that the user accrued.
4243
* @param from The address from which the aTokens will be burned
4344
* @param receiverOfUnderlying The address that will receive the underlying
44-
* @param amount The amount being burned
45-
* @param scaledAmount The scaled amount being burned
45+
* @param amount The amount of underlying to be burned (non scaled)
46+
* @param scaledAmount The scaled amount of aTokens to be burned (scaled)
4647
* @param index The next liquidity index of the reserve
4748
* @return `true` if the the new balance of the user is 0
4849
*/
@@ -62,11 +63,13 @@ interface IAToken is IERC20, IScaledBalanceToken, IInitializableAToken {
6263
function mintToTreasury(uint256 scaledAmount, uint256 index) external;
6364

6465
/**
65-
* @notice Transfers aTokens in the event of a borrow being liquidated, in case the liquidators reclaims the aToken
66+
* @notice Transfers aTokens in the event of a borrow being liquidated, in case the liquidator reclaims the aToken.
67+
* @dev Passing both the unscaled and scaled amounts enhances precision. The `scaledAmount` is used for precise balance updates,
68+
* while the `amount` is used for logging and consistency, preventing cumulative rounding errors.
6669
* @param from The address getting liquidated, current owner of the aTokens
6770
* @param to The recipient
68-
* @param amount The amount of tokens getting transferred
69-
* @param scaledAmount The scaled amount of tokens getting transferred
71+
* @param amount The amount of tokens getting transferred (non-scaled)
72+
* @param scaledAmount The scaled amount of tokens getting transferred (scaled)
7073
* @param index The next liquidity index of the reserve
7174
*/
7275
function transferOnLiquidation(

src/contracts/interfaces/IVariableDebtToken.sol

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import {IInitializableDebtToken} from './IInitializableDebtToken.sol';
1111
*/
1212
interface IVariableDebtToken is IScaledBalanceToken, IInitializableDebtToken {
1313
/**
14-
* @notice Mints debt token to the `onBehalfOf` address
14+
* @notice Mints debt token to the `onBehalfOf` address.
15+
* @dev Passing both the unscaled and scaled amounts enhances precision. The `scaledAmount` is used for precise balance updates,
16+
* while the `amount` is used for allowance checks, preventing cumulative rounding errors.
1517
* @param user The address receiving the borrowed underlying, being the delegatee in case
1618
* of credit delegate, or same as `onBehalfOf` otherwise
1719
* @param onBehalfOf The address receiving the debt tokens
18-
* @param amount The amount of debt being accounted for on the allowance
19-
* @param scaledAmount The scaledAmount of debt being minted
20+
* @param amount The unscaled amount of debt to be accounted for allowance
21+
* @param scaledAmount The scaled amount of debt tokens to mint
2022
* @param index The variable debt index of the reserve
2123
* @return The scaled total debt of the reserve
2224
*/
@@ -29,11 +31,11 @@ interface IVariableDebtToken is IScaledBalanceToken, IInitializableDebtToken {
2931
) external returns (uint256);
3032

3133
/**
32-
* @notice Burns user variable debt
33-
* @dev In some instances, a burn transaction will emit a mint event
34-
* if the amount to burn is less than the interest that the user accrued
34+
* @notice Burns user variable debt.
35+
* @dev Passing the scaled amount allows for more precise calculations and avoids cumulative errors from repeated conversions.
36+
* @dev In some instances, a burn transaction will emit a mint event if the amount to burn is less than the interest that the user accrued.
3537
* @param from The address from which the debt will be burned
36-
* @param scaledAmount The scaled amount getting burned
38+
* @param scaledAmount The scaled amount of debt getting burned
3739
* @param index The variable debt index of the reserve
3840
* @return True if the new balance is zero
3941
* @return The scaled total debt of the reserve

src/contracts/protocol/libraries/helpers/TokenMath.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: MIT
2-
pragma solidity ^0.8.10;
2+
pragma solidity ^0.8.0;
33

44
import {WadRayMath} from '../../libraries/math/WadRayMath.sol';
55

src/contracts/protocol/tokenization/AToken.sol

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -191,20 +191,41 @@ abstract contract AToken is VersionedInitializable, ScaledBalanceTokenBase, EIP7
191191
uint256 amount
192192
) external virtual override(IERC20, IncentivizedERC20) returns (bool) {
193193
uint256 index = POOL.getReserveNormalizedIncome(_underlyingAsset);
194+
uint256 scaledBalanceOfSender = super.balanceOf(sender);
194195
_spendAllowance(
195196
sender,
196197
_msgSender(),
197198
amount,
198-
// According to the ERC20 specification, the spent allowance should reflect the amount transferred.
199-
// Following the spec exactly is impossible though, as the allowance references the "scaled up" amount while the transfer operates with "scaled down" amount.
200-
// Because of this different handling of amounts, there are amounts that are impossible to accurately reflect on the balance.
201-
// As an example: transferFrom(..., 1) at a liquidity index of 2e27, can never transfer exactly `1` as the smallest unit that can be accounted for would be 2.
202-
// In addition to that the existing balance has an effect on the final "scaled up" balance after transfer.
203-
// As an example: transferFrom(..., 1) at a liquidity index of 1.1 where the recipient has a "scaled up" balance of `9 * 1.1 = 9.9 = 9` before the transfer, will have a balance of `10 * 1.1. = 11` after the Transfer.
204-
// While this problem is not solvable without introducing breaking changes, on Aave v3.5 the situation is improved in the following way:
205-
// - The `correct` amount to be deducted is considered to be `scaledUpFloor(scaledDownCeil(input.amount))`. This replicates the behavior on transfer, followed by a balanceOf.
206-
// - To avoid breaking existing integrations, the amount deducted from the allowance is the minimum of the available allowance and the actual up-scaled asset amount.
207-
amount.getATokenTransferScaledAmount(index).getATokenBalance(index)
199+
// This comment explains the logic behind the allowance spent calculation.
200+
//
201+
// Problem:
202+
// Simply decreasing the allowance by the input `amount` is not ideal for scaled-balance tokens.
203+
// Due to rounding, the actual decrease in the sender's balance (`amount_out`) can be slightly
204+
// larger than the input `amount`.
205+
//
206+
// Definitions:
207+
// - `amount`: The unscaled amount to be transferred, passed as an argument.
208+
// - `amount_out`: The actual unscaled amount deducted from the sender's balance.
209+
// - `amount_in`: The actual unscaled amount added to the recipient's balance.
210+
// - `allowance_spent`: The unscaled amount deducted from the spender's allowance.
211+
// - `amount_logged`: The amount logged in the `Transfer` event.
212+
//
213+
// Solution:
214+
// To fix this, `allowance_spent` must be exactly equal to `amount_out`.
215+
// We calculate `amount_out` precisely by simulating the effect of the transfer on the sender's balance.
216+
// `actualBalanceDecrease` in the code corresponds to `amount_out`.
217+
// By passing `actualBalanceDecrease` to `_spendAllowance`, we ensure `allowance_spent` is as close as possible to `amount_out`.
218+
// `amount_logged` is equal to `amount`. `amount_in` is the actual balance increase for the recipient, which is >= `amount` due to rounding.
219+
//
220+
// Backward Compatibility & Guarantees:
221+
// This implementation is backward-compatible and secure. The `_spendAllowance` function has a critical feature:
222+
// 1. It REQUIRES the allowance to be >= `amount` (the user's requested transfer amount).
223+
// 2. The amount consumed from the allowance is `actualBalanceDecrease`, but it is capped at the `currentAllowance`.
224+
// This means if a user has an allowance of 100 wei and calls `transferFrom` with an `amount` of 100, the call will succeed
225+
// even if the calculated `actualBalanceDecrease` is 101 wei. In that specific scenario, the allowance consumed will be 100 wei (since that is the `currentAllowance`),
226+
// and the transaction will not revert. But if the allowance is 101 wei, then the allowance consumed will be 101 wei.
227+
scaledBalanceOfSender.getATokenBalance(index) -
228+
(scaledBalanceOfSender - amount.getATokenTransferScaledAmount(index)).getATokenBalance(index)
208229
);
209230
_transfer(sender, recipient, amount.toUint120());
210231
return true;

0 commit comments

Comments
 (0)