Skip to content

Commit b7beed9

Browse files
authored
fix: adjust allowance consumption
Historically transferFrom etc have consumed ther users input as allowance. That has never really been accurate, as the users input might not actually be transferrable due to rounding, to account for that we improved the logic to consumer "up to the raw scaled up transfer amount".
1 parent 070cd23 commit b7beed9

13 files changed

Lines changed: 315 additions & 27 deletions

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ In v3.5 we decided to double down on these robustness improvements:
7474

7575
- on `repayWithAToken` and `withdraw`, the collateral flag is now properly set to `false` when burning all aTokens in the process. In previous versions of the protocol, there were edge cases in which the collateral flag was not properly updated.
7676

77+
### Improved allowance
78+
79+
Aave historically has never accurately tracked allowance. The reason for this is that in practice most operations are performed with the desired amount of `assets`, but the a/v token converting these amounts to `shares`.
80+
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.
81+
82+
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.
84+
7785
### Misc changes
7886

7987
- smaller refactoring in `LiquidationLogic` making the code more consistent

snapshots/AToken.transfer.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
2-
"full amount; receiver: ->enableCollateral": "138978",
3-
"full amount; sender: ->disableCollateral;": "97383",
4-
"full amount; sender: ->disableCollateral; receiver: ->enableCollateral": "139095",
5-
"full amount; sender: ->disableCollateral; receiver: dirty, ->enableCollateral": "127143",
6-
"full amount; sender: collateralDisabled": "97266",
7-
"partial amount; sender: collateralDisabled;": "97242",
8-
"partial amount; sender: collateralDisabled; receiver: ->enableCollateral": "138954",
9-
"partial amount; sender: collateralEnabled;": "97450",
10-
"partial amount; sender: collateralEnabled; receiver: ->enableCollateral": "139162"
2+
"full amount; receiver: ->enableCollateral": "138977",
3+
"full amount; sender: ->disableCollateral;": "97382",
4+
"full amount; sender: ->disableCollateral; receiver: ->enableCollateral": "139094",
5+
"full amount; sender: ->disableCollateral; receiver: dirty, ->enableCollateral": "127142",
6+
"full amount; sender: collateralDisabled": "97265",
7+
"partial amount; sender: collateralDisabled;": "97241",
8+
"partial amount; sender: collateralDisabled; receiver: ->enableCollateral": "138953",
9+
"partial amount; sender: collateralEnabled;": "97449",
10+
"partial amount; sender: collateralEnabled; receiver: ->enableCollateral": "139161"
1111
}
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
2-
"full amount; sender: with delegations, ->disableCollateral; receiver: without delegations, ->enableCollateral": "158388",
3-
"full amount; sender: with delegations, delegatee, ->disableCollateral; receiver: with delegations, delegatee, ->enableCollateral": "154326",
4-
"full amount; sender: with delegations, not delegatee, ->disableCollateral; receiver: with delegations, not delegatee, ->enableCollateral": "154326",
5-
"full amount; sender: without delegations, ->disableCollateral; receiver: with delegations, ->enableCollateral": "141288",
6-
"full amount; sender: without delegations, delegatee, ->disableCollateral; receiver: without delegations, delegatee, ->enableCollateral": "145351",
7-
"full amount; sender: without delegations, not delegatee, ->disableCollateral; receiver: without delegations, not delegatee, ->enableCollateral": "145351"
2+
"full amount; sender: with delegations, ->disableCollateral; receiver: without delegations, ->enableCollateral": "158387",
3+
"full amount; sender: with delegations, delegatee, ->disableCollateral; receiver: with delegations, delegatee, ->enableCollateral": "154325",
4+
"full amount; sender: with delegations, not delegatee, ->disableCollateral; receiver: with delegations, not delegatee, ->enableCollateral": "154325",
5+
"full amount; sender: without delegations, ->disableCollateral; receiver: with delegations, ->enableCollateral": "141287",
6+
"full amount; sender: without delegations, delegatee, ->disableCollateral; receiver: without delegations, delegatee, ->enableCollateral": "145350",
7+
"full amount; sender: without delegations, not delegatee, ->disableCollateral; receiver: without delegations, not delegatee, ->enableCollateral": "145350"
88
}

snapshots/Pool.Operations.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
"flashLoan: flash loan for two assets": "257524",
77
"flashLoan: flash loan for two assets and borrow": "461097",
88
"flashLoanSimple: simple flash loan": "138283",
9-
"liquidationCall: deficit on liquidated asset": "352448",
10-
"liquidationCall: deficit on liquidated asset + other asset": "438268",
11-
"liquidationCall: full liquidation": "352448",
12-
"liquidationCall: full liquidation and receive ATokens": "349752",
13-
"liquidationCall: partial liquidation": "343371",
14-
"liquidationCall: partial liquidation and receive ATokens": "340675",
9+
"liquidationCall: deficit on liquidated asset": "352447",
10+
"liquidationCall: deficit on liquidated asset + other asset": "438267",
11+
"liquidationCall: full liquidation": "352447",
12+
"liquidationCall: full liquidation and receive ATokens": "349750",
13+
"liquidationCall: partial liquidation": "343370",
14+
"liquidationCall: partial liquidation and receive ATokens": "340673",
1515
"repay: full repay": "161770",
1616
"repay: full repay with ATokens": "163597",
1717
"repay: partial repay": "158122",
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"batchLiquidate: liquidate 2 users": "486407",
2+
"batchLiquidate: liquidate 2 users": "486405",
33
"repayAndWithdraw: borrow disabled, collateral disabled": "290170",
44
"supplyAndBorrow: first supply->collateralEnabled, first borrow": "372109"
55
}

snapshots/StataTokenV2.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"claimRewards": "359625",
33
"deposit": "274833",
4-
"depositATokens": "218541",
4+
"depositATokens": "220899",
55
"redeem": "197171",
6-
"redeemAToken": "147142"
6+
"redeemAToken": "147141"
77
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"borrowETH": "236036",
2+
"borrowETH": "236535",
33
"depositETH": "195025",
44
"repayETH": "179154",
5-
"withdrawETH": "244706"
5+
"withdrawETH": "247524"
66
}

src/contracts/interfaces/ICreditDelegationToken.sol

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ interface ICreditDelegationToken {
2121
uint256 amount
2222
);
2323

24+
/**
25+
* @dev Indicates a failure with the `spender`’s `allowance`. Used in borrowing.
26+
* @param spender Address that may be allowed to operate on tokens without being their owner.
27+
* @param allowance Amount of tokens a `spender` is allowed to operate with.
28+
* @param needed Minimum amount required to perform a transfer.
29+
*/
30+
error InsufficientBorrowAllowance(address spender, uint256 allowance, uint256 needed);
31+
2432
/**
2533
* @notice Delegates borrowing power to a user on the specific debt token.
2634
* Delegation will still respect the liquidation constraints (even if delegated, a

src/contracts/protocol/tokenization/AToken.sol

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,32 @@ abstract contract AToken is VersionedInitializable, ScaledBalanceTokenBase, EIP7
190190
_approve(owner, spender, value);
191191
}
192192

193+
/// @inheritdoc IERC20
194+
function transferFrom(
195+
address sender,
196+
address recipient,
197+
uint256 amount
198+
) external virtual override(IERC20, IncentivizedERC20) returns (bool) {
199+
uint256 index = POOL.getReserveNormalizedIncome(_underlyingAsset);
200+
_spendAllowance(
201+
sender,
202+
_msgSender(),
203+
amount,
204+
// According to the ERC20 specification, the spent allowance should reflect the amount transferred.
205+
// Following the spec exactly is impossible though, as the allowance references the "scaled up" amount while the transfer operates with "scaled down" amount.
206+
// Because of this different handling of amounts, there are amounts that are impossible to accurately reflect on the balance.
207+
// 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.
208+
// In addition to that the existing balance has an effect on the final "scaled up" balance after transfer.
209+
// 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.
210+
// While this problem is not solvable without introducing breaking changes, on Aave v3.5 the situation is improved in the following way:
211+
// - The `correct` amount to be deducted is considered to be `scaledUpFloor(scaledDownCeil(input.amount))`. This replicates the behavior on transfer, followed by a balanceOf.
212+
// - In order to not introduce a breaking change for existing integrations, the deducted allowance is based on the available allowance as `Max(availableAllowance, (amount, correctAmount))`
213+
amount.getATokenTransferScaledAmount(index).getATokenBalance(index)
214+
);
215+
_transfer(sender, recipient, amount.toUint120());
216+
return true;
217+
}
218+
193219
/**
194220
* @notice Overrides the parent _transfer to force validated transfer() and transferFrom()
195221
* @param from The source address

src/contracts/protocol/tokenization/VariableDebtToken.sol

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,23 @@ abstract contract VariableDebtToken is DebtTokenBase, ScaledBalanceTokenBase, IV
8484
uint256 index
8585
) external virtual override onlyPool returns (uint256) {
8686
if (user != onBehalfOf) {
87-
_decreaseBorrowAllowance(onBehalfOf, user, amount);
87+
uint256 borrowAllowance = _borrowAllowances[onBehalfOf][user];
88+
if (borrowAllowance < amount) {
89+
revert InsufficientBorrowAllowance(user, borrowAllowance, amount);
90+
}
91+
// When borrowing on behalf of a user, the borrower specified an "amount", which is measured in the underlying the user wants to receive.
92+
// The protocol internally works, with a "scaled down" representation of the amount, which in most cases loses precision.
93+
// In practice this means that when borrowing `n`, the user might receive `n+m` debt.
94+
// Similar to the aToken `transferFrom` function, handling this scenario exactly is impossible.
95+
// While this problem is not solvable without introducing breaking changes, on Aave v3.5 the situation is improved in the following way:
96+
// - The `correct` amount to be deducted is considered to be `scaledUpCeil(scaledAmount)`. This replicates the behavior on borrow followed by a balanceOf.
97+
// - In order to not introduce a breaking change for existing integrations, the deducted allowance is based on the available allowance as `Max(availableAllowance, (amount, correctAmount))`
98+
uint256 scaledUp = scaledAmount.getVTokenBalance(index);
99+
_decreaseBorrowAllowance(
100+
onBehalfOf,
101+
user,
102+
borrowAllowance >= scaledUp ? scaledUp : borrowAllowance
103+
);
88104
}
89105
_mintScaled({
90106
caller: user,

0 commit comments

Comments
 (0)