Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
18 changes: 18 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,22 @@ library EIP712Types {
uint256 nonce;
uint256 deadline;
}

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

struct CreditDelegation {
address owner;
address spender;
uint256 reserveId;
uint256 amount;
uint256 nonce;
uint256 deadline;
}
}
101 changes: 101 additions & 0 deletions src/position-manager/CreditDelegationPositionManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// 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 {ISpoke} from 'src/spoke/interfaces/ISpoke.sol';
import {PositionManagerBase} from 'src/position-manager/PositionManagerBase.sol';
import {ICreditDelegationPositionManager} from 'src/position-manager/interfaces/ICreditDelegationPositionManager.sol';

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

/// @notice Mapping of credit delegations.
mapping(address owner => mapping(address spender => mapping(uint256 reserveId => uint256 amount)))
private _creditDelegations;

/// @dev Constructor.
/// @param spoke_ The address of the spoke contract.
constructor(address spoke_) PositionManagerBase(spoke_) {}

/// @inheritdoc ICreditDelegationPositionManager
function approveCreditDelegation(address spender, uint256 reserveId, uint256 amount) external {
_creditDelegations[msg.sender][spender][reserveId] = amount;
emit CreditDelegation(msg.sender, spender, reserveId, amount);
}

/// @inheritdoc ICreditDelegationPositionManager
function approveCreditDelegationWithSig(
Copy link
Contributor

Choose a reason for hiding this comment

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

should we have something like PositionManagerWithAllowanceBase.sol, which implements the map and the 2 functions for approvals (with and without sig)? (to avoid duplication)

wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For the signature part, I'd rather keep each unique typehash & type, rather than a common one used for both. As for the mapping, no real opinion there, the current duplication does not bother me.

Copy link
Contributor

Choose a reason for hiding this comment

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

ofc, still unique typehash, but we can pass typehash to the base

EIP712Types.CreditDelegation calldata params,
bytes calldata signature
) external {
require(block.timestamp <= params.deadline, InvalidSignature());
address user = params.owner;
bytes32 digest = _hashTypedData(params.hash());
require(SignatureChecker.isValidSignatureNow(user, digest, signature), InvalidSignature());
_useCheckedNonce(user, params.nonce);

_creditDelegations[user][params.spender][params.reserveId] = params.amount;
emit CreditDelegation(user, params.spender, params.reserveId, params.amount);
}

/// @inheritdoc ICreditDelegationPositionManager
function borrowOnBehalfOf(
uint256 reserveId,
uint256 amount,
address onBehalfOf
) external returns (uint256, uint256) {
require(amount > 0, InvalidAmount());
uint256 currentAllowance = _creditDelegations[onBehalfOf][msg.sender][reserveId];
require(currentAllowance >= amount, InsufficientCreditDelegation(currentAllowance, amount));
_creditDelegations[onBehalfOf][msg.sender][reserveId] = currentAllowance.uncheckedSub(amount);

IERC20 asset = _getReserveUnderlying(reserveId);
(uint256 borrowedShares, uint256 borrowedAmount) = ISpoke(SPOKE).borrow(
reserveId,
amount,
onBehalfOf
);
asset.safeTransfer(msg.sender, borrowedAmount);

return (borrowedShares, borrowedAmount);
}

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

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

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

function _domainNameAndVersion() internal pure override returns (string memory, string memory) {
return ('CreditDelegationPositionManager', '1');
}
}
71 changes: 71 additions & 0 deletions src/position-manager/PositionManagerBase.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2025 Aave Labs
pragma solidity 0.8.28;

import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol';
import {IERC20Permit} from 'src/dependencies/openzeppelin/IERC20Permit.sol';
import {Multicall} from 'src/utils/Multicall.sol';
import {EIP712Types} from 'src/libraries/types/EIP712Types.sol';
import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol';
import {IPositionManagerBase} from 'src/position-manager/interfaces/IPositionManagerBase.sol';

/// @title PositionManagerBase
/// @author Aave Labs
/// @notice Base implementation for position manager common functionalities.
abstract contract PositionManagerBase is IPositionManagerBase, Multicall {
/// @inheritdoc IPositionManagerBase
address public immutable override SPOKE;

/// @dev Constructor.
/// @param spoke_ The address of the spoke contract.
constructor(address spoke_) {
require(spoke_ != address(0), InvalidAddress());
SPOKE = spoke_;
}

/// @inheritdoc IPositionManagerBase
function setSelfAsUserPositionManagerWithSig(
EIP712Types.SetUserPositionManager calldata params,
bytes calldata signature
) external {
try
ISpoke(SPOKE).setUserPositionManagerWithSig(
address(this),
params.user,
params.approve,
params.nonce,
params.deadline,
signature
)
{} catch {}
}

/// @inheritdoc IPositionManagerBase
function permitReserve(
uint256 reserveId,
address onBehalfOf,
uint256 value,
uint256 deadline,
uint8 permitV,
bytes32 permitR,
bytes32 permitS
) external {
address underlying = address(_getReserveUnderlying(reserveId));
try
IERC20Permit(underlying).permit({
owner: onBehalfOf,
spender: address(this),
value: value,
deadline: deadline,
v: permitV,
r: permitR,
s: permitS
})
{} catch {}
}

/// @return The underlying asset for `reserveId` on the Spoke.
function _getReserveUnderlying(uint256 reserveId) internal view returns (IERC20) {
return IERC20(ISpoke(SPOKE).getReserve(reserveId).underlying);
}
}
49 changes: 49 additions & 0 deletions src/position-manager/SupplyRepayPositionManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2025 Aave Labs
pragma solidity 0.8.28;

import {SafeERC20, IERC20} from 'src/dependencies/openzeppelin/SafeERC20.sol';
import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol';
import {PositionManagerBase} from 'src/position-manager/PositionManagerBase.sol';
import {ISupplyRepayPositionManager} from 'src/position-manager/interfaces/ISupplyRepayPositionManager.sol';

/// @title SupplyRepayPositionManager
/// @author Aave Labs
/// @notice Position manager to handle supply and repay actions on behalf of users.
contract SupplyRepayPositionManager is ISupplyRepayPositionManager, PositionManagerBase {
Copy link
Member

Choose a reason for hiding this comment

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

wondering if we should find another name for this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

DonationPositionManager ?

using SafeERC20 for IERC20;

/// @dev Constructor.
/// @param spoke_ The address of the spoke contract.
constructor(address spoke_) PositionManagerBase(spoke_) {}

/// @inheritdoc ISupplyRepayPositionManager
function supplyOnBehalfOf(
Copy link
Member

Choose a reason for hiding this comment

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

don't we want to have the intents-based version of these actions?

uint256 reserveId,
uint256 amount,
address onBehalfOf
) external returns (uint256, uint256) {
require(amount > 0, InvalidAmount());
IERC20 asset = _getReserveUnderlying(reserveId);
asset.safeTransferFrom(msg.sender, address(this), amount);
asset.forceApprove(SPOKE, amount);
return ISpoke(SPOKE).supply(reserveId, amount, onBehalfOf);
Copy link
Contributor

Choose a reason for hiding this comment

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

given that the spoke returns both shares and amounts, I'm wondering whether we should assert (with a require) that amount returned is equal to amount. wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This function is just an extension of the Spoke method, so I think returning the amount & shares here should be sufficient so integrators can then verify the returned values.

}

/// @inheritdoc ISupplyRepayPositionManager
function repayOnBehalfOf(
uint256 reserveId,
uint256 amount,
address onBehalfOf
) external returns (uint256, uint256) {
require(amount > 0, InvalidAmount());
IERC20 asset = _getReserveUnderlying(reserveId);

uint256 userTotalDebt = ISpoke(SPOKE).getUserTotalDebt(reserveId, onBehalfOf);
uint256 repayAmount = amount > userTotalDebt ? userTotalDebt : amount;
Copy link
Member

Choose a reason for hiding this comment

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

do we care about the attack vector of borrowing more before they max repay kicks in? We d need to disallow max repay here.

Copy link
Contributor

@avniculae avniculae Dec 9, 2025

Choose a reason for hiding this comment

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

or check amount is not more than x% more than userTotalDebt? would it be an overkill?

or maybe take the following inputs (reserveId, amount, maxShares, onBehalfOf). we then revert if shares returned by spoke.repay are > maxShares. would this add too much friction?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it brings a lot of complexity or would block some users behaviors rather than solving the potential issue.
Imo it's up to user decision to approve+pass a large/larger than needed repayAmount, which would signify they are ready to repay up to that inputed amount.
In here, it's not like we simply override the amount to repay all debt if the inputed amount is higher than the debt of onBehalfOf, so we already have the parameter limiting how much the user is ready to repay at maximum.

Copy link
Member

Choose a reason for hiding this comment

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

I think blocking max_uint is a way to block unintentional max repayments, so using a fixed amount means user knows what is doing, while using max_uint is naive call by a noob user thus it's blocked to protect them

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But then just passing max_uint - 1 would serve the same usecase and skip the blocker, noob user might see it as "inputed the wrong value" and execute the same exact mistake with just - 1.


asset.safeTransferFrom(msg.sender, address(this), repayAmount);
asset.forceApprove(SPOKE, repayAmount);
return ISpoke(SPOKE).repay(reserveId, repayAmount, onBehalfOf);
}
}
101 changes: 101 additions & 0 deletions src/position-manager/WithdrawPermitPositionManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// 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 {ISpoke} from 'src/spoke/interfaces/ISpoke.sol';
import {PositionManagerBase} from 'src/position-manager/PositionManagerBase.sol';
import {IWithdrawPermitPositionManager} from 'src/position-manager/interfaces/IWithdrawPermitPositionManager.sol';

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

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

/// @dev Constructor.
/// @param spoke_ The address of the spoke contract.
constructor(address spoke_) PositionManagerBase(spoke_) {}

/// @inheritdoc IWithdrawPermitPositionManager
function approveWithdraw(address spender, uint256 reserveId, uint256 amount) external {
_withdrawAllowances[msg.sender][spender][reserveId] = amount;
emit WithdrawApproval(msg.sender, spender, reserveId, amount);
}

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

_withdrawAllowances[user][params.spender][params.reserveId] = params.amount;
emit WithdrawApproval(user, params.spender, params.reserveId, params.amount);
}

/// @inheritdoc IWithdrawPermitPositionManager
function withdrawOnBehalfOf(
uint256 reserveId,
uint256 amount,
address onBehalfOf
) external returns (uint256, uint256) {
require(amount > 0, InvalidAmount());
uint256 currentAllowance = _withdrawAllowances[onBehalfOf][msg.sender][reserveId];
require(currentAllowance >= amount, InsufficientWithdrawAllowance(currentAllowance, amount));
_withdrawAllowances[onBehalfOf][msg.sender][reserveId] = currentAllowance.uncheckedSub(amount);

IERC20 asset = _getReserveUnderlying(reserveId);
(uint256 withdrawnShares, uint256 withdrawnAmount) = ISpoke(SPOKE).withdraw(
reserveId,
amount,
onBehalfOf
);
asset.safeTransfer(msg.sender, withdrawnAmount);

return (withdrawnShares, withdrawnAmount);
}

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

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

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

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