Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
78 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
d6a31a3
fix : pull latest from dev + fix conflicts + gas snapshots
Kogaroshi Jan 14, 2026
d26b6f3
fix : use correct internal method
Kogaroshi Jan 14, 2026
87a0da0
Merge branch 'dev' of https://github.com/aave/aave-v4 into feat/new-p…
Kogaroshi Jan 14, 2026
38a39b0
chore : lint fix
Kogaroshi Jan 14, 2026
728fc5c
pull latest from dev
Kogaroshi Jan 15, 2026
c69e8ca
pull latest from dev
Kogaroshi Jan 16, 2026
1922ab3
fix : pull latest & fix conflict with setSelfAsUserPositionManagerWit…
Kogaroshi Jan 20, 2026
87cefba
Merge branch 'dev' of https://github.com/aave/aave-v4 into feat/new-p…
Kogaroshi Jan 20, 2026
7375b26
pull latest from dev
Kogaroshi Jan 21, 2026
bd27684
pull latest from dev
Kogaroshi Jan 23, 2026
950ddd8
chore : gas snapshots
Kogaroshi Jan 23, 2026
0bc1956
fix : fix Allowance Posm gas tests inconsistent
Kogaroshi Jan 23, 2026
c381ae6
pull latest from dev
Kogaroshi Jan 27, 2026
74e9556
pull latest from dev
Kogaroshi Jan 27, 2026
7754e70
pull latest from dev
Kogaroshi Jan 27, 2026
9aae00d
pull latest from dev
Kogaroshi Jan 30, 2026
7f976b2
pull latest from dev
Kogaroshi Jan 30, 2026
2f81479
chore : gas snapshots
Kogaroshi Jan 30, 2026
d783743
pull latest from main
Kogaroshi Feb 2, 2026
dc2b55d
pull latest from dev
Kogaroshi Feb 6, 2026
5c98657
fix : address pr comments
Kogaroshi Feb 6, 2026
2b4e475
fix : better Natspec
Kogaroshi Feb 6, 2026
db9220a
rft : use flags for multicall and registerSpoke
Kogaroshi Feb 6, 2026
6922f0f
fix : nameing and Natspec
Kogaroshi Feb 6, 2026
ea0d782
fix : use correct helper function to update allowances
Kogaroshi Feb 9, 2026
bb87690
pull latest from main
Kogaroshi Feb 9, 2026
a7bb6e4
fix : split EIP712Hash tests depending on locations
Kogaroshi Feb 9, 2026
8030c43
fix : AllowancePositionManager fixes & renaming credit delegation met…
Kogaroshi Feb 9, 2026
a5bc542
fix : remove registerSpoke flag & address pr comments
Kogaroshi Feb 9, 2026
c054b75
rft : block uint256_max repay on behalf
Kogaroshi Feb 9, 2026
c1d7dd0
chore : rename Config Posm
Kogaroshi Feb 9, 2026
2b18a65
fix : move errors
Kogaroshi Feb 10, 2026
2b747a2
chore : pull latest from main & fix conflicts & move TokenizedSpoke E…
Kogaroshi Feb 10, 2026
ceaf94f
pull latest from main
Kogaroshi Feb 10, 2026
2a52053
fix : invalid Natspec
Kogaroshi Feb 10, 2026
62d4edd
fix : address pr comments
Kogaroshi Feb 10, 2026
2f291a4
fix : address pr comments
Kogaroshi Feb 10, 2026
f06ebf0
fix : typo
Kogaroshi Feb 10, 2026
3285a8d
fix : address pr comments & rft config posm
Kogaroshi Feb 10, 2026
96368d1
fix : remove old gas snapshot
Kogaroshi Feb 10, 2026
56ac15f
fix : address pr comments
Kogaroshi Feb 19, 2026
a977805
Merge branch 'main' of https://github.com/aave/aave-v4 into feat/new-…
Kogaroshi Feb 19, 2026
ad40ee5
chore : rename Position Managers
Kogaroshi Feb 19, 2026
f557b01
tests : more multicall tests in posm
Kogaroshi Feb 19, 2026
b48de13
fix : use named args
Kogaroshi Feb 19, 2026
ffbbffd
fix : address pr comments
Kogaroshi Feb 20, 2026
03391a1
fix : correct multicall atomicity test
Kogaroshi Feb 20, 2026
64dd44c
fix : address pr comments
Kogaroshi Feb 20, 2026
0549c68
fix : reorder internal functions
Kogaroshi Feb 22, 2026
9b6cc7d
fix : address pr comments
Kogaroshi Feb 23, 2026
5e31b1d
fix : address pr comments
Kogaroshi Feb 23, 2026
c5881ef
fix : fix Natpsec
Kogaroshi Feb 23, 2026
2bf96b3
fix : small natspec change
Kogaroshi Feb 24, 2026
96ebca5
fix : change natspec
Kogaroshi Feb 24, 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
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;
}
}
91 changes: 91 additions & 0 deletions src/position-manager/CreditDelegationPositionManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// 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 {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(
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) = 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 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');
}
}
73 changes: 73 additions & 0 deletions src/position-manager/PositionManagerBase.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2025 Aave Labs
pragma solidity 0.8.28;

import {SafeERC20, IERC20} from 'src/dependencies/openzeppelin/SafeERC20.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 {
using SafeERC20 for IERC20;

/// @inheritdoc IPositionManagerBase
ISpoke public immutable override SPOKE;

/// @dev Constructor.
/// @param spoke_ The address of the spoke contract.
constructor(address spoke_) {
require(spoke_ != address(0), InvalidAddress());
SPOKE = ISpoke(spoke_);
}
Comment on lines +41 to +45
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

require(condition, CustomError()) is not a valid require overload in Solidity (it only accepts an optional string reason). These lines will not compile as written. Replace these with explicit if (!condition) revert ...; (and similarly for InvalidAddress() / UnsupportedAction()).

Copilot uses AI. Check for mistakes.

/// @inheritdoc IPositionManagerBase
function setSelfAsUserPositionManagerWithSig(
EIP712Types.SetUserPositionManager calldata params,
bytes calldata signature
) external {
try
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
Comment on lines +81 to +83
Copy link
Member

Choose a reason for hiding this comment

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

ok so we ensure underlying cannot be zero bc spoke is registered but maybe we rm modifier to allow through anyspoke (doesnt really matter anyway) save us the sload and do underlying not zero instead

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(SPOKE.getReserve(reserveId).underlying);
}
}
48 changes: 48 additions & 0 deletions src/position-manager/SupplyRepayPositionManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2025 Aave Labs
pragma solidity 0.8.28;

import {SafeERC20, IERC20} from 'src/dependencies/openzeppelin/SafeERC20.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 {
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(address(SPOKE), amount);
return SPOKE.supply(reserveId, amount, onBehalfOf);
}

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

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

asset.safeTransferFrom(msg.sender, address(this), repayAmount);
asset.forceApprove(address(SPOKE), repayAmount);
return SPOKE.repay(reserveId, repayAmount, onBehalfOf);
}
}
95 changes: 95 additions & 0 deletions src/position-manager/WithdrawPermitPositionManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// 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 {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) = 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 WITHDRAW_PERMIT_TYPEHASH() external pure returns (bytes32) {
return EIP712Hash.WITHDRAW_TYPEHASH;
}

function _domainNameAndVersion() internal pure override returns (string memory, string memory) {
return ('WithdrawPermitPositionManager', '1');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2025 Aave Labs
pragma solidity ^0.8.0;

import {EIP712Types} from 'src/libraries/types/EIP712Types.sol';
import {IPositionManagerBase} from 'src/position-manager/interfaces/IPositionManagerBase.sol';

/// @title ICreditDelegationPositionManager
/// @author Aave Labs
/// @notice Interface for position manager handling credit delegation actions.
interface ICreditDelegationPositionManager is IPositionManagerBase {
/// @notice Thrown when the credit delegation allowance is insufficient.
error InsufficientCreditDelegation(uint256 allowance, uint256 required);

/// @notice Emitted when a credit delegation is given.
event CreditDelegation(
address indexed owner,
address indexed spender,
uint256 indexed reserveId,
uint256 amount
);

/// @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.
/// @param amount The amount of allowance.
function approveCreditDelegation(address spender, uint256 reserveId, uint256 amount) external;

/// @notice Approves a credit delegation allowance for a spender via signature.
/// @param params The structured CreditDelegation parameters.
/// @param signature The signed bytes for the intent.
function approveCreditDelegationWithSig(
EIP712Types.CreditDelegation calldata params,
bytes calldata signature
) external;

/// @notice Executes a borrow on behalf of a user.
/// @dev The caller must have sufficient credit delegation allowance from `onBehalfOf`.
/// @dev The caller receives the borrowed assets.
/// @param reserveId The identifier of the reserve.
/// @param amount The amount to borrow.
/// @param onBehalfOf The address of the user to borrow on behalf of.
/// @return The amount of shares borrowed.
/// @return The amount of assets borrowed.
function borrowOnBehalfOf(
uint256 reserveId,
uint256 amount,
address onBehalfOf
) external returns (uint256, uint256);

/// @notice Returns the credit delegation allowance for a spender on behalf of an owner.
/// @param owner The address of the owner.
/// @param spender The address of the spender.
/// @param reserveId The identifier of the reserve.
/// @return The amount of credit delegation allowance.
function creditDelegationAllowance(
address owner,
address spender,
uint256 reserveId
) external view returns (uint256);

/// @notice Returns the type hash for the CreditDelegation intent.
function CREDIT_DELEGATION_TYPEHASH() external view returns (bytes32);
}
Loading
Loading