diff --git a/snapshots/NativeTokenGateway.Operations.json b/snapshots/NativeTokenGateway.Operations.json index 9beadd031..d60a06971 100644 --- a/snapshots/NativeTokenGateway.Operations.json +++ b/snapshots/NativeTokenGateway.Operations.json @@ -1,8 +1,8 @@ { - "borrowNative": "229604", - "repayNative": "168312", - "supplyAsCollateralNative": "160373", - "supplyNative": "136476", - "withdrawNative: full": "125620", - "withdrawNative: partial": "136825" + "borrowNative": "229560", + "repayNative": "168311", + "supplyAsCollateralNative": "160307", + "supplyNative": "136441", + "withdrawNative: full": "125585", + "withdrawNative: partial": "136781" } \ No newline at end of file diff --git a/snapshots/SignatureGateway.Operations.json b/snapshots/SignatureGateway.Operations.json index 54c084a4d..e6a8f77ae 100644 --- a/snapshots/SignatureGateway.Operations.json +++ b/snapshots/SignatureGateway.Operations.json @@ -1,10 +1,10 @@ { - "borrowWithSig": "215893", - "repayWithSig": "189160", - "setSelfAsUserPositionManagerWithSig": "74858", - "setUsingAsCollateralWithSig": "85053", - "supplyWithSig": "153205", - "updateUserDynamicConfigWithSig": "62769", - "updateUserRiskPremiumWithSig": "61579", - "withdrawWithSig": "131713" + "borrowWithSig": "215849", + "repayWithSig": "189159", + "setSelfAsUserPositionManagerWithSig": "82643", + "setUsingAsCollateralWithSig": "85031", + "supplyWithSig": "153170", + "updateUserDynamicConfigWithSig": "62857", + "updateUserRiskPremiumWithSig": "61557", + "withdrawWithSig": "131678" } \ No newline at end of file diff --git a/snapshots/Spoke.Operations.ZeroRiskPremium.json b/snapshots/Spoke.Operations.ZeroRiskPremium.json index dd334eaec..6cac6ef4c 100644 --- a/snapshots/Spoke.Operations.ZeroRiskPremium.json +++ b/snapshots/Spoke.Operations.ZeroRiskPremium.json @@ -1,33 +1,33 @@ { - "borrow: first": "191325", - "borrow: second action, same reserve": "171297", + "borrow: first": "191303", + "borrow: second action, same reserve": "171275", "liquidationCall (receiveShares): full": "300391", "liquidationCall (receiveShares): partial": "300109", "liquidationCall: full": "310756", "liquidationCall: partial": "310474", - "permitReserve + repay (multicall)": "166317", - "permitReserve + supply (multicall)": "146862", - "permitReserve + supply + enable collateral (multicall)": "160573", - "repay: full": "126382", - "repay: partial": "131271", - "setUserPositionManagerWithSig: disable": "44846", - "setUserPositionManagerWithSig: enable": "68875", - "supply + enable collateral (multicall)": "140624", - "supply: 0 borrows, collateral disabled": "123679", - "supply: 0 borrows, collateral enabled": "106601", - "supply: second action, same reserve": "106579", - "updateUserDynamicConfig: 1 collateral": "73694", - "updateUserDynamicConfig: 2 collaterals": "88551", - "updateUserRiskPremium: 1 borrow": "94804", - "updateUserRiskPremium: 2 borrows": "104619", - "usingAsCollateral: 0 borrows, enable": "58915", - "usingAsCollateral: 1 borrow, disable": "105072", - "usingAsCollateral: 1 borrow, enable": "41803", - "usingAsCollateral: 2 borrows, disable": "126055", - "usingAsCollateral: 2 borrows, enable": "41815", - "withdraw: 0 borrows, full": "128910", - "withdraw: 0 borrows, partial": "133473", - "withdraw: 1 borrow, partial": "161036", - "withdraw: 2 borrows, partial": "174214", - "withdraw: non collateral": "106544" + "permitReserve + repay (multicall)": "166294", + "permitReserve + supply (multicall)": "146774", + "permitReserve + supply + enable collateral (multicall)": "160463", + "repay: full": "126425", + "repay: partial": "131314", + "setUserPositionManagerWithSig: disable": "52333", + "setUserPositionManagerWithSig: enable": "76362", + "supply + enable collateral (multicall)": "140536", + "supply: 0 borrows, collateral disabled": "123657", + "supply: 0 borrows, collateral enabled": "106579", + "supply: second action, same reserve": "106557", + "updateUserDynamicConfig: 1 collateral": "73782", + "updateUserDynamicConfig: 2 collaterals": "88639", + "updateUserRiskPremium: 1 borrow": "94782", + "updateUserRiskPremium: 2 borrows": "104597", + "usingAsCollateral: 0 borrows, enable": "58893", + "usingAsCollateral: 1 borrow, disable": "105050", + "usingAsCollateral: 1 borrow, enable": "41781", + "usingAsCollateral: 2 borrows, disable": "126033", + "usingAsCollateral: 2 borrows, enable": "41793", + "withdraw: 0 borrows, full": "128888", + "withdraw: 0 borrows, partial": "133451", + "withdraw: 1 borrow, partial": "161014", + "withdraw: 2 borrows, partial": "174192", + "withdraw: non collateral": "106522" } \ No newline at end of file diff --git a/snapshots/Spoke.Operations.json b/snapshots/Spoke.Operations.json index 28821aab8..09f21d835 100644 --- a/snapshots/Spoke.Operations.json +++ b/snapshots/Spoke.Operations.json @@ -1,33 +1,33 @@ { - "borrow: first": "262009", - "borrow: second action, same reserve": "204981", + "borrow: first": "261987", + "borrow: second action, same reserve": "204959", "liquidationCall (receiveShares): full": "334242", "liquidationCall (receiveShares): partial": "333960", "liquidationCall: full": "344607", "liquidationCall: partial": "344325", - "permitReserve + repay (multicall)": "163504", - "permitReserve + supply (multicall)": "146862", - "permitReserve + supply + enable collateral (multicall)": "160573", - "repay: full": "120544", - "repay: partial": "139833", - "setUserPositionManagerWithSig: disable": "44846", - "setUserPositionManagerWithSig: enable": "68875", - "supply + enable collateral (multicall)": "140624", - "supply: 0 borrows, collateral disabled": "123679", - "supply: 0 borrows, collateral enabled": "106601", - "supply: second action, same reserve": "106579", - "updateUserDynamicConfig: 1 collateral": "73694", - "updateUserDynamicConfig: 2 collaterals": "88551", - "updateUserRiskPremium: 1 borrow": "151368", - "updateUserRiskPremium: 2 borrows": "204852", - "usingAsCollateral: 0 borrows, enable": "58915", - "usingAsCollateral: 1 borrow, disable": "161636", - "usingAsCollateral: 1 borrow, enable": "41803", - "usingAsCollateral: 2 borrows, disable": "234288", - "usingAsCollateral: 2 borrows, enable": "41815", - "withdraw: 0 borrows, full": "128910", - "withdraw: 0 borrows, partial": "133473", - "withdraw: 1 borrow, partial": "215098", - "withdraw: 2 borrows, partial": "259848", - "withdraw: non collateral": "106544" + "permitReserve + repay (multicall)": "163485", + "permitReserve + supply (multicall)": "146774", + "permitReserve + supply + enable collateral (multicall)": "160463", + "repay: full": "120587", + "repay: partial": "139876", + "setUserPositionManagerWithSig: disable": "52333", + "setUserPositionManagerWithSig: enable": "76362", + "supply + enable collateral (multicall)": "140536", + "supply: 0 borrows, collateral disabled": "123657", + "supply: 0 borrows, collateral enabled": "106579", + "supply: second action, same reserve": "106557", + "updateUserDynamicConfig: 1 collateral": "73782", + "updateUserDynamicConfig: 2 collaterals": "88639", + "updateUserRiskPremium: 1 borrow": "151346", + "updateUserRiskPremium: 2 borrows": "204830", + "usingAsCollateral: 0 borrows, enable": "58893", + "usingAsCollateral: 1 borrow, disable": "161614", + "usingAsCollateral: 1 borrow, enable": "41781", + "usingAsCollateral: 2 borrows, disable": "234266", + "usingAsCollateral: 2 borrows, enable": "41793", + "withdraw: 0 borrows, full": "128888", + "withdraw: 0 borrows, partial": "133451", + "withdraw: 1 borrow, partial": "215076", + "withdraw: 2 borrows, partial": "259826", + "withdraw: non collateral": "106522" } \ No newline at end of file diff --git a/src/libraries/misc/SignatureChecker.sol b/src/libraries/misc/SignatureChecker.sol new file mode 100644 index 000000000..df64abea8 --- /dev/null +++ b/src/libraries/misc/SignatureChecker.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {SignatureChecker as OpenZeppelinSignatureChecker} from 'src/dependencies/openzeppelin/SignatureChecker.sol'; + +/// @title SignatureChecker +/// @author Aave Labs +library SignatureChecker { + /// @notice Checks if a signature is valid for a given signer and data hash. + /// @dev External wrapper around OpenZeppelin's SignatureChecker.isValidSignatureNow to reduce code size at the expense of an external delegatecall. + /// @param signer The address of the signer. + /// @param hash The hash of the data to be signed. + /// @param signature The signature bytes. + /// @return True if the signature is valid, false otherwise. + function isValidSignatureNow( + address signer, + bytes32 hash, + bytes memory signature + ) external view returns (bool) { + return OpenZeppelinSignatureChecker.isValidSignatureNow(signer, hash, signature); + } +} diff --git a/src/libraries/types/EIP712Types.sol b/src/libraries/types/EIP712Types.sol index e647206a0..fa137f151 100644 --- a/src/libraries/types/EIP712Types.sol +++ b/src/libraries/types/EIP712Types.sol @@ -7,13 +7,17 @@ pragma solidity ^0.8.20; /// @notice Defines type structs used in EIP712-typed signatures. library EIP712Types { struct SetUserPositionManager { - address positionManager; address user; - bool approve; + PositionManagerUpdate[] updates; uint256 nonce; uint256 deadline; } + struct PositionManagerUpdate { + address positionManager; + bool approve; + } + struct Permit { address owner; address spender; diff --git a/src/position-manager/SignatureGateway.sol b/src/position-manager/SignatureGateway.sol index c4ab78a29..4e83881a8 100644 --- a/src/position-manager/SignatureGateway.sol +++ b/src/position-manager/SignatureGateway.sol @@ -176,15 +176,21 @@ contract SignatureGateway is ISignatureGateway, GatewayBase, NoncesKeyed, Multic uint256 deadline, bytes calldata signature ) external onlyRegisteredSpoke(spoke) { + EIP712Types.PositionManagerUpdate[] memory updates = new EIP712Types.PositionManagerUpdate[](1); + updates[0] = EIP712Types.PositionManagerUpdate({ + positionManager: address(this), + approve: approve + }); try - ISpoke(spoke).setUserPositionManagerWithSig({ - positionManager: address(this), - user: user, - approve: approve, - nonce: nonce, - deadline: deadline, - signature: signature - }) + ISpoke(spoke).setUserPositionManagerWithSig( + EIP712Types.SetUserPositionManager({ + updates: updates, + user: user, + nonce: nonce, + deadline: deadline + }), + signature + ) {} catch {} } diff --git a/src/position-manager/libraries/EIP712Hash.sol b/src/position-manager/libraries/EIP712Hash.sol index 060bd4575..c937f4002 100644 --- a/src/position-manager/libraries/EIP712Hash.sol +++ b/src/position-manager/libraries/EIP712Hash.sol @@ -8,6 +8,8 @@ import {EIP712Types} from 'src/libraries/types/EIP712Types.sol'; /// @author Aave Labs /// @notice Helper methods to hash EIP712 typed data structs. library EIP712Hash { + using EIP712Hash for *; + bytes32 public constant SUPPLY_TYPEHASH = // keccak256('Supply(address spoke,uint256 reserveId,uint256 amount,address onBehalfOf,uint256 nonce,uint256 deadline)') 0xe85497eb293c001e8483fe105efadd1d50aa0dadfc0570b27058031dfceab2e6; @@ -36,6 +38,14 @@ library EIP712Hash { // keccak256('UpdateUserDynamicConfig(address spoke,address user,uint256 nonce,uint256 deadline)') 0xba177b1f5b5e1e709f62c19f03c97988c57752ba561de58f383ebee4e8d0a71c; + bytes32 public constant SET_USER_POSITION_MANAGER_TYPEHASH = + // keccak256('SetUserPositionManager(address user,PositionManagerUpdate[] updates,uint256 nonce,uint256 deadline)PositionManagerUpdate(address positionManager,bool approve)') + 0x585e1e37b666d270ee2f5249e16d075b3790ba51e019b5c949396d40af4cb092; + + bytes32 public constant POSITION_MANAGER_UPDATE = + // keccak256('PositionManagerUpdate(address positionManager,bool approve)') + 0x187dbd227227274b90655fb4011fc21dd749e8966fc040bd91e0b92609202565; + function hash(EIP712Types.Supply calldata params) internal pure returns (bytes32) { return keccak256( @@ -138,4 +148,27 @@ library EIP712Hash { ) ); } + + function hash( + EIP712Types.SetUserPositionManager calldata params + ) internal pure returns (bytes32) { + bytes32[] memory updatesHashes = new bytes32[](params.updates.length); + for (uint256 i = 0; i < updatesHashes.length; ++i) { + updatesHashes[i] = params.updates[i].hash(); + } + return + keccak256( + abi.encode( + SET_USER_POSITION_MANAGER_TYPEHASH, + params.user, + keccak256(abi.encodePacked(updatesHashes)), + params.nonce, + params.deadline + ) + ); + } + + function hash(EIP712Types.PositionManagerUpdate calldata params) internal pure returns (bytes32) { + return keccak256(abi.encode(POSITION_MANAGER_UPDATE, params.positionManager, params.approve)); + } } diff --git a/src/spoke/Spoke.sol b/src/spoke/Spoke.sol index d71faaa03..6281b909c 100644 --- a/src/spoke/Spoke.sol +++ b/src/spoke/Spoke.sol @@ -5,9 +5,10 @@ pragma solidity 0.8.28; import {SafeCast} from 'src/dependencies/openzeppelin/SafeCast.sol'; import {SafeERC20, IERC20} from 'src/dependencies/openzeppelin/SafeERC20.sol'; import {IERC20Permit} from 'src/dependencies/openzeppelin/IERC20Permit.sol'; -import {SignatureChecker} from 'src/dependencies/openzeppelin/SignatureChecker.sol'; +import {SignatureChecker} from 'src/libraries/misc/SignatureChecker.sol'; import {AccessManagedUpgradeable} from 'src/dependencies/openzeppelin-upgradeable/AccessManagedUpgradeable.sol'; import {EIP712} from 'src/dependencies/solady/EIP712.sol'; +import {EIP712Hash, EIP712Types} from 'src/position-manager/libraries/EIP712Hash.sol'; import {MathUtils} from 'src/libraries/math/MathUtils.sol'; import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; @@ -37,11 +38,7 @@ abstract contract Spoke is ISpoke, Multicall, NoncesKeyed, AccessManagedUpgradea using PositionStatusMap for *; using ReserveFlagsMap for ReserveFlags; using UserPositionDebt for ISpoke.UserPosition; - - /// @inheritdoc ISpoke - bytes32 public constant SET_USER_POSITION_MANAGER_TYPEHASH = - // keccak256('SetUserPositionManager(address positionManager,address user,bool approve,uint256 nonce,uint256 deadline)') - 0x758d23a3c07218b7ea0b4f7f63903c4e9d5cbde72d3bcfe3e9896639025a0214; + using EIP712Hash for EIP712Types.SetUserPositionManager; /// @inheritdoc ISpoke address public immutable ORACLE; @@ -448,29 +445,23 @@ abstract contract Spoke is ISpoke, Multicall, NoncesKeyed, AccessManagedUpgradea /// @inheritdoc ISpoke function setUserPositionManagerWithSig( - address positionManager, - address user, - bool approve, - uint256 nonce, - uint256 deadline, + EIP712Types.SetUserPositionManager calldata params, bytes calldata signature ) external { - require(block.timestamp <= deadline, InvalidSignature()); - bytes32 digest = _hashTypedData( - keccak256( - abi.encode( - SET_USER_POSITION_MANAGER_TYPEHASH, - positionManager, - user, - approve, - nonce, - deadline - ) - ) + bytes32 digest = _hashTypedData(params.hash()); + require( + block.timestamp <= params.deadline && + SignatureChecker.isValidSignatureNow(params.user, digest, signature), + InvalidSignature() ); - require(SignatureChecker.isValidSignatureNow(user, digest, signature), InvalidSignature()); - _useCheckedNonce(user, nonce); - _setUserPositionManager({positionManager: positionManager, user: user, approve: approve}); + _useCheckedNonce(params.user, params.nonce); + for (uint256 i = 0; i < params.updates.length; ++i) { + _setUserPositionManager({ + positionManager: params.updates[i].positionManager, + user: params.user, + approve: params.updates[i].approve + }); + } } /// @inheritdoc ISpoke @@ -693,7 +684,7 @@ abstract contract Spoke is ISpoke, Multicall, NoncesKeyed, AccessManagedUpgradea function _setUserPositionManager(address positionManager, address user, bool approve) internal { PositionManagerConfig storage config = _positionManager[positionManager]; // only allow approval when position manager is active for improved UX - require(!approve || config.active, InactivePositionManager()); + require(!approve || config.active, InactivePositionManager()); // todo rm this ux check given sig batching? config.approval[user] = approve; emit SetUserPositionManager(user, positionManager, approve); } diff --git a/src/spoke/interfaces/ISpoke.sol b/src/spoke/interfaces/ISpoke.sol index 29a0a5819..71a3e905d 100644 --- a/src/spoke/interfaces/ISpoke.sol +++ b/src/spoke/interfaces/ISpoke.sol @@ -7,6 +7,7 @@ import {INoncesKeyed} from 'src/interfaces/INoncesKeyed.sol'; import {IMulticall} from 'src/interfaces/IMulticall.sol'; import {IHubBase} from 'src/hub/interfaces/IHubBase.sol'; import {ISpokeBase} from 'src/spoke/interfaces/ISpokeBase.sol'; +import {EIP712Types} from 'src/libraries/types/EIP712Types.sol'; type ReserveFlags is uint8; @@ -403,18 +404,10 @@ interface ISpoke is ISpokeBase, IMulticall, INoncesKeyed, IAccessManaged { /// @notice Enables a user to grant or revoke approval for a position manager using an EIP712-typed intent. /// @dev Uses keyed-nonces where for each key's namespace nonce is consumed sequentially. - /// @param positionManager The address of the position manager. - /// @param user The address of the user on whose behalf 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 signature. + /// @param params The structured setUserPositionManager parameters. /// @param signature The EIP712-compliant signature bytes. function setUserPositionManagerWithSig( - address positionManager, - address user, - bool approve, - uint256 nonce, - uint256 deadline, + EIP712Types.SetUserPositionManager calldata params, bytes calldata signature ) external; @@ -528,10 +521,6 @@ interface ISpoke is ISpokeBase, IMulticall, INoncesKeyed, IAccessManaged { /// @return The address of the library. function getLiquidationLogic() external pure returns (address); - /// @notice Returns the type hash for the SetUserPositionManager intent. - /// @return The bytes-encoded EIP-712 struct hash representing the intent. - function SET_USER_POSITION_MANAGER_TYPEHASH() external view returns (bytes32); - /// @notice Returns the address of the AaveOracle contract. function ORACLE() external view returns (address); } diff --git a/tests/Base.t.sol b/tests/Base.t.sol index 424bc3683..1385131fe 100644 --- a/tests/Base.t.sol +++ b/tests/Base.t.sol @@ -33,7 +33,7 @@ import {IERC1967} from 'src/dependencies/openzeppelin/IERC1967.sol'; import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; import {MathUtils} from 'src/libraries/math/MathUtils.sol'; import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; -import {EIP712Types} from 'src/libraries/types/EIP712Types.sol'; +import {EIP712Hash, EIP712Types} from 'src/position-manager/libraries/EIP712Hash.sol'; import {Roles} from 'src/libraries/types/Roles.sol'; import {Rescuable, IRescuable} from 'src/utils/Rescuable.sol'; import {NoncesKeyed, INoncesKeyed} from 'src/utils/NoncesKeyed.sol'; diff --git a/tests/Constants.sol b/tests/Constants.sol index b32ed1d4f..405e63232 100644 --- a/tests/Constants.sol +++ b/tests/Constants.sol @@ -16,7 +16,7 @@ library Constants { uint24 public constant MAX_ALLOWED_COLLATERAL_RISK = 1000_00; // 1000.00% uint256 public constant MAX_ALLOWED_DYNAMIC_CONFIG_KEY = type(uint24).max; bytes32 public constant SET_USER_POSITION_MANAGER_TYPEHASH = - // keccak256('SetUserPositionManager(address positionManager,address user,bool approve,uint256 nonce,uint256 deadline)') - 0x758d23a3c07218b7ea0b4f7f63903c4e9d5cbde72d3bcfe3e9896639025a0214; + // keccak256('SetUserPositionManager(address user,PositionManagerUpdate[] updates,uint256 nonce,uint256 deadline)PositionManagerUpdate(address positionManager,bool approve)') + 0x585e1e37b666d270ee2f5249e16d075b3790ba51e019b5c949396d40af4cb092; uint256 public constant MAX_ALLOWED_ASSET_ID = type(uint16).max; } diff --git a/tests/gas/Gateways.Operations.gas.t.sol b/tests/gas/Gateways.Operations.gas.t.sol index 148ba56c4..f0c39a5f2 100644 --- a/tests/gas/Gateways.Operations.gas.t.sol +++ b/tests/gas/Gateways.Operations.gas.t.sol @@ -236,10 +236,11 @@ contract SignatureGateway_Gas_Tests is SignatureGatewayBaseTest { function test_setSelfAsUserPositionManagerWithSig() public { vm.prank(alice); spoke1.useNonce(nonceKey); + EIP712Types.PositionManagerUpdate[] memory updates = new EIP712Types.PositionManagerUpdate[](1); + updates[0] = EIP712Types.PositionManagerUpdate(address(gateway), true); EIP712Types.SetUserPositionManager memory p = EIP712Types.SetUserPositionManager({ - positionManager: address(gateway), user: alice, - approve: true, + updates: updates, nonce: spoke1.nonces(alice, nonceKey), // note: this typed sig is forwarded to spoke deadline: _warpBeforeRandomDeadline() }); @@ -251,7 +252,7 @@ contract SignatureGateway_Gas_Tests is SignatureGatewayBaseTest { gateway.setSelfAsUserPositionManagerWithSig({ spoke: address(spoke1), user: p.user, - approve: p.approve, + approve: p.updates[0].approve, nonce: p.nonce, deadline: p.deadline, signature: signature diff --git a/tests/gas/Spoke.Operations.gas.t.sol b/tests/gas/Spoke.Operations.gas.t.sol index aa6d2b8d9..c850c249c 100644 --- a/tests/gas/Spoke.Operations.gas.t.sol +++ b/tests/gas/Spoke.Operations.gas.t.sol @@ -299,39 +299,27 @@ contract SpokeOperations_Gas_Tests is SpokeBase { vm.prank(user); spoke.useNonce(nonceKey); - EIP712Types.SetUserPositionManager memory params = EIP712Types.SetUserPositionManager({ - positionManager: positionManager, + EIP712Types.PositionManagerUpdate[] memory updates = new EIP712Types.PositionManagerUpdate[](1); + updates[0] = EIP712Types.PositionManagerUpdate(positionManager, true); + + EIP712Types.SetUserPositionManager memory p = EIP712Types.SetUserPositionManager({ user: user, - approve: true, + updates: updates, nonce: spoke.nonces(user, nonceKey), deadline: vm.randomUint(vm.getBlockTimestamp(), MAX_SKIP_TIME) }); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPk, _getTypedDataHash(spoke, params)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPk, _getTypedDataHash(spoke, p)); bytes memory signature = abi.encodePacked(r, s, v); - spoke.setUserPositionManagerWithSig( - params.positionManager, - params.user, - params.approve, - params.nonce, - params.deadline, - signature - ); + spoke.setUserPositionManagerWithSig(p, signature); vm.snapshotGasLastCall(NAMESPACE, 'setUserPositionManagerWithSig: enable'); - params.approve = false; - params.nonce = spoke.nonces(user, nonceKey); - (v, r, s) = vm.sign(userPk, _getTypedDataHash(spoke, params)); + p.updates[0].approve = false; + p.nonce = spoke.nonces(user, nonceKey); + (v, r, s) = vm.sign(userPk, _getTypedDataHash(spoke, p)); signature = abi.encodePacked(r, s, v); - spoke.setUserPositionManagerWithSig( - params.positionManager, - params.user, - params.approve, - params.nonce, - params.deadline, - signature - ); + spoke.setUserPositionManagerWithSig(p, signature); vm.snapshotGasLastCall(NAMESPACE, 'setUserPositionManagerWithSig: disable'); } diff --git a/tests/mocks/JsonBindings.sol b/tests/mocks/JsonBindings.sol index fad61c114..b79932edf 100644 --- a/tests/mocks/JsonBindings.sol +++ b/tests/mocks/JsonBindings.sol @@ -39,7 +39,9 @@ library JsonBindings { Vm constant vm = Vm(address(uint160(uint256(keccak256('hevm cheat code'))))); // prettier-ignore - string constant schema_SetUserPositionManager = "SetUserPositionManager(address positionManager,address user,bool approve,uint256 nonce,uint256 deadline)"; + string constant schema_SetUserPositionManager = "SetUserPositionManager(address user,PositionManagerUpdate[] updates,uint256 nonce,uint256 deadline)PositionManagerUpdate(address positionManager,bool approve)"; + // prettier-ignore + string constant schema_PositionManagerUpdate = "PositionManagerUpdate(address positionManager,bool approve)"; // prettier-ignore string constant schema_Permit = "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"; // prettier-ignore diff --git a/tests/unit/Spoke/Spoke.SetUserPositionManagerWithSig.t.sol b/tests/unit/Spoke/Spoke.SetUserPositionManagerWithSig.t.sol index 3ff896f34..62db02c3a 100644 --- a/tests/unit/Spoke/Spoke.SetUserPositionManagerWithSig.t.sol +++ b/tests/unit/Spoke/Spoke.SetUserPositionManagerWithSig.t.sol @@ -67,6 +67,10 @@ contract SpokeSetUserPositionManagerWithSigTest is SpokeBase { } function test_setUserPositionManager_typeHash() public pure { + assertEq( + Constants.SET_USER_POSITION_MANAGER_TYPEHASH, + EIP712Hash.SET_USER_POSITION_MANAGER_TYPEHASH + ); assertEq( Constants.SET_USER_POSITION_MANAGER_TYPEHASH, vm.eip712HashType('SetUserPositionManager') @@ -74,11 +78,23 @@ contract SpokeSetUserPositionManagerWithSigTest is SpokeBase { assertEq( Constants.SET_USER_POSITION_MANAGER_TYPEHASH, keccak256( - 'SetUserPositionManager(address positionManager,address user,bool approve,uint256 nonce,uint256 deadline)' + 'SetUserPositionManager(address user,PositionManagerUpdate[] updates,uint256 nonce,uint256 deadline)PositionManagerUpdate(address positionManager,bool approve)' ) ); } + function test_positionManagerUpdate_typeHash() public pure { + assertEq( + EIP712Hash.POSITION_MANAGER_UPDATE, + keccak256('PositionManagerUpdate(address positionManager,bool approve)') + ); + assertEq(EIP712Hash.POSITION_MANAGER_UPDATE, vm.eip712HashType('PositionManagerUpdate')); + assertEq( + EIP712Hash.POSITION_MANAGER_UPDATE, + keccak256('PositionManagerUpdate(address positionManager,bool approve)') + ); + } + function test_setUserPositionManagerWithSig_revertsWith_InvalidSignature_dueTo_ExpiredDeadline() public { @@ -93,14 +109,7 @@ contract SpokeSetUserPositionManagerWithSigTest is SpokeBase { vm.expectRevert(ISpoke.InvalidSignature.selector); vm.prank(vm.randomAddress()); - spoke1.setUserPositionManagerWithSig( - params.positionManager, - params.user, - params.approve, - params.nonce, - params.deadline, - signature - ); + spoke1.setUserPositionManagerWithSig(params, signature); } function test_setUserPositionManagerWithSig_revertsWith_InvalidSignature_dueTo_InvalidSigner() @@ -117,14 +126,7 @@ contract SpokeSetUserPositionManagerWithSigTest is SpokeBase { vm.expectRevert(ISpoke.InvalidSignature.selector); vm.prank(vm.randomAddress()); - spoke1.setUserPositionManagerWithSig( - params.positionManager, - params.user, - params.approve, - params.nonce, - params.deadline, - signature - ); + spoke1.setUserPositionManagerWithSig(params, signature); } function test_setUserPositionManagerWithSig_revertsWith_InvalidAccountNonce(bytes32) public { @@ -148,14 +150,7 @@ contract SpokeSetUserPositionManagerWithSigTest is SpokeBase { abi.encodeWithSelector(INoncesKeyed.InvalidAccountNonce.selector, params.user, currentNonce) ); vm.prank(vm.randomAddress()); - spoke1.setUserPositionManagerWithSig( - params.positionManager, - params.user, - params.approve, - params.nonce, - params.deadline, - signature - ); + spoke1.setUserPositionManagerWithSig(params, signature); } function test_setUserPositionManagerWithSig() public { @@ -170,20 +165,20 @@ contract SpokeSetUserPositionManagerWithSigTest is SpokeBase { bytes memory signature = abi.encodePacked(r, s, v); vm.expectEmit(address(spoke1)); - emit ISpoke.SetUserPositionManager(params.user, params.positionManager, params.approve); - - vm.prank(vm.randomAddress()); - spoke1.setUserPositionManagerWithSig( - params.positionManager, + emit ISpoke.SetUserPositionManager( params.user, - params.approve, - params.nonce, - params.deadline, - signature + params.updates[0].positionManager, + params.updates[0].approve ); + vm.prank(vm.randomAddress()); + spoke1.setUserPositionManagerWithSig(params, signature); + _assertNonceIncrement(spoke1, params.user, params.nonce); - assertEq(spoke1.isPositionManager(params.user, params.positionManager), params.approve); + assertEq( + spoke1.isPositionManager(params.user, params.updates[0].positionManager), + params.updates[0].approve + ); } function test_setUserPositionManagerWithSig_ERC1271_revertsWith_InvalidSignature_dueTo_ExpiredDeadline() @@ -205,14 +200,7 @@ contract SpokeSetUserPositionManagerWithSigTest is SpokeBase { vm.expectRevert(ISpoke.InvalidSignature.selector); vm.prank(vm.randomAddress()); - spoke1.setUserPositionManagerWithSig( - params.positionManager, - params.user, - params.approve, - params.nonce, - params.deadline, - signature - ); + spoke1.setUserPositionManagerWithSig(params, signature); } function test_setUserPositionManagerWithSig_ERC1271_revertsWith_InvalidSignature_dueTo_InvalidHash() @@ -235,7 +223,7 @@ contract SpokeSetUserPositionManagerWithSigTest is SpokeBase { address(smartWallet), deadline ); - invalidParams.positionManager = maliciousManager; + invalidParams.updates[0].positionManager = maliciousManager; (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePk, _getTypedDataHash(spoke1, invalidParams)); bytes memory signature = abi.encodePacked(r, s, v); @@ -243,16 +231,11 @@ contract SpokeSetUserPositionManagerWithSigTest is SpokeBase { vm.prank(alice); smartWallet.approveHash(digest); + invalidParams.nonce = params.nonce; + vm.expectRevert(ISpoke.InvalidSignature.selector); vm.prank(vm.randomAddress()); - spoke1.setUserPositionManagerWithSig( - invalidParams.positionManager, - invalidParams.user, - invalidParams.approve, - params.nonce, - invalidParams.deadline, - signature - ); + spoke1.setUserPositionManagerWithSig(invalidParams, signature); } function test_setUserPositionManagerWithSig_ERC1271_revertsWith_InvalidAccountNonce( @@ -286,14 +269,7 @@ contract SpokeSetUserPositionManagerWithSigTest is SpokeBase { ) ); vm.prank(vm.randomAddress()); - spoke1.setUserPositionManagerWithSig( - params.positionManager, - params.user, - params.approve, - params.nonce, - params.deadline, - signature - ); + spoke1.setUserPositionManagerWithSig(params, signature); } function test_setUserPositionManagerWithSig_ERC1271() public { @@ -319,30 +295,31 @@ contract SpokeSetUserPositionManagerWithSigTest is SpokeBase { bytes memory signature = abi.encodePacked(r, s, v); vm.expectEmit(address(spoke1)); - emit ISpoke.SetUserPositionManager(params.user, params.positionManager, params.approve); - - vm.prank(vm.randomAddress()); - spoke1.setUserPositionManagerWithSig( - params.positionManager, + emit ISpoke.SetUserPositionManager( params.user, - params.approve, - params.nonce, - params.deadline, - signature + params.updates[0].positionManager, + params.updates[0].approve ); + vm.prank(vm.randomAddress()); + spoke1.setUserPositionManagerWithSig(params, signature); + _assertNonceIncrement(spoke1, params.user, params.nonce); - assertEq(spoke1.isPositionManager(params.user, params.positionManager), params.approve); + assertEq( + spoke1.isPositionManager(params.user, params.updates[0].positionManager), + params.updates[0].approve + ); } function _setUserPositionManagerData( address user, uint256 deadline ) internal returns (EIP712Types.SetUserPositionManager memory) { + EIP712Types.PositionManagerUpdate[] memory updates = new EIP712Types.PositionManagerUpdate[](1); + updates[0] = EIP712Types.PositionManagerUpdate(POSITION_MANAGER, true); EIP712Types.SetUserPositionManager memory params = EIP712Types.SetUserPositionManager({ - positionManager: POSITION_MANAGER, user: user, - approve: vm.randomBool(), + updates: updates, nonce: spoke1.nonces(user, _randomNonceKey()), deadline: deadline }); diff --git a/tests/unit/misc/SignatureGateway/SignatureGateway.SetSelfAsUserPositionManagerWithSig.t.sol b/tests/unit/misc/SignatureGateway/SignatureGateway.SetSelfAsUserPositionManagerWithSig.t.sol index a524035ae..7c2815eb0 100644 --- a/tests/unit/misc/SignatureGateway/SignatureGateway.SetSelfAsUserPositionManagerWithSig.t.sol +++ b/tests/unit/misc/SignatureGateway/SignatureGateway.SetSelfAsUserPositionManagerWithSig.t.sol @@ -19,27 +19,28 @@ contract SignatureGatewaySetSelfAsUserPositionManagerTest is SignatureGatewayBas } function test_setSelfAsUserPositionManagerWithSig_forwards_correct_call() public { - address user = vm.randomAddress(); - bool approve = vm.randomBool(); - uint256 nonce = vm.randomUint(); - uint256 deadline = vm.randomUint(); + EIP712Types.PositionManagerUpdate[] memory updates = new EIP712Types.PositionManagerUpdate[](1); + updates[0] = EIP712Types.PositionManagerUpdate(address(gateway), vm.randomBool()); + EIP712Types.SetUserPositionManager memory p = EIP712Types.SetUserPositionManager({ + user: vm.randomAddress(), + updates: updates, + nonce: vm.randomUint(), + deadline: vm.randomUint() + }); bytes memory signature = vm.randomBytes(72); vm.expectCall( address(spoke1), - abi.encodeCall( - ISpoke.setUserPositionManagerWithSig, - (address(gateway), user, approve, nonce, deadline, signature) - ), + abi.encodeCall(ISpoke.setUserPositionManagerWithSig, (p, signature)), 1 ); vm.prank(vm.randomAddress()); gateway.setSelfAsUserPositionManagerWithSig({ spoke: address(spoke1), - user: user, - approve: approve, - nonce: nonce, - deadline: deadline, + user: p.user, + approve: p.updates[0].approve, + nonce: p.nonce, + deadline: p.deadline, signature: signature }); } @@ -68,10 +69,11 @@ contract SignatureGatewaySetSelfAsUserPositionManagerTest is SignatureGatewayBas uint192 nonceKey = _randomNonceKey(); vm.prank(alice); spoke1.useNonce(nonceKey); + EIP712Types.PositionManagerUpdate[] memory updates = new EIP712Types.PositionManagerUpdate[](1); + updates[0] = EIP712Types.PositionManagerUpdate(address(gateway), true); EIP712Types.SetUserPositionManager memory p = EIP712Types.SetUserPositionManager({ - positionManager: address(gateway), user: alice, - approve: true, + updates: updates, nonce: spoke1.nonces(alice, nonceKey), // note: this typed sig is forwarded to spoke deadline: _warpBeforeRandomDeadline() }); @@ -85,7 +87,7 @@ contract SignatureGatewaySetSelfAsUserPositionManagerTest is SignatureGatewayBas gateway.setSelfAsUserPositionManagerWithSig({ spoke: address(spoke1), user: p.user, - approve: p.approve, + approve: p.updates[0].approve, nonce: p.nonce, deadline: p.deadline, signature: signature diff --git a/tests/unit/misc/SignatureGateway/SignatureGateway.t.sol b/tests/unit/misc/SignatureGateway/SignatureGateway.t.sol index e12b26e0a..6166d58d5 100644 --- a/tests/unit/misc/SignatureGateway/SignatureGateway.t.sol +++ b/tests/unit/misc/SignatureGateway/SignatureGateway.t.sol @@ -232,23 +232,25 @@ contract SignatureGatewayTest is SignatureGatewayBaseTest { } function test_setSelfAsUserPositionManagerWithSig() public { + EIP712Types.PositionManagerUpdate[] memory updates = new EIP712Types.PositionManagerUpdate[](1); + updates[0] = EIP712Types.PositionManagerUpdate(address(gateway), true); + EIP712Types.SetUserPositionManager memory p = EIP712Types.SetUserPositionManager({ - positionManager: address(gateway), + updates: updates, user: alice, - approve: true, nonce: spoke1.nonces(address(alice), _randomNonceKey()), // note: this typed sig is forwarded to spoke deadline: _warpBeforeRandomDeadline() }); bytes memory signature = _sign(alicePk, _getTypedDataHash(spoke1, p)); vm.expectEmit(address(spoke1)); - emit ISpoke.SetUserPositionManager(alice, address(gateway), p.approve); + emit ISpoke.SetUserPositionManager(alice, address(gateway), p.updates[0].approve); vm.prank(vm.randomAddress()); gateway.setSelfAsUserPositionManagerWithSig({ spoke: address(spoke1), user: p.user, - approve: p.approve, + approve: p.updates[0].approve, nonce: p.nonce, deadline: p.deadline, signature: signature