Skip to content

Commit a443ac9

Browse files
committed
feat: temporary approvals in AllowancePositionManager
1 parent f33c26d commit a443ac9

File tree

5 files changed

+419
-48
lines changed

5 files changed

+419
-48
lines changed

snapshots/PositionManager.Operations.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"AllowancePositionManager - borrowOnBehalfOf": "306478",
3-
"AllowancePositionManager - withdrawOnBehalfOf: full": "121565",
4-
"AllowancePositionManager - withdrawOnBehalfOf: partial": "131756",
2+
"AllowancePositionManager - borrowOnBehalfOf": "307050",
3+
"AllowancePositionManager - withdrawOnBehalfOf: full": "122024",
4+
"AllowancePositionManager - withdrawOnBehalfOf: partial": "132329",
55
"Common - setSelfAsUserPositionManagerWithSig": "72588",
66
"SupplyRepayPositionManager - repayOnBehalfOf": "167131",
77
"SupplyRepayPositionManager - supplyOnBehalfOf": "135803"

src/position-manager/AllowancePositionManager.sol

Lines changed: 104 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ pragma solidity 0.8.28;
44

55
import {SignatureChecker} from 'src/dependencies/openzeppelin/SignatureChecker.sol';
66
import {SafeERC20, IERC20} from 'src/dependencies/openzeppelin/SafeERC20.sol';
7+
import {SlotDerivation} from 'src/dependencies/openzeppelin/SlotDerivation.sol';
8+
import {TransientSlot} from 'src/dependencies/openzeppelin/TransientSlot.sol';
79
import {EIP712} from 'src/dependencies/solady/EIP712.sol';
810
import {MathUtils} from 'src/libraries/math/MathUtils.sol';
911
import {NoncesKeyed} from 'src/utils/NoncesKeyed.sol';
@@ -24,15 +26,27 @@ contract AllowancePositionManager is
2426
using SafeERC20 for IERC20;
2527
using EIP712Hash for *;
2628
using MathUtils for uint256;
29+
using SlotDerivation for bytes32;
30+
using TransientSlot for *;
2731

2832
/// @notice Mapping of withdraw allowances.
2933
mapping(address owner => mapping(address spender => mapping(uint256 reserveId => uint256 amount)))
3034
private _withdrawAllowances;
3135

36+
/// @notice Slot for the temporary withdraw allowances.
37+
/// @dev keccak256('temporary.withdrawAllowances')
38+
bytes32 private constant _TEMPORARY_WITHDRAW_ALLOWANCES_SLOT =
39+
0x1c6a61279a13a86a789311ddf30aee38e2f4a9f6c4aad1ff4a2e75a4018e68c3;
40+
3241
/// @notice Mapping of credit delegations.
3342
mapping(address owner => mapping(address spender => mapping(uint256 reserveId => uint256 amount)))
3443
private _creditDelegations;
3544

45+
/// @notice Slot for the temporary credit delegations.
46+
/// @dev keccak256('temporary.creditDelegations')
47+
bytes32 private constant _TEMPORARY_CREDIT_DELEGATIONS_SLOT =
48+
0xcd470af8670f5baa744a0341af8a2e3f5d7ca086178908432a5cfaf39cb9299d;
49+
3650
/// @dev Constructor.
3751
/// @param spoke_ The address of the spoke contract.
3852
constructor(address spoke_) PositionManagerBase(spoke_) {}
@@ -58,6 +72,11 @@ contract AllowancePositionManager is
5872
emit WithdrawApproval(user, params.spender, params.reserveId, params.amount);
5973
}
6074

75+
/// @inheritdoc IAllowancePositionManager
76+
function temporaryApproveWithdraw(address spender, uint256 reserveId, uint256 amount) external {
77+
_temporaryWithdrawAllowancesSlot(msg.sender, spender, reserveId).tstore(amount);
78+
}
79+
6180
/// @inheritdoc IAllowancePositionManager
6281
function approveCreditDelegation(address spender, uint256 reserveId, uint256 amount) external {
6382
_creditDelegations[msg.sender][spender][reserveId] = amount;
@@ -79,6 +98,15 @@ contract AllowancePositionManager is
7998
emit CreditDelegation(user, params.spender, params.reserveId, params.amount);
8099
}
81100

101+
/// @inheritdoc IAllowancePositionManager
102+
function temporaryApproveCreditDelegation(
103+
address spender,
104+
uint256 reserveId,
105+
uint256 amount
106+
) external {
107+
_temporaryCreditDelegationsSlot(msg.sender, spender, reserveId).tstore(amount);
108+
}
109+
82110
/// @inheritdoc IAllowancePositionManager
83111
function renounceWithdrawAllowance(address owner, uint256 reserveId) external {
84112
_withdrawAllowances[owner][msg.sender][reserveId] = 0;
@@ -98,9 +126,7 @@ contract AllowancePositionManager is
98126
address onBehalfOf
99127
) external returns (uint256, uint256) {
100128
require(amount > 0, InvalidAmount());
101-
uint256 currentAllowance = _withdrawAllowances[onBehalfOf][msg.sender][reserveId];
102-
require(currentAllowance >= amount, InsufficientWithdrawAllowance(currentAllowance, amount));
103-
_withdrawAllowances[onBehalfOf][msg.sender][reserveId] = currentAllowance.uncheckedSub(amount);
129+
_spendWithdrawAllowance(onBehalfOf, msg.sender, reserveId, amount);
104130

105131
IERC20 asset = _getReserveUnderlying(reserveId);
106132
(uint256 withdrawnShares, uint256 withdrawnAmount) = ISpokeBase(SPOKE).withdraw(
@@ -120,9 +146,7 @@ contract AllowancePositionManager is
120146
address onBehalfOf
121147
) external returns (uint256, uint256) {
122148
require(amount > 0, InvalidAmount());
123-
uint256 currentAllowance = _creditDelegations[onBehalfOf][msg.sender][reserveId];
124-
require(currentAllowance >= amount, InsufficientCreditDelegation(currentAllowance, amount));
125-
_creditDelegations[onBehalfOf][msg.sender][reserveId] = currentAllowance.uncheckedSub(amount);
149+
_spendCreditDelegation(onBehalfOf, msg.sender, reserveId, amount);
126150

127151
IERC20 asset = _getReserveUnderlying(reserveId);
128152
(uint256 borrowedShares, uint256 borrowedAmount) = ISpokeBase(SPOKE).borrow(
@@ -171,4 +195,78 @@ contract AllowancePositionManager is
171195
function _domainNameAndVersion() internal pure override returns (string memory, string memory) {
172196
return ('AllowancePositionManager', '1');
173197
}
198+
199+
/// @dev Temporary allowance takes precedence over stored allowance, and does not cumulate.
200+
function _spendWithdrawAllowance(
201+
address onBehalfOf,
202+
address spender,
203+
uint256 reserveId,
204+
uint256 amount
205+
) internal {
206+
uint256 temporaryAllowance = _temporaryWithdrawAllowancesSlot(onBehalfOf, spender, reserveId)
207+
.tload();
208+
if (temporaryAllowance > 0) {
209+
require(
210+
temporaryAllowance >= amount,
211+
InsufficientWithdrawAllowance(temporaryAllowance, amount)
212+
);
213+
_temporaryWithdrawAllowancesSlot(onBehalfOf, spender, reserveId).tstore(
214+
temporaryAllowance.uncheckedSub(amount)
215+
);
216+
} else {
217+
uint256 allowance = _withdrawAllowances[onBehalfOf][spender][reserveId];
218+
require(allowance >= amount, InsufficientWithdrawAllowance(allowance, amount));
219+
_withdrawAllowances[onBehalfOf][spender][reserveId] = allowance.uncheckedSub(amount);
220+
}
221+
}
222+
223+
/// @dev Temporary allowance takes precedence over stored allowance, and does not cumulate.
224+
function _spendCreditDelegation(
225+
address onBehalfOf,
226+
address spender,
227+
uint256 reserveId,
228+
uint256 amount
229+
) internal {
230+
uint256 temporaryAllowance = _temporaryCreditDelegationsSlot(onBehalfOf, spender, reserveId)
231+
.tload();
232+
if (temporaryAllowance > 0) {
233+
require(
234+
temporaryAllowance >= amount,
235+
InsufficientCreditDelegation(temporaryAllowance, amount)
236+
);
237+
_temporaryCreditDelegationsSlot(onBehalfOf, spender, reserveId).tstore(
238+
temporaryAllowance.uncheckedSub(amount)
239+
);
240+
} else {
241+
uint256 allowance = _creditDelegations[onBehalfOf][spender][reserveId];
242+
require(allowance >= amount, InsufficientCreditDelegation(allowance, amount));
243+
_creditDelegations[onBehalfOf][spender][reserveId] = allowance.uncheckedSub(amount);
244+
}
245+
}
246+
247+
function _temporaryWithdrawAllowancesSlot(
248+
address owner,
249+
address spender,
250+
uint256 reserveId
251+
) internal pure returns (TransientSlot.Uint256Slot) {
252+
return
253+
_TEMPORARY_WITHDRAW_ALLOWANCES_SLOT
254+
.deriveMapping(owner)
255+
.deriveMapping(spender)
256+
.deriveMapping(reserveId)
257+
.asUint256();
258+
}
259+
260+
function _temporaryCreditDelegationsSlot(
261+
address owner,
262+
address spender,
263+
uint256 reserveId
264+
) internal pure returns (TransientSlot.Uint256Slot) {
265+
return
266+
_TEMPORARY_CREDIT_DELEGATIONS_SLOT
267+
.deriveMapping(owner)
268+
.deriveMapping(spender)
269+
.deriveMapping(reserveId)
270+
.asUint256();
271+
}
174272
}

src/position-manager/interfaces/IAllowancePositionManager.sol

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ interface IAllowancePositionManager is IPositionManagerBase {
4444
bytes calldata signature
4545
) external;
4646

47+
/// @notice Temporarily approves a spender to withdraw assets from the specified reserve on the spoke.
48+
/// @dev The allowance is discarded after the transaction.
49+
/// @param spender The address of the spender to receive the allowance.
50+
/// @param reserveId The identifier of the reserve.
51+
/// @param amount The amount of allowance.
52+
function temporaryApproveWithdraw(address spender, uint256 reserveId, uint256 amount) external;
53+
4754
/// @notice Approves a credit delegation allowance for a spender.
4855
/// @param spender The address of the spender to receive the allowance.
4956
/// @param reserveId The identifier of the reserve.
@@ -58,6 +65,17 @@ interface IAllowancePositionManager is IPositionManagerBase {
5865
bytes calldata signature
5966
) external;
6067

68+
/// @notice Temporarily approves a credit delegation allowance for a spender.
69+
/// @dev The allowance is discarded after the transaction.
70+
/// @param spender The address of the spender to receive the allowance.
71+
/// @param reserveId The identifier of the reserve.
72+
/// @param amount The amount of allowance.
73+
function temporaryApproveCreditDelegation(
74+
address spender,
75+
uint256 reserveId,
76+
uint256 amount
77+
) external;
78+
6179
/// @notice Renounces the withdraw allowance given by the owner.
6280
/// @param owner The address of the owner.
6381
/// @param reserveId The identifier of the reserve.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
// Copyright (c) 2025 Aave Labs
3+
pragma solidity ^0.8.0;
4+
5+
import {TransientSlot} from 'src/dependencies/openzeppelin/TransientSlot.sol';
6+
import {AllowancePositionManager} from 'src/position-manager/AllowancePositionManager.sol';
7+
8+
contract AllowancePositionManagerWrapper is AllowancePositionManager {
9+
using TransientSlot for *;
10+
11+
constructor(address spoke_) AllowancePositionManager(spoke_) {}
12+
13+
function temporaryWithdrawAllowance(
14+
address owner,
15+
address spender,
16+
uint256 reserveId
17+
) external view returns (uint256) {
18+
return _temporaryWithdrawAllowancesSlot(owner, spender, reserveId).tload();
19+
}
20+
21+
function temporaryCreditDelegation(
22+
address owner,
23+
address spender,
24+
uint256 reserveId
25+
) external view returns (uint256) {
26+
return _temporaryCreditDelegationsSlot(owner, spender, reserveId).tload();
27+
}
28+
}

0 commit comments

Comments
 (0)