Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
11 changes: 8 additions & 3 deletions snapshots/AllowancePositionManager.Operations.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
{
"approveWithdraw": "49895",
"approveWithdrawWithSig": "66560",
"borrowOnBehalfOf": "309435",
"borrowOnBehalfOf": "310092",
"borrowOnBehalfOf (with temporary allowance)": "247979",
"delegateCredit": "49864",
"delegateCreditWithSig": "66505",
"renounceCreditDelegation": "28020",
"renounceWithdrawAllowance": "28007",
"withdrawOnBehalfOf: full": "121329",
"withdrawOnBehalfOf: partial": "131461"
"temporaryApproveWithdraw": "25552",
"temporaryDelegateCredit": "25528",
"withdrawOnBehalfOf: full": "121837",
"withdrawOnBehalfOf: full (with temporary allowance)": "56154",
"withdrawOnBehalfOf: partial": "132096",
"withdrawOnBehalfOf: partial (with temporary allowance)": "66954"
}
134 changes: 126 additions & 8 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 @@ -23,8 +25,20 @@ contract AllowancePositionManager is
{
using SafeERC20 for IERC20;
using MathUtils for uint256;
using SlotDerivation for bytes32;
using TransientSlot for *;
using EIP712Hash 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;

mapping(address spoke => mapping(uint256 reserveId => mapping(address owner => mapping(address spender => uint256 amount))))
private _withdrawAllowances;

Expand Down Expand Up @@ -73,6 +87,21 @@ contract AllowancePositionManager is
});
}

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

/// @inheritdoc IAllowancePositionManager
function delegateCredit(
address spoke,
Expand Down Expand Up @@ -111,6 +140,21 @@ contract AllowancePositionManager is
});
}

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

/// @inheritdoc IAllowancePositionManager
function renounceWithdrawAllowance(
address spoke,
Expand Down Expand Up @@ -256,34 +300,108 @@ contract AllowancePositionManager is
emit CreditDelegation(spoke, reserveId, owner, spender, newCreditDelegation);
}

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

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

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

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

function _domainNameAndVersion() internal pure override returns (string memory, string memory) {
return ('AllowancePositionManager', '1');
}
Expand Down
38 changes: 36 additions & 2 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 spoke The address of the spoke.
Expand Down Expand Up @@ -62,6 +66,20 @@ 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 spoke The address of the spoke.
/// @param reserveId The identifier of the reserve.
/// @param spender The address of the spender to receive the allowance.
/// @param amount The amount of allowance.
function temporaryApproveWithdraw(
address spoke,
uint256 reserveId,
address spender,
uint256 amount
) external;

/// @notice Approves a credit delegation allowance for a spender.
/// @param spoke The address of the spoke.
/// @param reserveId The identifier of the reserve.
Expand All @@ -82,6 +100,20 @@ 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 spoke The address of the spoke.
/// @param reserveId The identifier of the reserve.
/// @param spender The address of the spender to receive the allowance.
/// @param amount The amount of allowance.
function temporaryDelegateCredit(
address spoke,
uint256 reserveId,
address spender,
uint256 amount
) external;

/// @notice Renounces the withdraw allowance given by the owner.
/// @param spoke The address of the spoke.
/// @param reserveId The identifier of the reserve.
Expand All @@ -95,7 +127,8 @@ interface IAllowancePositionManager is IPositionManagerBase {
function renounceCreditDelegation(address spoke, uint256 reserveId, address owner) external;

/// @notice Executes a withdraw on behalf of a user.
/// @dev The caller must have sufficient withdraw allowance from onBehalfOf.
/// @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 spoke The address of the spoke.
/// @param reserveId The identifier of the reserve.
Expand All @@ -111,7 +144,8 @@ interface IAllowancePositionManager is IPositionManagerBase {
) external returns (uint256, uint256);

/// @notice Executes a borrow on behalf of a user.
/// @dev The caller must have sufficient credit delegation allowance from onBehalfOf.
/// @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 spoke The address of the spoke.
/// @param reserveId The identifier of the reserve.
Expand Down
70 changes: 70 additions & 0 deletions tests/gas/PositionManagers.Operations.gas.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,38 @@ 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(
address(spoke1),
_daiReserveId(spoke1),
bob,
UINT256_MAX
);

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

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

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

vm.prank(bob);
positionManager.withdrawOnBehalfOf(address(spoke1), _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 @@ -158,6 +190,28 @@ 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(
address(spoke1),
_daiReserveId(spoke1),
bob,
borrowAmount
);

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

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

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

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

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

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

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

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

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

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

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

Expand Down
Loading
Loading