Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a00ec35
feat : new position managers
Kogaroshi Dec 8, 2025
ec2170c
fix : address pr comments & missing natspec & start testing
Kogaroshi Dec 9, 2025
7ae1fdb
fix : address pr comments & SupplyRepay posm tests
Kogaroshi Dec 9, 2025
0ffdb57
tests : WithdrawPermit posm tests
Kogaroshi Dec 9, 2025
abcf269
tests : CreditDelegation posm tests
Kogaroshi Dec 9, 2025
a42b632
fix : address pr comments & gas snapshot tests
Kogaroshi Dec 10, 2025
a1e33c0
frt : merge posms into Allowance Posm
Kogaroshi Dec 10, 2025
aee822d
fix : add renounce on allowance posm
Kogaroshi Dec 10, 2025
f33c26d
feat : new PositionConfig posm + tests & reorder posm test files
Kogaroshi Dec 11, 2025
ffa90f0
fix : address pr comments
Kogaroshi Dec 12, 2025
480e518
fix : missing gas tests + small fix
Kogaroshi Dec 12, 2025
8dbe9c0
chore: set self as posm consistency
DhairyaSethi Dec 15, 2025
68cb0d4
fix: add missing tests
DhairyaSethi Dec 15, 2025
6f718ba
chore: improve natspec
DhairyaSethi Dec 15, 2025
bc4a75c
pull from chore/sig-gateway-setself
Kogaroshi Dec 15, 2025
bf6476f
fix : apply convention to Posm Base
Kogaroshi Dec 15, 2025
883990b
fix : address pr comments
Kogaroshi Dec 16, 2025
19443f7
fix : address pr comments
Kogaroshi Dec 17, 2025
1063e3f
fix : address pr comments
Kogaroshi Dec 18, 2025
2e28199
fix : address pr comments
Kogaroshi Dec 19, 2025
f0477f1
fix : address pr comments
Kogaroshi Dec 19, 2025
c922404
rft : make Posm Spoke-agnostic & merge GatewayBase & PosmBase
Kogaroshi Dec 19, 2025
21bce1d
feat : add infinite allowances
Kogaroshi Dec 19, 2025
85105ec
Pull from dev and fix conflicts
Kogaroshi Jan 6, 2026
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: 11 additions & 0 deletions snapshots/AllowancePositionManager.Operations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"approveWithdraw": "49895",
"approveWithdrawWithSig": "66560",
"borrowOnBehalfOf": "309723",
"delegateCredit": "49864",
"delegateCreditWithSig": "66505",
"renounceCreditDelegation": "28020",
"renounceWithdrawAllowance": "28007",
"withdrawOnBehalfOf: full": "121329",
"withdrawOnBehalfOf: partial": "131461"
}
12 changes: 6 additions & 6 deletions snapshots/NativeTokenGateway.Operations.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"borrowNative": "229604",
"repayNative": "168312",
"supplyAsCollateralNative": "160373",
"supplyNative": "136476",
"withdrawNative: full": "125620",
"withdrawNative: partial": "136825"
"borrowNative": "229684",
"repayNative": "168370",
"supplyAsCollateralNative": "160453",
"supplyNative": "136523",
"withdrawNative: full": "125667",
"withdrawNative: partial": "136883"
}
13 changes: 13 additions & 0 deletions snapshots/PositionConfigPositionManager.Operations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"renounceGlobalPermission": "27699",
"renounceUserDynamicConfigPermission": "27763",
"renounceUserRiskPremiumPermission": "27720",
"renounceUsingAsCollateralPermission": "27698",
"setGlobalPermission": "49868",
"setUserDynamicConfigPermission": "49845",
"setUserRiskPremiumPermission": "49891",
"setUsingAsCollateralOnBehalfOf": "71916",
"setUsingAsCollateralPermission": "49847",
"updateUserDynamicConfigOnBehalfOf": "49577",
"updateUserRiskPremiumOnBehalfOf": "130940"
}
3 changes: 3 additions & 0 deletions snapshots/PositionManagerBase.Operations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"setSelfAsUserPositionManagerWithSig": "74809"
}
12 changes: 6 additions & 6 deletions snapshots/SignatureGateway.Operations.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"borrowWithSig": "215893",
"repayWithSig": "189160",
"setSelfAsUserPositionManagerWithSig": "74858",
"setUsingAsCollateralWithSig": "85053",
"supplyWithSig": "153205",
"updateUserDynamicConfigWithSig": "62769",
"updateUserRiskPremiumWithSig": "61579",
"withdrawWithSig": "131713"
"setSelfAsUserPositionManagerWithSig": "74880",
"setUsingAsCollateralWithSig": "85075",
"supplyWithSig": "153223",
"updateUserDynamicConfigWithSig": "62791",
"updateUserRiskPremiumWithSig": "61513",
"withdrawWithSig": "131731"
}
4 changes: 4 additions & 0 deletions snapshots/SupplyRepayPositionManager.Operations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"repayOnBehalfOf": "170151",
"supplyOnBehalfOf": "138004"
}
20 changes: 20 additions & 0 deletions src/libraries/types/EIP712Types.sol
Copy link
Contributor

@yan-man yan-man Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we still need SetUserPositionManager struct here? seems like theyre only used in the tests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no preference, could keep for future usage, or just move to tests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think if not used in src prefer to move it to test, perhaps TestTypes

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it will be used in src, on the spoke

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm where is this struct used? not seeing it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setUserPositionManagerWithSig

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

method isn't consistent w the rest

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually spoke field is annoying, we'll see. but this struct doesn't belong in tests dir for sure

Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,24 @@ library EIP712Types {
uint256 nonce;
uint256 deadline;
}

struct WithdrawPermit {
address spoke;
uint256 reserveId;
address owner;
address spender;
uint256 amount;
uint256 nonce;
uint256 deadline;
}

struct CreditDelegation {
address spoke;
uint256 reserveId;
address owner;
address spender;
uint256 amount;
uint256 nonce;
uint256 deadline;
}
}
290 changes: 290 additions & 0 deletions src/position-manager/AllowancePositionManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2025 Aave Labs
pragma solidity 0.8.28;

import {SignatureChecker} from 'src/dependencies/openzeppelin/SignatureChecker.sol';
import {SafeERC20, IERC20} from 'src/dependencies/openzeppelin/SafeERC20.sol';
import {EIP712} from 'src/dependencies/solady/EIP712.sol';
import {MathUtils} from 'src/libraries/math/MathUtils.sol';
import {NoncesKeyed} from 'src/utils/NoncesKeyed.sol';
import {EIP712Hash, EIP712Types} from 'src/position-manager/libraries/EIP712Hash.sol';
import {PositionManagerBase} from 'src/position-manager/PositionManagerBase.sol';
import {ISpokeBase} from 'src/spoke/interfaces/ISpokeBase.sol';
import {IAllowancePositionManager} from 'src/position-manager/interfaces/IAllowancePositionManager.sol';

/// @title AllowancePositionManager
/// @author Aave Labs
/// @notice Position manager to handle withdraw permit, credit delegation and borrow actions on behalf of users.
contract AllowancePositionManager is
IAllowancePositionManager,
PositionManagerBase,
NoncesKeyed,
EIP712
{
using SafeERC20 for IERC20;
using MathUtils for uint256;
using EIP712Hash for *;

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

mapping(address spoke => mapping(uint256 reserveId => mapping(address owner => mapping(address spender => uint256 amount))))
private _creditDelegations;
Copy link
Member

@miguelmtzinf miguelmtzinf Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dn't be opposed to use similar terms (e.g. _borrowAllowances) for credit delegation in general... But fine to keep it as it is if "giving borrow allowance" sounds "weird" and industry is used to "delegating credit"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, kept creditDelegation to reduce the misunderstandings, as it was already accepted by users with v3.


/// @dev Constructor.
/// @param initialOwner_ The address of the initial owner.
constructor(address initialOwner_) PositionManagerBase(initialOwner_) {}

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

/// @inheritdoc IAllowancePositionManager
function approveWithdrawWithSig(
EIP712Types.WithdrawPermit calldata params,
bytes calldata signature
) external onlyRegisteredSpoke(params.spoke) {
require(block.timestamp <= params.deadline, InvalidSignature());
bytes32 digest = _hashTypedData(params.hash());
require(
SignatureChecker.isValidSignatureNow(params.owner, digest, signature),
InvalidSignature()
);
_useCheckedNonce(params.owner, params.nonce);

_updateWithdrawAllowance({
spoke: params.spoke,
reserveId: params.reserveId,
owner: params.owner,
spender: params.spender,
newAllowance: params.amount
});
}

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

/// @inheritdoc IAllowancePositionManager
function delegateCreditWithSig(
EIP712Types.CreditDelegation calldata params,
bytes calldata signature
) external onlyRegisteredSpoke(params.spoke) {
require(block.timestamp <= params.deadline, InvalidSignature());
bytes32 digest = _hashTypedData(params.hash());
require(
SignatureChecker.isValidSignatureNow(params.owner, digest, signature),
InvalidSignature()
);
_useCheckedNonce(params.owner, params.nonce);

_updateCreditDelegation({
spoke: params.spoke,
reserveId: params.reserveId,
owner: params.owner,
spender: params.spender,
newCreditDelegation: params.amount
});
}

/// @inheritdoc IAllowancePositionManager
function renounceWithdrawAllowance(
address spoke,
uint256 reserveId,
address owner
) external onlyRegisteredSpoke(spoke) {
if (_withdrawAllowances[spoke][reserveId][owner][msg.sender] == 0) {
return;
}
_updateWithdrawAllowance({
spoke: spoke,
reserveId: reserveId,
owner: owner,
spender: msg.sender,
newAllowance: 0
});
}

/// @inheritdoc IAllowancePositionManager
function renounceCreditDelegation(
address spoke,
uint256 reserveId,
address owner
) external onlyRegisteredSpoke(spoke) {
if (_creditDelegations[spoke][reserveId][owner][msg.sender] == 0) {
return;
}
_updateCreditDelegation({
spoke: spoke,
reserveId: reserveId,
owner: owner,
spender: msg.sender,
newCreditDelegation: 0
});
}

/// @inheritdoc IAllowancePositionManager
function withdrawOnBehalfOf(
address spoke,
uint256 reserveId,
uint256 amount,
address onBehalfOf
) external onlyRegisteredSpoke(spoke) returns (uint256, uint256) {
IERC20 asset = IERC20(_getReserveUnderlying(spoke, reserveId));
_spendWithdrawAllowance({
spoke: spoke,
reserveId: reserveId,
owner: onBehalfOf,
spender: msg.sender,
amount: amount
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about max withdraw, which could be a bit confusing / hard to achieve...
It's only possible if the max_uint allowance is given, and will be consume in one go....

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

max withdraw can be executed by passing an absurdly high amount, that is not uint256_max, in which case the full allowance wouldn't be fully consumed (since Spoke only checks the passed against max withdrawable, uint256_max is not used as the forced value to make a max withdraw).
And in the case the owner gives uint256_max allowance and the spender also inputs uint256_max, then we both clear the position balance + the given allowance, which makes sense for me there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

21bce1d for non-decreasing infinite allowance.

});

(uint256 withdrawnShares, uint256 withdrawnAmount) = ISpokeBase(spoke).withdraw(
reserveId,
amount,
onBehalfOf
);
asset.safeTransfer(msg.sender, withdrawnAmount);

return (withdrawnShares, withdrawnAmount);
}

/// @inheritdoc IAllowancePositionManager
function borrowOnBehalfOf(
address spoke,
uint256 reserveId,
uint256 amount,
address onBehalfOf
) external onlyRegisteredSpoke(spoke) returns (uint256, uint256) {
IERC20 asset = IERC20(_getReserveUnderlying(spoke, reserveId));
_spendCreditDelegation({
spoke: spoke,
reserveId: reserveId,
owner: onBehalfOf,
spender: msg.sender,
amount: amount
});

(uint256 borrowedShares, uint256 borrowedAmount) = ISpokeBase(spoke).borrow(
reserveId,
amount,
onBehalfOf
);
asset.safeTransfer(msg.sender, borrowedAmount);

return (borrowedShares, borrowedAmount);
}

/// @inheritdoc IAllowancePositionManager
function withdrawAllowance(
address spoke,
uint256 reserveId,
address owner,
address spender
) external view returns (uint256) {
return _withdrawAllowances[spoke][reserveId][owner][spender];
}

/// @inheritdoc IAllowancePositionManager
function creditDelegation(
address spoke,
uint256 reserveId,
address owner,
address spender
) external view returns (uint256) {
return _creditDelegations[spoke][reserveId][owner][spender];
}

/// @inheritdoc IAllowancePositionManager
function DOMAIN_SEPARATOR() external view returns (bytes32) {
return _domainSeparator();
}

/// @inheritdoc IAllowancePositionManager
function WITHDRAW_PERMIT_TYPEHASH() external pure returns (bytes32) {
return EIP712Hash.WITHDRAW_PERMIT_TYPEHASH;
}

/// @inheritdoc IAllowancePositionManager
function CREDIT_DELEGATION_TYPEHASH() external pure returns (bytes32) {
return EIP712Hash.CREDIT_DELEGATION_TYPEHASH;
}

function _updateWithdrawAllowance(
address spoke,
uint256 reserveId,
address owner,
address spender,
uint256 newAllowance
) internal {
_withdrawAllowances[spoke][reserveId][owner][spender] = newAllowance;
emit WithdrawApproval(spoke, reserveId, owner, spender, newAllowance);
}

function _updateCreditDelegation(
address spoke,
uint256 reserveId,
address owner,
address spender,
uint256 newCreditDelegation
) internal {
_creditDelegations[spoke][reserveId][owner][spender] = newCreditDelegation;
emit CreditDelegation(spoke, reserveId, owner, spender, newCreditDelegation);
}

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);
}
}

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);
}
}

function _domainNameAndVersion() internal pure override returns (string memory, string memory) {
return ('AllowancePositionManager', '1');
}
}
Loading
Loading