diff --git a/snapshots/AllowancePositionManager.Operations.json b/snapshots/AllowancePositionManager.Operations.json new file mode 100644 index 000000000..9da4327ef --- /dev/null +++ b/snapshots/AllowancePositionManager.Operations.json @@ -0,0 +1,11 @@ +{ + "approveBorrow": "49802", + "approveBorrowWithSig": "65684", + "approveWithdraw": "49833", + "approveWithdrawWithSig": "65660", + "borrowOnBehalfOf": "310430", + "renounceBorrowAllowance": "27924", + "renounceWithdrawAllowance": "28000", + "withdrawOnBehalfOf: full": "120697", + "withdrawOnBehalfOf: partial": "130671" +} \ No newline at end of file diff --git a/snapshots/ConfigPositionManager.Operations.json b/snapshots/ConfigPositionManager.Operations.json new file mode 100644 index 000000000..0985e4e8a --- /dev/null +++ b/snapshots/ConfigPositionManager.Operations.json @@ -0,0 +1,13 @@ +{ + "renounceCanUpdateUserDynamicConfigPermission": "27659", + "renounceCanUpdateUserRiskPremiumPermission": "27681", + "renounceCanUpdateUsingAsCollateralPermission": "27682", + "renounceGlobalPermission": "27646", + "setCanUpdateUserDynamicConfigPermission": "49787", + "setCanUpdateUserRiskPremiumPermission": "49809", + "setCanUpdateUsingAsCollateralPermission": "49809", + "setGlobalPermission": "49761", + "setUsingAsCollateralOnBehalfOf": "72503", + "updateUserDynamicConfigOnBehalfOf": "50433", + "updateUserRiskPremiumOnBehalfOf": "130201" +} \ No newline at end of file diff --git a/snapshots/GiverPositionManager.Operations.json b/snapshots/GiverPositionManager.Operations.json new file mode 100644 index 000000000..8418f18ba --- /dev/null +++ b/snapshots/GiverPositionManager.Operations.json @@ -0,0 +1,4 @@ +{ + "repayOnBehalfOf": "167927", + "supplyOnBehalfOf": "136911" +} \ No newline at end of file diff --git a/snapshots/NativeTokenGateway.Operations.json b/snapshots/NativeTokenGateway.Operations.json index 1593c113b..1dc9db854 100644 --- a/snapshots/NativeTokenGateway.Operations.json +++ b/snapshots/NativeTokenGateway.Operations.json @@ -1,8 +1,8 @@ { - "borrowNative": "228557", - "repayNative": "166460", - "supplyAsCollateralNative": "160122", - "supplyNative": "135753", - "withdrawNative: full": "125548", - "withdrawNative: partial": "136735" + "borrowNative": "228618", + "repayNative": "166499", + "supplyAsCollateralNative": "160183", + "supplyNative": "135784", + "withdrawNative: full": "125598", + "withdrawNative: partial": "136797" } \ No newline at end of file diff --git a/snapshots/PositionManagerBase.Operations.json b/snapshots/PositionManagerBase.Operations.json new file mode 100644 index 000000000..c89ee7b0c --- /dev/null +++ b/snapshots/PositionManagerBase.Operations.json @@ -0,0 +1,3 @@ +{ + "setSelfAsUserPositionManagerWithSig": "75007" +} \ No newline at end of file diff --git a/snapshots/SignatureGateway.Operations.json b/snapshots/SignatureGateway.Operations.json index f16cfa12f..833d78ba9 100644 --- a/snapshots/SignatureGateway.Operations.json +++ b/snapshots/SignatureGateway.Operations.json @@ -1,10 +1,10 @@ { - "borrowWithSig": "213790", - "repayWithSig": "186732", - "setSelfAsUserPositionManagerWithSig": "75118", - "setUsingAsCollateralWithSig": "85387", - "supplyWithSig": "151985", - "updateUserDynamicConfigWithSig": "63120", - "updateUserRiskPremiumWithSig": "62090", - "withdrawWithSig": "130803" + "borrowWithSig": "213761", + "repayWithSig": "186703", + "setSelfAsUserPositionManagerWithSig": "75126", + "setUsingAsCollateralWithSig": "85380", + "supplyWithSig": "151980", + "updateUserDynamicConfigWithSig": "63113", + "updateUserRiskPremiumWithSig": "61995", + "withdrawWithSig": "130797" } \ No newline at end of file diff --git a/snapshots/SupplyRepayPositionManager.Operations.json b/snapshots/SupplyRepayPositionManager.Operations.json new file mode 100644 index 000000000..8418f18ba --- /dev/null +++ b/snapshots/SupplyRepayPositionManager.Operations.json @@ -0,0 +1,4 @@ +{ + "repayOnBehalfOf": "167927", + "supplyOnBehalfOf": "136911" +} \ No newline at end of file diff --git a/snapshots/TakerPositionManager.Operations.json b/snapshots/TakerPositionManager.Operations.json new file mode 100644 index 000000000..9da4327ef --- /dev/null +++ b/snapshots/TakerPositionManager.Operations.json @@ -0,0 +1,11 @@ +{ + "approveBorrow": "49802", + "approveBorrowWithSig": "65684", + "approveWithdraw": "49833", + "approveWithdrawWithSig": "65660", + "borrowOnBehalfOf": "310430", + "renounceBorrowAllowance": "27924", + "renounceWithdrawAllowance": "28000", + "withdrawOnBehalfOf: full": "120697", + "withdrawOnBehalfOf: partial": "130671" +} \ No newline at end of file diff --git a/src/position-manager/ConfigPositionManager.sol b/src/position-manager/ConfigPositionManager.sol new file mode 100644 index 000000000..d2c5734ec --- /dev/null +++ b/src/position-manager/ConfigPositionManager.sol @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity 0.8.28; + +import {ConfigPermissionsMap} from 'src/position-manager/libraries/ConfigPermissionsMap.sol'; +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import { + IConfigPositionManager, + ConfigPermissions +} from 'src/position-manager/interfaces/IConfigPositionManager.sol'; +import {PositionManagerBase} from 'src/position-manager/PositionManagerBase.sol'; + +/// @title ConfigPositionManager +/// @author Aave Labs +/// @notice Position manager to handle position configuration actions on behalf of users. +contract ConfigPositionManager is IConfigPositionManager, PositionManagerBase { + using ConfigPermissionsMap for ConfigPermissions; + + /// @dev Map of configuration permissions based on the spoke, delegator and delegatee. + mapping(address spoke => mapping(address delegator => mapping(address delegatee => ConfigPermissions))) + private _config; + + /// @dev Constructor. + /// @param initialOwner_ The address of the initial owner. + constructor(address initialOwner_) PositionManagerBase(initialOwner_) {} + + /// @inheritdoc IConfigPositionManager + function setGlobalPermission( + address spoke, + address delegatee, + bool permission + ) external onlyRegisteredSpoke(spoke) { + ConfigPermissions newPermissions = ConfigPermissionsMap.setFullPermissions(permission); + _updatePermissions({ + spoke: spoke, + delegator: msg.sender, + delegatee: delegatee, + oldPermissions: _config[spoke][msg.sender][delegatee], + newPermissions: newPermissions + }); + } + + /// @inheritdoc IConfigPositionManager + function setCanUpdateUsingAsCollateralPermission( + address spoke, + address delegatee, + bool permission + ) external onlyRegisteredSpoke(spoke) { + ConfigPermissions oldPermissions = _config[spoke][msg.sender][delegatee]; + ConfigPermissions newPermissions = oldPermissions.setCanSetUsingAsCollateral(permission); + _updatePermissions({ + spoke: spoke, + delegator: msg.sender, + delegatee: delegatee, + oldPermissions: oldPermissions, + newPermissions: newPermissions + }); + } + + /// @inheritdoc IConfigPositionManager + function setCanUpdateUserRiskPremiumPermission( + address spoke, + address delegatee, + bool permission + ) external onlyRegisteredSpoke(spoke) { + ConfigPermissions oldPermissions = _config[spoke][msg.sender][delegatee]; + ConfigPermissions newPermissions = oldPermissions.setCanUpdateUserRiskPremium(permission); + _updatePermissions({ + spoke: spoke, + delegator: msg.sender, + delegatee: delegatee, + oldPermissions: oldPermissions, + newPermissions: newPermissions + }); + } + + /// @inheritdoc IConfigPositionManager + function setCanUpdateUserDynamicConfigPermission( + address spoke, + address delegatee, + bool permission + ) external onlyRegisteredSpoke(spoke) { + ConfigPermissions oldPermissions = _config[spoke][msg.sender][delegatee]; + ConfigPermissions newPermissions = oldPermissions.setCanUpdateUserDynamicConfig(permission); + _updatePermissions({ + spoke: spoke, + delegator: msg.sender, + delegatee: delegatee, + oldPermissions: oldPermissions, + newPermissions: newPermissions + }); + } + + /// @inheritdoc IConfigPositionManager + function renounceGlobalPermission( + address spoke, + address delegator + ) external onlyRegisteredSpoke(spoke) { + ConfigPermissions newPermissions = ConfigPermissionsMap.setFullPermissions(false); + _updatePermissions({ + spoke: spoke, + delegator: delegator, + delegatee: msg.sender, + oldPermissions: _config[spoke][delegator][msg.sender], + newPermissions: newPermissions + }); + } + + /// @inheritdoc IConfigPositionManager + function renounceCanUpdateUsingAsCollateralPermission( + address spoke, + address delegator + ) external onlyRegisteredSpoke(spoke) { + ConfigPermissions oldPermissions = _config[spoke][delegator][msg.sender]; + ConfigPermissions newPermissions = oldPermissions.setCanSetUsingAsCollateral(false); + _updatePermissions({ + spoke: spoke, + delegator: delegator, + delegatee: msg.sender, + oldPermissions: oldPermissions, + newPermissions: newPermissions + }); + } + + /// @inheritdoc IConfigPositionManager + function renounceCanUpdateUserRiskPremiumPermission( + address spoke, + address delegator + ) external onlyRegisteredSpoke(spoke) { + ConfigPermissions oldPermissions = _config[spoke][delegator][msg.sender]; + ConfigPermissions newPermissions = oldPermissions.setCanUpdateUserRiskPremium(false); + _updatePermissions({ + spoke: spoke, + delegator: delegator, + delegatee: msg.sender, + oldPermissions: oldPermissions, + newPermissions: newPermissions + }); + } + + /// @inheritdoc IConfigPositionManager + function renounceCanUpdateUserDynamicConfigPermission( + address spoke, + address delegator + ) external onlyRegisteredSpoke(spoke) { + ConfigPermissions oldPermissions = _config[spoke][delegator][msg.sender]; + ConfigPermissions newPermissions = oldPermissions.setCanUpdateUserDynamicConfig(false); + _updatePermissions({ + spoke: spoke, + delegator: delegator, + delegatee: msg.sender, + oldPermissions: oldPermissions, + newPermissions: newPermissions + }); + } + + /// @inheritdoc IConfigPositionManager + function setUsingAsCollateralOnBehalfOf( + address spoke, + uint256 reserveId, + bool usingAsCollateral, + address onBehalfOf + ) external onlyRegisteredSpoke(spoke) { + require( + _config[spoke][onBehalfOf][msg.sender].canSetUsingAsCollateral(), + DelegateeNotAllowed() + ); + + ISpoke(spoke).setUsingAsCollateral(reserveId, usingAsCollateral, onBehalfOf); + } + + /// @inheritdoc IConfigPositionManager + function updateUserRiskPremiumOnBehalfOf( + address spoke, + address onBehalfOf + ) external onlyRegisteredSpoke(spoke) { + require( + _config[spoke][onBehalfOf][msg.sender].canUpdateUserRiskPremium(), + DelegateeNotAllowed() + ); + + ISpoke(spoke).updateUserRiskPremium(onBehalfOf); + } + + /// @inheritdoc IConfigPositionManager + function updateUserDynamicConfigOnBehalfOf( + address spoke, + address onBehalfOf + ) external onlyRegisteredSpoke(spoke) { + require( + _config[spoke][onBehalfOf][msg.sender].canUpdateUserDynamicConfig(), + DelegateeNotAllowed() + ); + + ISpoke(spoke).updateUserDynamicConfig(onBehalfOf); + } + + /// @inheritdoc IConfigPositionManager + function getConfigPermissions( + address spoke, + address delegatee, + address onBehalfOf + ) external view returns (ConfigPermissionValues memory) { + ConfigPermissions permissions = _config[spoke][onBehalfOf][delegatee]; + return + ConfigPermissionValues({ + canSetUsingAsCollateral: permissions.canSetUsingAsCollateral(), + canUpdateUserRiskPremium: permissions.canUpdateUserRiskPremium(), + canUpdateUserDynamicConfig: permissions.canUpdateUserDynamicConfig() + }); + } + + /// @dev Does not update if the new permissions are equal to the old permissions. + function _updatePermissions( + address spoke, + address delegator, + address delegatee, + ConfigPermissions oldPermissions, + ConfigPermissions newPermissions + ) internal { + if (oldPermissions.eq(newPermissions)) { + return; + } + _config[spoke][delegator][delegatee] = newPermissions; + emit ConfigPermissionsUpdated(spoke, delegator, delegatee, newPermissions); + } + + function _multicallEnabled() internal pure override returns (bool) { + return true; + } + + function _domainNameAndVersion() internal pure override returns (string memory, string memory) { + return ('ConfigPositionManager', '1'); + } +} diff --git a/src/position-manager/GatewayBase.sol b/src/position-manager/GatewayBase.sol deleted file mode 100644 index 3c081667d..000000000 --- a/src/position-manager/GatewayBase.sol +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Copyright (c) 2025 Aave Labs -pragma solidity 0.8.28; - -import {Ownable2Step, Ownable} from 'src/dependencies/openzeppelin/Ownable2Step.sol'; -import {Rescuable} from 'src/utils/Rescuable.sol'; -import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; -import {IGatewayBase} from 'src/position-manager/interfaces/IGatewayBase.sol'; - -/// @title GatewayBase -/// @author Aave Labs -/// @notice Base implementation for gateway common functionalities. -abstract contract GatewayBase is IGatewayBase, Rescuable, Ownable2Step { - mapping(address => bool) internal _registeredSpokes; - - /// @notice Modifier that checks if the specified spoke is registered. - modifier onlyRegisteredSpoke(address spoke) { - _isSpokeValid(spoke); - _; - } - - /// @dev Constructor. - /// @param initialOwner_ The address of the initial owner. - constructor(address initialOwner_) Ownable(initialOwner_) {} - - /// @inheritdoc IGatewayBase - function registerSpoke(address spoke, bool active) external onlyOwner { - require(spoke != address(0), InvalidAddress()); - _registeredSpokes[spoke] = active; - emit SpokeRegistered(spoke, active); - } - - /// @inheritdoc IGatewayBase - function renouncePositionManagerRole(address spoke, address user) external onlyOwner { - require(user != address(0), InvalidAddress()); - ISpoke(spoke).renouncePositionManagerRole(user); - } - - /// @inheritdoc IGatewayBase - function isSpokeRegistered(address spoke) external view returns (bool) { - return _registeredSpokes[spoke]; - } - - /// @dev Verifies the specified spoke is registered. - function _isSpokeValid(address spoke) internal view { - require(_registeredSpokes[spoke], SpokeNotRegistered()); - } - - /// @return The underlying asset for `reserveId` on the specified spoke. - function _getReserveUnderlying(address spoke, uint256 reserveId) internal view returns (address) { - return ISpoke(spoke).getReserve(reserveId).underlying; - } - - /// @dev The `owner()` is the allowed caller for Rescuable methods. - function _rescueGuardian() internal view override returns (address) { - return owner(); - } -} diff --git a/src/position-manager/GiverPositionManager.sol b/src/position-manager/GiverPositionManager.sol new file mode 100644 index 000000000..9c55c4b4d --- /dev/null +++ b/src/position-manager/GiverPositionManager.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity 0.8.28; + +import {SafeERC20, IERC20} from 'src/dependencies/openzeppelin/SafeERC20.sol'; +import {ISpokeBase} from 'src/spoke/interfaces/ISpokeBase.sol'; +import {IGiverPositionManager} from 'src/position-manager/interfaces/IGiverPositionManager.sol'; +import {PositionManagerBase} from 'src/position-manager/PositionManagerBase.sol'; + +/// @title GiverPositionManager +/// @author Aave Labs +/// @notice Position manager to handle supply and repay actions on behalf of users. +contract GiverPositionManager is IGiverPositionManager, PositionManagerBase { + using SafeERC20 for IERC20; + + /// @dev Constructor. + /// @param initialOwner_ The address of the initial owner. + constructor(address initialOwner_) PositionManagerBase(initialOwner_) {} + + /// @inheritdoc IGiverPositionManager + function supplyOnBehalfOf( + address spoke, + uint256 reserveId, + uint256 amount, + address onBehalfOf + ) external onlyRegisteredSpoke(spoke) returns (uint256, uint256) { + IERC20 underlying = IERC20(_getReserveUnderlying(spoke, reserveId)); + underlying.safeTransferFrom(msg.sender, address(this), amount); + underlying.forceApprove(spoke, amount); + return ISpokeBase(spoke).supply(reserveId, amount, onBehalfOf); + } + + /// @inheritdoc IGiverPositionManager + function repayOnBehalfOf( + address spoke, + uint256 reserveId, + uint256 amount, + address onBehalfOf + ) external onlyRegisteredSpoke(spoke) returns (uint256, uint256) { + require(amount != type(uint256).max, NoMaxUintRepayOnBehalfOfAllowed()); + IERC20 underlying = IERC20(_getReserveUnderlying(spoke, reserveId)); + + uint256 userTotalDebt = ISpokeBase(spoke).getUserTotalDebt(reserveId, onBehalfOf); + uint256 repayAmount = amount > userTotalDebt ? userTotalDebt : amount; + + underlying.safeTransferFrom(msg.sender, address(this), repayAmount); + underlying.forceApprove(spoke, repayAmount); + return ISpokeBase(spoke).repay(reserveId, repayAmount, onBehalfOf); + } + + function _multicallEnabled() internal pure override returns (bool) { + return true; + } + + function _domainNameAndVersion() internal pure override returns (string memory, string memory) { + return ('GiverPositionManager', '1'); + } +} diff --git a/src/position-manager/NativeTokenGateway.sol b/src/position-manager/NativeTokenGateway.sol index 8e3c047b3..7f3bbdcee 100644 --- a/src/position-manager/NativeTokenGateway.sol +++ b/src/position-manager/NativeTokenGateway.sol @@ -5,31 +5,34 @@ pragma solidity 0.8.28; import {ReentrancyGuardTransient} from 'src/dependencies/openzeppelin/ReentrancyGuardTransient.sol'; import {Address} from 'src/dependencies/openzeppelin/Address.sol'; import {SafeERC20, IERC20} from 'src/dependencies/openzeppelin/SafeERC20.sol'; -import {GatewayBase} from 'src/position-manager/GatewayBase.sol'; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; import {INativeWrapper} from 'src/position-manager/interfaces/INativeWrapper.sol'; import {INativeTokenGateway} from 'src/position-manager/interfaces/INativeTokenGateway.sol'; +import {PositionManagerBase} from 'src/position-manager/PositionManagerBase.sol'; /// @title NativeTokenGateway /// @author Aave Labs /// @notice Gateway to interact with a spoke using the native coin of a chain. -/// @dev Contract must be an active & approved user position manager in order to execute spoke actions on a user's behalf. -contract NativeTokenGateway is INativeTokenGateway, GatewayBase, ReentrancyGuardTransient { +contract NativeTokenGateway is INativeTokenGateway, PositionManagerBase, ReentrancyGuardTransient { using SafeERC20 for IERC20; - address public immutable NATIVE_WRAPPER; + /// @inheritdoc INativeTokenGateway + address public immutable NATIVE_TOKEN_WRAPPER; /// @dev Constructor. - /// @param nativeWrapper_ The address of the native wrapper contract. + /// @param nativeTokenWrapper_ The address of the native token wrapper contract. /// @param initialOwner_ The address of the initial owner. - constructor(address nativeWrapper_, address initialOwner_) GatewayBase(initialOwner_) { - require(nativeWrapper_ != address(0), InvalidAddress()); - NATIVE_WRAPPER = nativeWrapper_; + constructor( + address nativeTokenWrapper_, + address initialOwner_ + ) PositionManagerBase(initialOwner_) { + require(nativeTokenWrapper_ != address(0), InvalidAddress()); + NATIVE_TOKEN_WRAPPER = nativeTokenWrapper_; } /// @dev Checks only 'nativeWrapper' can transfer native tokens. receive() external payable { - require(msg.sender == NATIVE_WRAPPER, UnsupportedAction()); + require(msg.sender == NATIVE_TOKEN_WRAPPER, UnsupportedAction()); } /// @dev Unsupported fallback function. @@ -79,7 +82,7 @@ contract NativeTokenGateway is INativeTokenGateway, GatewayBase, ReentrancyGuard amount, msg.sender ); - INativeWrapper(NATIVE_WRAPPER).withdraw(withdrawnAmount); + INativeWrapper(NATIVE_TOKEN_WRAPPER).withdraw(withdrawnAmount); Address.sendValue(payable(msg.sender), withdrawnAmount); return (withdrawnShares, withdrawnAmount); @@ -99,7 +102,7 @@ contract NativeTokenGateway is INativeTokenGateway, GatewayBase, ReentrancyGuard amount, msg.sender ); - INativeWrapper(NATIVE_WRAPPER).withdraw(borrowedAmount); + INativeWrapper(NATIVE_TOKEN_WRAPPER).withdraw(borrowedAmount); Address.sendValue(payable(msg.sender), borrowedAmount); return (borrowedShares, borrowedAmount); @@ -123,8 +126,8 @@ contract NativeTokenGateway is INativeTokenGateway, GatewayBase, ReentrancyGuard repayAmount = userTotalDebt; } - INativeWrapper(NATIVE_WRAPPER).deposit{value: repayAmount}(); - IERC20(NATIVE_WRAPPER).forceApprove(spoke, repayAmount); + INativeWrapper(NATIVE_TOKEN_WRAPPER).deposit{value: repayAmount}(); + IERC20(NATIVE_TOKEN_WRAPPER).forceApprove(spoke, repayAmount); (uint256 repaidShares, uint256 repaidAmount) = ISpoke(spoke).repay( reserveId, repayAmount, @@ -148,13 +151,22 @@ contract NativeTokenGateway is INativeTokenGateway, GatewayBase, ReentrancyGuard address underlying = _getReserveUnderlying(spoke, reserveId); _validateParams(underlying, amount); - INativeWrapper(NATIVE_WRAPPER).deposit{value: amount}(); - IERC20(NATIVE_WRAPPER).forceApprove(spoke, amount); + INativeWrapper(NATIVE_TOKEN_WRAPPER).deposit{value: amount}(); + IERC20(NATIVE_TOKEN_WRAPPER).forceApprove(spoke, amount); return ISpoke(spoke).supply(reserveId, amount, user); } function _validateParams(address underlying, uint256 amount) internal view { - require(NATIVE_WRAPPER == underlying, NotNativeWrappedAsset()); + require(NATIVE_TOKEN_WRAPPER == underlying, NotNativeWrappedAsset()); require(amount > 0, InvalidAmount()); } + + /// @dev Multicall is disabled to prevent msg.value reuse across delegatecalls. + function _multicallEnabled() internal pure override returns (bool) { + return false; + } + + function _domainNameAndVersion() internal pure override returns (string memory, string memory) { + return ('NativeTokenGateway', '1'); + } } diff --git a/src/position-manager/PositionManagerBase.sol b/src/position-manager/PositionManagerBase.sol new file mode 100644 index 000000000..3a319431c --- /dev/null +++ b/src/position-manager/PositionManagerBase.sol @@ -0,0 +1,129 @@ +// 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 {Ownable2Step, Ownable} from 'src/dependencies/openzeppelin/Ownable2Step.sol'; +import {IntentConsumer} from 'src/utils/IntentConsumer.sol'; +import {IMulticall, Multicall} from 'src/utils/Multicall.sol'; +import {Rescuable} from 'src/utils/Rescuable.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. +/// @dev Contract must be an active & approved user position manager in order to execute spoke actions on a user's behalf. +/// @dev The `_multicallEnabled()` function must be implemented to specify whether multicall is enabled. +abstract contract PositionManagerBase is + IPositionManagerBase, + Ownable2Step, + IntentConsumer, + Rescuable, + Multicall +{ + /// @dev Map of registered spokes. + mapping(address => bool) internal _registeredSpokes; + + /// @notice Modifier that checks if the specified spoke is registered. + modifier onlyRegisteredSpoke(address spoke) { + require(_isSpokeRegistered(spoke), SpokeNotRegistered()); + _; + } + + /// @dev Constructor. + /// @param initialOwner_ The address of the initial owner. + constructor(address initialOwner_) Ownable(initialOwner_) {} + + /// @inheritdoc IPositionManagerBase + function registerSpoke(address spoke, bool registered) external onlyOwner { + require(spoke != address(0), InvalidAddress()); + _registeredSpokes[spoke] = registered; + emit SpokeRegistered(spoke, registered); + } + + /// @inheritdoc IPositionManagerBase + function setSelfAsUserPositionManagerWithSig( + address spoke, + address onBehalfOf, + bool approve, + uint256 nonce, + uint256 deadline, + bytes calldata signature + ) external onlyRegisteredSpoke(spoke) { + ISpoke.PositionManagerUpdate[] memory updates = new ISpoke.PositionManagerUpdate[](1); + updates[0] = ISpoke.PositionManagerUpdate({positionManager: address(this), approve: approve}); + try + ISpoke(spoke).setUserPositionManagersWithSig( + ISpoke.SetUserPositionManagers({ + onBehalfOf: onBehalfOf, + updates: updates, + nonce: nonce, + deadline: deadline + }), + signature + ) + {} catch {} + } + + /// @inheritdoc IPositionManagerBase + function permitReserveUnderlying( + address spoke, + uint256 reserveId, + address onBehalfOf, + uint256 value, + uint256 deadline, + uint8 permitV, + bytes32 permitR, + bytes32 permitS + ) external onlyRegisteredSpoke(spoke) { + address underlying = _getReserveUnderlying(spoke, reserveId); + try + IERC20Permit(underlying).permit({ + owner: onBehalfOf, + spender: address(this), + value: value, + deadline: deadline, + v: permitV, + r: permitR, + s: permitS + }) + {} catch {} + } + + /// @inheritdoc IPositionManagerBase + function renouncePositionManagerRole(address spoke, address user) external onlyOwner { + ISpoke(spoke).renouncePositionManagerRole(user); + } + + /// @inheritdoc IMulticall + function multicall( + bytes[] calldata data + ) public override(Multicall, IMulticall) returns (bytes[] memory) { + require(_multicallEnabled(), UnsupportedAction()); + return super.multicall(data); + } + + /// @inheritdoc IPositionManagerBase + function isSpokeRegistered(address spoke) external view returns (bool) { + return _isSpokeRegistered(spoke); + } + + /// @dev Verifies the specified spoke is registered. + function _isSpokeRegistered(address spoke) internal view returns (bool) { + return _registeredSpokes[spoke]; + } + + /// @return The underlying asset for `reserveId` on the specified spoke. + function _getReserveUnderlying(address spoke, uint256 reserveId) internal view returns (address) { + return ISpoke(spoke).getReserve(reserveId).underlying; + } + + /// @dev Flag to enable multicall usage. Needs to be set by the inheriting contracts. + function _multicallEnabled() internal pure virtual returns (bool); + + function _rescueGuardian() internal view override returns (address) { + return owner(); + } +} diff --git a/src/position-manager/SignatureGateway.sol b/src/position-manager/SignatureGateway.sol index 216ec28f8..426dee4f3 100644 --- a/src/position-manager/SignatureGateway.sol +++ b/src/position-manager/SignatureGateway.sol @@ -3,22 +3,19 @@ pragma solidity 0.8.28; import {SafeERC20, IERC20} from 'src/dependencies/openzeppelin/SafeERC20.sol'; -import {IERC20Permit} from 'src/dependencies/openzeppelin/IERC20Permit.sol'; import {EIP712Hash} from 'src/position-manager/libraries/EIP712Hash.sol'; import {MathUtils} from 'src/libraries/math/MathUtils.sol'; -import {GatewayBase} from 'src/position-manager/GatewayBase.sol'; -import {IntentConsumer} from 'src/utils/IntentConsumer.sol'; -import {Multicall} from 'src/utils/Multicall.sol'; +import {EIP712Hash} from 'src/position-manager/libraries/EIP712Hash.sol'; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; import {ISignatureGateway} from 'src/position-manager/interfaces/ISignatureGateway.sol'; +import {PositionManagerBase} from 'src/position-manager/PositionManagerBase.sol'; /// @title SignatureGateway /// @author Aave Labs /// @notice Gateway to consume EIP-712 typed intents for spoke actions on behalf of a user. -/// @dev Contract must be an active & approved user position manager to execute spoke actions on user's behalf. /// @dev Uses keyed-nonces where each key's namespace nonce is consumed sequentially. Intents bundled through /// multicall can be executed independently in order of signed nonce & deadline; does not guarantee batch atomicity. -contract SignatureGateway is ISignatureGateway, GatewayBase, IntentConsumer, Multicall { +contract SignatureGateway is ISignatureGateway, PositionManagerBase { using SafeERC20 for IERC20; using EIP712Hash for *; @@ -48,7 +45,7 @@ contract SignatureGateway is ISignatureGateway, GatewayBase, IntentConsumer, Mul /// @dev Constructor. /// @param initialOwner_ The address of the initial owner. - constructor(address initialOwner_) GatewayBase(initialOwner_) {} + constructor(address initialOwner_) PositionManagerBase(initialOwner_) {} /// @inheritdoc ISignatureGateway function supplyWithSig( @@ -204,53 +201,8 @@ contract SignatureGateway is ISignatureGateway, GatewayBase, IntentConsumer, Mul ISpoke(params.spoke).updateUserDynamicConfig(params.onBehalfOf); } - /// @inheritdoc ISignatureGateway - function setSelfAsUserPositionManagerWithSig( - address spoke, - address onBehalfOf, - bool approve, - uint256 nonce, - uint256 deadline, - bytes calldata signature - ) external onlyRegisteredSpoke(spoke) { - ISpoke.PositionManagerUpdate[] memory updates = new ISpoke.PositionManagerUpdate[](1); - updates[0] = ISpoke.PositionManagerUpdate({positionManager: address(this), approve: approve}); - try - ISpoke(spoke).setUserPositionManagersWithSig( - ISpoke.SetUserPositionManagers({ - onBehalfOf: onBehalfOf, - updates: updates, - nonce: nonce, - deadline: deadline - }), - signature - ) - {} catch {} - } - - /// @inheritdoc ISignatureGateway - function permitReserve( - address spoke, - uint256 reserveId, - address onBehalfOf, - uint256 value, - uint256 deadline, - uint8 permitV, - bytes32 permitR, - bytes32 permitS - ) external onlyRegisteredSpoke(spoke) { - address underlying = _getReserveUnderlying(spoke, reserveId); - try - IERC20Permit(underlying).permit({ - owner: onBehalfOf, - spender: address(this), - value: value, - deadline: deadline, - v: permitV, - r: permitR, - s: permitS - }) - {} catch {} + function _multicallEnabled() internal pure override returns (bool) { + return true; } function _domainNameAndVersion() internal pure override returns (string memory, string memory) { diff --git a/src/position-manager/TakerPositionManager.sol b/src/position-manager/TakerPositionManager.sol new file mode 100644 index 000000000..910c879c1 --- /dev/null +++ b/src/position-manager/TakerPositionManager.sol @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity 0.8.28; + +import {SafeERC20, IERC20} from 'src/dependencies/openzeppelin/SafeERC20.sol'; +import {MathUtils} from 'src/libraries/math/MathUtils.sol'; +import {EIP712Hash} from 'src/position-manager/libraries/EIP712Hash.sol'; +import {ISpokeBase} from 'src/spoke/interfaces/ISpokeBase.sol'; +import {ITakerPositionManager} from 'src/position-manager/interfaces/ITakerPositionManager.sol'; +import {PositionManagerBase} from 'src/position-manager/PositionManagerBase.sol'; + +/// @title TakerPositionManager +/// @author Aave Labs +/// @notice Position manager to handle withdraw permit and borrow permit actions on behalf of users. +contract TakerPositionManager is ITakerPositionManager, PositionManagerBase { + using SafeERC20 for IERC20; + using MathUtils for uint256; + using EIP712Hash for *; + + /// @inheritdoc ITakerPositionManager + bytes32 public constant WITHDRAW_PERMIT_TYPEHASH = EIP712Hash.WITHDRAW_PERMIT_TYPEHASH; + + /// @inheritdoc ITakerPositionManager + bytes32 public constant BORROW_PERMIT_TYPEHASH = EIP712Hash.BORROW_PERMIT_TYPEHASH; + + /// @dev Map of withdraw allowances based on the spoke, reserveId, owner and spender. + mapping(address spoke => mapping(uint256 reserveId => mapping(address owner => mapping(address spender => uint256 amount)))) + private _withdrawAllowances; + + /// @dev Map of borrow allowances based on the spoke, reserveId, owner and spender. + mapping(address spoke => mapping(uint256 reserveId => mapping(address owner => mapping(address spender => uint256 amount)))) + private _borrowAllowances; + + /// @dev Constructor. + /// @param initialOwner_ The address of the initial owner. + constructor(address initialOwner_) PositionManagerBase(initialOwner_) {} + + /// @inheritdoc ITakerPositionManager + 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 ITakerPositionManager + function approveWithdrawWithSig( + WithdrawPermit calldata params, + bytes calldata signature + ) external onlyRegisteredSpoke(params.spoke) { + _verifyAndConsumeIntent({ + signer: params.owner, + intentHash: params.hash(), + nonce: params.nonce, + deadline: params.deadline, + signature: signature + }); + + _updateWithdrawAllowance({ + spoke: params.spoke, + reserveId: params.reserveId, + owner: params.owner, + spender: params.spender, + newAllowance: params.amount + }); + } + + /// @inheritdoc ITakerPositionManager + function approveBorrow( + address spoke, + uint256 reserveId, + address spender, + uint256 amount + ) external onlyRegisteredSpoke(spoke) { + _updateBorrowAllowance({ + spoke: spoke, + reserveId: reserveId, + owner: msg.sender, + spender: spender, + newCreditDelegation: amount + }); + } + + /// @inheritdoc ITakerPositionManager + function approveBorrowWithSig( + BorrowPermit calldata params, + bytes calldata signature + ) external onlyRegisteredSpoke(params.spoke) { + _verifyAndConsumeIntent({ + signer: params.owner, + intentHash: params.hash(), + nonce: params.nonce, + deadline: params.deadline, + signature: signature + }); + + _updateBorrowAllowance({ + spoke: params.spoke, + reserveId: params.reserveId, + owner: params.owner, + spender: params.spender, + newCreditDelegation: params.amount + }); + } + + /// @inheritdoc ITakerPositionManager + 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 ITakerPositionManager + function renounceBorrowAllowance( + address spoke, + uint256 reserveId, + address owner + ) external onlyRegisteredSpoke(spoke) { + if (_borrowAllowances[spoke][reserveId][owner][msg.sender] == 0) { + return; + } + _updateBorrowAllowance({ + spoke: spoke, + reserveId: reserveId, + owner: owner, + spender: msg.sender, + newCreditDelegation: 0 + }); + } + + /// @inheritdoc ITakerPositionManager + 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 + }); + + (uint256 withdrawnShares, uint256 withdrawnAmount) = ISpokeBase(spoke).withdraw( + reserveId, + amount, + onBehalfOf + ); + asset.safeTransfer(msg.sender, withdrawnAmount); + + return (withdrawnShares, withdrawnAmount); + } + + /// @inheritdoc ITakerPositionManager + function borrowOnBehalfOf( + address spoke, + uint256 reserveId, + uint256 amount, + address onBehalfOf + ) external onlyRegisteredSpoke(spoke) returns (uint256, uint256) { + IERC20 asset = IERC20(_getReserveUnderlying(spoke, reserveId)); + _spendBorrowAllowance({ + 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 ITakerPositionManager + function withdrawAllowance( + address spoke, + uint256 reserveId, + address owner, + address spender + ) external view returns (uint256) { + return _getWithdrawAllowance(spoke, reserveId, owner, spender); + } + + /// @inheritdoc ITakerPositionManager + function borrowAllowance( + address spoke, + uint256 reserveId, + address owner, + address spender + ) external view returns (uint256) { + return _getBorrowAllowance(spoke, reserveId, owner, spender); + } + + function _getWithdrawAllowance( + address spoke, + uint256 reserveId, + address owner, + address spender + ) internal view returns (uint256) { + return _withdrawAllowances[spoke][reserveId][owner][spender]; + } + + function _getBorrowAllowance( + address spoke, + uint256 reserveId, + address owner, + address spender + ) internal view returns (uint256) { + return _borrowAllowances[spoke][reserveId][owner][spender]; + } + + 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 _updateBorrowAllowance( + address spoke, + uint256 reserveId, + address owner, + address spender, + uint256 newCreditDelegation + ) internal { + _borrowAllowances[spoke][reserveId][owner][spender] = newCreditDelegation; + emit BorrowApproval(spoke, reserveId, owner, spender, newCreditDelegation); + } + + function _spendWithdrawAllowance( + address spoke, + uint256 reserveId, + address owner, + address spender, + uint256 amount + ) internal { + uint256 currentAllowance = _getWithdrawAllowance(spoke, reserveId, owner, spender); + require(currentAllowance >= amount, InsufficientWithdrawAllowance(currentAllowance, amount)); + if (currentAllowance != type(uint256).max) { + _updateWithdrawAllowance({ + spoke: spoke, + reserveId: reserveId, + owner: owner, + spender: spender, + newAllowance: currentAllowance.uncheckedSub(amount) + }); + } + } + + function _spendBorrowAllowance( + address spoke, + uint256 reserveId, + address owner, + address spender, + uint256 amount + ) internal { + uint256 currentAllowance = _getBorrowAllowance(spoke, reserveId, owner, spender); + require(currentAllowance >= amount, InsufficientBorrowAllowance(currentAllowance, amount)); + if (currentAllowance != type(uint256).max) { + _updateBorrowAllowance({ + spoke: spoke, + reserveId: reserveId, + owner: owner, + spender: spender, + newCreditDelegation: currentAllowance.uncheckedSub(amount) + }); + } + } + + function _multicallEnabled() internal pure override returns (bool) { + return true; + } + + function _domainNameAndVersion() internal pure override returns (string memory, string memory) { + return ('TakerPositionManager', '1'); + } +} diff --git a/src/position-manager/interfaces/IConfigPositionManager.sol b/src/position-manager/interfaces/IConfigPositionManager.sol new file mode 100644 index 000000000..957f6201e --- /dev/null +++ b/src/position-manager/interfaces/IConfigPositionManager.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {IPositionManagerBase} from 'src/position-manager/interfaces/IPositionManagerBase.sol'; + +type ConfigPermissions is uint8; + +/// @title IConfigPositionManager +/// @author Aave Labs +/// @notice Interface for position manager handling user configuration actions on behalf of an user. +interface IConfigPositionManager is IPositionManagerBase { + /// @notice Struct to hold the config permission values. + /// @dev canSetUsingAsCollateral Whether the delegatee can set using as collateral on behalf of the user. + /// @dev canUpdateUserRiskPremium Whether the delegatee can update user risk premium on behalf of the user. + /// @dev canUpdateUserDynamicConfig Whether the delegatee can update user dynamic config on behalf of the user. + struct ConfigPermissionValues { + bool canSetUsingAsCollateral; + bool canUpdateUserRiskPremium; + bool canUpdateUserDynamicConfig; + } + + /// @notice Emitted when a global config permission is updated. + /// @param spoke The address of the spoke. + /// @param delegator The address of the delegator. + /// @param delegatee The address of the delegatee. + /// @param permissions The new config permissions. + event ConfigPermissionsUpdated( + address indexed spoke, + address indexed delegator, + address indexed delegatee, + ConfigPermissions permissions + ); + + /// @notice Thrown when the delegatee of a function was not given permission by the user. + error DelegateeNotAllowed(); + + /// @notice Sets the global permission for a delegatee. + /// @param spoke The address of the spoke. + /// @param delegatee The address of the delegatee. + /// @param permission The new permission status. + function setGlobalPermission(address spoke, address delegatee, bool permission) external; + + /// @notice Sets the using as collateral permission for a delegatee. + /// @param spoke The address of the spoke. + /// @param delegatee The address of the delegatee. + /// @param permission The new permission status. + function setCanUpdateUsingAsCollateralPermission( + address spoke, + address delegatee, + bool permission + ) external; + + /// @notice Sets the user risk premium permission for a delegatee. + /// @param spoke The address of the spoke. + /// @param delegatee The address of the delegatee. + /// @param permission The new permission status. + function setCanUpdateUserRiskPremiumPermission( + address spoke, + address delegatee, + bool permission + ) external; + + /// @notice Sets the user dynamic config permission for a delegatee. + /// @param spoke The address of the spoke. + /// @param delegatee The address of the delegatee. + /// @param permission The new permission status. + function setCanUpdateUserDynamicConfigPermission( + address spoke, + address delegatee, + bool permission + ) external; + + /// @notice Renounces the global permission given by the delegator. + /// @param spoke The address of the spoke. + /// @param delegator The address of the delegator. + function renounceGlobalPermission(address spoke, address delegator) external; + + /// @notice Renounces the using as collateral permission given by the delegator. + /// @param spoke The address of the spoke. + /// @param delegator The address of the delegator. + function renounceCanUpdateUsingAsCollateralPermission(address spoke, address delegator) external; + + /// @notice Renounces the user risk premium permission given by the delegator. + /// @param spoke The address of the spoke. + /// @param delegator The address of the delegator. + function renounceCanUpdateUserRiskPremiumPermission(address spoke, address delegator) external; + + /// @notice Renounces the user dynamic config permission given by the delegator. + /// @param spoke The address of the spoke. + /// @param delegator The address of the delegator. + function renounceCanUpdateUserDynamicConfigPermission(address spoke, address delegator) external; + + /// @notice Sets the using as collateral status on behalf of a user for a specified reserve. + /// @dev The `msg.sender` must have the permission to perform this action on behalf of the user. + /// @param spoke The address of the spoke. + /// @param reserveId The id of the reserve. + /// @param usingAsCollateral The new using as collateral status. + /// @param onBehalfOf The address of the user. + function setUsingAsCollateralOnBehalfOf( + address spoke, + uint256 reserveId, + bool usingAsCollateral, + address onBehalfOf + ) external; + + /// @notice Updates the user risk premium on behalf of a user. + /// @dev The `msg.sender` must have the permission to perform this action on behalf of the user. + /// @param spoke The address of the spoke. + /// @param onBehalfOf The address of the user. + function updateUserRiskPremiumOnBehalfOf(address spoke, address onBehalfOf) external; + + /// @notice Updates the user dynamic config on behalf of a user. + /// @dev The `msg.sender` must have the permission to perform this action on behalf of the user. + /// @param spoke The address of the spoke. + /// @param onBehalfOf The address of the user. + function updateUserDynamicConfigOnBehalfOf(address spoke, address onBehalfOf) external; + + /// @notice Returns the config permissions for a delegatee on behalf of a user. + /// @param spoke The address of the spoke. + /// @param delegatee The address of the delegatee. + /// @param onBehalfOf The address of the user. + /// @return The ConfigPermissionValues for the delegatee on behalf of the user. + function getConfigPermissions( + address spoke, + address delegatee, + address onBehalfOf + ) external view returns (ConfigPermissionValues memory); +} diff --git a/src/position-manager/interfaces/IGatewayBase.sol b/src/position-manager/interfaces/IGatewayBase.sol deleted file mode 100644 index 0b96f4c14..000000000 --- a/src/position-manager/interfaces/IGatewayBase.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Copyright (c) 2025 Aave Labs -pragma solidity ^0.8.0; - -import {IRescuable} from 'src/interfaces/IRescuable.sol'; - -/// @title IGatewayBase -/// @author Aave Labs -/// @notice Minimal interface for base gateway functionalities. -interface IGatewayBase is IRescuable { - /// @notice Emitted when a spoke is registered or deregistered. - event SpokeRegistered(address indexed spoke, bool active); - - /// @notice Thrown when the specified address is invalid. - error InvalidAddress(); - - /// @notice Thrown when the specified amount is invalid. - error InvalidAmount(); - - /// @notice Thrown when the specified spoke is not registered. - error SpokeNotRegistered(); - - /// @notice Allows contract to renounce its position manager role for `user`. - /// @dev Only authorized caller to invoke this method. - /// @param spoke The address of the registered `spoke`. - /// @param user The address of the user to renounce the position manager role for. - function renouncePositionManagerRole(address spoke, address user) external; - - /// @notice Permissioned operation to register or deregister a spoke. - /// @dev Only owner to invoke this method. - /// @param spoke The address of the `spoke`. - /// @param active `true` to register, `false` to deregister. - function registerSpoke(address spoke, bool active) external; - - /// @notice Returns whether the specified spoke is registered. - /// @param spoke The address of the `spoke`. - /// @return `true` if the spoke is registered, `false` otherwise. - function isSpokeRegistered(address spoke) external view returns (bool); -} diff --git a/src/position-manager/interfaces/IGiverPositionManager.sol b/src/position-manager/interfaces/IGiverPositionManager.sol new file mode 100644 index 000000000..ae1222311 --- /dev/null +++ b/src/position-manager/interfaces/IGiverPositionManager.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {IPositionManagerBase} from 'src/position-manager/interfaces/IPositionManagerBase.sol'; + +/// @title IGiverPositionManager +/// @author Aave Labs +/// @notice Interface for position manager handling supply and repay actions on behalf of users. +interface IGiverPositionManager is IPositionManagerBase { + /// @notice Error thrown when the repay amount is not an allowed value. + error NoMaxUintRepayOnBehalfOfAllowed(); + + /// @notice Executes a supply on behalf of a user. + /// @param spoke The address of the spoke. + /// @param reserveId The identifier of the reserve. + /// @param amount The amount to supply. + /// @param onBehalfOf The address of the user to supply on behalf of. + /// @return The amount of shares supplied. + /// @return The amount of assets supplied. + function supplyOnBehalfOf( + address spoke, + uint256 reserveId, + uint256 amount, + address onBehalfOf + ) external returns (uint256, uint256); + + /// @notice Executes a repay on behalf of a user. + /// @dev If the amount exceeds the user's current debt, the entire debt is repaid. + /// @dev Using `type(uint256).max` to repay the full debt is not allowed with this method. + /// @param spoke The address of the spoke. + /// @param reserveId The identifier of the reserve. + /// @param amount The amount to repay. + /// @param onBehalfOf The address of the user to repay on behalf of. + /// @return The amount of shares repaid. + /// @return The amount of assets repaid. + function repayOnBehalfOf( + address spoke, + uint256 reserveId, + uint256 amount, + address onBehalfOf + ) external returns (uint256, uint256); +} diff --git a/src/position-manager/interfaces/INativeTokenGateway.sol b/src/position-manager/interfaces/INativeTokenGateway.sol index 18abc75bf..b720d6294 100644 --- a/src/position-manager/interfaces/INativeTokenGateway.sol +++ b/src/position-manager/interfaces/INativeTokenGateway.sol @@ -2,22 +2,19 @@ // Copyright (c) 2025 Aave Labs pragma solidity ^0.8.0; -import {IGatewayBase} from 'src/position-manager/interfaces/IGatewayBase.sol'; +import {IPositionManagerBase} from 'src/position-manager/interfaces/IPositionManagerBase.sol'; /// @title INativeTokenGateway /// @author Aave Labs /// @notice Abstracts actions to the protocol involving the native token. /// @dev Must be set as `PositionManager` on the spoke for the user. -interface INativeTokenGateway is IGatewayBase { +interface INativeTokenGateway is IPositionManagerBase { /// @notice Thrown when the underlying asset is not the wrapped native asset. error NotNativeWrappedAsset(); /// @notice Thrown when the native amount sent does not match the given amount parameter. error NativeAmountMismatch(); - /// @notice Thrown when trying to call an unsupported action or sending native assets to this contract directly. - error UnsupportedAction(); - /// @notice Wraps the native asset and supplies to a specified registered `spoke`. /// @dev Contract must be an active & approved user position manager of the caller. /// @param spoke The address of the registered `spoke`. @@ -85,5 +82,5 @@ interface INativeTokenGateway is IGatewayBase { ) external payable returns (uint256, uint256); /// @notice Returns the address of the Native Wrapper. - function NATIVE_WRAPPER() external view returns (address); + function NATIVE_TOKEN_WRAPPER() external view returns (address); } diff --git a/src/position-manager/interfaces/IPositionManagerBase.sol b/src/position-manager/interfaces/IPositionManagerBase.sol new file mode 100644 index 000000000..633f07ced --- /dev/null +++ b/src/position-manager/interfaces/IPositionManagerBase.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {IIntentConsumer} from 'src/interfaces/IIntentConsumer.sol'; +import {IMulticall} from 'src/interfaces/IMulticall.sol'; +import {IRescuable} from 'src/interfaces/IRescuable.sol'; + +/// @title IPositionManagerBase +/// @author Aave Labs +/// @notice Base interface for position managers. +interface IPositionManagerBase is IIntentConsumer, IRescuable, IMulticall { + /// @notice Emitted when a spoke is registered or deregistered. + event SpokeRegistered(address indexed spoke, bool registered); + + /// @notice Thrown when the specified address is invalid. + error InvalidAddress(); + + /// @notice Thrown when the specified amount is invalid. + error InvalidAmount(); + + /// @notice Thrown when trying to call an unsupported action on this position manager. + error UnsupportedAction(); + + /// @notice Thrown when the specified spoke is not registered. + error SpokeNotRegistered(); + + /// @notice Facilitates setting this position manager as user position manager on the specified registered `spoke` + /// with a typed signature from `onBehalfOf`. + /// @dev The signature is consumed on the the specified registered `spoke`. + /// @dev The given data is passed to the `spoke` for the signature to be verified. + /// @param spoke The address of the registered spoke. + /// @param onBehalfOf The address of the user on whose behalf this position manager can act. + /// @param approve True to approve the position manager, false to revoke approval. + /// @param nonce The key-prefixed nonce for the signature. + /// @param deadline The deadline for the intent. + /// @param signature The EIP712-typed signed bytes for the intent. + function setSelfAsUserPositionManagerWithSig( + address spoke, + address onBehalfOf, + bool approve, + uint256 nonce, + uint256 deadline, + bytes calldata signature + ) external; + + /// @notice Facilitates consuming a permit for the given reserve's underlying asset on the specified registered `spoke`. + /// @dev The given data is passed to the underlying asset for the signature to be verified. + /// @dev Spender is this position manager contract. + /// @param spoke The address of the spoke. + /// @param reserveId The identifier of the reserve. + /// @param onBehalfOf The address of the user on whose behalf the permit is being used. + /// @param value The amount of the underlying asset to permit. + /// @param deadline The deadline for the permit. + function permitReserveUnderlying( + address spoke, + uint256 reserveId, + address onBehalfOf, + uint256 value, + uint256 deadline, + uint8 permitV, + bytes32 permitR, + bytes32 permitS + ) external; + + /// @notice Allows contract to renounce its position manager role for the specified user. + /// @param spoke The address of the registered Spoke. + /// @param user The address of the user to renounce the position manager role for. + function renouncePositionManagerRole(address spoke, address user) external; + + /// @notice Register or deregister a spoke. + /// @param spoke The address of the Spoke. + /// @param registered `true` to register, `false` to deregister. + function registerSpoke(address spoke, bool registered) external; + + /// @notice Returns whether the specified spoke is registered. + /// @param spoke The address of the `spoke`. + /// @return `true` if the spoke is registered, `false` otherwise. + function isSpokeRegistered(address spoke) external view returns (bool); +} diff --git a/src/position-manager/interfaces/ISignatureGateway.sol b/src/position-manager/interfaces/ISignatureGateway.sol index b13c09e05..10e3f4092 100644 --- a/src/position-manager/interfaces/ISignatureGateway.sol +++ b/src/position-manager/interfaces/ISignatureGateway.sol @@ -2,14 +2,12 @@ // Copyright (c) 2025 Aave Labs pragma solidity ^0.8.0; -import {IMulticall} from 'src/interfaces/IMulticall.sol'; -import {IIntentConsumer} from 'src/interfaces/IIntentConsumer.sol'; -import {IGatewayBase} from 'src/position-manager/interfaces/IGatewayBase.sol'; +import {IPositionManagerBase} from 'src/position-manager/interfaces/IPositionManagerBase.sol'; /// @title ISignatureGateway /// @author Aave Labs /// @notice Minimal interface for protocol actions involving signed intents. -interface ISignatureGateway is IGatewayBase, IIntentConsumer, IMulticall { +interface ISignatureGateway is IPositionManagerBase { /// @notice Intent data to supply assets to a reserve. /// @param spoke The address of the registered spoke. /// @param reserveId The identifier of the reserve. @@ -191,47 +189,6 @@ interface ISignatureGateway is IGatewayBase, IIntentConsumer, IMulticall { bytes calldata signature ) external; - /// @notice Facilitates setting this gateway as user position manager on the specified registered `spoke` - /// with a typed signature from `onBehalfOf`. - /// @dev The signature is consumed on the the specified registered `spoke`. - /// @dev The given data is passed to the `spoke` for the signature to be verified. - /// @param spoke The address of the registered spoke. - /// @param onBehalfOf The address of the user on whose behalf this gateway can act. - /// @param approve True to approve the gateway, false to revoke approval. - /// @param nonce The key-prefixed nonce for the signature. - /// @param deadline The deadline for the intent. - /// @param signature The EIP712-typed signed bytes for the intent. - function setSelfAsUserPositionManagerWithSig( - address spoke, - address onBehalfOf, - bool approve, - uint256 nonce, - uint256 deadline, - bytes calldata signature - ) external; - - /// @notice Facilitates consuming a permit for the given reserve's underlying asset on the specified registered `spoke`. - /// @dev The given data is passed to the underlying asset for the signature to be verified. - /// @dev The SignatureGateway must be configured as the spender. - /// @param spoke The address of the spoke. - /// @param reserveId The identifier of the reserve. - /// @param onBehalfOf The address of the user on whose behalf the permit is being used. - /// @param value The amount of the underlying asset to permit. - /// @param deadline The deadline for the permit. - /// @param permitV The V component of the permit signature. - /// @param permitR The R component of the permit signature. - /// @param permitS The S component of the permit signature. - function permitReserve( - address spoke, - uint256 reserveId, - address onBehalfOf, - uint256 value, - uint256 deadline, - uint8 permitV, - bytes32 permitR, - bytes32 permitS - ) external; - /// @notice Returns the type hash for the Supply intent. function SUPPLY_TYPEHASH() external view returns (bytes32); diff --git a/src/position-manager/interfaces/ITakerPositionManager.sol b/src/position-manager/interfaces/ITakerPositionManager.sol new file mode 100644 index 000000000..d26d952bb --- /dev/null +++ b/src/position-manager/interfaces/ITakerPositionManager.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {IPositionManagerBase} from 'src/position-manager/interfaces/IPositionManagerBase.sol'; + +/// @title ITakerPositionManager +/// @author Aave Labs +/// @notice Interface for position manager handling withdraw permit and borrow permit actions on behalf of users. +interface ITakerPositionManager is IPositionManagerBase { + /// @notice Structured parameters for withdraw permit intent. + /// @param spoke The address of the spoke. + /// @param reserveId The identifier of the reserve. + /// @param owner The address of the owner. + /// @param spender The address of the spender. + /// @param amount The amount of allowance. + /// @param nonce The key-prefixed nonce for the signature. + /// @param deadline The deadline for the intent. + struct WithdrawPermit { + address spoke; + uint256 reserveId; + address owner; + address spender; + uint256 amount; + uint256 nonce; + uint256 deadline; + } + + /// @notice Structured parameters for borrow permit intent. + /// @param spoke The address of the spoke. + /// @param reserveId The identifier of the reserve. + /// @param owner The address of the owner. + /// @param spender The address of the spender. + /// @param amount The amount of allowance. + /// @param nonce The key-prefixed nonce for the signature. + /// @param deadline The deadline for the intent. + struct BorrowPermit { + address spoke; + uint256 reserveId; + address owner; + address spender; + uint256 amount; + uint256 nonce; + uint256 deadline; + } + + /// @notice Emitted when owner approves spender to withdraw amount for reserveId on their behalf. + /// @param spoke The address of the spoke. + /// @param reserveId The identifier of the reserve. + /// @param owner The address of the owner. + /// @param spender The address of the spender. + /// @param amount The amount of allowance. + event WithdrawApproval( + address indexed spoke, + uint256 indexed reserveId, + address indexed owner, + address spender, + uint256 amount + ); + + /// @notice Emitted when owner approves spender to borrow amount from reserveId on their behalf. + /// @param spoke The address of the spoke. + /// @param reserveId The identifier of the reserve. + /// @param owner The address of the owner. + /// @param spender The address of the spender. + /// @param amount The amount of allowance. + event BorrowApproval( + address indexed spoke, + uint256 indexed reserveId, + address indexed owner, + address spender, + uint256 amount + ); + + /// @notice Thrown when the withdraw allowance is insufficient. + error InsufficientWithdrawAllowance(uint256 allowance, uint256 required); + + /// @notice Thrown when the borrow allowance is insufficient. + error InsufficientBorrowAllowance(uint256 allowance, uint256 required); + + /// @notice Approves a spender to withdraw assets from the specified reserve. + /// @dev Using `type(uint256).max` as the amount results in an infinite approval, so the allowance is never decreased. + /// @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 approveWithdraw( + address spoke, + uint256 reserveId, + address spender, + uint256 amount + ) external; + + /// @notice Approves a spender to withdraw from the specified reserve using an EIP712-typed intent. + /// @dev Using `type(uint256).max` as the amount results in an infinite approval, so the allowance is never decreased. + /// @param params The structured WithdrawPermit parameters. + /// @param signature The EIP712-compliant signature bytes. + function approveWithdrawWithSig( + WithdrawPermit calldata params, + bytes calldata signature + ) external; + + /// @notice Approves a borrow allowance for a spender. + /// @dev Using `type(uint256).max` as the amount results in an infinite approval, so the allowance is never decreased. + /// @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 approveBorrow( + address spoke, + uint256 reserveId, + address spender, + uint256 amount + ) external; + + /// @notice Approves a spender to borrow from the specified reserve using an EIP712-typed intent. + /// @dev Using `type(uint256).max` as the amount results in an infinite approval, so the allowance is never decreased. + /// @param params The structured BorrowPermit parameters. + /// @param signature The EIP712-compliant signature bytes. + function approveBorrowWithSig(BorrowPermit calldata params, bytes calldata signature) external; + + /// @notice Renounces the withdraw allowance given by the owner. + /// @param spoke The address of the spoke. + /// @param reserveId The identifier of the reserve. + /// @param owner The address of the owner. + function renounceWithdrawAllowance(address spoke, uint256 reserveId, address owner) external; + + /// @notice Renounces the borrow allowance given by the owner. + /// @param spoke The address of the spoke. + /// @param reserveId The identifier of the reserve. + /// @param owner The address of the owner. + function renounceBorrowAllowance(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 receives the withdrawn assets. + /// @param spoke The address of the spoke. + /// @param reserveId The identifier of the reserve. + /// @param amount The amount to withdraw. + /// @param onBehalfOf The address of the user to withdraw on behalf of. + /// @return The amount of shares withdrawn. + /// @return The amount of assets withdrawn. + function withdrawOnBehalfOf( + address spoke, + uint256 reserveId, + uint256 amount, + address onBehalfOf + ) external returns (uint256, uint256); + + /// @notice Executes a borrow on behalf of a user. + /// @dev The caller must have sufficient borrow allowance from onBehalfOf. + /// @dev The caller receives the borrowed assets. + /// @param spoke The address of the spoke. + /// @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( + address spoke, + uint256 reserveId, + uint256 amount, + address onBehalfOf + ) external returns (uint256, uint256); + + /// @notice Returns the withdraw allowance for a spender on behalf of an owner. + /// @param spoke The address of the spoke. + /// @param reserveId The identifier of the reserve. + /// @param owner The address of the owner. + /// @param spender The address of the spender. + /// @return The amount of withdraw allowance. + function withdrawAllowance( + address spoke, + uint256 reserveId, + address owner, + address spender + ) external view returns (uint256); + + /// @notice Returns the credit delegation allowance for a spender on behalf of an owner. + /// @param spoke The address of the spoke. + /// @param reserveId The identifier of the reserve. + /// @param owner The address of the owner. + /// @param spender The address of the spender. + /// @return The amount of credit delegation allowance. + function borrowAllowance( + address spoke, + uint256 reserveId, + address owner, + address spender + ) external view returns (uint256); + + /// @notice Returns the type hash for the WithdrawPermit intent. + function WITHDRAW_PERMIT_TYPEHASH() external view returns (bytes32); + + /// @notice Returns the type hash for the BorrowPermit intent. + function BORROW_PERMIT_TYPEHASH() external view returns (bytes32); +} diff --git a/src/position-manager/libraries/ConfigPermissionsMap.sol b/src/position-manager/libraries/ConfigPermissionsMap.sol new file mode 100644 index 000000000..556675fa3 --- /dev/null +++ b/src/position-manager/libraries/ConfigPermissionsMap.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.20; + +import {ConfigPermissions} from 'src/position-manager/interfaces/IConfigPositionManager.sol'; + +/// @title ConfigPermissions Library +/// @author Aave Labs +/// @notice Implements the bitmap logic to handle the ConfigPermissions configuration. +library ConfigPermissionsMap { + /// @dev Mask for the `canSetUsingAsCollateral` permission. + uint8 internal constant CAN_SET_USING_AS_COLLATERAL_MASK = 0x1; + /// @dev Mask for the `canUpdateUserRiskPremium` permission. + uint8 internal constant CAN_UPDATE_USER_RISK_PREMIUM_MASK = 0x2; + /// @dev Mask for the `canUpdateUserDynamicConfig` permission. + uint8 internal constant CAN_UPDATE_USER_DYNAMIC_CONFIG_MASK = 0x4; + /// @dev Mask for the full permissions. + uint8 internal constant FULL_PERMISSIONS_MASK = 0x7; + + /// @notice Creates a ConfigPermissions with all permissions set to the given status. + /// @param status The status for all permissions. + /// @return The created ConfigPermissions. + function setFullPermissions(bool status) internal pure returns (ConfigPermissions) { + return ConfigPermissions.wrap(status ? FULL_PERMISSIONS_MASK : 0); + } + + /// @notice Sets the new status for the `canSetUsingAsCollateral` permission. + /// @param self The current ConfigPermissions. + /// @param status The new status for the `canSetUsingAsCollateral` permission. + /// @return The updated ConfigPermissions. + function setCanSetUsingAsCollateral( + ConfigPermissions self, + bool status + ) internal pure returns (ConfigPermissions) { + return + ConfigPermissions.wrap( + _setStatus(ConfigPermissions.unwrap(self), CAN_SET_USING_AS_COLLATERAL_MASK, status) + ); + } + + /// @notice Sets the new status for the `canUpdateUserRiskPremium` permission. + /// @param self The current ConfigPermissions. + /// @param status The new status for the `canUpdateUserRiskPremium` permission. + /// @return The updated ConfigPermissions. + function setCanUpdateUserRiskPremium( + ConfigPermissions self, + bool status + ) internal pure returns (ConfigPermissions) { + return + ConfigPermissions.wrap( + _setStatus(ConfigPermissions.unwrap(self), CAN_UPDATE_USER_RISK_PREMIUM_MASK, status) + ); + } + + /// @notice Sets the new status for the `canUpdateUserDynamicConfig` permission. + /// @param self The current ConfigPermissions. + /// @param status The new status for the `canUpdateUserDynamicConfig` permission. + /// @return The updated ConfigPermissions. + function setCanUpdateUserDynamicConfig( + ConfigPermissions self, + bool status + ) internal pure returns (ConfigPermissions) { + return + ConfigPermissions.wrap( + _setStatus(ConfigPermissions.unwrap(self), CAN_UPDATE_USER_DYNAMIC_CONFIG_MASK, status) + ); + } + + /// @notice Returns whether the `canSetUsingAsCollateral` permission or global permissions are enabled. + /// @param self The current ConfigPermissions. + /// @return Whether the `canSetUsingAsCollateral` permission or global permissions are enabled. + function canSetUsingAsCollateral(ConfigPermissions self) internal pure returns (bool) { + return _getStatus(self, CAN_SET_USING_AS_COLLATERAL_MASK); + } + + /// @notice Returns whether the `canUpdateUserRiskPremium` permission or global permissions are enabled. + /// @param self The current ConfigPermissions. + /// @return Whether the `canUpdateUserRiskPremium` permission or global permissions are enabled + function canUpdateUserRiskPremium(ConfigPermissions self) internal pure returns (bool) { + return _getStatus(self, CAN_UPDATE_USER_RISK_PREMIUM_MASK); + } + + /// @notice Returns whether the `canUpdateUserDynamicConfig` permission or global permissions are enabled. + /// @param self The current ConfigPermissions. + /// @return Whether the `canUpdateUserDynamicConfig` permission or global permissions are enabled + function canUpdateUserDynamicConfig(ConfigPermissions self) internal pure returns (bool) { + return _getStatus(self, CAN_UPDATE_USER_DYNAMIC_CONFIG_MASK); + } + + /// @notice Compares two ConfigPermissions for equality. + /// @param self The first ConfigPermissions. + /// @param other The second ConfigPermissions. + /// @return True if both ConfigPermissions are equal, false otherwise. + function eq(ConfigPermissions self, ConfigPermissions other) internal pure returns (bool) { + return ConfigPermissions.unwrap(self) == ConfigPermissions.unwrap(other); + } + + /// @notice Sets the new status for the given permission. + function _setStatus(uint8 self, uint8 mask, bool status) private pure returns (uint8) { + return status ? self | mask : self & ~mask; + } + + /// @notice Returns whether the given permission is enabled. + function _getStatus(ConfigPermissions self, uint8 mask) private pure returns (bool) { + return _getStatus(ConfigPermissions.unwrap(self), mask); + } + + /// @notice Returns whether the given permission is enabled. + function _getStatus(uint8 self, uint8 mask) private pure returns (bool) { + return (self & mask) != 0; + } +} diff --git a/src/position-manager/libraries/EIP712Hash.sol b/src/position-manager/libraries/EIP712Hash.sol index d114eafb8..1d8bf5290 100644 --- a/src/position-manager/libraries/EIP712Hash.sol +++ b/src/position-manager/libraries/EIP712Hash.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.20; import {ISignatureGateway} from 'src/position-manager/interfaces/ISignatureGateway.sol'; +import {ITakerPositionManager} from 'src/position-manager/interfaces/ITakerPositionManager.sol'; /// @title EIP712Hash library /// @author Aave Labs @@ -36,6 +37,14 @@ library EIP712Hash { // keccak256('UpdateUserDynamicConfig(address spoke,address onBehalfOf,uint256 nonce,uint256 deadline)') 0x4a168dd8b32d260d07d6f0be832e23035a65a47f788675b0b02270c68b987886; + bytes32 public constant WITHDRAW_PERMIT_TYPEHASH = + // keccak256('WithdrawPermit(address spoke,uint256 reserveId,address owner,address spender,uint256 amount,uint256 nonce,uint256 deadline)') + 0x9e6642fd4c06a4c1a5e201f1e41c6b7892fcf06859c796b054c510b80e2a0a3f; + + bytes32 public constant BORROW_PERMIT_TYPEHASH = + // keccak256('BorrowPermit(address spoke,uint256 reserveId,address owner,address spender,uint256 amount,uint256 nonce,uint256 deadline)') + 0x14236ea048da65ffb52a9b32a2c840f24ab374cc31f65faeb7877d22ceca144e; + function hash(ISignatureGateway.Supply calldata params) internal pure returns (bytes32) { return keccak256( @@ -142,4 +151,40 @@ library EIP712Hash { ) ); } + + function hash( + ITakerPositionManager.WithdrawPermit calldata params + ) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + WITHDRAW_PERMIT_TYPEHASH, + params.spoke, + params.reserveId, + params.owner, + params.spender, + params.amount, + params.nonce, + params.deadline + ) + ); + } + + function hash( + ITakerPositionManager.BorrowPermit calldata params + ) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + BORROW_PERMIT_TYPEHASH, + params.spoke, + params.reserveId, + params.owner, + params.spender, + params.amount, + params.nonce, + params.deadline + ) + ); + } } diff --git a/src/utils/Multicall.sol b/src/utils/Multicall.sol index 67db1d98e..4308e4667 100644 --- a/src/utils/Multicall.sol +++ b/src/utils/Multicall.sol @@ -10,7 +10,7 @@ import {IMulticall} from 'src/interfaces/IMulticall.sol'; /// @dev Inspired by the OpenZeppelin Multicall contract. abstract contract Multicall is IMulticall { /// @inheritdoc IMulticall - function multicall(bytes[] calldata data) external returns (bytes[] memory) { + function multicall(bytes[] calldata data) public virtual returns (bytes[] memory) { bytes[] memory results = new bytes[](data.length); for (uint256 i; i < data.length; ++i) { (bool ok, bytes memory res) = address(this).delegatecall(data[i]); diff --git a/tests/Base.t.sol b/tests/Base.t.sol index 391409e97..2c314ed7d 100644 --- a/tests/Base.t.sol +++ b/tests/Base.t.sol @@ -75,9 +75,28 @@ import {TokenizationSpoke, ITokenizationSpoke} from 'src/spoke/TokenizationSpoke import {TokenizationSpokeInstance} from 'src/spoke/instances/TokenizationSpokeInstance.sol'; // position manager -import {GatewayBase, IGatewayBase} from 'src/position-manager/GatewayBase.sol'; +import { + PositionManagerBase, + IPositionManagerBase +} from 'src/position-manager/PositionManagerBase.sol'; import {NativeTokenGateway, INativeTokenGateway} from 'src/position-manager/NativeTokenGateway.sol'; import {SignatureGateway, ISignatureGateway} from 'src/position-manager/SignatureGateway.sol'; +import { + GiverPositionManager, + IGiverPositionManager +} from 'src/position-manager/GiverPositionManager.sol'; +import { + TakerPositionManager, + ITakerPositionManager +} from 'src/position-manager/TakerPositionManager.sol'; +import { + ConfigPositionManager, + IConfigPositionManager +} from 'src/position-manager/ConfigPositionManager.sol'; +import { + ConfigPermissions, + ConfigPermissionsMap +} from 'src/position-manager/libraries/ConfigPermissionsMap.sol'; // test import {Constants} from 'tests/Constants.sol'; @@ -91,7 +110,8 @@ import {MockERC20} from 'tests/mocks/MockERC20.sol'; import {MockPriceFeed} from 'tests/mocks/MockPriceFeed.sol'; import {PositionStatusMapWrapper} from 'tests/mocks/PositionStatusMapWrapper.sol'; import {RescuableWrapper} from 'tests/mocks/RescuableWrapper.sol'; -import {GatewayBaseWrapper} from 'tests/mocks/GatewayBaseWrapper.sol'; +import {PositionManagerBaseWrapper} from 'tests/mocks/PositionManagerBaseWrapper.sol'; +import {PositionManagerNoMulticall} from 'tests/mocks/PositionManagerNoMulticall.sol'; import {MockNoncesKeyed} from 'tests/mocks/MockNoncesKeyed.sol'; import {MockSpoke} from 'tests/mocks/MockSpoke.sol'; import {MockERC1271Wallet} from 'tests/mocks/MockERC1271Wallet.sol'; @@ -3183,10 +3203,15 @@ abstract contract Base is Test { address who, uint256 prevKeyNonce ) internal view { - (uint192 nonceKey, uint64 nonce) = _unpackNonce(prevKeyNonce); + (uint192 currentKey, ) = _unpackNonce(prevKeyNonce); + assertEq(verifier.nonces(who, currentKey), _getNextNoncePacked(prevKeyNonce)); + } + + function _getNextNoncePacked(uint256 currentKeyNonce) internal pure returns (uint256) { + (uint192 nonceKey, uint64 nonce) = _unpackNonce(currentKeyNonce); // prettier-ignore unchecked { ++nonce; } - assertEq(verifier.nonces(who, nonceKey), _packNonce(nonceKey, nonce)); + return _packNonce(nonceKey, nonce); } function _assertEntityHasNoBalanceOrAllowance( diff --git a/tests/gas/Gateways.Operations.gas.t.sol b/tests/gas/Gateways.Operations.gas.t.sol index 3b123cee1..1ac938f59 100644 --- a/tests/gas/Gateways.Operations.gas.t.sol +++ b/tests/gas/Gateways.Operations.gas.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import 'tests/Base.t.sol'; -import 'tests/unit/misc/SignatureGateway/SignatureGateway.Base.t.sol'; +import 'tests/unit/position-managers/SignatureGateway/SignatureGateway.Base.t.sol'; /// forge-config: default.isolate = true contract NativeTokenGateway_Gas_Tests is Base { diff --git a/tests/gas/PositionManagers.Operations.gas.t.sol b/tests/gas/PositionManagers.Operations.gas.t.sol new file mode 100644 index 000000000..f1f8e6bc3 --- /dev/null +++ b/tests/gas/PositionManagers.Operations.gas.t.sol @@ -0,0 +1,373 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/unit/Spoke/SpokeBase.t.sol'; + +/// forge-config: default.isolate = true +contract PositionManager_Gas_Tests is SpokeBase { + string internal NAMESPACE = 'PositionManagerBase.Operations'; + + PositionManagerBaseWrapper public positionManager; + uint192 internal nonceKey = 0; + + function setUp() public virtual override { + deployFixtures(); + initEnvironment(); + + positionManager = new PositionManagerBaseWrapper(address(ADMIN)); + + vm.prank(SPOKE_ADMIN); + spoke1.updatePositionManager(address(positionManager), true); + + vm.prank(ADMIN); + positionManager.registerSpoke(address(spoke1), true); + } + + function test_setSelfAsUserPositionManagerWithSig() public { + vm.prank(alice); + spoke1.useNonce(nonceKey); + + ISpoke.PositionManagerUpdate[] memory updates = new ISpoke.PositionManagerUpdate[](1); + updates[0] = ISpoke.PositionManagerUpdate(address(positionManager), true); + + ISpoke.SetUserPositionManagers memory p = ISpoke.SetUserPositionManagers({ + onBehalfOf: alice, + updates: updates, + nonce: spoke1.nonces(alice, nonceKey), + deadline: vm.getBlockTimestamp() + }); + bytes memory signature = _sign(alicePk, _getTypedDataHash(spoke1, p)); + + vm.prank(alice); + spoke1.setUserPositionManager(address(positionManager), false); + + positionManager.setSelfAsUserPositionManagerWithSig({ + spoke: address(spoke1), + onBehalfOf: p.onBehalfOf, + approve: p.updates[0].approve, + nonce: p.nonce, + deadline: p.deadline, + signature: signature + }); + vm.snapshotGasLastCall(NAMESPACE, 'setSelfAsUserPositionManagerWithSig'); + } +} + +/// forge-config: default.isolate = true +contract GiverPositionManager_Gas_Tests is SpokeBase { + string internal NAMESPACE = 'GiverPositionManager.Operations'; + + GiverPositionManager public positionManager; + + function setUp() public virtual override { + deployFixtures(); + initEnvironment(); + + positionManager = new GiverPositionManager(address(ADMIN)); + vm.prank(SPOKE_ADMIN); + spoke1.updatePositionManager(address(positionManager), true); + vm.prank(ADMIN); + positionManager.registerSpoke(address(spoke1), true); + vm.prank(alice); + spoke1.setUserPositionManager(address(positionManager), true); + vm.prank(bob); + tokenList.dai.approve(address(positionManager), UINT256_MAX); + } + + function test_supplyOnBehalfOf() public { + uint256 amount = 100e18; + Utils.supply(spoke1, _daiReserveId(spoke1), alice, amount, alice); + + vm.prank(bob); + positionManager.supplyOnBehalfOf(address(spoke1), _daiReserveId(spoke1), amount, alice); + vm.snapshotGasLastCall(NAMESPACE, 'supplyOnBehalfOf'); + } + + function test_repayOnBehalfOf() public { + uint256 aliceSupplyAmount = 1000e18; + uint256 bobSupplyAmount = 150e18; + uint256 borrowAmount = 100e18; + uint256 repayAmount = 50e18; + + Utils.supplyCollateral(spoke1, _daiReserveId(spoke1), alice, aliceSupplyAmount, alice); + Utils.supply(spoke1, _daiReserveId(spoke1), bob, bobSupplyAmount, bob); + Utils.borrow(spoke1, _daiReserveId(spoke1), alice, borrowAmount, alice); + Utils.repay(spoke1, _daiReserveId(spoke1), alice, 1e18, alice); + + vm.prank(bob); + positionManager.repayOnBehalfOf(address(spoke1), _daiReserveId(spoke1), repayAmount, alice); + vm.snapshotGasLastCall(NAMESPACE, 'repayOnBehalfOf'); + } +} + +/// forge-config: default.isolate = true +contract TakerPositionManager_Gas_Tests is SpokeBase { + string internal NAMESPACE = 'TakerPositionManager.Operations'; + + TakerPositionManager public positionManager; + uint192 internal withdrawNonceKey = 0; + uint192 internal creditNonceKey = 1; + + function setUp() public virtual override { + deployFixtures(); + initEnvironment(); + + positionManager = new TakerPositionManager(address(ADMIN)); + vm.prank(SPOKE_ADMIN); + spoke1.updatePositionManager(address(positionManager), true); + vm.prank(ADMIN); + positionManager.registerSpoke(address(spoke1), true); + vm.prank(alice); + spoke1.setUserPositionManager(address(positionManager), true); + } + + function test_withdrawOnBehalfOf() public { + uint256 amount = 100e18; + + vm.prank(alice); + positionManager.approveWithdraw(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'); + + vm.prank(alice); + positionManager.approveWithdraw(address(spoke1), _daiReserveId(spoke1), bob, UINT256_MAX); + vm.prank(bob); + positionManager.withdrawOnBehalfOf(address(spoke1), _daiReserveId(spoke1), UINT256_MAX, alice); + vm.snapshotGasLastCall(NAMESPACE, 'withdrawOnBehalfOf: full'); + } + + function test_borrowOnBehalfOf() public { + uint256 aliceSupplyAmount = 5000e18; + uint256 bobSupplyAmount = 1000e18; + uint256 borrowAmount = 750e18; + + vm.prank(alice); + positionManager.approveBorrow(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'); + } + + function test_approveWithdraw() public { + uint256 amount = 100e18; + + vm.prank(alice); + positionManager.approveWithdraw(address(spoke1), _daiReserveId(spoke1), bob, amount); + vm.snapshotGasLastCall(NAMESPACE, 'approveWithdraw'); + } + + function test_approveWithdrawWithSig() public { + uint256 amount = 100e18; + + vm.prank(alice); + positionManager.useNonce(withdrawNonceKey); + + ITakerPositionManager.WithdrawPermit memory p = ITakerPositionManager.WithdrawPermit({ + spoke: address(spoke1), + reserveId: _daiReserveId(spoke1), + owner: alice, + spender: bob, + amount: amount, + nonce: positionManager.nonces(alice, withdrawNonceKey), + deadline: vm.getBlockTimestamp() + }); + bytes32 digest = _typedDataHash( + positionManager, + vm.eip712HashStruct('WithdrawPermit', abi.encode(p)) + ); + bytes memory signature = _sign(alicePk, digest); + + vm.prank(vm.randomAddress()); + positionManager.approveWithdrawWithSig(p, signature); + vm.snapshotGasLastCall(NAMESPACE, 'approveWithdrawWithSig'); + } + + function test_renounceWithdrawAllowance() public { + uint256 amount = 100e18; + + vm.prank(alice); + positionManager.approveWithdraw(address(spoke1), _daiReserveId(spoke1), bob, amount); + + vm.prank(bob); + positionManager.renounceWithdrawAllowance(address(spoke1), _daiReserveId(spoke1), alice); + vm.snapshotGasLastCall(NAMESPACE, 'renounceWithdrawAllowance'); + } + + function test_creditDelegation() public { + uint256 amount = 100e18; + + vm.prank(alice); + positionManager.approveBorrow(address(spoke1), _daiReserveId(spoke1), bob, amount); + vm.snapshotGasLastCall(NAMESPACE, 'approveBorrow'); + } + + function test_delegateCreditWithSig() public { + uint256 amount = 100e18; + + vm.prank(alice); + positionManager.useNonce(creditNonceKey); + + ITakerPositionManager.BorrowPermit memory p = ITakerPositionManager.BorrowPermit({ + spoke: address(spoke1), + reserveId: _daiReserveId(spoke1), + owner: alice, + spender: bob, + amount: amount, + nonce: positionManager.nonces(alice, creditNonceKey), + deadline: vm.getBlockTimestamp() + }); + bytes32 digest = _typedDataHash( + positionManager, + vm.eip712HashStruct('BorrowPermit', abi.encode(p)) + ); + bytes memory signature = _sign(alicePk, digest); + + vm.prank(vm.randomAddress()); + positionManager.approveBorrowWithSig(p, signature); + vm.snapshotGasLastCall(NAMESPACE, 'approveBorrowWithSig'); + } + + function test_renounceCreditDelegation() public { + uint256 amount = 100e18; + + vm.prank(alice); + positionManager.approveBorrow(address(spoke1), _daiReserveId(spoke1), bob, amount); + + vm.prank(bob); + positionManager.renounceBorrowAllowance(address(spoke1), _daiReserveId(spoke1), alice); + vm.snapshotGasLastCall(NAMESPACE, 'renounceBorrowAllowance'); + } + + function _typedDataHash( + ITakerPositionManager _positionManager, + bytes32 typeHash + ) internal view returns (bytes32) { + return keccak256(abi.encodePacked('\x19\x01', _positionManager.DOMAIN_SEPARATOR(), typeHash)); + } +} + +/// forge-config: default.isolate = true +contract ConfigPositionManager_Gas_Tests is SpokeBase { + string internal NAMESPACE = 'ConfigPositionManager.Operations'; + + ConfigPositionManager public positionManager; + + function setUp() public virtual override { + deployFixtures(); + initEnvironment(); + + positionManager = new ConfigPositionManager(address(ADMIN)); + + vm.prank(SPOKE_ADMIN); + spoke1.updatePositionManager(address(positionManager), true); + vm.prank(ADMIN); + positionManager.registerSpoke(address(spoke1), true); + vm.prank(alice); + spoke1.setUserPositionManager(address(positionManager), true); + } + + function test_setGlobalPermission() public { + vm.prank(alice); + positionManager.setGlobalPermission(address(spoke1), bob, true); + vm.snapshotGasLastCall(NAMESPACE, 'setGlobalPermission'); + } + + function test_setCanUpdateUsingAsCollateralPermission() public { + vm.prank(alice); + positionManager.setCanUpdateUsingAsCollateralPermission(address(spoke1), bob, true); + vm.snapshotGasLastCall(NAMESPACE, 'setCanUpdateUsingAsCollateralPermission'); + } + + function test_setCanUpdateUserRiskPremiumPermission() public { + vm.prank(alice); + positionManager.setCanUpdateUserRiskPremiumPermission(address(spoke1), bob, true); + vm.snapshotGasLastCall(NAMESPACE, 'setCanUpdateUserRiskPremiumPermission'); + } + + function test_setCanUpdateUserDynamicConfigPermission() public { + vm.prank(alice); + positionManager.setCanUpdateUserDynamicConfigPermission(address(spoke1), bob, true); + vm.snapshotGasLastCall(NAMESPACE, 'setCanUpdateUserDynamicConfigPermission'); + } + + function test_renounceGlobalPermission() public { + vm.prank(alice); + positionManager.setGlobalPermission(address(spoke1), bob, true); + + vm.prank(bob); + positionManager.renounceGlobalPermission(address(spoke1), alice); + vm.snapshotGasLastCall(NAMESPACE, 'renounceGlobalPermission'); + } + + function test_renounceCanUpdateUsingAsCollateralPermission() public { + vm.prank(alice); + positionManager.setCanUpdateUsingAsCollateralPermission(address(spoke1), bob, true); + + vm.prank(bob); + positionManager.renounceCanUpdateUsingAsCollateralPermission(address(spoke1), alice); + vm.snapshotGasLastCall(NAMESPACE, 'renounceCanUpdateUsingAsCollateralPermission'); + } + + function test_renounceCanUpdateUserRiskPremiumPermission() public { + vm.prank(alice); + positionManager.setCanUpdateUserRiskPremiumPermission(address(spoke1), bob, true); + + vm.prank(bob); + positionManager.renounceCanUpdateUserRiskPremiumPermission(address(spoke1), alice); + vm.snapshotGasLastCall(NAMESPACE, 'renounceCanUpdateUserRiskPremiumPermission'); + } + + function test_renounceCanUpdateUserDynamicConfigPermission() public { + vm.prank(alice); + positionManager.setCanUpdateUserDynamicConfigPermission(address(spoke1), bob, true); + + vm.prank(bob); + positionManager.renounceCanUpdateUserDynamicConfigPermission(address(spoke1), alice); + vm.snapshotGasLastCall(NAMESPACE, 'renounceCanUpdateUserDynamicConfigPermission'); + } + + function test_setUsingAsCollateralOnBehalfOf_fuzz_withGlobalPermission() public { + vm.prank(alice); + positionManager.setGlobalPermission(address(spoke1), bob, true); + + vm.prank(bob); + positionManager.setUsingAsCollateralOnBehalfOf( + address(spoke1), + _daiReserveId(spoke1), + true, + alice + ); + vm.snapshotGasLastCall(NAMESPACE, 'setUsingAsCollateralOnBehalfOf'); + } + + function test_updateUserRiskPremiumOnBehalfOf_withGlobalPermission() public { + vm.prank(alice); + positionManager.setGlobalPermission(address(spoke1), bob, true); + + Utils.supplyCollateral(spoke1, _daiReserveId(spoke1), alice, 100e18, alice); + Utils.borrow(spoke1, _daiReserveId(spoke1), alice, 75e18, alice); + + vm.prank(bob); + positionManager.updateUserRiskPremiumOnBehalfOf(address(spoke1), alice); + vm.snapshotGasLastCall(NAMESPACE, 'updateUserRiskPremiumOnBehalfOf'); + } + + function test_updateUserDynamicConfigOnBehalfOf_withGlobalPermission() public { + vm.prank(alice); + positionManager.setGlobalPermission(address(spoke1), bob, true); + + vm.prank(bob); + positionManager.updateUserDynamicConfigOnBehalfOf(address(spoke1), alice); + vm.snapshotGasLastCall(NAMESPACE, 'updateUserDynamicConfigOnBehalfOf'); + } +} diff --git a/tests/mocks/ConfigPermissionsWrapper.sol b/tests/mocks/ConfigPermissionsWrapper.sol new file mode 100644 index 000000000..b456d23b5 --- /dev/null +++ b/tests/mocks/ConfigPermissionsWrapper.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import { + ConfigPermissions, + ConfigPermissionsMap +} from 'src/position-manager/libraries/ConfigPermissionsMap.sol'; + +contract ConfigPermissionsWrapper { + using ConfigPermissionsMap for ConfigPermissions; + + function setFullPermissions(bool status) external pure returns (ConfigPermissions) { + return ConfigPermissionsMap.setFullPermissions(status); + } + + function setCanSetUsingAsCollateral( + ConfigPermissions self, + bool status + ) external pure returns (ConfigPermissions) { + return self.setCanSetUsingAsCollateral(status); + } + + function setCanUpdateUserRiskPremium( + ConfigPermissions self, + bool status + ) external pure returns (ConfigPermissions) { + return self.setCanUpdateUserRiskPremium(status); + } + + function setCanUpdateUserDynamicConfig( + ConfigPermissions self, + bool status + ) external pure returns (ConfigPermissions) { + return self.setCanUpdateUserDynamicConfig(status); + } + + function canSetUsingAsCollateral(ConfigPermissions self) external pure returns (bool) { + return self.canSetUsingAsCollateral(); + } + + function canUpdateUserRiskPremium(ConfigPermissions self) external pure returns (bool) { + return self.canUpdateUserRiskPremium(); + } + + function canUpdateUserDynamicConfig(ConfigPermissions self) external pure returns (bool) { + return self.canUpdateUserDynamicConfig(); + } + + function CAN_SET_USING_AS_COLLATERAL_MASK() external pure returns (uint8) { + return ConfigPermissionsMap.CAN_SET_USING_AS_COLLATERAL_MASK; + } + + function CAN_UPDATE_USER_RISK_PREMIUM_MASK() external pure returns (uint8) { + return ConfigPermissionsMap.CAN_UPDATE_USER_RISK_PREMIUM_MASK; + } + + function CAN_UPDATE_USER_DYNAMIC_CONFIG_MASK() external pure returns (uint8) { + return ConfigPermissionsMap.CAN_UPDATE_USER_DYNAMIC_CONFIG_MASK; + } + + function FULL_PERMISSIONS_MASK() external pure returns (uint8) { + return ConfigPermissionsMap.FULL_PERMISSIONS_MASK; + } +} diff --git a/tests/mocks/EIP712Types.sol b/tests/mocks/EIP712Types.sol index 5b94a1605..1ce971747 100644 --- a/tests/mocks/EIP712Types.sol +++ b/tests/mocks/EIP712Types.sol @@ -88,6 +88,26 @@ library EIP712Types { uint256 deadline; } + struct WithdrawPermit { + address spoke; + uint256 reserveId; + address owner; + address spender; + uint256 amount; + uint256 nonce; + uint256 deadline; + } + + struct BorrowPermit { + address spoke; + uint256 reserveId; + address owner; + address spender; + uint256 amount; + uint256 nonce; + uint256 deadline; + } + /// @dev TokenizationSpoke Intents struct TokenizedDeposit { address depositor; diff --git a/tests/mocks/GatewayBaseWrapper.sol b/tests/mocks/GatewayBaseWrapper.sol deleted file mode 100644 index e35515108..000000000 --- a/tests/mocks/GatewayBaseWrapper.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Copyright (c) 2025 Aave Labs -pragma solidity 0.8.28; - -import {GatewayBase} from 'src/position-manager/GatewayBase.sol'; - -contract GatewayBaseWrapper is GatewayBase { - constructor(address initialOwner_) GatewayBase(initialOwner_) {} -} diff --git a/tests/mocks/JsonBindings.sol b/tests/mocks/JsonBindings.sol index 6919c2c62..831bb003a 100644 --- a/tests/mocks/JsonBindings.sol +++ b/tests/mocks/JsonBindings.sol @@ -60,6 +60,10 @@ library JsonBindings { // prettier-ignore string constant schema_UpdateUserDynamicConfig = "UpdateUserDynamicConfig(address spoke,address onBehalfOf,uint256 nonce,uint256 deadline)"; // prettier-ignore + string constant schema_WithdrawPermit = "WithdrawPermit(address spoke,uint256 reserveId,address owner,address spender,uint256 amount,uint256 nonce,uint256 deadline)"; + // prettier-ignore + string constant schema_BorrowPermit = "BorrowPermit(address spoke,uint256 reserveId,address owner,address spender,uint256 amount,uint256 nonce,uint256 deadline)"; + // prettier-ignore string constant schema_TokenizedDeposit = "TokenizedDeposit(address depositor,uint256 assets,address receiver,uint256 nonce,uint256 deadline)"; // prettier-ignore string constant schema_TokenizedMint = "TokenizedMint(address depositor,uint256 shares,address receiver,uint256 nonce,uint256 deadline)"; @@ -408,6 +412,82 @@ library JsonBindings { ); } + function serialize( + EIP712Types.WithdrawPermit memory value + ) internal pure returns (string memory) { + return vm.serializeJsonType(schema_WithdrawPermit, abi.encode(value)); + } + + function serialize( + EIP712Types.WithdrawPermit memory value, + string memory objectKey, + string memory valueKey + ) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_WithdrawPermit, abi.encode(value)); + } + + function deserializeWithdrawPermit( + string memory json + ) public pure returns (EIP712Types.WithdrawPermit memory) { + return abi.decode(vm.parseJsonType(json, schema_WithdrawPermit), (EIP712Types.WithdrawPermit)); + } + + function deserializeWithdrawPermit( + string memory json, + string memory path + ) public pure returns (EIP712Types.WithdrawPermit memory) { + return + abi.decode(vm.parseJsonType(json, path, schema_WithdrawPermit), (EIP712Types.WithdrawPermit)); + } + + function deserializeWithdrawPermitArray( + string memory json, + string memory path + ) public pure returns (EIP712Types.WithdrawPermit[] memory) { + return + abi.decode( + vm.parseJsonTypeArray(json, path, schema_WithdrawPermit), + (EIP712Types.WithdrawPermit[]) + ); + } + + function serialize(EIP712Types.BorrowPermit memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_BorrowPermit, abi.encode(value)); + } + + function serialize( + EIP712Types.BorrowPermit memory value, + string memory objectKey, + string memory valueKey + ) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_BorrowPermit, abi.encode(value)); + } + + function deserializeBorrowPermit( + string memory json + ) public pure returns (EIP712Types.BorrowPermit memory) { + return abi.decode(vm.parseJsonType(json, schema_BorrowPermit), (EIP712Types.BorrowPermit)); + } + + function deserializeBorrowPermit( + string memory json, + string memory path + ) public pure returns (EIP712Types.BorrowPermit memory) { + return + abi.decode(vm.parseJsonType(json, path, schema_BorrowPermit), (EIP712Types.BorrowPermit)); + } + + function deserializeBorrowPermitArray( + string memory json, + string memory path + ) public pure returns (EIP712Types.BorrowPermit[] memory) { + return + abi.decode( + vm.parseJsonTypeArray(json, path, schema_BorrowPermit), + (EIP712Types.BorrowPermit[]) + ); + } + function serialize( EIP712Types.TokenizedDeposit memory value ) internal pure returns (string memory) { diff --git a/tests/mocks/PositionManagerBaseWrapper.sol b/tests/mocks/PositionManagerBaseWrapper.sol new file mode 100644 index 000000000..cf176b2ef --- /dev/null +++ b/tests/mocks/PositionManagerBaseWrapper.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity 0.8.28; + +import {PositionManagerBase} from 'src/position-manager/PositionManagerBase.sol'; + +contract PositionManagerBaseWrapper is PositionManagerBase { + constructor(address initialOwner_) PositionManagerBase(initialOwner_) {} + + function getReserveUnderlying(address spoke, uint256 reserveId) external view returns (address) { + return address(_getReserveUnderlying(spoke, reserveId)); + } + + function _multicallEnabled() internal pure override returns (bool) { + return true; + } + + function _domainNameAndVersion() internal pure override returns (string memory, string memory) { + return ('PositionManagerBaseWrapper', '1'); + } +} diff --git a/tests/mocks/PositionManagerNoMulticall.sol b/tests/mocks/PositionManagerNoMulticall.sol new file mode 100644 index 000000000..28b12ad1a --- /dev/null +++ b/tests/mocks/PositionManagerNoMulticall.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity 0.8.28; + +import {PositionManagerBase} from 'src/position-manager/PositionManagerBase.sol'; + +contract PositionManagerNoMulticall is PositionManagerBase { + constructor(address initialOwner_) PositionManagerBase(initialOwner_) {} + + function getReserveUnderlying(address spoke, uint256 reserveId) external view returns (address) { + return address(_getReserveUnderlying(spoke, reserveId)); + } + + function _multicallEnabled() internal pure override returns (bool) { + return false; + } + + function _domainNameAndVersion() internal pure override returns (string memory, string memory) { + return ('PositionManagerNoMulticall', '1'); + } +} diff --git a/tests/unit/libraries/SpokeEIP712Hash.t.sol b/tests/unit/libraries/SpokeEIP712Hash.t.sol new file mode 100644 index 000000000..05835bd7f --- /dev/null +++ b/tests/unit/libraries/SpokeEIP712Hash.t.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {Test} from 'forge-std/Test.sol'; + +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {ITokenizationSpoke} from 'src/spoke/interfaces/ITokenizationSpoke.sol'; + +import {EIP712Hash} from 'src/spoke/libraries/EIP712Hash.sol'; + +contract SpokeEIP712HashTest is Test { + using EIP712Hash for *; + + function test_constants() public pure { + assertEq( + EIP712Hash.SET_USER_POSITION_MANAGERS_TYPEHASH, + keccak256( + 'SetUserPositionManagers(address onBehalfOf,PositionManagerUpdate[] updates,uint256 nonce,uint256 deadline)PositionManagerUpdate(address positionManager,bool approve)' + ) + ); + assertEq( + EIP712Hash.SET_USER_POSITION_MANAGERS_TYPEHASH, + vm.eip712HashType('SetUserPositionManagers') + ); + + assertEq( + EIP712Hash.POSITION_MANAGER_UPDATE, + keccak256('PositionManagerUpdate(address positionManager,bool approve)') + ); + assertEq(EIP712Hash.POSITION_MANAGER_UPDATE, vm.eip712HashType('PositionManagerUpdate')); + + assertEq( + EIP712Hash.TOKENIZED_DEPOSIT_TYPEHASH, + keccak256( + 'TokenizedDeposit(address depositor,uint256 assets,address receiver,uint256 nonce,uint256 deadline)' + ) + ); + assertEq(EIP712Hash.TOKENIZED_DEPOSIT_TYPEHASH, vm.eip712HashType('TokenizedDeposit')); + + assertEq( + EIP712Hash.TOKENIZED_MINT_TYPEHASH, + keccak256( + 'TokenizedMint(address depositor,uint256 shares,address receiver,uint256 nonce,uint256 deadline)' + ) + ); + assertEq(EIP712Hash.TOKENIZED_MINT_TYPEHASH, vm.eip712HashType('TokenizedMint')); + + assertEq( + EIP712Hash.TOKENIZED_WITHDRAW_TYPEHASH, + keccak256( + 'TokenizedWithdraw(address owner,uint256 assets,address receiver,uint256 nonce,uint256 deadline)' + ) + ); + assertEq(EIP712Hash.TOKENIZED_WITHDRAW_TYPEHASH, vm.eip712HashType('TokenizedWithdraw')); + + assertEq( + EIP712Hash.TOKENIZED_REDEEM_TYPEHASH, + keccak256( + 'TokenizedRedeem(address owner,uint256 shares,address receiver,uint256 nonce,uint256 deadline)' + ) + ); + assertEq(EIP712Hash.TOKENIZED_REDEEM_TYPEHASH, vm.eip712HashType('TokenizedRedeem')); + } + + function test_hash_setUserPositionManagers_fuzz( + ISpoke.SetUserPositionManagers calldata params + ) public pure { + bytes32[] memory updatesHashes = new bytes32[](params.updates.length); + for (uint256 i = 0; i < updatesHashes.length; ++i) { + updatesHashes[i] = params.updates[i].hash(); + } + + bytes32 expectedHash = keccak256( + abi.encode( + EIP712Hash.SET_USER_POSITION_MANAGERS_TYPEHASH, + params.onBehalfOf, + keccak256(abi.encodePacked(updatesHashes)), + params.nonce, + params.deadline + ) + ); + + assertEq(params.hash(), expectedHash); + assertEq(params.hash(), vm.eip712HashStruct('SetUserPositionManagers', abi.encode(params))); + } + + function test_hash_positionManagerUpdate_fuzz( + ISpoke.PositionManagerUpdate calldata params + ) public pure { + bytes32 expectedHash = keccak256( + abi.encode(EIP712Hash.POSITION_MANAGER_UPDATE, params.positionManager, params.approve) + ); + + assertEq(params.hash(), expectedHash); + assertEq(params.hash(), vm.eip712HashStruct('PositionManagerUpdate', abi.encode(params))); + } + + function test_hash_tokenizedDeposit_fuzz( + ITokenizationSpoke.TokenizedDeposit calldata params + ) public pure { + bytes32 expectedHash = keccak256(abi.encode(EIP712Hash.TOKENIZED_DEPOSIT_TYPEHASH, params)); + assertEq(params.hash(), expectedHash); + assertEq(params.hash(), vm.eip712HashStruct('TokenizedDeposit', abi.encode(params))); + } + + function test_hash_tokenizedMint_fuzz( + ITokenizationSpoke.TokenizedMint calldata params + ) public pure { + bytes32 expectedHash = keccak256(abi.encode(EIP712Hash.TOKENIZED_MINT_TYPEHASH, params)); + assertEq(params.hash(), expectedHash); + assertEq(params.hash(), vm.eip712HashStruct('TokenizedMint', abi.encode(params))); + } + + function test_hash_tokenizedWithdraw_fuzz( + ITokenizationSpoke.TokenizedWithdraw calldata params + ) public pure { + bytes32 expectedHash = keccak256(abi.encode(EIP712Hash.TOKENIZED_WITHDRAW_TYPEHASH, params)); + assertEq(params.hash(), expectedHash); + assertEq(params.hash(), vm.eip712HashStruct('TokenizedWithdraw', abi.encode(params))); + } + + function test_hash_tokenizedRedeem_fuzz( + ITokenizationSpoke.TokenizedRedeem calldata params + ) public pure { + bytes32 expectedHash = keccak256(abi.encode(EIP712Hash.TOKENIZED_REDEEM_TYPEHASH, params)); + assertEq(params.hash(), expectedHash); + assertEq(params.hash(), vm.eip712HashStruct('TokenizedRedeem', abi.encode(params))); + } +} diff --git a/tests/unit/misc/EIP712Hash.t.sol b/tests/unit/misc/EIP712Hash.t.sol deleted file mode 100644 index 3512d7f17..000000000 --- a/tests/unit/misc/EIP712Hash.t.sol +++ /dev/null @@ -1,259 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Copyright (c) 2025 Aave Labs -pragma solidity ^0.8.0; - -import {Test} from 'forge-std/Test.sol'; - -import {ISignatureGateway} from 'src/position-manager/interfaces/ISignatureGateway.sol'; -import {ITokenizationSpoke} from 'src/spoke/interfaces/ITokenizationSpoke.sol'; -import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; - -import {EIP712Hash as PositionManagerEIP712Hash} from 'src/position-manager/libraries/EIP712Hash.sol'; -import {EIP712Hash as SpokeEIP712Hash} from 'src/spoke/libraries/EIP712Hash.sol'; - -contract EIP712HashTest is Test { - using PositionManagerEIP712Hash for *; - using SpokeEIP712Hash for *; - - function test_constants() public pure { - assertEq( - PositionManagerEIP712Hash.SUPPLY_TYPEHASH, - keccak256( - 'Supply(address spoke,uint256 reserveId,uint256 amount,address onBehalfOf,uint256 nonce,uint256 deadline)' - ) - ); - assertEq(PositionManagerEIP712Hash.SUPPLY_TYPEHASH, vm.eip712HashType('Supply')); - - assertEq( - PositionManagerEIP712Hash.WITHDRAW_TYPEHASH, - keccak256( - 'Withdraw(address spoke,uint256 reserveId,uint256 amount,address onBehalfOf,uint256 nonce,uint256 deadline)' - ) - ); - assertEq(PositionManagerEIP712Hash.WITHDRAW_TYPEHASH, vm.eip712HashType('Withdraw')); - - assertEq( - PositionManagerEIP712Hash.BORROW_TYPEHASH, - keccak256( - 'Borrow(address spoke,uint256 reserveId,uint256 amount,address onBehalfOf,uint256 nonce,uint256 deadline)' - ) - ); - assertEq(PositionManagerEIP712Hash.BORROW_TYPEHASH, vm.eip712HashType('Borrow')); - - assertEq( - PositionManagerEIP712Hash.REPAY_TYPEHASH, - keccak256( - 'Repay(address spoke,uint256 reserveId,uint256 amount,address onBehalfOf,uint256 nonce,uint256 deadline)' - ) - ); - assertEq(PositionManagerEIP712Hash.REPAY_TYPEHASH, vm.eip712HashType('Repay')); - - assertEq( - PositionManagerEIP712Hash.SET_USING_AS_COLLATERAL_TYPEHASH, - keccak256( - 'SetUsingAsCollateral(address spoke,uint256 reserveId,bool useAsCollateral,address onBehalfOf,uint256 nonce,uint256 deadline)' - ) - ); - assertEq( - PositionManagerEIP712Hash.SET_USING_AS_COLLATERAL_TYPEHASH, - vm.eip712HashType('SetUsingAsCollateral') - ); - - assertEq( - PositionManagerEIP712Hash.UPDATE_USER_RISK_PREMIUM_TYPEHASH, - keccak256( - 'UpdateUserRiskPremium(address spoke,address onBehalfOf,uint256 nonce,uint256 deadline)' - ) - ); - assertEq( - PositionManagerEIP712Hash.UPDATE_USER_RISK_PREMIUM_TYPEHASH, - vm.eip712HashType('UpdateUserRiskPremium') - ); - - assertEq( - PositionManagerEIP712Hash.UPDATE_USER_DYNAMIC_CONFIG_TYPEHASH, - keccak256( - 'UpdateUserDynamicConfig(address spoke,address onBehalfOf,uint256 nonce,uint256 deadline)' - ) - ); - assertEq( - PositionManagerEIP712Hash.UPDATE_USER_DYNAMIC_CONFIG_TYPEHASH, - vm.eip712HashType('UpdateUserDynamicConfig') - ); - - assertEq( - SpokeEIP712Hash.SET_USER_POSITION_MANAGERS_TYPEHASH, - keccak256( - 'SetUserPositionManagers(address onBehalfOf,PositionManagerUpdate[] updates,uint256 nonce,uint256 deadline)PositionManagerUpdate(address positionManager,bool approve)' - ) - ); - assertEq( - SpokeEIP712Hash.SET_USER_POSITION_MANAGERS_TYPEHASH, - vm.eip712HashType('SetUserPositionManagers') - ); - - assertEq( - SpokeEIP712Hash.POSITION_MANAGER_UPDATE, - keccak256('PositionManagerUpdate(address positionManager,bool approve)') - ); - assertEq(SpokeEIP712Hash.POSITION_MANAGER_UPDATE, vm.eip712HashType('PositionManagerUpdate')); - - assertEq( - SpokeEIP712Hash.TOKENIZED_DEPOSIT_TYPEHASH, - keccak256( - 'TokenizedDeposit(address depositor,uint256 assets,address receiver,uint256 nonce,uint256 deadline)' - ) - ); - assertEq(SpokeEIP712Hash.TOKENIZED_DEPOSIT_TYPEHASH, vm.eip712HashType('TokenizedDeposit')); - - assertEq( - SpokeEIP712Hash.TOKENIZED_MINT_TYPEHASH, - keccak256( - 'TokenizedMint(address depositor,uint256 shares,address receiver,uint256 nonce,uint256 deadline)' - ) - ); - assertEq(SpokeEIP712Hash.TOKENIZED_MINT_TYPEHASH, vm.eip712HashType('TokenizedMint')); - - assertEq( - SpokeEIP712Hash.TOKENIZED_WITHDRAW_TYPEHASH, - keccak256( - 'TokenizedWithdraw(address owner,uint256 assets,address receiver,uint256 nonce,uint256 deadline)' - ) - ); - assertEq(SpokeEIP712Hash.TOKENIZED_WITHDRAW_TYPEHASH, vm.eip712HashType('TokenizedWithdraw')); - - assertEq( - SpokeEIP712Hash.TOKENIZED_REDEEM_TYPEHASH, - keccak256( - 'TokenizedRedeem(address owner,uint256 shares,address receiver,uint256 nonce,uint256 deadline)' - ) - ); - assertEq(SpokeEIP712Hash.TOKENIZED_REDEEM_TYPEHASH, vm.eip712HashType('TokenizedRedeem')); - } - - // @dev all struct params should be hashed & placed in the same order as the typehash - function test_hash_supply_fuzz(ISignatureGateway.Supply calldata params) public pure { - bytes32 expectedHash = keccak256(abi.encode(PositionManagerEIP712Hash.SUPPLY_TYPEHASH, params)); - assertEq(params.hash(), expectedHash); - assertEq(params.hash(), vm.eip712HashStruct('Supply', abi.encode(params))); - } - - function test_hash_withdraw_fuzz(ISignatureGateway.Withdraw calldata params) public pure { - bytes32 expectedHash = keccak256( - abi.encode(PositionManagerEIP712Hash.WITHDRAW_TYPEHASH, params) - ); - assertEq(params.hash(), expectedHash); - assertEq(params.hash(), vm.eip712HashStruct('Withdraw', abi.encode(params))); - } - - function test_hash_borrow_fuzz(ISignatureGateway.Borrow calldata params) public pure { - bytes32 expectedHash = keccak256(abi.encode(PositionManagerEIP712Hash.BORROW_TYPEHASH, params)); - assertEq(params.hash(), expectedHash); - assertEq(params.hash(), vm.eip712HashStruct('Borrow', abi.encode(params))); - } - - function test_hash_repay_fuzz(ISignatureGateway.Repay calldata params) public pure { - bytes32 expectedHash = keccak256(abi.encode(PositionManagerEIP712Hash.REPAY_TYPEHASH, params)); - assertEq(params.hash(), expectedHash); - assertEq(params.hash(), vm.eip712HashStruct('Repay', abi.encode(params))); - } - - function test_hash_setUsingAsCollateral_fuzz( - ISignatureGateway.SetUsingAsCollateral calldata params - ) public pure { - bytes32 expectedHash = keccak256( - abi.encode(PositionManagerEIP712Hash.SET_USING_AS_COLLATERAL_TYPEHASH, params) - ); - assertEq(params.hash(), expectedHash); - assertEq(params.hash(), vm.eip712HashStruct('SetUsingAsCollateral', abi.encode(params))); - } - - function test_hash_updateUserRiskPremium_fuzz( - ISignatureGateway.UpdateUserRiskPremium calldata params - ) public pure { - bytes32 expectedHash = keccak256( - abi.encode(PositionManagerEIP712Hash.UPDATE_USER_RISK_PREMIUM_TYPEHASH, params) - ); - assertEq(params.hash(), expectedHash); - assertEq(params.hash(), vm.eip712HashStruct('UpdateUserRiskPremium', abi.encode(params))); - } - - function test_hash_updateUserDynamicConfig_fuzz( - ISignatureGateway.UpdateUserDynamicConfig calldata params - ) public pure { - bytes32 expectedHash = keccak256( - abi.encode(PositionManagerEIP712Hash.UPDATE_USER_DYNAMIC_CONFIG_TYPEHASH, params) - ); - assertEq(params.hash(), expectedHash); - assertEq(params.hash(), vm.eip712HashStruct('UpdateUserDynamicConfig', abi.encode(params))); - } - - function test_hash_tokenizedDeposit_fuzz( - ITokenizationSpoke.TokenizedDeposit calldata params - ) public pure { - bytes32 expectedHash = keccak256( - abi.encode(SpokeEIP712Hash.TOKENIZED_DEPOSIT_TYPEHASH, params) - ); - assertEq(params.hash(), expectedHash); - assertEq(params.hash(), vm.eip712HashStruct('TokenizedDeposit', abi.encode(params))); - } - - function test_hash_tokenizedMint_fuzz( - ITokenizationSpoke.TokenizedMint calldata params - ) public pure { - bytes32 expectedHash = keccak256(abi.encode(SpokeEIP712Hash.TOKENIZED_MINT_TYPEHASH, params)); - assertEq(params.hash(), expectedHash); - assertEq(params.hash(), vm.eip712HashStruct('TokenizedMint', abi.encode(params))); - } - - function test_hash_tokenizedWithdraw_fuzz( - ITokenizationSpoke.TokenizedWithdraw calldata params - ) public pure { - bytes32 expectedHash = keccak256( - abi.encode(SpokeEIP712Hash.TOKENIZED_WITHDRAW_TYPEHASH, params) - ); - assertEq(params.hash(), expectedHash); - assertEq(params.hash(), vm.eip712HashStruct('TokenizedWithdraw', abi.encode(params))); - } - - function test_hash_tokenizedRedeem_fuzz( - ITokenizationSpoke.TokenizedRedeem calldata params - ) public pure { - bytes32 expectedHash = keccak256(abi.encode(SpokeEIP712Hash.TOKENIZED_REDEEM_TYPEHASH, params)); - assertEq(params.hash(), expectedHash); - assertEq(params.hash(), vm.eip712HashStruct('TokenizedRedeem', abi.encode(params))); - } - - function test_hash_setUserPositionManagers_fuzz( - ISpoke.SetUserPositionManagers calldata params - ) public pure { - bytes32[] memory updatesHashes = new bytes32[](params.updates.length); - for (uint256 i = 0; i < updatesHashes.length; ++i) { - updatesHashes[i] = params.updates[i].hash(); - } - - bytes32 expectedHash = keccak256( - abi.encode( - SpokeEIP712Hash.SET_USER_POSITION_MANAGERS_TYPEHASH, - params.onBehalfOf, - keccak256(abi.encodePacked(updatesHashes)), - params.nonce, - params.deadline - ) - ); - - assertEq(params.hash(), expectedHash); - assertEq(params.hash(), vm.eip712HashStruct('SetUserPositionManagers', abi.encode(params))); - } - - function test_hash_positionManagerUpdate_fuzz( - ISpoke.PositionManagerUpdate calldata params - ) public pure { - bytes32 expectedHash = keccak256( - abi.encode(SpokeEIP712Hash.POSITION_MANAGER_UPDATE, params.positionManager, params.approve) - ); - - assertEq(params.hash(), expectedHash); - assertEq(params.hash(), vm.eip712HashStruct('PositionManagerUpdate', abi.encode(params))); - } -} diff --git a/tests/unit/misc/GatewayBase.t.sol b/tests/unit/misc/GatewayBase.t.sol deleted file mode 100644 index 8a39b0377..000000000 --- a/tests/unit/misc/GatewayBase.t.sol +++ /dev/null @@ -1,111 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Copyright (c) 2025 Aave Labs -pragma solidity ^0.8.0; - -import 'tests/Base.t.sol'; - -contract GatewayBaseTest is Base { - GatewayBaseWrapper public gateway; - - function setUp() public virtual override { - super.setUp(); - initEnvironment(); - - gateway = new GatewayBaseWrapper(address(ADMIN)); - - vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager(address(gateway), true); - } - - function test_constructor() public view { - assertEq(gateway.owner(), address(ADMIN)); - assertEq(gateway.pendingOwner(), address(0)); - - assertEq(gateway.rescueGuardian(), address(ADMIN)); - } - - function test_registerSpoke_fuzz(address newSpoke) public { - vm.assume(newSpoke != address(0)); - assertFalse(gateway.isSpokeRegistered(newSpoke)); - - vm.expectEmit(address(gateway)); - emit IGatewayBase.SpokeRegistered(newSpoke, true); - vm.prank(ADMIN); - gateway.registerSpoke(newSpoke, true); - - assertTrue(gateway.isSpokeRegistered(newSpoke)); - } - - function test_registerSpoke_unregister() public { - assertFalse(gateway.isSpokeRegistered(address(spoke1))); - - vm.expectEmit(address(gateway)); - emit IGatewayBase.SpokeRegistered(address(spoke1), true); - vm.prank(ADMIN); - gateway.registerSpoke(address(spoke1), true); - - assertTrue(gateway.isSpokeRegistered(address(spoke1))); - - vm.expectEmit(address(gateway)); - emit IGatewayBase.SpokeRegistered(address(spoke1), false); - vm.prank(ADMIN); - gateway.registerSpoke(address(spoke1), false); - - assertFalse(gateway.isSpokeRegistered(address(spoke1))); - } - - function test_registerSpoke_revertsWith_OwnableUnauthorizedAccount() public { - address user = vm.randomAddress(); - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, user)); - vm.prank(user); - gateway.registerSpoke(address(spoke1), true); - } - - function test_registerSpoke_revertsWith_InvalidAddress() public { - vm.expectRevert(IGatewayBase.InvalidAddress.selector); - vm.prank(ADMIN); - gateway.registerSpoke(address(0), true); - } - - function test_renouncePositionManagerRole() public { - address user = vm.randomAddress(); - - vm.prank(user); - spoke1.setUserPositionManager(address(gateway), true); - vm.prank(ADMIN); - gateway.registerSpoke(address(spoke1), true); - - assertTrue(spoke1.isPositionManager(user, address(gateway))); - - vm.prank(ADMIN); - gateway.renouncePositionManagerRole(address(spoke1), user); - - assertFalse(spoke1.isPositionManager(user, address(gateway))); - } - - function test_renouncePositionManagerRole_revertsWith_OwnableUnauthorizedAccount() public { - address user = vm.randomAddress(); - - vm.prank(user); - spoke1.setUserPositionManager(address(gateway), true); - vm.prank(ADMIN); - gateway.registerSpoke(address(spoke1), true); - - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, user)); - vm.prank(user); - gateway.renouncePositionManagerRole(address(spoke1), user); - } - - function test_renouncePositionManagerRole_revertsWith_InvalidAddress() public { - address user = vm.randomAddress(); - - vm.prank(user); - spoke1.setUserPositionManager(address(gateway), true); - vm.prank(ADMIN); - gateway.registerSpoke(address(spoke1), true); - - vm.expectRevert(IGatewayBase.InvalidAddress.selector); - vm.prank(ADMIN); - gateway.renouncePositionManagerRole(address(spoke1), address(0)); - } -} diff --git a/tests/unit/position-managers/ConfigPositionManager.t.sol b/tests/unit/position-managers/ConfigPositionManager.t.sol new file mode 100644 index 000000000..fabfb4e3c --- /dev/null +++ b/tests/unit/position-managers/ConfigPositionManager.t.sol @@ -0,0 +1,622 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/unit/Spoke/SpokeBase.t.sol'; + +contract ConfigPositionManagerTest is SpokeBase { + using ConfigPermissionsMap for ConfigPermissions; + + ConfigPositionManager public positionManager; + TestReturnValues public returnValues; + + ConfigPermissions emptyPermissions; + + function setUp() public virtual override { + super.setUp(); + + positionManager = new ConfigPositionManager(address(ADMIN)); + + emptyPermissions = ConfigPermissions.wrap(0); + + vm.prank(SPOKE_ADMIN); + spoke1.updatePositionManager(address(positionManager), true); + + vm.prank(alice); + spoke1.setUserPositionManager(address(positionManager), true); + + vm.prank(ADMIN); + positionManager.registerSpoke(address(spoke1), true); + } + + function test_setGlobalPermission() public { + IConfigPositionManager.ConfigPermissionValues memory permissions = positionManager + .getConfigPermissions(address(spoke1), bob, alice); + assertFalse(permissions.canSetUsingAsCollateral); + assertFalse(permissions.canUpdateUserRiskPremium); + assertFalse(permissions.canUpdateUserDynamicConfig); + + ConfigPermissions newPermissions = emptyPermissions + .setCanSetUsingAsCollateral(true) + .setCanUpdateUserRiskPremium(true) + .setCanUpdateUserDynamicConfig(true); + + vm.expectEmit(address(positionManager)); + emit IConfigPositionManager.ConfigPermissionsUpdated( + address(spoke1), + alice, + bob, + newPermissions + ); + vm.prank(alice); + positionManager.setGlobalPermission(address(spoke1), bob, true); + + permissions = positionManager.getConfigPermissions(address(spoke1), bob, alice); + assertTrue(permissions.canSetUsingAsCollateral); + assertTrue(permissions.canUpdateUserRiskPremium); + assertTrue(permissions.canUpdateUserDynamicConfig); + } + + function test_setGlobalPermission_setThenRemove() public { + IConfigPositionManager.ConfigPermissionValues memory permissions = positionManager + .getConfigPermissions(address(spoke1), bob, alice); + assertFalse(permissions.canSetUsingAsCollateral); + assertFalse(permissions.canUpdateUserRiskPremium); + assertFalse(permissions.canUpdateUserDynamicConfig); + + ConfigPermissions newPermissions = emptyPermissions + .setCanSetUsingAsCollateral(true) + .setCanUpdateUserRiskPremium(true) + .setCanUpdateUserDynamicConfig(true); + + vm.expectEmit(address(positionManager)); + emit IConfigPositionManager.ConfigPermissionsUpdated( + address(spoke1), + alice, + bob, + newPermissions + ); + vm.prank(alice); + positionManager.setGlobalPermission(address(spoke1), bob, true); + + permissions = positionManager.getConfigPermissions(address(spoke1), bob, alice); + assertTrue(permissions.canSetUsingAsCollateral); + assertTrue(permissions.canUpdateUserRiskPremium); + assertTrue(permissions.canUpdateUserDynamicConfig); + + vm.expectEmit(address(positionManager)); + emit IConfigPositionManager.ConfigPermissionsUpdated( + address(spoke1), + alice, + bob, + emptyPermissions + ); + vm.prank(alice); + positionManager.setGlobalPermission(address(spoke1), bob, false); + + permissions = positionManager.getConfigPermissions(address(spoke1), bob, alice); + assertFalse(permissions.canSetUsingAsCollateral); + assertFalse(permissions.canUpdateUserRiskPremium); + assertFalse(permissions.canUpdateUserDynamicConfig); + } + + function test_setGlobalPermission_removeAllPermissions() public { + vm.prank(alice); + positionManager.setGlobalPermission(address(spoke1), bob, true); + + IConfigPositionManager.ConfigPermissionValues memory permissions = positionManager + .getConfigPermissions(address(spoke1), bob, alice); + assertTrue(permissions.canSetUsingAsCollateral); + assertTrue(permissions.canUpdateUserRiskPremium); + assertTrue(permissions.canUpdateUserDynamicConfig); + + ConfigPermissions newPermissions; + + vm.expectEmit(address(positionManager)); + emit IConfigPositionManager.ConfigPermissionsUpdated( + address(spoke1), + alice, + bob, + newPermissions + ); + vm.prank(alice); + positionManager.setGlobalPermission(address(spoke1), bob, false); + + permissions = positionManager.getConfigPermissions(address(spoke1), bob, alice); + assertFalse(permissions.canSetUsingAsCollateral); + assertFalse(permissions.canUpdateUserRiskPremium); + assertFalse(permissions.canUpdateUserDynamicConfig); + } + + function test_setGlobalPermission_removePreviousPermissions() public { + vm.prank(alice); + positionManager.setCanUpdateUsingAsCollateralPermission(address(spoke1), bob, true); + vm.prank(alice); + positionManager.setCanUpdateUserDynamicConfigPermission(address(spoke1), bob, true); + + IConfigPositionManager.ConfigPermissionValues memory permissions = positionManager + .getConfigPermissions(address(spoke1), bob, alice); + assertTrue(permissions.canSetUsingAsCollateral); + assertFalse(permissions.canUpdateUserRiskPremium); + assertTrue(permissions.canUpdateUserDynamicConfig); + + ConfigPermissions newPermissions; + + vm.expectEmit(address(positionManager)); + emit IConfigPositionManager.ConfigPermissionsUpdated( + address(spoke1), + alice, + bob, + newPermissions + ); + vm.prank(alice); + positionManager.setGlobalPermission(address(spoke1), bob, false); + + permissions = positionManager.getConfigPermissions(address(spoke1), bob, alice); + assertFalse(permissions.canSetUsingAsCollateral); + assertFalse(permissions.canUpdateUserRiskPremium); + assertFalse(permissions.canUpdateUserDynamicConfig); + } + + function test_setGlobalPermission_revertsWith_SpokeNotRegistered() public { + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); + vm.prank(alice); + positionManager.setGlobalPermission(address(spoke2), bob, true); + } + + function test_setCanUpdateUsingAsCollateralPermission() public { + assertFalse(_canUpdateUsingAsCollateral(address(spoke1), bob, alice)); + ConfigPermissions newPermissions = emptyPermissions.setCanSetUsingAsCollateral(true); + + vm.expectEmit(address(positionManager)); + emit IConfigPositionManager.ConfigPermissionsUpdated( + address(spoke1), + alice, + bob, + newPermissions + ); + vm.prank(alice); + positionManager.setCanUpdateUsingAsCollateralPermission(address(spoke1), bob, true); + + assertTrue(_canUpdateUsingAsCollateral(address(spoke1), bob, alice)); + } + + function test_setCanUpdateUsingAsCollateralPermission_remove() public { + vm.prank(alice); + positionManager.setCanUpdateUsingAsCollateralPermission(address(spoke1), bob, true); + assertTrue(_canUpdateUsingAsCollateral(address(spoke1), bob, alice)); + + ConfigPermissions newPermissions; + + vm.expectEmit(address(positionManager)); + emit IConfigPositionManager.ConfigPermissionsUpdated( + address(spoke1), + alice, + bob, + newPermissions + ); + vm.prank(alice); + positionManager.setCanUpdateUsingAsCollateralPermission(address(spoke1), bob, false); + + assertFalse(_canUpdateUsingAsCollateral(address(spoke1), bob, alice)); + } + + function test_setCanUpdateUsingAsCollateralPermission_revertsWith_SpokeNotRegistered() public { + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); + vm.prank(alice); + positionManager.setCanUpdateUsingAsCollateralPermission(address(spoke2), bob, true); + } + + function test_setCanUpdateUserRiskPremiumPermission() public { + assertFalse(_canUpdateUserRiskPremium(address(spoke1), bob, alice)); + ConfigPermissions newPermissions = emptyPermissions.setCanUpdateUserRiskPremium(true); + + vm.expectEmit(address(positionManager)); + emit IConfigPositionManager.ConfigPermissionsUpdated( + address(spoke1), + alice, + bob, + newPermissions + ); + vm.prank(alice); + positionManager.setCanUpdateUserRiskPremiumPermission(address(spoke1), bob, true); + + assertTrue(_canUpdateUserRiskPremium(address(spoke1), bob, alice)); + } + + function test_setCanUpdateUserRiskPremiumPermission_remove() public { + vm.prank(alice); + positionManager.setCanUpdateUserRiskPremiumPermission(address(spoke1), bob, true); + assertTrue(_canUpdateUserRiskPremium(address(spoke1), bob, alice)); + + ConfigPermissions newPermissions; + + vm.expectEmit(address(positionManager)); + emit IConfigPositionManager.ConfigPermissionsUpdated( + address(spoke1), + alice, + bob, + newPermissions + ); + vm.prank(alice); + positionManager.setCanUpdateUserRiskPremiumPermission(address(spoke1), bob, false); + + assertFalse(_canUpdateUserRiskPremium(address(spoke1), bob, alice)); + } + + function test_setCanUpdateUserRiskPremiumPermission_revertsWith_SpokeNotRegistered() public { + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); + vm.prank(alice); + positionManager.setCanUpdateUserRiskPremiumPermission(address(spoke2), bob, true); + } + + function test_setCanUpdateUserDynamicConfigPermission() public { + assertFalse(_canUpdateUserDynamicConfig(address(spoke1), bob, alice)); + ConfigPermissions newPermissions = emptyPermissions.setCanUpdateUserDynamicConfig(true); + + vm.expectEmit(address(positionManager)); + emit IConfigPositionManager.ConfigPermissionsUpdated( + address(spoke1), + alice, + bob, + newPermissions + ); + vm.prank(alice); + positionManager.setCanUpdateUserDynamicConfigPermission(address(spoke1), bob, true); + + assertTrue(_canUpdateUserDynamicConfig(address(spoke1), bob, alice)); + } + + function test_setCanUpdateUserDynamicConfigPermission_remove() public { + vm.prank(alice); + positionManager.setCanUpdateUserDynamicConfigPermission(address(spoke1), bob, true); + assertTrue(_canUpdateUserDynamicConfig(address(spoke1), bob, alice)); + + ConfigPermissions newPermissions; + + vm.expectEmit(address(positionManager)); + emit IConfigPositionManager.ConfigPermissionsUpdated( + address(spoke1), + alice, + bob, + newPermissions + ); + vm.prank(alice); + positionManager.setCanUpdateUserDynamicConfigPermission(address(spoke1), bob, false); + + assertFalse(_canUpdateUserDynamicConfig(address(spoke1), bob, alice)); + } + + function test_setCanUpdateUserDynamicConfigPermission_revertsWith_SpokeNotRegistered() public { + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); + vm.prank(alice); + positionManager.setCanUpdateUserDynamicConfigPermission(address(spoke2), bob, true); + } + + function test_renounceGlobalPermission() public { + vm.prank(alice); + positionManager.setGlobalPermission(address(spoke1), bob, true); + + IConfigPositionManager.ConfigPermissionValues memory permissions = positionManager + .getConfigPermissions(address(spoke1), bob, alice); + assertTrue(permissions.canSetUsingAsCollateral); + assertTrue(permissions.canUpdateUserRiskPremium); + assertTrue(permissions.canUpdateUserDynamicConfig); + + ConfigPermissions newPermissions; + + vm.expectEmit(address(positionManager)); + emit IConfigPositionManager.ConfigPermissionsUpdated( + address(spoke1), + alice, + bob, + newPermissions + ); + vm.prank(bob); + positionManager.renounceGlobalPermission(address(spoke1), alice); + + permissions = positionManager.getConfigPermissions(address(spoke1), bob, alice); + assertFalse(permissions.canSetUsingAsCollateral); + assertFalse(permissions.canUpdateUserRiskPremium); + assertFalse(permissions.canUpdateUserDynamicConfig); + } + + function test_renounceGlobalPermission_revertsWith_SpokeNotRegistered() public { + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); + vm.prank(bob); + positionManager.renounceGlobalPermission(address(spoke2), alice); + } + + function test_renounceCanUpdateUsingAsCollateralPermission() public { + vm.prank(alice); + positionManager.setCanUpdateUsingAsCollateralPermission(address(spoke1), bob, true); + + assertTrue(_canUpdateUsingAsCollateral(address(spoke1), bob, alice)); + + ConfigPermissions newPermissions; + + vm.expectEmit(address(positionManager)); + emit IConfigPositionManager.ConfigPermissionsUpdated( + address(spoke1), + alice, + bob, + newPermissions + ); + vm.prank(bob); + positionManager.renounceCanUpdateUsingAsCollateralPermission(address(spoke1), alice); + + assertFalse(_canUpdateUsingAsCollateral(address(spoke1), bob, alice)); + } + + function test_renounceCanUpdateUsingAsCollateralPermission_revertsWith_SpokeNotRegistered() + public + { + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); + vm.prank(bob); + positionManager.renounceCanUpdateUsingAsCollateralPermission(address(spoke2), alice); + } + + function test_renounceCanUpdateUserRiskPremiumPermission() public { + vm.prank(alice); + positionManager.setCanUpdateUserRiskPremiumPermission(address(spoke1), bob, true); + + assertTrue(_canUpdateUserRiskPremium(address(spoke1), bob, alice)); + + ConfigPermissions newPermissions; + + vm.expectEmit(address(positionManager)); + emit IConfigPositionManager.ConfigPermissionsUpdated( + address(spoke1), + alice, + bob, + newPermissions + ); + vm.prank(bob); + positionManager.renounceCanUpdateUserRiskPremiumPermission(address(spoke1), alice); + + assertFalse(_canUpdateUserRiskPremium(address(spoke1), bob, alice)); + } + + function test_renounceCanUpdateUserRiskPremiumPermission_revertsWith_SpokeNotRegistered() public { + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); + vm.prank(bob); + positionManager.renounceCanUpdateUserRiskPremiumPermission(address(spoke2), alice); + } + + function test_renounceCanUpdateUserDynamicConfigPermission() public { + vm.prank(alice); + positionManager.setCanUpdateUserDynamicConfigPermission(address(spoke1), bob, true); + + assertTrue(_canUpdateUserDynamicConfig(address(spoke1), bob, alice)); + + ConfigPermissions newPermissions; + + vm.expectEmit(address(positionManager)); + emit IConfigPositionManager.ConfigPermissionsUpdated( + address(spoke1), + alice, + bob, + newPermissions + ); + vm.prank(bob); + positionManager.renounceCanUpdateUserDynamicConfigPermission(address(spoke1), alice); + + assertFalse(_canUpdateUserDynamicConfig(address(spoke1), bob, alice)); + } + + function test_renounceCanUpdateUserDynamicConfigPermission_revertsWith_SpokeNotRegistered() + public + { + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); + vm.prank(bob); + positionManager.renounceCanUpdateUserDynamicConfigPermission(address(spoke2), alice); + } + + function test_setUsingAsCollateralOnBehalfOf_fuzz_withPermission( + uint256 reserveId, + bool useAsCollateral + ) public { + reserveId = bound(reserveId, 1, spoke1.getReserveCount() - 1); + + vm.prank(alice); + positionManager.setCanUpdateUsingAsCollateralPermission(address(spoke1), bob, true); + + vm.prank(alice); + spoke1.setUsingAsCollateral(reserveId, !useAsCollateral, alice); + + (bool isCollateral, ) = spoke1.getUserReserveStatus(reserveId, alice); + assertEq(isCollateral, !useAsCollateral); + + vm.expectEmit(address(spoke1)); + emit ISpoke.SetUsingAsCollateral(reserveId, address(positionManager), alice, useAsCollateral); + vm.prank(bob); + positionManager.setUsingAsCollateralOnBehalfOf( + address(spoke1), + reserveId, + useAsCollateral, + alice + ); + + (isCollateral, ) = spoke1.getUserReserveStatus(reserveId, alice); + assertEq(isCollateral, useAsCollateral); + } + + function test_setUsingAsCollateralOnBehalfOf_fuzz_withGlobalPermission( + uint256 reserveId, + bool useAsCollateral + ) public { + reserveId = bound(reserveId, 1, spoke1.getReserveCount() - 1); + + vm.prank(alice); + positionManager.setGlobalPermission(address(spoke1), bob, true); + + vm.prank(alice); + spoke1.setUsingAsCollateral(reserveId, !useAsCollateral, alice); + + (bool isCollateral, ) = spoke1.getUserReserveStatus(reserveId, alice); + assertEq(isCollateral, !useAsCollateral); + + vm.expectEmit(address(spoke1)); + emit ISpoke.SetUsingAsCollateral(reserveId, address(positionManager), alice, useAsCollateral); + vm.prank(bob); + positionManager.setUsingAsCollateralOnBehalfOf( + address(spoke1), + reserveId, + useAsCollateral, + alice + ); + + (isCollateral, ) = spoke1.getUserReserveStatus(reserveId, alice); + assertEq(isCollateral, useAsCollateral); + } + + function test_setUsingAsCollateralOnBehalfOf_revertsWith_CallerNotAllowed() public { + vm.expectRevert(IConfigPositionManager.DelegateeNotAllowed.selector); + vm.prank(bob); + positionManager.setUsingAsCollateralOnBehalfOf( + address(spoke1), + _daiReserveId(spoke1), + true, + alice + ); + } + + function test_setUsingAsCollateralOnBehalfOf_revertsWith_SpokeNotRegistered() public { + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); + vm.prank(bob); + positionManager.setUsingAsCollateralOnBehalfOf(address(spoke2), 1, true, alice); + } + + function test_updateUserRiskPremiumOnBehalfOf_withPermission() public { + vm.prank(alice); + positionManager.setCanUpdateUserRiskPremiumPermission(address(spoke1), bob, true); + + Utils.supplyCollateral(spoke1, _daiReserveId(spoke1), alice, 100e18, alice); + Utils.borrow(spoke1, _daiReserveId(spoke1), alice, 75e18, alice); + + vm.expectEmit(address(spoke1)); + emit ISpoke.UpdateUserRiskPremium(alice, _calculateExpectedUserRP(spoke1, alice)); + vm.prank(bob); + positionManager.updateUserRiskPremiumOnBehalfOf(address(spoke1), alice); + } + + function test_updateUserRiskPremiumOnBehalfOf_withGlobalPermission() public { + vm.prank(alice); + positionManager.setGlobalPermission(address(spoke1), bob, true); + + Utils.supplyCollateral(spoke1, _daiReserveId(spoke1), alice, 100e18, alice); + Utils.borrow(spoke1, _daiReserveId(spoke1), alice, 75e18, alice); + + vm.expectEmit(address(spoke1)); + emit ISpoke.UpdateUserRiskPremium(alice, _calculateExpectedUserRP(spoke1, alice)); + vm.prank(bob); + positionManager.updateUserRiskPremiumOnBehalfOf(address(spoke1), alice); + } + + function test_updateUserRiskPremiumOnBehalfOf_revertsWith_CallerNotAllowed() public { + vm.expectRevert(IConfigPositionManager.DelegateeNotAllowed.selector); + vm.prank(bob); + positionManager.updateUserRiskPremiumOnBehalfOf(address(spoke1), alice); + } + + function test_updateUserRiskPremiumOnBehalfOf_revertsWith_SpokeNotRegistered() public { + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); + vm.prank(bob); + positionManager.updateUserRiskPremiumOnBehalfOf(address(spoke2), alice); + } + + function test_updateUserDynamicConfigOnBehalfOf_withPermission() public { + vm.prank(alice); + positionManager.setCanUpdateUserDynamicConfigPermission(address(spoke1), bob, true); + + vm.expectEmit(address(spoke1)); + emit ISpoke.RefreshAllUserDynamicConfig(alice); + vm.prank(bob); + positionManager.updateUserDynamicConfigOnBehalfOf(address(spoke1), alice); + } + + function test_updateUserDynamicConfigOnBehalfOf_withGlobalPermission() public { + vm.prank(alice); + positionManager.setGlobalPermission(address(spoke1), bob, true); + + vm.expectEmit(address(spoke1)); + emit ISpoke.RefreshAllUserDynamicConfig(alice); + vm.prank(bob); + positionManager.updateUserDynamicConfigOnBehalfOf(address(spoke1), alice); + } + + function test_updateUserDynamicConfigOnBehalfOf_revertsWith_CallerNotAllowed() public { + vm.expectRevert(IConfigPositionManager.DelegateeNotAllowed.selector); + vm.prank(bob); + positionManager.updateUserDynamicConfigOnBehalfOf(address(spoke1), alice); + } + + function test_updateUserDynamicConfigOnBehalfOf_revertsWith_SpokeNotRegistered() public { + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); + vm.prank(bob); + positionManager.updateUserDynamicConfigOnBehalfOf(address(spoke2), alice); + } + + function test_multicall() public { + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSignature( + 'setGlobalPermission(address,address,bool)', + address(spoke1), + bob, + true + ); + calls[1] = abi.encodeWithSignature( + 'setGlobalPermission(address,address,bool)', + address(spoke1), + carol, + true + ); + + vm.prank(alice); + bytes[] memory res = positionManager.multicall(calls); + + assertEq(res[0].length, 0); + assertEq(res[1].length, 0); + + IConfigPositionManager.ConfigPermissionValues memory permissions = positionManager + .getConfigPermissions(address(spoke1), bob, alice); + assertTrue(permissions.canSetUsingAsCollateral); + assertTrue(permissions.canUpdateUserRiskPremium); + assertTrue(permissions.canUpdateUserDynamicConfig); + + permissions = positionManager.getConfigPermissions(address(spoke1), carol, alice); + assertTrue(permissions.canSetUsingAsCollateral); + assertTrue(permissions.canUpdateUserRiskPremium); + assertTrue(permissions.canUpdateUserDynamicConfig); + } + + function _canUpdateUsingAsCollateral( + address spoke, + address delegator, + address delegatee + ) internal view returns (bool) { + IConfigPositionManager.ConfigPermissionValues memory permissions = positionManager + .getConfigPermissions(spoke, delegator, delegatee); + return permissions.canSetUsingAsCollateral; + } + + function _canUpdateUserRiskPremium( + address spoke, + address delegator, + address delegatee + ) internal view returns (bool) { + IConfigPositionManager.ConfigPermissionValues memory permissions = positionManager + .getConfigPermissions(spoke, delegator, delegatee); + return permissions.canUpdateUserRiskPremium; + } + + function _canUpdateUserDynamicConfig( + address spoke, + address delegator, + address delegatee + ) internal view returns (bool) { + IConfigPositionManager.ConfigPermissionValues memory permissions = positionManager + .getConfigPermissions(spoke, delegator, delegatee); + return permissions.canUpdateUserDynamicConfig; + } +} diff --git a/tests/unit/position-managers/GiverPositionManager.t.sol b/tests/unit/position-managers/GiverPositionManager.t.sol new file mode 100644 index 000000000..806c13421 --- /dev/null +++ b/tests/unit/position-managers/GiverPositionManager.t.sol @@ -0,0 +1,390 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/unit/Spoke/SpokeBase.t.sol'; + +contract GiverPositionManagerTest is SpokeBase { + GiverPositionManager public positionManager; + TestReturnValues public returnValues; + + function setUp() public virtual override { + super.setUp(); + + positionManager = new GiverPositionManager(address(ADMIN)); + + vm.prank(SPOKE_ADMIN); + spoke1.updatePositionManager(address(positionManager), true); + + vm.prank(alice); + spoke1.setUserPositionManager(address(positionManager), true); + + vm.prank(ADMIN); + positionManager.registerSpoke(address(spoke1), true); + } + + function test_supplyOnBehalfOf() public { + test_supplyOnBehalfOf_fuzz(100e18); + } + + function test_supplyOnBehalfOf_fuzz(uint256 amount) public { + amount = bound(amount, 1, mintAmount_DAI); + + vm.prank(bob); + tokenList.dai.approve(address(positionManager), amount); + + uint256 userBalanceBefore = tokenList.dai.balanceOf(alice); + uint256 callerBalanceBefore = tokenList.dai.balanceOf(bob); + uint256 hubBalanceBefore = tokenList.dai.balanceOf(address(hub1)); + uint256 userSuppliedAmountBefore = spoke1.getUserSuppliedAssets(_daiReserveId(spoke1), alice); + uint256 callerSuppliedAmountBefore = spoke1.getUserSuppliedAssets(_daiReserveId(spoke1), bob); + + vm.expectEmit(address(spoke1)); + emit ISpokeBase.Supply( + _daiReserveId(spoke1), + address(positionManager), + alice, + hub1.previewAddByAssets(daiAssetId, amount), + amount + ); + vm.prank(bob); + (returnValues.shares, returnValues.amount) = positionManager.supplyOnBehalfOf( + address(spoke1), + _daiReserveId(spoke1), + amount, + alice + ); + + assertEq(returnValues.amount, amount); + assertEq(returnValues.shares, hub1.previewAddByAssets(daiAssetId, amount)); + + assertEq(tokenList.dai.balanceOf(alice), userBalanceBefore); + assertEq(tokenList.dai.balanceOf(bob), callerBalanceBefore - amount); + assertEq( + spoke1.getUserSuppliedAssets(_daiReserveId(spoke1), alice), + userSuppliedAmountBefore + amount + ); + assertEq(spoke1.getUserSuppliedAssets(_daiReserveId(spoke1), bob), callerSuppliedAmountBefore); + assertEq(tokenList.dai.balanceOf(address(hub1)), hubBalanceBefore + amount); + assertEq(tokenList.dai.balanceOf(address(positionManager)), 0); + assertEq(tokenList.dai.allowance(address(positionManager), address(hub1)), 0); + } + + function test_supplyOnBehalfOf_revertsWith_SpokeNotRegistered() public { + uint256 reserveId = _randomReserveId(spoke2); + + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); + vm.prank(bob); + positionManager.supplyOnBehalfOf(address(spoke2), reserveId, 100e18, alice); + } + + function test_supplyOnBehalfOf_revertsWith_ReserveNotListed() public { + uint256 reserveId = _randomInvalidReserveId(spoke1); + + vm.expectRevert(ISpoke.ReserveNotListed.selector); + vm.prank(bob); + positionManager.supplyOnBehalfOf(address(spoke1), reserveId, 100e18, alice); + } + + function test_repayOnBehalfOf() public { + test_repayOnBehalfOf_fuzz(50e18); + } + + function test_repayOnBehalfOf_fuzz(uint256 repayAmount) public { + uint256 aliceSupplyAmount = 1000e18; + uint256 bobSupplyAmount = 150e18; + uint256 borrowAmount = 100e18; + repayAmount = bound(repayAmount, 1, borrowAmount); + + Utils.supplyCollateral(spoke1, _daiReserveId(spoke1), alice, aliceSupplyAmount, alice); + Utils.supply(spoke1, _daiReserveId(spoke1), bob, bobSupplyAmount, bob); + Utils.borrow(spoke1, _daiReserveId(spoke1), alice, borrowAmount, alice); + + vm.prank(bob); + tokenList.dai.approve(address(positionManager), repayAmount); + + uint256 userBalanceBefore = tokenList.dai.balanceOf(alice); + uint256 callerBalanceBefore = tokenList.dai.balanceOf(bob); + uint256 hubBalanceBefore = tokenList.dai.balanceOf(address(hub1)); + + (uint256 userDrawnDebt, uint256 userPremiumDebt) = spoke1.getUserDebt( + _daiReserveId(spoke1), + alice + ); + (uint256 baseRestored, ) = _calculateExactRestoreAmount( + userDrawnDebt, + userPremiumDebt, + repayAmount, + daiAssetId + ); + IHubBase.PremiumDelta memory expectedPremiumDelta = _getExpectedPremiumDeltaForRestore( + spoke1, + alice, + _daiReserveId(spoke1), + repayAmount + ); + + vm.expectEmit(address(spoke1)); + emit ISpokeBase.Repay( + _daiReserveId(spoke1), + address(positionManager), + alice, + hub1.previewRestoreByAssets(daiAssetId, baseRestored), + repayAmount, + expectedPremiumDelta + ); + vm.prank(bob); + (returnValues.shares, returnValues.amount) = positionManager.repayOnBehalfOf( + address(spoke1), + _daiReserveId(spoke1), + repayAmount, + alice + ); + + (userDrawnDebt, userPremiumDebt) = spoke1.getUserDebt(_daiReserveId(spoke1), alice); + + assertEq(returnValues.amount, repayAmount); + assertEq(returnValues.shares, hub1.previewRestoreByAssets(daiAssetId, baseRestored)); + + assertEq(userDrawnDebt + userPremiumDebt, borrowAmount - repayAmount); + assertEq(tokenList.dai.balanceOf(address(hub1)), hubBalanceBefore + repayAmount); + assertEq(tokenList.dai.balanceOf(alice), userBalanceBefore); + assertEq(tokenList.dai.balanceOf(bob), callerBalanceBefore - repayAmount); + assertEq(tokenList.dai.balanceOf(address(positionManager)), 0); + assertEq(tokenList.dai.allowance(address(positionManager), address(hub1)), 0); + } + + function test_repayOnBehalfOf_fuzz_withInterest(uint256 repayAmount, uint256 elapsedTime) public { + uint256 borrowAmount = 100e18; + repayAmount = bound(repayAmount, borrowAmount, borrowAmount * 10); + elapsedTime = bound(elapsedTime, 100 days, 400 days); + + Utils.supplyCollateral(spoke1, _daiReserveId(spoke1), alice, 1000e18, alice); + Utils.supply(spoke1, _daiReserveId(spoke1), bob, 150e18, bob); + Utils.borrow(spoke1, _daiReserveId(spoke1), alice, borrowAmount, alice); + + skip(elapsedTime); + + vm.prank(bob); + tokenList.dai.approve(address(positionManager), repayAmount); + + uint256 userBalanceBefore = tokenList.dai.balanceOf(alice); + uint256 callerBalanceBefore = tokenList.dai.balanceOf(bob); + uint256 hubBalanceBefore = tokenList.dai.balanceOf(address(hub1)); + + (uint256 userDrawnDebt, uint256 userPremiumDebt) = spoke1.getUserDebt( + _daiReserveId(spoke1), + alice + ); + (uint256 baseRestored, uint256 premiumRestored) = _calculateExactRestoreAmount( + userDrawnDebt, + userPremiumDebt, + repayAmount, + daiAssetId + ); + + { + IHubBase.PremiumDelta memory expectedPremiumDelta = _getExpectedPremiumDeltaForRestore( + spoke1, + alice, + _daiReserveId(spoke1), + repayAmount + ); + uint256 repaidAmount = _min(userDrawnDebt + userPremiumDebt, repayAmount); + vm.expectEmit(address(spoke1)); + emit ISpokeBase.Repay( + _daiReserveId(spoke1), + address(positionManager), + alice, + hub1.previewRestoreByAssets(daiAssetId, baseRestored), + repaidAmount, + expectedPremiumDelta + ); + vm.prank(bob); + (returnValues.shares, returnValues.amount) = positionManager.repayOnBehalfOf( + address(spoke1), + _daiReserveId(spoke1), + repayAmount, + alice + ); + + assertApproxEqAbs(returnValues.amount, baseRestored + premiumRestored, 1); + assertEq(returnValues.shares, hub1.previewRestoreByAssets(daiAssetId, baseRestored)); + } + + (uint256 newUserDrawnDebt, uint256 newUserPremiumDebt) = spoke1.getUserDebt( + _daiReserveId(spoke1), + alice + ); + + assertApproxEqAbs( + newUserDrawnDebt + newUserPremiumDebt, + userDrawnDebt + userPremiumDebt - (baseRestored + premiumRestored), + 2 + ); + assertApproxEqAbs( + tokenList.dai.balanceOf(address(hub1)), + hubBalanceBefore + (baseRestored + premiumRestored), + 2 + ); + assertEq(tokenList.dai.balanceOf(alice), userBalanceBefore); + assertApproxEqAbs( + tokenList.dai.balanceOf(bob), + callerBalanceBefore - (baseRestored + premiumRestored), + 1 + ); + assertEq(tokenList.dai.balanceOf(address(positionManager)), 0); + assertEq(tokenList.dai.allowance(address(positionManager), address(hub1)), 0); + } + + function test_repayOnBehalfOf_maxRepay() public { + uint256 aliceSupplyAmount = 1000e18; + uint256 bobSupplyAmount = 150e18; + uint256 borrowAmount = 100e18; + uint256 repayAmount = 150e18; + + Utils.supplyCollateral(spoke1, _daiReserveId(spoke1), alice, aliceSupplyAmount, alice); + Utils.supply(spoke1, _daiReserveId(spoke1), bob, bobSupplyAmount, bob); + Utils.borrow(spoke1, _daiReserveId(spoke1), alice, borrowAmount, alice); + + skip(322 days); + + vm.prank(bob); + tokenList.dai.approve(address(positionManager), repayAmount); + + uint256 userBalanceBefore = tokenList.dai.balanceOf(alice); + uint256 callerBalanceBefore = tokenList.dai.balanceOf(bob); + uint256 hubBalanceBefore = tokenList.dai.balanceOf(address(hub1)); + + (uint256 userDrawnDebt, uint256 userPremiumDebt) = spoke1.getUserDebt( + _daiReserveId(spoke1), + alice + ); + (uint256 baseRestored, uint256 premiumRestored) = _calculateExactRestoreAmount( + userDrawnDebt, + userPremiumDebt, + repayAmount, + daiAssetId + ); + uint256 totalRepaid = baseRestored + premiumRestored; + IHubBase.PremiumDelta memory expectedPremiumDelta = _getExpectedPremiumDeltaForRestore( + spoke1, + alice, + _daiReserveId(spoke1), + repayAmount + ); + + vm.expectEmit(address(spoke1)); + emit ISpokeBase.Repay( + _daiReserveId(spoke1), + address(positionManager), + alice, + hub1.previewRestoreByAssets(daiAssetId, baseRestored), + totalRepaid, + expectedPremiumDelta + ); + vm.prank(bob); + (returnValues.shares, returnValues.amount) = positionManager.repayOnBehalfOf( + address(spoke1), + _daiReserveId(spoke1), + repayAmount, + alice + ); + + (userDrawnDebt, userPremiumDebt) = spoke1.getUserDebt(_daiReserveId(spoke1), alice); + + assertEq(returnValues.amount, baseRestored + premiumRestored); + assertEq(returnValues.shares, hub1.previewRestoreByAssets(daiAssetId, baseRestored)); + + assertEq(userDrawnDebt + userPremiumDebt, 0); + assertEq(tokenList.dai.balanceOf(address(hub1)), hubBalanceBefore + totalRepaid); + assertEq(tokenList.dai.balanceOf(alice), userBalanceBefore); + assertEq(tokenList.dai.balanceOf(bob), callerBalanceBefore - totalRepaid); + assertEq(tokenList.dai.balanceOf(address(positionManager)), 0); + assertEq(tokenList.dai.allowance(address(positionManager), address(hub1)), 0); + } + + function test_repayOnBehalfOf_maxRepay_revertsWith_InvalidRepayAmount() public { + uint256 aliceSupplyAmount = 1000e18; + uint256 bobSupplyAmount = 150e18; + uint256 borrowAmount = 100e18; + + Utils.supplyCollateral(spoke1, _daiReserveId(spoke1), alice, aliceSupplyAmount, alice); + Utils.supply(spoke1, _daiReserveId(spoke1), bob, bobSupplyAmount, bob); + Utils.borrow(spoke1, _daiReserveId(spoke1), alice, borrowAmount, alice); + + vm.prank(bob); + tokenList.dai.approve(address(positionManager), UINT256_MAX); + + vm.expectRevert(IGiverPositionManager.NoMaxUintRepayOnBehalfOfAllowed.selector); + vm.prank(bob); + positionManager.repayOnBehalfOf(address(spoke1), _daiReserveId(spoke1), UINT256_MAX, alice); + } + + function test_repayOnBehalfOf_revertsWith_ReserveNotListed() public { + uint256 reserveId = _randomInvalidReserveId(spoke1); + + vm.expectRevert(ISpoke.ReserveNotListed.selector); + vm.prank(bob); + positionManager.repayOnBehalfOf(address(spoke1), reserveId, 100e18, alice); + } + + function test_repayOnBehalfOf_revertsWith_SpokeNotRegistered() public { + uint256 reserveId = _randomReserveId(spoke2); + + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); + vm.prank(bob); + positionManager.repayOnBehalfOf(address(spoke2), reserveId, 100e18, alice); + } + + function test_multicall() public { + uint256 amount = 100e18; + + vm.prank(carol); + spoke1.setUserPositionManager(address(positionManager), true); + + vm.prank(bob); + tokenList.dai.approve(address(positionManager), UINT256_MAX); + + uint256 aliceSuppliedAmountBefore = spoke1.getUserSuppliedAssets(_daiReserveId(spoke1), alice); + uint256 carolSuppliedAmountBefore = spoke1.getUserSuppliedAssets(_daiReserveId(spoke1), carol); + + uint256 expectedShares = hub1.previewAddByAssets(daiAssetId, amount); + + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSignature( + 'supplyOnBehalfOf(address,uint256,uint256,address)', + address(spoke1), + _daiReserveId(spoke1), + amount, + alice + ); + calls[1] = abi.encodeWithSignature( + 'supplyOnBehalfOf(address,uint256,uint256,address)', + address(spoke1), + _daiReserveId(spoke1), + amount, + carol + ); + + vm.prank(bob); + bytes[] memory res = positionManager.multicall(calls); + + (uint256 aliceShares, uint256 aliceAmount) = abi.decode(res[0], (uint256, uint256)); + (uint256 carolShares, uint256 carolAmount) = abi.decode(res[1], (uint256, uint256)); + + assertEq(aliceAmount, amount); + assertEq(carolAmount, amount); + assertEq(aliceShares, expectedShares); + assertEq(carolShares, expectedShares); + + assertEq( + spoke1.getUserSuppliedAssets(_daiReserveId(spoke1), alice), + aliceSuppliedAmountBefore + amount + ); + assertEq( + spoke1.getUserSuppliedAssets(_daiReserveId(spoke1), carol), + carolSuppliedAmountBefore + amount + ); + } +} diff --git a/tests/unit/misc/NativeTokenGateway.t.sol b/tests/unit/position-managers/NativeTokenGateway.t.sol similarity index 95% rename from tests/unit/misc/NativeTokenGateway.t.sol rename to tests/unit/position-managers/NativeTokenGateway.t.sol index dd6105774..b79a33c7e 100644 --- a/tests/unit/misc/NativeTokenGateway.t.sol +++ b/tests/unit/position-managers/NativeTokenGateway.t.sol @@ -26,14 +26,14 @@ contract NativeTokenGatewayTest is SpokeBase { function test_constructor() public { NativeTokenGateway gateway = new NativeTokenGateway(address(tokenList.weth), address(ADMIN)); - assertEq(gateway.NATIVE_WRAPPER(), address(tokenList.weth)); + assertEq(gateway.NATIVE_TOKEN_WRAPPER(), address(tokenList.weth)); assertEq(gateway.owner(), address(ADMIN)); assertEq(gateway.pendingOwner(), address(0)); assertEq(gateway.rescueGuardian(), address(ADMIN)); } function test_constructor_revertsWith_InvalidAddress() public { - vm.expectRevert(IGatewayBase.InvalidAddress.selector); + vm.expectRevert(IPositionManagerBase.InvalidAddress.selector); new NativeTokenGateway(address(0), address(ADMIN)); } @@ -125,17 +125,17 @@ contract NativeTokenGatewayTest is SpokeBase { function test_supplyNative_revertsWith_SpokeNotRegistered() public { uint256 amount = 100e18; - vm.expectRevert(IGatewayBase.SpokeNotRegistered.selector); + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); vm.prank(bob); nativeTokenGateway.supplyNative{value: amount}(address(spoke2), _wethReserveId(spoke1), amount); - vm.expectRevert(IGatewayBase.SpokeNotRegistered.selector); + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); vm.prank(bob); nativeTokenGateway.supplyNative{value: amount}(address(0), _wethReserveId(spoke1), amount); } function test_supplyNative_revertsWith_InvalidAmount() public { - vm.expectRevert(IGatewayBase.InvalidAmount.selector); + vm.expectRevert(IPositionManagerBase.InvalidAmount.selector); vm.prank(bob); nativeTokenGateway.supplyNative{value: 0}(address(spoke1), _wethReserveId(spoke1), 0); } @@ -414,17 +414,17 @@ contract NativeTokenGatewayTest is SpokeBase { function test_withdrawNative_revertsWith_SpokeNotRegistered() public { uint256 amount = 100e18; - vm.expectRevert(IGatewayBase.SpokeNotRegistered.selector); + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); vm.prank(bob); nativeTokenGateway.withdrawNative(address(spoke2), _wethReserveId(spoke1), amount); - vm.expectRevert(IGatewayBase.SpokeNotRegistered.selector); + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); vm.prank(bob); nativeTokenGateway.withdrawNative(address(0), _wethReserveId(spoke1), amount); } function test_withdrawNative_revertsWith_InvalidAmount() public { - vm.expectRevert(IGatewayBase.InvalidAmount.selector); + vm.expectRevert(IPositionManagerBase.InvalidAmount.selector); vm.prank(bob); nativeTokenGateway.withdrawNative(address(spoke1), _wethReserveId(spoke1), 0); } @@ -526,17 +526,17 @@ contract NativeTokenGatewayTest is SpokeBase { function test_borrowNative_revertsWith_SpokeNotRegistered() public { uint256 amount = 100e18; - vm.expectRevert(IGatewayBase.SpokeNotRegistered.selector); + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); vm.prank(bob); nativeTokenGateway.borrowNative(address(spoke2), _wethReserveId(spoke1), amount); - vm.expectRevert(IGatewayBase.SpokeNotRegistered.selector); + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); vm.prank(bob); nativeTokenGateway.borrowNative(address(0), _wethReserveId(spoke1), amount); } function test_borrowNative_revertsWith_InvalidAmount() public { - vm.expectRevert(IGatewayBase.InvalidAmount.selector); + vm.expectRevert(IPositionManagerBase.InvalidAmount.selector); vm.prank(bob); nativeTokenGateway.borrowNative(address(spoke1), _wethReserveId(spoke1), 0); } @@ -792,7 +792,7 @@ contract NativeTokenGatewayTest is SpokeBase { function test_repayNative_revertsWith_SpokeNotRegistered() public { uint256 repayAmount = 5e18; - vm.expectRevert(IGatewayBase.SpokeNotRegistered.selector); + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); vm.prank(bob); nativeTokenGateway.repayNative{value: repayAmount}( address(spoke2), @@ -800,7 +800,7 @@ contract NativeTokenGatewayTest is SpokeBase { repayAmount ); - vm.expectRevert(IGatewayBase.SpokeNotRegistered.selector); + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); vm.prank(bob); nativeTokenGateway.repayNative{value: repayAmount}( address(0), @@ -810,7 +810,7 @@ contract NativeTokenGatewayTest is SpokeBase { } function test_repayNative_revertsWith_InvalidAmount() public { - vm.expectRevert(IGatewayBase.InvalidAmount.selector); + vm.expectRevert(IPositionManagerBase.InvalidAmount.selector); vm.prank(bob); nativeTokenGateway.repayNative{value: 0}(address(spoke1), _wethReserveId(spoke1), 0); } @@ -846,7 +846,7 @@ contract NativeTokenGatewayTest is SpokeBase { function test_receive_revertsWith_UnsupportedAction() public { deal(address(this), 1 ether); - vm.expectRevert(INativeTokenGateway.UnsupportedAction.selector); + vm.expectRevert(IPositionManagerBase.UnsupportedAction.selector); (bool success, ) = address(nativeTokenGateway).call{value: 1 ether}(new bytes(0)); assertTrue(success); } @@ -856,11 +856,19 @@ contract NativeTokenGatewayTest is SpokeBase { bytes memory invalidCall = abi.encode('invalidFunction()'); - vm.expectRevert(INativeTokenGateway.UnsupportedAction.selector); + vm.expectRevert(IPositionManagerBase.UnsupportedAction.selector); (bool success, ) = address(nativeTokenGateway).call{value: 1 ether}(invalidCall); assertTrue(success); } + function test_multicall_revertsWith_UnsupportedAction() public { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSignature('randomFunction()'); + + vm.expectRevert(IPositionManagerBase.UnsupportedAction.selector); + nativeTokenGateway.multicall(calls); + } + function _getUserData(address user) internal view returns (ISpoke.UserPosition memory) { return getUserInfo(spoke1, user, _wethReserveId(spoke1)); } diff --git a/tests/unit/position-managers/PositionManagerBase.t.sol b/tests/unit/position-managers/PositionManagerBase.t.sol new file mode 100644 index 000000000..ac7e51f20 --- /dev/null +++ b/tests/unit/position-managers/PositionManagerBase.t.sol @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/unit/Spoke/SpokeBase.t.sol'; + +contract PositionManagerBaseTest is SpokeBase { + PositionManagerBaseWrapper public positionManager; + PositionManagerNoMulticall public positionManager2; + + function setUp() public virtual override { + super.setUp(); + + positionManager = new PositionManagerBaseWrapper(address(ADMIN)); + positionManager2 = new PositionManagerNoMulticall(address(ADMIN)); + + vm.startPrank(SPOKE_ADMIN); + spoke1.updatePositionManager(address(positionManager), true); + spoke1.updatePositionManager(address(positionManager2), true); + vm.stopPrank(); + } + + function test_constructor() public view { + assertEq(positionManager.owner(), address(ADMIN)); + assertEq(positionManager.pendingOwner(), address(0)); + + assertEq(positionManager.rescueGuardian(), address(ADMIN)); + } + + function test_getReserveUnderlying_fuzz(uint256 reserveId) public view { + reserveId = bound(reserveId, 0, spoke1.getReserveCount() - 1); + address expectedUnderlying = address(_underlying(spoke1, reserveId)); + + assertEq(positionManager.getReserveUnderlying(address(spoke1), reserveId), expectedUnderlying); + } + + function test_getReserveUnderlying_revertsWith_ReserveNotListed() public { + uint256 reserveId = _randomInvalidReserveId(spoke1); + + vm.expectRevert(abi.encodeWithSelector(ISpoke.ReserveNotListed.selector)); + positionManager.getReserveUnderlying(address(spoke1), reserveId); + } + + function test_setSelfAsUserPositionManagerWithSig() public { + ISpoke.PositionManagerUpdate[] memory updates = new ISpoke.PositionManagerUpdate[](1); + updates[0] = ISpoke.PositionManagerUpdate(address(positionManager), true); + + ISpoke.SetUserPositionManagers memory p = ISpoke.SetUserPositionManagers({ + onBehalfOf: alice, + updates: updates, + nonce: spoke1.nonces(address(alice), _randomNonceKey()), // note: this typed sig is forwarded to spoke1 + deadline: _warpBeforeRandomDeadline() + }); + bytes memory signature = _sign(alicePk, _getTypedDataHash(spoke1, p)); + + vm.prank(ADMIN); + positionManager.registerSpoke(address(spoke1), true); + + assertFalse(spoke1.isPositionManager(alice, address(positionManager))); + + vm.expectEmit(address(spoke1)); + emit ISpoke.SetUserPositionManager(alice, address(positionManager), p.updates[0].approve); + + vm.prank(vm.randomAddress()); + positionManager.setSelfAsUserPositionManagerWithSig( + address(spoke1), + p.onBehalfOf, + p.updates[0].approve, + p.nonce, + p.deadline, + signature + ); + + _assertNonceIncrement(ISignatureGateway(address(spoke1)), alice, p.nonce); // note: nonce consumed on spoke1 + assertTrue(spoke1.isPositionManager(alice, address(positionManager))); + } + + function test_permitReserveUnderlying_revertsWith_ReserveNotListed() public { + vm.prank(ADMIN); + positionManager.registerSpoke(address(spoke1), true); + uint256 unlistedReserveId = vm.randomUint(spoke1.getReserveCount() + 1, UINT256_MAX); + vm.expectRevert(ISpoke.ReserveNotListed.selector); + vm.prank(vm.randomAddress()); + positionManager.permitReserveUnderlying( + address(spoke1), + unlistedReserveId, + vm.randomAddress(), + vm.randomUint(), + vm.randomUint(), + uint8(vm.randomUint()), + bytes32(vm.randomUint()), + bytes32(vm.randomUint()) + ); + } + + function test_permitReserveUnderlying_forwards_correct_call() public { + uint256 reserveId = _randomReserveId(spoke1); + address owner = vm.randomAddress(); + address spender = address(positionManager); + uint256 value = vm.randomUint(); + uint256 deadline = vm.randomUint(); + uint8 v = uint8(vm.randomUint()); + bytes32 r = bytes32(vm.randomUint()); + bytes32 s = bytes32(vm.randomUint()); + + vm.prank(ADMIN); + positionManager.registerSpoke(address(spoke1), true); + + vm.expectCall( + address(_underlying(spoke1, reserveId)), + abi.encodeCall(TestnetERC20.permit, (owner, spender, value, deadline, v, r, s)), + 1 + ); + vm.prank(vm.randomAddress()); + positionManager.permitReserveUnderlying( + address(spoke1), + reserveId, + owner, + value, + deadline, + v, + r, + s + ); + } + + function test_permitReserveUnderlying_ignores_permit_reverts() public { + uint256 reserveId = _randomReserveId(spoke1); + address token = address(_underlying(spoke1, reserveId)); + + vm.prank(ADMIN); + positionManager.registerSpoke(address(spoke1), true); + + vm.mockCallRevert(token, TestnetERC20.permit.selector, vm.randomBytes(64)); + + vm.prank(vm.randomAddress()); + positionManager.permitReserveUnderlying( + address(spoke1), + reserveId, + vm.randomAddress(), + vm.randomUint(), + vm.randomUint(), + uint8(vm.randomUint()), + bytes32(vm.randomUint()), + bytes32(vm.randomUint()) + ); + } + + function test_permitReserveUnderlying() public { + (address user, uint256 userPk) = makeAddrAndKey('user'); + uint256 reserveId = _daiReserveId(spoke1); + TestnetERC20 token = TestnetERC20(address(_underlying(spoke1, reserveId))); + + vm.prank(ADMIN); + positionManager.registerSpoke(address(spoke1), true); + + assertEq(token.allowance(user, address(positionManager)), 0); + + EIP712Types.Permit memory params = EIP712Types.Permit({ + owner: user, + spender: address(positionManager), + value: 100e18, + deadline: _warpBeforeRandomDeadline(), + nonce: token.nonces(user) + }); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPk, _getTypedDataHash(token, params)); + + vm.expectEmit(address(token)); + emit IERC20.Approval(user, address(positionManager), params.value); + vm.prank(vm.randomAddress()); + positionManager.permitReserveUnderlying( + address(spoke1), + reserveId, + user, + params.value, + params.deadline, + v, + r, + s + ); + + assertEq(token.allowance(user, address(positionManager)), params.value); + } + + function test_registerSpoke_fuzz(address newSpoke) public { + vm.assume(newSpoke != address(0)); + assertFalse(positionManager.isSpokeRegistered(newSpoke)); + + vm.expectEmit(address(positionManager)); + emit IPositionManagerBase.SpokeRegistered(newSpoke, true); + vm.prank(ADMIN); + positionManager.registerSpoke(newSpoke, true); + + assertTrue(positionManager.isSpokeRegistered(newSpoke)); + } + + function test_registerSpoke_unregister() public { + assertFalse(positionManager.isSpokeRegistered(address(spoke1))); + + vm.expectEmit(address(positionManager)); + emit IPositionManagerBase.SpokeRegistered(address(spoke1), true); + vm.prank(ADMIN); + positionManager.registerSpoke(address(spoke1), true); + + assertTrue(positionManager.isSpokeRegistered(address(spoke1))); + + vm.expectEmit(address(positionManager)); + emit IPositionManagerBase.SpokeRegistered(address(spoke1), false); + vm.prank(ADMIN); + positionManager.registerSpoke(address(spoke1), false); + + assertFalse(positionManager.isSpokeRegistered(address(spoke1))); + } + + function test_registerSpoke_revertsWith_OwnableUnauthorizedAccount() public { + address user = vm.randomAddress(); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, user)); + vm.prank(user); + positionManager.registerSpoke(address(spoke1), true); + } + + function test_registerSpoke_revertsWith_InvalidAddress() public { + vm.expectRevert(IPositionManagerBase.InvalidAddress.selector); + vm.prank(ADMIN); + positionManager.registerSpoke(address(0), true); + } + + function test_multicall_revertsWith_UnsupportedAction() public { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSignature('randomFunction()'); + + vm.expectRevert(IPositionManagerBase.UnsupportedAction.selector); + positionManager2.multicall(calls); + } + + function test_multicall() public { + address spoke2 = makeAddr('spoke2'); + + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSignature('registerSpoke(address,bool)', address(spoke1), true); + calls[1] = abi.encodeWithSignature('registerSpoke(address,bool)', address(spoke2), true); + + vm.prank(ADMIN); + bytes[] memory res = positionManager.multicall(calls); + + assertEq(res[0].length, 0); + assertEq(res[1].length, 0); + + assertTrue(positionManager.isSpokeRegistered(address(spoke1))); + assertTrue(positionManager.isSpokeRegistered(address(spoke2))); + } + + function test_multicall_atomicity_on_revert() public { + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSignature('registerSpoke(address,bool)', address(spoke1), true); + calls[1] = abi.encodeWithSignature('registerSpoke(address,bool)', address(0), true); // will revert + + vm.prank(ADMIN); + vm.expectRevert(IPositionManagerBase.InvalidAddress.selector); + positionManager.multicall(calls); + + assertFalse(positionManager.isSpokeRegistered(address(spoke1))); + } + + function test_renouncePositionManagerRole() public { + address user = vm.randomAddress(); + + vm.prank(user); + spoke1.setUserPositionManager(address(positionManager), true); + vm.prank(ADMIN); + positionManager.registerSpoke(address(spoke1), true); + + assertTrue(spoke1.isPositionManager(user, address(positionManager))); + + vm.prank(ADMIN); + positionManager.renouncePositionManagerRole(address(spoke1), user); + + assertFalse(spoke1.isPositionManager(user, address(positionManager))); + } + + function test_renouncePositionManagerRole_revertsWith_OwnableUnauthorizedAccount() public { + address user = vm.randomAddress(); + + vm.prank(user); + spoke1.setUserPositionManager(address(positionManager), true); + vm.prank(ADMIN); + positionManager.registerSpoke(address(spoke1), true); + + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, user)); + vm.prank(user); + positionManager.renouncePositionManagerRole(address(spoke1), user); + } +} diff --git a/tests/unit/misc/SignatureGateway/SignatureGateway.Base.t.sol b/tests/unit/position-managers/SignatureGateway/SignatureGateway.Base.t.sol similarity index 92% rename from tests/unit/misc/SignatureGateway/SignatureGateway.Base.t.sol rename to tests/unit/position-managers/SignatureGateway/SignatureGateway.Base.t.sol index 4bb9d612a..6c3253a37 100644 --- a/tests/unit/misc/SignatureGateway/SignatureGateway.Base.t.sol +++ b/tests/unit/position-managers/SignatureGateway/SignatureGateway.Base.t.sol @@ -18,7 +18,7 @@ contract SignatureGatewayBaseTest is SpokeBase { function _supplyData( ISpoke spoke, - address who, + address user, uint256 deadline ) internal returns (ISignatureGateway.Supply memory) { return @@ -26,15 +26,15 @@ contract SignatureGatewayBaseTest is SpokeBase { spoke: address(spoke), reserveId: _randomReserveId(spoke), amount: vm.randomUint(1, MAX_SUPPLY_AMOUNT), - onBehalfOf: who, - nonce: gateway.nonces(who, _randomNonceKey()), + onBehalfOf: user, + nonce: gateway.nonces(user, _randomNonceKey()), deadline: deadline }); } function _withdrawData( ISpoke spoke, - address who, + address user, uint256 deadline ) internal returns (ISignatureGateway.Withdraw memory) { return @@ -42,15 +42,15 @@ contract SignatureGatewayBaseTest is SpokeBase { spoke: address(spoke), reserveId: _randomReserveId(spoke), amount: vm.randomUint(1, MAX_SUPPLY_AMOUNT), - onBehalfOf: who, - nonce: gateway.nonces(who, _randomNonceKey()), + onBehalfOf: user, + nonce: gateway.nonces(user, _randomNonceKey()), deadline: deadline }); } function _borrowData( ISpoke spoke, - address who, + address user, uint256 deadline ) internal returns (ISignatureGateway.Borrow memory) { return @@ -58,15 +58,15 @@ contract SignatureGatewayBaseTest is SpokeBase { spoke: address(spoke), reserveId: _randomReserveId(spoke), amount: vm.randomUint(1, MAX_SUPPLY_AMOUNT), - onBehalfOf: who, - nonce: gateway.nonces(who, _randomNonceKey()), + onBehalfOf: user, + nonce: gateway.nonces(user, _randomNonceKey()), deadline: deadline }); } function _repayData( ISpoke spoke, - address who, + address user, uint256 deadline ) internal returns (ISignatureGateway.Repay memory) { return @@ -74,15 +74,15 @@ contract SignatureGatewayBaseTest is SpokeBase { spoke: address(spoke), reserveId: _randomReserveId(spoke), amount: vm.randomUint(1, MAX_SUPPLY_AMOUNT), - onBehalfOf: who, - nonce: gateway.nonces(who, _randomNonceKey()), + onBehalfOf: user, + nonce: gateway.nonces(user, _randomNonceKey()), deadline: deadline }); } function _setAsCollateralData( ISpoke spoke, - address who, + address user, uint256 deadline ) internal returns (ISignatureGateway.SetUsingAsCollateral memory) { return @@ -90,8 +90,8 @@ contract SignatureGatewayBaseTest is SpokeBase { spoke: address(spoke), reserveId: _randomReserveId(spoke), useAsCollateral: vm.randomBool(), - onBehalfOf: who, - nonce: gateway.nonces(who, _randomNonceKey()), + onBehalfOf: user, + nonce: gateway.nonces(user, _randomNonceKey()), deadline: deadline }); } diff --git a/tests/unit/misc/SignatureGateway/SignatureGateway.Constants.t.sol b/tests/unit/position-managers/SignatureGateway/SignatureGateway.Constants.t.sol similarity index 98% rename from tests/unit/misc/SignatureGateway/SignatureGateway.Constants.t.sol rename to tests/unit/position-managers/SignatureGateway/SignatureGateway.Constants.t.sol index eb8fc2087..7114c780a 100644 --- a/tests/unit/misc/SignatureGateway/SignatureGateway.Constants.t.sol +++ b/tests/unit/position-managers/SignatureGateway/SignatureGateway.Constants.t.sol @@ -2,7 +2,7 @@ // Copyright (c) 2025 Aave Labs pragma solidity ^0.8.0; -import 'tests/unit/misc/SignatureGateway/SignatureGateway.Base.t.sol'; +import 'tests/unit/position-managers/SignatureGateway/SignatureGateway.Base.t.sol'; contract SignatureGatewayConstantsTest is SignatureGatewayBaseTest { function test_constructor() public { diff --git a/tests/unit/misc/SignatureGateway/SignatureGateway.PermitReserve.t.sol b/tests/unit/position-managers/SignatureGateway/SignatureGateway.PermitReserve.t.sol similarity index 84% rename from tests/unit/misc/SignatureGateway/SignatureGateway.PermitReserve.t.sol rename to tests/unit/position-managers/SignatureGateway/SignatureGateway.PermitReserve.t.sol index bfb4ab39f..919a18764 100644 --- a/tests/unit/misc/SignatureGateway/SignatureGateway.PermitReserve.t.sol +++ b/tests/unit/position-managers/SignatureGateway/SignatureGateway.PermitReserve.t.sol @@ -2,14 +2,14 @@ // Copyright (c) 2025 Aave Labs pragma solidity ^0.8.0; -import 'tests/unit/misc/SignatureGateway/SignatureGateway.Base.t.sol'; +import 'tests/unit/position-managers/SignatureGateway/SignatureGateway.Base.t.sol'; contract SignatureGatewayPermitReserveTest is SignatureGatewayBaseTest { function test_permitReserve_revertsWith_SpokeNotRegistered() public { uint256 reserveId = _randomReserveId(spoke1); - vm.expectRevert(IGatewayBase.SpokeNotRegistered.selector); + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); vm.prank(vm.randomAddress()); - gateway.permitReserve( + gateway.permitReserveUnderlying( address(spoke2), reserveId, vm.randomAddress(), @@ -25,7 +25,7 @@ contract SignatureGatewayPermitReserveTest is SignatureGatewayBaseTest { uint256 unlistedReserveId = vm.randomUint(spoke1.getReserveCount() + 1, UINT256_MAX); vm.expectRevert(ISpoke.ReserveNotListed.selector); vm.prank(vm.randomAddress()); - gateway.permitReserve( + gateway.permitReserveUnderlying( address(spoke1), unlistedReserveId, vm.randomAddress(), @@ -53,7 +53,7 @@ contract SignatureGatewayPermitReserveTest is SignatureGatewayBaseTest { 1 ); vm.prank(vm.randomAddress()); - gateway.permitReserve(address(spoke1), reserveId, owner, value, deadline, v, r, s); + gateway.permitReserveUnderlying(address(spoke1), reserveId, owner, value, deadline, v, r, s); } function test_permitReserve_ignores_permit_reverts() public { @@ -63,7 +63,7 @@ contract SignatureGatewayPermitReserveTest is SignatureGatewayBaseTest { vm.mockCallRevert(token, TestnetERC20.permit.selector, vm.randomBytes(64)); vm.prank(vm.randomAddress()); - gateway.permitReserve( + gateway.permitReserveUnderlying( address(spoke1), reserveId, vm.randomAddress(), @@ -95,7 +95,16 @@ contract SignatureGatewayPermitReserveTest is SignatureGatewayBaseTest { vm.expectEmit(address(token)); emit IERC20.Approval(user, address(gateway), params.value); vm.prank(vm.randomAddress()); - gateway.permitReserve(address(spoke1), reserveId, user, params.value, params.deadline, v, r, s); + gateway.permitReserveUnderlying( + address(spoke1), + reserveId, + user, + params.value, + params.deadline, + v, + r, + s + ); assertEq(token.allowance(user, address(gateway)), params.value); } diff --git a/tests/unit/misc/SignatureGateway/SignatureGateway.Reverts.InsufficientAllowance.t.sol b/tests/unit/position-managers/SignatureGateway/SignatureGateway.Reverts.InsufficientAllowance.t.sol similarity index 96% rename from tests/unit/misc/SignatureGateway/SignatureGateway.Reverts.InsufficientAllowance.t.sol rename to tests/unit/position-managers/SignatureGateway/SignatureGateway.Reverts.InsufficientAllowance.t.sol index 54cbf1d5b..dc92464e1 100644 --- a/tests/unit/misc/SignatureGateway/SignatureGateway.Reverts.InsufficientAllowance.t.sol +++ b/tests/unit/position-managers/SignatureGateway/SignatureGateway.Reverts.InsufficientAllowance.t.sol @@ -2,7 +2,7 @@ // Copyright (c) 2025 Aave Labs pragma solidity ^0.8.0; -import 'tests/unit/misc/SignatureGateway/SignatureGateway.Base.t.sol'; +import 'tests/unit/position-managers/SignatureGateway/SignatureGateway.Base.t.sol'; contract SignatureGateway_InsufficientAllowance_Test is SignatureGatewayBaseTest { function setUp() public virtual override { diff --git a/tests/unit/misc/SignatureGateway/SignatureGateway.Reverts.InvalidSignature.t.sol b/tests/unit/position-managers/SignatureGateway/SignatureGateway.Reverts.InvalidSignature.t.sol similarity index 99% rename from tests/unit/misc/SignatureGateway/SignatureGateway.Reverts.InvalidSignature.t.sol rename to tests/unit/position-managers/SignatureGateway/SignatureGateway.Reverts.InvalidSignature.t.sol index 5d1f67d1e..5a2156c23 100644 --- a/tests/unit/misc/SignatureGateway/SignatureGateway.Reverts.InvalidSignature.t.sol +++ b/tests/unit/position-managers/SignatureGateway/SignatureGateway.Reverts.InvalidSignature.t.sol @@ -2,7 +2,7 @@ // Copyright (c) 2025 Aave Labs pragma solidity ^0.8.0; -import 'tests/unit/misc/SignatureGateway/SignatureGateway.Base.t.sol'; +import 'tests/unit/position-managers/SignatureGateway/SignatureGateway.Base.t.sol'; contract SignatureGatewayInvalidSignatureTest is SignatureGatewayBaseTest { function test_supplyWithSig_revertsWith_InvalidSignature_dueTo_ExpiredDeadline() public { diff --git a/tests/unit/misc/SignatureGateway/SignatureGateway.Reverts.SpokeNotRegistered.t.sol b/tests/unit/position-managers/SignatureGateway/SignatureGateway.Reverts.SpokeNotRegistered.t.sol similarity index 80% rename from tests/unit/misc/SignatureGateway/SignatureGateway.Reverts.SpokeNotRegistered.t.sol rename to tests/unit/position-managers/SignatureGateway/SignatureGateway.Reverts.SpokeNotRegistered.t.sol index cd9ad6eeb..26f0c9a72 100644 --- a/tests/unit/misc/SignatureGateway/SignatureGateway.Reverts.SpokeNotRegistered.t.sol +++ b/tests/unit/position-managers/SignatureGateway/SignatureGateway.Reverts.SpokeNotRegistered.t.sol @@ -2,7 +2,7 @@ // Copyright (c) 2025 Aave Labs pragma solidity ^0.8.0; -import 'tests/unit/misc/SignatureGateway/SignatureGateway.Base.t.sol'; +import 'tests/unit/position-managers/SignatureGateway/SignatureGateway.Base.t.sol'; contract SignatureGateway_SpokeNotRegistered_Test is SignatureGatewayBaseTest { function setUp() public virtual override { @@ -25,7 +25,7 @@ contract SignatureGateway_SpokeNotRegistered_Test is SignatureGatewayBaseTest { ) public { bytes memory signature = vm.randomBytes(32); - vm.expectRevert(IGatewayBase.SpokeNotRegistered.selector); + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); vm.prank(vm.randomAddress()); gateway.supplyWithSig(p, signature); } @@ -35,7 +35,7 @@ contract SignatureGateway_SpokeNotRegistered_Test is SignatureGatewayBaseTest { ) public { bytes memory signature = vm.randomBytes(32); - vm.expectRevert(IGatewayBase.SpokeNotRegistered.selector); + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); vm.prank(vm.randomAddress()); gateway.withdrawWithSig(p, signature); } @@ -45,7 +45,7 @@ contract SignatureGateway_SpokeNotRegistered_Test is SignatureGatewayBaseTest { ) public { bytes memory signature = vm.randomBytes(32); - vm.expectRevert(IGatewayBase.SpokeNotRegistered.selector); + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); vm.prank(vm.randomAddress()); gateway.borrowWithSig(p, signature); } @@ -55,7 +55,7 @@ contract SignatureGateway_SpokeNotRegistered_Test is SignatureGatewayBaseTest { ) public { bytes memory signature = vm.randomBytes(32); - vm.expectRevert(IGatewayBase.SpokeNotRegistered.selector); + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); vm.prank(vm.randomAddress()); gateway.repayWithSig(p, signature); } @@ -65,7 +65,7 @@ contract SignatureGateway_SpokeNotRegistered_Test is SignatureGatewayBaseTest { ) public { bytes memory signature = vm.randomBytes(32); - vm.expectRevert(IGatewayBase.SpokeNotRegistered.selector); + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); vm.prank(vm.randomAddress()); gateway.setUsingAsCollateralWithSig(p, signature); } @@ -76,7 +76,7 @@ contract SignatureGateway_SpokeNotRegistered_Test is SignatureGatewayBaseTest { bytes memory signature = vm.randomBytes(32); vm.expectRevert( - abi.encodeWithSelector(IGatewayBase.SpokeNotRegistered.selector, address(gateway)) + abi.encodeWithSelector(IPositionManagerBase.SpokeNotRegistered.selector, address(gateway)) ); vm.prank(vm.randomAddress()); gateway.updateUserRiskPremiumWithSig(p, signature); @@ -88,7 +88,7 @@ contract SignatureGateway_SpokeNotRegistered_Test is SignatureGatewayBaseTest { bytes memory signature = vm.randomBytes(32); vm.expectRevert( - abi.encodeWithSelector(IGatewayBase.SpokeNotRegistered.selector, address(gateway)) + abi.encodeWithSelector(IPositionManagerBase.SpokeNotRegistered.selector, address(gateway)) ); vm.prank(vm.randomAddress()); gateway.updateUserDynamicConfigWithSig(p, signature); diff --git a/tests/unit/misc/SignatureGateway/SignatureGateway.Reverts.Unauthorized.t.sol b/tests/unit/position-managers/SignatureGateway/SignatureGateway.Reverts.Unauthorized.t.sol similarity index 97% rename from tests/unit/misc/SignatureGateway/SignatureGateway.Reverts.Unauthorized.t.sol rename to tests/unit/position-managers/SignatureGateway/SignatureGateway.Reverts.Unauthorized.t.sol index f9343ac35..85d37c9c4 100644 --- a/tests/unit/misc/SignatureGateway/SignatureGateway.Reverts.Unauthorized.t.sol +++ b/tests/unit/position-managers/SignatureGateway/SignatureGateway.Reverts.Unauthorized.t.sol @@ -2,7 +2,7 @@ // Copyright (c) 2025 Aave Labs pragma solidity ^0.8.0; -import 'tests/unit/misc/SignatureGateway/SignatureGateway.Base.t.sol'; +import 'tests/unit/position-managers/SignatureGateway/SignatureGateway.Base.t.sol'; contract SignatureGateway_Unauthorized_PositionManagerNotActive_Test is SignatureGatewayBaseTest { function setUp() public virtual override { diff --git a/tests/unit/misc/SignatureGateway/SignatureGateway.SetSelfAsUserPositionManagerWithSig.t.sol b/tests/unit/position-managers/SignatureGateway/SignatureGateway.SetSelfAsUserPositionManagerWithSig.t.sol similarity index 95% rename from tests/unit/misc/SignatureGateway/SignatureGateway.SetSelfAsUserPositionManagerWithSig.t.sol rename to tests/unit/position-managers/SignatureGateway/SignatureGateway.SetSelfAsUserPositionManagerWithSig.t.sol index da565043a..acdf12b94 100644 --- a/tests/unit/misc/SignatureGateway/SignatureGateway.SetSelfAsUserPositionManagerWithSig.t.sol +++ b/tests/unit/position-managers/SignatureGateway/SignatureGateway.SetSelfAsUserPositionManagerWithSig.t.sol @@ -2,11 +2,11 @@ // Copyright (c) 2025 Aave Labs pragma solidity ^0.8.0; -import 'tests/unit/misc/SignatureGateway/SignatureGateway.Base.t.sol'; +import 'tests/unit/position-managers/SignatureGateway/SignatureGateway.Base.t.sol'; contract SignatureGatewaySetSelfAsUserPositionManagerTest is SignatureGatewayBaseTest { function test_setSelfAsUserPositionManagerWithSig_revertsWith_SpokeNotRegistered() public { - vm.expectRevert(IGatewayBase.SpokeNotRegistered.selector); + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); vm.prank(vm.randomAddress()); gateway.setSelfAsUserPositionManagerWithSig({ spoke: address(spoke2), diff --git a/tests/unit/misc/SignatureGateway/SignatureGateway.t.sol b/tests/unit/position-managers/SignatureGateway/SignatureGateway.t.sol similarity index 68% rename from tests/unit/misc/SignatureGateway/SignatureGateway.t.sol rename to tests/unit/position-managers/SignatureGateway/SignatureGateway.t.sol index 7a0d19e52..07238eace 100644 --- a/tests/unit/misc/SignatureGateway/SignatureGateway.t.sol +++ b/tests/unit/position-managers/SignatureGateway/SignatureGateway.t.sol @@ -2,7 +2,7 @@ // Copyright (c) 2025 Aave Labs pragma solidity ^0.8.0; -import 'tests/unit/misc/SignatureGateway/SignatureGateway.Base.t.sol'; +import 'tests/unit/position-managers/SignatureGateway/SignatureGateway.Base.t.sol'; contract SignatureGatewayTest is SignatureGatewayBaseTest { using SafeCast for *; @@ -43,10 +43,10 @@ contract SignatureGatewayTest is SignatureGatewayBaseTest { } function test_renouncePositionManagerRole() public { - address who = vm.randomAddress(); - vm.expectCall(address(spoke1), abi.encodeCall(ISpoke.renouncePositionManagerRole, (who))); + address user = vm.randomAddress(); + vm.expectCall(address(spoke1), abi.encodeCall(ISpoke.renouncePositionManagerRole, (user))); vm.prank(ADMIN); - gateway.renouncePositionManagerRole(address(spoke1), who); + gateway.renouncePositionManagerRole(address(spoke1), user); } function test_supplyWithSig() public { @@ -264,4 +264,110 @@ contract SignatureGatewayTest is SignatureGatewayBaseTest { _assertGatewayHasNoBalanceOrAllowance(spoke1, gateway, alice); _assertGatewayHasNoActivePosition(spoke1, gateway); } + + function test_multicall() public { + uint256 deadline = _warpBeforeRandomDeadline(); + uint256 reserveId = _daiReserveId(spoke1); + + ISignatureGateway.Supply memory p = _supplyData(spoke1, alice, deadline); + p.reserveId = reserveId; + p.nonce = _burnRandomNoncesAtKey(gateway, p.onBehalfOf); + bytes memory signature = _sign(alicePk, _getTypedDataHash(gateway, p)); + Utils.approve(spoke1, p.reserveId, alice, address(gateway), p.amount); + + uint256 expectedShares = _hub(spoke1, reserveId).previewAddByAssets( + _reserveAssetId(spoke1, reserveId), + p.amount + ); + + ISignatureGateway.SetUsingAsCollateral memory p2 = _setAsCollateralData( + spoke1, + alice, + deadline + ); + p2.nonce = _getNextNoncePacked(p.nonce); + p2.reserveId = reserveId; + bytes memory signature2 = _sign(alicePk, _getTypedDataHash(gateway, p2)); + + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeCall(gateway.supplyWithSig, (p, signature)); + calls[1] = abi.encodeCall(gateway.setUsingAsCollateralWithSig, (p2, signature2)); + + bytes[] memory res = gateway.multicall(calls); + + (uint256 returnedShares, uint256 returnedAmount) = abi.decode(res[0], (uint256, uint256)); + assertEq(returnedShares, expectedShares); + assertEq(returnedAmount, p.amount); + assertEq(res[1].length, 0); // setUsingAsCollateralWithSig has no return values + + _assertNonceIncrement(gateway, alice, p2.nonce); + _assertGatewayHasNoBalanceOrAllowance(spoke1, gateway, alice); + _assertGatewayHasNoActivePosition(spoke1, gateway); + } + + function test_multicall_atomicity_on_revert() public { + uint256 deadline = _warpBeforeRandomDeadline(); + uint256 reserveId = _daiReserveId(spoke1); + + ISignatureGateway.Supply memory p1 = _supplyData(spoke1, alice, deadline); + p1.reserveId = reserveId; + p1.nonce = _burnRandomNoncesAtKey(gateway, p1.onBehalfOf); + bytes memory sig1 = _sign(alicePk, _getTypedDataHash(gateway, p1)); + Utils.approve(spoke1, p1.reserveId, alice, address(gateway), p1.amount); + + ISignatureGateway.Supply memory p2 = _supplyData(spoke1, alice, deadline); + p2.reserveId = reserveId; + p2.nonce = _getNextNoncePacked(p1.nonce); + bytes memory sig2 = _sign(bobPk, _getTypedDataHash(gateway, p2)); + Utils.approve(spoke1, p2.reserveId, alice, address(gateway), p2.amount); + + uint256 balanceBefore = _underlying(spoke1, reserveId).balanceOf(alice); + + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeCall(gateway.supplyWithSig, (p1, sig1)); + calls[1] = abi.encodeCall(gateway.supplyWithSig, (p2, sig2)); + + vm.expectRevert(IIntentConsumer.InvalidSignature.selector); + gateway.multicall(calls); + + assertEq(_underlying(spoke1, reserveId).balanceOf(alice), balanceBefore); + assertEq(spoke1.getUserSuppliedShares(reserveId, alice), 0); + _assertGatewayHasNoActivePosition(spoke1, gateway); + } + + function test_multicall_no_atomicity_with_trycatch() public { + uint256 deadline = _warpBeforeRandomDeadline(); + uint256 reserveId = _daiReserveId(spoke1); + + ISignatureGateway.Supply memory p = _supplyData(spoke1, alice, deadline); + p.reserveId = reserveId; + p.nonce = _burnRandomNoncesAtKey(gateway, p.onBehalfOf); + bytes memory signature = _sign(alicePk, _getTypedDataHash(gateway, p)); + Utils.approve(spoke1, p.reserveId, alice, address(gateway), p.amount); + + uint256 expectedShares = _hub(spoke1, reserveId).previewAddByAssets( + _reserveAssetId(spoke1, reserveId), + p.amount + ); + + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeCall( + gateway.permitReserveUnderlying, + (address(spoke1), reserveId, alice, 100e18, deadline, uint8(0), bytes32(0), bytes32(0)) + ); + calls[1] = abi.encodeCall(gateway.supplyWithSig, (p, signature)); + + bytes[] memory res = gateway.multicall(calls); + + assertEq(res[0].length, 0); + (uint256 returnedShares, uint256 returnedAmount) = abi.decode(res[1], (uint256, uint256)); + assertEq(returnedShares, expectedShares); + assertEq(returnedAmount, p.amount); + + assertEq(_underlying(spoke1, reserveId).allowance(alice, address(gateway)), 0); + + _assertNonceIncrement(gateway, alice, p.nonce); + _assertGatewayHasNoBalanceOrAllowance(spoke1, gateway, alice); + _assertGatewayHasNoActivePosition(spoke1, gateway); + } } diff --git a/tests/unit/position-managers/TakerPositionManager/TakerPositionManager.Base.t.sol b/tests/unit/position-managers/TakerPositionManager/TakerPositionManager.Base.t.sol new file mode 100644 index 000000000..d2a731a68 --- /dev/null +++ b/tests/unit/position-managers/TakerPositionManager/TakerPositionManager.Base.t.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/unit/Spoke/SpokeBase.t.sol'; + +contract TakerPositionManagerBaseTest is SpokeBase { + TakerPositionManager public positionManager; + TestReturnValues public returnValues; + + function setUp() public virtual override { + super.setUp(); + + positionManager = new TakerPositionManager(address(ADMIN)); + + vm.prank(SPOKE_ADMIN); + spoke1.updatePositionManager(address(positionManager), true); + + vm.prank(alice); + spoke1.setUserPositionManager(address(positionManager), true); + + vm.prank(ADMIN); + positionManager.registerSpoke(address(spoke1), true); + } + + function _withdrawPermitData( + address spender, + address onBehalfOf, + uint256 deadline + ) internal returns (ITakerPositionManager.WithdrawPermit memory) { + return + ITakerPositionManager.WithdrawPermit({ + spoke: address(spoke1), + reserveId: _randomReserveId(spoke1), + owner: onBehalfOf, + spender: spender, + amount: vm.randomUint(1, MAX_SUPPLY_AMOUNT), + nonce: positionManager.nonces(onBehalfOf, _randomNonceKey()), + deadline: deadline + }); + } + + function _approveBorrowData( + address spender, + address onBehalfOf, + uint256 deadline + ) internal returns (ITakerPositionManager.BorrowPermit memory) { + return + ITakerPositionManager.BorrowPermit({ + spoke: address(spoke1), + reserveId: _randomReserveId(spoke1), + owner: onBehalfOf, + spender: spender, + amount: vm.randomUint(1, MAX_SUPPLY_AMOUNT), + nonce: positionManager.nonces(onBehalfOf, _randomNonceKey()), + deadline: deadline + }); + } + + function _getTypedDataHash( + ITakerPositionManager _positionManager, + ITakerPositionManager.WithdrawPermit memory _params + ) internal view returns (bytes32) { + return + _typedDataHash(_positionManager, vm.eip712HashStruct('WithdrawPermit', abi.encode(_params))); + } + + function _getTypedDataHash( + ITakerPositionManager _positionManager, + ITakerPositionManager.BorrowPermit memory _params + ) internal view returns (bytes32) { + return + _typedDataHash(_positionManager, vm.eip712HashStruct('BorrowPermit', abi.encode(_params))); + } + + function _typedDataHash( + ITakerPositionManager _positionManager, + bytes32 typeHash + ) internal view returns (bytes32) { + return keccak256(abi.encodePacked('\x19\x01', _positionManager.DOMAIN_SEPARATOR(), typeHash)); + } +} diff --git a/tests/unit/position-managers/TakerPositionManager/TakerPositionManager.Permit.t.sol b/tests/unit/position-managers/TakerPositionManager/TakerPositionManager.Permit.t.sol new file mode 100644 index 000000000..7ba13ccb7 --- /dev/null +++ b/tests/unit/position-managers/TakerPositionManager/TakerPositionManager.Permit.t.sol @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/unit/position-managers/TakerPositionManager/TakerPositionManager.Base.t.sol'; + +contract TakerPositionManagerPermitTest is TakerPositionManagerBaseTest { + function setUp() public virtual override { + super.setUp(); + } + + function test_eip712Domain() public { + TakerPositionManager instance = new TakerPositionManager{salt: bytes32(vm.randomUint())}( + vm.randomAddress() + ); + ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ) = IERC5267(address(instance)).eip712Domain(); + + assertEq(fields, bytes1(0x0f)); + assertEq(name, 'TakerPositionManager'); + assertEq(version, '1'); + assertEq(chainId, block.chainid); + assertEq(verifyingContract, address(instance)); + assertEq(salt, bytes32(0)); + assertEq(extensions.length, 0); + } + + function test_DOMAIN_SEPARATOR() public { + TakerPositionManager instance = new TakerPositionManager{salt: bytes32(vm.randomUint())}( + vm.randomAddress() + ); + bytes32 expectedDomainSeparator = keccak256( + abi.encode( + keccak256( + 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' + ), + keccak256('TakerPositionManager'), + keccak256('1'), + block.chainid, + address(instance) + ) + ); + assertEq(instance.DOMAIN_SEPARATOR(), expectedDomainSeparator); + } + + function test_withdrawPermit_typeHash() public view { + assertEq(positionManager.WITHDRAW_PERMIT_TYPEHASH(), vm.eip712HashType('WithdrawPermit')); + assertEq( + positionManager.WITHDRAW_PERMIT_TYPEHASH(), + keccak256( + 'WithdrawPermit(address spoke,uint256 reserveId,address owner,address spender,uint256 amount,uint256 nonce,uint256 deadline)' + ) + ); + } + + function test_borrowPermit_typeHash() public view { + assertEq(positionManager.BORROW_PERMIT_TYPEHASH(), vm.eip712HashType('BorrowPermit')); + assertEq( + positionManager.BORROW_PERMIT_TYPEHASH(), + keccak256( + 'BorrowPermit(address spoke,uint256 reserveId,address owner,address spender,uint256 amount,uint256 nonce,uint256 deadline)' + ) + ); + } + + function test_approveWithdrawWithSig_fuzz( + address spender, + uint256 reserveId, + uint256 amount + ) public { + vm.assume(spender != address(0)); + reserveId = bound(reserveId, 0, spoke1.getReserveCount() - 1); + amount = bound(amount, 1, mintAmount_DAI); + + ITakerPositionManager.WithdrawPermit memory p = _withdrawPermitData( + spender, + alice, + _warpBeforeRandomDeadline() + ); + p.amount = amount; + p.reserveId = reserveId; + p.nonce = _burnRandomNoncesAtKey(positionManager, alice); + bytes memory signature = _sign(alicePk, _getTypedDataHash(positionManager, p)); + + vm.expectEmit(address(positionManager)); + emit ITakerPositionManager.WithdrawApproval(address(spoke1), reserveId, alice, spender, amount); + vm.prank(vm.randomAddress()); + positionManager.approveWithdrawWithSig(p, signature); + + assertEq(positionManager.withdrawAllowance(address(spoke1), reserveId, alice, spender), amount); + } + + function test_approveWithdrawWithSig_revertsWith_InvalidSignature_dueTo_ExpiredDeadline() public { + ITakerPositionManager.WithdrawPermit memory p = _withdrawPermitData( + vm.randomAddress(), + alice, + _warpAfterRandomDeadline() + ); + bytes memory signature = _sign(alicePk, _getTypedDataHash(positionManager, p)); + + vm.expectRevert(IIntentConsumer.InvalidSignature.selector); + vm.prank(vm.randomAddress()); + positionManager.approveWithdrawWithSig(p, signature); + } + + function test_approveWithdrawWithSig_revertsWith_InvalidSignature_dueTo_InvalidSigner() public { + (address randomUser, uint256 randomUserPk) = makeAddrAndKey(string(vm.randomBytes(32))); + address onBehalfOf = vm.randomAddress(); + while (onBehalfOf == randomUser) onBehalfOf = vm.randomAddress(); + + ITakerPositionManager.WithdrawPermit memory p = _withdrawPermitData( + randomUser, + onBehalfOf, + _warpAfterRandomDeadline() + ); + bytes memory signature = _sign(randomUserPk, _getTypedDataHash(positionManager, p)); + + vm.expectRevert(IIntentConsumer.InvalidSignature.selector); + vm.prank(vm.randomAddress()); + positionManager.approveWithdrawWithSig(p, signature); + } + + function test_approveWithdrawWithSig_revertsWith_InvalidAccountNonce(bytes32) public { + ITakerPositionManager.WithdrawPermit memory p = _withdrawPermitData( + vm.randomAddress(), + alice, + _warpBeforeRandomDeadline() + ); + uint192 nonceKey = _randomNonceKey(); + uint256 currentNonce = _burnRandomNoncesAtKey(positionManager, p.owner, nonceKey); + p.nonce = _getRandomInvalidNonceAtKey(positionManager, p.owner, nonceKey); + + bytes memory signature = _sign(alicePk, _getTypedDataHash(positionManager, p)); + + vm.expectRevert( + abi.encodeWithSelector(INoncesKeyed.InvalidAccountNonce.selector, p.owner, currentNonce) + ); + vm.prank(vm.randomAddress()); + positionManager.approveWithdrawWithSig(p, signature); + } + + function test_approveWithdrawWithSig_revertsWith_SpokeNotRegistered() public { + ITakerPositionManager.WithdrawPermit memory p = _withdrawPermitData( + bob, + alice, + _warpBeforeRandomDeadline() + ); + p.spoke = address(spoke2); + p.nonce = _burnRandomNoncesAtKey(positionManager, alice); + bytes memory signature = _sign(alicePk, _getTypedDataHash(positionManager, p)); + + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); + vm.prank(alice); + positionManager.approveWithdrawWithSig(p, signature); + } + + function test_approveBorrowWithSig_fuzz( + address spender, + uint256 reserveId, + uint256 amount + ) public { + vm.assume(spender != address(0)); + reserveId = bound(reserveId, 0, spoke1.getReserveCount() - 1); + amount = bound(amount, 1, mintAmount_DAI); + + ITakerPositionManager.BorrowPermit memory p = _approveBorrowData( + spender, + alice, + _warpBeforeRandomDeadline() + ); + p.amount = amount; + p.reserveId = reserveId; + p.nonce = _burnRandomNoncesAtKey(positionManager, alice); + bytes memory signature = _sign(alicePk, _getTypedDataHash(positionManager, p)); + + vm.expectEmit(address(positionManager)); + emit ITakerPositionManager.BorrowApproval(address(spoke1), reserveId, alice, spender, amount); + vm.prank(vm.randomAddress()); + positionManager.approveBorrowWithSig(p, signature); + + assertEq(positionManager.borrowAllowance(address(spoke1), reserveId, alice, spender), amount); + } + + function test_approveBorrowWithSig_revertsWith_InvalidSignature_dueTo_ExpiredDeadline() public { + ITakerPositionManager.BorrowPermit memory p = _approveBorrowData( + vm.randomAddress(), + alice, + _warpAfterRandomDeadline() + ); + bytes memory signature = _sign(alicePk, _getTypedDataHash(positionManager, p)); + + vm.expectRevert(IIntentConsumer.InvalidSignature.selector); + vm.prank(vm.randomAddress()); + positionManager.approveBorrowWithSig(p, signature); + } + + function test_approveBorrowWithSig_revertsWith_InvalidSignature_dueTo_InvalidSigner() public { + (address randomUser, uint256 randomUserPk) = makeAddrAndKey(string(vm.randomBytes(32))); + address onBehalfOf = vm.randomAddress(); + while (onBehalfOf == randomUser) onBehalfOf = vm.randomAddress(); + + ITakerPositionManager.BorrowPermit memory p = _approveBorrowData( + randomUser, + onBehalfOf, + _warpAfterRandomDeadline() + ); + bytes memory signature = _sign(randomUserPk, _getTypedDataHash(positionManager, p)); + + vm.expectRevert(IIntentConsumer.InvalidSignature.selector); + vm.prank(vm.randomAddress()); + positionManager.approveBorrowWithSig(p, signature); + } + + function test_approveBorrowWithSig_revertsWith_InvalidAccountNonce(bytes32) public { + ITakerPositionManager.BorrowPermit memory p = _approveBorrowData( + vm.randomAddress(), + alice, + _warpBeforeRandomDeadline() + ); + uint192 nonceKey = _randomNonceKey(); + uint256 currentNonce = _burnRandomNoncesAtKey(positionManager, p.owner, nonceKey); + p.nonce = _getRandomInvalidNonceAtKey(positionManager, p.owner, nonceKey); + + bytes memory signature = _sign(alicePk, _getTypedDataHash(positionManager, p)); + + vm.expectRevert( + abi.encodeWithSelector(INoncesKeyed.InvalidAccountNonce.selector, p.owner, currentNonce) + ); + vm.prank(vm.randomAddress()); + positionManager.approveBorrowWithSig(p, signature); + } + + function test_approveBorrowWithSig_revertsWith_SpokeNotRegistered() public { + ITakerPositionManager.BorrowPermit memory p = _approveBorrowData( + bob, + alice, + _warpBeforeRandomDeadline() + ); + p.spoke = address(spoke2); + p.nonce = _burnRandomNoncesAtKey(positionManager, alice); + bytes memory signature = _sign(alicePk, _getTypedDataHash(positionManager, p)); + + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); + vm.prank(alice); + positionManager.approveBorrowWithSig(p, signature); + } +} diff --git a/tests/unit/position-managers/TakerPositionManager/TakerPositionManager.t.sol b/tests/unit/position-managers/TakerPositionManager/TakerPositionManager.t.sol new file mode 100644 index 000000000..bcba99263 --- /dev/null +++ b/tests/unit/position-managers/TakerPositionManager/TakerPositionManager.t.sol @@ -0,0 +1,639 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/unit/position-managers/TakerPositionManager/TakerPositionManager.Base.t.sol'; + +contract TakerPositionManagerTest is TakerPositionManagerBaseTest { + function setUp() public virtual override { + super.setUp(); + } + + function test_approveWithdraw_fuzz(address spender, uint256 reserveId, uint256 amount) public { + vm.assume(spender != address(0)); + reserveId = bound(reserveId, 0, spoke1.getReserveCount() - 1); + amount = bound(amount, 1, mintAmount_DAI); + + vm.expectEmit(address(positionManager)); + emit ITakerPositionManager.WithdrawApproval(address(spoke1), reserveId, alice, spender, amount); + vm.prank(alice); + positionManager.approveWithdraw(address(spoke1), reserveId, spender, amount); + + assertEq(positionManager.withdrawAllowance(address(spoke1), reserveId, alice, spender), amount); + } + + function test_approveWithdraw_revertsWith_SpokeNotRegistered() public { + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); + vm.prank(alice); + positionManager.approveWithdraw(address(spoke2), 1, bob, 100e18); + } + + function test_renounceWithdrawAllowance_fuzz(uint256 initialAllowance) public { + uint256 reserveId = _randomReserveId(spoke1); + initialAllowance = bound(initialAllowance, 1, mintAmount_DAI); + + vm.prank(alice); + positionManager.approveWithdraw(address(spoke1), reserveId, bob, initialAllowance); + + vm.expectEmit(address(positionManager)); + emit ITakerPositionManager.WithdrawApproval(address(spoke1), reserveId, alice, bob, 0); + vm.prank(bob); + positionManager.renounceWithdrawAllowance(address(spoke1), reserveId, alice); + + assertEq(positionManager.withdrawAllowance(address(spoke1), reserveId, alice, bob), 0); + } + + function test_renounceWithdrawAllowance_noop_alreadyRenounced() public { + uint256 reserveId = _randomReserveId(spoke1); + + vm.prank(alice); + positionManager.approveWithdraw(address(spoke1), reserveId, bob, 100e18); + vm.prank(bob); + positionManager.renounceWithdrawAllowance(address(spoke1), reserveId, alice); + + vm.recordLogs(); + vm.prank(bob); + positionManager.renounceWithdrawAllowance(address(spoke1), reserveId, alice); + assertEq(vm.getRecordedLogs().length, 0); + } + + function test_renounceWithdrawAllowance_revertsWith_SpokeNotRegistered() public { + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); + vm.prank(bob); + positionManager.renounceWithdrawAllowance(address(spoke2), 1, alice); + } + + function test_withdrawOnBehalfOf() public { + test_withdrawOnBehalfOf_fuzz(100e18); + } + + function test_withdrawOnBehalfOf_fuzz(uint256 amount) public { + amount = bound(amount, 1, mintAmount_DAI); + + Utils.supply({ + spoke: spoke1, + reserveId: _daiReserveId(spoke1), + caller: alice, + amount: mintAmount_DAI, + onBehalfOf: alice + }); + uint256 expectedSupplyShares = hub1.previewAddByAssets(daiAssetId, mintAmount_DAI); + + vm.prank(alice); + positionManager.approveWithdraw(address(spoke1), _daiReserveId(spoke1), bob, amount); + + uint256 userBalanceBefore = tokenList.dai.balanceOf(alice); + uint256 callerBalanceBefore = tokenList.dai.balanceOf(bob); + uint256 hubBalanceBefore = tokenList.dai.balanceOf(address(hub1)); + uint256 userSuppliedAmountBefore = spoke1.getUserSuppliedAssets(_daiReserveId(spoke1), alice); + + assertEq(spoke1.getUserSuppliedShares(_daiReserveId(spoke1), alice), expectedSupplyShares); + + vm.expectEmit(address(positionManager)); + emit ITakerPositionManager.WithdrawApproval( + address(spoke1), + _daiReserveId(spoke1), + alice, + bob, + 0 + ); + vm.expectEmit(address(spoke1)); + emit ISpokeBase.Withdraw( + _daiReserveId(spoke1), + address(positionManager), + alice, + hub1.previewRemoveByAssets(daiAssetId, amount), + amount + ); + vm.prank(bob); + (returnValues.shares, returnValues.amount) = positionManager.withdrawOnBehalfOf( + address(spoke1), + _daiReserveId(spoke1), + amount, + alice + ); + + assertEq(returnValues.amount, amount); + assertEq(returnValues.shares, hub1.previewRemoveByAssets(daiAssetId, amount)); + + assertEq(tokenList.dai.balanceOf(alice), userBalanceBefore); + assertEq(tokenList.dai.balanceOf(bob), callerBalanceBefore + amount); + assertEq( + spoke1.getUserSuppliedAssets(_daiReserveId(spoke1), alice), + userSuppliedAmountBefore - amount + ); + assertEq(tokenList.dai.balanceOf(address(hub1)), hubBalanceBefore - amount); + assertEq(tokenList.dai.balanceOf(address(positionManager)), 0); + assertEq(tokenList.dai.allowance(address(positionManager), address(hub1)), 0); + assertEq( + positionManager.withdrawAllowance(address(spoke1), _daiReserveId(spoke1), alice, bob), + 0 + ); + } + + function test_withdrawOnBehalfOf_fuzz_allBalance(uint256 supplyAmount) public { + supplyAmount = bound(supplyAmount, 1, mintAmount_DAI); + + Utils.supply({ + spoke: spoke1, + reserveId: _daiReserveId(spoke1), + caller: alice, + amount: supplyAmount, + onBehalfOf: alice + }); + uint256 expectedSupplyShares = hub1.previewAddByAssets(daiAssetId, supplyAmount); + + vm.prank(alice); + positionManager.approveWithdraw(address(spoke1), _daiReserveId(spoke1), bob, supplyAmount * 10); + + uint256 userBalanceBefore = tokenList.dai.balanceOf(alice); + uint256 callerBalanceBefore = tokenList.dai.balanceOf(bob); + uint256 hubBalanceBefore = tokenList.dai.balanceOf(address(hub1)); + uint256 allowanceBefore = positionManager.withdrawAllowance( + address(spoke1), + _daiReserveId(spoke1), + alice, + bob + ); + + assertEq(spoke1.getUserSuppliedShares(_daiReserveId(spoke1), alice), expectedSupplyShares); + + vm.expectEmit(address(positionManager)); + emit ITakerPositionManager.WithdrawApproval( + address(spoke1), + _daiReserveId(spoke1), + alice, + bob, + allowanceBefore - (supplyAmount * 2) + ); + vm.expectEmit(address(spoke1)); + emit ISpokeBase.Withdraw( + _daiReserveId(spoke1), + address(positionManager), + alice, + expectedSupplyShares, + supplyAmount + ); + vm.prank(bob); + (returnValues.shares, returnValues.amount) = positionManager.withdrawOnBehalfOf( + address(spoke1), + _daiReserveId(spoke1), + supplyAmount * 2, + alice + ); + + assertEq(returnValues.amount, supplyAmount); + assertEq(returnValues.shares, expectedSupplyShares); + + assertEq(tokenList.dai.balanceOf(alice), userBalanceBefore); + assertEq(tokenList.dai.balanceOf(bob), callerBalanceBefore + supplyAmount); + assertEq(spoke1.getUserSuppliedAssets(_daiReserveId(spoke1), alice), 0); + assertEq(tokenList.dai.balanceOf(address(hub1)), hubBalanceBefore - supplyAmount); + assertEq(tokenList.dai.balanceOf(address(positionManager)), 0); + assertEq(tokenList.dai.allowance(address(positionManager), address(hub1)), 0); + assertEq( + positionManager.withdrawAllowance(address(spoke1), _daiReserveId(spoke1), alice, bob), + allowanceBefore - (supplyAmount * 2) + ); + } + + function test_withdrawOnBehalfOf_fuzz_allBalance_noAllowanceDecreased( + uint256 supplyAmount + ) public { + supplyAmount = bound(supplyAmount, 1, mintAmount_DAI); + + Utils.supply({ + spoke: spoke1, + reserveId: _daiReserveId(spoke1), + caller: alice, + amount: supplyAmount, + onBehalfOf: alice + }); + uint256 expectedSupplyShares = hub1.previewAddByAssets(daiAssetId, supplyAmount); + + vm.prank(alice); + positionManager.approveWithdraw(address(spoke1), _daiReserveId(spoke1), bob, type(uint256).max); + + uint256 userBalanceBefore = tokenList.dai.balanceOf(alice); + uint256 callerBalanceBefore = tokenList.dai.balanceOf(bob); + uint256 hubBalanceBefore = tokenList.dai.balanceOf(address(hub1)); + + assertEq(spoke1.getUserSuppliedShares(_daiReserveId(spoke1), alice), expectedSupplyShares); + + vm.expectEmit(address(spoke1)); + emit ISpokeBase.Withdraw( + _daiReserveId(spoke1), + address(positionManager), + alice, + expectedSupplyShares, + supplyAmount + ); + vm.recordLogs(); + vm.prank(bob); + (returnValues.shares, returnValues.amount) = positionManager.withdrawOnBehalfOf( + address(spoke1), + _daiReserveId(spoke1), + type(uint256).max, + alice + ); + vm.getRecordedLogs(); + _assertEventNotEmitted(ITakerPositionManager.WithdrawApproval.selector); + + assertEq(returnValues.amount, supplyAmount); + assertEq(returnValues.shares, expectedSupplyShares); + + assertEq(tokenList.dai.balanceOf(alice), userBalanceBefore); + assertEq(tokenList.dai.balanceOf(bob), callerBalanceBefore + supplyAmount); + assertEq(spoke1.getUserSuppliedAssets(_daiReserveId(spoke1), alice), 0); + assertEq(tokenList.dai.balanceOf(address(hub1)), hubBalanceBefore - supplyAmount); + assertEq(tokenList.dai.balanceOf(address(positionManager)), 0); + assertEq(tokenList.dai.allowance(address(positionManager), address(hub1)), 0); + assertEq( + positionManager.withdrawAllowance(address(spoke1), _daiReserveId(spoke1), alice, bob), + type(uint256).max + ); + } + + function test_withdrawOnBehalfOf_fuzz_allBalanceWithInterest( + uint256 supplyAmount, + uint256 borrowAmount + ) public { + supplyAmount = bound(supplyAmount, 2, mintAmount_DAI / 2); + borrowAmount = bound(borrowAmount, 1, supplyAmount / 2); + + Utils.supplyCollateral({ + spoke: spoke1, + reserveId: _daiReserveId(spoke1), + caller: alice, + amount: supplyAmount, + onBehalfOf: alice + }); + Utils.supplyCollateral({ + spoke: spoke1, + reserveId: _daiReserveId(spoke1), + caller: bob, + amount: supplyAmount, + onBehalfOf: bob + }); + uint256 expectedSupplyShares = hub1.previewAddByAssets(daiAssetId, supplyAmount); + + Utils.borrow({ + spoke: spoke1, + reserveId: _daiReserveId(spoke1), + caller: bob, + amount: borrowAmount, + onBehalfOf: bob + }); + + skip(322 days); + vm.assume(hub1.getAddedAssets(daiAssetId) > supplyAmount); + uint256 repayAmount = spoke1.getReserveTotalDebt(_daiReserveId(spoke1)); + deal(address(tokenList.dai), bob, repayAmount); + + Utils.repay({ + spoke: spoke1, + reserveId: _daiReserveId(spoke1), + caller: bob, + amount: UINT256_MAX, + onBehalfOf: bob + }); + + uint256 expectedWithdrawAmount = spoke1.getUserSuppliedAssets(_daiReserveId(spoke1), alice); + + vm.prank(alice); + positionManager.approveWithdraw(address(spoke1), _daiReserveId(spoke1), bob, supplyAmount * 10); + + uint256 userBalanceBefore = tokenList.dai.balanceOf(alice); + uint256 callerBalanceBefore = tokenList.dai.balanceOf(bob); + uint256 hubBalanceBefore = tokenList.dai.balanceOf(address(hub1)); + + assertEq(spoke1.getUserSuppliedShares(_daiReserveId(spoke1), alice), expectedSupplyShares); + + vm.expectEmit(address(positionManager)); + emit ITakerPositionManager.WithdrawApproval( + address(spoke1), + _daiReserveId(spoke1), + alice, + bob, + 0 + ); + vm.expectEmit(address(spoke1)); + emit ISpokeBase.Withdraw( + _daiReserveId(spoke1), + address(positionManager), + alice, + expectedSupplyShares, + expectedWithdrawAmount + ); + vm.prank(bob); + (returnValues.shares, returnValues.amount) = positionManager.withdrawOnBehalfOf( + address(spoke1), + _daiReserveId(spoke1), + supplyAmount * 10, + alice + ); + + assertEq(returnValues.amount, expectedWithdrawAmount); + assertEq(returnValues.shares, expectedSupplyShares); + + assertEq(tokenList.dai.balanceOf(alice), userBalanceBefore); + assertEq(tokenList.dai.balanceOf(bob), callerBalanceBefore + expectedWithdrawAmount); + assertEq(spoke1.getUserSuppliedAssets(_daiReserveId(spoke1), alice), 0); + assertEq(tokenList.dai.balanceOf(address(hub1)), hubBalanceBefore - expectedWithdrawAmount); + assertEq(tokenList.dai.balanceOf(address(positionManager)), 0); + assertEq(tokenList.dai.allowance(address(positionManager), address(hub1)), 0); + assertEq( + positionManager.withdrawAllowance(address(spoke1), _daiReserveId(spoke1), alice, bob), + 0 + ); + } + + function test_withdrawOnBehalfOf_revertsWith_InsufficientWithdrawAllowance( + uint256 approvalAmount + ) public { + uint256 amount = 100e18; + approvalAmount = bound(approvalAmount, 1, amount - 1); + + Utils.supply({ + spoke: spoke1, + reserveId: _daiReserveId(spoke1), + caller: alice, + amount: mintAmount_DAI, + onBehalfOf: alice + }); + + vm.prank(alice); + positionManager.approveWithdraw(address(spoke1), _daiReserveId(spoke1), bob, approvalAmount); + + vm.expectRevert( + abi.encodeWithSelector( + ITakerPositionManager.InsufficientWithdrawAllowance.selector, + approvalAmount, + amount + ) + ); + vm.prank(bob); + positionManager.withdrawOnBehalfOf(address(spoke1), _daiReserveId(spoke1), amount, alice); + } + + function test_withdrawOnBehalfOf_revertsWith_ReserveNotListed() public { + uint256 reserveId = _randomInvalidReserveId(spoke1); + + vm.prank(alice); + positionManager.approveWithdraw(address(spoke1), reserveId, bob, 100e18); + + vm.expectRevert(ISpoke.ReserveNotListed.selector); + vm.prank(bob); + positionManager.withdrawOnBehalfOf(address(spoke1), reserveId, 100e18, alice); + } + + function test_withdrawOnBehalfOf_revertsWith_SpokeNotRegistered() public { + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); + vm.prank(bob); + positionManager.withdrawOnBehalfOf(address(spoke2), 1, 100e18, alice); + } + + function test_approveBorrow_fuzz(address spender, uint256 reserveId, uint256 amount) public { + vm.assume(spender != address(0)); + reserveId = bound(reserveId, 0, spoke1.getReserveCount() - 1); + amount = bound(amount, 1, mintAmount_DAI); + + vm.expectEmit(address(positionManager)); + emit ITakerPositionManager.BorrowApproval(address(spoke1), reserveId, alice, spender, amount); + vm.prank(alice); + positionManager.approveBorrow(address(spoke1), reserveId, spender, amount); + + assertEq(positionManager.borrowAllowance(address(spoke1), reserveId, alice, spender), amount); + } + + function test_approveBorrow_revertsWith_SpokeNotRegistered() public { + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); + vm.prank(alice); + positionManager.approveBorrow(address(spoke2), 1, bob, 100e18); + } + + function test_renounceBorrowAllowance_fuzz(uint256 initialAllowance) public { + uint256 reserveId = _randomReserveId(spoke1); + initialAllowance = bound(initialAllowance, 1, mintAmount_DAI); + + vm.prank(alice); + positionManager.approveBorrow(address(spoke1), reserveId, bob, initialAllowance); + + vm.expectEmit(address(positionManager)); + emit ITakerPositionManager.BorrowApproval(address(spoke1), reserveId, alice, bob, 0); + vm.prank(bob); + positionManager.renounceBorrowAllowance(address(spoke1), reserveId, alice); + + assertEq(positionManager.borrowAllowance(address(spoke1), reserveId, alice, bob), 0); + } + + function test_renounceBorrowAllowance_noop_alreadyRenounced() public { + uint256 reserveId = _randomReserveId(spoke1); + + vm.prank(alice); + positionManager.approveBorrow(address(spoke1), reserveId, bob, 100e18); + vm.prank(bob); + positionManager.renounceBorrowAllowance(address(spoke1), reserveId, alice); + + vm.recordLogs(); + vm.prank(bob); + positionManager.renounceBorrowAllowance(address(spoke1), reserveId, alice); + assertEq(vm.getRecordedLogs().length, 0); + } + + function test_renounceBorrowAllowance_revertsWith_SpokeNotRegistered() public { + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); + vm.prank(bob); + positionManager.renounceBorrowAllowance(address(spoke2), 1, alice); + } + + function test_borrowOnBehalfOf() public { + test_borrowOnBehalfOf_fuzz(5e18, 5e18); + } + + function test_borrowOnBehalfOf_fuzz(uint256 borrowAmount, uint256 approveBorrowAmount) public { + uint256 aliceSupplyAmount = 5000e18; + uint256 bobSupplyAmount = 1000e18; + borrowAmount = bound(borrowAmount, 1, bobSupplyAmount); + approveBorrowAmount = bound(approveBorrowAmount, borrowAmount, borrowAmount * 10); + + Utils.supplyCollateral(spoke1, _daiReserveId(spoke1), alice, aliceSupplyAmount, alice); + Utils.supplyCollateral(spoke1, _daiReserveId(spoke1), bob, bobSupplyAmount, bob); + + vm.prank(alice); + positionManager.approveBorrow(address(spoke1), _daiReserveId(spoke1), bob, approveBorrowAmount); + + uint256 userBalanceBefore = tokenList.dai.balanceOf(alice); + uint256 callerBalanceBefore = tokenList.dai.balanceOf(bob); + uint256 hubBalanceBefore = tokenList.dai.balanceOf(address(hub1)); + + vm.expectEmit(address(positionManager)); + emit ITakerPositionManager.BorrowApproval( + address(spoke1), + _daiReserveId(spoke1), + alice, + bob, + approveBorrowAmount - borrowAmount + ); + vm.expectEmit(address(spoke1)); + emit ISpokeBase.Borrow( + _daiReserveId(spoke1), + address(positionManager), + alice, + hub1.previewRestoreByAssets(daiAssetId, borrowAmount), + borrowAmount + ); + vm.prank(bob); + (returnValues.shares, returnValues.amount) = positionManager.borrowOnBehalfOf( + address(spoke1), + _daiReserveId(spoke1), + borrowAmount, + alice + ); + + (uint256 userDrawnDebt, uint256 userPremiumDebt) = spoke1.getUserDebt( + _daiReserveId(spoke1), + alice + ); + + assertEq(returnValues.amount, borrowAmount); + assertEq(returnValues.shares, hub1.previewDrawByAssets(daiAssetId, borrowAmount)); + + assertEq(userDrawnDebt + userPremiumDebt, borrowAmount); + assertEq(tokenList.dai.balanceOf(address(hub1)), hubBalanceBefore - borrowAmount); + assertEq(tokenList.dai.balanceOf(address(alice)), userBalanceBefore); + assertEq(tokenList.dai.balanceOf(address(bob)), callerBalanceBefore + borrowAmount); + assertEq(tokenList.dai.allowance(address(positionManager), address(hub1)), 0); + assertEq( + positionManager.borrowAllowance(address(spoke1), _daiReserveId(spoke1), alice, bob), + approveBorrowAmount - borrowAmount + ); + } + + function test_borrowOnBehalfOf_fuzz_noAllowanceDecrease(uint256 borrowAmount) public { + uint256 aliceSupplyAmount = 5000e18; + uint256 bobSupplyAmount = 1000e18; + borrowAmount = bound(borrowAmount, 1, bobSupplyAmount); + + Utils.supplyCollateral(spoke1, _daiReserveId(spoke1), alice, aliceSupplyAmount, alice); + Utils.supplyCollateral(spoke1, _daiReserveId(spoke1), bob, bobSupplyAmount, bob); + + vm.prank(alice); + positionManager.approveBorrow(address(spoke1), _daiReserveId(spoke1), bob, type(uint256).max); + + uint256 userBalanceBefore = tokenList.dai.balanceOf(alice); + uint256 callerBalanceBefore = tokenList.dai.balanceOf(bob); + uint256 hubBalanceBefore = tokenList.dai.balanceOf(address(hub1)); + + vm.expectEmit(address(spoke1)); + emit ISpokeBase.Borrow( + _daiReserveId(spoke1), + address(positionManager), + alice, + hub1.previewRestoreByAssets(daiAssetId, borrowAmount), + borrowAmount + ); + vm.recordLogs(); + vm.prank(bob); + (returnValues.shares, returnValues.amount) = positionManager.borrowOnBehalfOf( + address(spoke1), + _daiReserveId(spoke1), + borrowAmount, + alice + ); + vm.getRecordedLogs(); + _assertEventNotEmitted(ITakerPositionManager.BorrowApproval.selector); + + (uint256 userDrawnDebt, uint256 userPremiumDebt) = spoke1.getUserDebt( + _daiReserveId(spoke1), + alice + ); + + assertEq(returnValues.amount, borrowAmount); + assertEq(returnValues.shares, hub1.previewDrawByAssets(daiAssetId, borrowAmount)); + + assertEq(userDrawnDebt + userPremiumDebt, borrowAmount); + assertEq(tokenList.dai.balanceOf(address(hub1)), hubBalanceBefore - borrowAmount); + assertEq(tokenList.dai.balanceOf(address(alice)), userBalanceBefore); + assertEq(tokenList.dai.balanceOf(address(bob)), callerBalanceBefore + borrowAmount); + assertEq(tokenList.dai.allowance(address(positionManager), address(hub1)), 0); + assertEq( + positionManager.borrowAllowance(address(spoke1), _daiReserveId(spoke1), alice, bob), + type(uint256).max + ); + } + + function test_borrowOnBehalfOf_revertsWith_InsufficientBorrowAllowance( + uint256 approveBorrowAmount + ) public { + uint256 borrowAmount = 100e18; + approveBorrowAmount = bound(approveBorrowAmount, 1, borrowAmount - 1); + Utils.supplyCollateral(spoke1, _daiReserveId(spoke1), alice, borrowAmount, alice); + Utils.supplyCollateral(spoke1, _daiReserveId(spoke1), bob, borrowAmount, bob); + + vm.prank(alice); + positionManager.approveBorrow(address(spoke1), _daiReserveId(spoke1), bob, approveBorrowAmount); + + vm.expectRevert( + abi.encodeWithSelector( + ITakerPositionManager.InsufficientBorrowAllowance.selector, + approveBorrowAmount, + borrowAmount + ) + ); + vm.prank(bob); + positionManager.borrowOnBehalfOf(address(spoke1), _daiReserveId(spoke1), borrowAmount, alice); + } + + function test_borrowOnBehalfOf_revertsWith_ReserveNotListed() public { + uint256 reserveId = _randomInvalidReserveId(spoke1); + + vm.prank(alice); + positionManager.approveBorrow(address(spoke1), reserveId, bob, 100e18); + + vm.expectRevert(ISpoke.ReserveNotListed.selector); + vm.prank(bob); + positionManager.borrowOnBehalfOf(address(spoke1), reserveId, 100e18, alice); + } + + function test_borrowOnBehalfOf_revertsWith_SpokeNotRegistered() public { + vm.expectRevert(IPositionManagerBase.SpokeNotRegistered.selector); + vm.prank(bob); + positionManager.borrowOnBehalfOf(address(spoke2), 1, 100e18, alice); + } + + function test_multicall() public { + uint256 amount = 100e18; + + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSignature( + 'approveWithdraw(address,uint256,address,uint256)', + address(spoke1), + _daiReserveId(spoke1), + bob, + amount + ); + calls[1] = abi.encodeWithSignature( + 'approveBorrow(address,uint256,address,uint256)', + address(spoke1), + _daiReserveId(spoke1), + bob, + amount + ); + + vm.prank(alice); + bytes[] memory res = positionManager.multicall(calls); + + assertEq(res[0].length, 0); + assertEq(res[1].length, 0); + + assertEq( + positionManager.withdrawAllowance(address(spoke1), _daiReserveId(spoke1), alice, bob), + amount + ); + assertEq( + positionManager.borrowAllowance(address(spoke1), _daiReserveId(spoke1), alice, bob), + amount + ); + } +} diff --git a/tests/unit/position-managers/libraries/ConfigPermissions.t.sol b/tests/unit/position-managers/libraries/ConfigPermissions.t.sol new file mode 100644 index 000000000..ddea438e5 --- /dev/null +++ b/tests/unit/position-managers/libraries/ConfigPermissions.t.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {Test} from 'forge-std/Test.sol'; +import {ConfigPermissions} from 'src/position-manager/libraries/ConfigPermissionsMap.sol'; +import {ConfigPermissionsWrapper} from 'tests/mocks/ConfigPermissionsWrapper.sol'; + +contract ConfigPermissionsTests is Test { + uint8 internal constant CAN_SET_USING_AS_COLLATERAL_MASK = 0x1; + uint8 internal constant CAN_UPDATE_USER_RISK_PREMIUM_MASK = 0x2; + uint8 internal constant CAN_UPDATE_USER_DYNAMIC_CONFIG_MASK = 0x4; + uint8 internal constant FULL_PERMISSIONS_MASK = 0x7; + + ConfigPermissionsWrapper internal w; + + function setUp() public { + w = new ConfigPermissionsWrapper(); + } + + function test_constants() public view { + assertEq(w.CAN_SET_USING_AS_COLLATERAL_MASK(), CAN_SET_USING_AS_COLLATERAL_MASK); + assertEq(w.CAN_UPDATE_USER_RISK_PREMIUM_MASK(), CAN_UPDATE_USER_RISK_PREMIUM_MASK); + assertEq(w.CAN_UPDATE_USER_DYNAMIC_CONFIG_MASK(), CAN_UPDATE_USER_DYNAMIC_CONFIG_MASK); + assertEq(w.FULL_PERMISSIONS_MASK(), FULL_PERMISSIONS_MASK); + } + + function test_setFullPermissions_fuzz(bool status) public view { + ConfigPermissions updatedPerms = w.setFullPermissions(status); + + uint8 expected = status ? FULL_PERMISSIONS_MASK : 0; + assertEq(uint8(ConfigPermissions.unwrap(updatedPerms)), expected); + assertEq(w.canSetUsingAsCollateral(updatedPerms), status); + assertEq(w.canUpdateUserRiskPremium(updatedPerms), status); + assertEq(w.canUpdateUserDynamicConfig(updatedPerms), status); + } + + function test_setCanSetUsingAsCollateral_fuzz(uint8 rawPermissions, bool status) public view { + ConfigPermissions perms = _sanitizePermissions(rawPermissions); + ConfigPermissions updatedPerms = w.setCanSetUsingAsCollateral(perms, status); + + uint8 expected = _changeStatus(perms, CAN_SET_USING_AS_COLLATERAL_MASK, status); + assertEq(uint8(ConfigPermissions.unwrap(updatedPerms)), expected); + assertEq(w.canSetUsingAsCollateral(updatedPerms), status); + } + + function test_setCanUpdateUserRiskPremium_fuzz(uint8 rawPermissions, bool status) public view { + ConfigPermissions perms = _sanitizePermissions(rawPermissions); + ConfigPermissions updatedPerms = w.setCanUpdateUserRiskPremium(perms, status); + + uint8 expected = _changeStatus(perms, CAN_UPDATE_USER_RISK_PREMIUM_MASK, status); + assertEq(uint8(ConfigPermissions.unwrap(updatedPerms)), expected); + assertEq(w.canUpdateUserRiskPremium(updatedPerms), status); + } + + function test_setCanUpdateUserDynamicConfig_fuzz(uint8 rawPermissions, bool status) public view { + ConfigPermissions perms = _sanitizePermissions(rawPermissions); + ConfigPermissions updatedPerms = w.setCanUpdateUserDynamicConfig(perms, status); + + uint8 expected = _changeStatus(perms, CAN_UPDATE_USER_DYNAMIC_CONFIG_MASK, status); + assertEq(uint8(ConfigPermissions.unwrap(updatedPerms)), expected); + assertEq(w.canUpdateUserDynamicConfig(updatedPerms), status); + } + + /// @dev Sanitizes the raw permissions by masking out any irrelevant bits. + function _sanitizePermissions(uint8 rawPermissions) internal pure returns (ConfigPermissions) { + uint8 sanitizedPermissions = rawPermissions & FULL_PERMISSIONS_MASK; + return ConfigPermissions.wrap(sanitizedPermissions); + } + + function _changeStatus( + ConfigPermissions perms, + uint8 mask, + bool status + ) internal pure returns (uint8) { + return + status + ? (uint8(ConfigPermissions.unwrap(perms)) | mask) + : (uint8(ConfigPermissions.unwrap(perms)) & ~mask); + } +} diff --git a/tests/unit/position-managers/libraries/PositionManagerEIP712Hash.t.sol b/tests/unit/position-managers/libraries/PositionManagerEIP712Hash.t.sol new file mode 100644 index 000000000..ad00c43ef --- /dev/null +++ b/tests/unit/position-managers/libraries/PositionManagerEIP712Hash.t.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {Test} from 'forge-std/Test.sol'; + +import {ISignatureGateway} from 'src/position-manager/interfaces/ISignatureGateway.sol'; +import {ITakerPositionManager} from 'src/position-manager/interfaces/ITakerPositionManager.sol'; + +import {EIP712Hash} from 'src/position-manager/libraries/EIP712Hash.sol'; + +contract PositionManagerEIP712HashTest is Test { + using EIP712Hash for *; + + function test_constants() public pure { + assertEq( + EIP712Hash.SUPPLY_TYPEHASH, + keccak256( + 'Supply(address spoke,uint256 reserveId,uint256 amount,address onBehalfOf,uint256 nonce,uint256 deadline)' + ) + ); + assertEq(EIP712Hash.SUPPLY_TYPEHASH, vm.eip712HashType('Supply')); + + assertEq( + EIP712Hash.WITHDRAW_TYPEHASH, + keccak256( + 'Withdraw(address spoke,uint256 reserveId,uint256 amount,address onBehalfOf,uint256 nonce,uint256 deadline)' + ) + ); + assertEq(EIP712Hash.WITHDRAW_TYPEHASH, vm.eip712HashType('Withdraw')); + + assertEq( + EIP712Hash.BORROW_TYPEHASH, + keccak256( + 'Borrow(address spoke,uint256 reserveId,uint256 amount,address onBehalfOf,uint256 nonce,uint256 deadline)' + ) + ); + assertEq(EIP712Hash.BORROW_TYPEHASH, vm.eip712HashType('Borrow')); + + assertEq( + EIP712Hash.REPAY_TYPEHASH, + keccak256( + 'Repay(address spoke,uint256 reserveId,uint256 amount,address onBehalfOf,uint256 nonce,uint256 deadline)' + ) + ); + assertEq(EIP712Hash.REPAY_TYPEHASH, vm.eip712HashType('Repay')); + + assertEq( + EIP712Hash.SET_USING_AS_COLLATERAL_TYPEHASH, + keccak256( + 'SetUsingAsCollateral(address spoke,uint256 reserveId,bool useAsCollateral,address onBehalfOf,uint256 nonce,uint256 deadline)' + ) + ); + assertEq( + EIP712Hash.SET_USING_AS_COLLATERAL_TYPEHASH, + vm.eip712HashType('SetUsingAsCollateral') + ); + + assertEq( + EIP712Hash.UPDATE_USER_RISK_PREMIUM_TYPEHASH, + keccak256( + 'UpdateUserRiskPremium(address spoke,address onBehalfOf,uint256 nonce,uint256 deadline)' + ) + ); + assertEq( + EIP712Hash.UPDATE_USER_RISK_PREMIUM_TYPEHASH, + vm.eip712HashType('UpdateUserRiskPremium') + ); + + assertEq( + EIP712Hash.UPDATE_USER_DYNAMIC_CONFIG_TYPEHASH, + keccak256( + 'UpdateUserDynamicConfig(address spoke,address onBehalfOf,uint256 nonce,uint256 deadline)' + ) + ); + assertEq( + EIP712Hash.UPDATE_USER_DYNAMIC_CONFIG_TYPEHASH, + vm.eip712HashType('UpdateUserDynamicConfig') + ); + + assertEq( + EIP712Hash.WITHDRAW_PERMIT_TYPEHASH, + keccak256( + 'WithdrawPermit(address spoke,uint256 reserveId,address owner,address spender,uint256 amount,uint256 nonce,uint256 deadline)' + ) + ); + assertEq(EIP712Hash.WITHDRAW_PERMIT_TYPEHASH, vm.eip712HashType('WithdrawPermit')); + + assertEq( + EIP712Hash.BORROW_PERMIT_TYPEHASH, + keccak256( + 'BorrowPermit(address spoke,uint256 reserveId,address owner,address spender,uint256 amount,uint256 nonce,uint256 deadline)' + ) + ); + assertEq(EIP712Hash.BORROW_PERMIT_TYPEHASH, vm.eip712HashType('BorrowPermit')); + } + + // @dev all struct params should be hashed & placed in the same order as the typehash + function test_hash_supply_fuzz(ISignatureGateway.Supply calldata params) public pure { + bytes32 expectedHash = keccak256(abi.encode(EIP712Hash.SUPPLY_TYPEHASH, params)); + assertEq(params.hash(), expectedHash); + assertEq(params.hash(), vm.eip712HashStruct('Supply', abi.encode(params))); + } + + function test_hash_withdraw_fuzz(ISignatureGateway.Withdraw calldata params) public pure { + bytes32 expectedHash = keccak256(abi.encode(EIP712Hash.WITHDRAW_TYPEHASH, params)); + assertEq(params.hash(), expectedHash); + assertEq(params.hash(), vm.eip712HashStruct('Withdraw', abi.encode(params))); + } + + function test_hash_borrow_fuzz(ISignatureGateway.Borrow calldata params) public pure { + bytes32 expectedHash = keccak256(abi.encode(EIP712Hash.BORROW_TYPEHASH, params)); + assertEq(params.hash(), expectedHash); + assertEq(params.hash(), vm.eip712HashStruct('Borrow', abi.encode(params))); + } + + function test_hash_repay_fuzz(ISignatureGateway.Repay calldata params) public pure { + bytes32 expectedHash = keccak256(abi.encode(EIP712Hash.REPAY_TYPEHASH, params)); + assertEq(params.hash(), expectedHash); + assertEq(params.hash(), vm.eip712HashStruct('Repay', abi.encode(params))); + } + + function test_hash_setUsingAsCollateral_fuzz( + ISignatureGateway.SetUsingAsCollateral calldata params + ) public pure { + bytes32 expectedHash = keccak256( + abi.encode(EIP712Hash.SET_USING_AS_COLLATERAL_TYPEHASH, params) + ); + assertEq(params.hash(), expectedHash); + assertEq(params.hash(), vm.eip712HashStruct('SetUsingAsCollateral', abi.encode(params))); + } + + function test_hash_updateUserRiskPremium_fuzz( + ISignatureGateway.UpdateUserRiskPremium calldata params + ) public pure { + bytes32 expectedHash = keccak256( + abi.encode(EIP712Hash.UPDATE_USER_RISK_PREMIUM_TYPEHASH, params) + ); + assertEq(params.hash(), expectedHash); + assertEq(params.hash(), vm.eip712HashStruct('UpdateUserRiskPremium', abi.encode(params))); + } + + function test_hash_updateUserDynamicConfig_fuzz( + ISignatureGateway.UpdateUserDynamicConfig calldata params + ) public pure { + bytes32 expectedHash = keccak256( + abi.encode(EIP712Hash.UPDATE_USER_DYNAMIC_CONFIG_TYPEHASH, params) + ); + assertEq(params.hash(), expectedHash); + assertEq(params.hash(), vm.eip712HashStruct('UpdateUserDynamicConfig', abi.encode(params))); + } + + function test_hash_withdrawPermit_fuzz( + ITakerPositionManager.WithdrawPermit calldata params + ) public pure { + bytes32 expectedHash = keccak256(abi.encode(EIP712Hash.WITHDRAW_PERMIT_TYPEHASH, params)); + + assertEq(params.hash(), expectedHash); + } + + function test_hash_creditDelegation_fuzz( + ITakerPositionManager.BorrowPermit calldata params + ) public pure { + bytes32 expectedHash = keccak256(abi.encode(EIP712Hash.BORROW_PERMIT_TYPEHASH, params)); + + assertEq(params.hash(), expectedHash); + } +}