Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions snapshots/AllowancePositionManager.Operations.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
{
"approveWithdraw": "46795",
"approveWithdrawWithSig": "63006",
"borrowOnBehalfOf": "306519",
"borrowOnBehalfOf": "307050",
"borrowOnBehalfOf (with temporary allowance)": "247328",
"delegateCredit": "46784",
"delegateCreditWithSig": "63059",
"renounceCreditDelegation": "24915",
"renounceWithdrawAllowance": "24926",
"withdrawOnBehalfOf: full": "121598",
"withdrawOnBehalfOf: partial": "131797"
"delegateCreditWithSig": "63081",
"renounceCreditDelegation": "24937",
"renounceWithdrawAllowance": "24837",
"temporaryApproveWithdraw": "22680",
"temporaryDelegateCredit": "22768",
"withdrawOnBehalfOf: full": "122006",
"withdrawOnBehalfOf: full (with temporary allowance)": "56184",
"withdrawOnBehalfOf: partial": "132307",
"withdrawOnBehalfOf: partial (with temporary allowance)": "66984"
}
95 changes: 89 additions & 6 deletions src/position-manager/AllowancePositionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ pragma solidity 0.8.28;

import {SignatureChecker} from 'src/dependencies/openzeppelin/SignatureChecker.sol';
import {SafeERC20, IERC20} from 'src/dependencies/openzeppelin/SafeERC20.sol';
import {SlotDerivation} from 'src/dependencies/openzeppelin/SlotDerivation.sol';
import {TransientSlot} from 'src/dependencies/openzeppelin/TransientSlot.sol';
import {EIP712} from 'src/dependencies/solady/EIP712.sol';
import {MathUtils} from 'src/libraries/math/MathUtils.sol';
import {NoncesKeyed} from 'src/utils/NoncesKeyed.sol';
Expand All @@ -24,6 +26,18 @@ contract AllowancePositionManager is
using SafeERC20 for IERC20;
using EIP712Hash for *;
using MathUtils for uint256;
using SlotDerivation for bytes32;
using TransientSlot for *;

/// @notice Slot for the temporary withdraw allowances.
/// @dev keccak256(abi.encode(uint256(keccak256("aave.transient.WITHDRAW_ALLOWANCES")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant _TEMPORARY_WITHDRAW_ALLOWANCES_SLOT =
0x4b5553e643854b1bacc0d454fec49da235a0faac2caff4f059541ccf9f154700;

/// @notice Slot for the temporary credit delegations.
/// @dev keccak256(abi.encode(uint256(keccak256("aave.transient.CREDIT_DELEGATIONS")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant _TEMPORARY_CREDIT_DELEGATIONS_SLOT =
0x5aa827cbd079fec1557555542f5232f82e413903ea6ea8e935f719e23b7c4a00;

/// @notice Mapping of withdraw allowances.
mapping(address owner => mapping(address spender => mapping(uint256 reserveId => uint256 amount)))
Expand Down Expand Up @@ -68,6 +82,12 @@ contract AllowancePositionManager is
});
}

/// @inheritdoc IAllowancePositionManager
function temporaryApproveWithdraw(address spender, uint256 reserveId, uint256 amount) external {
_temporaryWithdrawAllowancesSlot({owner: msg.sender, spender: spender, reserveId: reserveId})
.tstore(amount);
}

/// @inheritdoc IAllowancePositionManager
function delegateCredit(address spender, uint256 reserveId, uint256 amount) external {
_updateCreditDelegation({
Expand Down Expand Up @@ -99,6 +119,12 @@ contract AllowancePositionManager is
});
}

/// @inheritdoc IAllowancePositionManager
function temporaryDelegateCredit(address spender, uint256 reserveId, uint256 amount) external {
_temporaryDelegateCreditsSlot({owner: msg.sender, spender: spender, reserveId: reserveId})
.tstore(amount);
}

/// @inheritdoc IAllowancePositionManager
function renounceWithdrawAllowance(address owner, uint256 reserveId) external {
if (_withdrawAllowances[owner][msg.sender][reserveId] == 0) {
Expand Down Expand Up @@ -206,26 +232,57 @@ contract AllowancePositionManager is
return EIP712Hash.CREDIT_DELEGATION_TYPEHASH;
}

/// @dev Temporary allowance takes precedence over stored allowance, and does not cumulate.
function _spendWithdrawAllowance(
address owner,
address spender,
uint256 reserveId,
uint256 amount
) internal {
uint256 currentAllowance = _withdrawAllowances[owner][spender][reserveId];
require(currentAllowance >= amount, InsufficientWithdrawAllowance(currentAllowance, amount));
_withdrawAllowances[owner][spender][reserveId] = currentAllowance.uncheckedSub(amount);
uint256 temporaryAllowance = _temporaryWithdrawAllowancesSlot({
owner: owner,
spender: spender,
reserveId: reserveId
}).tload();
if (temporaryAllowance > 0) {
require(
temporaryAllowance >= amount,
InsufficientTemporaryWithdrawAllowance(temporaryAllowance, amount)
);
_temporaryWithdrawAllowancesSlot({owner: owner, spender: spender, reserveId: reserveId})
.tstore(temporaryAllowance.uncheckedSub(amount));
} else {
uint256 allowance = _withdrawAllowances[owner][spender][reserveId];
require(allowance >= amount, InsufficientWithdrawAllowance(allowance, amount));
_withdrawAllowances[owner][spender][reserveId] = allowance.uncheckedSub(amount);
}
}

/// @dev Temporary allowance takes precedence over stored allowance, and does not cumulate.
function _spendCreditDelegation(
address owner,
address spender,
uint256 reserveId,
uint256 amount
) internal {
uint256 currentAllowance = _creditDelegations[owner][spender][reserveId];
require(currentAllowance >= amount, InsufficientCreditDelegation(currentAllowance, amount));
_creditDelegations[owner][spender][reserveId] = currentAllowance.uncheckedSub(amount);
uint256 temporaryAllowance = _temporaryDelegateCreditsSlot({
owner: owner,
spender: spender,
reserveId: reserveId
}).tload();
if (temporaryAllowance > 0) {
require(
temporaryAllowance >= amount,
InsufficientTemporaryCreditDelegation(temporaryAllowance, amount)
);
_temporaryDelegateCreditsSlot({owner: owner, spender: spender, reserveId: reserveId}).tstore(
temporaryAllowance.uncheckedSub(amount)
);
} else {
uint256 allowance = _creditDelegations[owner][spender][reserveId];
require(allowance >= amount, InsufficientCreditDelegation(allowance, amount));
_creditDelegations[owner][spender][reserveId] = allowance.uncheckedSub(amount);
}
}

function _updateWithdrawAllowance(
Expand All @@ -248,6 +305,32 @@ contract AllowancePositionManager is
emit CreditDelegation(owner, spender, reserveId, newCreditDelegation);
}

function _temporaryWithdrawAllowancesSlot(
address owner,
address spender,
uint256 reserveId
) internal pure returns (TransientSlot.Uint256Slot) {
return
_TEMPORARY_WITHDRAW_ALLOWANCES_SLOT
.deriveMapping(owner)
.deriveMapping(spender)
.deriveMapping(reserveId)
.asUint256();
}

function _temporaryDelegateCreditsSlot(
address owner,
address spender,
uint256 reserveId
) internal pure returns (TransientSlot.Uint256Slot) {
return
_TEMPORARY_CREDIT_DELEGATIONS_SLOT
.deriveMapping(owner)
.deriveMapping(spender)
.deriveMapping(reserveId)
.asUint256();
}

function _domainNameAndVersion() internal pure override returns (string memory, string memory) {
return ('AllowancePositionManager', '1');
}
Expand Down
22 changes: 22 additions & 0 deletions src/position-manager/interfaces/IAllowancePositionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ import {IPositionManagerBase} from 'src/position-manager/interfaces/IPositionMan
interface IAllowancePositionManager is IPositionManagerBase {
/// @notice Thrown when the withdraw allowance is insufficient.
error InsufficientWithdrawAllowance(uint256 allowance, uint256 required);
/// @notice Thrown when the temporary withdraw allowance is insufficient.
error InsufficientTemporaryWithdrawAllowance(uint256 allowance, uint256 required);
/// @notice Thrown when the credit delegation allowance is insufficient.
error InsufficientCreditDelegation(uint256 allowance, uint256 required);
/// @notice Thrown when the temporary credit delegation allowance is insufficient.
error InsufficientTemporaryCreditDelegation(uint256 allowance, uint256 required);

/// @notice Emitted when `owner` approves `spender` to withdraw `amount` for `reserveId` on their behalf.
/// @param owner The address of the owner.
Expand Down Expand Up @@ -52,6 +56,14 @@ interface IAllowancePositionManager is IPositionManagerBase {
bytes calldata signature
) external;

/// @notice Temporarily approves a spender to withdraw assets from the specified reserve on the spoke.
/// @dev Temporary allowance takes precedence over stored allowance, and does not cumulate.
/// @dev The allowance is discarded after the transaction.
/// @param spender The address of the spender to receive the allowance.
/// @param reserveId The identifier of the reserve.
/// @param amount The amount of allowance.
function temporaryApproveWithdraw(address spender, uint256 reserveId, uint256 amount) external;

/// @notice Approves a credit delegation allowance for a spender.
/// @param spender The address of the spender to receive the allowance.
/// @param reserveId The identifier of the reserve.
Expand All @@ -66,6 +78,14 @@ interface IAllowancePositionManager is IPositionManagerBase {
bytes calldata signature
) external;

/// @notice Temporarily approves a credit delegation allowance for a spender.
/// @dev Temporary allowance takes precedence over stored allowance, and does not cumulate.
/// @dev The allowance is discarded after the transaction.
/// @param spender The address of the spender to receive the allowance.
/// @param reserveId The identifier of the reserve.
/// @param amount The amount of allowance.
function temporaryDelegateCredit(address spender, uint256 reserveId, uint256 amount) external;

/// @notice Renounces the withdraw allowance given by the owner.
/// @param owner The address of the owner.
/// @param reserveId The identifier of the reserve.
Expand All @@ -78,6 +98,7 @@ interface IAllowancePositionManager is IPositionManagerBase {

/// @notice Executes a withdraw on behalf of a user.
/// @dev The caller must have sufficient withdraw allowance from `onBehalfOf`.
/// @dev Temporary allowance takes precedence over stored allowance, and does not cumulate.
/// @dev The caller receives the withdrawn assets.
/// @param reserveId The identifier of the reserve.
/// @param amount The amount to withdraw.
Expand All @@ -92,6 +113,7 @@ interface IAllowancePositionManager is IPositionManagerBase {

/// @notice Executes a borrow on behalf of a user.
/// @dev The caller must have sufficient credit delegation allowance from `onBehalfOf`.
/// @dev Temporary allowance takes precedence over stored allowance, and does not cumulate.
/// @dev The caller receives the borrowed assets.
/// @param reserveId The identifier of the reserve.
/// @param amount The amount to borrow.
Expand Down
55 changes: 55 additions & 0 deletions tests/gas/PositionManagers.Operations.gas.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,28 @@ contract AllowancePositionManager_Gas_Tests is SpokeBase {
vm.snapshotGasLastCall(NAMESPACE, 'withdrawOnBehalfOf: full');
}

/// forge-config: default.isolate = false
function test_withdrawOnBehalfOf_WithTemporaryWithdrawAllowance() public {
uint256 amount = 100e18;

vm.prank(alice);
positionManager.temporaryApproveWithdraw(bob, _daiReserveId(spoke1), UINT256_MAX);

Utils.supply(spoke1, _daiReserveId(spoke1), alice, mintAmount_DAI, alice);
Utils.withdraw(spoke1, _daiReserveId(spoke1), alice, amount, alice);

vm.prank(bob);
positionManager.withdrawOnBehalfOf(_daiReserveId(spoke1), amount, alice);
vm.snapshotGasLastCall(NAMESPACE, 'withdrawOnBehalfOf: partial (with temporary allowance)');

vm.prank(alice);
positionManager.temporaryApproveWithdraw(bob, _daiReserveId(spoke1), UINT256_MAX);

vm.prank(bob);
positionManager.withdrawOnBehalfOf(_daiReserveId(spoke1), UINT256_MAX, alice);
vm.snapshotGasLastCall(NAMESPACE, 'withdrawOnBehalfOf: full (with temporary allowance)');
}

function test_borrowOnBehalfOf() public {
uint256 aliceSupplyAmount = 5000e18;
uint256 bobSupplyAmount = 1000e18;
Expand All @@ -151,6 +173,23 @@ contract AllowancePositionManager_Gas_Tests is SpokeBase {
vm.snapshotGasLastCall(NAMESPACE, 'borrowOnBehalfOf');
}

/// forge-config: default.isolate = false
function test_borrowOnBehalfOf_WithTemporaryDelegateCredit() public {
uint256 aliceSupplyAmount = 5000e18;
uint256 bobSupplyAmount = 1000e18;
uint256 borrowAmount = 750e18;

vm.prank(alice);
positionManager.temporaryDelegateCredit(bob, _daiReserveId(spoke1), borrowAmount);

Utils.supplyCollateral(spoke1, _daiReserveId(spoke1), alice, aliceSupplyAmount, alice);
Utils.supplyCollateral(spoke1, _daiReserveId(spoke1), bob, bobSupplyAmount, bob);

vm.prank(bob);
positionManager.borrowOnBehalfOf(_daiReserveId(spoke1), borrowAmount, alice);
vm.snapshotGasLastCall(NAMESPACE, 'borrowOnBehalfOf (with temporary allowance)');
}

function test_approveWithdraw() public {
uint256 amount = 100e18;

Expand Down Expand Up @@ -182,6 +221,14 @@ contract AllowancePositionManager_Gas_Tests is SpokeBase {
vm.snapshotGasLastCall(NAMESPACE, 'approveWithdrawWithSig');
}

function test_temporaryApproveWithdraw() public {
uint256 amount = 100e18;

vm.prank(alice);
positionManager.temporaryApproveWithdraw(bob, _daiReserveId(spoke1), amount);
vm.snapshotGasLastCall(NAMESPACE, 'temporaryApproveWithdraw');
}

function test_renounceWithdrawAllowance() public {
uint256 amount = 100e18;

Expand Down Expand Up @@ -224,6 +271,14 @@ contract AllowancePositionManager_Gas_Tests is SpokeBase {
vm.snapshotGasLastCall(NAMESPACE, 'delegateCreditWithSig');
}

function test_temporaryDelegateCredit() public {
uint256 amount = 100e18;

vm.prank(alice);
positionManager.temporaryDelegateCredit(bob, _daiReserveId(spoke1), amount);
vm.snapshotGasLastCall(NAMESPACE, 'temporaryDelegateCredit');
}

function test_renounceCreditDelegation() public {
uint256 amount = 100e18;

Expand Down
28 changes: 28 additions & 0 deletions tests/mocks/AllowancePositionManagerWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2025 Aave Labs
pragma solidity ^0.8.0;

import {TransientSlot} from 'src/dependencies/openzeppelin/TransientSlot.sol';
import {AllowancePositionManager} from 'src/position-manager/AllowancePositionManager.sol';

contract AllowancePositionManagerWrapper is AllowancePositionManager {
using TransientSlot for *;

constructor(address spoke_) AllowancePositionManager(spoke_) {}

function temporaryWithdrawAllowance(
address owner,
address spender,
uint256 reserveId
) external view returns (uint256) {
return _temporaryWithdrawAllowancesSlot(owner, spender, reserveId).tload();
}

function temporaryDelegateCredit(
address owner,
address spender,
uint256 reserveId
) external view returns (uint256) {
return _temporaryDelegateCreditsSlot(owner, spender, reserveId).tload();
}
}
Loading
Loading