From b28335783ae290c4577d574cb6dbe68478dc210a Mon Sep 17 00:00:00 2001 From: AlbertoCentonze <11707683+AlbertoCentonze@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:07:01 +0200 Subject: [PATCH] refactor: make spoke gates first class --- foundry.toml | 1 - .../deploy/examples/AaveV4DeployAnvil.s.sol | 1 + .../ConfigPositionManager.Operations.json | 6 +- .../GiverPositionManager.Operations.json | 4 +- snapshots/NativeTokenGateway.Operations.json | 12 +- snapshots/PermissionedSpoke.Operations.json | 17 -- snapshots/PositionManagerBase.Operations.json | 2 +- snapshots/SignatureGateway.Operations.json | 16 +- snapshots/Spoke.Gate.Operations.json | 17 ++ snapshots/Spoke.Getters.json | 10 +- .../Spoke.Operations.ZeroRiskPremium.json | 64 +++--- snapshots/Spoke.Operations.json | 64 +++--- .../TakerPositionManager.Operations.json | 6 +- snapshots/TokenizationSpoke.Operations.json | 6 +- .../batches/AaveV4SpokeInstanceBatch.sol | 7 +- src/deployments/libraries/BatchReports.sol | 2 + .../libraries/OrchestrationReports.sol | 2 + .../orchestration/AaveV4DeployBase.sol | 3 + .../AaveV4DeployOrchestration.sol | 21 ++ .../spoke/AaveV4SpokeDeployProcedure.sol | 11 +- .../roles/AaveV4SpokeRolesProcedure.sol | 11 + .../utils/libraries/InputUtils.sol | 2 + src/deployments/utils/libraries/Roles.sol | 5 +- src/position-manager/PositionManagerBase.sol | 17 +- src/spoke/Spoke.sol | 117 ++--------- src/spoke/SpokeConfigurator.sol | 7 +- src/spoke/SpokeStorage.sol | 5 +- src/spoke/gates/PositionManagerGate.sol | 122 +++++++++++ .../instances/PermissionedSpokeInstance.sol | 38 ---- src/spoke/instances/SpokeInstance.sol | 6 +- src/spoke/instances/SpokeInstanceBase.sol | 9 +- src/spoke/interfaces/IPositionManagerGate.sol | 70 +++++++ src/spoke/interfaces/ISpoke.sol | 106 ++-------- src/spoke/interfaces/ISpokeGate.sol | 7 +- src/spoke/libraries/EIP712Hash.sol | 40 ---- .../PositionManagerGateEIP712Hash.sol | 48 +++++ tests/config-engine/AaveV4Payload.t.sol | 40 ++-- tests/config-engine/BaseConfigEngine.t.sol | 13 +- .../config-engine/PositionManagerEngine.t.sol | 15 +- tests/config-engine/SpokeEngine.t.sol | 18 +- .../ConfigPositionManager.Base.t.sol | 4 +- .../GiverPositionManager.t.sol | 6 +- .../position-manager/NativeTokenGateway.t.sol | 36 ++-- .../PositionManagerBase.t.sol | 48 +++-- ...ateway.Reverts.InsufficientAllowance.t.sol | 8 +- ...reGateway.Reverts.SpokeNotRegistered.t.sol | 8 +- ...ignatureGateway.Reverts.Unauthorized.t.sol | 10 +- ....SetSelfAsUserPositionManagerWithSig.t.sol | 54 ++--- .../SignatureGateway/SignatureGateway.t.sol | 45 ++-- .../TakerPositionManager.Base.t.sol | 4 +- .../Spoke.DynamicConfig.Triggers.t.sol | 6 +- .../configuration/Spoke.DynamicConfig.t.sol | 2 +- ...keConfigurator.GranularAccessControl.t.sol | 2 +- .../configurator/SpokeConfigurator.t.sol | 13 +- .../spoke/libraries/EIP712Hash.t.sol | 34 ++- ...rmissionedSpoke.t.sol => Spoke.Gate.t.sol} | 31 +-- .../spoke/misc/Spoke.Upgradeable.t.sol | 6 +- .../spoke/misc/Spoke.UserAccountData.t.sol | 2 +- .../Spoke.PositionManager.t.sol | 64 +++--- .../Spoke.SetUserPositionManagerWithSig.t.sol | 198 +++++++++++------- tests/deployments/AaveV4BatchDeployment.t.sol | 1 + .../batches/AaveV4SpokeInstanceBatch.t.sol | 6 + tests/deployments/batches/BatchBase.t.sol | 3 + .../fork/PostDeploymentVerificationTest.t.sol | 2 + .../orchestration/AaveV4TestOrchestration.sol | 19 +- .../procedures/ProceduresBase.t.sol | 3 + .../roles/AaveV4SpokeRolesProcedure.t.sol | 22 +- .../spoke/AaveV4SpokeDeployProcedure.t.sol | 5 + tests/gas/Gateways.Operations.gas.t.sol | 31 +-- .../gas/PositionManagers.Operations.gas.t.sol | 41 ++-- ....t.sol => Spoke.Gate.Operations.gas.t.sol} | 13 +- tests/gas/Spoke.Operations.gas.t.sol | 33 +-- .../mocks/AaveV4TestOrchestrationWrapper.sol | 11 + tests/helpers/mocks/EIP712Types.sol | 1 + tests/helpers/mocks/JsonBindings.sol | 2 +- tests/helpers/mocks/MockSpoke.sol | 7 +- tests/helpers/mocks/MockSpokeGate.sol | 5 +- tests/helpers/mocks/MockSpokeInstance.sol | 8 +- tests/helpers/mocks/PolicyGates.sol | 15 +- .../AaveV4SpokeDeployProcedureWrapper.sol | 2 + tests/helpers/spoke/EIP712Helpers.sol | 5 +- tests/helpers/spoke/MathHelpers.sol | 4 +- .../spoke/PositionManagerGateAdapter.sol | 95 +++++++++ tests/helpers/spoke/SetupHelpers.sol | 15 +- .../scripts/AaveV4DeployBatchBaseScript.t.sol | 1 + tests/setup/Base.t.sol | 10 +- ...ssionedSpokeBase.sol => GateSpokeBase.sol} | 14 +- 87 files changed, 1157 insertions(+), 766 deletions(-) delete mode 100644 snapshots/PermissionedSpoke.Operations.json create mode 100644 snapshots/Spoke.Gate.Operations.json create mode 100644 src/spoke/gates/PositionManagerGate.sol delete mode 100644 src/spoke/instances/PermissionedSpokeInstance.sol create mode 100644 src/spoke/interfaces/IPositionManagerGate.sol create mode 100644 src/spoke/libraries/PositionManagerGateEIP712Hash.sol rename tests/contracts/spoke/misc/{PermissionedSpoke.t.sol => Spoke.Gate.t.sol} (86%) rename tests/gas/{PermissionedSpoke.Operations.gas.t.sol => Spoke.Gate.Operations.gas.t.sol} (79%) create mode 100644 tests/helpers/spoke/PositionManagerGateAdapter.sol rename tests/setup/{PermissionedSpokeBase.sol => GateSpokeBase.sol} (84%) diff --git a/foundry.toml b/foundry.toml index 72f9ab48a..1411ebe9a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -27,7 +27,6 @@ additional_compiler_profiles = [ compilation_restrictions = [ { paths = "src/hub/instances/HubInstance.sol", via_ir = true, optimizer_runs = 22_300 }, { paths = "src/spoke/instances/SpokeInstance.sol", via_ir = true, optimizer_runs = 750 }, - { paths = "src/spoke/instances/PermissionedSpokeInstance.sol", via_ir = true, optimizer_runs = 750 }, ] [bind_json] diff --git a/scripts/deploy/examples/AaveV4DeployAnvil.s.sol b/scripts/deploy/examples/AaveV4DeployAnvil.s.sol index 279740f7f..4e6f21a41 100644 --- a/scripts/deploy/examples/AaveV4DeployAnvil.s.sol +++ b/scripts/deploy/examples/AaveV4DeployAnvil.s.sol @@ -49,6 +49,7 @@ contract AaveV4DeployAnvil is AaveV4DeployBatchBaseScript { spokeConfiguratorAdmin: address(1), gatewayOwner: address(2), positionManagerOwner: address(3), + spokeGate: address(0), nativeWrapper: weth, deployNativeTokenGateway: true, deploySignatureGateway: true, diff --git a/snapshots/ConfigPositionManager.Operations.json b/snapshots/ConfigPositionManager.Operations.json index d6a8bc2dc..49164b453 100644 --- a/snapshots/ConfigPositionManager.Operations.json +++ b/snapshots/ConfigPositionManager.Operations.json @@ -7,7 +7,7 @@ "setCanUpdateUserDynamicConfigPermission": "50158", "setCanUpdateUserRiskPremiumPermission": "50158", "setGlobalPermission": "50110", - "setUsingAsCollateralOnBehalfOf": "77828", - "updateUserDynamicConfigOnBehalfOf": "52342", - "updateUserRiskPremiumOnBehalfOf": "140135" + "setUsingAsCollateralOnBehalfOf": "81569", + "updateUserDynamicConfigOnBehalfOf": "56254", + "updateUserRiskPremiumOnBehalfOf": "143955" } \ No newline at end of file diff --git a/snapshots/GiverPositionManager.Operations.json b/snapshots/GiverPositionManager.Operations.json index 35e0a2001..9e2c5c7cf 100644 --- a/snapshots/GiverPositionManager.Operations.json +++ b/snapshots/GiverPositionManager.Operations.json @@ -1,4 +1,4 @@ { - "repayOnBehalfOf": "176497", - "supplyOnBehalfOf": "143072" + "repayOnBehalfOf": "180173", + "supplyOnBehalfOf": "146148" } \ No newline at end of file diff --git a/snapshots/NativeTokenGateway.Operations.json b/snapshots/NativeTokenGateway.Operations.json index 837c577c6..d623965c3 100644 --- a/snapshots/NativeTokenGateway.Operations.json +++ b/snapshots/NativeTokenGateway.Operations.json @@ -1,8 +1,8 @@ { - "borrowNative": "238707", - "repayNative": "172321", - "supplyAsCollateralNative": "165113", - "supplyNative": "139728", - "withdrawNative: full": "129898", - "withdrawNative: partial": "142172" + "borrowNative": "242424", + "repayNative": "175997", + "supplyAsCollateralNative": "170331", + "supplyNative": "142804", + "withdrawNative: full": "133022", + "withdrawNative: partial": "146077" } \ No newline at end of file diff --git a/snapshots/PermissionedSpoke.Operations.json b/snapshots/PermissionedSpoke.Operations.json deleted file mode 100644 index 7f5bed503..000000000 --- a/snapshots/PermissionedSpoke.Operations.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "borrow: borrow-allowlist policy": "303427", - "borrow: global-manager policy": "297890", - "borrow: position-manager policy": "297766", - "repay: partial, borrow-allowlist policy": "150680", - "repay: partial, global-manager policy": "150537", - "repay: partial, position-manager policy": "150413", - "supply: borrow-allowlist policy": "132289", - "supply: global-manager policy": "132146", - "supply: position-manager policy": "132022", - "usingAsCollateral: enable, borrow-allowlist policy": "64562", - "usingAsCollateral: enable, global-manager policy": "64419", - "usingAsCollateral: enable, position-manager policy": "64295", - "withdraw: partial, borrow-allowlist policy": "185421", - "withdraw: partial, global-manager policy": "185278", - "withdraw: partial, position-manager policy": "185154" -} \ No newline at end of file diff --git a/snapshots/PositionManagerBase.Operations.json b/snapshots/PositionManagerBase.Operations.json index afd9324b0..6039c5588 100644 --- a/snapshots/PositionManagerBase.Operations.json +++ b/snapshots/PositionManagerBase.Operations.json @@ -1,3 +1,3 @@ { - "setSelfAsUserPositionManagerWithSig": "75041" + "setSelfAsUserPositionManagerWithSig": "97035" } \ No newline at end of file diff --git a/snapshots/SignatureGateway.Operations.json b/snapshots/SignatureGateway.Operations.json index 93a4414b0..4e39aaa30 100644 --- a/snapshots/SignatureGateway.Operations.json +++ b/snapshots/SignatureGateway.Operations.json @@ -1,10 +1,10 @@ { - "borrowWithSig": "222144", - "repayWithSig": "192513", - "setSelfAsUserPositionManagerWithSig": "75138", - "setUsingAsCollateralWithSig": "85380", - "supplyWithSig": "155914", - "updateUserDynamicConfigWithSig": "63113", - "updateUserRiskPremiumWithSig": "61995", - "withdrawWithSig": "135124" + "borrowWithSig": "225860", + "repayWithSig": "196189", + "setSelfAsUserPositionManagerWithSig": "97112", + "setUsingAsCollateralWithSig": "89253", + "supplyWithSig": "158990", + "updateUserDynamicConfigWithSig": "67025", + "updateUserRiskPremiumWithSig": "65875", + "withdrawWithSig": "138497" } \ No newline at end of file diff --git a/snapshots/Spoke.Gate.Operations.json b/snapshots/Spoke.Gate.Operations.json new file mode 100644 index 000000000..bb55a971b --- /dev/null +++ b/snapshots/Spoke.Gate.Operations.json @@ -0,0 +1,17 @@ +{ + "borrow: borrow-allowlist policy": "302550", + "borrow: global-manager policy": "297141", + "borrow: position-manager policy": "297290", + "repay: partial, borrow-allowlist policy": "149851", + "repay: partial, global-manager policy": "149774", + "repay: partial, position-manager policy": "149923", + "supply: borrow-allowlist policy": "131268", + "supply: global-manager policy": "131191", + "supply: position-manager policy": "131340", + "usingAsCollateral: enable, borrow-allowlist policy": "63093", + "usingAsCollateral: enable, global-manager policy": "63016", + "usingAsCollateral: enable, position-manager policy": "63165", + "withdraw: partial, borrow-allowlist policy": "184831", + "withdraw: partial, global-manager policy": "184754", + "withdraw: partial, position-manager policy": "184903" +} \ No newline at end of file diff --git a/snapshots/Spoke.Getters.json b/snapshots/Spoke.Getters.json index 157832701..b70a77a13 100644 --- a/snapshots/Spoke.Getters.json +++ b/snapshots/Spoke.Getters.json @@ -1,7 +1,7 @@ { - "getUserAccountData: supplies: 0, borrows: 0": "13014", - "getUserAccountData: supplies: 1, borrows: 0": "56072", - "getUserAccountData: supplies: 2, borrows: 0": "89894", - "getUserAccountData: supplies: 2, borrows: 1": "112389", - "getUserAccountData: supplies: 2, borrows: 2": "133792" + "getUserAccountData: supplies: 0, borrows: 0": "12860", + "getUserAccountData: supplies: 1, borrows: 0": "55918", + "getUserAccountData: supplies: 2, borrows: 0": "89740", + "getUserAccountData: supplies: 2, borrows: 1": "112187", + "getUserAccountData: supplies: 2, borrows: 2": "133590" } \ No newline at end of file diff --git a/snapshots/Spoke.Operations.ZeroRiskPremium.json b/snapshots/Spoke.Operations.ZeroRiskPremium.json index fcf844086..2e2c28755 100644 --- a/snapshots/Spoke.Operations.ZeroRiskPremium.json +++ b/snapshots/Spoke.Operations.ZeroRiskPremium.json @@ -1,34 +1,34 @@ { - "borrow: first": "199509", - "borrow: second action, same reserve": "179375", - "liquidationCall (receiveShares): full": "314227", - "liquidationCall (receiveShares): partial": "313645", - "liquidationCall (reportDeficit): full": "380307", - "liquidationCall: full": "332763", - "liquidationCall: partial": "332181", - "permitReserve + repay (multicall)": "169938", - "permitReserve + supply (multicall)": "151663", - "permitReserve + supply + enable collateral (multicall)": "166114", - "repay: full": "129276", - "repay: partial": "134234", - "setUserPositionManagersWithSig: disable": "46772", - "setUserPositionManagersWithSig: enable": "68684", - "supply + enable collateral (multicall)": "146316", - "supply: 0 borrows, collateral disabled": "127753", - "supply: 0 borrows, collateral enabled": "110724", - "supply: second action, same reserve": "110653", - "updateUserDynamicConfig: 1 collateral": "76251", - "updateUserDynamicConfig: 2 collaterals": "92825", - "updateUserRiskPremium: 1 borrow": "104446", - "updateUserRiskPremium: 2 borrows": "114563", - "usingAsCollateral: 0 borrows, enable": "59616", - "usingAsCollateral: 1 borrow, disable": "114490", - "usingAsCollateral: 1 borrow, enable": "42504", - "usingAsCollateral: 2 borrows, disable": "138182", - "usingAsCollateral: 2 borrows, enable": "42516", - "withdraw: 0 borrows, full": "135058", - "withdraw: 0 borrows, partial": "140394", - "withdraw: 1 borrow, partial": "169591", - "withdraw: 2 borrows, partial": "186292", - "withdraw: non collateral": "111299" + "borrow: first": "203221", + "borrow: second action, same reserve": "183087", + "liquidationCall (receiveShares): full": "313977", + "liquidationCall (receiveShares): partial": "313395", + "liquidationCall (reportDeficit): full": "380105", + "liquidationCall: full": "332513", + "liquidationCall: partial": "331931", + "permitReserve + repay (multicall)": "173482", + "permitReserve + supply (multicall)": "155254", + "permitReserve + supply + enable collateral (multicall)": "170993", + "repay: full": "133058", + "repay: partial": "138016", + "setUserPositionManagersWithSig: disable": "43946", + "setUserPositionManagersWithSig: enable": "82934", + "supply + enable collateral (multicall)": "151244", + "supply: 0 borrows, collateral disabled": "131594", + "supply: 0 borrows, collateral enabled": "114565", + "supply: second action, same reserve": "114494", + "updateUserDynamicConfig: 1 collateral": "80093", + "updateUserDynamicConfig: 2 collaterals": "96667", + "updateUserRiskPremium: 1 borrow": "108196", + "updateUserRiskPremium: 2 borrows": "118314", + "usingAsCollateral: 0 borrows, enable": "63419", + "usingAsCollateral: 1 borrow, disable": "118224", + "usingAsCollateral: 1 borrow, enable": "46307", + "usingAsCollateral: 2 borrows, disable": "141916", + "usingAsCollateral: 2 borrows, enable": "46319", + "withdraw: 0 borrows, full": "138950", + "withdraw: 0 borrows, partial": "144286", + "withdraw: 1 borrow, partial": "173435", + "withdraw: 2 borrows, partial": "190136", + "withdraw: non collateral": "115200" } \ No newline at end of file diff --git a/snapshots/Spoke.Operations.json b/snapshots/Spoke.Operations.json index 086bec26b..272a22415 100644 --- a/snapshots/Spoke.Operations.json +++ b/snapshots/Spoke.Operations.json @@ -1,34 +1,34 @@ { - "borrow: first": "269297", - "borrow: second action, same reserve": "212163", - "liquidationCall (receiveShares): full": "347124", - "liquidationCall (receiveShares): partial": "346542", - "liquidationCall (reportDeficit): full": "372507", - "liquidationCall: full": "365660", - "liquidationCall: partial": "365078", - "permitReserve + repay (multicall)": "166334", - "permitReserve + supply (multicall)": "151663", - "permitReserve + supply + enable collateral (multicall)": "166114", - "repay: full": "123355", - "repay: partial": "142713", - "setUserPositionManagersWithSig: disable": "46772", - "setUserPositionManagersWithSig: enable": "68684", - "supply + enable collateral (multicall)": "146316", - "supply: 0 borrows, collateral disabled": "127753", - "supply: 0 borrows, collateral enabled": "110724", - "supply: second action, same reserve": "110653", - "updateUserDynamicConfig: 1 collateral": "76251", - "updateUserDynamicConfig: 2 collaterals": "92825", - "updateUserRiskPremium: 1 borrow": "158658", - "updateUserRiskPremium: 2 borrows": "210210", - "usingAsCollateral: 0 borrows, enable": "59616", - "usingAsCollateral: 1 borrow, disable": "168699", - "usingAsCollateral: 1 borrow, enable": "42504", - "usingAsCollateral: 2 borrows, disable": "241825", - "usingAsCollateral: 2 borrows, enable": "42516", - "withdraw: 0 borrows, full": "135058", - "withdraw: 0 borrows, partial": "140394", - "withdraw: 1 borrow, partial": "221298", - "withdraw: 2 borrows, partial": "270470", - "withdraw: non collateral": "111299" + "borrow: first": "273010", + "borrow: second action, same reserve": "215876", + "liquidationCall (receiveShares): full": "346874", + "liquidationCall (receiveShares): partial": "346292", + "liquidationCall (reportDeficit): full": "372305", + "liquidationCall: full": "365410", + "liquidationCall: partial": "364828", + "permitReserve + repay (multicall)": "169169", + "permitReserve + supply (multicall)": "155254", + "permitReserve + supply + enable collateral (multicall)": "170993", + "repay: full": "127137", + "repay: partial": "146495", + "setUserPositionManagersWithSig: disable": "43946", + "setUserPositionManagersWithSig: enable": "82934", + "supply + enable collateral (multicall)": "151244", + "supply: 0 borrows, collateral disabled": "131594", + "supply: 0 borrows, collateral enabled": "114565", + "supply: second action, same reserve": "114494", + "updateUserDynamicConfig: 1 collateral": "80093", + "updateUserDynamicConfig: 2 collaterals": "96667", + "updateUserRiskPremium: 1 borrow": "162408", + "updateUserRiskPremium: 2 borrows": "213960", + "usingAsCollateral: 0 borrows, enable": "63419", + "usingAsCollateral: 1 borrow, disable": "172433", + "usingAsCollateral: 1 borrow, enable": "46307", + "usingAsCollateral: 2 borrows, disable": "245560", + "usingAsCollateral: 2 borrows, enable": "46319", + "withdraw: 0 borrows, full": "138950", + "withdraw: 0 borrows, partial": "144286", + "withdraw: 1 borrow, partial": "225142", + "withdraw: 2 borrows, partial": "274314", + "withdraw: non collateral": "115200" } \ No newline at end of file diff --git a/snapshots/TakerPositionManager.Operations.json b/snapshots/TakerPositionManager.Operations.json index fac6a661e..fd2900b46 100644 --- a/snapshots/TakerPositionManager.Operations.json +++ b/snapshots/TakerPositionManager.Operations.json @@ -3,9 +3,9 @@ "approveBorrowWithSig": "65689", "approveWithdraw": "49816", "approveWithdrawWithSig": "65643", - "borrowOnBehalfOf": "332512", + "borrowOnBehalfOf": "336008", "renounceBorrowAllowance": "27929", "renounceWithdrawAllowance": "27983", - "withdrawOnBehalfOf: full": "127209", - "withdrawOnBehalfOf: partial": "138811" + "withdrawOnBehalfOf: full": "130333", + "withdrawOnBehalfOf: partial": "142716" } \ No newline at end of file diff --git a/snapshots/TokenizationSpoke.Operations.json b/snapshots/TokenizationSpoke.Operations.json index 9568365f8..b61abef91 100644 --- a/snapshots/TokenizationSpoke.Operations.json +++ b/snapshots/TokenizationSpoke.Operations.json @@ -2,16 +2,16 @@ "deposit": "118614", "depositWithSig": "129518", "mint": "118251", - "mintWithSig": "129130", + "mintWithSig": "129118", "permit": "62766", "redeem: on behalf, full": "95212", "redeem: on behalf, partial": "119015", "redeem: self, full": "94282", "redeem: self, partial": "113482", - "redeemWithSig": "128864", + "redeemWithSig": "128852", "withdraw: on behalf, full": "95646", "withdraw: on behalf, partial": "119557", "withdraw: self, full": "94824", "withdraw: self, partial": "114024", - "withdrawWithSig": "129405" + "withdrawWithSig": "129417" } \ No newline at end of file diff --git a/src/deployments/batches/AaveV4SpokeInstanceBatch.sol b/src/deployments/batches/AaveV4SpokeInstanceBatch.sol index fd18008a8..f3852587f 100644 --- a/src/deployments/batches/AaveV4SpokeInstanceBatch.sol +++ b/src/deployments/batches/AaveV4SpokeInstanceBatch.sol @@ -16,6 +16,7 @@ contract AaveV4SpokeInstanceBatch is AaveV4SpokeDeployProcedure, AaveV4AaveOracl /// @dev Constructor. /// @param proxyAdminOwner_ The owner of the proxy admin. /// @param authority_ The access-control authority for the Spoke. + /// @param gate_ The immutable gate for the Spoke. /// @param spokeBytecode_ The creation bytecode of the Spoke implementation. /// @param oracleDecimals_ The decimal precision for the AaveOracle. /// @param maxUserReservesLimit_ The maximum number of reserves a user can interact with. @@ -23,6 +24,7 @@ contract AaveV4SpokeInstanceBatch is AaveV4SpokeDeployProcedure, AaveV4AaveOracl constructor( address proxyAdminOwner_, address authority_, + address gate_, bytes memory spokeBytecode_, uint8 oracleDecimals_, uint16 maxUserReservesLimit_, @@ -33,6 +35,7 @@ contract AaveV4SpokeInstanceBatch is AaveV4SpokeDeployProcedure, AaveV4AaveOracl proxyAdminOwner: proxyAdminOwner_, authority: authority_, oracle: aaveOracle, + gate: gate_, spokeBytecode: spokeBytecode_, salt: salt_, maxUserReservesLimit: maxUserReservesLimit_ @@ -40,12 +43,14 @@ contract AaveV4SpokeInstanceBatch is AaveV4SpokeDeployProcedure, AaveV4AaveOracl IAaveOracle(aaveOracle).setSpoke(spokeProxy); require(ISpoke(spokeProxy).ORACLE() == aaveOracle, 'spoke oracle mismatch'); + require(ISpoke(spokeProxy).GATE() == gate_, 'spoke gate mismatch'); require(IAaveOracle(aaveOracle).spoke() == spokeProxy, 'oracle spoke mismatch'); _report = BatchReports.SpokeInstanceBatchReport({ aaveOracle: aaveOracle, spokeImplementation: spokeImplementation, - spokeProxy: spokeProxy + spokeProxy: spokeProxy, + gate: gate_ }); } diff --git a/src/deployments/libraries/BatchReports.sol b/src/deployments/libraries/BatchReports.sol index b05070568..2a14c3f75 100644 --- a/src/deployments/libraries/BatchReports.sol +++ b/src/deployments/libraries/BatchReports.sol @@ -20,10 +20,12 @@ library BatchReports { /// @dev spokeProxy The deployed Spoke proxy contract address. /// @dev spokeImplementation The deployed Spoke implementation contract address. /// @dev aaveOracle The deployed AaveOracle contract address. + /// @dev gate The immutable gate used by the Spoke. struct SpokeInstanceBatchReport { address spokeProxy; address spokeImplementation; address aaveOracle; + address gate; } /// @dev hubProxy The deployed Hub proxy contract address. diff --git a/src/deployments/libraries/OrchestrationReports.sol b/src/deployments/libraries/OrchestrationReports.sol index bd4d2e76c..e09bf744b 100644 --- a/src/deployments/libraries/OrchestrationReports.sol +++ b/src/deployments/libraries/OrchestrationReports.sol @@ -28,6 +28,7 @@ library OrchestrationReports { /// @dev hubInstanceBatchReports Per-hub deployment reports. /// @dev gatewaysBatchReport Gateway deployment report. /// @dev positionManagerBatchReport PositionManager deployment report. + /// @dev spokeGate The gate shared by deployed Spokes. /// @dev salt The salt used to derive deterministic contract addresses. struct FullDeploymentReport { BatchReports.AuthorityBatchReport authorityBatchReport; @@ -37,6 +38,7 @@ library OrchestrationReports { HubDeploymentReport[] hubInstanceBatchReports; BatchReports.GatewaysBatchReport gatewaysBatchReport; BatchReports.PositionManagerBatchReport positionManagerBatchReport; + address spokeGate; bytes32 salt; } } diff --git a/src/deployments/orchestration/AaveV4DeployBase.sol b/src/deployments/orchestration/AaveV4DeployBase.sol index 5fef5ee8e..119c000ef 100644 --- a/src/deployments/orchestration/AaveV4DeployBase.sol +++ b/src/deployments/orchestration/AaveV4DeployBase.sol @@ -85,6 +85,7 @@ library AaveV4DeployBase { /// @notice Deploys the Spoke instance batch containing the Spoke proxy, implementation, and AaveOracle. /// @param proxyAdminOwner The owner of the proxy admin. /// @param authority The access-control authority for the Spoke. + /// @param gate The immutable gate for the Spoke. /// @param spokeBytecode The creation bytecode of the SpokeInstance contract. /// @param oracleDecimals The decimal precision for the AaveOracle. /// @param maxUserReservesLimit The maximum number of reserves a user can interact with. @@ -93,6 +94,7 @@ library AaveV4DeployBase { function deploySpokeInstanceBatch( address proxyAdminOwner, address authority, + address gate, bytes memory spokeBytecode, uint8 oracleDecimals, uint16 maxUserReservesLimit, @@ -101,6 +103,7 @@ library AaveV4DeployBase { AaveV4SpokeInstanceBatch spokeInstanceBatch = new AaveV4SpokeInstanceBatch({ proxyAdminOwner_: proxyAdminOwner, authority_: authority, + gate_: gate, spokeBytecode_: spokeBytecode, oracleDecimals_: oracleDecimals, maxUserReservesLimit_: maxUserReservesLimit, diff --git a/src/deployments/orchestration/AaveV4DeployOrchestration.sol b/src/deployments/orchestration/AaveV4DeployOrchestration.sol index 55d963730..e3a366b28 100644 --- a/src/deployments/orchestration/AaveV4DeployOrchestration.sol +++ b/src/deployments/orchestration/AaveV4DeployOrchestration.sol @@ -13,6 +13,8 @@ import {AaveV4SpokeConfiguratorRolesProcedure} from 'src/deployments/procedures/ import {InputUtils} from 'src/deployments/utils/libraries/InputUtils.sol'; import {Logger} from 'src/deployments/utils/Logger.sol'; import {DeployConstants} from 'src/deployments/utils/libraries/DeployConstants.sol'; +import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; +import {PositionManagerGate} from 'src/spoke/gates/PositionManagerGate.sol'; /// @title AaveV4DeployOrchestration Library /// @author Aave Labs @@ -48,6 +50,17 @@ library AaveV4DeployOrchestration { address accessManager = report.authorityBatchReport.accessManager; + report.spokeGate = deployInputs.spokeGate; + if (report.spokeGate == address(0) && deployInputs.spokeLabels.length > 0) { + report.spokeGate = Create2Utils.create2Deploy({ + salt: _deriveChildSalt(salt, 'gate', 'position-manager'), + bytecode: abi.encodePacked( + type(PositionManagerGate).creationCode, + abi.encode(accessManager) + ) + }); + } + // Label all protocol roles logger.logHeader1('labeling roles'); AaveV4AccessManagerRolesProcedure.labelAllRoles(accessManager); @@ -88,6 +101,7 @@ library AaveV4DeployOrchestration { report.spokeInstanceBatchReports = _deploySpokes({ logger: logger, authority: accessManager, + gate: report.spokeGate, inputs: deployInputs, spokeBytecode: spokeBytecode, salt: salt @@ -201,6 +215,7 @@ library AaveV4DeployOrchestration { function _deploySpokes( Logger logger, address authority, + address gate, InputUtils.FullDeployInputs memory inputs, bytes memory spokeBytecode, bytes32 salt @@ -215,6 +230,7 @@ library AaveV4DeployOrchestration { logger: logger, proxyAdminOwner: inputs.proxyAdminOwner, authority: authority, + gate: gate, label: inputs.spokeLabels[i], spokeBytecode: spokeBytecode, maxUserReservesLimit: limitsLen > 0 @@ -232,6 +248,7 @@ library AaveV4DeployOrchestration { Logger logger, address proxyAdminOwner, address authority, + address gate, string memory label, bytes memory spokeBytecode, uint16 maxUserReservesLimit, @@ -245,6 +262,7 @@ library AaveV4DeployOrchestration { logger: logger, proxyAdminOwner: proxyAdminOwner, authority: authority, + gate: gate, spokeBytecode: spokeBytecode, oracleDecimals: oracleDecimals, maxUserReservesLimit: maxUserReservesLimit, @@ -311,6 +329,7 @@ library AaveV4DeployOrchestration { Logger logger, address proxyAdminOwner, address authority, + address gate, bytes memory spokeBytecode, uint8 oracleDecimals, uint16 maxUserReservesLimit, @@ -320,6 +339,7 @@ library AaveV4DeployOrchestration { report = AaveV4DeployBase.deploySpokeInstanceBatch({ proxyAdminOwner: proxyAdminOwner, authority: authority, + gate: gate, spokeBytecode: spokeBytecode, oracleDecimals: oracleDecimals, maxUserReservesLimit: maxUserReservesLimit, @@ -493,6 +513,7 @@ library AaveV4DeployOrchestration { logger.logDetail('Spoke', report.spokeProxy); logger.logDetail('SpokeImpl', report.spokeImplementation); logger.logDetail('AaveOracle', report.aaveOracle); + logger.logDetail('Gate', report.gate); } /// @dev Derives the root salt with deployer address in the first 160 bits diff --git a/src/deployments/procedures/deploy/spoke/AaveV4SpokeDeployProcedure.sol b/src/deployments/procedures/deploy/spoke/AaveV4SpokeDeployProcedure.sol index 22221ce67..f18d1b4b3 100644 --- a/src/deployments/procedures/deploy/spoke/AaveV4SpokeDeployProcedure.sol +++ b/src/deployments/procedures/deploy/spoke/AaveV4SpokeDeployProcedure.sol @@ -13,6 +13,7 @@ contract AaveV4SpokeDeployProcedure is AaveV4DeployProcedureBase { /// @param proxyAdminOwner The owner of the proxy admin contract. /// @param authority The access control authority address used to initialize the Spoke. /// @param oracle The oracle address used by the Spoke instance. + /// @param gate The immutable gate used by the Spoke instance. /// @param spokeBytecode The creation bytecode of the Spoke implementation. /// @param maxUserReservesLimit The maximum number of reserves a single user can interact with. /// @param salt The CREATE2 salt for deterministic deployment. @@ -22,6 +23,7 @@ contract AaveV4SpokeDeployProcedure is AaveV4DeployProcedureBase { address proxyAdminOwner, address authority, address oracle, + address gate, bytes memory spokeBytecode, uint16 maxUserReservesLimit, bytes32 salt @@ -29,10 +31,11 @@ contract AaveV4SpokeDeployProcedure is AaveV4DeployProcedureBase { require(proxyAdminOwner != address(0), 'invalid proxy admin owner'); require(authority != address(0), 'invalid authority'); require(oracle != address(0), 'invalid oracle'); + require(gate != address(0), 'invalid gate'); require(maxUserReservesLimit > 0, 'invalid max user reserves limit'); spokeImplementation = Create2Utils.create2Deploy({ salt: salt, - bytecode: _getSpokeInstanceInitCode(spokeBytecode, oracle, maxUserReservesLimit) + bytecode: _getSpokeInstanceInitCode(spokeBytecode, oracle, maxUserReservesLimit, gate) }); spokeProxy = Create2Utils.proxify({ salt: salt, @@ -47,12 +50,14 @@ contract AaveV4SpokeDeployProcedure is AaveV4DeployProcedureBase { /// @param spokeBytecode The creation bytecode of the Spoke implementation. /// @param oracle The oracle address to encode as a constructor argument. /// @param maxUserReservesLimit The maximum number of user reserves to encode as a constructor argument. + /// @param gate The gate address to encode as a constructor argument. /// @return The complete init code with encoded constructor arguments. function _getSpokeInstanceInitCode( bytes memory spokeBytecode, address oracle, - uint16 maxUserReservesLimit + uint16 maxUserReservesLimit, + address gate ) internal pure returns (bytes memory) { - return abi.encodePacked(spokeBytecode, abi.encode(oracle, maxUserReservesLimit)); + return abi.encodePacked(spokeBytecode, abi.encode(oracle, maxUserReservesLimit, gate)); } } diff --git a/src/deployments/procedures/roles/AaveV4SpokeRolesProcedure.sol b/src/deployments/procedures/roles/AaveV4SpokeRolesProcedure.sol index 868ab62b8..546d486f0 100644 --- a/src/deployments/procedures/roles/AaveV4SpokeRolesProcedure.sol +++ b/src/deployments/procedures/roles/AaveV4SpokeRolesProcedure.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.0; import {IAccessManager} from 'src/dependencies/openzeppelin/IAccessManager.sol'; import {Roles} from 'src/deployments/utils/libraries/Roles.sol'; +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {IPositionManagerGate} from 'src/spoke/interfaces/IPositionManagerGate.sol'; /// @title AaveV4SpokeRolesProcedure Library /// @author Aave Labs @@ -42,6 +44,15 @@ library AaveV4SpokeRolesProcedure { roleId: Roles.SPOKE_CONFIGURATOR_ROLE, selectors: Roles.getSpokeConfiguratorRoleSelectors() }); + + bytes4[] memory gateSelectors = new bytes4[](1); + gateSelectors[0] = IPositionManagerGate.updatePositionManager.selector; + setupSpokeRole({ + accessManager: accessManager, + spoke: ISpoke(spoke).GATE(), + roleId: Roles.SPOKE_CONFIGURATOR_ROLE, + selectors: gateSelectors + }); } /// @notice Sets up a specific spoke role by assigning function selectors to the target. diff --git a/src/deployments/utils/libraries/InputUtils.sol b/src/deployments/utils/libraries/InputUtils.sol index da21a62e0..88fe5454f 100644 --- a/src/deployments/utils/libraries/InputUtils.sol +++ b/src/deployments/utils/libraries/InputUtils.sol @@ -16,6 +16,7 @@ library InputUtils { /// @dev spokeConfiguratorAdmin The admin granted all spoke configurator roles. Only used when grantRoles is true. /// @dev gatewayOwner The owner of the native token and signature gateways. /// @dev positionManagerOwner The owner of the position manager contracts (giver/taker/config). + /// @dev spokeGate Optional gate shared by deployed Spokes. A zero address deploys PositionManagerGate. /// @dev nativeWrapper The address of the native wrapper (required when deployNativeTokenGateway is true). /// @dev deployNativeTokenGateway Whether to deploy the NativeTokenGateway. /// @dev deploySignatureGateway Whether to deploy the SignatureGateway. @@ -38,6 +39,7 @@ library InputUtils { address spokeConfiguratorAdmin; address gatewayOwner; address positionManagerOwner; + address spokeGate; address nativeWrapper; bool deployNativeTokenGateway; bool deploySignatureGateway; diff --git a/src/deployments/utils/libraries/Roles.sol b/src/deployments/utils/libraries/Roles.sol index 697d9cf74..ad5de7452 100644 --- a/src/deployments/utils/libraries/Roles.sol +++ b/src/deployments/utils/libraries/Roles.sol @@ -129,14 +129,13 @@ library Roles { /// @notice Returns the function selectors associated with the Spoke Configurator role. function getSpokeConfiguratorRoleSelectors() internal pure returns (bytes4[] memory) { - bytes4[] memory selectors = new bytes4[](7); + bytes4[] memory selectors = new bytes4[](6); selectors[0] = ISpoke.updateLiquidationConfig.selector; selectors[1] = ISpoke.addReserve.selector; selectors[2] = ISpoke.updateReserveConfig.selector; selectors[3] = ISpoke.updateDynamicReserveConfig.selector; selectors[4] = ISpoke.addDynamicReserveConfig.selector; - selectors[5] = ISpoke.updatePositionManager.selector; - selectors[6] = ISpoke.updateReservePriceSource.selector; + selectors[5] = ISpoke.updateReservePriceSource.selector; return selectors; } diff --git a/src/position-manager/PositionManagerBase.sol b/src/position-manager/PositionManagerBase.sol index fab80e6dc..6f23ef2ca 100644 --- a/src/position-manager/PositionManagerBase.sol +++ b/src/position-manager/PositionManagerBase.sol @@ -7,6 +7,7 @@ import {Ownable2Step, Ownable} from 'src/dependencies/openzeppelin/Ownable2Step. import {IMulticall, Multicall} from 'src/utils/Multicall.sol'; import {Rescuable} from 'src/utils/Rescuable.sol'; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {IPositionManagerGate} from 'src/spoke/interfaces/IPositionManagerGate.sol'; import {IPositionManagerBase} from 'src/position-manager/interfaces/IPositionManagerBase.sol'; /// @title PositionManagerBase @@ -45,11 +46,17 @@ abstract contract PositionManagerBase is IPositionManagerBase, Ownable2Step, Res 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}); + IPositionManagerGate.PositionManagerUpdate[] + memory updates = new IPositionManagerGate.PositionManagerUpdate[](1); + updates[0] = IPositionManagerGate.PositionManagerUpdate({ + positionManager: address(this), + approve: approve + }); + IPositionManagerGate gate = IPositionManagerGate(ISpoke(spoke).GATE()); try - ISpoke(spoke).setUserPositionManagersWithSig( - ISpoke.SetUserPositionManagers({ + gate.setUserPositionManagersWithSig( + IPositionManagerGate.SetUserPositionManagers({ + spoke: spoke, onBehalfOf: onBehalfOf, updates: updates, nonce: nonce, @@ -90,7 +97,7 @@ abstract contract PositionManagerBase is IPositionManagerBase, Ownable2Step, Res address spoke, address user ) external onlyOwner onlyRegisteredSpoke(spoke) { - ISpoke(spoke).renouncePositionManagerRole(user); + IPositionManagerGate(ISpoke(spoke).GATE()).renouncePositionManagerRole(spoke, user); } /// @inheritdoc IMulticall diff --git a/src/spoke/Spoke.sol b/src/spoke/Spoke.sol index 959206ac5..8840f7b17 100644 --- a/src/spoke/Spoke.sol +++ b/src/spoke/Spoke.sol @@ -12,18 +12,17 @@ import {MathUtils} from 'src/libraries/math/MathUtils.sol'; import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; import {SpokeUtils} from 'src/spoke/libraries/SpokeUtils.sol'; -import {EIP712Hash} from 'src/spoke/libraries/EIP712Hash.sol'; import {KeyValueList} from 'src/spoke/libraries/KeyValueList.sol'; import {LiquidationLogic} from 'src/spoke/libraries/LiquidationLogic.sol'; import {PositionStatusMap} from 'src/spoke/libraries/PositionStatusMap.sol'; import {ReserveFlags, ReserveFlagsMap} from 'src/spoke/libraries/ReserveFlagsMap.sol'; import {UserPositionUtils} from 'src/spoke/libraries/UserPositionUtils.sol'; -import {IntentConsumer} from 'src/utils/IntentConsumer.sol'; import {Multicall} from 'src/utils/Multicall.sol'; import {ExtSload} from 'src/utils/ExtSload.sol'; import {IAaveOracle} from 'src/spoke/interfaces/IAaveOracle.sol'; import {IHubBase} from 'src/hub/interfaces/IHubBase.sol'; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {ISpokeGate} from 'src/spoke/interfaces/ISpokeGate.sol'; import {SpokeStorage} from 'src/spoke/SpokeStorage.sol'; /// @title Spoke @@ -34,7 +33,6 @@ abstract contract Spoke is ISpoke, SpokeStorage, AccessManagedUpgradeable, - IntentConsumer, ExtSload, Multicall, ReentrancyGuardTransient @@ -45,22 +43,20 @@ abstract contract Spoke is using PercentageMath for *; using WadRayMath for *; using SpokeUtils for *; - using EIP712Hash for *; using KeyValueList for KeyValueList.List; using PositionStatusMap for *; using ReserveFlagsMap for ReserveFlags; using UserPositionUtils for ISpoke.UserPosition; - /// @inheritdoc ISpoke - bytes32 public constant SET_USER_POSITION_MANAGERS_TYPEHASH = - EIP712Hash.SET_USER_POSITION_MANAGERS_TYPEHASH; - /// @inheritdoc ISpoke uint16 public immutable MAX_USER_RESERVES_LIMIT; /// @inheritdoc ISpoke address public immutable ORACLE; + /// @inheritdoc ISpoke + address public immutable GATE; + /// @dev The number of decimals used by the oracle. uint8 internal constant ORACLE_DECIMALS = SpokeUtils.ORACLE_DECIMALS; @@ -86,20 +82,23 @@ abstract contract Spoke is uint256 internal constant DUST_LIQUIDATION_THRESHOLD = LiquidationLogic.DUST_LIQUIDATION_THRESHOLD; - /// @notice Modifier that checks if the caller is authorized to act on the position of `onBehalfOf`. - modifier onlyPositionManager(address onBehalfOf) { - require(_isAuthorizedPositionManagerCall(msg.sender, onBehalfOf, msg.data), Unauthorized()); + /// @notice Modifier that asks the gate whether the caller can act on the position of `onBehalfOf`. + modifier onlyPositionActionAllowed(address onBehalfOf) { + require(_isPositionActionAllowed(msg.sender, onBehalfOf, msg.data), Unauthorized()); _; } /// @dev Constructor. /// @param oracle_ The address of the AaveOracle contract. /// @param maxUserReservesLimit_ The maximum number of collateral and borrow reserves a user can have. - constructor(address oracle_, uint16 maxUserReservesLimit_) { + /// @param gate_ The address of the gate authorizing position actions. + constructor(address oracle_, uint16 maxUserReservesLimit_, address gate_) { require(IAaveOracle(oracle_).decimals() == ORACLE_DECIMALS, InvalidOracleDecimals()); require(maxUserReservesLimit_ > 0, InvalidMaxUserReservesLimit()); + require(gate_ != address(0), InvalidAddress()); ORACLE = oracle_; MAX_USER_RESERVES_LIMIT = maxUserReservesLimit_; + GATE = gate_; } /// @dev To be overridden by the inheriting Spoke instance contract. @@ -215,18 +214,12 @@ abstract contract Spoke is emit UpdateDynamicReserveConfig(reserveId, dynamicConfigKey, dynamicConfig); } - /// @inheritdoc ISpoke - function updatePositionManager(address positionManager, bool active) external restricted { - _positionManager[positionManager].active = active; - emit UpdatePositionManager(positionManager, active); - } - /// @inheritdoc ISpoke function supply( uint256 reserveId, uint256 amount, address onBehalfOf - ) external nonReentrant onlyPositionManager(onBehalfOf) returns (uint256, uint256) { + ) external nonReentrant onlyPositionActionAllowed(onBehalfOf) returns (uint256, uint256) { Reserve storage reserve = _reserves.get(reserveId); UserPosition storage userPosition = _userPositions[onBehalfOf][reserveId]; _validateSupply(reserve.flags); @@ -245,7 +238,7 @@ abstract contract Spoke is uint256 reserveId, uint256 amount, address onBehalfOf - ) external nonReentrant onlyPositionManager(onBehalfOf) returns (uint256, uint256) { + ) external nonReentrant onlyPositionActionAllowed(onBehalfOf) returns (uint256, uint256) { Reserve storage reserve = _reserves.get(reserveId); UserPosition storage userPosition = _userPositions[onBehalfOf][reserveId]; _validateWithdraw(reserve.flags); @@ -275,7 +268,7 @@ abstract contract Spoke is uint256 reserveId, uint256 amount, address onBehalfOf - ) external nonReentrant onlyPositionManager(onBehalfOf) returns (uint256, uint256) { + ) external nonReentrant onlyPositionActionAllowed(onBehalfOf) returns (uint256, uint256) { Reserve storage reserve = _reserves.get(reserveId); UserPosition storage userPosition = _userPositions[onBehalfOf][reserveId]; PositionStatus storage positionStatus = _positionStatus[onBehalfOf]; @@ -306,7 +299,7 @@ abstract contract Spoke is uint256 reserveId, uint256 amount, address onBehalfOf - ) external nonReentrant onlyPositionManager(onBehalfOf) returns (uint256, uint256) { + ) external nonReentrant onlyPositionActionAllowed(onBehalfOf) returns (uint256, uint256) { Reserve storage reserve = _reserves.get(reserveId); UserPosition storage userPosition = _userPositions[onBehalfOf][reserveId]; _validateRepay(reserve.flags); @@ -392,7 +385,7 @@ abstract contract Spoke is uint256 reserveId, bool usingAsCollateral, address onBehalfOf - ) external nonReentrant onlyPositionManager(onBehalfOf) { + ) external nonReentrant onlyPositionActionAllowed(onBehalfOf) { Reserve storage reserve = _reserves.get(reserveId); PositionStatus storage positionStatus = _positionStatus[onBehalfOf]; if (positionStatus.isUsingAsCollateral(reserveId) == usingAsCollateral) { @@ -413,7 +406,7 @@ abstract contract Spoke is /// @inheritdoc ISpoke function updateUserRiskPremium(address onBehalfOf) external nonReentrant { - if (!_isPositionManager({user: onBehalfOf, manager: msg.sender})) { + if (!_isPositionActionAllowed(msg.sender, onBehalfOf, msg.data)) { _checkCanCall(msg.sender, msg.data); } uint256 newRiskPremium = _calculateUserAccountData(onBehalfOf).riskPremium; @@ -422,49 +415,13 @@ abstract contract Spoke is /// @inheritdoc ISpoke function updateUserDynamicConfig(address onBehalfOf) external nonReentrant { - if (!_isPositionManager({user: onBehalfOf, manager: msg.sender})) { + if (!_isPositionActionAllowed(msg.sender, onBehalfOf, msg.data)) { _checkCanCall(msg.sender, msg.data); } uint256 newRiskPremium = _refreshAndValidateUserAccountData(onBehalfOf).riskPremium; _notifyRiskPremiumUpdate(onBehalfOf, newRiskPremium); } - /// @inheritdoc ISpoke - function setUserPositionManager(address positionManager, bool approve) external { - _setUserPositionManager({positionManager: positionManager, user: msg.sender, approve: approve}); - } - - /// @inheritdoc ISpoke - function setUserPositionManagersWithSig( - SetUserPositionManagers calldata params, - bytes calldata signature - ) external { - _verifyAndConsumeIntent({ - signer: params.onBehalfOf, - intentHash: params.hash(), - nonce: params.nonce, - deadline: params.deadline, - signature: signature - }); - - for (uint256 i = 0; i < params.updates.length; ++i) { - _setUserPositionManager({ - positionManager: params.updates[i].positionManager, - user: params.onBehalfOf, - approve: params.updates[i].approve - }); - } - } - - /// @inheritdoc ISpoke - function renouncePositionManagerRole(address onBehalfOf) external { - if (!_positionManager[msg.sender].approval[onBehalfOf]) { - return; - } - _positionManager[msg.sender].approval[onBehalfOf] = false; - emit SetUserPositionManager(onBehalfOf, msg.sender, false); - } - /// @inheritdoc ISpoke function permitReserve( uint256 reserveId, @@ -653,16 +610,6 @@ abstract contract Spoke is }); } - /// @inheritdoc ISpoke - function isPositionManagerActive(address positionManager) external view returns (bool) { - return _positionManager[positionManager].active; - } - - /// @inheritdoc ISpoke - function isPositionManager(address user, address positionManager) external view returns (bool) { - return _isPositionManager(user, positionManager); - } - /// @inheritdoc ISpoke function getLiquidationLogic() external pure returns (address) { return address(LiquidationLogic); @@ -674,12 +621,6 @@ abstract contract Spoke is emit UpdateReservePriceSource(reserveId, priceSource); } - function _setUserPositionManager(address positionManager, address user, bool approve) internal { - PositionManagerConfig storage config = _positionManager[positionManager]; - config.approval[user] = approve; - emit SetUserPositionManager(user, positionManager, approve); - } - /// @notice Calculates and validates the user account data. /// @dev It refreshes the dynamic config before calculation. /// @dev It checks that the health factor is above the liquidation threshold. @@ -905,21 +846,13 @@ abstract contract Spoke is return _reserves[reserveId].assetId == assetId && address(_reserves[reserveId].hub) == hub; } - /// @notice Returns whether `manager` is active and approved positionManager for `user`. - function _isPositionManager(address user, address manager) internal view returns (bool) { - if (user == manager) return true; - PositionManagerConfig storage config = _positionManager[manager]; - return config.active && config.approval[user]; - } - - /// @notice Returns whether `caller` is authorized to act on the position of `user` for the given calldata. - /// @dev The default implementation requires the caller to be `user` or an approved position manager for `user`. - function _isAuthorizedPositionManagerCall( + /// @notice Returns whether `caller` is allowed to act on the position of `user` for the given calldata. + function _isPositionActionAllowed( address caller, address user, - bytes calldata - ) internal view virtual returns (bool) { - return _isPositionManager({user: user, manager: caller}); + bytes calldata data + ) internal view returns (bool) { + return ISpokeGate(GATE).isCallAllowed({caller: caller, onBehalfOf: user, data: data}); } function _validateReserveConfig(ReserveConfig calldata config) internal pure { @@ -939,10 +872,6 @@ abstract contract Spoke is require(config.liquidationFee <= PercentageMath.PERCENTAGE_FACTOR, InvalidLiquidationFee()); } - function _domainNameAndVersion() internal pure override returns (string memory, string memory) { - return ('Spoke', '1'); - } - function _castToView( function(address, bool) internal returns (UserAccountData memory) fnIn ) diff --git a/src/spoke/SpokeConfigurator.sol b/src/spoke/SpokeConfigurator.sol index bd82ae6c2..2173a5ef4 100644 --- a/src/spoke/SpokeConfigurator.sol +++ b/src/spoke/SpokeConfigurator.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.28; import {SafeCast} from 'src/dependencies/openzeppelin/SafeCast.sol'; import {AccessManaged} from 'src/dependencies/openzeppelin/AccessManaged.sol'; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {IPositionManagerGate} from 'src/spoke/interfaces/IPositionManagerGate.sol'; import {ISpokeConfigurator} from 'src/spoke/interfaces/ISpokeConfigurator.sol'; /// @title SpokeConfigurator @@ -285,7 +286,11 @@ contract SpokeConfigurator is AccessManaged, ISpokeConfigurator { address positionManager, bool active ) external restricted { - ISpoke(spoke).updatePositionManager(positionManager, active); + IPositionManagerGate(ISpoke(spoke).GATE()).updatePositionManager( + spoke, + positionManager, + active + ); } /// @dev Returns the last dynamic config key of the reserve for the specified Spoke. diff --git a/src/spoke/SpokeStorage.sol b/src/spoke/SpokeStorage.sol index 726398afd..f9b7a578b 100644 --- a/src/spoke/SpokeStorage.sol +++ b/src/spoke/SpokeStorage.sol @@ -32,9 +32,6 @@ abstract contract SpokeStorage { mapping(address user => mapping(uint256 reserveId => ISpoke.UserPosition)) internal _userPositions; - /// @dev Map of position manager addresses to their configuration data. - mapping(address positionManager => ISpoke.PositionManagerConfig) internal _positionManager; - /// @dev Reserved storage space to allow for future layout updates. - uint256[50] private __gap; + uint256[51] private __gap; } diff --git a/src/spoke/gates/PositionManagerGate.sol b/src/spoke/gates/PositionManagerGate.sol new file mode 100644 index 000000000..d9c997554 --- /dev/null +++ b/src/spoke/gates/PositionManagerGate.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: LicenseRef-BUSL +pragma solidity 0.8.28; + +import {AccessManaged} from 'src/dependencies/openzeppelin/AccessManaged.sol'; +import {IntentConsumer} from 'src/utils/IntentConsumer.sol'; +import {IPositionManagerGate} from 'src/spoke/interfaces/IPositionManagerGate.sol'; +import {ISpokeGate} from 'src/spoke/interfaces/ISpokeGate.sol'; +import {PositionManagerGateEIP712Hash} from 'src/spoke/libraries/PositionManagerGateEIP712Hash.sol'; + +/// @title PositionManagerGate +/// @author Aave Labs +/// @notice Gate implementing the canonical user-approved position-manager policy. +/// @dev A single gate can serve multiple Spokes. All state is scoped by the calling Spoke. +contract PositionManagerGate is IPositionManagerGate, AccessManaged, IntentConsumer { + using PositionManagerGateEIP712Hash for *; + + struct PositionManagerConfig { + mapping(address user => bool) approval; + bool active; + } + + bytes32 public constant SET_USER_POSITION_MANAGERS_TYPEHASH = + PositionManagerGateEIP712Hash.SET_USER_POSITION_MANAGERS_TYPEHASH; + + mapping(address spoke => mapping(address positionManager => PositionManagerConfig)) + internal _positionManagers; + + constructor(address authority_) AccessManaged(authority_) { + require(authority_ != address(0), InvalidAddress()); + } + + /// @inheritdoc IPositionManagerGate + function updatePositionManager( + address spoke, + address positionManager, + bool active + ) external restricted { + require(spoke != address(0), InvalidAddress()); + _positionManagers[spoke][positionManager].active = active; + emit UpdatePositionManager(spoke, positionManager, active); + } + + /// @inheritdoc IPositionManagerGate + function setUserPositionManager(address spoke, address positionManager, bool approve) external { + _setUserPositionManager(spoke, positionManager, msg.sender, approve); + } + + /// @inheritdoc IPositionManagerGate + function setUserPositionManagersWithSig( + SetUserPositionManagers calldata params, + bytes calldata signature + ) external { + require(params.spoke != address(0), InvalidAddress()); + _verifyAndConsumeIntent({ + signer: params.onBehalfOf, + intentHash: params.hash(), + nonce: params.nonce, + deadline: params.deadline, + signature: signature + }); + + for (uint256 i = 0; i < params.updates.length; ++i) { + _setUserPositionManager( + params.spoke, + params.updates[i].positionManager, + params.onBehalfOf, + params.updates[i].approve + ); + } + } + + /// @inheritdoc IPositionManagerGate + function renouncePositionManagerRole(address spoke, address user) external { + PositionManagerConfig storage config = _positionManagers[spoke][msg.sender]; + if (!config.approval[user]) return; + config.approval[user] = false; + emit SetUserPositionManager(spoke, user, msg.sender, false); + } + + /// @inheritdoc IPositionManagerGate + function isPositionManagerActive( + address spoke, + address positionManager + ) external view returns (bool) { + return _positionManagers[spoke][positionManager].active; + } + + /// @inheritdoc IPositionManagerGate + function isPositionManager( + address spoke, + address user, + address positionManager + ) public view returns (bool) { + if (user == positionManager) return true; + PositionManagerConfig storage config = _positionManagers[spoke][positionManager]; + return config.active && config.approval[user]; + } + + /// @inheritdoc ISpokeGate + function isCallAllowed( + address caller, + address onBehalfOf, + bytes calldata + ) external view returns (bool) { + return isPositionManager(msg.sender, onBehalfOf, caller); + } + + function _setUserPositionManager( + address spoke, + address positionManager, + address user, + bool approve + ) internal { + require(spoke != address(0), InvalidAddress()); + _positionManagers[spoke][positionManager].approval[user] = approve; + emit SetUserPositionManager(spoke, user, positionManager, approve); + } + + function _domainNameAndVersion() internal pure override returns (string memory, string memory) { + return ('PositionManagerGate', '1'); + } +} diff --git a/src/spoke/instances/PermissionedSpokeInstance.sol b/src/spoke/instances/PermissionedSpokeInstance.sol deleted file mode 100644 index adab2a7f0..000000000 --- a/src/spoke/instances/PermissionedSpokeInstance.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-BUSL -pragma solidity 0.8.28; - -import {SpokeInstanceBase} from 'src/spoke/instances/SpokeInstanceBase.sol'; -import {ISpokeGate} from 'src/spoke/interfaces/ISpokeGate.sol'; - -/// @title PermissionedSpokeInstance -/// @author Aave Labs -/// @notice Spoke implementation where a gate replaces the default position manager authorization -/// on position actions. -contract PermissionedSpokeInstance is SpokeInstanceBase { - /// @notice The gate deciding whether position actions are allowed. - address public immutable GATE; - - /// @dev Constructor. - /// @param oracle_ The address of the oracle. - /// @param maxUserReservesLimit_ The maximum number of collateral and borrow reserves a user can have. - /// @param gate_ The address of the gate. - constructor( - address oracle_, - uint16 maxUserReservesLimit_, - address gate_ - ) SpokeInstanceBase(oracle_, maxUserReservesLimit_) { - require(gate_ != address(0), InvalidAddress()); - GATE = gate_; - } - - /// @dev The gate fully decides whether the call is allowed, based on the caller, the position - /// owner and the calldata. It can preserve the default authorization by calling back - /// `isPositionManager`. - function _isAuthorizedPositionManagerCall( - address caller, - address user, - bytes calldata data - ) internal view override returns (bool) { - return ISpokeGate(GATE).isCallAllowed({caller: caller, onBehalfOf: user, data: data}); - } -} diff --git a/src/spoke/instances/SpokeInstance.sol b/src/spoke/instances/SpokeInstance.sol index 7da310634..cd1ed3fca 100644 --- a/src/spoke/instances/SpokeInstance.sol +++ b/src/spoke/instances/SpokeInstance.sol @@ -10,8 +10,10 @@ contract SpokeInstance is SpokeInstanceBase { /// @dev Constructor. /// @param oracle_ The address of the oracle. /// @param maxUserReservesLimit_ The maximum number of collateral and borrow reserves a user can have. + /// @param gate_ The address of the gate authorizing position actions. constructor( address oracle_, - uint16 maxUserReservesLimit_ - ) SpokeInstanceBase(oracle_, maxUserReservesLimit_) {} + uint16 maxUserReservesLimit_, + address gate_ + ) SpokeInstanceBase(oracle_, maxUserReservesLimit_, gate_) {} } diff --git a/src/spoke/instances/SpokeInstanceBase.sol b/src/spoke/instances/SpokeInstanceBase.sol index 1e213d4e9..ce659e155 100644 --- a/src/spoke/instances/SpokeInstanceBase.sol +++ b/src/spoke/instances/SpokeInstanceBase.sol @@ -13,7 +13,12 @@ abstract contract SpokeInstanceBase is Spoke { /// @dev During upgrade, must ensure that the new oracle is supporting existing assets on the Spoke and the replaced oracle. /// @param oracle_ The address of the oracle. /// @param maxUserReservesLimit_ The maximum number of collateral and borrow reserves a user can have. - constructor(address oracle_, uint16 maxUserReservesLimit_) Spoke(oracle_, maxUserReservesLimit_) { + /// @param gate_ The address of the gate authorizing position actions. + constructor( + address oracle_, + uint16 maxUserReservesLimit_, + address gate_ + ) Spoke(oracle_, maxUserReservesLimit_, gate_) { _disableInitializers(); } @@ -21,7 +26,7 @@ abstract contract SpokeInstanceBase is Spoke { /// @dev The authority contract must implement the `AccessManaged` interface for access control. /// @param authority The address of the authority contract which manages permissions. function initialize(address authority) external virtual override reinitializer(SPOKE_REVISION) { - emit SetSpokeImmutables(ORACLE, MAX_USER_RESERVES_LIMIT); + emit SetSpokeImmutables(ORACLE, MAX_USER_RESERVES_LIMIT, GATE); require(authority != address(0), InvalidAddress()); __AccessManaged_init(authority); diff --git a/src/spoke/interfaces/IPositionManagerGate.sol b/src/spoke/interfaces/IPositionManagerGate.sol new file mode 100644 index 000000000..5080b5f2d --- /dev/null +++ b/src/spoke/interfaces/IPositionManagerGate.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: LicenseRef-BUSL +pragma solidity ^0.8.0; + +import {IAccessManaged} from 'src/dependencies/openzeppelin/IAccessManaged.sol'; +import {IIntentConsumer} from 'src/interfaces/IIntentConsumer.sol'; +import {ISpokeGate} from 'src/spoke/interfaces/ISpokeGate.sol'; + +/// @title IPositionManagerGate +/// @author Aave Labs +/// @notice Gate implementing opt-in position-manager delegation for one or more Spokes. +interface IPositionManagerGate is ISpokeGate, IAccessManaged, IIntentConsumer { + /// @notice Intent data to update a user's position-manager approvals for a Spoke. + struct SetUserPositionManagers { + address spoke; + address onBehalfOf; + PositionManagerUpdate[] updates; + uint256 nonce; + uint256 deadline; + } + + /// @notice A position-manager approval update. + struct PositionManagerUpdate { + address positionManager; + bool approve; + } + + /// @notice Emitted when governance updates a position manager's active status for a Spoke. + event UpdatePositionManager(address indexed spoke, address indexed positionManager, bool active); + + /// @notice Emitted when a user updates a position manager's approval for a Spoke. + event SetUserPositionManager( + address indexed spoke, + address indexed user, + address indexed positionManager, + bool approve + ); + + error InvalidAddress(); + + /// @notice Updates a position manager's active status for a Spoke. + function updatePositionManager(address spoke, address positionManager, bool active) external; + + /// @notice Grants or revokes a position manager's approval for the caller on a Spoke. + function setUserPositionManager(address spoke, address positionManager, bool approve) external; + + /// @notice Applies position-manager approval updates authorized by an EIP-712 signature. + function setUserPositionManagersWithSig( + SetUserPositionManagers calldata params, + bytes calldata signature + ) external; + + /// @notice Lets a position manager renounce a user's approval on a Spoke. + function renouncePositionManagerRole(address spoke, address user) external; + + /// @notice Returns whether a position manager is active for a Spoke. + function isPositionManagerActive( + address spoke, + address positionManager + ) external view returns (bool); + + /// @notice Returns whether a position manager is active and approved by a user for a Spoke. + function isPositionManager( + address spoke, + address user, + address positionManager + ) external view returns (bool); + + /// @notice Returns the type hash for the SetUserPositionManagers intent. + function SET_USER_POSITION_MANAGERS_TYPEHASH() external view returns (bytes32); +} diff --git a/src/spoke/interfaces/ISpoke.sol b/src/spoke/interfaces/ISpoke.sol index e61be5ffb..4489ee393 100644 --- a/src/spoke/interfaces/ISpoke.sol +++ b/src/spoke/interfaces/ISpoke.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.0; import {IAccessManaged} from 'src/dependencies/openzeppelin/IAccessManaged.sol'; -import {IIntentConsumer} from 'src/interfaces/IIntentConsumer.sol'; import {IMulticall} from 'src/interfaces/IMulticall.sol'; import {IHubBase} from 'src/hub/interfaces/IHubBase.sol'; import {IExtSload} from 'src/interfaces/IExtSload.sol'; @@ -12,27 +11,7 @@ type ReserveFlags is uint8; /// @title ISpoke /// @author Aave Labs /// @notice Full interface for Spoke. -interface ISpoke is IAccessManaged, IIntentConsumer, IExtSload, IMulticall { - /// @notice Intent data to set user position managers with EIP712-typed signature. - /// @param onBehalfOf The address of the user on whose behalf position manager can act. - /// @param updates The array of position manager updates. - /// @param nonce The nonce for the signature. - /// @param deadline The deadline for the signature. - struct SetUserPositionManagers { - address onBehalfOf; - PositionManagerUpdate[] updates; - uint256 nonce; - uint256 deadline; - } - - /// @notice Sub-Intent data to apply position manager update for user. - /// @param positionManager The address of the position manager. - /// @param approve True to approve the position manager, false to revoke approval. - struct PositionManagerUpdate { - address positionManager; - bool approve; - } - +interface ISpoke is IAccessManaged, IExtSload, IMulticall { /// @notice Reserve level data. /// @dev underlying The address of the underlying asset. /// @dev hub The address of the associated Hub. @@ -102,14 +81,6 @@ interface ISpoke is IAccessManaged, IIntentConsumer, IExtSload, IMulticall { uint32 dynamicConfigKey; } - /// @notice Position manager configuration data. - /// @dev approval The mapping of position manager user approvals. - /// @dev active True if the position manager is active. - struct PositionManagerConfig { - mapping(address user => bool) approval; - bool active; - } - /// @notice User position status data. /// @dev map The map of bitmap buckets for the position status. /// @dev riskPremium The risk premium of the user position, expressed in BPS. @@ -139,7 +110,12 @@ interface ISpoke is IAccessManaged, IIntentConsumer, IExtSload, IMulticall { /// @notice Emitted when the immutable variables of the Spoke are set. /// @param oracle The address of the oracle. /// @param maxUserReservesLimit The max user reserves limit. - event SetSpokeImmutables(address indexed oracle, uint16 maxUserReservesLimit); + /// @param gate The immutable position-action gate. + event SetSpokeImmutables( + address indexed oracle, + uint16 maxUserReservesLimit, + address indexed gate + ); /// @notice Emitted when a liquidation config is updated. /// @param config The new liquidation config. @@ -183,11 +159,6 @@ interface ISpoke is IAccessManaged, IIntentConsumer, IExtSload, IMulticall { DynamicReserveConfig config ); - /// @notice Emitted on updatePositionManager action. - /// @param positionManager The address of the position manager. - /// @param active True if position manager has become active. - event UpdatePositionManager(address indexed positionManager, bool active); - /// @notice Emitted on the supply action. /// @param reserveId The reserve identifier of the underlying asset. /// @param caller The transaction initiator, and supplier of the underlying asset. @@ -310,12 +281,6 @@ interface ISpoke is IAccessManaged, IIntentConsumer, IExtSload, IMulticall { /// @param reserveId The identifier of the reserve. event RefreshSingleUserDynamicConfig(address indexed user, uint256 reserveId); - /// @notice Emitted on setUserPositionManager or renouncePositionManagerRole action. - /// @param user The address of the user on whose behalf position manager can act. - /// @param positionManager The address of the position manager. - /// @param approve True if position manager approval was granted, false if it was revoked. - event SetUserPositionManager(address indexed user, address indexed positionManager, bool approve); - /// @notice Emitted on refreshPremiumDebt action. /// @param reserveId The identifier of the reserve. /// @param user The address of the user. @@ -471,15 +436,10 @@ interface ISpoke is IAccessManaged, IIntentConsumer, IExtSload, IMulticall { DynamicReserveConfig calldata dynamicConfig ) external; - /// @notice Allows an approved caller (admin) to toggle the active status of position manager. - /// @param positionManager The address of the position manager. - /// @param active True if positionManager is to be set as active. - function updatePositionManager(address positionManager, bool active) external; - /// @notice Supplies an amount of underlying asset of the specified reserve. /// @dev It reverts if the reserve associated with the given reserve identifier is not listed. /// @dev The Spoke pulls the underlying asset from the caller, so prior token approval is required. - /// @dev Caller must be `onBehalfOf` or an authorized position manager for `onBehalfOf`. + /// @dev Caller must be authorized by the Spoke's gate for `onBehalfOf`. /// @param reserveId The reserve identifier. /// @param amount The amount of asset to supply. /// @param onBehalfOf The owner of the position to add supply shares to. @@ -494,7 +454,7 @@ interface ISpoke is IAccessManaged, IIntentConsumer, IExtSload, IMulticall { /// @notice Withdraws a specified amount of underlying asset from the given reserve. /// @dev It reverts if the reserve associated with the given reserve identifier is not listed. /// @dev Providing an amount greater than the maximum withdrawable value signals a full withdrawal. - /// @dev Caller must be `onBehalfOf` or an authorized position manager for `onBehalfOf`. + /// @dev Caller must be authorized by the Spoke's gate for `onBehalfOf`. /// @dev Caller receives the underlying asset withdrawn. /// @param reserveId The identifier of the reserve. /// @param amount The amount of asset to withdraw. @@ -510,7 +470,7 @@ interface ISpoke is IAccessManaged, IIntentConsumer, IExtSload, IMulticall { /// @notice Borrows a specified amount of underlying asset from the given reserve. /// @dev It reverts if the reserve associated with the given reserve identifier is not listed. /// @dev It reverts if the user would borrow more than the maximum allowed number of borrowed reserves. - /// @dev Caller must be `onBehalfOf` or an authorized position manager for `onBehalfOf`. + /// @dev Caller must be authorized by the Spoke's gate for `onBehalfOf`. /// @dev Caller receives the underlying asset borrowed. /// @param reserveId The identifier of the reserve. /// @param amount The amount of asset to borrow. @@ -526,7 +486,7 @@ interface ISpoke is IAccessManaged, IIntentConsumer, IExtSload, IMulticall { /// @notice Repays a specified amount of underlying asset to a given reserve. /// @dev It reverts if the reserve associated with the given reserve identifier is not listed. /// @dev The Spoke pulls the underlying asset from the caller, so prior approval is required. - /// @dev Caller must be `onBehalfOf` or an authorized position manager for `onBehalfOf`. + /// @dev Caller must be authorized by the Spoke's gate for `onBehalfOf`. /// @param reserveId The identifier of the reserve. /// @param amount The amount of asset to repay. /// @param onBehalfOf The owner of the position whose debt is repaid. @@ -558,7 +518,7 @@ interface ISpoke is IAccessManaged, IIntentConsumer, IExtSload, IMulticall { /// @dev It reverts if the reserve associated with the given reserve identifier is not listed. /// @dev It reverts if the user exceeds the maximum allowed collateral reserves when enabling. /// @dev Reserves with zero supplied or zero collateral factor count towards the max allowed collateral reserves. - /// @dev Caller must be `onBehalfOf` or an authorized position manager for `onBehalfOf`. + /// @dev Caller must be authorized by the Spoke's gate for `onBehalfOf`. /// @param reserveId The reserve identifier of the underlying asset. /// @param usingAsCollateral True if the user wants to use the supply as collateral. /// @param onBehalfOf The owner of the position being modified. @@ -569,35 +529,15 @@ interface ISpoke is IAccessManaged, IIntentConsumer, IExtSload, IMulticall { ) external; /// @notice Allows updating the risk premium on onBehalfOf position. - /// @dev Caller must be `onBehalfOf`, an authorized position manager for `onBehalfOf`, or admin. + /// @dev Caller must be authorized by the Spoke's gate for `onBehalfOf`, or be an admin. /// @param onBehalfOf The owner of the position being modified. function updateUserRiskPremium(address onBehalfOf) external; /// @notice Allows updating the dynamic configuration for all collateral reserves on onBehalfOf position. - /// @dev Caller must be `onBehalfOf`, an authorized position manager for `onBehalfOf`, or admin. + /// @dev Caller must be authorized by the Spoke's gate for `onBehalfOf`, or be an admin. /// @param onBehalfOf The owner of the position being modified. function updateUserDynamicConfig(address onBehalfOf) external; - /// @notice Enables a user to grant or revoke approval for a position manager. - /// @dev Allows approving inactive position managers. - /// @param positionManager The address of the position manager. - /// @param approve True to approve the position manager, false to revoke approval. - function setUserPositionManager(address positionManager, bool approve) external; - - /// @notice Enables a user to grant or revoke approval for an array of position managers using an EIP712-typed intent. - /// @dev Uses keyed-nonces where for each key's namespace nonce is consumed sequentially. - /// @dev Allows duplicated updates and the last one is persisted. Allows approving inactive position managers. - /// @param params The structured setUserPositionManagers parameter. - /// @param signature The EIP712-compliant signature bytes. - function setUserPositionManagersWithSig( - SetUserPositionManagers calldata params, - bytes calldata signature - ) external; - - /// @notice Allows position manager (as caller) to renounce their approval given by the user. - /// @param user The address of the user. - function renouncePositionManagerRole(address user) external; - /// @notice Allows consuming a permit signature for the given reserve's underlying asset. /// @dev It reverts if the reserve associated with the given reserve identifier is not listed. /// @dev The Spoke must be configured as the spender. @@ -761,27 +701,15 @@ interface ISpoke is IAccessManaged, IIntentConsumer, IExtSload, IMulticall { uint256 healthFactor ) external view returns (uint256); - /// @notice Returns whether positionManager is currently activated by governance. - /// @param positionManager The address of the position manager. - /// @return True if positionManager is currently active. - function isPositionManagerActive(address positionManager) external view returns (bool); - - /// @notice Returns whether positionManager is active and approved by user. - /// @param user The address of the user. - /// @param positionManager The address of the position manager. - /// @return True if positionManager is active and approved by user. - function isPositionManager(address user, address positionManager) external view returns (bool); - /// @notice Returns the address of the external `LiquidationLogic` library. function getLiquidationLogic() external pure returns (address); - /// @notice Returns the type hash for the SetUserPositionManagers intent. - /// @return The bytes-encoded EIP-712 struct hash representing the intent. - function SET_USER_POSITION_MANAGERS_TYPEHASH() external view returns (bytes32); - /// @notice Returns the address of the AaveOracle contract. function ORACLE() external view returns (address); + /// @notice Returns the immutable gate authorizing position actions. + function GATE() external view returns (address); + /// @notice Returns the maximum allowed number of collateral and borrow reserves per user (each counted separately). function MAX_USER_RESERVES_LIMIT() external view returns (uint16); } diff --git a/src/spoke/interfaces/ISpokeGate.sol b/src/spoke/interfaces/ISpokeGate.sol index 075c68f7a..0d4e3ddf9 100644 --- a/src/spoke/interfaces/ISpokeGate.sol +++ b/src/spoke/interfaces/ISpokeGate.sol @@ -3,12 +3,11 @@ pragma solidity ^0.8.0; /// @title ISpokeGate /// @author Aave Labs -/// @notice Interface for a gate, which replaces the default position manager authorization on -/// position actions of a permissioned Spoke. +/// @notice Interface for the immutable policy contract authorizing position actions on a Spoke. interface ISpokeGate { /// @notice Returns whether a position action on the Spoke is allowed. - /// @dev Called by the Spoke, so it can preserve the default authorization by calling back - /// `ISpoke(msg.sender).isPositionManager`. + /// @dev Called by the Spoke. Implementations can be stateful and shared by multiple Spokes; + /// `msg.sender` identifies the calling Spoke. /// @param caller The transaction initiator on the Spoke. /// @param onBehalfOf The owner of the position being modified. /// @param data The full calldata of the Spoke call, allowing per-action decoding. diff --git a/src/spoke/libraries/EIP712Hash.sol b/src/spoke/libraries/EIP712Hash.sol index ad019f8b2..a73c04a53 100644 --- a/src/spoke/libraries/EIP712Hash.sol +++ b/src/spoke/libraries/EIP712Hash.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.20; import {ITokenizationSpoke} from 'src/spoke/interfaces/ITokenizationSpoke.sol'; -import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; /// @title EIP712Hash library /// @author Aave Labs @@ -10,14 +9,6 @@ import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; library EIP712Hash { using EIP712Hash for *; - bytes32 public constant SET_USER_POSITION_MANAGERS_TYPEHASH = - // keccak256('SetUserPositionManagers(address onBehalfOf,PositionManagerUpdate[] updates,uint256 nonce,uint256 deadline)PositionManagerUpdate(address positionManager,bool approve)') - 0xba01f7bf3d3674c63670ec4a78b0d56aac1ad6e8c84468920b9e61bfe0b9851a; - - bytes32 public constant POSITION_MANAGER_UPDATE = - // keccak256('PositionManagerUpdate(address positionManager,bool approve)') - 0x187dbd227227274b90655fb4011fc21dd749e8966fc040bd91e0b92609202565; - bytes32 public constant TOKENIZED_DEPOSIT_TYPEHASH = // keccak256('TokenizedDeposit(address depositor,uint256 assets,address receiver,uint256 nonce,uint256 deadline)') 0xdecc632fabbd6d9f578203db4396740eb2d81cf0fd7681b726d116e49cbc240c; @@ -38,37 +29,6 @@ library EIP712Hash { // keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)') 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; - function hash(ISpoke.SetUserPositionManagers 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_MANAGERS_TYPEHASH, - params.onBehalfOf, - keccak256(abi.encodePacked(updatesHashes)), - params.nonce, - params.deadline - ) - ); - } - - function hash( - ISpoke.PositionManagerUpdate calldata params - ) internal pure returns (bytes32 digest) { - // equivalent to: keccak256(abi.encode(POSITION_MANAGER_UPDATE, params.positionManager, params.approve)) - assembly { - let fmp := mload(0x40) - mstore(0, POSITION_MANAGER_UPDATE) - mstore(0x20, shr(96, shl(96, calldataload(params)))) // params.positionManager - mstore(0x40, iszero(iszero(calldataload(add(params, 0x20))))) // params.approve - digest := keccak256(0, 0x60) - mstore(0x40, fmp) - } - } - function hash( ITokenizationSpoke.TokenizedDeposit calldata params ) internal pure returns (bytes32) { diff --git a/src/spoke/libraries/PositionManagerGateEIP712Hash.sol b/src/spoke/libraries/PositionManagerGateEIP712Hash.sol new file mode 100644 index 000000000..deb348a67 --- /dev/null +++ b/src/spoke/libraries/PositionManagerGateEIP712Hash.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: LicenseRef-BUSL +pragma solidity ^0.8.20; + +import {IPositionManagerGate} from 'src/spoke/interfaces/IPositionManagerGate.sol'; + +/// @title PositionManagerGateEIP712Hash +/// @author Aave Labs +/// @notice EIP-712 hashing helpers for PositionManagerGate intents. +library PositionManagerGateEIP712Hash { + using PositionManagerGateEIP712Hash for *; + + bytes32 public constant SET_USER_POSITION_MANAGERS_TYPEHASH = + keccak256( + 'SetUserPositionManagers(address spoke,address onBehalfOf,PositionManagerUpdate[] updates,uint256 nonce,uint256 deadline)PositionManagerUpdate(address positionManager,bool approve)' + ); + + bytes32 public constant POSITION_MANAGER_UPDATE_TYPEHASH = + keccak256('PositionManagerUpdate(address positionManager,bool approve)'); + + function hash( + IPositionManagerGate.SetUserPositionManagers 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_MANAGERS_TYPEHASH, + params.spoke, + params.onBehalfOf, + keccak256(abi.encodePacked(updatesHashes)), + params.nonce, + params.deadline + ) + ); + } + + function hash( + IPositionManagerGate.PositionManagerUpdate calldata params + ) internal pure returns (bytes32 digest) { + return + keccak256( + abi.encode(POSITION_MANAGER_UPDATE_TYPEHASH, params.positionManager, params.approve) + ); + } +} diff --git a/tests/config-engine/AaveV4Payload.t.sol b/tests/config-engine/AaveV4Payload.t.sol index fa71d3ac2..ef9bd221c 100644 --- a/tests/config-engine/AaveV4Payload.t.sol +++ b/tests/config-engine/AaveV4Payload.t.sol @@ -68,7 +68,7 @@ contract AaveV4PayloadTest is BaseConfigEngineTest { payload.execute(); - assertTrue(spoke1().isPositionManagerActive(address(payloadPositionManager))); + assertTrue(_isPositionManagerActive(spoke1(), address(payloadPositionManager))); } function test_execute_accessManagerAction_delegatesCorrectly() public { @@ -128,7 +128,7 @@ contract AaveV4PayloadTest is BaseConfigEngineTest { ); assertTrue(spokeConfig.halted); - assertTrue(spoke1().isPositionManagerActive(address(payloadPositionManager))); + assertTrue(_isPositionManagerActive(spoke1(), address(payloadPositionManager))); (bool isMember, ) = accessManager.hasRole(Roles.HUB_CONFIGURATOR_ROLE, ACCOUNT); assertTrue(isMember); @@ -826,7 +826,7 @@ contract AaveV4PayloadTest is BaseConfigEngineTest { payload.execute(); - assertTrue(spoke1().isPositionManagerActive(address(payloadPositionManager))); + assertTrue(_isPositionManagerActive(spoke1(), address(payloadPositionManager))); } function test_execute_accessManagerRoleMemberships_revoke() public { @@ -1010,8 +1010,8 @@ contract AaveV4PayloadTest is BaseConfigEngineTest { payload.execute(); vm.prank(USER); - spoke1().setUserPositionManager(address(freshPm), true); - assertTrue(spoke1().isPositionManager(USER, address(freshPm))); + _setUserPositionManager(spoke1(), address(freshPm), true); + assertTrue(_isPositionManager(spoke1(), USER, address(freshPm))); IAaveV4ConfigEngine.PositionManagerRoleRenouncement[] memory renouncements = new IAaveV4ConfigEngine.PositionManagerRoleRenouncement[](1); @@ -1022,14 +1022,19 @@ contract AaveV4PayloadTest is BaseConfigEngineTest { }); payload.setPositionManagerRoleRenouncements(renouncements); - vm.expectEmit(address(spoke1())); - emit ISpoke.SetUserPositionManager(USER, address(freshPm), false); + vm.expectEmit(spoke1().GATE()); + emit IPositionManagerGate.SetUserPositionManager( + address(spoke1()), + USER, + address(freshPm), + false + ); payload.execute(); // the position manager is still active on the Spoke, so isPositionManager being false // proves the approval itself was cleared - assertTrue(spoke1().isPositionManagerActive(address(freshPm))); - assertFalse(spoke1().isPositionManager(USER, address(freshPm))); + assertTrue(_isPositionManagerActive(spoke1(), address(freshPm))); + assertFalse(_isPositionManager(spoke1(), USER, address(freshPm))); } function test_execute_positionManagerDeregistrationWithRenouncement() public { @@ -1055,8 +1060,8 @@ contract AaveV4PayloadTest is BaseConfigEngineTest { payload.execute(); vm.prank(USER); - spoke1().setUserPositionManager(address(freshPm), true); - assertTrue(spoke1().isPositionManager(USER, address(freshPm))); + _setUserPositionManager(spoke1(), address(freshPm), true); + assertTrue(_isPositionManager(spoke1(), USER, address(freshPm))); // single payload winding down the position manager: renounce USER's role and deregister the Spoke regs[0].registered = false; @@ -1072,14 +1077,19 @@ contract AaveV4PayloadTest is BaseConfigEngineTest { }); payload.setPositionManagerRoleRenouncements(renouncements); - vm.expectEmit(address(spoke1())); - emit ISpoke.SetUserPositionManager(USER, address(freshPm), false); + vm.expectEmit(spoke1().GATE()); + emit IPositionManagerGate.SetUserPositionManager( + address(spoke1()), + USER, + address(freshPm), + false + ); payload.execute(); // the position manager is still active on the Spoke, so isPositionManager being false // proves the approval itself was cleared - assertTrue(spoke1().isPositionManagerActive(address(freshPm))); - assertFalse(spoke1().isPositionManager(USER, address(freshPm))); + assertTrue(_isPositionManagerActive(spoke1(), address(freshPm))); + assertFalse(_isPositionManager(spoke1(), USER, address(freshPm))); assertFalse(freshPm.isSpokeRegistered(address(spoke1()))); } diff --git a/tests/config-engine/BaseConfigEngine.t.sol b/tests/config-engine/BaseConfigEngine.t.sol index 89b22cca1..6af3c93ac 100644 --- a/tests/config-engine/BaseConfigEngine.t.sol +++ b/tests/config-engine/BaseConfigEngine.t.sol @@ -9,6 +9,11 @@ import {IAccessManager} from 'src/dependencies/openzeppelin/IAccessManager.sol'; import {IHub} from 'src/hub/interfaces/IHub.sol'; import {IHubConfigurator} from 'src/hub/interfaces/IHubConfigurator.sol'; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {IPositionManagerGate} from 'src/spoke/interfaces/IPositionManagerGate.sol'; +import { + PositionManagerGateAdapter, + PositionManagerGateTestHelpers +} from 'tests/helpers/spoke/PositionManagerGateAdapter.sol'; import {ISpokeConfigurator} from 'src/spoke/interfaces/ISpokeConfigurator.sol'; import {IAaveOracle} from 'src/spoke/interfaces/IAaveOracle.sol'; import {IAssetInterestRateStrategy} from 'src/hub/interfaces/IAssetInterestRateStrategy.sol'; @@ -42,7 +47,8 @@ import {MockGovernanceExecutor} from 'tests/helpers/mocks/config-engine/MockGove import {MockPriceFeed} from 'tests/helpers/mocks/MockPriceFeed.sol'; import {PositionManagerBaseWrapper} from 'tests/helpers/mocks/PositionManagerBaseWrapper.sol'; -abstract contract BaseConfigEngineTest is Test, Create2TestHelper { +abstract contract BaseConfigEngineTest is Test, Create2TestHelper, PositionManagerGateTestHelpers { + using PositionManagerGateAdapter for ISpoke; uint256 internal constant NUM_HUBS = 2; uint256 internal constant NUM_SPOKES = 3; uint256 internal constant NUM_TOKENS = 4; @@ -154,6 +160,7 @@ abstract contract BaseConfigEngineTest is Test, Create2TestHelper { for (uint256 i; i < NUM_SPOKES; ++i) { spokes[i] = ISpoke(report.spokeReports[i].spoke); oracles[i] = IAaveOracle(report.spokeReports[i].aaveOracle); + _cachePositionManagerGate(spokes[i]); } executor = new MockGovernanceExecutor(PAYLOADS_CONTROLLER); @@ -206,7 +213,9 @@ abstract contract BaseConfigEngineTest is Test, Create2TestHelper { ); AaveV4SpokeRolesProcedure.setupSpokeAllRoles(address(accessManager), report.spoke); vm.stopPrank(); - return (ISpoke(report.spoke), IAaveOracle(report.aaveOracle)); + ISpoke spoke = ISpoke(report.spoke); + _cachePositionManagerGate(spoke); + return (spoke, IAaveOracle(report.aaveOracle)); } function _setupRoles(TestTypes.TestEnvReport memory report) internal { diff --git a/tests/config-engine/PositionManagerEngine.t.sol b/tests/config-engine/PositionManagerEngine.t.sol index a60150039..3d13bca1b 100644 --- a/tests/config-engine/PositionManagerEngine.t.sol +++ b/tests/config-engine/PositionManagerEngine.t.sol @@ -112,15 +112,20 @@ contract PositionManagerEngineTest is BaseConfigEngineTest { ); vm.prank(USER); - spoke1().setUserPositionManager(address(positionManager), true); + _setUserPositionManager(spoke1(), address(positionManager), true); vm.expectCall( address(positionManager), abi.encodeCall(IPositionManagerBase.renouncePositionManagerRole, (address(spoke1()), USER)) ); - vm.expectEmit(address(spoke1())); - emit ISpoke.SetUserPositionManager(USER, address(positionManager), false); + vm.expectEmit(spoke1().GATE()); + emit IPositionManagerGate.SetUserPositionManager( + address(spoke1()), + USER, + address(positionManager), + false + ); engine.executePositionManagerRoleRenouncements( _toPositionManagerRoleRenouncementArray( @@ -132,7 +137,7 @@ contract PositionManagerEngineTest is BaseConfigEngineTest { ) ); - assertFalse(spoke1().isPositionManager(USER, address(positionManager))); + assertFalse(_isPositionManager(spoke1(), USER, address(positionManager))); } function test_executePositionManagerRoleRenouncements_revert() public { @@ -204,7 +209,7 @@ contract PositionManagerEngineTest is BaseConfigEngineTest { ) ); - assertFalse(spoke1().isPositionManager(USER, address(positionManager))); + assertFalse(_isPositionManager(spoke1(), USER, address(positionManager))); } function test_executePositionManagerSpokeRegistrations_batchMultipleSpokes() public { diff --git a/tests/config-engine/SpokeEngine.t.sol b/tests/config-engine/SpokeEngine.t.sol index abfb1f8c7..bce55550e 100644 --- a/tests/config-engine/SpokeEngine.t.sol +++ b/tests/config-engine/SpokeEngine.t.sol @@ -674,19 +674,23 @@ contract SpokeEngineTest is BaseConfigEngineTest { ) ); - vm.expectEmit(address(spoke1())); - emit ISpoke.UpdatePositionManager(address(positionManager), true); + vm.expectEmit(spoke1().GATE()); + emit IPositionManagerGate.UpdatePositionManager( + address(spoke1()), + address(positionManager), + true + ); engine.executeSpokePositionManagerUpdates(_toPositionManagerUpdateArray(update)); - assertTrue(spoke1().isPositionManagerActive(address(positionManager))); + assertTrue(_isPositionManagerActive(spoke1(), address(positionManager))); } function test_executeSpokePositionManagerUpdates_deactivate() public { engine.executeSpokePositionManagerUpdates( _toPositionManagerUpdateArray(_defaultPositionManagerUpdate()) ); - assertTrue(spoke1().isPositionManagerActive(address(positionManager))); + assertTrue(_isPositionManagerActive(spoke1(), address(positionManager))); IAaveV4ConfigEngine.PositionManagerUpdate memory update = IAaveV4ConfigEngine .PositionManagerUpdate({ @@ -698,7 +702,7 @@ contract SpokeEngineTest is BaseConfigEngineTest { engine.executeSpokePositionManagerUpdates(_toPositionManagerUpdateArray(update)); - assertFalse(spoke1().isPositionManagerActive(address(positionManager))); + assertFalse(_isPositionManagerActive(spoke1(), address(positionManager))); } function test_executeSpokeReserveConfigUpdates_multipleSpokes() public { @@ -896,8 +900,8 @@ contract SpokeEngineTest is BaseConfigEngineTest { engine.executeSpokePositionManagerUpdates(updates); - assertTrue(spoke1().isPositionManagerActive(address(positionManager))); - assertTrue(spoke1().isPositionManagerActive(address(pm2))); + assertTrue(_isPositionManagerActive(spoke1(), address(positionManager))); + assertTrue(_isPositionManagerActive(spoke1(), address(pm2))); } function test_executeSpokeReserveListings_multipleReserves() public { diff --git a/tests/contracts/position-manager/ConfigPositionManager/ConfigPositionManager.Base.t.sol b/tests/contracts/position-manager/ConfigPositionManager/ConfigPositionManager.Base.t.sol index 3fd887628..666ff28e2 100644 --- a/tests/contracts/position-manager/ConfigPositionManager/ConfigPositionManager.Base.t.sol +++ b/tests/contracts/position-manager/ConfigPositionManager/ConfigPositionManager.Base.t.sol @@ -18,10 +18,10 @@ contract ConfigPositionManagerBaseTest is Base, ConfigPositionManagerHelpers { emptyPermissions = ConfigPermissions.wrap(0); vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager(address(positionManager), true); + _updatePositionManager(spoke1, address(positionManager), true); vm.prank(alice); - spoke1.setUserPositionManager(address(positionManager), true); + _setUserPositionManager(spoke1, address(positionManager), true); vm.prank(ADMIN); positionManager.registerSpoke(address(spoke1), true); diff --git a/tests/contracts/position-manager/GiverPositionManager.t.sol b/tests/contracts/position-manager/GiverPositionManager.t.sol index 0a6a1da93..e8d6dad83 100644 --- a/tests/contracts/position-manager/GiverPositionManager.t.sol +++ b/tests/contracts/position-manager/GiverPositionManager.t.sol @@ -13,10 +13,10 @@ contract GiverPositionManagerTest is Base { positionManager = new GiverPositionManager(address(ADMIN)); vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager(address(positionManager), true); + _updatePositionManager(spoke1, address(positionManager), true); vm.prank(alice); - spoke1.setUserPositionManager(address(positionManager), true); + _setUserPositionManager(spoke1, address(positionManager), true); vm.prank(ADMIN); positionManager.registerSpoke(address(spoke1), true); @@ -452,7 +452,7 @@ contract GiverPositionManagerTest is Base { uint256 amount = 100e18; vm.prank(carol); - spoke1.setUserPositionManager(address(positionManager), true); + _setUserPositionManager(spoke1, address(positionManager), true); vm.prank(bob); tokenList.dai.approve(address(positionManager), UINT256_MAX); diff --git a/tests/contracts/position-manager/NativeTokenGateway.t.sol b/tests/contracts/position-manager/NativeTokenGateway.t.sol index c787105b6..d233b18d8 100644 --- a/tests/contracts/position-manager/NativeTokenGateway.t.sol +++ b/tests/contracts/position-manager/NativeTokenGateway.t.sol @@ -13,7 +13,7 @@ contract NativeTokenGatewayTest is Base { nativeTokenGateway = new NativeTokenGateway(address(tokenList.weth), address(ADMIN)); vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager(address(nativeTokenGateway), true); + _updatePositionManager(spoke1, address(nativeTokenGateway), true); vm.prank(address(ADMIN)); nativeTokenGateway.registerSpoke(address(spoke1), true); @@ -43,7 +43,7 @@ contract NativeTokenGatewayTest is Base { function test_supplyNative_fuzz(uint256 amount) public { amount = bound(amount, 1, MAX_SUPPLY_AMOUNT_WETH); vm.prank(bob); - spoke1.setUserPositionManager(address(nativeTokenGateway), true); + _setUserPositionManager(spoke1, address(nativeTokenGateway), true); uint256 prevUserBalance = bob.balance; uint256 prevHubBalance = tokenList.weth.balanceOf(address(hub1)); @@ -83,7 +83,7 @@ contract NativeTokenGatewayTest is Base { function test_supplyNative_revertsWith_ReentrancyGuardReentrantCall_spokeSupply() public { vm.prank(bob); - spoke1.setUserPositionManager(address(nativeTokenGateway), true); + _setUserPositionManager(spoke1, address(nativeTokenGateway), true); uint256 amount = 100e18; MockReentrantCaller reentrantCaller = new MockReentrantCaller( @@ -103,7 +103,7 @@ contract NativeTokenGatewayTest is Base { function test_supplyNative_revertsWith_ReentrancyGuardReentrantCall_hubAdd() public { vm.prank(bob); - spoke1.setUserPositionManager(address(nativeTokenGateway), true); + _setUserPositionManager(spoke1, address(nativeTokenGateway), true); uint256 amount = 100e18; MockReentrantCaller reentrantCaller = new MockReentrantCaller( @@ -167,7 +167,7 @@ contract NativeTokenGatewayTest is Base { function test_supplyAndCollateralNative_fuzz(uint256 amount) public { amount = bound(amount, 1, MAX_SUPPLY_AMOUNT_WETH); vm.prank(bob); - spoke1.setUserPositionManager(address(nativeTokenGateway), true); + _setUserPositionManager(spoke1, address(nativeTokenGateway), true); uint256 prevUserBalance = bob.balance; uint256 prevHubBalance = tokenList.weth.balanceOf(address(hub1)); @@ -220,7 +220,7 @@ contract NativeTokenGatewayTest is Base { uint256 expectedSupplyShares = hub1.previewAddByAssets(wethAssetId, MAX_SUPPLY_AMOUNT_WETH); vm.prank(bob); - spoke1.setUserPositionManager(address(nativeTokenGateway), true); + _setUserPositionManager(spoke1, address(nativeTokenGateway), true); uint256 prevUserBalance = bob.balance; uint256 prevHubBalance = tokenList.weth.balanceOf(address(hub1)); @@ -268,7 +268,7 @@ contract NativeTokenGatewayTest is Base { uint256 expectedSupplyShares = hub1.previewAddByAssets(wethAssetId, supplyAmount); vm.prank(bob); - spoke1.setUserPositionManager(address(nativeTokenGateway), true); + _setUserPositionManager(spoke1, address(nativeTokenGateway), true); uint256 prevUserBalance = bob.balance; uint256 prevHubBalance = tokenList.weth.balanceOf(address(hub1)); @@ -307,7 +307,7 @@ contract NativeTokenGatewayTest is Base { borrowAmount = bound(borrowAmount, 1, supplyAmount / 2); vm.prank(bob); - spoke1.setUserPositionManager(address(nativeTokenGateway), true); + _setUserPositionManager(spoke1, address(nativeTokenGateway), true); SpokeActions.supplyCollateral({ spoke: spoke1, @@ -373,7 +373,7 @@ contract NativeTokenGatewayTest is Base { function test_withdrawNative_revertsWith_ReentrancyGuardReentrantCall_spokeWithdraw() public { vm.prank(bob); - spoke1.setUserPositionManager(address(nativeTokenGateway), true); + _setUserPositionManager(spoke1, address(nativeTokenGateway), true); uint256 amount = 100e18; MockReentrantCaller reentrantCaller = new MockReentrantCaller( @@ -393,7 +393,7 @@ contract NativeTokenGatewayTest is Base { function test_withdrawNative_revertsWith_ReentrancyGuardReentrantCall_hubRemove() public { vm.prank(bob); - spoke1.setUserPositionManager(address(nativeTokenGateway), true); + _setUserPositionManager(spoke1, address(nativeTokenGateway), true); uint256 amount = 100e18; MockReentrantCaller reentrantCaller = new MockReentrantCaller( @@ -446,7 +446,7 @@ contract NativeTokenGatewayTest is Base { borrowAmount = bound(borrowAmount, 1, aliceSupplyAmount); vm.prank(bob); - spoke1.setUserPositionManager(address(nativeTokenGateway), true); + _setUserPositionManager(spoke1, address(nativeTokenGateway), true); SpokeActions.supplyCollateral({ spoke: spoke1, @@ -497,7 +497,7 @@ contract NativeTokenGatewayTest is Base { function test_borrowNative_revertsWith_ReentrancyGuardReentrantCall_spokeBorrow() public { vm.prank(bob); - spoke1.setUserPositionManager(address(nativeTokenGateway), true); + _setUserPositionManager(spoke1, address(nativeTokenGateway), true); uint256 amount = 100e18; MockReentrantCaller reentrantCaller = new MockReentrantCaller( @@ -517,7 +517,7 @@ contract NativeTokenGatewayTest is Base { function test_borrowNative_revertsWith_ReentrancyGuardReentrantCall_hubDraw() public { vm.prank(bob); - spoke1.setUserPositionManager(address(nativeTokenGateway), true); + _setUserPositionManager(spoke1, address(nativeTokenGateway), true); uint256 amount = 100e18; MockReentrantCaller reentrantCaller = new MockReentrantCaller( @@ -571,7 +571,7 @@ contract NativeTokenGatewayTest is Base { repayAmount = bound(repayAmount, 1, borrowAmount); vm.prank(bob); - spoke1.setUserPositionManager(address(nativeTokenGateway), true); + _setUserPositionManager(spoke1, address(nativeTokenGateway), true); SpokeActions.supplyCollateral({ spoke: spoke1, @@ -649,7 +649,7 @@ contract NativeTokenGatewayTest is Base { elapsedTime = bound(elapsedTime, 100 days, 400 days); vm.prank(bob); - spoke1.setUserPositionManager(address(nativeTokenGateway), true); + _setUserPositionManager(spoke1, address(nativeTokenGateway), true); SpokeActions.supplyCollateral({ spoke: spoke1, @@ -742,7 +742,7 @@ contract NativeTokenGatewayTest is Base { uint256 repayAmount = 15e18; vm.prank(bob); - spoke1.setUserPositionManager(address(nativeTokenGateway), true); + _setUserPositionManager(spoke1, address(nativeTokenGateway), true); SpokeActions.supplyCollateral({ spoke: spoke1, @@ -819,7 +819,7 @@ contract NativeTokenGatewayTest is Base { function test_repayNative_revertsWith_ReentrancyGuardReentrantCall_spokeRepay() public { vm.prank(bob); - spoke1.setUserPositionManager(address(nativeTokenGateway), true); + _setUserPositionManager(spoke1, address(nativeTokenGateway), true); uint256 amount = 100e18; MockReentrantCaller reentrantCaller = new MockReentrantCaller( @@ -839,7 +839,7 @@ contract NativeTokenGatewayTest is Base { function test_repayNative_revertsWith_ReentrancyGuardReentrantCall_hubRestore() public { vm.prank(bob); - spoke1.setUserPositionManager(address(nativeTokenGateway), true); + _setUserPositionManager(spoke1, address(nativeTokenGateway), true); uint256 amount = 100e18; MockReentrantCaller reentrantCaller = new MockReentrantCaller( diff --git a/tests/contracts/position-manager/PositionManagerBase.t.sol b/tests/contracts/position-manager/PositionManagerBase.t.sol index b3d56b9c5..66d64e46a 100644 --- a/tests/contracts/position-manager/PositionManagerBase.t.sol +++ b/tests/contracts/position-manager/PositionManagerBase.t.sol @@ -14,8 +14,8 @@ contract PositionManagerBaseTest is Base { positionManager2 = new PositionManagerNoMulticall(address(ADMIN)); vm.startPrank(SPOKE_ADMIN); - spoke1.updatePositionManager(address(positionManager), true); - spoke1.updatePositionManager(address(positionManager2), true); + _updatePositionManager(spoke1, address(positionManager), true); + _updatePositionManager(spoke1, address(positionManager2), true); vm.stopPrank(); } @@ -41,24 +41,32 @@ contract PositionManagerBaseTest is Base { } 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(MAX_SKIP_TIME) - }); + IPositionManagerGate.PositionManagerUpdate[] + memory updates = new IPositionManagerGate.PositionManagerUpdate[](1); + updates[0] = IPositionManagerGate.PositionManagerUpdate(address(positionManager), true); + + IPositionManagerGate.SetUserPositionManagers memory p = IPositionManagerGate + .SetUserPositionManagers({ + spoke: address(spoke1), + onBehalfOf: alice, + updates: updates, + nonce: IPositionManagerGate(spoke1.GATE()).nonces(address(alice), _randomNonceKey()), // note: this typed sig is forwarded to spoke1 + deadline: _warpBeforeRandomDeadline(MAX_SKIP_TIME) + }); bytes memory signature = _sign(alicePk, _getTypedDataHash(spoke1, p)); vm.prank(ADMIN); positionManager.registerSpoke(address(spoke1), true); - assertFalse(spoke1.isPositionManager(alice, address(positionManager))); + assertFalse(_isPositionManager(spoke1, alice, address(positionManager))); - vm.expectEmit(address(spoke1)); - emit ISpoke.SetUserPositionManager(alice, address(positionManager), p.updates[0].approve); + vm.expectEmit(spoke1.GATE()); + emit IPositionManagerGate.SetUserPositionManager( + address(spoke1), + alice, + address(positionManager), + p.updates[0].approve + ); vm.prank(vm.randomAddress()); positionManager.setSelfAsUserPositionManagerWithSig( @@ -70,8 +78,8 @@ contract PositionManagerBaseTest is Base { signature ); - _assertNonceIncrement(ISignatureGateway(address(spoke1)), alice, p.nonce); // note: nonce consumed on spoke1 - assertTrue(spoke1.isPositionManager(alice, address(positionManager))); + _assertNonceIncrement(IPositionManagerGate(spoke1.GATE()), alice, p.nonce); + assertTrue(_isPositionManager(spoke1, alice, address(positionManager))); } function test_permitReserveUnderlying_revertsWith_ReserveNotListed() public { @@ -268,16 +276,16 @@ contract PositionManagerBaseTest is Base { address user = vm.randomAddress(); vm.prank(user); - spoke1.setUserPositionManager(address(positionManager), true); + _setUserPositionManager(spoke1, address(positionManager), true); vm.prank(ADMIN); positionManager.registerSpoke(address(spoke1), true); - assertTrue(spoke1.isPositionManager(user, address(positionManager))); + assertTrue(_isPositionManager(spoke1, user, address(positionManager))); vm.prank(ADMIN); positionManager.renouncePositionManagerRole(address(spoke1), user); - assertFalse(spoke1.isPositionManager(user, address(positionManager))); + assertFalse(_isPositionManager(spoke1, user, address(positionManager))); } function test_renouncePositionManagerRole_revertsWith_OwnableUnauthorizedAccount() public { @@ -285,7 +293,7 @@ contract PositionManagerBaseTest is Base { while (caller == ADMIN) caller = vm.randomAddress(); vm.prank(caller); - spoke1.setUserPositionManager(address(positionManager), true); + _setUserPositionManager(spoke1, address(positionManager), true); vm.prank(ADMIN); positionManager.registerSpoke(address(spoke1), true); diff --git a/tests/contracts/position-manager/SignatureGateway/SignatureGateway.Reverts.InsufficientAllowance.t.sol b/tests/contracts/position-manager/SignatureGateway/SignatureGateway.Reverts.InsufficientAllowance.t.sol index 051173f0d..3b4992bc7 100644 --- a/tests/contracts/position-manager/SignatureGateway/SignatureGateway.Reverts.InsufficientAllowance.t.sol +++ b/tests/contracts/position-manager/SignatureGateway/SignatureGateway.Reverts.InsufficientAllowance.t.sol @@ -8,12 +8,12 @@ contract SignatureGateway_InsufficientAllowance_Test is SignatureGatewayBaseTest super.setUp(); vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager(address(gateway), true); + _updatePositionManager(spoke1, address(gateway), true); vm.prank(alice); - spoke1.setUserPositionManager(address(gateway), true); + _setUserPositionManager(spoke1, address(gateway), true); - assertTrue(spoke1.isPositionManagerActive(address(gateway))); - assertTrue(spoke1.isPositionManager(alice, address(gateway))); + assertTrue(_isPositionManagerActive(spoke1, address(gateway))); + assertTrue(_isPositionManager(spoke1, alice, address(gateway))); } function test_supplyWithSig_revertsWith_ERC20InsufficientAllowance() public { diff --git a/tests/contracts/position-manager/SignatureGateway/SignatureGateway.Reverts.SpokeNotRegistered.t.sol b/tests/contracts/position-manager/SignatureGateway/SignatureGateway.Reverts.SpokeNotRegistered.t.sol index e396dc9ee..bb87cf6ce 100644 --- a/tests/contracts/position-manager/SignatureGateway/SignatureGateway.Reverts.SpokeNotRegistered.t.sol +++ b/tests/contracts/position-manager/SignatureGateway/SignatureGateway.Reverts.SpokeNotRegistered.t.sol @@ -8,14 +8,14 @@ contract SignatureGateway_SpokeNotRegistered_Test is SignatureGatewayBaseTest { super.setUp(); vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager(address(gateway), true); + _updatePositionManager(spoke1, address(gateway), true); vm.prank(alice); - spoke1.setUserPositionManager(address(gateway), true); + _setUserPositionManager(spoke1, address(gateway), true); vm.prank(address(ADMIN)); gateway.registerSpoke(address(spoke1), false); - assertTrue(spoke1.isPositionManagerActive(address(gateway))); - assertTrue(spoke1.isPositionManager(alice, address(gateway))); + assertTrue(_isPositionManagerActive(spoke1, address(gateway))); + assertTrue(_isPositionManager(spoke1, alice, address(gateway))); assertFalse(gateway.isSpokeRegistered(address(spoke1))); } diff --git a/tests/contracts/position-manager/SignatureGateway/SignatureGateway.Reverts.Unauthorized.t.sol b/tests/contracts/position-manager/SignatureGateway/SignatureGateway.Reverts.Unauthorized.t.sol index d263b04a2..2083adc20 100644 --- a/tests/contracts/position-manager/SignatureGateway/SignatureGateway.Reverts.Unauthorized.t.sol +++ b/tests/contracts/position-manager/SignatureGateway/SignatureGateway.Reverts.Unauthorized.t.sol @@ -8,8 +8,8 @@ contract SignatureGateway_Unauthorized_PositionManagerNotActive_Test is Signatur super.setUp(); _approveAllUnderlying(spoke1, alice, address(gateway)); - assertFalse(spoke1.isPositionManagerActive(address(gateway))); - assertFalse(spoke1.isPositionManager(alice, address(gateway))); + assertFalse(_isPositionManagerActive(spoke1, address(gateway))); + assertFalse(_isPositionManager(spoke1, alice, address(gateway))); } function test_supplyWithSig_revertsWith_Unauthorized() public { @@ -111,10 +111,10 @@ contract SignatureGateway_Unauthorized_PositionManagerActive_Test is function setUp() public override { super.setUp(); vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager(address(gateway), true); + _updatePositionManager(spoke1, address(gateway), true); vm.prank(address(ADMIN)); gateway.registerSpoke(address(spoke1), true); - assertTrue(spoke1.isPositionManagerActive(address(gateway))); - assertFalse(spoke1.isPositionManager(alice, address(gateway))); + assertTrue(_isPositionManagerActive(spoke1, address(gateway))); + assertFalse(_isPositionManager(spoke1, alice, address(gateway))); } } diff --git a/tests/contracts/position-manager/SignatureGateway/SignatureGateway.SetSelfAsUserPositionManagerWithSig.t.sol b/tests/contracts/position-manager/SignatureGateway/SignatureGateway.SetSelfAsUserPositionManagerWithSig.t.sol index 33a824c54..2bcaea857 100644 --- a/tests/contracts/position-manager/SignatureGateway/SignatureGateway.SetSelfAsUserPositionManagerWithSig.t.sol +++ b/tests/contracts/position-manager/SignatureGateway/SignatureGateway.SetSelfAsUserPositionManagerWithSig.t.sol @@ -18,19 +18,22 @@ contract SignatureGatewaySetSelfAsUserPositionManagerTest is SignatureGatewayBas } function test_setSelfAsUserPositionManagerWithSig_forwards_correct_call() public { - ISpoke.PositionManagerUpdate[] memory updates = new ISpoke.PositionManagerUpdate[](1); - updates[0] = ISpoke.PositionManagerUpdate(address(gateway), vm.randomBool()); - ISpoke.SetUserPositionManagers memory p = ISpoke.SetUserPositionManagers({ - onBehalfOf: vm.randomAddress(), - updates: updates, - nonce: vm.randomUint(), - deadline: vm.randomUint() - }); + IPositionManagerGate.PositionManagerUpdate[] + memory updates = new IPositionManagerGate.PositionManagerUpdate[](1); + updates[0] = IPositionManagerGate.PositionManagerUpdate(address(gateway), vm.randomBool()); + IPositionManagerGate.SetUserPositionManagers memory p = IPositionManagerGate + .SetUserPositionManagers({ + spoke: address(spoke1), + onBehalfOf: vm.randomAddress(), + updates: updates, + nonce: vm.randomUint(), + deadline: vm.randomUint() + }); bytes memory signature = vm.randomBytes(72); vm.expectCall( - address(spoke1), - abi.encodeCall(ISpoke.setUserPositionManagersWithSig, (p, signature)), + spoke1.GATE(), + abi.encodeCall(IPositionManagerGate.setUserPositionManagersWithSig, (p, signature)), 1 ); vm.prank(vm.randomAddress()); @@ -47,7 +50,7 @@ contract SignatureGatewaySetSelfAsUserPositionManagerTest is SignatureGatewayBas function test_setSelfAsUserPositionManagerWithSig_ignores_underlying_spoke_reverts() public { vm.mockCallRevert( address(spoke1), - ISpoke.setUserPositionManagersWithSig.selector, + IPositionManagerGate.setUserPositionManagersWithSig.selector, vm.randomBytes(64) ); @@ -61,27 +64,30 @@ contract SignatureGatewaySetSelfAsUserPositionManagerTest is SignatureGatewayBas signature: vm.randomBytes(72) }); - assertFalse(spoke1.isPositionManager(alice, address(gateway))); + assertFalse(_isPositionManager(spoke1, alice, address(gateway))); } function test_setSelfAsUserPositionManagerWithSig() public { uint192 nonceKey = _randomNonceKey(); vm.prank(alice); - spoke1.useNonce(nonceKey); - ISpoke.PositionManagerUpdate[] memory updates = new ISpoke.PositionManagerUpdate[](1); - updates[0] = ISpoke.PositionManagerUpdate(address(gateway), true); - ISpoke.SetUserPositionManagers memory p = ISpoke.SetUserPositionManagers({ - onBehalfOf: alice, - updates: updates, - nonce: spoke1.nonces(alice, nonceKey), // note: this typed sig is forwarded to spoke - deadline: _warpBeforeRandomDeadline(MAX_SKIP_TIME) - }); + IPositionManagerGate(spoke1.GATE()).useNonce(nonceKey); + IPositionManagerGate.PositionManagerUpdate[] + memory updates = new IPositionManagerGate.PositionManagerUpdate[](1); + updates[0] = IPositionManagerGate.PositionManagerUpdate(address(gateway), true); + IPositionManagerGate.SetUserPositionManagers memory p = IPositionManagerGate + .SetUserPositionManagers({ + spoke: address(spoke1), + onBehalfOf: alice, + updates: updates, + nonce: IPositionManagerGate(spoke1.GATE()).nonces(alice, nonceKey), // note: this typed sig is forwarded to spoke + deadline: _warpBeforeRandomDeadline(MAX_SKIP_TIME) + }); bytes memory signature = _sign(alicePk, _getTypedDataHash(spoke1, p)); vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager(address(gateway), true); + _updatePositionManager(spoke1, address(gateway), true); vm.prank(alice); - spoke1.setUserPositionManager(address(gateway), false); + _setUserPositionManager(spoke1, address(gateway), false); gateway.setSelfAsUserPositionManagerWithSig({ spoke: address(spoke1), @@ -92,6 +98,6 @@ contract SignatureGatewaySetSelfAsUserPositionManagerTest is SignatureGatewayBas signature: signature }); - assertTrue(spoke1.isPositionManager(alice, address(gateway))); + assertTrue(_isPositionManager(spoke1, alice, address(gateway))); } } diff --git a/tests/contracts/position-manager/SignatureGateway/SignatureGateway.t.sol b/tests/contracts/position-manager/SignatureGateway/SignatureGateway.t.sol index f29b89cc2..2440c9b5e 100644 --- a/tests/contracts/position-manager/SignatureGateway/SignatureGateway.t.sol +++ b/tests/contracts/position-manager/SignatureGateway/SignatureGateway.t.sol @@ -9,12 +9,12 @@ contract SignatureGatewayTest is SignatureGatewayBaseTest { function setUp() public virtual override { super.setUp(); vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager(address(gateway), true); + _updatePositionManager(spoke1, address(gateway), true); vm.prank(alice); - spoke1.setUserPositionManager(address(gateway), true); + _setUserPositionManager(spoke1, address(gateway), true); - assertTrue(spoke1.isPositionManagerActive(address(gateway))); - assertTrue(spoke1.isPositionManager(alice, address(gateway))); + assertTrue(_isPositionManagerActive(spoke1, address(gateway))); + assertTrue(_isPositionManager(spoke1, alice, address(gateway))); } function test_useNonce_monotonic(bytes32) public { @@ -43,7 +43,10 @@ contract SignatureGatewayTest is SignatureGatewayBaseTest { function test_renouncePositionManagerRole() public { address user = vm.randomAddress(); - vm.expectCall(address(spoke1), abi.encodeCall(ISpoke.renouncePositionManagerRole, (user))); + vm.expectCall( + spoke1.GATE(), + abi.encodeCall(IPositionManagerGate.renouncePositionManagerRole, (address(spoke1), user)) + ); vm.prank(ADMIN); gateway.renouncePositionManagerRole(address(spoke1), user); } @@ -305,19 +308,27 @@ contract SignatureGatewayTest is SignatureGatewayBaseTest { } function test_setSelfAsUserPositionManagerWithSig() public { - ISpoke.PositionManagerUpdate[] memory updates = new ISpoke.PositionManagerUpdate[](1); - updates[0] = ISpoke.PositionManagerUpdate(address(gateway), true); - - ISpoke.SetUserPositionManagers memory p = ISpoke.SetUserPositionManagers({ - updates: updates, - onBehalfOf: alice, - nonce: spoke1.nonces(address(alice), _randomNonceKey()), // note: this typed sig is forwarded to spoke - deadline: _warpBeforeRandomDeadline(MAX_SKIP_TIME) - }); + IPositionManagerGate.PositionManagerUpdate[] + memory updates = new IPositionManagerGate.PositionManagerUpdate[](1); + updates[0] = IPositionManagerGate.PositionManagerUpdate(address(gateway), true); + + IPositionManagerGate.SetUserPositionManagers memory p = IPositionManagerGate + .SetUserPositionManagers({ + spoke: address(spoke1), + updates: updates, + onBehalfOf: alice, + nonce: IPositionManagerGate(spoke1.GATE()).nonces(address(alice), _randomNonceKey()), // note: this typed sig is forwarded to spoke + deadline: _warpBeforeRandomDeadline(MAX_SKIP_TIME) + }); bytes memory signature = _sign(alicePk, _getTypedDataHash(spoke1, p)); - vm.expectEmit(address(spoke1)); - emit ISpoke.SetUserPositionManager(alice, address(gateway), p.updates[0].approve); + vm.expectEmit(spoke1.GATE()); + emit IPositionManagerGate.SetUserPositionManager( + address(spoke1), + alice, + address(gateway), + p.updates[0].approve + ); vm.prank(vm.randomAddress()); gateway.setSelfAsUserPositionManagerWithSig({ @@ -329,7 +340,7 @@ contract SignatureGatewayTest is SignatureGatewayBaseTest { signature: signature }); - _assertNonceIncrement(ISignatureGateway(address(spoke1)), alice, p.nonce); // note: nonce consumed on spoke + _assertNonceIncrement(IPositionManagerGate(spoke1.GATE()), alice, p.nonce); _assertGatewayHasNoBalanceOrAllowance(spoke1, gateway, alice); _assertGatewayHasNoActivePosition(spoke1, gateway); } diff --git a/tests/contracts/position-manager/TakerPositionManager/TakerPositionManager.Base.t.sol b/tests/contracts/position-manager/TakerPositionManager/TakerPositionManager.Base.t.sol index b70fdccf4..b2aba7d3d 100644 --- a/tests/contracts/position-manager/TakerPositionManager/TakerPositionManager.Base.t.sol +++ b/tests/contracts/position-manager/TakerPositionManager/TakerPositionManager.Base.t.sol @@ -14,10 +14,10 @@ contract TakerPositionManagerBaseTest is Base, TakerPositionManagerHelpers { positionManager = new TakerPositionManager(address(ADMIN)); vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager(address(positionManager), true); + _updatePositionManager(spoke1, address(positionManager), true); vm.prank(alice); - spoke1.setUserPositionManager(address(positionManager), true); + _setUserPositionManager(spoke1, address(positionManager), true); vm.prank(ADMIN); positionManager.registerSpoke(address(spoke1), true); diff --git a/tests/contracts/spoke/configuration/Spoke.DynamicConfig.Triggers.t.sol b/tests/contracts/spoke/configuration/Spoke.DynamicConfig.Triggers.t.sol index 1cf3825c3..70d4b8c3c 100644 --- a/tests/contracts/spoke/configuration/Spoke.DynamicConfig.Triggers.t.sol +++ b/tests/contracts/spoke/configuration/Spoke.DynamicConfig.Triggers.t.sol @@ -402,7 +402,7 @@ contract SpokeDynamicConfigTriggersTest is Base { vm.prank(caller); spoke1.updateUserDynamicConfig(alice); - assertFalse(spoke1.isPositionManager(alice, POSITION_MANAGER)); + assertFalse(_isPositionManager(spoke1, alice, POSITION_MANAGER)); vm.expectRevert( abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, POSITION_MANAGER) ); @@ -410,10 +410,10 @@ contract SpokeDynamicConfigTriggersTest is Base { spoke1.updateUserDynamicConfig(alice); vm.prank(ADMIN); - spoke1.updatePositionManager({positionManager: POSITION_MANAGER, active: true}); + _updatePositionManager(spoke1, POSITION_MANAGER, true); vm.prank(alice); - spoke1.setUserPositionManager(POSITION_MANAGER, true); + _setUserPositionManager(spoke1, POSITION_MANAGER, true); _updateUserDynamicConfig({caller: alice, existingConfigs: configs}); _updateUserDynamicConfig({caller: POSITION_MANAGER, existingConfigs: configs}); diff --git a/tests/contracts/spoke/configuration/Spoke.DynamicConfig.t.sol b/tests/contracts/spoke/configuration/Spoke.DynamicConfig.t.sol index 15bcc2e22..0fae41f44 100644 --- a/tests/contracts/spoke/configuration/Spoke.DynamicConfig.t.sol +++ b/tests/contracts/spoke/configuration/Spoke.DynamicConfig.t.sol @@ -14,7 +14,7 @@ contract SpokeDynamicConfigTest is Base { super.setUp(); spoke = MockSpoke(address(spoke1)); address mockSpokeImpl = address( - new MockSpoke(address(spoke.ORACLE()), MAX_ALLOWED_USER_RESERVES_LIMIT) + new MockSpoke(address(spoke.ORACLE()), MAX_ALLOWED_USER_RESERVES_LIMIT, spoke.GATE()) ); vm.etch(address(spoke1), mockSpokeImpl.code); } diff --git a/tests/contracts/spoke/configurator/SpokeConfigurator.GranularAccessControl.t.sol b/tests/contracts/spoke/configurator/SpokeConfigurator.GranularAccessControl.t.sol index 82487d7ca..5d6741948 100644 --- a/tests/contracts/spoke/configurator/SpokeConfigurator.GranularAccessControl.t.sol +++ b/tests/contracts/spoke/configurator/SpokeConfigurator.GranularAccessControl.t.sol @@ -413,6 +413,6 @@ contract SpokeConfiguratorGranularAccessControlTest is Base { active: true }); - assertTrue(spoke.isPositionManagerActive(newPM)); + assertTrue(_isPositionManagerActive(spoke, newPM)); } } diff --git a/tests/contracts/spoke/configurator/SpokeConfigurator.t.sol b/tests/contracts/spoke/configurator/SpokeConfigurator.t.sol index d6f3e61df..b5a327189 100644 --- a/tests/contracts/spoke/configurator/SpokeConfigurator.t.sol +++ b/tests/contracts/spoke/configurator/SpokeConfigurator.t.sol @@ -793,14 +793,17 @@ contract SpokeConfiguratorTest is Base { for (uint256 i = 0; i < 2; i += 1) { bool active = (i == 0) ? true : false; vm.expectCall( - spokeAddr, - abi.encodeCall(ISpoke.updatePositionManager, (newPositionManager, active)) + spoke.GATE(), + abi.encodeCall( + IPositionManagerGate.updatePositionManager, + (spokeAddr, newPositionManager, active) + ) ); - vm.expectEmit(address(spoke)); - emit ISpoke.UpdatePositionManager(newPositionManager, active); + vm.expectEmit(spoke.GATE()); + emit IPositionManagerGate.UpdatePositionManager(spokeAddr, newPositionManager, active); vm.prank(SPOKE_CONFIGURATOR_ADMIN); spokeConfigurator.updatePositionManager(spokeAddr, newPositionManager, active); - assertEq(spoke.isPositionManagerActive(newPositionManager), active); + assertEq(_isPositionManagerActive(spoke, newPositionManager), active); } } } diff --git a/tests/contracts/spoke/libraries/EIP712Hash.t.sol b/tests/contracts/spoke/libraries/EIP712Hash.t.sol index ff8717904..eb628ef89 100644 --- a/tests/contracts/spoke/libraries/EIP712Hash.t.sol +++ b/tests/contracts/spoke/libraries/EIP712Hash.t.sol @@ -4,28 +4,34 @@ pragma solidity ^0.8.0; import {ITokenizationSpoke} from 'src/spoke/interfaces/ITokenizationSpoke.sol'; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; import {EIP712Hash} from 'src/spoke/libraries/EIP712Hash.sol'; +import {IPositionManagerGate} from 'src/spoke/interfaces/IPositionManagerGate.sol'; +import {PositionManagerGateEIP712Hash} from 'src/spoke/libraries/PositionManagerGateEIP712Hash.sol'; import {Test} from 'forge-std/Test.sol'; contract EIP712HashTest is Test { using EIP712Hash for *; + using PositionManagerGateEIP712Hash for *; function test_constants() public pure { assertEq( - EIP712Hash.SET_USER_POSITION_MANAGERS_TYPEHASH, + PositionManagerGateEIP712Hash.SET_USER_POSITION_MANAGERS_TYPEHASH, keccak256( - 'SetUserPositionManagers(address onBehalfOf,PositionManagerUpdate[] updates,uint256 nonce,uint256 deadline)PositionManagerUpdate(address positionManager,bool approve)' + 'SetUserPositionManagers(address spoke,address onBehalfOf,PositionManagerUpdate[] updates,uint256 nonce,uint256 deadline)PositionManagerUpdate(address positionManager,bool approve)' ) ); assertEq( - EIP712Hash.SET_USER_POSITION_MANAGERS_TYPEHASH, + PositionManagerGateEIP712Hash.SET_USER_POSITION_MANAGERS_TYPEHASH, vm.eip712HashType('SetUserPositionManagers') ); assertEq( - EIP712Hash.POSITION_MANAGER_UPDATE, + PositionManagerGateEIP712Hash.POSITION_MANAGER_UPDATE_TYPEHASH, keccak256('PositionManagerUpdate(address positionManager,bool approve)') ); - assertEq(EIP712Hash.POSITION_MANAGER_UPDATE, vm.eip712HashType('PositionManagerUpdate')); + assertEq( + PositionManagerGateEIP712Hash.POSITION_MANAGER_UPDATE_TYPEHASH, + vm.eip712HashType('PositionManagerUpdate') + ); assertEq( EIP712Hash.TOKENIZED_DEPOSIT_TYPEHASH, @@ -61,7 +67,7 @@ contract EIP712HashTest is Test { } function test_hash_setUserPositionManagers_fuzz( - ISpoke.SetUserPositionManagers calldata params + IPositionManagerGate.SetUserPositionManagers calldata params ) public pure { bytes32[] memory updatesHashes = new bytes32[](params.updates.length); for (uint256 i = 0; i < updatesHashes.length; ++i) { @@ -70,7 +76,8 @@ contract EIP712HashTest is Test { bytes32 expectedHash = keccak256( abi.encode( - EIP712Hash.SET_USER_POSITION_MANAGERS_TYPEHASH, + PositionManagerGateEIP712Hash.SET_USER_POSITION_MANAGERS_TYPEHASH, + params.spoke, params.onBehalfOf, keccak256(abi.encodePacked(updatesHashes)), params.nonce, @@ -79,14 +86,21 @@ contract EIP712HashTest is Test { ); assertEq(params.hash(), expectedHash); - assertEq(params.hash(), vm.eip712HashStruct('SetUserPositionManagers', abi.encode(params))); + assertEq( + PositionManagerGateEIP712Hash.hash(params), + vm.eip712HashStruct('SetUserPositionManagers', abi.encode(params)) + ); } function test_hash_positionManagerUpdate_fuzz( - ISpoke.PositionManagerUpdate calldata params + IPositionManagerGate.PositionManagerUpdate calldata params ) public pure { bytes32 expectedHash = keccak256( - abi.encode(EIP712Hash.POSITION_MANAGER_UPDATE, params.positionManager, params.approve) + abi.encode( + PositionManagerGateEIP712Hash.POSITION_MANAGER_UPDATE_TYPEHASH, + params.positionManager, + params.approve + ) ); assertEq(params.hash(), expectedHash); diff --git a/tests/contracts/spoke/misc/PermissionedSpoke.t.sol b/tests/contracts/spoke/misc/Spoke.Gate.t.sol similarity index 86% rename from tests/contracts/spoke/misc/PermissionedSpoke.t.sol rename to tests/contracts/spoke/misc/Spoke.Gate.t.sol index 8d4bdc919..b5e1eb49a 100644 --- a/tests/contracts/spoke/misc/PermissionedSpoke.t.sol +++ b/tests/contracts/spoke/misc/Spoke.Gate.t.sol @@ -1,21 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import 'tests/setup/PermissionedSpokeBase.sol'; +import 'tests/setup/GateSpokeBase.sol'; -contract PermissionedSpokeTest is PermissionedSpokeBase { +contract SpokeGateTest is GateSpokeBase { function test_constructor() public { - assertEq(PermissionedSpokeInstance(address(spoke)).GATE(), address(gate)); + assertEq(spoke.GATE(), address(gate)); vm.expectRevert(ISpoke.InvalidAddress.selector); - new PermissionedSpokeInstance({ + new SpokeInstance({ oracle_: address(oracle1), maxUserReservesLimit_: DeployConstants.MAX_ALLOWED_USER_RESERVES_LIMIT, gate_: address(0) }); } - function test_defaultBehaviorViaCallback() public { + function test_gateControlsDefaultBehavior() public { _supplyCollateralAndBorrow(alice, 100e6); // an unapproved caller still cannot act on behalf of alice @@ -62,9 +62,12 @@ contract PermissionedSpokeTest is PermissionedSpokeBase { assertEq(spoke.getUserTotalDebt(usdxReserveId, alice), 100e6); } - function test_approvedPositionManagersPreservedViaCallback() public { + function test_positionManagerPolicyLivesInGate() public { + PositionManagerGate positionManagerGate = new PositionManagerGate(address(accessManager)); + ISpoke target = _deploySpokeWithGate(address(positionManagerGate)); + SpokeActions.supply({ - spoke: spoke, + spoke: target, reserveId: usdxReserveId, caller: alice, amount: 100e6, @@ -74,28 +77,28 @@ contract PermissionedSpokeTest is PermissionedSpokeBase { // bob is not an approved position manager for alice vm.expectRevert(ISpoke.Unauthorized.selector); SpokeActions.withdraw({ - spoke: spoke, + spoke: target, reserveId: usdxReserveId, caller: bob, amount: 50e6, onBehalfOf: alice }); - // approving bob as position manager makes the call pass through the callback - vm.prank(SPOKE_ADMIN); - spoke.updatePositionManager(bob, true); + // position-manager state and user approvals are owned by the gate + vm.prank(ADMIN); + positionManagerGate.updatePositionManager(address(target), bob, true); vm.prank(alice); - spoke.setUserPositionManager(bob, true); + positionManagerGate.setUserPositionManager(address(target), bob, true); SpokeActions.withdraw({ - spoke: spoke, + spoke: target, reserveId: usdxReserveId, caller: bob, amount: 50e6, onBehalfOf: alice }); - assertEq(spoke.getUserSuppliedAssets(usdxReserveId, alice), 50e6); + assertEq(target.getUserSuppliedAssets(usdxReserveId, alice), 50e6); } /// @dev Horizon-style forced transfer: the RWA manager moves alice's position to bob by diff --git a/tests/contracts/spoke/misc/Spoke.Upgradeable.t.sol b/tests/contracts/spoke/misc/Spoke.Upgradeable.t.sol index a81ced420..be7ee6a56 100644 --- a/tests/contracts/spoke/misc/Spoke.Upgradeable.t.sol +++ b/tests/contracts/spoke/misc/Spoke.Upgradeable.t.sol @@ -6,9 +6,11 @@ import 'tests/setup/Base.t.sol'; contract SpokeUpgradeableTest is Base { address public proxyAdminOwner = makeAddr('proxyAdminOwner'); address public oracle = makeAddr('AaveOracle'); + address public gate; function setUp() public override { super.setUp(); + gate = address(new PositionManagerGate(address(accessManager))); vm.mockCall(oracle, abi.encodeCall(IPriceOracle.decimals, ()), abi.encode(8)); } @@ -43,7 +45,7 @@ contract SpokeUpgradeableTest is Base { vm.expectEmit(spokeProxyAddress); emit IERC1967.Upgraded(address(spokeImpl)); vm.expectEmit(spokeProxyAddress); - emit ISpoke.SetSpokeImmutables(oracle, MAX_ALLOWED_USER_RESERVES_LIMIT); + emit ISpoke.SetSpokeImmutables(oracle, MAX_ALLOWED_USER_RESERVES_LIMIT, gate); vm.expectEmit(spokeProxyAddress); emit IAccessManaged.AuthorityUpdated(address(accessManager)); vm.expectEmit(spokeProxyAddress); @@ -228,7 +230,7 @@ contract SpokeUpgradeableTest is Base { function _deployMockSpokeInstance(uint64 revision) internal returns (ISpokeInstance) { return ISpokeInstance( - address(new MockSpokeInstance(revision, oracle, MAX_ALLOWED_USER_RESERVES_LIMIT)) + address(new MockSpokeInstance(revision, oracle, MAX_ALLOWED_USER_RESERVES_LIMIT, gate)) ); } } diff --git a/tests/contracts/spoke/misc/Spoke.UserAccountData.t.sol b/tests/contracts/spoke/misc/Spoke.UserAccountData.t.sol index b6e7b3f30..c9963beeb 100644 --- a/tests/contracts/spoke/misc/Spoke.UserAccountData.t.sol +++ b/tests/contracts/spoke/misc/Spoke.UserAccountData.t.sol @@ -13,7 +13,7 @@ contract SpokeUserAccountDataTest is Base { super.setUp(); spoke = MockSpoke(address(spoke1)); address mockSpokeImpl = address( - new MockSpoke(address(spoke.ORACLE()), MAX_ALLOWED_USER_RESERVES_LIMIT) + new MockSpoke(address(spoke.ORACLE()), MAX_ALLOWED_USER_RESERVES_LIMIT, spoke.GATE()) ); vm.etch(address(spoke1), mockSpokeImpl.code); diff --git a/tests/contracts/spoke/position-manager/Spoke.PositionManager.t.sol b/tests/contracts/spoke/position-manager/Spoke.PositionManager.t.sol index b018aa217..69782722a 100644 --- a/tests/contracts/spoke/position-manager/Spoke.PositionManager.t.sol +++ b/tests/contracts/spoke/position-manager/Spoke.PositionManager.t.sol @@ -5,48 +5,52 @@ import 'tests/setup/Base.t.sol'; contract SpokePositionManagerTest is Base { function test_setApprovalForPositionManager(bytes32) public { - vm.setArbitraryStorage(address(spoke1)); + vm.setArbitraryStorage(spoke1.GATE()); address user = vm.randomAddress(); address positionManager = vm.randomAddress(); bool approve = vm.randomBool(); - vm.expectEmit(address(spoke1)); - emit ISpoke.SetUserPositionManager(user, positionManager, approve); + vm.expectEmit(spoke1.GATE()); + emit IPositionManagerGate.SetUserPositionManager( + address(spoke1), + user, + positionManager, + approve + ); vm.prank(user); - spoke1.setUserPositionManager(positionManager, approve); + _setUserPositionManager(spoke1, positionManager, approve); } function test_renouncePositionManagerRole() public { - vm.setArbitraryStorage(address(spoke1)); - address user = vm.randomAddress(); address positionManager = vm.randomAddress(); - if (!spoke1.isPositionManager(user, positionManager)) { - vm.expectEmit(address(spoke1)); - emit ISpoke.SetUserPositionManager(user, positionManager, false); - } + vm.prank(user); + _setUserPositionManager(spoke1, positionManager, true); + + vm.expectEmit(spoke1.GATE()); + emit IPositionManagerGate.SetUserPositionManager(address(spoke1), user, positionManager, false); vm.prank(positionManager); - spoke1.renouncePositionManagerRole(user); + _renouncePositionManagerRole(spoke1, user); - assertFalse(spoke1.isPositionManager(user, positionManager)); + assertFalse(_isPositionManager(spoke1, user, positionManager)); } function test_renouncePositionManagerRole_noop_from_disabled() public { - vm.setArbitraryStorage(address(spoke1)); + vm.setArbitraryStorage(spoke1.GATE()); address user = vm.randomAddress(); vm.prank(user); - spoke1.setUserPositionManager(POSITION_MANAGER, false); + _setUserPositionManager(spoke1, POSITION_MANAGER, false); vm.recordLogs(); vm.prank(POSITION_MANAGER); - spoke1.renouncePositionManagerRole(user); + _renouncePositionManagerRole(spoke1, user); assertEq(vm.getRecordedLogs().length, 0); - assertFalse(spoke1.isPositionManager(user, POSITION_MANAGER)); + assertFalse(_isPositionManager(spoke1, user, POSITION_MANAGER)); } function test_onlyPositionManager_on_supply() public { @@ -454,30 +458,30 @@ contract SpokePositionManagerTest is Base { } function _approvePositionManager(address who) internal { - assertFalse(spoke1.isPositionManager(who, POSITION_MANAGER)); - assertFalse(spoke1.isPositionManagerActive(POSITION_MANAGER)); + assertFalse(_isPositionManager(spoke1, who, POSITION_MANAGER)); + assertFalse(_isPositionManagerActive(spoke1, POSITION_MANAGER)); - vm.expectEmit(address(spoke1)); - emit ISpoke.UpdatePositionManager(POSITION_MANAGER, true); + vm.expectEmit(spoke1.GATE()); + emit IPositionManagerGate.UpdatePositionManager(address(spoke1), POSITION_MANAGER, true); vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager({positionManager: POSITION_MANAGER, active: true}); + _updatePositionManager(spoke1, POSITION_MANAGER, true); - vm.expectEmit(address(spoke1)); - emit ISpoke.SetUserPositionManager(who, POSITION_MANAGER, true); + vm.expectEmit(spoke1.GATE()); + emit IPositionManagerGate.SetUserPositionManager(address(spoke1), who, POSITION_MANAGER, true); vm.prank(who); - spoke1.setUserPositionManager(POSITION_MANAGER, true); + _setUserPositionManager(spoke1, POSITION_MANAGER, true); - assertTrue(spoke1.isPositionManager(who, POSITION_MANAGER)); - assertTrue(spoke1.isPositionManagerActive(POSITION_MANAGER)); + assertTrue(_isPositionManager(spoke1, who, POSITION_MANAGER)); + assertTrue(_isPositionManagerActive(spoke1, POSITION_MANAGER)); } function _disablePositionManager() internal { - vm.expectEmit(address(spoke1)); - emit ISpoke.UpdatePositionManager(POSITION_MANAGER, false); + vm.expectEmit(spoke1.GATE()); + emit IPositionManagerGate.UpdatePositionManager(address(spoke1), POSITION_MANAGER, false); vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager({positionManager: POSITION_MANAGER, active: false}); + _updatePositionManager(spoke1, POSITION_MANAGER, false); - assertFalse(spoke1.isPositionManagerActive(POSITION_MANAGER)); + assertFalse(_isPositionManagerActive(spoke1, POSITION_MANAGER)); } function _resetTokenAllowance(address who) internal { diff --git a/tests/contracts/spoke/position-manager/Spoke.SetUserPositionManagerWithSig.t.sol b/tests/contracts/spoke/position-manager/Spoke.SetUserPositionManagerWithSig.t.sol index 1264db4fa..d2b841e63 100644 --- a/tests/contracts/spoke/position-manager/Spoke.SetUserPositionManagerWithSig.t.sol +++ b/tests/contracts/spoke/position-manager/Spoke.SetUserPositionManagerWithSig.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; import 'tests/setup/Base.t.sol'; -import {EIP712Hash} from 'src/spoke/libraries/EIP712Hash.sol'; +import {PositionManagerGateEIP712Hash} from 'src/spoke/libraries/PositionManagerGateEIP712Hash.sol'; contract SpokeSetUserPositionManagersWithSigTest is Base { using SafeCast for *; @@ -12,23 +12,24 @@ contract SpokeSetUserPositionManagersWithSigTest is Base { function setUp() public override { super.setUp(); vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager({positionManager: POSITION_MANAGER, active: true}); + _updatePositionManager(spoke1, POSITION_MANAGER, true); } function test_useNonce_monotonic(bytes32) public { - vm.setArbitraryStorage(address(spoke1)); + IPositionManagerGate gate = _positionManagerGate(spoke1); + vm.setArbitraryStorage(address(gate)); address user = vm.randomAddress(); uint192 nonceKey = vm.randomUint(0, type(uint192).max).toUint192(); - (, uint64 nonce) = _unpackNonce(spoke1.nonces(user, nonceKey)); + (, uint64 nonce) = _unpackNonce(gate.nonces(user, nonceKey)); vm.prank(user); - spoke1.useNonce(nonceKey); + gate.useNonce(nonceKey); // prettier-ignore unchecked { ++nonce; } - assertEq(spoke1.nonces(user, nonceKey), _packNonce(nonceKey, nonce)); + assertEq(gate.nonces(user, nonceKey), _packNonce(nonceKey, nonce)); } function test_eip712Domain() public { @@ -41,13 +42,13 @@ contract SpokeSetUserPositionManagersWithSigTest is Base { address verifyingContract, bytes32 salt, uint256[] memory extensions - ) = IERC5267(address(spoke)).eip712Domain(); + ) = IERC5267(spoke.GATE()).eip712Domain(); assertEq(fields, bytes1(0x0f)); - assertEq(name, 'Spoke'); + assertEq(name, 'PositionManagerGate'); assertEq(version, '1'); assertEq(chainId, block.chainid); - assertEq(verifyingContract, address(spoke)); + assertEq(verifyingContract, spoke.GATE()); assertEq(salt, bytes32(0)); assertEq(extensions.length, 0); } @@ -59,38 +60,41 @@ contract SpokeSetUserPositionManagersWithSigTest is Base { keccak256( 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' ), - keccak256('Spoke'), + keccak256('PositionManagerGate'), keccak256('1'), block.chainid, - address(spoke) + spoke.GATE() ) ); - assertEq(spoke.DOMAIN_SEPARATOR(), expectedDomainSeparator); + assertEq(IPositionManagerGate(spoke.GATE()).DOMAIN_SEPARATOR(), expectedDomainSeparator); } function test_setUserPositionManager_typeHash() public view { assertEq( - EIP712Hash.SET_USER_POSITION_MANAGERS_TYPEHASH, + PositionManagerGateEIP712Hash.SET_USER_POSITION_MANAGERS_TYPEHASH, vm.eip712HashType('SetUserPositionManagers') ); assertEq( - EIP712Hash.SET_USER_POSITION_MANAGERS_TYPEHASH, + PositionManagerGateEIP712Hash.SET_USER_POSITION_MANAGERS_TYPEHASH, keccak256( - 'SetUserPositionManagers(address onBehalfOf,PositionManagerUpdate[] updates,uint256 nonce,uint256 deadline)PositionManagerUpdate(address positionManager,bool approve)' + 'SetUserPositionManagers(address spoke,address onBehalfOf,PositionManagerUpdate[] updates,uint256 nonce,uint256 deadline)PositionManagerUpdate(address positionManager,bool approve)' ) ); assertEq( - EIP712Hash.SET_USER_POSITION_MANAGERS_TYPEHASH, - spoke1.SET_USER_POSITION_MANAGERS_TYPEHASH() + PositionManagerGateEIP712Hash.SET_USER_POSITION_MANAGERS_TYPEHASH, + IPositionManagerGate(spoke1.GATE()).SET_USER_POSITION_MANAGERS_TYPEHASH() ); } function test_positionManagerUpdate_typeHash() public pure { assertEq( - EIP712Hash.POSITION_MANAGER_UPDATE, + PositionManagerGateEIP712Hash.POSITION_MANAGER_UPDATE_TYPEHASH, keccak256('PositionManagerUpdate(address positionManager,bool approve)') ); - assertEq(EIP712Hash.POSITION_MANAGER_UPDATE, vm.eip712HashType('PositionManagerUpdate')); + assertEq( + PositionManagerGateEIP712Hash.POSITION_MANAGER_UPDATE_TYPEHASH, + vm.eip712HashType('PositionManagerUpdate') + ); } function test_setUserPositionManagersWithSig_revertsWith_InvalidSignature_dueTo_ExpiredDeadline() @@ -98,7 +102,10 @@ contract SpokeSetUserPositionManagersWithSigTest is Base { { uint256 deadline = _warpAfterRandomDeadline(MAX_SKIP_TIME); - ISpoke.SetUserPositionManagers memory params = _setUserPositionManagerData(alice, deadline); + IPositionManagerGate.SetUserPositionManagers memory params = _setUserPositionManagerData( + alice, + deadline + ); bytes32 digest = _getTypedDataHash(spoke1, params); (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePk, digest); @@ -106,7 +113,7 @@ contract SpokeSetUserPositionManagersWithSigTest is Base { vm.expectRevert(IIntentConsumer.InvalidSignature.selector); vm.prank(vm.randomAddress()); - spoke1.setUserPositionManagersWithSig(params, signature); + _setUserPositionManagersWithSig(spoke1, params, signature); } function test_setUserPositionManagersWithSig_revertsWith_InvalidSignature_dueTo_InvalidSigner() @@ -116,14 +123,17 @@ contract SpokeSetUserPositionManagersWithSigTest is Base { vm.assume(randomUser != alice); uint256 deadline = _warpAfterRandomDeadline(MAX_SKIP_TIME); - ISpoke.SetUserPositionManagers memory params = _setUserPositionManagerData(alice, deadline); + IPositionManagerGate.SetUserPositionManagers memory params = _setUserPositionManagerData( + alice, + deadline + ); bytes32 digest = _getTypedDataHash(spoke1, params); (uint8 v, bytes32 r, bytes32 s) = vm.sign(randomUserPk, digest); bytes memory signature = abi.encodePacked(r, s, v); vm.expectRevert(IIntentConsumer.InvalidSignature.selector); vm.prank(vm.randomAddress()); - spoke1.setUserPositionManagersWithSig(params, signature); + _setUserPositionManagersWithSig(spoke1, params, signature); } function test_setUserPositionManagersWithSig_revertsWith_InvalidAccountNonce(bytes32) public { @@ -131,13 +141,24 @@ contract SpokeSetUserPositionManagersWithSigTest is Base { vm.label(user, 'user'); address positionManager = vm.randomAddress(); vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager({positionManager: positionManager, active: true}); + _updatePositionManager(spoke1, positionManager, true); uint256 deadline = _warpBeforeRandomDeadline(MAX_SKIP_TIME); uint192 nonceKey = _randomNonceKey(); - ISpoke.SetUserPositionManagers memory params = _setUserPositionManagerData(user, deadline); - uint256 currentNonce = _burnRandomNoncesAtKey(spoke1, params.onBehalfOf, nonceKey); - params.nonce = _getRandomInvalidNonceAtKey(spoke1, params.onBehalfOf, nonceKey); + IPositionManagerGate.SetUserPositionManagers memory params = _setUserPositionManagerData( + user, + deadline + ); + uint256 currentNonce = _burnRandomNoncesAtKey( + IPositionManagerGate(spoke1.GATE()), + params.onBehalfOf, + nonceKey + ); + params.nonce = _getRandomInvalidNonceAtKey( + IPositionManagerGate(spoke1.GATE()), + params.onBehalfOf, + nonceKey + ); bytes32 digest = _getTypedDataHash(spoke1, params); (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPk, digest); @@ -151,33 +172,37 @@ contract SpokeSetUserPositionManagersWithSigTest is Base { ) ); vm.prank(vm.randomAddress()); - spoke1.setUserPositionManagersWithSig(params, signature); + _setUserPositionManagersWithSig(spoke1, params, signature); } function test_setUserPositionManagersWithSig() public { (address user, uint256 userPk) = makeAddrAndKey(string(vm.randomBytes(32))); vm.label(user, 'user'); uint256 deadline = _warpBeforeRandomDeadline(MAX_SKIP_TIME); - ISpoke.SetUserPositionManagers memory params = _setUserPositionManagerData(user, deadline); - params.nonce = _burnRandomNoncesAtKey(spoke1, params.onBehalfOf); + IPositionManagerGate.SetUserPositionManagers memory params = _setUserPositionManagerData( + user, + deadline + ); + params.nonce = _burnRandomNoncesAtKey(IPositionManagerGate(spoke1.GATE()), params.onBehalfOf); bytes32 digest = _getTypedDataHash(spoke1, params); (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPk, digest); bytes memory signature = abi.encodePacked(r, s, v); - vm.expectEmit(address(spoke1)); - emit ISpoke.SetUserPositionManager( + vm.expectEmit(spoke1.GATE()); + emit IPositionManagerGate.SetUserPositionManager( + address(spoke1), params.onBehalfOf, params.updates[0].positionManager, params.updates[0].approve ); vm.prank(vm.randomAddress()); - spoke1.setUserPositionManagersWithSig(params, signature); + _setUserPositionManagersWithSig(spoke1, params, signature); - _assertNonceIncrement(spoke1, params.onBehalfOf, params.nonce); + _assertNonceIncrement(IPositionManagerGate(spoke1.GATE()), params.onBehalfOf, params.nonce); assertEq( - spoke1.isPositionManager(params.onBehalfOf, params.updates[0].positionManager), + _isPositionManager(spoke1, params.onBehalfOf, params.updates[0].positionManager), params.updates[0].approve ); } @@ -186,9 +211,12 @@ contract SpokeSetUserPositionManagersWithSigTest is Base { (address user, uint256 userPk) = makeAddrAndKey(string(vm.randomBytes(32))); vm.label(user, 'user'); uint256 deadline = _warpBeforeRandomDeadline(MAX_SKIP_TIME); - ISpoke.SetUserPositionManagers memory params = _setUserPositionManagerData(user, deadline); - params.updates = new ISpoke.PositionManagerUpdate[](0); - params.nonce = _burnRandomNoncesAtKey(spoke1, params.onBehalfOf); + IPositionManagerGate.SetUserPositionManagers memory params = _setUserPositionManagerData( + user, + deadline + ); + params.updates = new IPositionManagerGate.PositionManagerUpdate[](0); + params.nonce = _burnRandomNoncesAtKey(IPositionManagerGate(spoke1.GATE()), params.onBehalfOf); bytes32 digest = _getTypedDataHash(spoke1, params); (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPk, digest); @@ -197,21 +225,24 @@ contract SpokeSetUserPositionManagersWithSigTest is Base { vm.recordLogs(); vm.prank(vm.randomAddress()); - spoke1.setUserPositionManagersWithSig(params, signature); + _setUserPositionManagersWithSig(spoke1, params, signature); assertEq(vm.getRecordedLogs().length, 0); - _assertNonceIncrement(spoke1, params.onBehalfOf, params.nonce); + _assertNonceIncrement(IPositionManagerGate(spoke1.GATE()), params.onBehalfOf, params.nonce); } function test_setUserPositionManagersWithSig_multiple_updates( - ISpoke.PositionManagerUpdate[] memory updates + IPositionManagerGate.PositionManagerUpdate[] memory updates ) public { vm.assume(updates.length < 1024); // for performance - vm.setArbitraryStorage(address(spoke1)); // arbitrary nonce, position manager active state + vm.setArbitraryStorage(spoke1.GATE()); // arbitrary nonce, position manager active state (address user, uint256 userPk) = makeAddrAndKey(string(vm.randomBytes(32))); vm.label(user, 'user'); uint256 deadline = _warpBeforeRandomDeadline(MAX_SKIP_TIME); - ISpoke.SetUserPositionManagers memory params = _setUserPositionManagerData(user, deadline); + IPositionManagerGate.SetUserPositionManagers memory params = _setUserPositionManagerData( + user, + deadline + ); params.updates = updates; bytes32 digest = _getTypedDataHash(spoke1, params); @@ -221,20 +252,25 @@ contract SpokeSetUserPositionManagersWithSigTest is Base { for (uint256 i; i < updates.length; ++i) { address positionManager = params.updates[i].positionManager; bool approve = params.updates[i].approve; - vm.expectEmit(address(spoke1)); - emit ISpoke.SetUserPositionManager(params.onBehalfOf, positionManager, approve); + vm.expectEmit(spoke1.GATE()); + emit IPositionManagerGate.SetUserPositionManager( + address(spoke1), + params.onBehalfOf, + positionManager, + approve + ); // overwrite cached lookup such that latest state is checked for duplicated entries - _lookup[positionManager] = approve && spoke1.isPositionManagerActive(positionManager); + _lookup[positionManager] = approve && _isPositionManagerActive(spoke1, positionManager); } vm.prank(vm.randomAddress()); - spoke1.setUserPositionManagersWithSig(params, signature); + _setUserPositionManagersWithSig(spoke1, params, signature); - _assertNonceIncrement(spoke1, params.onBehalfOf, params.nonce); + _assertNonceIncrement(IPositionManagerGate(spoke1.GATE()), params.onBehalfOf, params.nonce); for (uint256 i; i < updates.length; ++i) { address positionManager = params.updates[i].positionManager; assertEq( - spoke1.isPositionManager(params.onBehalfOf, positionManager), + _isPositionManager(spoke1, params.onBehalfOf, positionManager), (positionManager == user) || _lookup[positionManager] ); } @@ -244,7 +280,7 @@ contract SpokeSetUserPositionManagersWithSigTest is Base { public { MockERC1271Wallet smartWallet = new MockERC1271Wallet(alice); - ISpoke.SetUserPositionManagers memory params = _setUserPositionManagerData( + IPositionManagerGate.SetUserPositionManagers memory params = _setUserPositionManagerData( address(smartWallet), _warpAfterRandomDeadline(MAX_SKIP_TIME) ); @@ -258,7 +294,7 @@ contract SpokeSetUserPositionManagersWithSigTest is Base { vm.expectRevert(IIntentConsumer.InvalidSignature.selector); vm.prank(vm.randomAddress()); - spoke1.setUserPositionManagersWithSig(params, signature); + _setUserPositionManagersWithSig(spoke1, params, signature); } function test_setUserPositionManagersWithSig_ERC1271_revertsWith_InvalidSignature_dueTo_InvalidHash() @@ -267,16 +303,16 @@ contract SpokeSetUserPositionManagersWithSigTest is Base { address maliciousManager = makeAddr('maliciousManager'); MockERC1271Wallet smartWallet = new MockERC1271Wallet(alice); vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager({positionManager: maliciousManager, active: true}); + _updatePositionManager(spoke1, maliciousManager, true); uint256 deadline = _warpAfterRandomDeadline(MAX_SKIP_TIME); - ISpoke.SetUserPositionManagers memory params = _setUserPositionManagerData( + IPositionManagerGate.SetUserPositionManagers memory params = _setUserPositionManagerData( address(smartWallet), deadline ); bytes32 digest = _getTypedDataHash(spoke1, params); - ISpoke.SetUserPositionManagers memory invalidParams = _setUserPositionManagerData( + IPositionManagerGate.SetUserPositionManagers memory invalidParams = _setUserPositionManagerData( address(smartWallet), deadline ); @@ -292,7 +328,7 @@ contract SpokeSetUserPositionManagersWithSigTest is Base { vm.expectRevert(IIntentConsumer.InvalidSignature.selector); vm.prank(vm.randomAddress()); - spoke1.setUserPositionManagersWithSig(invalidParams, signature); + _setUserPositionManagersWithSig(spoke1, invalidParams, signature); } function test_setUserPositionManagersWithSig_ERC1271_revertsWith_InvalidAccountNonce( @@ -302,13 +338,21 @@ contract SpokeSetUserPositionManagersWithSigTest is Base { uint256 deadline = _warpBeforeRandomDeadline(MAX_SKIP_TIME); uint192 nonceKey = _randomNonceKey(); - ISpoke.SetUserPositionManagers memory params = _setUserPositionManagerData( + IPositionManagerGate.SetUserPositionManagers memory params = _setUserPositionManagerData( address(smartWallet), deadline ); - uint256 currentNonce = _burnRandomNoncesAtKey(spoke1, address(smartWallet), nonceKey); - params.nonce = _getRandomInvalidNonceAtKey(spoke1, address(smartWallet), nonceKey); + uint256 currentNonce = _burnRandomNoncesAtKey( + IPositionManagerGate(spoke1.GATE()), + address(smartWallet), + nonceKey + ); + params.nonce = _getRandomInvalidNonceAtKey( + IPositionManagerGate(spoke1.GATE()), + address(smartWallet), + nonceKey + ); bytes32 digest = _getTypedDataHash(spoke1, params); (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePk, digest); @@ -325,7 +369,7 @@ contract SpokeSetUserPositionManagersWithSigTest is Base { ) ); vm.prank(vm.randomAddress()); - spoke1.setUserPositionManagersWithSig(params, signature); + _setUserPositionManagersWithSig(spoke1, params, signature); } function test_setUserPositionManagersWithSig_ERC1271() public { @@ -335,10 +379,10 @@ contract SpokeSetUserPositionManagersWithSigTest is Base { vm.label(address(smartWallet), 'smartWallet'); address positionManager = vm.randomAddress(); vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager({positionManager: positionManager, active: true}); + _updatePositionManager(spoke1, positionManager, true); uint256 deadline = _warpBeforeRandomDeadline(MAX_SKIP_TIME); - ISpoke.SetUserPositionManagers memory params = _setUserPositionManagerData( + IPositionManagerGate.SetUserPositionManagers memory params = _setUserPositionManagerData( address(smartWallet), deadline ); @@ -350,19 +394,20 @@ contract SpokeSetUserPositionManagersWithSigTest is Base { (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPk, digest); bytes memory signature = abi.encodePacked(r, s, v); - vm.expectEmit(address(spoke1)); - emit ISpoke.SetUserPositionManager( + vm.expectEmit(spoke1.GATE()); + emit IPositionManagerGate.SetUserPositionManager( + address(spoke1), params.onBehalfOf, params.updates[0].positionManager, params.updates[0].approve ); vm.prank(vm.randomAddress()); - spoke1.setUserPositionManagersWithSig(params, signature); + _setUserPositionManagersWithSig(spoke1, params, signature); - _assertNonceIncrement(spoke1, params.onBehalfOf, params.nonce); + _assertNonceIncrement(IPositionManagerGate(spoke1.GATE()), params.onBehalfOf, params.nonce); assertEq( - spoke1.isPositionManager(params.onBehalfOf, params.updates[0].positionManager), + _isPositionManager(spoke1, params.onBehalfOf, params.updates[0].positionManager), params.updates[0].approve ); } @@ -370,15 +415,18 @@ contract SpokeSetUserPositionManagersWithSigTest is Base { function _setUserPositionManagerData( address user, uint256 deadline - ) internal returns (ISpoke.SetUserPositionManagers memory) { - ISpoke.PositionManagerUpdate[] memory updates = new ISpoke.PositionManagerUpdate[](1); - updates[0] = ISpoke.PositionManagerUpdate(POSITION_MANAGER, true); - ISpoke.SetUserPositionManagers memory params = ISpoke.SetUserPositionManagers({ - onBehalfOf: user, - updates: updates, - nonce: spoke1.nonces(user, _randomNonceKey()), - deadline: deadline - }); + ) internal returns (IPositionManagerGate.SetUserPositionManagers memory) { + IPositionManagerGate.PositionManagerUpdate[] + memory updates = new IPositionManagerGate.PositionManagerUpdate[](1); + updates[0] = IPositionManagerGate.PositionManagerUpdate(POSITION_MANAGER, true); + IPositionManagerGate.SetUserPositionManagers memory params = IPositionManagerGate + .SetUserPositionManagers({ + spoke: address(spoke1), + onBehalfOf: user, + updates: updates, + nonce: IPositionManagerGate(spoke1.GATE()).nonces(user, _randomNonceKey()), + deadline: deadline + }); return params; } } diff --git a/tests/deployments/AaveV4BatchDeployment.t.sol b/tests/deployments/AaveV4BatchDeployment.t.sol index a1870f64f..6930ff3eb 100644 --- a/tests/deployments/AaveV4BatchDeployment.t.sol +++ b/tests/deployments/AaveV4BatchDeployment.t.sol @@ -17,6 +17,7 @@ contract AaveV4BatchDeploymentTest is BatchTestProcedures { spokeConfiguratorAdmin: makeAddr('spokeConfiguratorAdmin'), gatewayOwner: makeAddr('gatewayOwner'), positionManagerOwner: makeAddr('positionManagerOwner'), + spokeGate: address(0), nativeWrapper: _weth9, deployNativeTokenGateway: true, deploySignatureGateway: true, diff --git a/tests/deployments/batches/AaveV4SpokeInstanceBatch.t.sol b/tests/deployments/batches/AaveV4SpokeInstanceBatch.t.sol index 108c3f2f9..aea59e7a9 100644 --- a/tests/deployments/batches/AaveV4SpokeInstanceBatch.t.sol +++ b/tests/deployments/batches/AaveV4SpokeInstanceBatch.t.sol @@ -12,6 +12,7 @@ contract AaveV4SpokeInstanceBatchTest is BatchBaseTest { spokeBatch = new AaveV4SpokeInstanceBatch({ proxyAdminOwner_: admin, authority_: accessManager, + gate_: gate, spokeBytecode_: spokeBytecode, oracleDecimals_: 8, maxUserReservesLimit_: 128, @@ -48,6 +49,7 @@ contract AaveV4SpokeInstanceBatchTest is BatchBaseTest { new AaveV4SpokeInstanceBatch({ proxyAdminOwner_: admin, authority_: address(0), + gate_: gate, spokeBytecode_: spokeBytecode, oracleDecimals_: 8, maxUserReservesLimit_: 128, @@ -60,6 +62,7 @@ contract AaveV4SpokeInstanceBatchTest is BatchBaseTest { new AaveV4SpokeInstanceBatch({ proxyAdminOwner_: address(0), authority_: accessManager, + gate_: gate, spokeBytecode_: spokeBytecode, oracleDecimals_: 8, maxUserReservesLimit_: 128, @@ -72,6 +75,7 @@ contract AaveV4SpokeInstanceBatchTest is BatchBaseTest { new AaveV4SpokeInstanceBatch({ proxyAdminOwner_: admin, authority_: accessManager, + gate_: gate, spokeBytecode_: spokeBytecode, oracleDecimals_: 0, maxUserReservesLimit_: 128, @@ -84,6 +88,7 @@ contract AaveV4SpokeInstanceBatchTest is BatchBaseTest { new AaveV4SpokeInstanceBatch({ proxyAdminOwner_: admin, authority_: accessManager, + gate_: gate, spokeBytecode_: spokeBytecode, oracleDecimals_: 8, maxUserReservesLimit_: 0, @@ -95,6 +100,7 @@ contract AaveV4SpokeInstanceBatchTest is BatchBaseTest { AaveV4SpokeInstanceBatch newBatch = new AaveV4SpokeInstanceBatch({ proxyAdminOwner_: admin, authority_: accessManager, + gate_: gate, spokeBytecode_: spokeBytecode, oracleDecimals_: 8, maxUserReservesLimit_: 128, diff --git a/tests/deployments/batches/BatchBase.t.sol b/tests/deployments/batches/BatchBase.t.sol index 90eedb465..07b8b07f8 100644 --- a/tests/deployments/batches/BatchBase.t.sol +++ b/tests/deployments/batches/BatchBase.t.sol @@ -31,12 +31,14 @@ import {IAccessManagerEnumerable} from 'src/access/interfaces/IAccessManagerEnum import {TreasurySpoke} from 'src/spoke/TreasurySpoke.sol'; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; import {IPriceOracle} from 'src/spoke/interfaces/IPriceOracle.sol'; +import {PositionManagerGate} from 'src/spoke/gates/PositionManagerGate.sol'; contract BatchBaseTest is Create2TestHelper { address public admin = makeAddr('admin'); address public feeReceiver = makeAddr('feeReceiver'); bytes32 public salt; address public accessManager; + address public gate; address public nativeWrapper; bytes internal hubBytecode; bytes internal spokeBytecode; @@ -51,6 +53,7 @@ contract BatchBaseTest is Create2TestHelper { // used Hub, Spoke, Configurator batches AaveV4AuthorityBatch authorityBatch = new AaveV4AuthorityBatch({admin_: admin, salt_: salt}); accessManager = authorityBatch.getReport().accessManager; + gate = address(new PositionManagerGate(accessManager)); // used by Gateway batch nativeWrapper = address(new WETH9()); diff --git a/tests/deployments/fork/PostDeploymentVerificationTest.t.sol b/tests/deployments/fork/PostDeploymentVerificationTest.t.sol index 69daaa5bf..f0cb74bea 100644 --- a/tests/deployments/fork/PostDeploymentVerificationTest.t.sol +++ b/tests/deployments/fork/PostDeploymentVerificationTest.t.sol @@ -207,6 +207,7 @@ contract PostDeploymentVerificationTest is PostDeploymentVerificationBase, AaveV spokeConfiguratorAdmin: params.spokeConfiguratorAdmin, gatewayOwner: params.gatewayOwner, positionManagerOwner: params.positionManagerOwner, + spokeGate: address(0), nativeWrapper: (params.deployNativeTokenGateway && params.useValidNativeWrapper) ? weth : address(0), @@ -261,6 +262,7 @@ contract PostDeploymentVerificationTest is PostDeploymentVerificationBase, AaveV spokeConfiguratorAdmin: makeAddr('spokeConfiguratorAdmin'), gatewayOwner: makeAddr('gatewayOwner'), positionManagerOwner: makeAddr('positionManagerOwner'), + spokeGate: address(0), nativeWrapper: weth, deployNativeTokenGateway: true, deploySignatureGateway: true, diff --git a/tests/deployments/orchestration/AaveV4TestOrchestration.sol b/tests/deployments/orchestration/AaveV4TestOrchestration.sol index 256fac747..f16a9bb11 100644 --- a/tests/deployments/orchestration/AaveV4TestOrchestration.sol +++ b/tests/deployments/orchestration/AaveV4TestOrchestration.sol @@ -27,6 +27,7 @@ import {IHubConfigurator} from 'src/hub/interfaces/IHubConfigurator.sol'; import {IHubInstance} from 'src/deployments/utils/interfaces/IHubInstance.sol'; import {ISpokeInstance} from 'src/deployments/utils/interfaces/ISpokeInstance.sol'; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {PositionManagerGate} from 'src/spoke/gates/PositionManagerGate.sol'; import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; import {TransparentUpgradeableProxy} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; @@ -72,6 +73,7 @@ library AaveV4TestOrchestration { report.accessManager = AaveV4DeployBase .deployAuthorityBatch({admin: admin, salt: salt}) .accessManager; + address gate = address(new PositionManagerGate(report.accessManager)); // Deploy TreasurySpoke Batch (single instance for all hubs) report.treasurySpoke = AaveV4DeployBase @@ -100,6 +102,7 @@ library AaveV4TestOrchestration { .deploySpokeInstanceBatch({ proxyAdminOwner: admin, authority: report.accessManager, + gate: gate, spokeBytecode: spokeBytecode, oracleDecimals: DeployConstants.ORACLE_DECIMALS, maxUserReservesLimit: DeployConstants.MAX_ALLOWED_USER_RESERVES_LIMIT, @@ -161,10 +164,12 @@ library AaveV4TestOrchestration { bytes32 salt ) external returns (TestTypes.TestSpokeReport memory) { TestTypes.TestSpokeReport memory report; + address gate = address(new PositionManagerGate(accessManager)); BatchReports.SpokeInstanceBatchReport memory spokeReport = AaveV4DeployBase .deploySpokeInstanceBatch({ proxyAdminOwner: proxyAdminOwner, authority: accessManager, + gate: gate, spokeBytecode: spokeBytecode, oracleDecimals: DeployConstants.ORACLE_DECIMALS, maxUserReservesLimit: maxUserReservesLimit, @@ -422,17 +427,27 @@ library AaveV4TestOrchestration { address oracle, uint16 maxUserReservesLimit ) internal returns (ISpokeInstance) { - return deploySpokeImplementation(oracle, maxUserReservesLimit, ''); + address gate = address(new PositionManagerGate(address(1))); + return deploySpokeImplementation(oracle, maxUserReservesLimit, gate, ''); } function deploySpokeImplementation( address oracle, uint16 maxUserReservesLimit, + address gate + ) internal returns (ISpokeInstance) { + return deploySpokeImplementation(oracle, maxUserReservesLimit, gate, ''); + } + + function deploySpokeImplementation( + address oracle, + uint16 maxUserReservesLimit, + address gate, bytes32 salt ) internal returns (ISpokeInstance) { bytes memory initCode = abi.encodePacked( vm.getCode('src/spoke/instances/SpokeInstance.sol:SpokeInstance'), - abi.encode(oracle, maxUserReservesLimit) + abi.encode(oracle, maxUserReservesLimit, gate) ); return ISpokeInstance(_create2Deploy(salt, initCode)); } diff --git a/tests/deployments/procedures/ProceduresBase.t.sol b/tests/deployments/procedures/ProceduresBase.t.sol index 1e21a15e5..5b7d65f03 100644 --- a/tests/deployments/procedures/ProceduresBase.t.sol +++ b/tests/deployments/procedures/ProceduresBase.t.sol @@ -41,12 +41,14 @@ import {ITreasurySpoke} from 'src/spoke/interfaces/ITreasurySpoke.sol'; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; import {IAccessManagerEnumerable} from 'src/access/interfaces/IAccessManagerEnumerable.sol'; import {IAccessManager} from 'src/dependencies/openzeppelin/IAccessManager.sol'; +import {PositionManagerGate} from 'src/spoke/gates/PositionManagerGate.sol'; import {ITokenizationSpoke} from 'src/spoke/interfaces/ITokenizationSpoke.sol'; import {Create2TestHelper} from 'tests/utils/Create2TestHelper.sol'; contract ProceduresBase is Create2TestHelper { address public owner = makeAddr('owner'); address public accessManager; + address public gate; address public hub = makeAddr('hub'); address public nativeWrapper = makeAddr('nativeWrapper'); address public accessManagerAdmin = makeAddr('accessManagerAdmin'); @@ -66,6 +68,7 @@ contract ProceduresBase is Create2TestHelper { hubBytecode = vm.getCode('src/hub/instances/HubInstance.sol:HubInstance'); spokeBytecode = vm.getCode('src/spoke/instances/SpokeInstance.sol:SpokeInstance'); accessManager = address(new AccessManagerEnumerable(accessManagerAdmin)); + gate = address(new PositionManagerGate(accessManager)); aaveOracle = address(new AaveOracle(oracleDecimals)); salt = keccak256('testSalt'); } diff --git a/tests/deployments/procedures/deploy/roles/AaveV4SpokeRolesProcedure.t.sol b/tests/deployments/procedures/deploy/roles/AaveV4SpokeRolesProcedure.t.sol index 50e32dd84..2eb554e2a 100644 --- a/tests/deployments/procedures/deploy/roles/AaveV4SpokeRolesProcedure.t.sol +++ b/tests/deployments/procedures/deploy/roles/AaveV4SpokeRolesProcedure.t.sol @@ -2,12 +2,14 @@ pragma solidity ^0.8.0; import 'tests/deployments/procedures/ProceduresBase.t.sol'; +import {IPositionManagerGate} from 'src/spoke/interfaces/IPositionManagerGate.sol'; contract AaveV4SpokeRolesProcedureTest is ProceduresBase { AaveV4SpokeRolesProcedureWrapper public aaveV4SpokeRolesProcedureWrapper; function setUp() public override { super.setUp(); aaveV4SpokeRolesProcedureWrapper = new AaveV4SpokeRolesProcedureWrapper(); + vm.mockCall(spoke, abi.encodeCall(ISpoke.GATE, ()), abi.encode(gate)); } function test_grantSpokeAllRoles_reverts() public { @@ -111,6 +113,13 @@ contract AaveV4SpokeRolesProcedureTest is ProceduresBase { IAccessManager(accessManager).getTargetFunctionRole(spoke, ISpoke.addReserve.selector), Roles.SPOKE_CONFIGURATOR_ROLE ); + assertEq( + IAccessManager(accessManager).getTargetFunctionRole( + gate, + IPositionManagerGate.updatePositionManager.selector + ), + Roles.SPOKE_CONFIGURATOR_ROLE + ); } function _grantAdminToWrapper(address wrapper) internal { @@ -129,14 +138,13 @@ contract AaveV4SpokeRolesProcedureTest is ProceduresBase { function test_getSpokeConfiguratorRoleSelectors() public view { bytes4[] memory selectors = aaveV4SpokeRolesProcedureWrapper .getSpokeConfiguratorRoleSelectors(); - assertEq(selectors.length, 7); + assertEq(selectors.length, 6); assertEq(selectors[0], ISpoke.updateLiquidationConfig.selector); assertEq(selectors[1], ISpoke.addReserve.selector); assertEq(selectors[2], ISpoke.updateReserveConfig.selector); assertEq(selectors[3], ISpoke.updateDynamicReserveConfig.selector); assertEq(selectors[4], ISpoke.addDynamicReserveConfig.selector); - assertEq(selectors[5], ISpoke.updatePositionManager.selector); - assertEq(selectors[6], ISpoke.updateReservePriceSource.selector); + assertEq(selectors[5], ISpoke.updateReservePriceSource.selector); } function test_canCall_spokePositionUpdaterRole() public { @@ -232,5 +240,13 @@ contract AaveV4SpokeRolesProcedureTest is ProceduresBase { assertTrue(allowed); assertEq(delay, 0); } + + (bool gateAllowed, uint32 gateDelay) = IAccessManager(accessManager).canCall( + admin, + gate, + IPositionManagerGate.updatePositionManager.selector + ); + assertTrue(gateAllowed); + assertEq(gateDelay, 0); } } diff --git a/tests/deployments/procedures/deploy/spoke/AaveV4SpokeDeployProcedure.t.sol b/tests/deployments/procedures/deploy/spoke/AaveV4SpokeDeployProcedure.t.sol index 0507bab17..67754d998 100644 --- a/tests/deployments/procedures/deploy/spoke/AaveV4SpokeDeployProcedure.t.sol +++ b/tests/deployments/procedures/deploy/spoke/AaveV4SpokeDeployProcedure.t.sol @@ -16,6 +16,7 @@ contract AaveV4SpokeDeployProcedureTest is ProceduresBase { owner, accessManager, aaveOracle, + gate, spokeBytecode, maxUserReservesLimit, salt @@ -33,6 +34,7 @@ contract AaveV4SpokeDeployProcedureTest is ProceduresBase { proxyAdminOwner: address(0), authority: accessManager, oracle: aaveOracle, + gate: gate, spokeBytecode: spokeBytecode, maxUserReservesLimit: maxUserReservesLimit, salt: salt @@ -43,6 +45,7 @@ contract AaveV4SpokeDeployProcedureTest is ProceduresBase { proxyAdminOwner: owner, authority: address(0), oracle: aaveOracle, + gate: gate, spokeBytecode: spokeBytecode, maxUserReservesLimit: maxUserReservesLimit, salt: salt @@ -53,6 +56,7 @@ contract AaveV4SpokeDeployProcedureTest is ProceduresBase { proxyAdminOwner: owner, authority: accessManager, oracle: address(0), + gate: gate, spokeBytecode: spokeBytecode, maxUserReservesLimit: maxUserReservesLimit, salt: salt @@ -63,6 +67,7 @@ contract AaveV4SpokeDeployProcedureTest is ProceduresBase { proxyAdminOwner: owner, authority: accessManager, oracle: aaveOracle, + gate: gate, spokeBytecode: spokeBytecode, maxUserReservesLimit: 0, salt: salt diff --git a/tests/gas/Gateways.Operations.gas.t.sol b/tests/gas/Gateways.Operations.gas.t.sol index 90894a2fe..70fd44965 100644 --- a/tests/gas/Gateways.Operations.gas.t.sol +++ b/tests/gas/Gateways.Operations.gas.t.sol @@ -15,11 +15,11 @@ contract NativeTokenGateway_Gas_Tests is Base { nativeTokenGateway = new NativeTokenGateway(address(tokenList.weth), address(ADMIN)); vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager(address(nativeTokenGateway), true); + _updatePositionManager(spoke1, address(nativeTokenGateway), true); vm.prank(address(ADMIN)); nativeTokenGateway.registerSpoke(address(spoke1), true); vm.prank(bob); - spoke1.setUserPositionManager(address(nativeTokenGateway), true); + _setUserPositionManager(spoke1, address(nativeTokenGateway), true); deal(address(tokenList.weth), MAX_SUPPLY_AMOUNT); deal(bob, MAX_SUPPLY_AMOUNT_WETH); @@ -176,9 +176,9 @@ contract SignatureGateway_Gas_Tests is Base, SignatureGatewayHelpers { vm.prank(address(ADMIN)); gateway.registerSpoke(address(spoke1), true); vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager(address(gateway), true); + _updatePositionManager(spoke1, address(gateway), true); vm.prank(alice); - spoke1.setUserPositionManager(address(gateway), true); + _setUserPositionManager(spoke1, address(gateway), true); vm.prank(alice); gateway.useNonce(nonceKey); } @@ -370,19 +370,22 @@ contract SignatureGateway_Gas_Tests is Base, SignatureGatewayHelpers { function test_setSelfAsUserPositionManagerWithSig() public { vm.prank(alice); - spoke1.useNonce(nonceKey); - ISpoke.PositionManagerUpdate[] memory updates = new ISpoke.PositionManagerUpdate[](1); - updates[0] = ISpoke.PositionManagerUpdate(address(gateway), true); - ISpoke.SetUserPositionManagers memory p = ISpoke.SetUserPositionManagers({ - onBehalfOf: alice, - updates: updates, - nonce: spoke1.nonces(alice, nonceKey), // note: this typed sig is forwarded to spoke - deadline: vm.getBlockTimestamp() - }); + IPositionManagerGate(spoke1.GATE()).useNonce(nonceKey); + IPositionManagerGate.PositionManagerUpdate[] + memory updates = new IPositionManagerGate.PositionManagerUpdate[](1); + updates[0] = IPositionManagerGate.PositionManagerUpdate(address(gateway), true); + IPositionManagerGate.SetUserPositionManagers memory p = IPositionManagerGate + .SetUserPositionManagers({ + spoke: address(spoke1), + onBehalfOf: alice, + updates: updates, + nonce: IPositionManagerGate(spoke1.GATE()).nonces(alice, nonceKey), // note: this typed sig is forwarded to spoke + deadline: vm.getBlockTimestamp() + }); bytes memory signature = _sign(alicePk, _getTypedDataHash(spoke1, p)); vm.prank(alice); - spoke1.setUserPositionManager(address(gateway), false); + _setUserPositionManager(spoke1, address(gateway), false); gateway.setSelfAsUserPositionManagerWithSig({ spoke: address(spoke1), diff --git a/tests/gas/PositionManagers.Operations.gas.t.sol b/tests/gas/PositionManagers.Operations.gas.t.sol index 1499a6ac4..0ef342f8b 100644 --- a/tests/gas/PositionManagers.Operations.gas.t.sol +++ b/tests/gas/PositionManagers.Operations.gas.t.sol @@ -16,7 +16,7 @@ contract PositionManager_Gas_Tests is Base { positionManager = new PositionManagerBaseWrapper(address(ADMIN)); vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager(address(positionManager), true); + _updatePositionManager(spoke1, address(positionManager), true); vm.prank(ADMIN); positionManager.registerSpoke(address(spoke1), true); @@ -24,21 +24,24 @@ contract PositionManager_Gas_Tests is Base { 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() - }); + IPositionManagerGate(spoke1.GATE()).useNonce(nonceKey); + + IPositionManagerGate.PositionManagerUpdate[] + memory updates = new IPositionManagerGate.PositionManagerUpdate[](1); + updates[0] = IPositionManagerGate.PositionManagerUpdate(address(positionManager), true); + + IPositionManagerGate.SetUserPositionManagers memory p = IPositionManagerGate + .SetUserPositionManagers({ + spoke: address(spoke1), + onBehalfOf: alice, + updates: updates, + nonce: IPositionManagerGate(spoke1.GATE()).nonces(alice, nonceKey), + deadline: vm.getBlockTimestamp() + }); bytes memory signature = _sign(alicePk, _getTypedDataHash(spoke1, p)); vm.prank(alice); - spoke1.setUserPositionManager(address(positionManager), false); + _setUserPositionManager(spoke1, address(positionManager), false); positionManager.setSelfAsUserPositionManagerWithSig({ spoke: address(spoke1), @@ -63,11 +66,11 @@ contract GiverPositionManager_Gas_Tests is Base { positionManager = new GiverPositionManager(address(ADMIN)); vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager(address(positionManager), true); + _updatePositionManager(spoke1, address(positionManager), true); vm.prank(ADMIN); positionManager.registerSpoke(address(spoke1), true); vm.prank(alice); - spoke1.setUserPositionManager(address(positionManager), true); + _setUserPositionManager(spoke1, address(positionManager), true); vm.prank(bob); tokenList.dai.approve(address(positionManager), UINT256_MAX); } @@ -141,11 +144,11 @@ contract TakerPositionManager_Gas_Tests is Base { positionManager = new TakerPositionManager(address(ADMIN)); vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager(address(positionManager), true); + _updatePositionManager(spoke1, address(positionManager), true); vm.prank(ADMIN); positionManager.registerSpoke(address(spoke1), true); vm.prank(alice); - spoke1.setUserPositionManager(address(positionManager), true); + _setUserPositionManager(spoke1, address(positionManager), true); } function test_withdrawOnBehalfOf() public { @@ -318,11 +321,11 @@ contract ConfigPositionManager_Gas_Tests is Base { positionManager = new ConfigPositionManager(address(ADMIN)); vm.prank(SPOKE_ADMIN); - spoke1.updatePositionManager(address(positionManager), true); + _updatePositionManager(spoke1, address(positionManager), true); vm.prank(ADMIN); positionManager.registerSpoke(address(spoke1), true); vm.prank(alice); - spoke1.setUserPositionManager(address(positionManager), true); + _setUserPositionManager(spoke1, address(positionManager), true); } function test_setGlobalPermission() public { diff --git a/tests/gas/PermissionedSpoke.Operations.gas.t.sol b/tests/gas/Spoke.Gate.Operations.gas.t.sol similarity index 79% rename from tests/gas/PermissionedSpoke.Operations.gas.t.sol rename to tests/gas/Spoke.Gate.Operations.gas.t.sol index 77c0b296b..f936f2777 100644 --- a/tests/gas/PermissionedSpoke.Operations.gas.t.sol +++ b/tests/gas/Spoke.Gate.Operations.gas.t.sol @@ -1,28 +1,27 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import 'tests/setup/PermissionedSpokeBase.sol'; +import 'tests/setup/GateSpokeBase.sol'; import { - PositionManagerPolicyGate, GlobalManagerPolicyGate, BorrowAllowlistPolicyGate, MockAllowlist } from 'tests/helpers/mocks/PolicyGates.sol'; /// forge-config: default.isolate = true -contract PermissionedSpokeOperations_Gas_Tests is PermissionedSpokeBase { - string internal NAMESPACE = 'PermissionedSpoke.Operations'; +contract SpokeGateOperations_Gas_Tests is GateSpokeBase { + string internal NAMESPACE = 'Spoke.Gate.Operations'; /// @dev Same authorization as the standard spoke, routed through the gate. function test_operations_positionManagerPolicy() public { - ISpoke target = _deployPermissionedSpoke(address(new PositionManagerPolicyGate())); + ISpoke target = _deploySpokeWithGate(address(new PositionManagerGate(address(accessManager)))); _snapshotOperations(target, 'position-manager policy'); } /// @dev Horizon-style policy: a fixed global manager may act for any user. function test_operations_globalManagerPolicy() public { - ISpoke target = _deployPermissionedSpoke(address(new GlobalManagerPolicyGate(RWA_MANAGER))); + ISpoke target = _deploySpokeWithGate(address(new GlobalManagerPolicyGate(RWA_MANAGER))); _snapshotOperations(target, 'global-manager policy'); } @@ -30,7 +29,7 @@ contract PermissionedSpokeOperations_Gas_Tests is PermissionedSpokeBase { function test_operations_borrowAllowlistPolicy() public { MockAllowlist allowlist = new MockAllowlist(); allowlist.setAllowed(alice, true); - ISpoke target = _deployPermissionedSpoke(address(new BorrowAllowlistPolicyGate(allowlist))); + ISpoke target = _deploySpokeWithGate(address(new BorrowAllowlistPolicyGate(allowlist))); _snapshotOperations(target, 'borrow-allowlist policy'); } diff --git a/tests/gas/Spoke.Operations.gas.t.sol b/tests/gas/Spoke.Operations.gas.t.sol index de6df29af..859f40766 100644 --- a/tests/gas/Spoke.Operations.gas.t.sol +++ b/tests/gas/Spoke.Operations.gas.t.sol @@ -328,33 +328,36 @@ contract SpokeOperations_Gas_Tests is Base { (address user, uint256 userPk) = makeAddrAndKey('user'); address positionManager = makeAddr('positionManager'); vm.prank(SPOKE_ADMIN); - spoke.updatePositionManager({positionManager: positionManager, active: true}); + _updatePositionManager(spoke, positionManager, true); uint192 nonceKey = 100; vm.prank(user); - spoke.useNonce(nonceKey); - - ISpoke.PositionManagerUpdate[] memory updates = new ISpoke.PositionManagerUpdate[](1); - updates[0] = ISpoke.PositionManagerUpdate(positionManager, true); - - ISpoke.SetUserPositionManagers memory p = ISpoke.SetUserPositionManagers({ - onBehalfOf: user, - updates: updates, - nonce: spoke.nonces(user, nonceKey), - deadline: vm.getBlockTimestamp() - }); + IPositionManagerGate(spoke.GATE()).useNonce(nonceKey); + + IPositionManagerGate.PositionManagerUpdate[] + memory updates = new IPositionManagerGate.PositionManagerUpdate[](1); + updates[0] = IPositionManagerGate.PositionManagerUpdate(positionManager, true); + + IPositionManagerGate.SetUserPositionManagers memory p = IPositionManagerGate + .SetUserPositionManagers({ + spoke: address(spoke), + onBehalfOf: user, + updates: updates, + nonce: IPositionManagerGate(spoke.GATE()).nonces(user, nonceKey), + deadline: vm.getBlockTimestamp() + }); (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPk, _getTypedDataHash(spoke, p)); bytes memory signature = abi.encodePacked(r, s, v); - spoke.setUserPositionManagersWithSig(p, signature); + _setUserPositionManagersWithSig(spoke, p, signature); vm.snapshotGasLastCall(NAMESPACE, 'setUserPositionManagersWithSig: enable'); p.updates[0].approve = false; - p.nonce = spoke.nonces(user, nonceKey); + p.nonce = IPositionManagerGate(spoke.GATE()).nonces(user, nonceKey); (v, r, s) = vm.sign(userPk, _getTypedDataHash(spoke, p)); signature = abi.encodePacked(r, s, v); - spoke.setUserPositionManagersWithSig(p, signature); + _setUserPositionManagersWithSig(spoke, p, signature); vm.snapshotGasLastCall(NAMESPACE, 'setUserPositionManagersWithSig: disable'); } diff --git a/tests/helpers/mocks/AaveV4TestOrchestrationWrapper.sol b/tests/helpers/mocks/AaveV4TestOrchestrationWrapper.sol index c981a0508..611402a45 100644 --- a/tests/helpers/mocks/AaveV4TestOrchestrationWrapper.sol +++ b/tests/helpers/mocks/AaveV4TestOrchestrationWrapper.sol @@ -11,6 +11,17 @@ contract AaveV4TestOrchestrationWrapper { return address(AaveV4TestOrchestration.deploySpokeImplementation(oracle, maxUserReservesLimit)); } + function deploySpokeImplementation( + address oracle, + uint16 maxUserReservesLimit, + address gate + ) external returns (address) { + return + address( + AaveV4TestOrchestration.deploySpokeImplementation(oracle, maxUserReservesLimit, gate) + ); + } + function deployHub(address authority, address proxyAdminOwner) external returns (address) { return address(AaveV4TestOrchestration.deployHub(authority, proxyAdminOwner)); } diff --git a/tests/helpers/mocks/EIP712Types.sol b/tests/helpers/mocks/EIP712Types.sol index 2b68f4336..6ff001a3f 100644 --- a/tests/helpers/mocks/EIP712Types.sol +++ b/tests/helpers/mocks/EIP712Types.sol @@ -8,6 +8,7 @@ pragma solidity ^0.8.20; library EIP712Types { /// @dev Spoke Intents struct SetUserPositionManagers { + address spoke; address onBehalfOf; PositionManagerUpdate[] updates; uint256 nonce; diff --git a/tests/helpers/mocks/JsonBindings.sol b/tests/helpers/mocks/JsonBindings.sol index d28d50486..22c6bc3b6 100644 --- a/tests/helpers/mocks/JsonBindings.sol +++ b/tests/helpers/mocks/JsonBindings.sol @@ -42,7 +42,7 @@ library JsonBindings { Vm constant vm = Vm(address(uint160(uint256(keccak256('hevm cheat code'))))); // prettier-ignore - string constant schema_SetUserPositionManagers = "SetUserPositionManagers(address onBehalfOf,PositionManagerUpdate[] updates,uint256 nonce,uint256 deadline)PositionManagerUpdate(address positionManager,bool approve)"; + string constant schema_SetUserPositionManagers = "SetUserPositionManagers(address spoke,address onBehalfOf,PositionManagerUpdate[] updates,uint256 nonce,uint256 deadline)PositionManagerUpdate(address positionManager,bool approve)"; // prettier-ignore string constant schema_PositionManagerUpdate = "PositionManagerUpdate(address positionManager,bool approve)"; // prettier-ignore diff --git a/tests/helpers/mocks/MockSpoke.sol b/tests/helpers/mocks/MockSpoke.sol index adb49b805..3110ece06 100644 --- a/tests/helpers/mocks/MockSpoke.sol +++ b/tests/helpers/mocks/MockSpoke.sol @@ -27,8 +27,9 @@ contract MockSpoke is Spoke, Test { constructor( address oracle_, - uint16 maxUserReservesLimit_ - ) Spoke(oracle_, maxUserReservesLimit_) {} + uint16 maxUserReservesLimit_, + address gate_ + ) Spoke(oracle_, maxUserReservesLimit_, gate_) {} function initialize(address) external override {} @@ -37,7 +38,7 @@ contract MockSpoke is Spoke, Test { uint256 reserveId, uint256 amount, address onBehalfOf - ) external nonReentrant onlyPositionManager(onBehalfOf) returns (uint256, uint256) { + ) external nonReentrant onlyPositionActionAllowed(onBehalfOf) returns (uint256, uint256) { Reserve storage reserve = _reserves.get(reserveId); UserPosition storage userPosition = _userPositions[onBehalfOf][reserveId]; PositionStatus storage positionStatus = _positionStatus[onBehalfOf]; diff --git a/tests/helpers/mocks/MockSpokeGate.sol b/tests/helpers/mocks/MockSpokeGate.sol index 062a01ce4..9cfc2a21b 100644 --- a/tests/helpers/mocks/MockSpokeGate.sol +++ b/tests/helpers/mocks/MockSpokeGate.sol @@ -2,12 +2,11 @@ pragma solidity ^0.8.0; import {ISpokeGate} from 'src/spoke/interfaces/ISpokeGate.sol'; -import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; /// @dev Gate mock: /// - `globalManager`s are allowed to act on behalf of any user (e.g. an RWA manager) /// - `gated` selectors additionally require the position owner to be `eligible` -/// - otherwise falls back to the calling Spoke's default position manager authorization +/// - otherwise allows users to manage their own positions contract MockSpokeGate is ISpokeGate { mapping(address caller => bool) public globalManager; mapping(bytes4 selector => bool) public gated; @@ -32,6 +31,6 @@ contract MockSpokeGate is ISpokeGate { ) external view returns (bool) { if (globalManager[caller]) return true; if (gated[bytes4(data)] && !eligible[onBehalfOf]) return false; - return ISpoke(msg.sender).isPositionManager(onBehalfOf, caller); + return caller == onBehalfOf; } } diff --git a/tests/helpers/mocks/MockSpokeInstance.sol b/tests/helpers/mocks/MockSpokeInstance.sol index b4e05b328..5cda53aaa 100644 --- a/tests/helpers/mocks/MockSpokeInstance.sol +++ b/tests/helpers/mocks/MockSpokeInstance.sol @@ -14,19 +14,21 @@ contract MockSpokeInstance is Spoke { * @param spokeRevision_ The revision of the spoke contract. * @param oracle_ The address of the oracle. * @param maxUserReservesLimit_ The maximum number of reserves a user can have (both collaterals and borrows). + * @param gate_ The gate authorizing position actions. */ constructor( uint64 spokeRevision_, address oracle_, - uint16 maxUserReservesLimit_ - ) Spoke(oracle_, maxUserReservesLimit_) { + uint16 maxUserReservesLimit_, + address gate_ + ) Spoke(oracle_, maxUserReservesLimit_, gate_) { SPOKE_REVISION = spokeRevision_; _disableInitializers(); } /// @inheritdoc Spoke function initialize(address _authority) external override reinitializer(SPOKE_REVISION) { - emit SetSpokeImmutables(ORACLE, MAX_USER_RESERVES_LIMIT); + emit SetSpokeImmutables(ORACLE, MAX_USER_RESERVES_LIMIT, GATE); require(_authority != address(0), InvalidAddress()); __AccessManaged_init(_authority); diff --git a/tests/helpers/mocks/PolicyGates.sol b/tests/helpers/mocks/PolicyGates.sol index f488b27ce..b8edebd88 100644 --- a/tests/helpers/mocks/PolicyGates.sol +++ b/tests/helpers/mocks/PolicyGates.sol @@ -8,17 +8,6 @@ interface IAllowlist { function isAllowed(address account) external view returns (bool); } -/// @dev Gate replicating the default position-manager authorization. -contract PositionManagerPolicyGate is ISpokeGate { - function isCallAllowed( - address caller, - address onBehalfOf, - bytes calldata - ) external view returns (bool) { - return ISpoke(msg.sender).isPositionManager(onBehalfOf, caller); - } -} - /// @dev Gate allowing a fixed global manager to act on behalf of any user (e.g. an RWA manager). contract GlobalManagerPolicyGate is ISpokeGate { address public immutable GLOBAL_MANAGER; @@ -33,7 +22,7 @@ contract GlobalManagerPolicyGate is ISpokeGate { bytes calldata ) external view returns (bool) { if (caller == GLOBAL_MANAGER) return true; - return ISpoke(msg.sender).isPositionManager(onBehalfOf, caller); + return caller == onBehalfOf; } } @@ -51,7 +40,7 @@ contract BorrowAllowlistPolicyGate is ISpokeGate { bytes calldata data ) external view returns (bool) { if (bytes4(data) == ISpoke.borrow.selector && !ALLOWLIST.isAllowed(onBehalfOf)) return false; - return ISpoke(msg.sender).isPositionManager(onBehalfOf, caller); + return caller == onBehalfOf; } } diff --git a/tests/helpers/mocks/deployments/procedures/AaveV4SpokeDeployProcedureWrapper.sol b/tests/helpers/mocks/deployments/procedures/AaveV4SpokeDeployProcedureWrapper.sol index 1c4ade46f..8a525731d 100644 --- a/tests/helpers/mocks/deployments/procedures/AaveV4SpokeDeployProcedureWrapper.sol +++ b/tests/helpers/mocks/deployments/procedures/AaveV4SpokeDeployProcedureWrapper.sol @@ -10,6 +10,7 @@ contract AaveV4SpokeDeployProcedureWrapper is AaveV4SpokeDeployProcedure { address proxyAdminOwner, address authority, address oracle, + address gate, bytes memory spokeBytecode, uint16 maxUserReservesLimit, bytes32 salt @@ -19,6 +20,7 @@ contract AaveV4SpokeDeployProcedureWrapper is AaveV4SpokeDeployProcedure { proxyAdminOwner, authority, oracle, + gate, spokeBytecode, maxUserReservesLimit, salt diff --git a/tests/helpers/spoke/EIP712Helpers.sol b/tests/helpers/spoke/EIP712Helpers.sol index c97203ed3..353533637 100644 --- a/tests/helpers/spoke/EIP712Helpers.sol +++ b/tests/helpers/spoke/EIP712Helpers.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import {Test} from 'forge-std/Test.sol'; import {INoncesKeyed} from 'src/utils/NoncesKeyed.sol'; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {IPositionManagerGate} from 'src/spoke/interfaces/IPositionManagerGate.sol'; import {EIP712Types} from 'tests/helpers/mocks/EIP712Types.sol'; import {TestnetERC20} from 'tests/helpers/mocks/TestnetERC20.sol'; import {SafeCast} from 'src/dependencies/openzeppelin/SafeCast.sol'; @@ -33,13 +34,13 @@ abstract contract EIP712Helpers is Test { function _getTypedDataHash( ISpoke spoke, - ISpoke.SetUserPositionManagers memory setUserPositionManagers + IPositionManagerGate.SetUserPositionManagers memory setUserPositionManagers ) internal view returns (bytes32) { return keccak256( abi.encodePacked( '\x19\x01', - spoke.DOMAIN_SEPARATOR(), + IPositionManagerGate(spoke.GATE()).DOMAIN_SEPARATOR(), vm.eip712HashStruct('SetUserPositionManagers', abi.encode(setUserPositionManagers)) ) ); diff --git a/tests/helpers/spoke/MathHelpers.sol b/tests/helpers/spoke/MathHelpers.sol index 7a43bdb14..51fb08d8e 100644 --- a/tests/helpers/spoke/MathHelpers.sol +++ b/tests/helpers/spoke/MathHelpers.sol @@ -145,7 +145,9 @@ abstract contract MathHelpers is QueryHelpers { ) internal returns (ISpoke.UserAccountData memory) { uint256 snapshot = vm.snapshotState(); - address mockSpoke = address(new MockSpoke(spoke.ORACLE(), MAX_ALLOWED_USER_RESERVES_LIMIT)); + address mockSpoke = address( + new MockSpoke(spoke.ORACLE(), MAX_ALLOWED_USER_RESERVES_LIMIT, spoke.GATE()) + ); address implementation = _getImplementationAddress(address(spoke)); diff --git a/tests/helpers/spoke/PositionManagerGateAdapter.sol b/tests/helpers/spoke/PositionManagerGateAdapter.sol new file mode 100644 index 000000000..b9bf1ab00 --- /dev/null +++ b/tests/helpers/spoke/PositionManagerGateAdapter.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {IPositionManagerGate} from 'src/spoke/interfaces/IPositionManagerGate.sol'; + +/// @dev Test adapter preserving concise Spoke-oriented calls while position-manager state lives in the gate. +library PositionManagerGateAdapter { + function updatePositionManager(ISpoke spoke, address positionManager, bool active) internal { + _gate(spoke).updatePositionManager(address(spoke), positionManager, active); + } + + function setUserPositionManager(ISpoke spoke, address positionManager, bool approve) internal { + _gate(spoke).setUserPositionManager(address(spoke), positionManager, approve); + } + + function setUserPositionManagersWithSig( + ISpoke spoke, + IPositionManagerGate.SetUserPositionManagers memory params, + bytes memory signature + ) internal { + _gate(spoke).setUserPositionManagersWithSig(params, signature); + } + + function renouncePositionManagerRole(ISpoke spoke, address user) internal { + _gate(spoke).renouncePositionManagerRole(address(spoke), user); + } + + function isPositionManagerActive( + ISpoke spoke, + address positionManager + ) internal view returns (bool) { + return _gate(spoke).isPositionManagerActive(address(spoke), positionManager); + } + + function isPositionManager( + ISpoke spoke, + address user, + address positionManager + ) internal view returns (bool) { + return _gate(spoke).isPositionManager(address(spoke), user, positionManager); + } + + function _gate(ISpoke spoke) private view returns (IPositionManagerGate) { + return IPositionManagerGate(spoke.GATE()); + } +} + +abstract contract PositionManagerGateTestHelpers { + mapping(address spoke => IPositionManagerGate gate) private _positionManagerGates; + + function _cachePositionManagerGate(ISpoke spoke) internal { + _positionManagerGates[address(spoke)] = IPositionManagerGate(spoke.GATE()); + } + + function _positionManagerGate(ISpoke spoke) internal view returns (IPositionManagerGate gate) { + gate = _positionManagerGates[address(spoke)]; + if (address(gate) == address(0)) gate = IPositionManagerGate(spoke.GATE()); + } + + function _updatePositionManager(ISpoke spoke, address positionManager, bool active) internal { + _positionManagerGate(spoke).updatePositionManager(address(spoke), positionManager, active); + } + + function _setUserPositionManager(ISpoke spoke, address positionManager, bool approve) internal { + _positionManagerGate(spoke).setUserPositionManager(address(spoke), positionManager, approve); + } + + function _setUserPositionManagersWithSig( + ISpoke spoke, + IPositionManagerGate.SetUserPositionManagers memory params, + bytes memory signature + ) internal { + _positionManagerGate(spoke).setUserPositionManagersWithSig(params, signature); + } + + function _renouncePositionManagerRole(ISpoke spoke, address user) internal { + _positionManagerGate(spoke).renouncePositionManagerRole(address(spoke), user); + } + + function _isPositionManagerActive( + ISpoke spoke, + address positionManager + ) internal view returns (bool) { + return _positionManagerGate(spoke).isPositionManagerActive(address(spoke), positionManager); + } + + function _isPositionManager( + ISpoke spoke, + address user, + address positionManager + ) internal view returns (bool) { + return _positionManagerGate(spoke).isPositionManager(address(spoke), user, positionManager); + } +} diff --git a/tests/helpers/spoke/SetupHelpers.sol b/tests/helpers/spoke/SetupHelpers.sol index 1b6a44790..abe32bbc1 100644 --- a/tests/helpers/spoke/SetupHelpers.sol +++ b/tests/helpers/spoke/SetupHelpers.sol @@ -8,6 +8,7 @@ import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; import {IHub} from 'src/hub/interfaces/IHub.sol'; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {PositionManagerGate} from 'src/spoke/gates/PositionManagerGate.sol'; import {IAaveOracle} from 'src/spoke/interfaces/IAaveOracle.sol'; import {AaveOracle} from 'src/spoke/AaveOracle.sol'; import {AaveV4TestOrchestration} from 'tests/deployments/orchestration/AaveV4TestOrchestration.sol'; @@ -239,7 +240,9 @@ abstract contract SetupHelpers is CheckedActions, ConfigHelpers, MockHelpers { uint256 reserveId, uint256 debtAmount ) internal { - address mockSpoke = address(new MockSpoke(spoke.ORACLE(), MAX_ALLOWED_USER_RESERVES_LIMIT)); + address mockSpoke = address( + new MockSpoke(spoke.ORACLE(), MAX_ALLOWED_USER_RESERVES_LIMIT, spoke.GATE()) + ); address implementation = _getImplementationAddress(address(spoke)); @@ -407,7 +410,8 @@ abstract contract SetupHelpers is CheckedActions, ConfigHelpers, MockHelpers { address currentImpl = _getImplementationAddress(address(spoke)); ISpokeInstance newImpl = AaveV4TestOrchestration.deploySpokeImplementation( spoke.ORACLE(), - newLimit + newLimit, + spoke.GATE() ); vm.etch(currentImpl, address(newImpl).code); } @@ -432,11 +436,16 @@ abstract contract SetupHelpers is CheckedActions, ConfigHelpers, MockHelpers { vm.startPrank(deployer); IAaveOracle oracle = new AaveOracle(8); + PositionManagerGate gate = new PositionManagerGate(_accessManager); ISpoke spoke = ISpoke( AaveV4TestOrchestration.proxify( address( - AaveV4TestOrchestration.deploySpokeImplementation(address(oracle), maxUserReservesLimit) + AaveV4TestOrchestration.deploySpokeImplementation( + address(oracle), + maxUserReservesLimit, + address(gate) + ) ), proxyAdminOwner, abi.encodeCall(ISpokeInstance.initialize, (_accessManager)) diff --git a/tests/scripts/AaveV4DeployBatchBaseScript.t.sol b/tests/scripts/AaveV4DeployBatchBaseScript.t.sol index 60b769d01..33f3d9cd2 100644 --- a/tests/scripts/AaveV4DeployBatchBaseScript.t.sol +++ b/tests/scripts/AaveV4DeployBatchBaseScript.t.sol @@ -60,6 +60,7 @@ contract AaveV4DeployBatchBaseScriptTest is Test { spokeConfiguratorAdmin: makeAddr('spokeConfiguratorAdmin'), gatewayOwner: makeAddr('gatewayOwner'), positionManagerOwner: makeAddr('positionManagerOwner'), + spokeGate: address(0), nativeWrapper: address(new WETH9()), deployNativeTokenGateway: true, deploySignatureGateway: true, diff --git a/tests/setup/Base.t.sol b/tests/setup/Base.t.sol index f6f511f8e..34a600b18 100644 --- a/tests/setup/Base.t.sol +++ b/tests/setup/Base.t.sol @@ -57,6 +57,12 @@ import { // spoke import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {IPositionManagerGate} from 'src/spoke/interfaces/IPositionManagerGate.sol'; +import {PositionManagerGate} from 'src/spoke/gates/PositionManagerGate.sol'; +import { + PositionManagerGateAdapter, + PositionManagerGateTestHelpers +} from 'tests/helpers/spoke/PositionManagerGateAdapter.sol'; import {TreasurySpoke, ITreasurySpoke} from 'src/spoke/TreasurySpoke.sol'; import {TreasurySpokeInstance} from 'src/spoke/instances/TreasurySpokeInstance.sol'; import {IPriceOracle} from 'src/spoke/interfaces/IPriceOracle.sol'; @@ -136,7 +142,8 @@ import {SpokeUtilsWrapper} from 'tests/helpers/mocks/SpokeUtilsWrapper.sol'; import 'tests/utils/BatchTestProcedures.sol'; -abstract contract Base is BaseHelpers, BatchTestProcedures { +abstract contract Base is BaseHelpers, BatchTestProcedures, PositionManagerGateTestHelpers { + using PositionManagerGateAdapter for ISpoke; using stdStorage for StdStorage; using WadRayMath for *; using SharesMath for uint256; @@ -178,6 +185,7 @@ abstract contract Base is BaseHelpers, BatchTestProcedures { for (uint256 i; i < numSpokes; ++i) { _spokes.push(ISpoke(report.spokeReports[i].spoke)); _oracles.push(IAaveOracle(report.spokeReports[i].aaveOracle)); + _cachePositionManagerGate(ISpoke(report.spokeReports[i].spoke)); vm.label(report.spokeReports[i].spoke, string.concat('spoke', string(abi.encode(i)))); vm.label(report.spokeReports[i].aaveOracle, string.concat('oracle', string(abi.encode(i)))); diff --git a/tests/setup/PermissionedSpokeBase.sol b/tests/setup/GateSpokeBase.sol similarity index 84% rename from tests/setup/PermissionedSpokeBase.sol rename to tests/setup/GateSpokeBase.sol index 50746ff31..44d9b68d7 100644 --- a/tests/setup/PermissionedSpokeBase.sol +++ b/tests/setup/GateSpokeBase.sol @@ -5,12 +5,12 @@ import 'tests/setup/Base.t.sol'; import {TransparentUpgradeableProxy} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; import {DeployConstants} from 'src/deployments/utils/libraries/DeployConstants.sol'; -import {PermissionedSpokeInstance} from 'src/spoke/instances/PermissionedSpokeInstance.sol'; +import {SpokeInstance} from 'src/spoke/instances/SpokeInstance.sol'; import {MockSpokeGate} from 'tests/helpers/mocks/MockSpokeGate.sol'; -/// @dev Deploys a spoke with the `PermissionedSpokeInstance` implementation gated by a mock gate, +/// @dev Deploys a unified Spoke implementation with a mock gate, /// with two reserves on hub1 (weth as collateral, usdx as borrowable). -abstract contract PermissionedSpokeBase is Base { +abstract contract GateSpokeBase is Base { ISpoke internal spoke; MockSpokeGate internal gate; address internal RWA_MANAGER = makeAddr('RWA_MANAGER'); @@ -23,13 +23,13 @@ abstract contract PermissionedSpokeBase is Base { super.setUp(); gate = new MockSpokeGate(); - spoke = _deployPermissionedSpoke(address(gate)); + spoke = _deploySpokeWithGate(address(gate)); } - /// @dev Deploys a permissioned spoke with the given gate, mirroring the standard fixture config. - function _deployPermissionedSpoke(address newGate) internal returns (ISpoke newSpoke) { + /// @dev Deploys a Spoke with the given gate, mirroring the standard fixture config. + function _deploySpokeWithGate(address newGate) internal returns (ISpoke newSpoke) { AaveOracle oracle = new AaveOracle(8); - PermissionedSpokeInstance implementation = new PermissionedSpokeInstance({ + SpokeInstance implementation = new SpokeInstance({ oracle_: address(oracle), maxUserReservesLimit_: DeployConstants.MAX_ALLOWED_USER_RESERVES_LIMIT, gate_: newGate