diff --git a/.gitignore b/.gitignore index 619603d4d..52e2f2d53 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,6 @@ lcov* report/ .DS_Store -.venv/ \ No newline at end of file +.venv/ + +output/ \ No newline at end of file diff --git a/Makefile b/Makefile index 45bd7435d..1a71efb51 100644 --- a/Makefile +++ b/Makefile @@ -29,3 +29,12 @@ coverage : make coverage-clean make coverage-report make coverage-badge + +# Deploy + +# Full deployment, including hubs, spokes, and gateways +deploy-full :; + FOUNDRY_PROFILE=${CHAIN} forge script scripts/deploy/AaveV4DeployBatch.s.sol:AaveV4DeployBatchScript \ + --rpc-url ${CHAIN} --sender ${SENDER} --account ${ACCOUNT} --slow \ + --broadcast + diff --git a/config/AaveV4DeployInput.json b/config/AaveV4DeployInput.json new file mode 100644 index 000000000..1271033b3 --- /dev/null +++ b/config/AaveV4DeployInput.json @@ -0,0 +1,15 @@ +{ + "accessManagerAdmin": "0x0000000000000000000000000000000000000001", + "hubConfiguratorOwner": "0x0000000000000000000000000000000000000002", + "hubAdmin": "0x0000000000000000000000000000000000000003", + "treasurySpokeOwner": "0x0000000000000000000000000000000000000003", + "spokeConfiguratorOwner": "0x0000000000000000000000000000000000000004", + "spokeAdmin": "0x0000000000000000000000000000000000000005", + "spokeProxyAdminOwner": "0x0000000000000000000000000000000000000005", + "gatewayOwner": "0x0000000000000000000000000000000000000005", + "nativeWrapper": "0x0000000000000000000000000000000000000006", + "grantRoles": true, + "hubLabels": ["Hub 1", "Hub 2", "Hub 3"], + "spokeLabels": ["Spoke 1", "Spoke 2", "Spoke 3"], + "salt": "salt" +} diff --git a/foundry.toml b/foundry.toml index 7ce9cc4c7..4a6d7e00a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,11 @@ src = 'src' test = 'tests' out = 'out' libs = ['lib'] -fs_permissions = [{ access = "read", path = "tests/mocks/JsonBindings.sol" }] +fs_permissions = [ + { access = "read", path = "tests/mocks/JsonBindings.sol" }, + { access = "read", path = "./config" }, + { access = "read-write", path = "./output" } +] solc_version = "0.8.28" evm_version = "cancun" optimizer = true @@ -11,6 +15,7 @@ optimizer_runs = 200 bytecode_hash = "none" gas_snapshot_check = false gas_limit = 1099511627776 +ffi = true [bind_json] out = "tests/mocks/JsonBindings.sol" @@ -49,6 +54,7 @@ zkevm = "${RPC_ZKEVM}" gnosis = "${RPC_GNOSIS}" bnb = "${RPC_BNB}" celo = "${RPC_CELO}" +anvil = "http://127.0.0.1:8545" [etherscan] mainnet = { key = "${ETHERSCAN_API_KEY_MAINNET}", chainId = 1 } diff --git a/scripts/deploy/AaveV4DeployBatch.s.sol b/scripts/deploy/AaveV4DeployBatch.s.sol new file mode 100644 index 000000000..b0a719202 --- /dev/null +++ b/scripts/deploy/AaveV4DeployBatch.s.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'scripts/deploy/AaveV4DeployBatchBase.s.sol'; + +contract AaveV4DeployBatchScript is AaveV4DeployBatchBaseScript { + string internal constant INPUT_FILE = 'AaveV4DeployInput.json'; + string internal constant OUTPUT_FILE = 'AaveV4DeployBatch.json'; + constructor() AaveV4DeployBatchBaseScript(INPUT_FILE, OUTPUT_FILE) {} +} diff --git a/scripts/deploy/AaveV4DeployBatchBase.s.sol b/scripts/deploy/AaveV4DeployBatchBase.s.sol new file mode 100644 index 000000000..f9ae25d83 --- /dev/null +++ b/scripts/deploy/AaveV4DeployBatchBase.s.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {OrchestrationReports} from 'src/deployments/libraries/OrchestrationReports.sol'; +import {InputUtils} from 'src/deployments/utils/InputUtils.sol'; +import {MetadataLogger} from 'src/deployments/utils/MetadataLogger.sol'; +import { + AaveV4DeployOrchestration +} from 'src/deployments/orchestration/AaveV4DeployOrchestration.sol'; + +import {Script} from 'forge-std/Script.sol'; + +abstract contract AaveV4DeployBatchBaseScript is Script, InputUtils { + struct Warnings { + string[] s; + } + + string internal constant INPUT_PATH = 'config/'; + string internal constant OUTPUT_DIR = 'output/reports/deployments/'; + string internal _inputFileName; + string internal _outputFileName; + Warnings internal _warnings; + + constructor(string memory inputFileName_, string memory outputFileName_) { + _inputFileName = inputFileName_; + _outputFileName = outputFileName_; + } + + function run() external virtual { + vm.createDir(OUTPUT_DIR, true); + MetadataLogger logger = new MetadataLogger(OUTPUT_DIR); + FullDeployInputs memory inputs = loadFullDeployInputs( + string.concat(INPUT_PATH, _inputFileName) + ); + (, address deployer, ) = vm.readCallers(); + inputs = _loadWarningsAndSanitizeInputs(logger, inputs, deployer); + + logger.log('CHAIN ID', block.chainid); + logger.log('...Starting Aave V4 Batch Deployment...'); + vm.startBroadcast(deployer); + OrchestrationReports.FullDeploymentReport memory report = AaveV4DeployOrchestration + .deployAaveV4(logger, deployer, inputs); + vm.stopBroadcast(); + logger.writeJsonReportMarket(report); + logger.log('...Batch Deployment Completed...'); + logger.log('...Saving Logs...'); + logger.save({fileName: _outputFileName, withTimestamp: true}); + } + + function _loadWarningsAndSanitizeInputs( + MetadataLogger logger, + FullDeployInputs memory inputs, + address deployer + ) internal virtual returns (FullDeployInputs memory) { + string memory message = ' is zero address'; + string memory outcome = '; defaulting to deployer'; + + FullDeployInputs memory sanitizedInputs = inputs; + bool hadWarnings = false; + if (inputs.grantRoles) { + _logAndAppend(logger, string.concat('Roles are being set')); + hadWarnings = true; + if (inputs.accessManagerAdmin == address(0)) { + _logAndAppend(logger, string.concat('Access Manager Admin', message, outcome)); + sanitizedInputs.accessManagerAdmin = deployer; + } + if (inputs.hubConfiguratorOwner == address(0)) { + _logAndAppend(logger, string.concat('Hub Configurator Owner', message, outcome)); + sanitizedInputs.hubConfiguratorOwner = deployer; + } + if (inputs.spokeConfiguratorOwner == address(0)) { + _logAndAppend(logger, string.concat('Spoke Configurator Owner', message, outcome)); + sanitizedInputs.spokeConfiguratorOwner = deployer; + } + if (inputs.spokeProxyAdminOwner == address(0)) { + _logAndAppend(logger, string.concat('Spoke Proxy Admin Owner', message, outcome)); + sanitizedInputs.spokeProxyAdminOwner = deployer; + } + if (inputs.treasurySpokeOwner == address(0)) { + _logAndAppend(logger, string.concat('Treasury Spoke Owner', message, outcome)); + sanitizedInputs.treasurySpokeOwner = deployer; + } + if (inputs.spokeAdmin == address(0)) { + _logAndAppend(logger, string.concat('Spoke Admin', message, outcome)); + sanitizedInputs.spokeAdmin = deployer; + } + if (inputs.hubAdmin == address(0)) { + _logAndAppend(logger, string.concat('Hub Admin', message, outcome)); + sanitizedInputs.hubAdmin = deployer; + } + } + if (inputs.hubLabels.length == 0) { + _logAndAppend(logger, string.concat('Hub will not be deployed')); + hadWarnings = true; + sanitizedInputs.hubLabels = new string[](0); + } + if (inputs.spokeLabels.length == 0) { + _logAndAppend(logger, string.concat('Spoke will not be deployed')); + hadWarnings = true; + sanitizedInputs.spokeLabels = new string[](0); + } + if (inputs.nativeWrapper == address(0)) { + _logAndAppend( + logger, + string.concat( + 'Native wrapper', + message, + "; NativeTokenGateway & SignatureGateway will not be deployed'" + ) + ); + hadWarnings = true; + sanitizedInputs.nativeWrapper = address(0); + } + if (inputs.gatewayOwner == address(0)) { + _logAndAppend(logger, string.concat('Gateway owner', message, outcome)); + hadWarnings = true; + sanitizedInputs.gatewayOwner = deployer; + } + if (hadWarnings) { + _executeUserPrompt(); + } + return sanitizedInputs; + } + + function _executeUserPrompt() internal virtual { + string memory ack = vm.prompt( + string.concat(_joinWarnings(_warnings), "\nEnter 'y' to continue") + ); + if (keccak256(bytes(ack)) != keccak256(bytes('y'))) { + revert('User did not acknowledge warnings. Please try again.'); + } + } + + function _logAndAppend(MetadataLogger logger, string memory warning) internal virtual { + warning = string.concat('WARNING: ', warning); + logger.log(warning); + _warnings.s.push(warning); + } + + function _joinWarnings(Warnings storage warnings) internal view virtual returns (string memory) { + uint256 n = warnings.s.length; + if (n == 0) return ''; + string memory out = warnings.s[0]; + for (uint256 i = 1; i < n; i++) { + out = string.concat(out, '\n', warnings.s[i]); + } + return string.concat(out, '\n'); + } +} diff --git a/snapshots/Hub.Operations.json b/snapshots/Hub.Operations.json index 54b1b87c4..57cc062f4 100644 --- a/snapshots/Hub.Operations.json +++ b/snapshots/Hub.Operations.json @@ -1,6 +1,6 @@ { "add": "88006", - "add: with transfer": "109613", + "add: with transfer": "109652", "draw": "105931", "eliminateDeficit: full": "59781", "eliminateDeficit: partial": "69429", @@ -11,8 +11,8 @@ "remove: partial": "81640", "reportDeficit": "115225", "restore: full": "80471", - "restore: full - with transfer": "173377", + "restore: full - with transfer": "173421", "restore: partial": "89137", - "restore: partial - with transfer": "147400", + "restore: partial - with transfer": "147439", "transferShares": "71192" } \ No newline at end of file diff --git a/snapshots/SignatureGateway.Operations.json b/snapshots/SignatureGateway.Operations.json index 96eb0ef3e..e87d6eaea 100644 --- a/snapshots/SignatureGateway.Operations.json +++ b/snapshots/SignatureGateway.Operations.json @@ -1,7 +1,7 @@ { "borrowWithSig": "215605", - "repayWithSig": "188872", - "setSelfAsUserPositionManagerWithSig": "75402", + "repayWithSig": "188860", + "setSelfAsUserPositionManagerWithSig": "75390", "setUsingAsCollateralWithSig": "85053", "supplyWithSig": "153205", "updateUserDynamicConfigWithSig": "62769", diff --git a/snapshots/Spoke.Operations.ZeroRiskPremium.json b/snapshots/Spoke.Operations.ZeroRiskPremium.json index ed2faa23d..9b4f9ec68 100644 --- a/snapshots/Spoke.Operations.ZeroRiskPremium.json +++ b/snapshots/Spoke.Operations.ZeroRiskPremium.json @@ -10,7 +10,7 @@ "permitReserve + supply + enable collateral (multicall)": "160573", "repay: full": "126094", "repay: partial": "130983", - "setUserPositionManagerWithSig: disable": "44846", + "setUserPositionManagerWithSig: disable": "44822", "setUserPositionManagerWithSig: enable": "68875", "supply + enable collateral (multicall)": "140624", "supply: 0 borrows, collateral disabled": "123679", diff --git a/snapshots/Spoke.Operations.json b/snapshots/Spoke.Operations.json index deed9c95d..316fe74ec 100644 --- a/snapshots/Spoke.Operations.json +++ b/snapshots/Spoke.Operations.json @@ -10,7 +10,7 @@ "permitReserve + supply + enable collateral (multicall)": "160573", "repay: full": "120256", "repay: partial": "139545", - "setUserPositionManagerWithSig: disable": "44846", + "setUserPositionManagerWithSig: disable": "44822", "setUserPositionManagerWithSig: enable": "68875", "supply + enable collateral (multicall)": "140624", "supply: 0 borrows, collateral disabled": "123679", diff --git a/src/deployments/batches/AaveV4AccessBatch.sol b/src/deployments/batches/AaveV4AccessBatch.sol new file mode 100644 index 000000000..438868e6b --- /dev/null +++ b/src/deployments/batches/AaveV4AccessBatch.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; +import { + AaveV4AccessManagerEnumerableDeployProcedure +} from 'src/deployments/procedures/deploy/AaveV4AccessManagerEnumerableDeployProcedure.sol'; + +contract AaveV4AccessBatch is AaveV4AccessManagerEnumerableDeployProcedure { + BatchReports.AccessBatchReport internal _report; + + constructor(address admin_, bytes32 salt_) { + address accessManager = _deployAccessManagerEnumerable( + admin_, + keccak256(abi.encodePacked(SALT, salt_, 'accessManager')) + ); + _report = BatchReports.AccessBatchReport({accessManager: accessManager}); + } + + function getReport() external view returns (BatchReports.AccessBatchReport memory) { + return _report; + } +} diff --git a/src/deployments/batches/AaveV4ConfiguratorBatch.sol b/src/deployments/batches/AaveV4ConfiguratorBatch.sol new file mode 100644 index 000000000..c83e7bf8d --- /dev/null +++ b/src/deployments/batches/AaveV4ConfiguratorBatch.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; +import { + AaveV4HubConfiguratorDeployProcedure +} from 'src/deployments/procedures/deploy/hub/AaveV4HubConfiguratorDeployProcedure.sol'; +import { + AaveV4SpokeConfiguratorDeployProcedure +} from 'src/deployments/procedures/deploy/spoke/AaveV4SpokeConfiguratorDeployProcedure.sol'; + +contract AaveV4ConfiguratorBatch is + AaveV4HubConfiguratorDeployProcedure, + AaveV4SpokeConfiguratorDeployProcedure +{ + BatchReports.ConfiguratorBatchReport internal _report; + + constructor(address hubConfiguratorOwner_, address spokeConfiguratorOwner_, bytes32 salt_) { + address hubConfigurator = _deployHubConfigurator( + hubConfiguratorOwner_, + keccak256(abi.encodePacked(SALT, salt_, 'hubConfigurator')) + ); + address spokeConfigurator = _deploySpokeConfigurator( + spokeConfiguratorOwner_, + keccak256(abi.encodePacked(SALT, salt_, 'spokeConfigurator')) + ); + + _report = BatchReports.ConfiguratorBatchReport({ + hubConfigurator: hubConfigurator, + spokeConfigurator: spokeConfigurator + }); + } + + function getReport() external view returns (BatchReports.ConfiguratorBatchReport memory) { + return _report; + } +} diff --git a/src/deployments/batches/AaveV4GatewayBatch.sol b/src/deployments/batches/AaveV4GatewayBatch.sol new file mode 100644 index 000000000..9e69db2bd --- /dev/null +++ b/src/deployments/batches/AaveV4GatewayBatch.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; +import { + AaveV4NativeTokenGatewayDeployProcedure +} from 'src/deployments/procedures/deploy/position-manager/AaveV4NativeTokenGatewayDeployProcedure.sol'; +import { + AaveV4SignatureGatewayDeployProcedure +} from 'src/deployments/procedures/deploy/position-manager/AaveV4SignatureGatewayDeployProcedure.sol'; + +contract AaveV4GatewayBatch is + AaveV4NativeTokenGatewayDeployProcedure, + AaveV4SignatureGatewayDeployProcedure +{ + BatchReports.GatewaysBatchReport internal _report; + + constructor(address owner_, address nativeWrapper_, bytes32 salt_) { + address nativeGateway = _deployNativeTokenGateway({ + nativeWrapper: nativeWrapper_, + owner: owner_, + salt: keccak256(abi.encodePacked(SALT, salt_, 'nativeGateway')) + }); + address signatureGateway = _deploySignatureGateway( + owner_, + keccak256(abi.encodePacked(SALT, salt_, 'signatureGateway')) + ); + + _report = BatchReports.GatewaysBatchReport({ + nativeGateway: nativeGateway, + signatureGateway: signatureGateway + }); + } + + function getReport() external view returns (BatchReports.GatewaysBatchReport memory) { + return _report; + } +} diff --git a/src/deployments/batches/AaveV4HubBatch.sol b/src/deployments/batches/AaveV4HubBatch.sol new file mode 100644 index 000000000..3fe889d32 --- /dev/null +++ b/src/deployments/batches/AaveV4HubBatch.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; +import { + AaveV4HubDeployProcedure +} from 'src/deployments/procedures/deploy/hub/AaveV4HubDeployProcedure.sol'; +import { + AaveV4InterestRateStrategyDeployProcedure +} from 'src/deployments/procedures/deploy/hub/AaveV4InterestRateStrategyDeployProcedure.sol'; +import { + AaveV4TreasurySpokeDeployProcedure +} from 'src/deployments/procedures/deploy/spoke/AaveV4TreasurySpokeDeployProcedure.sol'; + +contract AaveV4HubBatch is + AaveV4HubDeployProcedure, + AaveV4InterestRateStrategyDeployProcedure, + AaveV4TreasurySpokeDeployProcedure +{ + BatchReports.HubBatchReport internal _report; + + constructor(address treasurySpokeOwner_, address accessManager_, bytes32 salt_) { + address hub = _deployHub(accessManager_, keccak256(abi.encodePacked(SALT, salt_, 'hub'))); + address irStrategy = _deployInterestRateStrategy( + hub, + keccak256(abi.encodePacked(SALT, salt_, 'irStrategy')) + ); + address treasurySpoke = _deployTreasurySpoke( + treasurySpokeOwner_, + hub, + keccak256(abi.encodePacked(SALT, salt_, 'treasurySpoke')) + ); + + _report = BatchReports.HubBatchReport({ + hub: hub, + irStrategy: irStrategy, + treasurySpoke: treasurySpoke + }); + } + + function getReport() external view returns (BatchReports.HubBatchReport memory) { + return _report; + } +} diff --git a/src/deployments/batches/AaveV4SpokeInstanceBatch.sol b/src/deployments/batches/AaveV4SpokeInstanceBatch.sol new file mode 100644 index 000000000..a521876ea --- /dev/null +++ b/src/deployments/batches/AaveV4SpokeInstanceBatch.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; +import { + AaveV4AaveOracleDeployProcedure +} from 'src/deployments/procedures/deploy/spoke/AaveV4AaveOracleDeployProcedure.sol'; +import { + AaveV4SpokeDeployProcedure +} from 'src/deployments/procedures/deploy/spoke/AaveV4SpokeDeployProcedure.sol'; +import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; + +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {IAaveOracle} from 'src/spoke/interfaces/IAaveOracle.sol'; + +contract AaveV4SpokeInstanceBatch is AaveV4SpokeDeployProcedure, AaveV4AaveOracleDeployProcedure { + BatchReports.SpokeInstanceBatchReport internal _report; + + constructor( + address spokeProxyAdminOwner_, + address accessManager_, + uint8 oracleDecimals_, + string memory oracleDescription_, + bytes32 salt_ + ) { + bytes32 spokeInstanceSalt = keccak256(abi.encodePacked(SALT, salt_, 'spokeInstance')); + // starting from contract nonce of 1 + address predictedOracle = Create2Utils.computeCreateAddress(address(this), 1); + address predictedSpokeProxy = _computeSpokeInstanceAddress({ + spokeProxyAdminOwner: spokeProxyAdminOwner_, + accessManager: accessManager_, + oracle: predictedOracle, + salt: spokeInstanceSalt + }); + address aaveOracle = _deployAaveOracle( + predictedSpokeProxy, + oracleDecimals_, + oracleDescription_, + keccak256(abi.encodePacked(SALT, salt_, 'aaveOracle')) + ); + (address spokeProxy, address spokeImplementation) = _deployUpgradableSpokeInstance({ + spokeProxyAdminOwner: spokeProxyAdminOwner_, + accessManager: accessManager_, + oracle: aaveOracle, + salt: spokeInstanceSalt + }); + + assert(aaveOracle == predictedOracle); + assert(spokeProxy == predictedSpokeProxy); + assert(ISpoke(spokeProxy).ORACLE() == aaveOracle); + assert(IAaveOracle(aaveOracle).SPOKE() == spokeProxy); + + _report = BatchReports.SpokeInstanceBatchReport({ + aaveOracle: aaveOracle, + spokeImplementation: spokeImplementation, + spokeProxy: spokeProxy + }); + } + + function getReport() external view returns (BatchReports.SpokeInstanceBatchReport memory) { + return _report; + } +} diff --git a/src/deployments/libraries/BatchReports.sol b/src/deployments/libraries/BatchReports.sol new file mode 100644 index 000000000..9c12db59f --- /dev/null +++ b/src/deployments/libraries/BatchReports.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +library BatchReports { + struct AccessBatchReport { + address accessManager; + } + + struct ConfiguratorBatchReport { + address hubConfigurator; + address spokeConfigurator; + } + + struct SpokeInstanceBatchReport { + address spokeImplementation; + address spokeProxy; + address aaveOracle; + } + + struct HubBatchReport { + address hub; + address irStrategy; + address treasurySpoke; + } + + struct GatewaysBatchReport { + address signatureGateway; + address nativeGateway; + } +} diff --git a/src/deployments/libraries/ConfigData.sol b/src/deployments/libraries/ConfigData.sol new file mode 100644 index 000000000..7ec7e5bb8 --- /dev/null +++ b/src/deployments/libraries/ConfigData.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {IHub} from 'src/hub/interfaces/IHub.sol'; +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; + +library ConfigData { + struct AddAssetParams { + address hub; + address underlying; + uint8 decimals; + address feeReceiver; + uint16 liquidityFee; + address irStrategy; + address reinvestmentController; + bytes irData; + } + + struct UpdateAssetConfigParams { + address hub; + uint256 assetId; + IHub.AssetConfig config; + bytes irData; + } + + struct AddSpokeParams { + address hub; + uint256 assetId; + address spoke; + IHub.SpokeConfig config; + } + + struct AddSpokeToAssetsParams { + address hub; + address spoke; + uint256[] assetIds; + IHub.SpokeConfig[] configs; + } + + struct UpdateLiquidationConfigParams { + address spoke; + ISpoke.LiquidationConfig config; + } + + struct AddReserveParams { + address spoke; + address hub; + uint256 assetId; + address priceSource; + ISpoke.ReserveConfig config; + ISpoke.DynamicReserveConfig dynamicConfig; + } +} diff --git a/src/deployments/libraries/OrchestrationReports.sol b/src/deployments/libraries/OrchestrationReports.sol new file mode 100644 index 000000000..8d956c8a1 --- /dev/null +++ b/src/deployments/libraries/OrchestrationReports.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; + +library OrchestrationReports { + struct SpokeDeploymentReport { + string label; + BatchReports.SpokeInstanceBatchReport report; + } + + struct HubDeploymentReport { + string label; + BatchReports.HubBatchReport report; + } + + struct FullDeploymentReport { + BatchReports.AccessBatchReport accessBatchReport; + BatchReports.ConfiguratorBatchReport configuratorBatchReport; + SpokeDeploymentReport[] spokeInstanceBatchReports; + HubDeploymentReport[] hubBatchReports; + BatchReports.GatewaysBatchReport gatewaysBatchReport; + } +} diff --git a/src/deployments/orchestration/AaveV4DeployBase.sol b/src/deployments/orchestration/AaveV4DeployBase.sol new file mode 100644 index 000000000..2507fdf95 --- /dev/null +++ b/src/deployments/orchestration/AaveV4DeployBase.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; +import {OrchestrationReports} from 'src/deployments/libraries/OrchestrationReports.sol'; + +import {AaveV4AccessBatch} from 'src/deployments/batches/AaveV4AccessBatch.sol'; +import {AaveV4ConfiguratorBatch} from 'src/deployments/batches/AaveV4ConfiguratorBatch.sol'; +import {AaveV4GatewayBatch} from 'src/deployments/batches/AaveV4GatewayBatch.sol'; +import {AaveV4HubBatch} from 'src/deployments/batches/AaveV4HubBatch.sol'; +import {AaveV4SpokeInstanceBatch} from 'src/deployments/batches/AaveV4SpokeInstanceBatch.sol'; + +library AaveV4DeployBase { + function deployAccessBatch( + address admin, + bytes32 salt + ) internal returns (BatchReports.AccessBatchReport memory) { + AaveV4AccessBatch accessBatch = new AaveV4AccessBatch(admin, salt); + return accessBatch.getReport(); + } + + function deployConfiguratorBatch( + address hubConfiguratorOwner, + address spokeConfiguratorOwner, + bytes32 salt + ) internal returns (BatchReports.ConfiguratorBatchReport memory) { + AaveV4ConfiguratorBatch configuratorBatch = new AaveV4ConfiguratorBatch( + hubConfiguratorOwner, + spokeConfiguratorOwner, + salt + ); + return configuratorBatch.getReport(); + } + + function deployHubBatch( + address treasurySpokeOwner, + address accessManager, + bytes32 salt + ) internal returns (BatchReports.HubBatchReport memory) { + AaveV4HubBatch hubBatch = new AaveV4HubBatch(treasurySpokeOwner, accessManager, salt); + return hubBatch.getReport(); + } + + function deploySpokeInstanceBatch( + address spokeProxyAdminOwner, + address accessManager, + uint8 oracleDecimals, + string memory oracleSuffix, + string memory label, + bytes32 salt + ) internal returns (BatchReports.SpokeInstanceBatchReport memory) { + AaveV4SpokeInstanceBatch spokeInstanceBatch = new AaveV4SpokeInstanceBatch({ + spokeProxyAdminOwner_: spokeProxyAdminOwner, + accessManager_: accessManager, + oracleDecimals_: oracleDecimals, + oracleDescription_: string.concat(label, oracleSuffix), + salt_: salt + }); + return spokeInstanceBatch.getReport(); + } + + function deployGatewaysBatch( + address owner, + address nativeWrapper, + bytes32 salt + ) internal returns (BatchReports.GatewaysBatchReport memory) { + AaveV4GatewayBatch gatewayBatch = new AaveV4GatewayBatch({ + owner_: owner, + nativeWrapper_: nativeWrapper, + salt_: salt + }); + return gatewayBatch.getReport(); + } +} diff --git a/src/deployments/orchestration/AaveV4DeployOrchestration.sol b/src/deployments/orchestration/AaveV4DeployOrchestration.sol new file mode 100644 index 000000000..df9a07d73 --- /dev/null +++ b/src/deployments/orchestration/AaveV4DeployOrchestration.sol @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; +import {OrchestrationReports} from 'src/deployments/libraries/OrchestrationReports.sol'; + +import {AaveV4DeployBase} from 'src/deployments/orchestration/AaveV4DeployBase.sol'; + +import {AaveV4DeployBase} from 'src/deployments/orchestration/AaveV4DeployBase.sol'; +import {AaveV4AccessBatch} from 'src/deployments/batches/AaveV4AccessBatch.sol'; +import {AaveV4ConfiguratorBatch} from 'src/deployments/batches/AaveV4ConfiguratorBatch.sol'; +import {AaveV4SpokeInstanceBatch} from 'src/deployments/batches/AaveV4SpokeInstanceBatch.sol'; +import {AaveV4GatewayBatch} from 'src/deployments/batches/AaveV4GatewayBatch.sol'; +import {AaveV4HubBatch} from 'src/deployments/batches/AaveV4HubBatch.sol'; + +import { + AaveV4AccessManagerRolesProcedure +} from 'src/deployments/procedures/roles/AaveV4AccessManagerRolesProcedure.sol'; +import { + AaveV4HubRolesProcedure +} from 'src/deployments/procedures/roles/AaveV4HubRolesProcedure.sol'; +import { + AaveV4SpokeRolesProcedure +} from 'src/deployments/procedures/roles/AaveV4SpokeRolesProcedure.sol'; + +import {InputUtils} from 'src/deployments/utils/InputUtils.sol'; +import {Logger} from 'src/deployments/utils/Logger.sol'; + +library AaveV4DeployOrchestration { + uint8 private constant ORACLE_DECIMALS = 8; + string private constant ORACLE_SUFFIX = ' (USD)'; + + function deployAaveV4( + Logger logger, + address deployer, + InputUtils.FullDeployInputs memory deployInputs + ) internal returns (OrchestrationReports.FullDeploymentReport memory report) { + bytes32 rootSalt = keccak256(abi.encode(deployInputs.salt)); + + // Deploy Access Batch + // initialize with deployer as access manager admin + address initialAdmin = deployer; + report.accessBatchReport = _deployAccessBatch({ + logger: logger, + accessManagerAdmin: initialAdmin, + salt: rootSalt + }); + + // Deploy Configurator Batch + report.configuratorBatchReport = _deployConfiguratorBatch({ + logger: logger, + hubConfiguratorOwner: deployInputs.hubConfiguratorOwner, + spokeConfiguratorOwner: deployInputs.spokeConfiguratorOwner, + salt: keccak256(abi.encode(rootSalt, 'config')) + }); + + // Deploy Hub Batches + report.hubBatchReports = _deployHubs({ + logger: logger, + treasurySpokeOwner: deployInputs.treasurySpokeOwner, + accessManager: report.accessBatchReport.accessManager, + hubLabels: deployInputs.hubLabels, + rootSalt: rootSalt + }); + + // Deploy Spoke Instance Batches + report.spokeInstanceBatchReports = _deploySpokes({ + logger: logger, + spokeProxyAdminOwner: deployInputs.spokeProxyAdminOwner, + accessManager: report.accessBatchReport.accessManager, + spokeLabels: deployInputs.spokeLabels, + rootSalt: rootSalt + }); + + // Deploy Gateways Batch if native wrapper is not zero address + if (deployInputs.nativeWrapper != address(0)) { + report.gatewaysBatchReport = _deployGatewayBatch({ + logger: logger, + gatewayOwner: deployInputs.gatewayOwner, + nativeWrapper: deployInputs.nativeWrapper, + salt: keccak256(abi.encode(rootSalt, 'gateways')) + }); + } + + // Set Roles if needed + if (deployInputs.grantRoles) { + if (deployInputs.hubLabels.length > 0) { + _grantHubRoles({logger: logger, report: report, hubAdmin: deployInputs.hubAdmin}); + } + if (deployInputs.spokeLabels.length > 0) { + _grantSpokeRoles({logger: logger, report: report, spokeAdmin: deployInputs.spokeAdmin}); + } + + if (deployInputs.accessManagerAdmin != initialAdmin) { + logger.log('...Granting AccessManager Root Admin role...'); + AaveV4AccessManagerRolesProcedure.replaceDefaultAdminRole({ + accessManager: report.accessBatchReport.accessManager, + adminToAdd: deployInputs.accessManagerAdmin, + adminToRemove: initialAdmin + }); + } + } + + return + _generateFullReport( + report.accessBatchReport, + report.configuratorBatchReport, + report.hubBatchReports, + report.spokeInstanceBatchReports, + report.gatewaysBatchReport + ); + } + + function _grantHubRoles( + Logger logger, + OrchestrationReports.FullDeploymentReport memory report, + address hubAdmin + ) internal { + logger.log('...Granting Hub Admin role...'); + AaveV4HubRolesProcedure.grantHubAdminRole({ + accessManager: report.accessBatchReport.accessManager, + admin: hubAdmin + }); + + logger.log('...Granting Hub Configurator roles...'); + AaveV4HubRolesProcedure.grantHubConfiguratorRole({ + accessManager: report.accessBatchReport.accessManager, + admin: report.configuratorBatchReport.hubConfigurator + }); + } + + function _grantSpokeRoles( + Logger logger, + OrchestrationReports.FullDeploymentReport memory report, + address spokeAdmin + ) internal { + logger.log('...Granting Spoke Admin role...'); + AaveV4SpokeRolesProcedure.grantSpokeAdminRole({ + accessManager: report.accessBatchReport.accessManager, + admin: spokeAdmin + }); + + logger.log('...Granting Spoke Configurator roles...'); + AaveV4SpokeRolesProcedure.grantSpokeConfiguratorRole({ + accessManager: report.accessBatchReport.accessManager, + admin: report.configuratorBatchReport.spokeConfigurator + }); + } + + function _deployAccessBatch( + Logger logger, + address accessManagerAdmin, + bytes32 salt + ) internal returns (BatchReports.AccessBatchReport memory report) { + logger.log('...Deploying AccessBatch...'); + + report = AaveV4DeployBase.deployAccessBatch({admin: accessManagerAdmin, salt: salt}); + + logger.log('AccessManager', report.accessManager); + logger.log(''); + return report; + } + + function _deployConfiguratorBatch( + Logger logger, + address hubConfiguratorOwner, + address spokeConfiguratorOwner, + bytes32 salt + ) internal returns (BatchReports.ConfiguratorBatchReport memory report) { + logger.log('...Deploying ConfiguratorBatch...'); + + report = AaveV4DeployBase.deployConfiguratorBatch({ + hubConfiguratorOwner: hubConfiguratorOwner, + spokeConfiguratorOwner: spokeConfiguratorOwner, + salt: salt + }); + + logger.log('HubConfigurator', report.hubConfigurator); + logger.log('SpokeConfigurator', report.spokeConfigurator); + logger.log(''); + return report; + } + + function _deployHubs( + Logger logger, + address treasurySpokeOwner, + address accessManager, + string[] memory hubLabels, + bytes32 rootSalt + ) internal returns (OrchestrationReports.HubDeploymentReport[] memory hubBatchReports) { + uint256 hubCount = hubLabels.length; + hubBatchReports = new OrchestrationReports.HubDeploymentReport[](hubCount); + for (uint256 i; i < hubCount; ++i) { + hubBatchReports[i] = _deployHub({ + logger: logger, + treasurySpokeOwner: treasurySpokeOwner, + accessManager: accessManager, + label: hubLabels[i], + salt: keccak256(abi.encode(rootSalt, 'hub', hubLabels[i])) + }); + } + logger.log(''); + return hubBatchReports; + } + + function _deployHub( + Logger logger, + address treasurySpokeOwner, + address accessManager, + string memory label, + bytes32 salt + ) internal returns (OrchestrationReports.HubDeploymentReport memory) { + OrchestrationReports.HubDeploymentReport memory hubReport; + hubReport.label = label; + hubReport.report = _deployHubBatch({ + logger: logger, + treasurySpokeOwner: treasurySpokeOwner, + accessManager: accessManager, + salt: salt + }); + + logger.log(label); + logger.log(' Hub', hubReport.report.hub); + logger.log(' InterestRateStrategy', hubReport.report.irStrategy); + logger.log(' TreasurySpoke', hubReport.report.treasurySpoke); + + logger.log('...Setting Hub roles...'); + AaveV4HubRolesProcedure.setupHubRoles(accessManager, hubReport.report.hub); + + return hubReport; + } + + function _deploySpokes( + Logger logger, + address spokeProxyAdminOwner, + address accessManager, + string[] memory spokeLabels, + bytes32 rootSalt + ) internal returns (OrchestrationReports.SpokeDeploymentReport[] memory spokeBatchReports) { + uint256 spokeCount = spokeLabels.length; + spokeBatchReports = new OrchestrationReports.SpokeDeploymentReport[](spokeCount); + for (uint256 i; i < spokeCount; ++i) { + spokeBatchReports[i] = _deploySpoke({ + logger: logger, + spokeProxyAdminOwner: spokeProxyAdminOwner, + accessManager: accessManager, + label: spokeLabels[i], + salt: keccak256(abi.encode(rootSalt, 'spoke', spokeLabels[i])) + }); + } + logger.log(''); + return spokeBatchReports; + } + + function _deploySpoke( + Logger logger, + address spokeProxyAdminOwner, + address accessManager, + string memory label, + bytes32 salt + ) internal returns (OrchestrationReports.SpokeDeploymentReport memory) { + OrchestrationReports.SpokeDeploymentReport memory spokeReport; + + spokeReport.label = label; + spokeReport.report = _deploySpokeInstanceBatch({ + logger: logger, + spokeProxyAdminOwner: spokeProxyAdminOwner, + accessManager: accessManager, + label: label, + salt: salt + }); + + logger.log(label); + logger.log(' SpokeInstance Proxy', spokeReport.report.spokeProxy); + logger.log(' SpokeInstance Implementation', spokeReport.report.spokeImplementation); + logger.log(' AaveOracle', spokeReport.report.aaveOracle); + + logger.log('...Setting Spoke roles...'); + AaveV4SpokeRolesProcedure.setupSpokeRoles({ + accessManager: accessManager, + spoke: spokeReport.report.spokeProxy + }); + + return spokeReport; + } + + function _deploySpokeInstanceBatch( + Logger logger, + address spokeProxyAdminOwner, + address accessManager, + string memory label, + bytes32 salt + ) internal returns (BatchReports.SpokeInstanceBatchReport memory report) { + logger.log('...Deploying AaveV4SpokeInstanceBatch...'); + report = AaveV4DeployBase.deploySpokeInstanceBatch({ + spokeProxyAdminOwner: spokeProxyAdminOwner, + accessManager: accessManager, + oracleDecimals: ORACLE_DECIMALS, + oracleSuffix: ORACLE_SUFFIX, + label: label, + salt: salt + }); + return report; + } + + function _deployHubBatch( + Logger logger, + address treasurySpokeOwner, + address accessManager, + bytes32 salt + ) internal returns (BatchReports.HubBatchReport memory report) { + logger.log('...Deploying HubBatch...'); + report = AaveV4DeployBase.deployHubBatch({ + treasurySpokeOwner: treasurySpokeOwner, + accessManager: accessManager, + salt: salt + }); + return report; + } + + function _deployGatewayBatch( + Logger logger, + address gatewayOwner, + address nativeWrapper, + bytes32 salt + ) internal returns (BatchReports.GatewaysBatchReport memory report) { + logger.log('...Deploying GatewayBatch...'); + report = AaveV4DeployBase.deployGatewaysBatch({ + owner: gatewayOwner, + nativeWrapper: nativeWrapper, + salt: salt + }); + logger.log('NativeTokenGateway', report.nativeGateway); + logger.log('SignatureGateway', report.signatureGateway); + return report; + } + + function _generateFullReport( + BatchReports.AccessBatchReport memory accessBatchReport, + BatchReports.ConfiguratorBatchReport memory configuratorBatchReport, + OrchestrationReports.HubDeploymentReport[] memory hubBatchReports, + OrchestrationReports.SpokeDeploymentReport[] memory spokeBatchReports, + BatchReports.GatewaysBatchReport memory gatewaysBatchReport + ) internal pure returns (OrchestrationReports.FullDeploymentReport memory report) { + report.accessBatchReport = accessBatchReport; + report.configuratorBatchReport = configuratorBatchReport; + report.hubBatchReports = hubBatchReports; + report.spokeInstanceBatchReports = spokeBatchReports; + report.gatewaysBatchReport = gatewaysBatchReport; + return report; + } +} diff --git a/src/deployments/procedures/AaveV4DeployProcedureBase.sol b/src/deployments/procedures/AaveV4DeployProcedureBase.sol new file mode 100644 index 000000000..80fb799ef --- /dev/null +++ b/src/deployments/procedures/AaveV4DeployProcedureBase.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +contract AaveV4DeployProcedureBase { + bytes32 public constant SALT = keccak256('AAVE_V4_v1'); +} diff --git a/src/deployments/procedures/config/AaveV4HubConfigProcedures.sol b/src/deployments/procedures/config/AaveV4HubConfigProcedures.sol new file mode 100644 index 000000000..2b4038ed0 --- /dev/null +++ b/src/deployments/procedures/config/AaveV4HubConfigProcedures.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {ConfigData} from 'src/deployments/libraries/ConfigData.sol'; + +import {IHub} from 'src/hub/interfaces/IHub.sol'; +import {IHubConfigurator} from 'src/hub/interfaces/IHubConfigurator.sol'; + +library AaveV4HubConfigProcedures { + function addAsset(ConfigData.AddAssetParams memory params) internal returns (uint256) { + uint256 assetId = IHub(params.hub).addAsset( + params.underlying, + params.decimals, + params.feeReceiver, + params.irStrategy, + params.irData + ); + if (params.liquidityFee > 0 || params.reinvestmentController != address(0)) { + IHub(params.hub).updateAssetConfig( + assetId, + IHub.AssetConfig({ + liquidityFee: params.liquidityFee, + feeReceiver: params.feeReceiver, + irStrategy: params.irStrategy, + reinvestmentController: params.reinvestmentController + }), + bytes('') + ); + } + return assetId; + } + + function addAssetViaConfigurator( + address configurator, + ConfigData.AddAssetParams memory params + ) internal returns (uint256) { + return + IHubConfigurator(configurator).addAsset( + params.hub, + params.underlying, + params.decimals, + params.feeReceiver, + params.liquidityFee, + params.irStrategy, + params.irData + ); + } + + function updateAssetConfig(ConfigData.UpdateAssetConfigParams memory params) internal { + IHub(params.hub).updateAssetConfig(params.assetId, params.config, params.irData); + } + + function addSpoke(ConfigData.AddSpokeParams memory params) internal { + IHub(params.hub).addSpoke(params.assetId, params.spoke, params.config); + } + + function addSpokeViaConfigurator( + address configurator, + ConfigData.AddSpokeParams memory params + ) internal { + IHubConfigurator(configurator).addSpoke( + params.hub, + params.spoke, + params.assetId, + params.config + ); + } + + function addSpokeToAssetsViaConfigurator( + address configurator, + ConfigData.AddSpokeToAssetsParams memory params + ) internal { + IHubConfigurator(configurator).addSpokeToAssets( + params.hub, + params.spoke, + params.assetIds, + params.configs + ); + } +} diff --git a/src/deployments/procedures/config/AaveV4SpokeConfigProcedures.sol b/src/deployments/procedures/config/AaveV4SpokeConfigProcedures.sol new file mode 100644 index 000000000..bb9f3502c --- /dev/null +++ b/src/deployments/procedures/config/AaveV4SpokeConfigProcedures.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {ConfigData} from 'src/deployments/libraries/ConfigData.sol'; + +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {ISpokeConfigurator} from 'src/spoke/interfaces/ISpokeConfigurator.sol'; + +library AaveV4SpokeConfigProcedures { + function updateLiquidationConfig( + ConfigData.UpdateLiquidationConfigParams memory params + ) internal { + ISpoke(params.spoke).updateLiquidationConfig(params.config); + } + + function updateLiquidationConfigViaConfigurator( + address configurator, + ConfigData.UpdateLiquidationConfigParams memory params + ) internal { + ISpokeConfigurator(configurator).updateLiquidationConfig(params.spoke, params.config); + } + + function addReserve(ConfigData.AddReserveParams memory params) internal returns (uint256) { + return + ISpoke(params.spoke).addReserve( + params.hub, + params.assetId, + params.priceSource, + params.config, + params.dynamicConfig + ); + } + + function addReserveViaConfigurator( + address configurator, + ConfigData.AddReserveParams memory params + ) internal returns (uint256) { + return + ISpokeConfigurator(configurator).addReserve( + params.spoke, + params.hub, + params.assetId, + params.priceSource, + params.config, + params.dynamicConfig + ); + } +} diff --git a/src/deployments/procedures/deploy/AaveV4AccessManagerEnumerableDeployProcedure.sol b/src/deployments/procedures/deploy/AaveV4AccessManagerEnumerableDeployProcedure.sol new file mode 100644 index 000000000..b48039c40 --- /dev/null +++ b/src/deployments/procedures/deploy/AaveV4AccessManagerEnumerableDeployProcedure.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {AaveV4DeployProcedureBase} from 'src/deployments/procedures/AaveV4DeployProcedureBase.sol'; +import {AccessManagerEnumerable} from 'src/access/AccessManagerEnumerable.sol'; +import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; + +contract AaveV4AccessManagerEnumerableDeployProcedure is AaveV4DeployProcedureBase { + function _deployAccessManagerEnumerable(address admin, bytes32 salt) internal returns (address) { + require(admin != address(0), 'invalid admin'); + return + Create2Utils.create2Deploy( + salt, + abi.encodePacked(type(AccessManagerEnumerable).creationCode, abi.encode(admin)) + ); + } +} diff --git a/src/deployments/procedures/deploy/hub/AaveV4HubConfiguratorDeployProcedure.sol b/src/deployments/procedures/deploy/hub/AaveV4HubConfiguratorDeployProcedure.sol new file mode 100644 index 000000000..427f31a71 --- /dev/null +++ b/src/deployments/procedures/deploy/hub/AaveV4HubConfiguratorDeployProcedure.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; +import {AaveV4DeployProcedureBase} from 'src/deployments/procedures/AaveV4DeployProcedureBase.sol'; +import {HubConfigurator} from 'src/hub/HubConfigurator.sol'; + +contract AaveV4HubConfiguratorDeployProcedure is AaveV4DeployProcedureBase { + function _deployHubConfigurator(address owner, bytes32 salt) internal returns (address) { + require(owner != address(0), 'invalid owner'); + return + Create2Utils.create2Deploy( + salt, + abi.encodePacked(type(HubConfigurator).creationCode, abi.encode(owner)) + ); + } +} diff --git a/src/deployments/procedures/deploy/hub/AaveV4HubDeployProcedure.sol b/src/deployments/procedures/deploy/hub/AaveV4HubDeployProcedure.sol new file mode 100644 index 000000000..1bc799bd2 --- /dev/null +++ b/src/deployments/procedures/deploy/hub/AaveV4HubDeployProcedure.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {AaveV4DeployProcedureBase} from 'src/deployments/procedures/AaveV4DeployProcedureBase.sol'; +import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; +import {Hub} from 'src/hub/Hub.sol'; + +contract AaveV4HubDeployProcedure is AaveV4DeployProcedureBase { + function _deployHub(address accessManager, bytes32 salt) internal returns (address) { + require(accessManager != address(0), 'invalid access manager'); + return + Create2Utils.create2Deploy( + salt, + abi.encodePacked(type(Hub).creationCode, abi.encode(accessManager)) + ); + } +} diff --git a/src/deployments/procedures/deploy/hub/AaveV4InterestRateStrategyDeployProcedure.sol b/src/deployments/procedures/deploy/hub/AaveV4InterestRateStrategyDeployProcedure.sol new file mode 100644 index 000000000..4db31c912 --- /dev/null +++ b/src/deployments/procedures/deploy/hub/AaveV4InterestRateStrategyDeployProcedure.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {AaveV4DeployProcedureBase} from 'src/deployments/procedures/AaveV4DeployProcedureBase.sol'; +import {AssetInterestRateStrategy} from 'src/hub/AssetInterestRateStrategy.sol'; +import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; + +contract AaveV4InterestRateStrategyDeployProcedure is AaveV4DeployProcedureBase { + function _deployInterestRateStrategy(address hub, bytes32 salt) internal returns (address) { + require(hub != address(0), 'invalid hub'); + return + Create2Utils.create2Deploy( + salt, + abi.encodePacked(type(AssetInterestRateStrategy).creationCode, abi.encode(hub)) + ); + } +} diff --git a/src/deployments/procedures/deploy/position-manager/AaveV4NativeTokenGatewayDeployProcedure.sol b/src/deployments/procedures/deploy/position-manager/AaveV4NativeTokenGatewayDeployProcedure.sol new file mode 100644 index 000000000..9c15cc516 --- /dev/null +++ b/src/deployments/procedures/deploy/position-manager/AaveV4NativeTokenGatewayDeployProcedure.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {AaveV4DeployProcedureBase} from 'src/deployments/procedures/AaveV4DeployProcedureBase.sol'; +import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; +import {NativeTokenGateway} from 'src/position-manager/NativeTokenGateway.sol'; + +contract AaveV4NativeTokenGatewayDeployProcedure is AaveV4DeployProcedureBase { + function _deployNativeTokenGateway( + address nativeWrapper, + address owner, + bytes32 salt + ) internal returns (address) { + require(nativeWrapper != address(0), 'invalid native wrapper'); + require(owner != address(0), 'invalid owner'); + return + Create2Utils.create2Deploy( + salt, + abi.encodePacked(type(NativeTokenGateway).creationCode, abi.encode(nativeWrapper, owner)) + ); + } +} diff --git a/src/deployments/procedures/deploy/position-manager/AaveV4SignatureGatewayDeployProcedure.sol b/src/deployments/procedures/deploy/position-manager/AaveV4SignatureGatewayDeployProcedure.sol new file mode 100644 index 000000000..0f72d1d92 --- /dev/null +++ b/src/deployments/procedures/deploy/position-manager/AaveV4SignatureGatewayDeployProcedure.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {AaveV4DeployProcedureBase} from 'src/deployments/procedures/AaveV4DeployProcedureBase.sol'; +import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; +import {SignatureGateway} from 'src/position-manager/SignatureGateway.sol'; + +contract AaveV4SignatureGatewayDeployProcedure is AaveV4DeployProcedureBase { + function _deploySignatureGateway(address owner, bytes32 salt) internal returns (address) { + require(owner != address(0), 'invalid owner'); + return + Create2Utils.create2Deploy( + salt, + abi.encodePacked(type(SignatureGateway).creationCode, abi.encode(owner)) + ); + } +} diff --git a/src/deployments/procedures/deploy/spoke/AaveV4AaveOracleDeployProcedure.sol b/src/deployments/procedures/deploy/spoke/AaveV4AaveOracleDeployProcedure.sol new file mode 100644 index 000000000..323735788 --- /dev/null +++ b/src/deployments/procedures/deploy/spoke/AaveV4AaveOracleDeployProcedure.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {AaveOracle} from 'src/spoke/AaveOracle.sol'; +import {AaveV4DeployProcedureBase} from 'src/deployments/procedures/AaveV4DeployProcedureBase.sol'; +contract AaveV4AaveOracleDeployProcedure is AaveV4DeployProcedureBase { + function _deployAaveOracle( + address spoke_, + uint8 decimals_, + string memory description_, + bytes32 salt_ + ) internal returns (address) { + require(spoke_ != address(0), 'invalid spoke'); + require(decimals_ > 0, 'invalid oracle decimals'); + require(bytes(description_).length > 0, 'invalid oracle description'); + // AaveOracle must be deployed via create to compute the predicted address via without inputs + return + address(new AaveOracle({spoke_: spoke_, decimals_: decimals_, description_: description_})); + } +} diff --git a/src/deployments/procedures/deploy/spoke/AaveV4SpokeConfiguratorDeployProcedure.sol b/src/deployments/procedures/deploy/spoke/AaveV4SpokeConfiguratorDeployProcedure.sol new file mode 100644 index 000000000..68f0034d0 --- /dev/null +++ b/src/deployments/procedures/deploy/spoke/AaveV4SpokeConfiguratorDeployProcedure.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; +import {AaveV4DeployProcedureBase} from 'src/deployments/procedures/AaveV4DeployProcedureBase.sol'; +import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; +import {SpokeConfigurator} from 'src/spoke/SpokeConfigurator.sol'; + +contract AaveV4SpokeConfiguratorDeployProcedure is AaveV4DeployProcedureBase { + function _deploySpokeConfigurator(address owner, bytes32 salt) internal returns (address) { + require(owner != address(0), 'invalid owner'); + return + Create2Utils.create2Deploy( + salt, + abi.encodePacked(type(SpokeConfigurator).creationCode, abi.encode(owner)) + ); + } +} diff --git a/src/deployments/procedures/deploy/spoke/AaveV4SpokeDeployProcedure.sol b/src/deployments/procedures/deploy/spoke/AaveV4SpokeDeployProcedure.sol new file mode 100644 index 000000000..ef95c9197 --- /dev/null +++ b/src/deployments/procedures/deploy/spoke/AaveV4SpokeDeployProcedure.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {AaveV4DeployProcedureBase} from 'src/deployments/procedures/AaveV4DeployProcedureBase.sol'; +import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; +import {SpokeInstance} from 'src/spoke/instances/SpokeInstance.sol'; +import { + TransparentUpgradeableProxy +} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; + +contract AaveV4SpokeDeployProcedure is AaveV4DeployProcedureBase { + function _deployUpgradableSpokeInstance( + address spokeProxyAdminOwner, + address accessManager, + address oracle, + bytes32 salt + ) internal returns (address spokeProxy, address spokeImplementation) { + require(spokeProxyAdminOwner != address(0), 'invalid spoke proxy admin owner'); + require(accessManager != address(0), 'invalid access manager'); + require(oracle != address(0), 'invalid oracle'); + spokeImplementation = Create2Utils.create2Deploy( + salt, + abi.encodePacked(type(SpokeInstance).creationCode, abi.encode(oracle)) + ); + spokeProxy = Create2Utils.proxify( + salt, + spokeImplementation, + spokeProxyAdminOwner, + abi.encodeCall(SpokeInstance.initialize, (accessManager)) + ); + return (spokeProxy, spokeImplementation); + } + + function _computeSpokeInstanceAddress( + bytes32 salt, + address oracle, + address spokeProxyAdminOwner, + address accessManager + ) internal pure returns (address) { + address spokeImplementation = Create2Utils.computeCreate2Address( + salt, + abi.encodePacked(type(SpokeInstance).creationCode, abi.encode(oracle)) + ); + bytes memory initCode = abi.encodePacked( + type(TransparentUpgradeableProxy).creationCode, + abi.encode( + spokeImplementation, + spokeProxyAdminOwner, + abi.encodeCall(SpokeInstance.initialize, (accessManager)) + ) + ); + return Create2Utils.computeCreate2Address(salt, keccak256(initCode)); + } +} diff --git a/src/deployments/procedures/deploy/spoke/AaveV4TreasurySpokeDeployProcedure.sol b/src/deployments/procedures/deploy/spoke/AaveV4TreasurySpokeDeployProcedure.sol new file mode 100644 index 000000000..c2b7eccaf --- /dev/null +++ b/src/deployments/procedures/deploy/spoke/AaveV4TreasurySpokeDeployProcedure.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {AaveV4DeployProcedureBase} from 'src/deployments/procedures/AaveV4DeployProcedureBase.sol'; +import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; +import {TreasurySpoke} from 'src/spoke/TreasurySpoke.sol'; + +contract AaveV4TreasurySpokeDeployProcedure is AaveV4DeployProcedureBase { + function _deployTreasurySpoke( + address owner, + address hub, + bytes32 salt + ) internal returns (address) { + require(owner != address(0), 'invalid owner'); + require(hub != address(0), 'invalid hub'); + return + Create2Utils.create2Deploy( + salt, + abi.encodePacked(type(TreasurySpoke).creationCode, abi.encode(owner, hub)) + ); + } +} diff --git a/src/deployments/procedures/roles/AaveV4AccessManagerRolesProcedure.sol b/src/deployments/procedures/roles/AaveV4AccessManagerRolesProcedure.sol new file mode 100644 index 000000000..8537647fd --- /dev/null +++ b/src/deployments/procedures/roles/AaveV4AccessManagerRolesProcedure.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {AaveV4DeployProcedureBase} from 'src/deployments/procedures/AaveV4DeployProcedureBase.sol'; +import {IAccessManager} from 'src/dependencies/openzeppelin/IAccessManager.sol'; +import {Roles} from 'src/deployments/utils/libraries/Roles.sol'; + +library AaveV4AccessManagerRolesProcedure { + function replaceDefaultAdminRole( + address accessManager, + address adminToAdd, + address adminToRemove + ) internal { + require(accessManager != address(0), 'invalid access manager'); + require(adminToAdd != address(0), 'invalid admin to add'); + require(adminToRemove != address(0), 'invalid admin to remove'); + IAccessManager(accessManager).grantRole({ + roleId: Roles.DEFAULT_ADMIN_ROLE, + account: adminToAdd, + executionDelay: 0 + }); + IAccessManager(accessManager).renounceRole({ + roleId: Roles.DEFAULT_ADMIN_ROLE, + callerConfirmation: adminToRemove + }); + } +} diff --git a/src/deployments/procedures/roles/AaveV4HubRolesProcedure.sol b/src/deployments/procedures/roles/AaveV4HubRolesProcedure.sol new file mode 100644 index 000000000..496726537 --- /dev/null +++ b/src/deployments/procedures/roles/AaveV4HubRolesProcedure.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {AaveV4DeployProcedureBase} from 'src/deployments/procedures/AaveV4DeployProcedureBase.sol'; +import {IAccessManager} from 'src/dependencies/openzeppelin/IAccessManager.sol'; +import {Roles} from 'src/deployments/utils/libraries/Roles.sol'; +import {IHub} from 'src/hub/interfaces/IHub.sol'; + +library AaveV4HubRolesProcedure { + function grantHubAdminRole(address accessManager, address admin) internal { + grantHubFeeMinterRole(accessManager, admin); + grantHubConfiguratorRole(accessManager, admin); + } + + function grantHubFeeMinterRole(address accessManager, address admin) internal { + _validateAccessManagerAndAdmin(accessManager, admin); + IAccessManager(accessManager).grantRole({ + roleId: Roles.HUB_FEE_MINTER_ROLE, + account: admin, + executionDelay: 0 + }); + } + + function grantHubConfiguratorRole(address accessManager, address admin) internal { + _validateAccessManagerAndAdmin(accessManager, admin); + IAccessManager(accessManager).grantRole({ + roleId: Roles.HUB_CONFIGURATOR_ROLE, + account: admin, + executionDelay: 0 + }); + } + + function setupHubRoles(address accessManager, address hub) internal { + setupHubFeeMinterRole(accessManager, hub); + setupHubConfiguratorRole(accessManager, hub); + } + + function setupHubFeeMinterRole(address accessManager, address hub) internal { + _validateAccessManagerAndHub(accessManager, hub); + bytes4[] memory selectors = getHubFeeMinterRoleSelectors(); + IAccessManager(accessManager).setTargetFunctionRole(hub, selectors, Roles.HUB_FEE_MINTER_ROLE); + } + + function setupHubConfiguratorRole(address accessManager, address hub) internal { + _validateAccessManagerAndHub(accessManager, hub); + bytes4[] memory selectors = getHubConfiguratorRoleSelectors(); + IAccessManager(accessManager).setTargetFunctionRole( + hub, + selectors, + Roles.HUB_CONFIGURATOR_ROLE + ); + } + + function getHubFeeMinterRoleSelectors() internal pure returns (bytes4[] memory) { + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = IHub.mintFeeShares.selector; + return selectors; + } + + function getHubConfiguratorRoleSelectors() internal pure returns (bytes4[] memory) { + bytes4[] memory selectors = new bytes4[](5); + selectors[0] = IHub.addAsset.selector; + selectors[1] = IHub.updateAssetConfig.selector; + selectors[2] = IHub.addSpoke.selector; + selectors[3] = IHub.updateSpokeConfig.selector; + selectors[4] = IHub.setInterestRateData.selector; + return selectors; + } + + function _validateAccessManagerAndHub(address accessManager, address hub) private pure { + require(accessManager != address(0), 'invalid access manager'); + require(hub != address(0), 'invalid hub'); + } + + function _validateAccessManagerAndAdmin(address accessManager, address admin) private pure { + require(accessManager != address(0), 'invalid access manager'); + require(admin != address(0), 'invalid admin'); + } +} diff --git a/src/deployments/procedures/roles/AaveV4SpokeRolesProcedure.sol b/src/deployments/procedures/roles/AaveV4SpokeRolesProcedure.sol new file mode 100644 index 000000000..04f38ff0e --- /dev/null +++ b/src/deployments/procedures/roles/AaveV4SpokeRolesProcedure.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {AaveV4DeployProcedureBase} from 'src/deployments/procedures/AaveV4DeployProcedureBase.sol'; +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'; + +library AaveV4SpokeRolesProcedure { + function grantSpokeAdminRole(address accessManager, address admin) internal { + grantSpokePositionUpdaterRole(accessManager, admin); + grantSpokeConfiguratorRole(accessManager, admin); + } + + function grantSpokePositionUpdaterRole(address accessManager, address admin) internal { + _validateAccessManagerAndAdmin(accessManager, admin); + IAccessManager(accessManager).grantRole({ + roleId: Roles.SPOKE_POSITION_UPDATER_ROLE, + account: admin, + executionDelay: 0 + }); + } + + function grantSpokeConfiguratorRole(address accessManager, address admin) internal { + _validateAccessManagerAndAdmin(accessManager, admin); + IAccessManager(accessManager).grantRole({ + roleId: Roles.SPOKE_CONFIGURATOR_ROLE, + account: admin, + executionDelay: 0 + }); + } + + function setupSpokeRoles(address accessManager, address spoke) internal { + setupSpokePositionUpdaterRole(accessManager, spoke); + setupSpokeConfiguratorRole(accessManager, spoke); + } + + function setupSpokePositionUpdaterRole(address accessManager, address spoke) internal { + _validateAccessManagerAndSpoke(accessManager, spoke); + bytes4[] memory selectors = getSpokePositionUpdaterRoleSelectors(); + IAccessManager(accessManager).setTargetFunctionRole( + spoke, + selectors, + Roles.SPOKE_POSITION_UPDATER_ROLE + ); + } + + function setupSpokeConfiguratorRole(address accessManager, address spoke) internal { + _validateAccessManagerAndSpoke(accessManager, spoke); + bytes4[] memory selectors = getSpokeConfiguratorRoleSelectors(); + IAccessManager(accessManager).setTargetFunctionRole( + spoke, + selectors, + Roles.SPOKE_CONFIGURATOR_ROLE + ); + } + + function getSpokePositionUpdaterRoleSelectors() internal pure returns (bytes4[] memory) { + bytes4[] memory selectors = new bytes4[](2); + selectors[0] = ISpoke.updateUserDynamicConfig.selector; + selectors[1] = ISpoke.updateUserRiskPremium.selector; + return selectors; + } + + function getSpokeConfiguratorRoleSelectors() internal pure returns (bytes4[] memory) { + bytes4[] memory selectors = new bytes4[](7); + 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; + return selectors; + } + + function _validateAccessManagerAndSpoke(address accessManager, address spoke) private pure { + require(accessManager != address(0), 'invalid access manager'); + require(spoke != address(0), 'invalid spoke'); + } + + function _validateAccessManagerAndAdmin(address accessManager, address admin) private pure { + require(accessManager != address(0), 'invalid access manager'); + require(admin != address(0), 'invalid admin'); + } +} diff --git a/src/deployments/utils/InputUtils.sol b/src/deployments/utils/InputUtils.sol new file mode 100644 index 000000000..0f4d80c16 --- /dev/null +++ b/src/deployments/utils/InputUtils.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'forge-std/StdJson.sol'; +import 'forge-std/Vm.sol'; +import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; + +contract InputUtils { + using stdJson for string; + + Vm private constant vm = Vm(address(bytes20(uint160(uint256(keccak256('hevm cheat code')))))); + + /// @dev accessManagerAdmin The default admin of the access manager. + /// @dev hubAdmin The admin of the hub. + /// @dev hubConfiguratorOwner The admin of the hub configurator. + /// @dev treasurySpokeOwner The owner of the treasury spoke. + /// @dev spokeAdmin The spoke admin. + /// @dev spokeProxyAdminOwner The owner of the spoke proxyAdmin. + /// @dev spokeConfiguratorOwner The admin of the spoke configurator. + /// @dev gatewayOwner The owner of the native token and signature gateways. + /// @dev nativeWrapper The address of the native wrapper. + /// @dev grantRoles A boolean indicating if roles should be granted. + /// @dev hubLabels An array of hub labels; the number of hub labels defines the number of hubs to deploy. + /// @dev spokeLabels An array of spoke labels; the number of spoke labels defines the number of spokes to deploy. + /// @dev salt The root salt to use for the deployment. + struct FullDeployInputs { + address accessManagerAdmin; + address hubAdmin; + address hubConfiguratorOwner; + address treasurySpokeOwner; + address spokeAdmin; + address spokeProxyAdminOwner; + address spokeConfiguratorOwner; + address gatewayOwner; + address nativeWrapper; + bool grantRoles; + string[] hubLabels; + string[] spokeLabels; + bytes32 salt; + } + + struct SpokeDeployInputs { + address admin; + bool grantRoles; + string spokeLabel; + } + + struct HubDeployInputs { + address admin; + bool grantRoles; + string hubLabel; + } + + function loadFullDeployInputs( + string memory inputPath + ) public view returns (FullDeployInputs memory inputs) { + string memory json = vm.readFile(inputPath); + inputs.accessManagerAdmin = json.readAddress('.accessManagerAdmin'); + inputs.hubAdmin = json.readAddress('.hubAdmin'); + inputs.hubConfiguratorOwner = json.readAddress('.hubConfiguratorOwner'); + inputs.treasurySpokeOwner = json.readAddress('.treasurySpokeOwner'); + inputs.spokeAdmin = json.readAddress('.spokeAdmin'); + inputs.spokeProxyAdminOwner = json.readAddress('.spokeProxyAdminOwner'); + inputs.spokeConfiguratorOwner = json.readAddress('.spokeConfiguratorOwner'); + inputs.gatewayOwner = json.readAddress('.gatewayOwner'); + inputs.nativeWrapper = json.readAddress('.nativeWrapper'); + inputs.grantRoles = json.readBool('.grantRoles'); + inputs.hubLabels = json.readStringArray('.hubLabels'); + inputs.spokeLabels = json.readStringArray('.spokeLabels'); + } + + function loadSpokeDeployInputs( + string memory inputPath + ) public view returns (SpokeDeployInputs memory) { + string memory json = vm.readFile(inputPath); + bytes memory data = vm.parseJson(json); + SpokeDeployInputs memory inputs = abi.decode(data, (SpokeDeployInputs)); + return inputs; + } + + function loadHubDeployInputs( + string memory inputPath + ) public view returns (HubDeployInputs memory) { + string memory json = vm.readFile(inputPath); + bytes memory data = vm.parseJson(json); + HubDeployInputs memory inputs = abi.decode(data, (HubDeployInputs)); + return inputs; + } + + function _etchCreate2Factory() internal virtual { + vm.etch( + Create2Utils.CREATE2_FACTORY, + hex'7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3' + ); + } +} diff --git a/src/deployments/utils/Logger.sol b/src/deployments/utils/Logger.sol new file mode 100644 index 000000000..4ba1ba64f --- /dev/null +++ b/src/deployments/utils/Logger.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'forge-std/StdJson.sol'; +import 'forge-std/Vm.sol'; +import {console2 as console} from 'forge-std/console2.sol'; + +contract Logger { + using stdJson for string; + + Vm private constant vm = Vm(address(bytes20(uint160(uint256(keccak256('hevm cheat code')))))); + struct AddressEntry { + string label; + address value; + } + + struct ValueEntry { + string label; + uint256 value; + } + + string internal _outputPath; + string internal _root; + string internal _json; + + constructor(string memory outputPath_) { + _root = 'root'; + _outputPath = outputPath_; + _json = _root; + } + + function write(string memory label, address value) public { + _write(label, value); + } + + function write(string memory label, uint256 value) public { + _write(label, value); + } + + function write(string memory value) public { + _write(value); + } + + function writeGroup(string memory groupLabel, AddressEntry[] memory entries) public { + _writeGroup(groupLabel, entries); + } + + function writeGroup(string memory groupLabel, ValueEntry[] memory entries) public { + _writeGroup(groupLabel, entries); + } + + function save(string memory fileName, bool withTimestamp) public { + console.log(); + console.log('Saving log to %s', _outputPath); + string memory appendedMetadata = withTimestamp ? string.concat(_getTimestamp(), '-') : ''; + vm.writeJson( + _json, + string.concat(_outputPath, appendedMetadata, vm.toString(block.chainid), '-', fileName) + ); + } + + function log(string memory label, address value) public pure { + _log(label, value); + } + + function log(string memory label, uint256 value) public pure { + _log(label, value); + } + + function log(string memory value) public pure { + _log(value); + } + + function _write(string memory label, address value) internal { + _json = vm.serializeAddress(_root, label, value); + } + + function _write(string memory label, uint256 value) internal { + _json = vm.serializeUint(_root, label, value); + } + + function _write(string memory value) internal { + _json = vm.serializeString(_root, 'message', value); + } + + function _writeGroup(string memory groupLabel, AddressEntry[] memory entries) internal { + string memory group; + for (uint256 i = 0; i < entries.length; i++) { + group = vm.serializeAddress(groupLabel, entries[i].label, entries[i].value); + } + _json = vm.serializeString(_root, groupLabel, group); + } + + function _writeGroup(string memory groupLabel, ValueEntry[] memory entries) internal { + string memory group; + for (uint256 i = 0; i < entries.length; i++) { + group = vm.serializeString(groupLabel, entries[i].label, vm.toString(entries[i].value)); + } + _json = vm.serializeString(_root, groupLabel, group); + } + + function _getTimestamp() internal returns (string memory result) { + string[] memory command = new string[](3); + + command[0] = 'bash'; + command[1] = '-c'; + command[2] = 'response="$(date +%s)"; cast abi-encode "response(string)" $response;'; + bytes memory timestamp = vm.ffi(command); + (result) = abi.decode(timestamp, (string)); + + return result; + } + + function _log(string memory label, address value) internal pure { + console.log('%s: %s', label, value); + } + + function _log(string memory label, uint256 value) internal pure { + console.log('%s: %s', label, value); + } + + function _log(string memory value) internal pure { + console.log(value); + } +} diff --git a/src/deployments/utils/MetadataLogger.sol b/src/deployments/utils/MetadataLogger.sol new file mode 100644 index 000000000..126b743d4 --- /dev/null +++ b/src/deployments/utils/MetadataLogger.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {OrchestrationReports} from 'src/deployments/libraries/OrchestrationReports.sol'; +import {Logger} from 'src/deployments/utils/Logger.sol'; + +contract MetadataLogger is Logger { + constructor(string memory outputPath_) Logger(outputPath_) {} + + function writeJsonReportMarket(OrchestrationReports.FullDeploymentReport memory report) public { + _write('AccessManager', report.accessBatchReport.accessManager); + _write('HubConfigurator', report.configuratorBatchReport.hubConfigurator); + _write('SpokeConfigurator', report.configuratorBatchReport.spokeConfigurator); + + for (uint256 i; i < report.hubBatchReports.length; i++) { + Logger.AddressEntry[] memory hubEntries = new Logger.AddressEntry[](3); + hubEntries[0] = Logger.AddressEntry({ + label: 'Hub', + value: report.hubBatchReports[i].report.hub + }); + hubEntries[1] = Logger.AddressEntry({ + label: 'InterestRateStrategy', + value: report.hubBatchReports[i].report.hub + }); + hubEntries[2] = Logger.AddressEntry({ + label: 'TreasurySpoke', + value: report.hubBatchReports[i].report.treasurySpoke + }); + _writeGroup(report.hubBatchReports[i].label, hubEntries); + } + + for (uint256 i; i < report.spokeInstanceBatchReports.length; i++) { + Logger.AddressEntry[] memory spokeInstanceEntries = new Logger.AddressEntry[](3); + spokeInstanceEntries[0] = Logger.AddressEntry({ + label: 'SpokeInstance Proxy', + value: report.spokeInstanceBatchReports[i].report.spokeProxy + }); + spokeInstanceEntries[1] = Logger.AddressEntry({ + label: 'SpokeInstance Implementation', + value: report.spokeInstanceBatchReports[i].report.spokeImplementation + }); + spokeInstanceEntries[2] = Logger.AddressEntry({ + label: 'AaveOracle', + value: report.spokeInstanceBatchReports[i].report.aaveOracle + }); + _writeGroup(report.spokeInstanceBatchReports[i].label, spokeInstanceEntries); + } + + _write('SignatureGateway', report.gatewaysBatchReport.signatureGateway); + _write('NativeTokenGateway', report.gatewaysBatchReport.nativeGateway); + } +} diff --git a/src/deployments/utils/libraries/Create2Utils.sol b/src/deployments/utils/libraries/Create2Utils.sol new file mode 100644 index 000000000..fb979c8cd --- /dev/null +++ b/src/deployments/utils/libraries/Create2Utils.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.20; + +import { + TransparentUpgradeableProxy +} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; +import {SpokeInstance} from 'src/spoke/instances/SpokeInstance.sol'; + +library Create2Utils { + // https://github.com/safe-global/safe-singleton-factory + address public constant CREATE2_FACTORY = 0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7; + + error missingCreate2Factory(); + error create2AddressDerivationFailure(); + error nonceNotSupported(); + error failedCreate2FactoryCall(); + error contractAlreadyDeployed(); + + function create2Deploy(bytes32 salt, bytes memory bytecode) internal returns (address) { + require(isContractDeployed(CREATE2_FACTORY), missingCreate2Factory()); + address computed = computeCreate2Address(salt, bytecode); + require(!isContractDeployed(computed), contractAlreadyDeployed()); + bytes memory creationBytecode = abi.encodePacked(salt, bytecode); + (bool success, bytes memory returnData) = CREATE2_FACTORY.call(creationBytecode); + require(success, failedCreate2FactoryCall()); + address deployedAt = address(uint160(bytes20(returnData))); + require(deployedAt == computed, create2AddressDerivationFailure()); + return deployedAt; + } + + function proxify( + bytes32 salt, + address logic, + address initialOwner, + bytes memory data + ) internal returns (address) { + return + create2Deploy( + salt, + abi.encodePacked( + type(TransparentUpgradeableProxy).creationCode, + abi.encode(logic, initialOwner, data) + ) + ); + } + + function isContractDeployed(address _addr) internal view returns (bool isContract) { + return (_addr.code.length > 0); + } + + function computeCreateAddress(address deployer, uint8 nonce) internal pure returns (address) { + // RLP([deployer, nonce]) for 0 <= nonce <= 0x7f + // nonce == 0 is encoded as the empty string (0x80) in RLP + require(nonce < 0x80, nonceNotSupported()); + bytes1 nonceRlp = nonce == 0 ? bytes1(0x80) : bytes1(nonce); + bytes memory rlp = abi.encodePacked(bytes1(0xd6), bytes1(0x94), deployer, nonceRlp); + return address(uint160(uint256(keccak256(rlp)))); + } + + function computeCreate2Address( + bytes32 salt, + bytes32 initcodeHash + ) internal pure returns (address) { + return + addressFromLast20Bytes( + keccak256(abi.encodePacked(bytes1(0xff), CREATE2_FACTORY, salt, initcodeHash)) + ); + } + + function computeCreate2Address( + bytes32 salt, + bytes memory bytecode + ) internal pure returns (address) { + return computeCreate2Address(salt, keccak256(abi.encodePacked(bytecode))); + } + + function addressFromLast20Bytes(bytes32 bytesValue) internal pure returns (address) { + return address(uint160(uint256(bytesValue))); + } +} diff --git a/src/libraries/types/Roles.sol b/src/deployments/utils/libraries/Roles.sol similarity index 51% rename from src/libraries/types/Roles.sol rename to src/deployments/utils/libraries/Roles.sol index ed3436165..51c9727a8 100644 --- a/src/libraries/types/Roles.sol +++ b/src/deployments/utils/libraries/Roles.sol @@ -1,13 +1,14 @@ // SPDX-License-Identifier: UNLICENSED // Copyright (c) 2025 Aave Labs -pragma solidity ^0.8.20; +pragma solidity ^0.8.0; /// @title Roles library /// @author Aave Labs /// @notice Defines the different roles used by the protocol. library Roles { uint64 public constant DEFAULT_ADMIN_ROLE = 0; - uint64 public constant HUB_ADMIN_ROLE = 1; - uint64 public constant SPOKE_ADMIN_ROLE = 2; - uint64 public constant USER_POSITION_UPDATER_ROLE = 3; + uint64 public constant HUB_CONFIGURATOR_ROLE = 1; + uint64 public constant HUB_FEE_MINTER_ROLE = 2; + uint64 public constant SPOKE_CONFIGURATOR_ROLE = 3; + uint64 public constant SPOKE_POSITION_UPDATER_ROLE = 4; } diff --git a/tests/Base.t.sol b/tests/Base.t.sol index 424bc3683..c2d7f41a0 100644 --- a/tests/Base.t.sol +++ b/tests/Base.t.sol @@ -11,7 +11,10 @@ import {console2 as console} from 'forge-std/console2.sol'; // dependencies import {AggregatorV3Interface} from 'src/dependencies/chainlink/AggregatorV3Interface.sol'; -import {TransparentUpgradeableProxy, ITransparentUpgradeableProxy} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; +import { + TransparentUpgradeableProxy, + ITransparentUpgradeableProxy +} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; import {IERC20Metadata} from 'src/dependencies/openzeppelin/IERC20Metadata.sol'; import {SafeCast} from 'src/dependencies/openzeppelin/SafeCast.sol'; import {IERC20Errors} from 'src/dependencies/openzeppelin/IERC20Errors.sol'; @@ -23,7 +26,6 @@ import {IAccessManaged} from 'src/dependencies/openzeppelin/IAccessManaged.sol'; import {AuthorityUtils} from 'src/dependencies/openzeppelin/AuthorityUtils.sol'; import {Ownable2Step, Ownable} from 'src/dependencies/openzeppelin/Ownable2Step.sol'; import {Math} from 'src/dependencies/openzeppelin/Math.sol'; -import {WETH9} from 'src/dependencies/weth/WETH9.sol'; import {LibBit} from 'src/dependencies/solady/LibBit.sol'; import {Initializable} from 'src/dependencies/openzeppelin-upgradeable/Initializable.sol'; @@ -34,7 +36,6 @@ import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; import {MathUtils} from 'src/libraries/math/MathUtils.sol'; import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; import {EIP712Types} from 'src/libraries/types/EIP712Types.sol'; -import {Roles} from 'src/libraries/types/Roles.sol'; import {Rescuable, IRescuable} from 'src/utils/Rescuable.sol'; import {NoncesKeyed, INoncesKeyed} from 'src/utils/NoncesKeyed.sol'; import {UnitPriceFeed} from 'src/misc/UnitPriceFeed.sol'; @@ -44,7 +45,11 @@ import {AccessManagerEnumerable} from 'src/access/AccessManagerEnumerable.sol'; import {HubConfigurator, IHubConfigurator} from 'src/hub/HubConfigurator.sol'; import {Hub, IHub, IHubBase} from 'src/hub/Hub.sol'; import {SharesMath} from 'src/hub/libraries/SharesMath.sol'; -import {AssetInterestRateStrategy, IAssetInterestRateStrategy, IBasicInterestRateStrategy} from 'src/hub/AssetInterestRateStrategy.sol'; +import { + AssetInterestRateStrategy, + IAssetInterestRateStrategy, + IBasicInterestRateStrategy +} from 'src/hub/AssetInterestRateStrategy.sol'; // spoke import {Spoke, ISpoke, ISpokeBase} from 'src/spoke/Spoke.sol'; @@ -67,6 +72,12 @@ import {SignatureGateway, ISignatureGateway} from 'src/position-manager/Signatur // test import {Constants} from 'tests/Constants.sol'; import {Utils} from 'tests/Utils.sol'; +import {TestTypes} from 'tests/utils/TestTypes.sol'; + +// orchestration +import {ConfigData} from 'src/deployments/libraries/ConfigData.sol'; +import {OrchestrationReports} from 'src/deployments/libraries/OrchestrationReports.sol'; +import {Roles} from 'src/deployments/utils/libraries/Roles.sol'; // mocks import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; @@ -81,7 +92,9 @@ import {MockERC1271Wallet} from 'tests/mocks/MockERC1271Wallet.sol'; import {MockSpokeInstance} from 'tests/mocks/MockSpokeInstance.sol'; import {MockSkimSpoke} from 'tests/mocks/MockSkimSpoke.sol'; -abstract contract Base is Test { +import 'tests/utils/BatchTestProcedures.sol'; + +abstract contract Base is BatchTestProcedures { using stdStorage for StdStorage; using WadRayMath for *; using SharesMath for uint256; @@ -90,22 +103,11 @@ abstract contract Base is Test { using MathUtils for uint256; using ReserveFlagsMap for ReserveFlags; - bytes32 internal constant ERC1967_ADMIN_SLOT = - 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; - bytes32 internal constant IMPLEMENTATION_SLOT = - 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - uint256 internal constant MAX_SUPPLY_AMOUNT = 1e30; uint256 internal constant MIN_TOKEN_DECIMALS_SUPPORTED = 6; uint256 internal constant MAX_TOKEN_DECIMALS_SUPPORTED = 18; uint256 internal constant MAX_SUPPLY_ASSET_UNITS = MAX_SUPPLY_AMOUNT / 10 ** MAX_TOKEN_DECIMALS_SUPPORTED; - uint256 internal MAX_SUPPLY_AMOUNT_USDX; - uint256 internal MAX_SUPPLY_AMOUNT_DAI; - uint256 internal MAX_SUPPLY_AMOUNT_WBTC; - uint256 internal MAX_SUPPLY_AMOUNT_WETH; - uint256 internal MAX_SUPPLY_AMOUNT_USDY; - uint256 internal MAX_SUPPLY_AMOUNT_USDZ; uint256 internal constant MAX_SUPPLY_IN_BASE_CURRENCY = 1e39; uint24 internal constant MIN_COLLATERAL_RISK_BPS = 1; uint24 internal constant MAX_COLLATERAL_RISK_BPS = 1000_00; @@ -127,6 +129,13 @@ abstract contract Base is Test { PercentageMath.PERCENTAGE_FACTOR; IHubBase.PremiumDelta internal ZERO_PREMIUM_DELTA = ZERO_PREMIUM_DELTA; + IHub[] internal _hubs; + ISpoke[] internal _spokes; + IAaveOracle[] internal _oracles; + ITreasurySpoke[] internal _treasurySpokes; + AssetInterestRateStrategy[] internal _irStrategies; + IAccessManager[] internal _accessManagers; + IAaveOracle internal oracle1; IAaveOracle internal oracle2; IAaveOracle internal oracle3; @@ -137,6 +146,8 @@ abstract contract Base is Test { ISpoke internal spoke3; AssetInterestRateStrategy internal irStrategy; IAccessManager internal accessManager; + IHubConfigurator internal hubConfigurator; + ISpokeConfigurator internal spokeConfigurator; address internal alice = makeAddr('alice'); address internal bob = makeAddr('bob'); @@ -150,8 +161,10 @@ abstract contract Base is Test { address internal TREASURY_ADMIN = makeAddr('TREASURY_ADMIN'); address internal LIQUIDATOR = makeAddr('LIQUIDATOR'); address internal POSITION_MANAGER = makeAddr('POSITION_MANAGER'); + address internal HUB_CONFIGURATOR_ADMIN = makeAddr('HUB_CONFIGURATOR_ADMIN'); + address internal SPOKE_CONFIGURATOR_ADMIN = makeAddr('SPOKE_CONFIGURATOR_ADMIN'); - TokenList internal tokenList; + TestTypes.TokenList internal tokenList; uint256 internal wethAssetId = 0; uint256 internal usdxAssetId = 1; uint256 internal daiAssetId = 2; @@ -166,6 +179,13 @@ abstract contract Base is Test { uint256 internal mintAmount_USDY = MAX_SUPPLY_AMOUNT; uint256 internal mintAmount_USDZ = MAX_SUPPLY_AMOUNT; + uint256 internal MAX_SUPPLY_AMOUNT_USDX; + uint256 internal MAX_SUPPLY_AMOUNT_DAI; + uint256 internal MAX_SUPPLY_AMOUNT_WBTC; + uint256 internal MAX_SUPPLY_AMOUNT_WETH; + uint256 internal MAX_SUPPLY_AMOUNT_USDY; + uint256 internal MAX_SUPPLY_AMOUNT_USDZ; + Decimals internal _decimals = Decimals({usdx: 6, usdy: 18, dai: 18, wbtc: 8, weth: 18, usdz: 18}); struct Decimals { @@ -177,15 +197,6 @@ abstract contract Base is Test { uint8 usdz; } - struct TokenList { - WETH9 weth; - TestnetERC20 usdx; - TestnetERC20 dai; - TestnetERC20 wbtc; - TestnetERC20 usdy; - TestnetERC20 usdz; - } - struct SpokeInfo { ReserveInfo weth; ReserveInfo wbtc; @@ -202,6 +213,13 @@ abstract contract Base is Test { ISpoke.DynamicReserveConfig dynReserveConfig; } + struct FixtureAssetList { + IERC20Metadata underlying; + uint16 liquidityFee; + address reinvestmentController; + bytes irData; + } + struct DrawnAccounting { uint256 totalOwed; uint256 drawn; @@ -257,99 +275,128 @@ abstract contract Base is Test { mapping(ISpoke => SpokeInfo) internal spokeInfo; - function setUp() public virtual { - deployFixtures(); - } + IAssetInterestRateStrategy.InterestRateData internal _defaultIrData = + IAssetInterestRateStrategy.InterestRateData({ + optimalUsageRatio: 90_00, // 90.00% + baseVariableBorrowRate: 5_00, // 5.00% + variableRateSlope1: 5_00, // 5.00% + variableRateSlope2: 5_00 // 5.00% + }); - function _getProxyAdminAddress(address proxy) internal view returns (address) { - bytes32 slotData = vm.load(proxy, ERC1967_ADMIN_SLOT); - return address(uint160(uint256(slotData))); - } + function setUp() public virtual override { + _etchCreate2Factory(); + _initTokenList(); + _setupFixtures(); + } + + function _initEnvironment() internal { + _mintAndApproveTokenList(); + _configureHubsAndSpokes(); + } + + function _setupFixtures() internal virtual { + TestTypes.TestEnvReport memory report = _deployFixtures({numHubs: 1, numSpokes: 3}); + _setupFixturesRoles(report); + + // todo rm when tests adapted to multiple hubs and spokes + hub1 = IHub(report.hubReports[0].hub); + irStrategy = AssetInterestRateStrategy(report.hubReports[0].irStrategy); + treasurySpoke = ITreasurySpoke(report.hubReports[0].treasurySpoke); + spoke1 = ISpoke(report.spokeReports[0].spoke); + spoke2 = ISpoke(report.spokeReports[1].spoke); + spoke3 = ISpoke(report.spokeReports[2].spoke); + oracle1 = IAaveOracle(report.spokeReports[0].aaveOracle); + oracle2 = IAaveOracle(report.spokeReports[1].aaveOracle); + oracle3 = IAaveOracle(report.spokeReports[2].aaveOracle); + accessManager = IAccessManager(report.accessManager); + hubConfigurator = IHubConfigurator(report.configuratorReport.hubConfigurator); + spokeConfigurator = ISpokeConfigurator(report.configuratorReport.spokeConfigurator); + } + + function _deployFixtures( + uint256 numHubs, + uint256 numSpokes + ) internal virtual returns (TestTypes.TestEnvReport memory report) { + report = AaveV4TestOrchestration.deployTestEnv({ + admin: ADMIN, + treasuryAdmin: TREASURY_ADMIN, + hubConfiguratorAdmin: HUB_CONFIGURATOR_ADMIN, + spokeConfiguratorAdmin: SPOKE_CONFIGURATOR_ADMIN, + hubCount: numHubs, + spokeCount: numSpokes, + nativeWrapper: address(tokenList.weth), + salt: keccak256(abi.encodePacked(vm.randomBytes(32))) + }); + for (uint256 i; i < numHubs; ++i) { + _hubs.push(IHub(report.hubReports[i].hub)); + _irStrategies.push(AssetInterestRateStrategy(report.hubReports[i].irStrategy)); + _treasurySpokes.push(ITreasurySpoke(report.hubReports[i].treasurySpoke)); + + vm.label(report.hubReports[i].hub, string.concat('hub', string(abi.encode(i)))); + vm.label(report.hubReports[i].irStrategy, string.concat('irStrategy', string(abi.encode(i)))); + vm.label( + report.hubReports[i].treasurySpoke, + string.concat('treasurySpoke', string(abi.encode(i))) + ); + } - function _getImplementationAddress(address proxy) internal view returns (address) { - bytes32 slotData = vm.load(proxy, IMPLEMENTATION_SLOT); - return address(uint160(uint256(slotData))); - } + for (uint256 i; i < numSpokes; ++i) { + _spokes.push(ISpoke(report.spokeReports[i].spoke)); + _oracles.push(IAaveOracle(report.spokeReports[i].aaveOracle)); - function deployFixtures() internal virtual { - vm.startPrank(ADMIN); - accessManager = IAccessManager(address(new AccessManagerEnumerable(ADMIN))); - hub1 = new Hub(address(accessManager)); - irStrategy = new AssetInterestRateStrategy(address(hub1)); - (spoke1, oracle1) = _deploySpokeWithOracle(ADMIN, address(accessManager), 'Spoke 1 (USD)'); - (spoke2, oracle2) = _deploySpokeWithOracle(ADMIN, address(accessManager), 'Spoke 2 (USD)'); - (spoke3, oracle3) = _deploySpokeWithOracle(ADMIN, address(accessManager), 'Spoke 3 (USD)'); - treasurySpoke = ITreasurySpoke(new TreasurySpoke(TREASURY_ADMIN, address(hub1))); - vm.stopPrank(); + 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)))); + } - vm.label(address(spoke1), 'spoke1'); - vm.label(address(spoke2), 'spoke2'); - vm.label(address(spoke3), 'spoke3'); + vm.label(report.configuratorReport.hubConfigurator, 'hubConfigurator'); + vm.label(report.configuratorReport.spokeConfigurator, 'spokeConfigurator'); - setUpRoles(hub1, spoke1, accessManager); - setUpRoles(hub1, spoke2, accessManager); - setUpRoles(hub1, spoke3, accessManager); + return report; } - function setUpRoles(IHub targetHub, ISpoke spoke, IAccessManager manager) internal virtual { - vm.startPrank(ADMIN); - // Grant roles with 0 delay - manager.grantRole(Roles.HUB_ADMIN_ROLE, ADMIN, 0); - manager.grantRole(Roles.HUB_ADMIN_ROLE, HUB_ADMIN, 0); - - manager.grantRole(Roles.SPOKE_ADMIN_ROLE, ADMIN, 0); - manager.grantRole(Roles.SPOKE_ADMIN_ROLE, SPOKE_ADMIN, 0); + function _setupFixturesRoles(TestTypes.TestEnvReport memory report) internal virtual { + if (report.accessManager == address(0)) report.accessManager = address(accessManager); - manager.grantRole(Roles.USER_POSITION_UPDATER_ROLE, SPOKE_ADMIN, 0); - manager.grantRole(Roles.USER_POSITION_UPDATER_ROLE, USER_POSITION_UPDATER, 0); - - // Grant responsibilities to roles - { - bytes4[] memory selectors = new bytes4[](7); - 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; - manager.setTargetFunctionRole(address(spoke), selectors, Roles.SPOKE_ADMIN_ROLE); - } + // temporary grant admin role to address(this) to execute setAndGrantRolesTestEnv from its context + vm.startPrank(ADMIN); + IAccessManager(report.accessManager).grantRole(Roles.DEFAULT_ADMIN_ROLE, address(this), 0); + vm.stopPrank(); - { - bytes4[] memory selectors = new bytes4[](2); - selectors[0] = ISpoke.updateUserDynamicConfig.selector; - selectors[1] = ISpoke.updateUserRiskPremium.selector; - manager.setTargetFunctionRole(address(spoke), selectors, Roles.USER_POSITION_UPDATER_ROLE); - } + AaveV4TestOrchestration.setRolesTestEnv(report); + AaveV4TestOrchestration.grantRolesTestEnv(report, ADMIN, HUB_ADMIN, SPOKE_ADMIN); - { - bytes4[] memory selectors = new bytes4[](6); - selectors[0] = IHub.addAsset.selector; - selectors[1] = IHub.updateAssetConfig.selector; - selectors[2] = IHub.addSpoke.selector; - selectors[3] = IHub.updateSpokeConfig.selector; - selectors[4] = IHub.setInterestRateData.selector; - selectors[5] = IHub.mintFeeShares.selector; - manager.setTargetFunctionRole(address(targetHub), selectors, Roles.HUB_ADMIN_ROLE); - } - vm.stopPrank(); + IAccessManager(report.accessManager).renounceRole(Roles.DEFAULT_ADMIN_ROLE, address(this)); } - function initEnvironment() internal { - deployMintAndApproveTokenList(); - configureTokenList(); - } + function _initTokenList() internal { + TestTypes.TestTokenInput[] memory tokenInputs = new TestTypes.TestTokenInput[](5); + tokenInputs[0] = TestTypes.TestTokenInput({ + name: 'USDX', + symbol: 'USDX', + decimals: _decimals.usdx + }); + tokenInputs[1] = TestTypes.TestTokenInput({ + name: 'DAI', + symbol: 'DAI', + decimals: _decimals.dai + }); + tokenInputs[2] = TestTypes.TestTokenInput({ + name: 'WBTC', + symbol: 'WBTC', + decimals: _decimals.wbtc + }); + tokenInputs[3] = TestTypes.TestTokenInput({ + name: 'USDY', + symbol: 'USDY', + decimals: _decimals.usdy + }); + tokenInputs[4] = TestTypes.TestTokenInput({ + name: 'USDZ', + symbol: 'USDZ', + decimals: _decimals.usdz + }); - function deployMintAndApproveTokenList() internal { - tokenList = TokenList( - new WETH9(), - new TestnetERC20('USDX', 'USDX', _decimals.usdx), - new TestnetERC20('DAI', 'DAI', _decimals.dai), - new TestnetERC20('WBTC', 'WBTC', _decimals.wbtc), - new TestnetERC20('USDY', 'USDY', _decimals.usdy), - new TestnetERC20('USDZ', 'USDZ', _decimals.usdz) - ); + tokenList = AaveV4TestOrchestration.deployTestTokens(tokenInputs); vm.label(address(tokenList.weth), 'WETH'); vm.label(address(tokenList.usdx), 'USDX'); @@ -363,7 +410,9 @@ abstract contract Base is Test { MAX_SUPPLY_AMOUNT_WBTC = MAX_SUPPLY_ASSET_UNITS * 10 ** tokenList.wbtc.decimals(); MAX_SUPPLY_AMOUNT_USDY = MAX_SUPPLY_ASSET_UNITS * 10 ** tokenList.usdy.decimals(); MAX_SUPPLY_AMOUNT_USDZ = MAX_SUPPLY_ASSET_UNITS * 10 ** tokenList.usdz.decimals(); + } + function _mintAndApproveTokenList() internal { address[7] memory users = [ alice, bob, @@ -374,13 +423,6 @@ abstract contract Base is Test { POSITION_MANAGER ]; - address[4] memory spokes = [ - address(spoke1), - address(spoke2), - address(spoke3), - address(treasurySpoke) - ]; - for (uint256 x; x < users.length; ++x) { tokenList.usdx.mint(users[x], mintAmount_USDX); tokenList.dai.mint(users[x], mintAmount_DAI); @@ -390,13 +432,23 @@ abstract contract Base is Test { deal(address(tokenList.weth), users[x], mintAmount_WETH); vm.startPrank(users[x]); - for (uint256 y; y < spokes.length; ++y) { - tokenList.weth.approve(spokes[y], UINT256_MAX); - tokenList.usdx.approve(spokes[y], UINT256_MAX); - tokenList.dai.approve(spokes[y], UINT256_MAX); - tokenList.wbtc.approve(spokes[y], UINT256_MAX); - tokenList.usdy.approve(spokes[y], UINT256_MAX); - tokenList.usdz.approve(spokes[y], UINT256_MAX); + for (uint256 y; y < _spokes.length; ++y) { + address spoke = address(_spokes[y]); + tokenList.weth.approve(spoke, UINT256_MAX); + tokenList.usdx.approve(spoke, UINT256_MAX); + tokenList.dai.approve(spoke, UINT256_MAX); + tokenList.wbtc.approve(spoke, UINT256_MAX); + tokenList.usdy.approve(spoke, UINT256_MAX); + tokenList.usdz.approve(spoke, UINT256_MAX); + } + for (uint256 y; y < _treasurySpokes.length; ++y) { + address spoke = address(_treasurySpokes[y]); + tokenList.weth.approve(spoke, UINT256_MAX); + tokenList.usdx.approve(spoke, UINT256_MAX); + tokenList.dai.approve(spoke, UINT256_MAX); + tokenList.wbtc.approve(spoke, UINT256_MAX); + tokenList.usdy.approve(spoke, UINT256_MAX); + tokenList.usdz.approve(spoke, UINT256_MAX); } vm.stopPrank(); } @@ -430,7 +482,58 @@ abstract contract Base is Test { } } - function configureTokenList() internal { + function _configureHubsAndSpokes() internal { + vm.startPrank(ADMIN); + accessManager.grantRole(Roles.HUB_CONFIGURATOR_ROLE, address(this), 0); + accessManager.grantRole(Roles.SPOKE_CONFIGURATOR_ROLE, address(this), 0); + vm.stopPrank(); + + ( + ConfigData.UpdateLiquidationConfigParams[] memory liquidationParams, + ConfigData.AddReserveParams[] memory reserveParams + ) = _getSpokeReserveParams(); + AaveV4TestOrchestration.configureHubsAssets(_getAddAssetParams()); + AaveV4TestOrchestration.configureHubsSpokes(_getAddSpokeParams()); + TestTypes.SpokeReserveId[] memory spokeReserveIds = AaveV4TestOrchestration.configureSpokes( + liquidationParams, + reserveParams + ); + + _loadSpokeInfo(spokeReserveIds); + + accessManager.renounceRole(Roles.HUB_CONFIGURATOR_ROLE, address(this)); + accessManager.renounceRole(Roles.SPOKE_CONFIGURATOR_ROLE, address(this)); + } + + function _loadSpokeInfo(TestTypes.SpokeReserveId[] memory spokeReserveIds) internal { + // Persist reserveIds into spokeInfo to mirror manual configureTokenList setup + for (uint256 i; i < spokeReserveIds.length; ++i) { + TestTypes.SpokeReserveId memory spokeReserveId = spokeReserveIds[i]; + uint256 reserveId = spokeReserveId.reserveId; + ISpoke spoke = ISpoke(spokeReserveId.spoke); + uint256 assetId = spoke.getReserve(reserveId).assetId; + + if (assetId == wethAssetId) { + spokeInfo[spoke].weth.reserveId = reserveId; + } else if (assetId == wbtcAssetId) { + spokeInfo[spoke].wbtc.reserveId = reserveId; + } else if (assetId == daiAssetId) { + spokeInfo[spoke].dai.reserveId = reserveId; + } else if (assetId == usdxAssetId) { + spokeInfo[spoke].usdx.reserveId = reserveId; + } else if (assetId == usdyAssetId) { + spokeInfo[spoke].usdy.reserveId = reserveId; + } else if (assetId == usdzAssetId) { + spokeInfo[spoke].usdz.reserveId = reserveId; + } + } + } + + function _getAddSpokeParams() + internal + view + returns (ConfigData.AddSpokeParams[] memory paramsList) + { IHub.SpokeConfig memory spokeConfig = IHub.SpokeConfig({ active: true, paused: false, @@ -438,373 +541,396 @@ abstract contract Base is Test { drawCap: Constants.MAX_ALLOWED_SPOKE_CAP, riskPremiumThreshold: Constants.MAX_ALLOWED_COLLATERAL_RISK }); + paramsList = new ConfigData.AddSpokeParams[](15); + + // spoke1 + paramsList[0] = ConfigData.AddSpokeParams({ + spoke: address(spoke1), + hub: address(hub1), + assetId: wethAssetId, + config: spokeConfig + }); + paramsList[1] = ConfigData.AddSpokeParams({ + spoke: address(spoke1), + hub: address(hub1), + assetId: wbtcAssetId, + config: spokeConfig + }); + paramsList[2] = ConfigData.AddSpokeParams({ + spoke: address(spoke1), + hub: address(hub1), + assetId: daiAssetId, + config: spokeConfig + }); + paramsList[3] = ConfigData.AddSpokeParams({ + spoke: address(spoke1), + hub: address(hub1), + assetId: usdxAssetId, + config: spokeConfig + }); + paramsList[4] = ConfigData.AddSpokeParams({ + spoke: address(spoke1), + hub: address(hub1), + assetId: usdyAssetId, + config: spokeConfig + }); - bytes memory encodedIrData = abi.encode( - IAssetInterestRateStrategy.InterestRateData({ - optimalUsageRatio: 90_00, // 90.00% - baseVariableBorrowRate: 5_00, // 5.00% - variableRateSlope1: 5_00, // 5.00% - variableRateSlope2: 5_00 // 5.00% - }) - ); + // spoke2 + paramsList[5] = ConfigData.AddSpokeParams({ + spoke: address(spoke2), + hub: address(hub1), + assetId: wbtcAssetId, + config: spokeConfig + }); + paramsList[6] = ConfigData.AddSpokeParams({ + spoke: address(spoke2), + hub: address(hub1), + assetId: wethAssetId, + config: spokeConfig + }); + paramsList[7] = ConfigData.AddSpokeParams({ + spoke: address(spoke2), + hub: address(hub1), + assetId: daiAssetId, + config: spokeConfig + }); + paramsList[8] = ConfigData.AddSpokeParams({ + spoke: address(spoke2), + hub: address(hub1), + assetId: usdxAssetId, + config: spokeConfig + }); + paramsList[9] = ConfigData.AddSpokeParams({ + spoke: address(spoke2), + hub: address(hub1), + assetId: usdyAssetId, + config: spokeConfig + }); + paramsList[10] = ConfigData.AddSpokeParams({ + spoke: address(spoke2), + hub: address(hub1), + assetId: usdzAssetId, + config: spokeConfig + }); - // Add all assets to the Hub - vm.startPrank(ADMIN); - // add WETH - hub1.addAsset( - address(tokenList.weth), - tokenList.weth.decimals(), - address(treasurySpoke), - address(irStrategy), - encodedIrData - ); - hub1.updateAssetConfig( - wethAssetId, - IHub.AssetConfig({ - liquidityFee: 10_00, - feeReceiver: address(treasurySpoke), - irStrategy: address(irStrategy), - reinvestmentController: address(0) - }), - new bytes(0) - ); - // add USDX - hub1.addAsset( - address(tokenList.usdx), - tokenList.usdx.decimals(), - address(treasurySpoke), - address(irStrategy), - encodedIrData - ); - hub1.updateAssetConfig( - usdxAssetId, - IHub.AssetConfig({ - liquidityFee: 5_00, - feeReceiver: address(treasurySpoke), - irStrategy: address(irStrategy), - reinvestmentController: address(0) - }), - new bytes(0) - ); - // add DAI - hub1.addAsset( - address(tokenList.dai), - tokenList.dai.decimals(), - address(treasurySpoke), - address(irStrategy), - encodedIrData - ); - hub1.updateAssetConfig( - daiAssetId, - IHub.AssetConfig({ - liquidityFee: 5_00, - feeReceiver: address(treasurySpoke), - irStrategy: address(irStrategy), - reinvestmentController: address(0) - }), - new bytes(0) - ); - // add WBTC - hub1.addAsset( - address(tokenList.wbtc), - tokenList.wbtc.decimals(), - address(treasurySpoke), - address(irStrategy), - encodedIrData - ); - hub1.updateAssetConfig( - wbtcAssetId, - IHub.AssetConfig({ - liquidityFee: 10_00, - feeReceiver: address(treasurySpoke), - irStrategy: address(irStrategy), - reinvestmentController: address(0) - }), - new bytes(0) - ); - // add USDY - hub1.addAsset( - address(tokenList.usdy), - tokenList.usdy.decimals(), - address(treasurySpoke), - address(irStrategy), - encodedIrData - ); - hub1.updateAssetConfig( - usdyAssetId, - IHub.AssetConfig({ - liquidityFee: 10_00, - feeReceiver: address(treasurySpoke), - irStrategy: address(irStrategy), - reinvestmentController: address(0) - }), - new bytes(0) - ); - // add USDZ - hub1.addAsset( - address(tokenList.usdz), - tokenList.usdz.decimals(), - address(treasurySpoke), - address(irStrategy), - encodedIrData - ); - hub1.updateAssetConfig( - hub1.getAssetCount() - 1, - IHub.AssetConfig({ - liquidityFee: 5_00, - feeReceiver: address(treasurySpoke), - irStrategy: address(irStrategy), - reinvestmentController: address(0) - }), - new bytes(0) - ); + // spoke3 + paramsList[11] = ConfigData.AddSpokeParams({ + spoke: address(spoke3), + hub: address(hub1), + assetId: daiAssetId, + config: spokeConfig + }); + paramsList[12] = ConfigData.AddSpokeParams({ + spoke: address(spoke3), + hub: address(hub1), + assetId: usdxAssetId, + config: spokeConfig + }); + paramsList[13] = ConfigData.AddSpokeParams({ + spoke: address(spoke3), + hub: address(hub1), + assetId: wethAssetId, + config: spokeConfig + }); + paramsList[14] = ConfigData.AddSpokeParams({ + spoke: address(spoke3), + hub: address(hub1), + assetId: wbtcAssetId, + config: spokeConfig + }); - // Liquidation configs - spoke1.updateLiquidationConfig( - ISpoke.LiquidationConfig({ + return paramsList; + } + + function _getSpokeReserveParams() + internal + returns ( + ConfigData.UpdateLiquidationConfigParams[] memory, + ConfigData.AddReserveParams[] memory + ) + { + ConfigData.UpdateLiquidationConfigParams[] + memory liquidationParams = new ConfigData.UpdateLiquidationConfigParams[](3); + liquidationParams[0] = ConfigData.UpdateLiquidationConfigParams({ + spoke: address(spoke1), + config: ISpoke.LiquidationConfig({ targetHealthFactor: 1.05e18, healthFactorForMaxBonus: 0.7e18, liquidationBonusFactor: 20_00 }) - ); - spoke2.updateLiquidationConfig( - ISpoke.LiquidationConfig({ + }); + liquidationParams[1] = ConfigData.UpdateLiquidationConfigParams({ + spoke: address(spoke2), + config: ISpoke.LiquidationConfig({ targetHealthFactor: 1.04e18, healthFactorForMaxBonus: 0.8e18, liquidationBonusFactor: 15_00 }) - ); - spoke3.updateLiquidationConfig( - ISpoke.LiquidationConfig({ + }); + liquidationParams[2] = ConfigData.UpdateLiquidationConfigParams({ + spoke: address(spoke3), + config: ISpoke.LiquidationConfig({ targetHealthFactor: 1.03e18, healthFactorForMaxBonus: 0.9e18, liquidationBonusFactor: 10_00 }) - ); + }); - // Spoke 1 reserve configs - spokeInfo[spoke1].weth.reserveConfig = _getDefaultReserveConfig(15_00); - spokeInfo[spoke1].weth.dynReserveConfig = ISpoke.DynamicReserveConfig({ - collateralFactor: 80_00, - maxLiquidationBonus: 105_00, - liquidationFee: 10_00 + ConfigData.AddReserveParams[] memory reserveParams = new ConfigData.AddReserveParams[](15); + // spoke1 + reserveParams[0] = ConfigData.AddReserveParams({ + spoke: address(spoke1), + hub: address(hub1), + assetId: wethAssetId, + priceSource: _deployMockPriceFeed(spoke1, 2000e8), + config: _getDefaultReserveConfig(15_00), + dynamicConfig: ISpoke.DynamicReserveConfig({ + collateralFactor: 80_00, + maxLiquidationBonus: 105_00, + liquidationFee: 10_00 + }) }); - spokeInfo[spoke1].wbtc.reserveConfig = _getDefaultReserveConfig(15_00); - spokeInfo[spoke1].wbtc.dynReserveConfig = ISpoke.DynamicReserveConfig({ - collateralFactor: 75_00, - maxLiquidationBonus: 103_00, - liquidationFee: 15_00 + reserveParams[1] = ConfigData.AddReserveParams({ + spoke: address(spoke1), + hub: address(hub1), + assetId: wbtcAssetId, + priceSource: _deployMockPriceFeed(spoke1, 50_000e8), + config: _getDefaultReserveConfig(15_00), + dynamicConfig: ISpoke.DynamicReserveConfig({ + collateralFactor: 75_00, + maxLiquidationBonus: 103_00, + liquidationFee: 15_00 + }) }); - spokeInfo[spoke1].dai.reserveConfig = _getDefaultReserveConfig(20_00); - spokeInfo[spoke1].dai.dynReserveConfig = ISpoke.DynamicReserveConfig({ - collateralFactor: 78_00, - maxLiquidationBonus: 102_00, - liquidationFee: 10_00 + reserveParams[2] = ConfigData.AddReserveParams({ + spoke: address(spoke1), + hub: address(hub1), + assetId: daiAssetId, + priceSource: _deployMockPriceFeed(spoke1, 1e8), + config: _getDefaultReserveConfig(20_00), + dynamicConfig: ISpoke.DynamicReserveConfig({ + collateralFactor: 78_00, + maxLiquidationBonus: 102_00, + liquidationFee: 10_00 + }) }); - spokeInfo[spoke1].usdx.reserveConfig = _getDefaultReserveConfig(50_00); - spokeInfo[spoke1].usdx.dynReserveConfig = ISpoke.DynamicReserveConfig({ - collateralFactor: 78_00, - maxLiquidationBonus: 101_00, - liquidationFee: 12_00 + reserveParams[3] = ConfigData.AddReserveParams({ + spoke: address(spoke1), + hub: address(hub1), + assetId: usdxAssetId, + priceSource: _deployMockPriceFeed(spoke1, 1e8), + config: _getDefaultReserveConfig(50_00), + dynamicConfig: ISpoke.DynamicReserveConfig({ + collateralFactor: 78_00, + maxLiquidationBonus: 101_00, + liquidationFee: 12_00 + }) }); - spokeInfo[spoke1].usdy.reserveConfig = _getDefaultReserveConfig(50_00); - spokeInfo[spoke1].usdy.dynReserveConfig = ISpoke.DynamicReserveConfig({ - collateralFactor: 78_00, - maxLiquidationBonus: 101_50, - liquidationFee: 15_00 + reserveParams[4] = ConfigData.AddReserveParams({ + spoke: address(spoke1), + hub: address(hub1), + assetId: usdyAssetId, + priceSource: _deployMockPriceFeed(spoke1, 1e8), + config: _getDefaultReserveConfig(50_00), + dynamicConfig: ISpoke.DynamicReserveConfig({ + collateralFactor: 78_00, + maxLiquidationBonus: 101_50, + liquidationFee: 15_00 + }) }); - spokeInfo[spoke1].weth.reserveId = spoke1.addReserve( - address(hub1), - wethAssetId, - _deployMockPriceFeed(spoke1, 2000e8), - spokeInfo[spoke1].weth.reserveConfig, - spokeInfo[spoke1].weth.dynReserveConfig - ); - spokeInfo[spoke1].wbtc.reserveId = spoke1.addReserve( - address(hub1), - wbtcAssetId, - _deployMockPriceFeed(spoke1, 50_000e8), - spokeInfo[spoke1].wbtc.reserveConfig, - spokeInfo[spoke1].wbtc.dynReserveConfig - ); - spokeInfo[spoke1].dai.reserveId = spoke1.addReserve( - address(hub1), - daiAssetId, - _deployMockPriceFeed(spoke1, 1e8), - spokeInfo[spoke1].dai.reserveConfig, - spokeInfo[spoke1].dai.dynReserveConfig - ); - spokeInfo[spoke1].usdx.reserveId = spoke1.addReserve( - address(hub1), - usdxAssetId, - _deployMockPriceFeed(spoke1, 1e8), - spokeInfo[spoke1].usdx.reserveConfig, - spokeInfo[spoke1].usdx.dynReserveConfig - ); - spokeInfo[spoke1].usdy.reserveId = spoke1.addReserve( - address(hub1), - usdyAssetId, - _deployMockPriceFeed(spoke1, 1e8), - spokeInfo[spoke1].usdy.reserveConfig, - spokeInfo[spoke1].usdy.dynReserveConfig - ); - - hub1.addSpoke(wethAssetId, address(spoke1), spokeConfig); - hub1.addSpoke(wbtcAssetId, address(spoke1), spokeConfig); - hub1.addSpoke(daiAssetId, address(spoke1), spokeConfig); - hub1.addSpoke(usdxAssetId, address(spoke1), spokeConfig); - hub1.addSpoke(usdyAssetId, address(spoke1), spokeConfig); - - // Spoke 2 reserve configs - spokeInfo[spoke2].wbtc.reserveConfig = _getDefaultReserveConfig(0); - spokeInfo[spoke2].wbtc.dynReserveConfig = ISpoke.DynamicReserveConfig({ - collateralFactor: 80_00, - maxLiquidationBonus: 105_00, - liquidationFee: 10_00 + // spoke2 + reserveParams[5] = ConfigData.AddReserveParams({ + spoke: address(spoke2), + hub: address(hub1), + assetId: wbtcAssetId, + priceSource: _deployMockPriceFeed(spoke2, 50_000e8), + config: _getDefaultReserveConfig(0), + dynamicConfig: ISpoke.DynamicReserveConfig({ + collateralFactor: 80_00, + maxLiquidationBonus: 105_00, + liquidationFee: 10_00 + }) }); - spokeInfo[spoke2].weth.reserveConfig = _getDefaultReserveConfig(10_00); - spokeInfo[spoke2].weth.dynReserveConfig = ISpoke.DynamicReserveConfig({ - collateralFactor: 76_00, - maxLiquidationBonus: 103_00, - liquidationFee: 15_00 + reserveParams[6] = ConfigData.AddReserveParams({ + spoke: address(spoke2), + hub: address(hub1), + assetId: wethAssetId, + priceSource: _deployMockPriceFeed(spoke2, 2000e8), + config: _getDefaultReserveConfig(10_00), + dynamicConfig: ISpoke.DynamicReserveConfig({ + collateralFactor: 76_00, + maxLiquidationBonus: 103_00, + liquidationFee: 15_00 + }) }); - spokeInfo[spoke2].dai.reserveConfig = _getDefaultReserveConfig(20_00); - spokeInfo[spoke2].dai.dynReserveConfig = ISpoke.DynamicReserveConfig({ - collateralFactor: 72_00, - maxLiquidationBonus: 102_00, - liquidationFee: 10_00 + reserveParams[7] = ConfigData.AddReserveParams({ + spoke: address(spoke2), + hub: address(hub1), + assetId: daiAssetId, + priceSource: _deployMockPriceFeed(spoke2, 1e8), + config: _getDefaultReserveConfig(20_00), + dynamicConfig: ISpoke.DynamicReserveConfig({ + collateralFactor: 72_00, + maxLiquidationBonus: 102_00, + liquidationFee: 10_00 + }) }); - spokeInfo[spoke2].usdx.reserveConfig = _getDefaultReserveConfig(50_00); - spokeInfo[spoke2].usdx.dynReserveConfig = ISpoke.DynamicReserveConfig({ - collateralFactor: 72_00, - maxLiquidationBonus: 101_00, - liquidationFee: 12_00 + reserveParams[8] = ConfigData.AddReserveParams({ + spoke: address(spoke2), + hub: address(hub1), + assetId: usdxAssetId, + priceSource: _deployMockPriceFeed(spoke2, 1e8), + config: _getDefaultReserveConfig(50_00), + dynamicConfig: ISpoke.DynamicReserveConfig({ + collateralFactor: 72_00, + maxLiquidationBonus: 101_00, + liquidationFee: 12_00 + }) }); - spokeInfo[spoke2].usdy.reserveConfig = _getDefaultReserveConfig(50_00); - spokeInfo[spoke2].usdy.dynReserveConfig = ISpoke.DynamicReserveConfig({ - collateralFactor: 72_00, - maxLiquidationBonus: 101_50, - liquidationFee: 15_00 + reserveParams[9] = ConfigData.AddReserveParams({ + spoke: address(spoke2), + hub: address(hub1), + assetId: usdyAssetId, + priceSource: _deployMockPriceFeed(spoke2, 1e8), + config: _getDefaultReserveConfig(50_00), + dynamicConfig: ISpoke.DynamicReserveConfig({ + collateralFactor: 72_00, + maxLiquidationBonus: 101_50, + liquidationFee: 15_00 + }) }); - spokeInfo[spoke2].usdz.reserveConfig = _getDefaultReserveConfig(100_00); - spokeInfo[spoke2].usdz.dynReserveConfig = ISpoke.DynamicReserveConfig({ - collateralFactor: 70_00, - maxLiquidationBonus: 106_00, - liquidationFee: 10_00 + reserveParams[10] = ConfigData.AddReserveParams({ + spoke: address(spoke2), + hub: address(hub1), + assetId: usdzAssetId, + priceSource: _deployMockPriceFeed(spoke2, 1e8), + config: _getDefaultReserveConfig(100_00), + dynamicConfig: ISpoke.DynamicReserveConfig({ + collateralFactor: 70_00, + maxLiquidationBonus: 106_00, + liquidationFee: 10_00 + }) }); - spokeInfo[spoke2].wbtc.reserveId = spoke2.addReserve( - address(hub1), - wbtcAssetId, - _deployMockPriceFeed(spoke2, 50_000e8), - spokeInfo[spoke2].wbtc.reserveConfig, - spokeInfo[spoke2].wbtc.dynReserveConfig - ); - spokeInfo[spoke2].weth.reserveId = spoke2.addReserve( - address(hub1), - wethAssetId, - _deployMockPriceFeed(spoke2, 2000e8), - spokeInfo[spoke2].weth.reserveConfig, - spokeInfo[spoke2].weth.dynReserveConfig - ); - spokeInfo[spoke2].dai.reserveId = spoke2.addReserve( - address(hub1), - daiAssetId, - _deployMockPriceFeed(spoke2, 1e8), - spokeInfo[spoke2].dai.reserveConfig, - spokeInfo[spoke2].dai.dynReserveConfig - ); - spokeInfo[spoke2].usdx.reserveId = spoke2.addReserve( - address(hub1), - usdxAssetId, - _deployMockPriceFeed(spoke2, 1e8), - spokeInfo[spoke2].usdx.reserveConfig, - spokeInfo[spoke2].usdx.dynReserveConfig - ); - spokeInfo[spoke2].usdy.reserveId = spoke2.addReserve( - address(hub1), - usdyAssetId, - _deployMockPriceFeed(spoke2, 1e8), - spokeInfo[spoke2].usdy.reserveConfig, - spokeInfo[spoke2].usdy.dynReserveConfig - ); - spokeInfo[spoke2].usdz.reserveId = spoke2.addReserve( - address(hub1), - usdzAssetId, - _deployMockPriceFeed(spoke2, 1e8), - spokeInfo[spoke2].usdz.reserveConfig, - spokeInfo[spoke2].usdz.dynReserveConfig - ); - - hub1.addSpoke(wbtcAssetId, address(spoke2), spokeConfig); - hub1.addSpoke(wethAssetId, address(spoke2), spokeConfig); - hub1.addSpoke(daiAssetId, address(spoke2), spokeConfig); - hub1.addSpoke(usdxAssetId, address(spoke2), spokeConfig); - hub1.addSpoke(usdyAssetId, address(spoke2), spokeConfig); - hub1.addSpoke(usdzAssetId, address(spoke2), spokeConfig); - - // Spoke 3 reserve configs - spokeInfo[spoke3].dai.reserveConfig = _getDefaultReserveConfig(0); - spokeInfo[spoke3].dai.dynReserveConfig = ISpoke.DynamicReserveConfig({ - collateralFactor: 75_00, - maxLiquidationBonus: 104_00, - liquidationFee: 11_00 + // spoke3 + reserveParams[11] = ConfigData.AddReserveParams({ + spoke: address(spoke3), + hub: address(hub1), + assetId: daiAssetId, + priceSource: _deployMockPriceFeed(spoke3, 1e8), + config: _getDefaultReserveConfig(0), + dynamicConfig: ISpoke.DynamicReserveConfig({ + collateralFactor: 75_00, + maxLiquidationBonus: 104_00, + liquidationFee: 11_00 + }) }); - spokeInfo[spoke3].usdx.reserveConfig = _getDefaultReserveConfig(10_00); - spokeInfo[spoke3].usdx.dynReserveConfig = ISpoke.DynamicReserveConfig({ - collateralFactor: 75_00, - maxLiquidationBonus: 103_00, - liquidationFee: 15_00 + reserveParams[12] = ConfigData.AddReserveParams({ + spoke: address(spoke3), + hub: address(hub1), + assetId: usdxAssetId, + priceSource: _deployMockPriceFeed(spoke3, 1e8), + config: _getDefaultReserveConfig(10_00), + dynamicConfig: ISpoke.DynamicReserveConfig({ + collateralFactor: 75_00, + maxLiquidationBonus: 103_00, + liquidationFee: 15_00 + }) }); - spokeInfo[spoke3].weth.reserveConfig = _getDefaultReserveConfig(20_00); - spokeInfo[spoke3].weth.dynReserveConfig = ISpoke.DynamicReserveConfig({ - collateralFactor: 79_00, - maxLiquidationBonus: 102_00, - liquidationFee: 10_00 + reserveParams[13] = ConfigData.AddReserveParams({ + spoke: address(spoke3), + hub: address(hub1), + assetId: wethAssetId, + priceSource: _deployMockPriceFeed(spoke3, 2000e8), + config: _getDefaultReserveConfig(20_00), + dynamicConfig: ISpoke.DynamicReserveConfig({ + collateralFactor: 79_00, + maxLiquidationBonus: 102_00, + liquidationFee: 10_00 + }) }); - spokeInfo[spoke3].wbtc.reserveConfig = _getDefaultReserveConfig(50_00); - spokeInfo[spoke3].wbtc.dynReserveConfig = ISpoke.DynamicReserveConfig({ - collateralFactor: 77_00, - maxLiquidationBonus: 101_00, - liquidationFee: 12_00 + reserveParams[14] = ConfigData.AddReserveParams({ + spoke: address(spoke3), + hub: address(hub1), + assetId: wbtcAssetId, + priceSource: _deployMockPriceFeed(spoke3, 50_000e8), + config: _getDefaultReserveConfig(50_00), + dynamicConfig: ISpoke.DynamicReserveConfig({ + collateralFactor: 77_00, + maxLiquidationBonus: 101_00, + liquidationFee: 12_00 + }) }); - spokeInfo[spoke3].dai.reserveId = spoke3.addReserve( - address(hub1), - daiAssetId, - _deployMockPriceFeed(spoke3, 1e8), - spokeInfo[spoke3].dai.reserveConfig, - spokeInfo[spoke3].dai.dynReserveConfig - ); - spokeInfo[spoke3].usdx.reserveId = spoke3.addReserve( - address(hub1), - usdxAssetId, - _deployMockPriceFeed(spoke3, 1e8), - spokeInfo[spoke3].usdx.reserveConfig, - spokeInfo[spoke3].usdx.dynReserveConfig - ); - spokeInfo[spoke3].weth.reserveId = spoke3.addReserve( - address(hub1), - wethAssetId, - _deployMockPriceFeed(spoke3, 2000e8), - spokeInfo[spoke3].weth.reserveConfig, - spokeInfo[spoke3].weth.dynReserveConfig - ); - spokeInfo[spoke3].wbtc.reserveId = spoke3.addReserve( - address(hub1), - wbtcAssetId, - _deployMockPriceFeed(spoke3, 50_000e8), - spokeInfo[spoke3].wbtc.reserveConfig, - spokeInfo[spoke3].wbtc.dynReserveConfig - ); + return (liquidationParams, reserveParams); + } - hub1.addSpoke(daiAssetId, address(spoke3), spokeConfig); - hub1.addSpoke(usdxAssetId, address(spoke3), spokeConfig); - hub1.addSpoke(wethAssetId, address(spoke3), spokeConfig); - hub1.addSpoke(wbtcAssetId, address(spoke3), spokeConfig); + function _getAddAssetParams() internal view returns (ConfigData.AddAssetParams[] memory) { + bytes memory encodedIrData = abi.encode(_defaultIrData); - vm.stopPrank(); + ConfigData.AddAssetParams[] memory assetParams = new ConfigData.AddAssetParams[](6); + assetParams[0] = ConfigData.AddAssetParams({ + hub: address(hub1), + underlying: address(tokenList.weth), + decimals: tokenList.weth.decimals(), + feeReceiver: address(treasurySpoke), + liquidityFee: 10_00, + irStrategy: address(irStrategy), + reinvestmentController: address(0), + irData: encodedIrData + }); + assetParams[1] = ConfigData.AddAssetParams({ + hub: address(hub1), + underlying: address(tokenList.usdx), + decimals: tokenList.usdx.decimals(), + feeReceiver: address(treasurySpoke), + liquidityFee: 5_00, + irStrategy: address(irStrategy), + reinvestmentController: address(0), + irData: encodedIrData + }); + assetParams[2] = ConfigData.AddAssetParams({ + hub: address(hub1), + underlying: address(tokenList.dai), + decimals: tokenList.dai.decimals(), + feeReceiver: address(treasurySpoke), + liquidityFee: 5_00, + irStrategy: address(irStrategy), + reinvestmentController: address(0), + irData: encodedIrData + }); + assetParams[3] = ConfigData.AddAssetParams({ + hub: address(hub1), + underlying: address(tokenList.wbtc), + decimals: tokenList.wbtc.decimals(), + feeReceiver: address(treasurySpoke), + liquidityFee: 10_00, + irStrategy: address(irStrategy), + reinvestmentController: address(0), + irData: encodedIrData + }); + assetParams[4] = ConfigData.AddAssetParams({ + hub: address(hub1), + underlying: address(tokenList.usdy), + decimals: tokenList.usdy.decimals(), + feeReceiver: address(treasurySpoke), + liquidityFee: 10_00, + irStrategy: address(irStrategy), + reinvestmentController: address(0), + irData: encodedIrData + }); + assetParams[5] = ConfigData.AddAssetParams({ + hub: address(hub1), + underlying: address(tokenList.usdz), + decimals: tokenList.usdz.decimals(), + feeReceiver: address(treasurySpoke), + liquidityFee: 5_00, + irStrategy: address(irStrategy), + reinvestmentController: address(0), + irData: encodedIrData + }); + return assetParams; } /* @dev Configures Hub 2 with the following assetIds: @@ -813,65 +939,38 @@ abstract contract Base is Test { * 2: DAI * 3: WBTC */ - function hub2Fixture() internal returns (IHub, AssetInterestRateStrategy) { - IAccessManager accessManager2 = IAccessManager(address(new AccessManagerEnumerable(ADMIN))); - IHub hub2 = new Hub(address(accessManager2)); - vm.label(address(hub2), 'Hub2'); - AssetInterestRateStrategy hub2IrStrategy = new AssetInterestRateStrategy(address(hub2)); - - // Configure IR Strategy for hub 2 - bytes memory encodedIrData = abi.encode( - IAssetInterestRateStrategy.InterestRateData({ - optimalUsageRatio: 90_00, // 90.00% - baseVariableBorrowRate: 5_00, // 5.00% - variableRateSlope1: 5_00, // 5.00% - variableRateSlope2: 5_00 // 5.00% - }) - ); - - vm.startPrank(ADMIN); - - // Add assets to the second hub - // Add WETH - hub2.addAsset( - address(tokenList.weth), - tokenList.weth.decimals(), - address(treasurySpoke), - address(hub2IrStrategy), - encodedIrData - ); - - // Add USDX - hub2.addAsset( - address(tokenList.usdx), - tokenList.usdx.decimals(), - address(treasurySpoke), - address(hub2IrStrategy), - encodedIrData - ); - - // Add DAI - hub2.addAsset( - address(tokenList.dai), - tokenList.dai.decimals(), - address(treasurySpoke), - address(hub2IrStrategy), - encodedIrData - ); + function _hub2Fixture() internal returns (IHub, AssetInterestRateStrategy) { + FixtureAssetList[] memory assetsList = new FixtureAssetList[](4); + assetsList[0] = FixtureAssetList({ + underlying: IERC20Metadata(address(tokenList.weth)), + liquidityFee: 0, + reinvestmentController: address(0), + irData: abi.encode(_defaultIrData) + }); + assetsList[1] = FixtureAssetList({ + underlying: IERC20Metadata(address(tokenList.usdx)), + liquidityFee: 0, + reinvestmentController: address(0), + irData: abi.encode(_defaultIrData) + }); + assetsList[2] = FixtureAssetList({ + underlying: IERC20Metadata(address(tokenList.dai)), + liquidityFee: 0, + reinvestmentController: address(0), + irData: abi.encode(_defaultIrData) + }); + assetsList[3] = FixtureAssetList({ + underlying: IERC20Metadata(address(tokenList.wbtc)), + liquidityFee: 0, + reinvestmentController: address(0), + irData: abi.encode(_defaultIrData) + }); - // Add WBTC - hub2.addAsset( - address(tokenList.wbtc), - tokenList.wbtc.decimals(), - address(treasurySpoke), - address(hub2IrStrategy), - encodedIrData + TestTypes.TestEnvReport memory report = _addFixture('2', assetsList); + return ( + IHub(report.hubReports[0].hub), + AssetInterestRateStrategy(report.hubReports[0].irStrategy) ); - vm.stopPrank(); - - setUpRoles(hub2, spoke1, accessManager2); - - return (hub2, hub2IrStrategy); } /* @dev Configures Hub 3 with the following assetIds: @@ -880,63 +979,99 @@ abstract contract Base is Test { * 2: WBTC * 3: WETH */ - function hub3Fixture() internal returns (IHub, AssetInterestRateStrategy) { - IAccessManager accessManager3 = IAccessManager(address(new AccessManagerEnumerable(ADMIN))); - IHub hub3 = new Hub(address(accessManager3)); - AssetInterestRateStrategy hub3IrStrategy = new AssetInterestRateStrategy(address(hub3)); - - // Configure IR Strategy for hub 3 - bytes memory encodedIrData = abi.encode( - IAssetInterestRateStrategy.InterestRateData({ - optimalUsageRatio: 90_00, // 90.00% - baseVariableBorrowRate: 5_00, // 5.00% - variableRateSlope1: 5_00, // 5.00% - variableRateSlope2: 5_00 // 5.00% - }) - ); + function _hub3Fixture() internal returns (IHub, AssetInterestRateStrategy) { + FixtureAssetList[] memory assetsList = new FixtureAssetList[](4); + assetsList[0] = FixtureAssetList({ + underlying: IERC20Metadata(address(tokenList.dai)), + liquidityFee: 0, + reinvestmentController: address(0), + irData: abi.encode(_defaultIrData) + }); + assetsList[1] = FixtureAssetList({ + underlying: IERC20Metadata(address(tokenList.usdx)), + liquidityFee: 0, + reinvestmentController: address(0), + irData: abi.encode(_defaultIrData) + }); + assetsList[2] = FixtureAssetList({ + underlying: IERC20Metadata(address(tokenList.wbtc)), + liquidityFee: 0, + reinvestmentController: address(0), + irData: abi.encode(_defaultIrData) + }); + assetsList[3] = FixtureAssetList({ + underlying: IERC20Metadata(address(tokenList.weth)), + liquidityFee: 0, + reinvestmentController: address(0), + irData: abi.encode(_defaultIrData) + }); + + TestTypes.TestEnvReport memory report = _addFixture('3', assetsList); + return ( + IHub(report.hubReports[0].hub), + AssetInterestRateStrategy(report.hubReports[0].irStrategy) + ); + } + + function _addFixture( + string memory label, + FixtureAssetList[] memory assetsList + ) internal returns (TestTypes.TestEnvReport memory report) { + report = _deployFixtures({numHubs: 1, numSpokes: 0}); + _setupFixturesRoles(report); + + ConfigData.AddAssetParams[] memory assetParams = new ConfigData.AddAssetParams[]( + assetsList.length + ); + for (uint256 i; i < assetsList.length; ++i) { + assetParams[i] = ConfigData.AddAssetParams({ + hub: report.hubReports[0].hub, + underlying: address(assetsList[i].underlying), + decimals: assetsList[i].underlying.decimals(), + feeReceiver: report.hubReports[0].treasurySpoke, + liquidityFee: assetsList[i].liquidityFee, + irStrategy: report.hubReports[0].irStrategy, + irData: assetsList[i].irData, + reinvestmentController: assetsList[i].reinvestmentController + }); + } vm.startPrank(ADMIN); - // Add DAI - hub3.addAsset( - address(tokenList.dai), - tokenList.dai.decimals(), - address(treasurySpoke), - address(hub3IrStrategy), - encodedIrData - ); + IAccessManager(report.accessManager).grantRole(Roles.HUB_CONFIGURATOR_ROLE, address(this), 0); + IAccessManager(report.accessManager).grantRole(Roles.SPOKE_CONFIGURATOR_ROLE, address(this), 0); + vm.stopPrank(); - // Add USDX - hub3.addAsset( - address(tokenList.usdx), - tokenList.usdx.decimals(), - address(treasurySpoke), - address(hub3IrStrategy), - encodedIrData - ); + AaveV4TestOrchestration.configureHubsAssets(assetParams); - // Add WBTC - hub3.addAsset( - address(tokenList.wbtc), - tokenList.wbtc.decimals(), - address(treasurySpoke), - address(hub3IrStrategy), - encodedIrData - ); + // Renounce temporary roles + IAccessManager(report.accessManager).renounceRole(Roles.HUB_CONFIGURATOR_ROLE, address(this)); + IAccessManager(report.accessManager).renounceRole(Roles.SPOKE_CONFIGURATOR_ROLE, address(this)); - // Add WETH - hub3.addAsset( - address(tokenList.weth), - tokenList.weth.decimals(), - address(treasurySpoke), - address(hub3IrStrategy), - encodedIrData - ); + vm.label(report.hubReports[0].hub, string.concat('Hub', label)); + vm.label(report.hubReports[0].irStrategy, string.concat('IrStrategy', label)); + vm.label(report.hubReports[0].treasurySpoke, string.concat('TreasurySpoke', label)); + + return report; + } + function _grantSpokeConfiguratorRole(ISpoke spoke, address configurator) internal { + vm.startPrank(ADMIN); + IAccessManager(spoke.authority()).grantRole(Roles.SPOKE_CONFIGURATOR_ROLE, configurator, 0); vm.stopPrank(); + } - setUpRoles(hub3, spoke1, accessManager3); + function _grantHubAdminRole(IHub hub, address admin) internal { + vm.startPrank(ADMIN); + // hub admin consists of hub admin role and hub configurator role + IAccessManager(hub.authority()).grantRole(Roles.HUB_FEE_MINTER_ROLE, admin, 0); + IAccessManager(hub.authority()).grantRole(Roles.HUB_CONFIGURATOR_ROLE, admin, 0); + vm.stopPrank(); + } - return (hub3, hub3IrStrategy); + function _grantHubConfiguratorRole(IHub hub, address admin) internal { + vm.startPrank(ADMIN); + IAccessManager(hub.authority()).grantRole(Roles.HUB_CONFIGURATOR_ROLE, admin, 0); + vm.stopPrank(); } function updateAssetFeeReceiver( @@ -1598,7 +1733,7 @@ abstract contract Base is Test { ); uint256 restoredPremiumRay = (premiumAmountToRestore * WadRayMath.RAY).min(premiumDebtRay); - uint256 restoredShares = drawnDebtToRestore.rayDivDown(hub.getAssetDrawnIndex(reserveId)); + uint256 restoredShares = drawnDebtToRestore.rayDivDown(hub.getAssetDrawnIndex(assetId)); uint256 riskPremium = _getUserLastRiskPremium(spoke, user); return @@ -2147,9 +2282,9 @@ abstract contract Base is Test { user != address(spoke1) && user != address(spoke2) && user != address(spoke3) && - user != _getProxyAdminAddress(address(spoke1)) && - user != _getProxyAdminAddress(address(spoke2)) && - user != _getProxyAdminAddress(address(spoke3)) + user != ProxyHelper.getProxyAdmin(address(spoke1)) && + user != ProxyHelper.getProxyAdmin(address(spoke2)) && + user != ProxyHelper.getProxyAdmin(address(spoke3)) ); } @@ -2202,6 +2337,10 @@ abstract contract Base is Test { return vm.randomUint(0, PercentageMath.PERCENTAGE_FACTOR).toUint16(); } + function _randomBps(uint256 maxBps) internal returns (uint16) { + return vm.randomUint(0, maxBps).toUint16(); + } + function _hub(ISpoke spoke, uint256 reserveId) internal view returns (IHub) { return IHub(address(spoke.getReserve(reserveId).hub)); } diff --git a/tests/Constants.sol b/tests/Constants.sol index b32ed1d4f..3c9693d8d 100644 --- a/tests/Constants.sol +++ b/tests/Constants.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.0; library Constants { + bool public constant IS_TEST = true; + /// @dev Hub Constants uint8 public constant MAX_ALLOWED_UNDERLYING_DECIMALS = 18; uint8 public constant MIN_ALLOWED_UNDERLYING_DECIMALS = 6; @@ -10,7 +12,6 @@ library Constants { uint24 public constant MAX_RISK_PREMIUM_THRESHOLD = type(uint24).max; // 167772.15% /// @dev Spoke Constants - uint8 public constant ORACLE_DECIMALS = 8; uint64 public constant HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 1e18; uint256 public constant DUST_LIQUIDATION_THRESHOLD = 1000e26; uint24 public constant MAX_ALLOWED_COLLATERAL_RISK = 1000_00; // 1000.00% @@ -19,4 +20,8 @@ library Constants { // keccak256('SetUserPositionManager(address positionManager,address user,bool approve,uint256 nonce,uint256 deadline)') 0x758d23a3c07218b7ea0b4f7f63903c4e9d5cbde72d3bcfe3e9896639025a0214; uint256 public constant MAX_ALLOWED_ASSET_ID = type(uint16).max; + + /// @dev Deployments + uint8 public constant ORACLE_DECIMALS = 8; + string public constant ORACLE_SUFFIX = ' (USD)'; } diff --git a/tests/deployments/AaveV4BatchDeployment.t.sol b/tests/deployments/AaveV4BatchDeployment.t.sol new file mode 100644 index 000000000..4fdd41a5a --- /dev/null +++ b/tests/deployments/AaveV4BatchDeployment.t.sol @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/utils/BatchTestProcedures.sol'; + +contract AaveV4BatchDeploymentTest is BatchTestProcedures { + function setUp() public override { + super.setUp(); + + _inputs = FullDeployInputs({ + accessManagerAdmin: makeAddr('accessManagerAdmin'), + hubConfiguratorOwner: makeAddr('hubConfiguratorOwner'), + hubAdmin: makeAddr('hubAdmin'), + treasurySpokeOwner: makeAddr('treasurySpokeOwner'), + spokeProxyAdminOwner: makeAddr('spokeProxyAdminOwner'), + spokeConfiguratorOwner: makeAddr('spokeConfiguratorOwner'), + spokeAdmin: makeAddr('spokeAdmin'), + gatewayOwner: makeAddr('gatewayOwner'), + nativeWrapper: _weth9, + grantRoles: true, + hubLabels: _hubLabels, + spokeLabels: _spokeLabels, + salt: bytes32(0) + }); + } + + function testAaveV4BatchDeployment() public { + checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withoutRoles() public { + _inputs.grantRoles = false; + checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withoutNativeGateway() public { + _inputs.nativeWrapper = address(0); + checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withoutHubs() public { + _inputs.hubLabels = new string[](0); + checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withoutSpokes() public { + _inputs.spokeLabels = new string[](0); + checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withZeroAccessManagerAdmin_withRoles_reverts() public { + // only reverts if grantRoles is true, as access manager admin replaces deployer as default admin + _inputs.accessManagerAdmin = address(0); + _inputs.grantRoles = true; + + vm.expectRevert('invalid admin to add'); + this.checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withZeroAccessManagerAdmin_withoutRoles() public { + _inputs.accessManagerAdmin = address(0); + _inputs.grantRoles = false; + + checkedV4Deployment(); + } + + /// @dev Reverts as hubConfigurator is always deployed + /// and owners are needed on initial deployment + function testAaveV4BatchDeployment_withZeroHubConfiguratorOwner_reverts() public { + _inputs.hubConfiguratorOwner = address(0); + _inputs.grantRoles = vm.randomBool(); + + vm.expectRevert('invalid owner'); + this.checkedV4Deployment(); + } + + /// @dev Reverts as treasurySpoke is always deployed if hubs are being deployed + /// and owner is needed on initial deployment + function testAaveV4BatchDeployment_fuzz_withZeroTreasurySpokeOwner( + bool withoutHubs, + bool grantRoles + ) public { + _inputs.treasurySpokeOwner = address(0); + _inputs.grantRoles = grantRoles; + + if (withoutHubs) { + _inputs.hubLabels = new string[](0); + } + + (bool isExpectedError, bytes memory errorMessage) = _getExpectedError(); + if (isExpectedError) { + vm.expectRevert(errorMessage); + this.checkedV4Deployment(); + } else { + checkedV4Deployment(); + } + } + + function testAaveV4BatchDeployment_fuzz_withZeroSpokeProxyAdminOwner( + bool withoutSpokes, + bool grantRoles + ) public { + _inputs.spokeProxyAdminOwner = address(0); + _inputs.grantRoles = grantRoles; + if (withoutSpokes) { + _inputs.spokeLabels = new string[](0); + } + + (bool isExpectedError, bytes memory errorMessage) = _getExpectedError(); + if (isExpectedError) { + vm.expectRevert(errorMessage); + this.checkedV4Deployment(); + } else { + checkedV4Deployment(); + } + } + + function testAaveV4BatchDeployment_withZeroSpokeConfiguratorOwner_reverts() public { + _inputs.spokeConfiguratorOwner = address(0); + _inputs.grantRoles = vm.randomBool(); + + vm.expectRevert('invalid owner'); + this.checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withZeroSpokeConfiguratorOwner_withoutRoles_reverts() public { + _inputs.spokeConfiguratorOwner = address(0); + _inputs.grantRoles = false; + + vm.expectRevert('invalid owner'); + this.checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_fuzz_withoutRoles( + FullDeployInputs memory deployInputs, + address deployer, + bool withoutHubs, + bool withoutSpokes, + bool withoutNativeWrapper + ) public { + deployInputs.grantRoles = false; + if (withoutNativeWrapper) { + deployInputs.nativeWrapper = address(0); + } else { + deployInputs.nativeWrapper = _inputs.nativeWrapper; + } + if (withoutHubs) { + deployInputs.hubLabels = new string[](0); + } else { + deployInputs.hubLabels = _inputs.hubLabels; + } + if (withoutSpokes) { + deployInputs.spokeLabels = new string[](0); + } else { + deployInputs.spokeLabels = _inputs.spokeLabels; + } + _deployer = deployer; + _inputs = deployInputs; + + (bool isExpectedError, bytes memory errorMessage) = _getExpectedError(); + if (isExpectedError) { + vm.expectRevert(errorMessage); + this.checkedV4Deployment(); + } else { + checkedV4Deployment(); + } + } + + function testAaveV4BatchDeployment_fuzz_withRoles( + FullDeployInputs memory deployInputs, + address deployer, + bool withoutHubs, + bool withoutSpokes, + bool withoutNativeWrapper + ) public { + deployInputs.grantRoles = true; + if (withoutNativeWrapper) { + deployInputs.nativeWrapper = address(0); + } else { + deployInputs.nativeWrapper = _inputs.nativeWrapper; + } + deployInputs.nativeWrapper = _inputs.nativeWrapper; + if (withoutHubs) { + deployInputs.hubLabels = new string[](0); + } else { + deployInputs.hubLabels = _inputs.hubLabels; + } + if (withoutSpokes) { + deployInputs.spokeLabels = new string[](0); + } else { + deployInputs.spokeLabels = _inputs.spokeLabels; + } + _deployer = deployer; + _inputs = deployInputs; + + (bool isExpectedError, bytes memory errorMessage) = _getExpectedError(); + if (isExpectedError) { + vm.expectRevert(errorMessage); + this.checkedV4Deployment(); + } else { + checkedV4Deployment(); + } + } + + /// @dev Sanitized inputs should never fail when deploying + function testAaveV4BatchDeployment_fuzz_sanitizedInputs( + FullDeployInputs memory deployInputs + ) public { + deployInputs = _sanitizeInputs(deployInputs); + + assertNotEq(deployInputs.accessManagerAdmin, address(0)); + assertNotEq(deployInputs.hubConfiguratorOwner, address(0)); + assertNotEq(deployInputs.treasurySpokeOwner, address(0)); + assertNotEq(deployInputs.spokeProxyAdminOwner, address(0)); + assertNotEq(deployInputs.spokeConfiguratorOwner, address(0)); + assertNotEq(deployInputs.gatewayOwner, address(0)); + assertNotEq(deployInputs.accessManagerAdmin, address(0)); + assertNotEq(deployInputs.hubAdmin, address(0)); + assertNotEq(deployInputs.spokeAdmin, address(0)); + + checkedV4Deployment(); + } + + function _getExpectedError() + internal + view + returns (bool isExpectedError, bytes memory errorMessage) + { + // deployer is initial admin for access manager + if (_deployer == address(0)) return (true, bytes('invalid deployer')); + + // configurators always deployed + if (_inputs.spokeConfiguratorOwner == address(0)) { + return (true, bytes('invalid owner')); + } + if (_inputs.hubConfiguratorOwner == address(0)) { + return (true, bytes('invalid owner')); + } + + // gateways only when native wrapper is set + if (_inputs.nativeWrapper != address(0) && _inputs.gatewayOwner == address(0)) { + return (true, bytes('invalid owner')); + } + + // hubs require treasury owner when deployed + if (_inputs.hubLabels.length > 0 && _inputs.treasurySpokeOwner == address(0)) { + return (true, bytes('invalid owner')); + } + + // spokes require proxy admin owner when deployed + if (_inputs.spokeLabels.length > 0 && _inputs.spokeProxyAdminOwner == address(0)) { + return (true, bytes('invalid spoke proxy admin owner')); + } + + if (_inputs.grantRoles) { + if (_inputs.accessManagerAdmin == address(0)) { + return (true, bytes('invalid admin')); + } + if (_inputs.hubLabels.length > 0 && _inputs.hubAdmin == address(0)) { + return (true, bytes('invalid admin')); + } + if (_inputs.spokeLabels.length > 0 && _inputs.spokeAdmin == address(0)) { + return (true, bytes('invalid admin')); + } + } + } +} diff --git a/tests/deployments/batches/AaveV4AccessBatch.t.sol b/tests/deployments/batches/AaveV4AccessBatch.t.sol new file mode 100644 index 000000000..46d082f98 --- /dev/null +++ b/tests/deployments/batches/AaveV4AccessBatch.t.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/batches/BatchBase.t.sol'; + +contract AaveV4AccessBatchTest is BatchBaseTest { + AaveV4AccessBatch public aaveV4AccessBatch; + function setUp() public override { + super.setUp(); + aaveV4AccessBatch = new AaveV4AccessBatch(admin, salt); + } + + function test_getReport() public view { + BatchReports.AccessBatchReport memory report = aaveV4AccessBatch.getReport(); + assertNotEq(report.accessManager, address(0)); + + (bool hasRole, uint32 executionDelay) = IAccessManagerEnumerable(report.accessManager).hasRole( + Roles.DEFAULT_ADMIN_ROLE, + admin + ); + assertTrue(hasRole); + assertEq(executionDelay, 0); + } +} diff --git a/tests/deployments/batches/BatchBase.t.sol b/tests/deployments/batches/BatchBase.t.sol new file mode 100644 index 000000000..75a62b750 --- /dev/null +++ b/tests/deployments/batches/BatchBase.t.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {Test} from 'forge-std/Test.sol'; + +import {InputUtils} from 'src/deployments/utils/InputUtils.sol'; +import {Roles} from 'src/deployments/utils/libraries/Roles.sol'; +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; +import {AaveV4AccessBatch} from 'src/deployments/batches/AaveV4AccessBatch.sol'; +import {IAccessManagerEnumerable} from 'src/access/interfaces/IAccessManagerEnumerable.sol'; + +contract BatchBaseTest is Test, InputUtils { + address public admin = makeAddr('admin'); + bytes32 public salt; + function setUp() public virtual { + salt = keccak256('testSalt'); + _etchCreate2Factory(); + } +} diff --git a/tests/deployments/batches/TestTokensBatch.sol b/tests/deployments/batches/TestTokensBatch.sol new file mode 100644 index 000000000..82d5de510 --- /dev/null +++ b/tests/deployments/batches/TestTokensBatch.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; +import {ConfigData} from 'src/deployments/libraries/ConfigData.sol'; + +import {WETHDeployProcedure} from 'tests/deployments/procedures/WETHDeployProcedure.sol'; +import { + TestnetERC20DeployProcedure +} from 'tests/deployments/procedures/TestnetERC20DeployProcedure.sol'; +import {TestTypes} from 'tests/utils/TestTypes.sol'; + +contract TestTokensBatch is WETHDeployProcedure, TestnetERC20DeployProcedure { + TestTypes.TestTokensBatchReport internal _report; + + constructor(TestTypes.TestTokenInput[] memory inputs_) { + _report.tokens = new address[](inputs_.length); + _report.weth = _deployWETH(); + + for (uint256 i; i < inputs_.length; i++) { + TestTypes.TestTokenInput memory input = inputs_[i]; + address token = _deployTestnetERC20(input.name, input.symbol, input.decimals); + _report.tokens[i] = token; + } + } + + function getReport() external view returns (TestTypes.TestTokensBatchReport memory) { + return _report; + } +} diff --git a/tests/deployments/orchestration/AaveV4TestOrchestration.sol b/tests/deployments/orchestration/AaveV4TestOrchestration.sol new file mode 100644 index 000000000..5c7a3ca55 --- /dev/null +++ b/tests/deployments/orchestration/AaveV4TestOrchestration.sol @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'forge-std/Vm.sol'; + +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; +import {OrchestrationReports} from 'src/deployments/libraries/OrchestrationReports.sol'; +import {ConfigData} from 'src/deployments/libraries/ConfigData.sol'; + +import {AaveV4AccessBatch} from 'src/deployments/batches/AaveV4AccessBatch.sol'; +import {AaveV4HubBatch} from 'src/deployments/batches/AaveV4HubBatch.sol'; +import {AaveV4SpokeInstanceBatch} from 'src/deployments/batches/AaveV4SpokeInstanceBatch.sol'; + +import {TestTokensBatch} from 'tests/deployments/batches/TestTokensBatch.sol'; + +import { + AaveV4AccessManagerRolesProcedure +} from 'src/deployments/procedures/roles/AaveV4AccessManagerRolesProcedure.sol'; +import { + AaveV4HubRolesProcedure +} from 'src/deployments/procedures/roles/AaveV4HubRolesProcedure.sol'; +import { + AaveV4SpokeRolesProcedure +} from 'src/deployments/procedures/roles/AaveV4SpokeRolesProcedure.sol'; + +import { + AaveV4HubConfigProcedures +} from 'src/deployments/procedures/config/AaveV4HubConfigProcedures.sol'; +import { + AaveV4SpokeConfigProcedures +} from 'src/deployments/procedures/config/AaveV4SpokeConfigProcedures.sol'; + +import {AaveV4DeployBase} from 'src/deployments/orchestration/AaveV4DeployBase.sol'; + +import {TestTypes} from 'tests/utils/TestTypes.sol'; +import {WETH9} from 'src/dependencies/weth/WETH9.sol'; + +import {Constants} from 'tests/Constants.sol'; + +import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; + +library AaveV4TestOrchestration { + bool public constant IS_TEST = true; + Vm private constant vm = Vm(address(bytes20(uint160(uint256(keccak256('hevm cheat code')))))); + + function deployTestTokens( + TestTypes.TestTokenInput[] memory tokenInputs + ) external returns (TestTypes.TokenList memory) { + TestTypes.TestTokensReport memory tokensReport = _deployTestTokensBatch(tokenInputs); + + TestTypes.TokenList memory tokenList; + tokenList.weth = WETH9(payable(tokensReport.weth)); + tokenList.usdx = TestnetERC20(tokensReport.testTokens[0]); + tokenList.dai = TestnetERC20(tokensReport.testTokens[1]); + tokenList.wbtc = TestnetERC20(tokensReport.testTokens[2]); + tokenList.usdy = TestnetERC20(tokensReport.testTokens[3]); + tokenList.usdz = TestnetERC20(tokensReport.testTokens[4]); + return tokenList; + } + + function deployTestEnv( + address admin, + address treasuryAdmin, + address hubConfiguratorAdmin, + address spokeConfiguratorAdmin, + uint256 hubCount, + uint256 spokeCount, + address nativeWrapper, + bytes32 salt + ) external returns (TestTypes.TestEnvReport memory) { + TestTypes.TestEnvReport memory report; + + report.hubReports = new TestTypes.TestHubReport[](hubCount); + report.spokeReports = new TestTypes.TestSpokeReport[](spokeCount); + + // Deploy Access Batch + report.accessManager = AaveV4DeployBase.deployAccessBatch(admin, salt).accessManager; + + // Deploy Hub Batches + for (uint256 i; i < hubCount; ++i) { + BatchReports.HubBatchReport memory hubReport = AaveV4DeployBase.deployHubBatch({ + treasurySpokeOwner: treasuryAdmin, + accessManager: report.accessManager, + salt: keccak256(abi.encodePacked(salt, 'hub-', string(abi.encode(i)))) + }); + report.hubReports[i].hub = hubReport.hub; + report.hubReports[i].irStrategy = hubReport.irStrategy; + report.hubReports[i].treasurySpoke = hubReport.treasurySpoke; + } + + // Deploy Spoke Instance Batches + for (uint256 i; i < spokeCount; ++i) { + BatchReports.SpokeInstanceBatchReport memory spokeReport = AaveV4DeployBase + .deploySpokeInstanceBatch({ + spokeProxyAdminOwner: admin, + accessManager: report.accessManager, + oracleDecimals: Constants.ORACLE_DECIMALS, + oracleSuffix: Constants.ORACLE_SUFFIX, + label: string.concat('Spoke ', string(abi.encode(i)), Constants.ORACLE_SUFFIX), + salt: keccak256(abi.encodePacked(salt, 'spoke-', string(abi.encode(i)))) + }); + report.spokeReports[i].spoke = spokeReport.spokeProxy; + report.spokeReports[i].aaveOracle = spokeReport.aaveOracle; + } + + // Deploy Configurator Batches + BatchReports.ConfiguratorBatchReport memory configuratorReport = AaveV4DeployBase + .deployConfiguratorBatch({ + hubConfiguratorOwner: hubConfiguratorAdmin, + spokeConfiguratorOwner: spokeConfiguratorAdmin, + salt: keccak256(abi.encodePacked(salt, 'configurator')) + }); + report.configuratorReport.hubConfigurator = configuratorReport.hubConfigurator; + report.configuratorReport.spokeConfigurator = configuratorReport.spokeConfigurator; + + // Deploy Gateways Batch + BatchReports.GatewaysBatchReport memory gatewaysReport = AaveV4DeployBase.deployGatewaysBatch({ + owner: admin, + nativeWrapper: nativeWrapper, + salt: keccak256(abi.encodePacked(salt, 'gateways')) + }); + report.gatewaysReport.signatureGateway = gatewaysReport.signatureGateway; + report.gatewaysReport.nativeGateway = gatewaysReport.nativeGateway; + + return report; + } + + function configureHubsSpokes(ConfigData.AddSpokeParams[] memory paramsList) external { + for (uint256 i; i < paramsList.length; ++i) { + AaveV4HubConfigProcedures.addSpoke(paramsList[i]); + } + } + + function configureSpokes( + ConfigData.UpdateLiquidationConfigParams[] memory liquidationParamsList, + ConfigData.AddReserveParams[] memory reserveParamsList + ) external returns (TestTypes.SpokeReserveId[] memory) { + for (uint256 i; i < liquidationParamsList.length; ++i) { + AaveV4SpokeConfigProcedures.updateLiquidationConfig(liquidationParamsList[i]); + } + TestTypes.SpokeReserveId[] memory spokeReserveIds = new TestTypes.SpokeReserveId[]( + reserveParamsList.length + ); + for (uint256 i; i < reserveParamsList.length; ++i) { + spokeReserveIds[i] = TestTypes.SpokeReserveId({ + spoke: reserveParamsList[i].spoke, + reserveId: AaveV4SpokeConfigProcedures.addReserve(reserveParamsList[i]) + }); + } + return spokeReserveIds; + } + + function setRolesTestEnv(TestTypes.TestEnvReport memory report) public { + // Set Hub Roles + for (uint256 i; i < report.hubReports.length; ++i) { + AaveV4HubRolesProcedure.setupHubRoles(report.accessManager, report.hubReports[i].hub); + } + + // Set Spoke Roles + for (uint256 i; i < report.spokeReports.length; ++i) { + AaveV4SpokeRolesProcedure.setupSpokeRoles(report.accessManager, report.spokeReports[i].spoke); + } + } + + function grantRolesTestEnv( + TestTypes.TestEnvReport memory report, + address admin, + address hubAdmin, + address spokeAdmin + ) public { + grantHubRolesTestEnv(report, admin, hubAdmin); + grantSpokeRolesTestEnv(report, admin, spokeAdmin); + } + + function grantHubRolesTestEnv( + TestTypes.TestEnvReport memory report, + address admin, + address hubAdmin + ) public { + // grant Hub Admin roles + AaveV4HubRolesProcedure.grantHubAdminRole(report.accessManager, admin); + AaveV4HubRolesProcedure.grantHubAdminRole(report.accessManager, hubAdmin); + + // grant Hub Configurator roles + AaveV4HubRolesProcedure.grantHubConfiguratorRole( + report.accessManager, + report.configuratorReport.hubConfigurator + ); + } + + function grantSpokeRolesTestEnv( + TestTypes.TestEnvReport memory report, + address admin, + address spokeAdmin + ) public { + // grant Spoke roles + AaveV4SpokeRolesProcedure.grantSpokeAdminRole(report.accessManager, admin); + AaveV4SpokeRolesProcedure.grantSpokeAdminRole(report.accessManager, spokeAdmin); + + // grant Spoke Configurator roles + AaveV4SpokeRolesProcedure.grantSpokeConfiguratorRole( + report.accessManager, + report.configuratorReport.spokeConfigurator + ); + } + + function configureHubsAssets( + ConfigData.AddAssetParams[] memory paramsList + ) public returns (uint256[] memory) { + uint256[] memory assetIds = new uint256[](paramsList.length); + for (uint256 i; i < paramsList.length; ++i) { + assetIds[i] = AaveV4HubConfigProcedures.addAsset(paramsList[i]); + } + return assetIds; + } + + function configureHubsAssetsViaConfigurator( + ConfigData.AddAssetParams[] memory paramsList, + address hubConfigurator + ) public returns (uint256[] memory) { + uint256[] memory assetIds = new uint256[](paramsList.length); + for (uint256 i; i < paramsList.length; ++i) { + assetIds[i] = AaveV4HubConfigProcedures.addAssetViaConfigurator( + hubConfigurator, + paramsList[i] + ); + } + return assetIds; + } + + function _deployTestTokensBatch( + TestTypes.TestTokenInput[] memory tokenInputs + ) internal returns (TestTypes.TestTokensReport memory) { + TestTypes.TestTokensReport memory report; + + report.testTokens = new address[](tokenInputs.length); + + // Deploy Test Tokens Batch + TestTypes.TestTokensBatchReport memory tokensReport = _deployTokensBatch(tokenInputs); + report.weth = tokensReport.weth; + report.testTokens = tokensReport.tokens; + + return report; + } + + function _deployTokensBatch( + TestTypes.TestTokenInput[] memory tokenInputs + ) internal returns (TestTypes.TestTokensBatchReport memory) { + TestTokensBatch tokensBatch = new TestTokensBatch(tokenInputs); + return tokensBatch.getReport(); + } +} diff --git a/tests/deployments/procedures/ProceduresBase.t.sol b/tests/deployments/procedures/ProceduresBase.t.sol new file mode 100644 index 000000000..982be2a44 --- /dev/null +++ b/tests/deployments/procedures/ProceduresBase.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {Test} from 'forge-std/Test.sol'; + +import {Ownable} from 'src/dependencies/openzeppelin/Ownable.sol'; +import { + AaveV4HubConfiguratorDeployProcedureWrapper +} from 'tests/mocks/deployments/procedures/AaveV4HubConfiguratorDeployProcedureWrapper.sol'; +import { + AaveV4HubDeployProcedureWrapper +} from 'tests/mocks/deployments/procedures/AaveV4HubDeployProcedureWrapper.sol'; +import { + AaveV4InterestRateStrategyDeployProcedureWrapper +} from 'tests/mocks/deployments/procedures/AaveV4InterestRateStrategyDeployProcedureWrapper.sol'; +import { + AaveV4NativeTokenGatewayDeployProcedureWrapper +} from 'tests/mocks/deployments/procedures/AaveV4NativeTokenGatewayDeployProcedureWrapper.sol'; +import { + AaveV4SignatureGatewayDeployProcedureWrapper +} from 'tests/mocks/deployments/procedures/AaveV4SignatureGatewayDeployProcedureWrapper.sol'; +import { + AaveV4AccessManagerEnumerableDeployProcedureWrapper +} from 'tests/mocks/deployments/procedures/AaveV4AccessManagerEnumerableDeployProcedureWrapper.sol'; +import { + AaveV4AaveOracleDeployProcedureWrapper +} from 'tests/mocks/deployments/procedures/AaveV4AaveOracleDeployProcedureWrapper.sol'; +import { + AaveV4SpokeDeployProcedureWrapper +} from 'tests/mocks/deployments/procedures/AaveV4SpokeDeployProcedureWrapper.sol'; +import { + AaveV4TreasurySpokeDeployProcedureWrapper +} from 'tests/mocks/deployments/procedures/AaveV4TreasurySpokeDeployProcedureWrapper.sol'; +import { + AaveV4SpokeConfiguratorDeployProcedureWrapper +} from 'tests/mocks/deployments/procedures/AaveV4SpokeConfiguratorDeployProcedureWrapper.sol'; +import { + AaveV4AccessManagerRolesProcedureWrapper +} from 'tests/mocks/deployments/procedures/AaveV4AccessManagerRolesProcedureWrapper.sol'; +import { + AaveV4SpokeRolesProcedureWrapper +} from 'tests/mocks/deployments/procedures/AaveV4SpokeRolesProcedureWrapper.sol'; +import { + AaveV4HubRolesProcedureWrapper +} from 'tests/mocks/deployments/procedures/AaveV4HubRolesProcedureWrapper.sol'; + +import {AaveV4DeployProcedureBase} from 'src/deployments/procedures/AaveV4DeployProcedureBase.sol'; + +import {AaveOracle} from 'src/spoke/AaveOracle.sol'; +import {AccessManagerEnumerable} from 'src/access/AccessManagerEnumerable.sol'; +import {Roles} from 'src/deployments/utils/libraries/Roles.sol'; +import {ProxyHelper} from 'tests/utils/ProxyHelper.sol'; + +import {IHub} from 'src/hub/interfaces/IHub.sol'; +import {IAssetInterestRateStrategy} from 'src/hub/interfaces/IAssetInterestRateStrategy.sol'; +import {IAaveOracle} from 'src/spoke/interfaces/IAaveOracle.sol'; +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 {InputUtils} from 'src/deployments/utils/InputUtils.sol'; + +contract ProceduresBase is Test, InputUtils { + address public owner = makeAddr('owner'); + address public accessManager; + address public hub = makeAddr('hub'); + address public nativeWrapper = makeAddr('nativeWrapper'); + address public accessManagerAdmin = makeAddr('accessManagerAdmin'); + uint8 public oracleDecimals = 8; + string public oracleDescription = 'Oracle Description'; + address public spoke = makeAddr('spoke'); + address public aaveOracle; + address public treasurySpoke = makeAddr('treasurySpoke'); + address public admin = makeAddr('admin'); + bytes32 public salt; + function setUp() public virtual { + _etchCreate2Factory(); + + accessManager = address(new AccessManagerEnumerable(accessManagerAdmin)); + aaveOracle = address(new AaveOracle(spoke, oracleDecimals, oracleDescription)); + salt = keccak256('testSalt'); + } +} diff --git a/tests/deployments/procedures/TestnetERC20DeployProcedure.sol b/tests/deployments/procedures/TestnetERC20DeployProcedure.sol new file mode 100644 index 000000000..982f917ed --- /dev/null +++ b/tests/deployments/procedures/TestnetERC20DeployProcedure.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; + +contract TestnetERC20DeployProcedure { + function _deployTestnetERC20( + string memory name_, + string memory symbol_, + uint8 decimals_ + ) internal returns (address) { + address token = address( + new TestnetERC20({name_: name_, symbol_: symbol_, decimals_: decimals_}) + ); + + return token; + } +} diff --git a/tests/deployments/procedures/WETHDeployProcedure.sol b/tests/deployments/procedures/WETHDeployProcedure.sol new file mode 100644 index 000000000..d6d6b5e57 --- /dev/null +++ b/tests/deployments/procedures/WETHDeployProcedure.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {WETH9} from 'src/dependencies/weth/WETH9.sol'; + +contract WETHDeployProcedure { + function _deployWETH() internal returns (address) { + return address(new WETH9()); + } +} diff --git a/tests/deployments/procedures/deploy/AaveV4AccessManagerEnumerableDeployProcedure.t.sol b/tests/deployments/procedures/deploy/AaveV4AccessManagerEnumerableDeployProcedure.t.sol new file mode 100644 index 000000000..b9a086014 --- /dev/null +++ b/tests/deployments/procedures/deploy/AaveV4AccessManagerEnumerableDeployProcedure.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/procedures/ProceduresBase.t.sol'; + +contract AaveV4AccessManagerEnumerableDeployProcedureTest is ProceduresBase { + AaveV4AccessManagerEnumerableDeployProcedureWrapper + public aaveV4AccessManagerEnumerableDeployProcedureWrapper; + function setUp() public override { + super.setUp(); + aaveV4AccessManagerEnumerableDeployProcedureWrapper = new AaveV4AccessManagerEnumerableDeployProcedureWrapper(); + } + + function test_deployAccessManagerEnumerable() public { + address accessManagerEnumerable = aaveV4AccessManagerEnumerableDeployProcedureWrapper + .deployAccessManagerEnumerable(accessManagerAdmin, salt); + assertNotEq(accessManagerEnumerable, address(0)); + (bool hasRole, uint32 executionDelay) = IAccessManagerEnumerable(accessManagerEnumerable) + .hasRole( + uint64(AccessManagerEnumerable(accessManagerEnumerable).ADMIN_ROLE()), + accessManagerAdmin + ); + assertTrue(hasRole); + assertEq(executionDelay, 0); + } + + function test_deployAccessManagerEnumerable_reverts() public { + vm.expectRevert('invalid admin'); + aaveV4AccessManagerEnumerableDeployProcedureWrapper.deployAccessManagerEnumerable( + address(0), + salt + ); + } +} diff --git a/tests/deployments/procedures/deploy/hub/AaveV4HubConfiguratorDeployProcedure.t.sol b/tests/deployments/procedures/deploy/hub/AaveV4HubConfiguratorDeployProcedure.t.sol new file mode 100644 index 000000000..e3e2575d9 --- /dev/null +++ b/tests/deployments/procedures/deploy/hub/AaveV4HubConfiguratorDeployProcedure.t.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/procedures/ProceduresBase.t.sol'; + +contract AaveV4HubConfiguratorDeployProcedureTest is ProceduresBase { + AaveV4HubConfiguratorDeployProcedureWrapper public aaveV4HubConfiguratorDeployProcedureWrapper; + function setUp() public override { + super.setUp(); + aaveV4HubConfiguratorDeployProcedureWrapper = new AaveV4HubConfiguratorDeployProcedureWrapper(); + } + + function test_deployHubConfigurator() public { + address hubConfigurator = aaveV4HubConfiguratorDeployProcedureWrapper.deployHubConfigurator( + owner, + salt + ); + assertNotEq(hubConfigurator, address(0)); + assertEq(Ownable(hubConfigurator).owner(), owner); + } + + function test_deployHubConfigurator_reverts() public { + vm.expectRevert('invalid owner'); + aaveV4HubConfiguratorDeployProcedureWrapper.deployHubConfigurator({ + owner: address(0), + salt: salt + }); + } +} diff --git a/tests/deployments/procedures/deploy/hub/AaveV4HubDeployProcedure.t.sol b/tests/deployments/procedures/deploy/hub/AaveV4HubDeployProcedure.t.sol new file mode 100644 index 000000000..4e461d4ad --- /dev/null +++ b/tests/deployments/procedures/deploy/hub/AaveV4HubDeployProcedure.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/procedures/ProceduresBase.t.sol'; + +contract AaveV4HubDeployProcedureTest is ProceduresBase { + AaveV4HubDeployProcedureWrapper public aaveV4HubDeployProcedureWrapper; + function setUp() public override { + super.setUp(); + aaveV4HubDeployProcedureWrapper = new AaveV4HubDeployProcedureWrapper(); + } + + function test_deployHub() public { + address hub = aaveV4HubDeployProcedureWrapper.deployHub(accessManager, salt); + assertNotEq(hub, address(0)); + assertEq(IHub(hub).authority(), accessManager); + } + + function test_deployHub_reverts() public { + vm.expectRevert('invalid access manager'); + aaveV4HubDeployProcedureWrapper.deployHub({accessManager: address(0), salt: salt}); + } +} diff --git a/tests/deployments/procedures/deploy/hub/AaveV4InterestRateStrategyDeployProcedure.t.sol b/tests/deployments/procedures/deploy/hub/AaveV4InterestRateStrategyDeployProcedure.t.sol new file mode 100644 index 000000000..1bb54726f --- /dev/null +++ b/tests/deployments/procedures/deploy/hub/AaveV4InterestRateStrategyDeployProcedure.t.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/procedures/ProceduresBase.t.sol'; + +contract AaveV4InterestRateStrategyDeployProcedureTest is ProceduresBase { + AaveV4InterestRateStrategyDeployProcedureWrapper + public aaveV4InterestRateStrategyDeployProcedureWrapper; + function setUp() public override { + super.setUp(); + aaveV4InterestRateStrategyDeployProcedureWrapper = new AaveV4InterestRateStrategyDeployProcedureWrapper(); + } + + function test_deployInterestRateStrategy() public { + address interestRateStrategy = aaveV4InterestRateStrategyDeployProcedureWrapper + .deployInterestRateStrategy(hub, salt); + assertNotEq(interestRateStrategy, address(0)); + assertEq(IAssetInterestRateStrategy(interestRateStrategy).HUB(), hub); + } + + function test_deployInterestRateStrategy_reverts() public { + vm.expectRevert('invalid hub'); + aaveV4InterestRateStrategyDeployProcedureWrapper.deployInterestRateStrategy({ + hub: address(0), + salt: salt + }); + } +} diff --git a/tests/deployments/procedures/deploy/position-manager/AaveV4NativeTokenGatewayDeployProcedure.t.sol b/tests/deployments/procedures/deploy/position-manager/AaveV4NativeTokenGatewayDeployProcedure.t.sol new file mode 100644 index 000000000..53c58bc91 --- /dev/null +++ b/tests/deployments/procedures/deploy/position-manager/AaveV4NativeTokenGatewayDeployProcedure.t.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/procedures/ProceduresBase.t.sol'; + +contract AaveV4NativeTokenGatewayDeployProcedureTest is ProceduresBase { + AaveV4NativeTokenGatewayDeployProcedureWrapper + public aaveV4NativeTokenGatewayDeployProcedureWrapper; + function setUp() public override { + super.setUp(); + aaveV4NativeTokenGatewayDeployProcedureWrapper = new AaveV4NativeTokenGatewayDeployProcedureWrapper(); + } + + function test_deployHubConfigurator() public { + address nativeTokenGateway = aaveV4NativeTokenGatewayDeployProcedureWrapper + .deployNativeTokenGateway(nativeWrapper, owner, salt); + assertNotEq(nativeTokenGateway, address(0)); + assertEq(Ownable(nativeTokenGateway).owner(), owner); + } + + function test_deployNativeTokenGateway_reverts() public { + vm.expectRevert('invalid native wrapper'); + aaveV4NativeTokenGatewayDeployProcedureWrapper.deployNativeTokenGateway({ + nativeWrapper: address(0), + owner: owner, + salt: salt + }); + + vm.expectRevert('invalid owner'); + aaveV4NativeTokenGatewayDeployProcedureWrapper.deployNativeTokenGateway({ + nativeWrapper: nativeWrapper, + owner: address(0), + salt: salt + }); + } +} diff --git a/tests/deployments/procedures/deploy/position-manager/AaveV4SignatureGatewayDeployProcedure.t.sol b/tests/deployments/procedures/deploy/position-manager/AaveV4SignatureGatewayDeployProcedure.t.sol new file mode 100644 index 000000000..cf57b2e57 --- /dev/null +++ b/tests/deployments/procedures/deploy/position-manager/AaveV4SignatureGatewayDeployProcedure.t.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/procedures/ProceduresBase.t.sol'; + +contract AaveV4SignatureGatewayDeployProcedureTest is ProceduresBase { + AaveV4SignatureGatewayDeployProcedureWrapper public aaveV4SignatureGatewayDeployProcedureWrapper; + function setUp() public override { + super.setUp(); + + aaveV4SignatureGatewayDeployProcedureWrapper = new AaveV4SignatureGatewayDeployProcedureWrapper(); + } + + function test_deploySignatureGateway() public { + address signatureGateway = aaveV4SignatureGatewayDeployProcedureWrapper.deploySignatureGateway( + owner, + salt + ); + assertNotEq(signatureGateway, address(0)); + assertEq(Ownable(signatureGateway).owner(), owner); + } + + function test_deploySignatureGateway_reverts() public { + vm.expectRevert('invalid owner'); + aaveV4SignatureGatewayDeployProcedureWrapper.deploySignatureGateway(address(0), salt); + } +} diff --git a/tests/deployments/procedures/deploy/roles/AaveV4AccessManagerRolesProcedure.t.sol b/tests/deployments/procedures/deploy/roles/AaveV4AccessManagerRolesProcedure.t.sol new file mode 100644 index 000000000..dc5c3b9bb --- /dev/null +++ b/tests/deployments/procedures/deploy/roles/AaveV4AccessManagerRolesProcedure.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/procedures/ProceduresBase.t.sol'; + +contract AaveV4AccessManagerRolesProcedureTest is ProceduresBase { + AaveV4AccessManagerRolesProcedureWrapper public aaveV4AccessManagerRolesProcedureWrapper; + function setUp() public override { + super.setUp(); + aaveV4AccessManagerRolesProcedureWrapper = new AaveV4AccessManagerRolesProcedureWrapper(); + } + + function test_grantRootAdminRole() public { + address newAdmin = makeAddr('newAdmin'); + + _grantTmpRootAdminRole(newAdmin); + (bool hasRole, uint32 executionDelay) = IAccessManagerEnumerable(accessManager).hasRole( + Roles.DEFAULT_ADMIN_ROLE, + newAdmin + ); + assertTrue(hasRole); + assertEq(executionDelay, 0); + } + + function test_grantRootAdminRole_reverts() public { + address newAdmin = makeAddr('newAdmin'); + vm.expectRevert('invalid access manager'); + aaveV4AccessManagerRolesProcedureWrapper.grantRootAdminRole({ + accessManager: address(0), + adminToAdd: newAdmin, + adminToRemove: address(0) + }); + + vm.expectRevert('invalid admin to add'); + aaveV4AccessManagerRolesProcedureWrapper.grantRootAdminRole({ + accessManager: accessManager, + adminToAdd: address(0), + adminToRemove: newAdmin + }); + + vm.expectRevert('invalid admin to remove'); + aaveV4AccessManagerRolesProcedureWrapper.grantRootAdminRole({ + accessManager: accessManager, + adminToAdd: newAdmin, + adminToRemove: address(0) + }); + } + + /// @dev Grants a temporary root admin role to the wrapper contract to execute the procedure. + function _grantTmpRootAdminRole(address newAdmin) internal { + vm.startPrank(accessManagerAdmin); + IAccessManager(accessManager).grantRole( + Roles.DEFAULT_ADMIN_ROLE, + address(aaveV4AccessManagerRolesProcedureWrapper), + 0 + ); + aaveV4AccessManagerRolesProcedureWrapper.grantRootAdminRole({ + accessManager: accessManager, + adminToAdd: newAdmin, + adminToRemove: address(aaveV4AccessManagerRolesProcedureWrapper) + }); + vm.stopPrank(); + } +} diff --git a/tests/deployments/procedures/deploy/roles/AaveV4HubRolesProcedure.t.sol b/tests/deployments/procedures/deploy/roles/AaveV4HubRolesProcedure.t.sol new file mode 100644 index 000000000..7f0b391d2 --- /dev/null +++ b/tests/deployments/procedures/deploy/roles/AaveV4HubRolesProcedure.t.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/procedures/ProceduresBase.t.sol'; + +contract AaveV4HubRolesProcedureTest is ProceduresBase { + AaveV4HubRolesProcedureWrapper public aaveV4HubRolesProcedureWrapper; + function setUp() public override { + super.setUp(); + aaveV4HubRolesProcedureWrapper = new AaveV4HubRolesProcedureWrapper(); + } + + function test_grantHubAdminRole_reverts() public { + vm.expectRevert('invalid access manager'); + aaveV4HubRolesProcedureWrapper.grantHubAdminRole({accessManager: address(0), admin: admin}); + + vm.expectRevert('invalid admin'); + aaveV4HubRolesProcedureWrapper.grantHubAdminRole({ + accessManager: accessManager, + admin: address(0) + }); + } + + function test_grantHubFeeMinterRole_reverts() public { + vm.expectRevert('invalid access manager'); + aaveV4HubRolesProcedureWrapper.grantHubFeeMinterRole({accessManager: address(0), admin: admin}); + + vm.expectRevert('invalid admin'); + aaveV4HubRolesProcedureWrapper.grantHubFeeMinterRole({ + accessManager: accessManager, + admin: address(0) + }); + } + + function test_grantHubConfiguratorRole_reverts() public { + vm.expectRevert('invalid access manager'); + aaveV4HubRolesProcedureWrapper.grantHubConfiguratorRole({ + accessManager: address(0), + admin: admin + }); + + vm.expectRevert('invalid admin'); + aaveV4HubRolesProcedureWrapper.grantHubConfiguratorRole({ + accessManager: accessManager, + admin: address(0) + }); + } + + function test_setupHubRoles_reverts() public { + vm.expectRevert('invalid access manager'); + aaveV4HubRolesProcedureWrapper.setupHubRoles({accessManager: address(0), hub: hub}); + + vm.expectRevert('invalid hub'); + aaveV4HubRolesProcedureWrapper.setupHubRoles({accessManager: accessManager, hub: address(0)}); + } + + function test_setupHubFeeMinterRole_reverts() public { + vm.expectRevert('invalid access manager'); + aaveV4HubRolesProcedureWrapper.setupHubFeeMinterRole({accessManager: address(0), hub: hub}); + + vm.expectRevert('invalid hub'); + aaveV4HubRolesProcedureWrapper.setupHubFeeMinterRole({ + accessManager: accessManager, + hub: address(0) + }); + } + + function test_setupHubConfiguratorRole_reverts() public { + vm.expectRevert('invalid access manager'); + aaveV4HubRolesProcedureWrapper.setupHubConfiguratorRole({accessManager: address(0), hub: hub}); + + vm.expectRevert('invalid hub'); + aaveV4HubRolesProcedureWrapper.setupHubConfiguratorRole({ + accessManager: accessManager, + hub: address(0) + }); + } + + function test_getHubFeeMinterRoleSelectors() public view { + bytes4[] memory selectors = aaveV4HubRolesProcedureWrapper.getHubFeeMinterRoleSelectors(); + assertEq(selectors.length, 1); + assertEq(selectors[0], IHub.mintFeeShares.selector); + } + + function test_getHubConfiguratorRoleSelectors() public view { + bytes4[] memory selectors = aaveV4HubRolesProcedureWrapper.getHubConfiguratorRoleSelectors(); + assertEq(selectors.length, 5); + assertEq(selectors[0], IHub.addAsset.selector); + assertEq(selectors[1], IHub.updateAssetConfig.selector); + assertEq(selectors[2], IHub.addSpoke.selector); + assertEq(selectors[3], IHub.updateSpokeConfig.selector); + assertEq(selectors[4], IHub.setInterestRateData.selector); + } +} diff --git a/tests/deployments/procedures/deploy/roles/AaveV4SpokeRolesProcedure.t.sol b/tests/deployments/procedures/deploy/roles/AaveV4SpokeRolesProcedure.t.sol new file mode 100644 index 000000000..680e5543a --- /dev/null +++ b/tests/deployments/procedures/deploy/roles/AaveV4SpokeRolesProcedure.t.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/procedures/ProceduresBase.t.sol'; + +contract AaveV4SpokeRolesProcedureTest is ProceduresBase { + AaveV4SpokeRolesProcedureWrapper public aaveV4SpokeRolesProcedureWrapper; + function setUp() public override { + super.setUp(); + aaveV4SpokeRolesProcedureWrapper = new AaveV4SpokeRolesProcedureWrapper(); + } + + function test_grantSpokeAdminRole_reverts() public { + vm.expectRevert('invalid access manager'); + aaveV4SpokeRolesProcedureWrapper.grantSpokeAdminRole({accessManager: address(0), admin: admin}); + + vm.expectRevert('invalid admin'); + aaveV4SpokeRolesProcedureWrapper.grantSpokeAdminRole({ + accessManager: accessManager, + admin: address(0) + }); + } + + function test_grantSpokePositionUpdaterRole_reverts() public { + vm.expectRevert('invalid access manager'); + aaveV4SpokeRolesProcedureWrapper.grantSpokePositionUpdaterRole({ + accessManager: address(0), + admin: admin + }); + + vm.expectRevert('invalid admin'); + aaveV4SpokeRolesProcedureWrapper.grantSpokePositionUpdaterRole({ + accessManager: accessManager, + admin: address(0) + }); + } + + function test_grantSpokeConfiguratorRole_reverts() public { + vm.expectRevert('invalid access manager'); + aaveV4SpokeRolesProcedureWrapper.grantSpokeConfiguratorRole({ + accessManager: address(0), + admin: admin + }); + + vm.expectRevert('invalid admin'); + aaveV4SpokeRolesProcedureWrapper.grantSpokeConfiguratorRole({ + accessManager: accessManager, + admin: address(0) + }); + } + + function test_setupSpokeRoles_reverts() public { + vm.expectRevert('invalid access manager'); + aaveV4SpokeRolesProcedureWrapper.setupSpokeRoles({accessManager: address(0), spoke: spoke}); + + vm.expectRevert('invalid spoke'); + aaveV4SpokeRolesProcedureWrapper.setupSpokeRoles({ + accessManager: accessManager, + spoke: address(0) + }); + } + + function test_setupSpokePositionUpdaterRole_reverts() public { + vm.expectRevert('invalid access manager'); + aaveV4SpokeRolesProcedureWrapper.setupSpokePositionUpdaterRole({ + accessManager: address(0), + spoke: spoke + }); + + vm.expectRevert('invalid spoke'); + aaveV4SpokeRolesProcedureWrapper.setupSpokePositionUpdaterRole({ + accessManager: accessManager, + spoke: address(0) + }); + } + + function test_setupSpokeConfiguratorRole_reverts() public { + vm.expectRevert('invalid access manager'); + aaveV4SpokeRolesProcedureWrapper.setupSpokeConfiguratorRole({ + accessManager: address(0), + spoke: spoke + }); + + vm.expectRevert('invalid spoke'); + aaveV4SpokeRolesProcedureWrapper.setupSpokeConfiguratorRole({ + accessManager: accessManager, + spoke: address(0) + }); + } + + function test_getSpokePositionUpdaterRoleSelectors() public view { + bytes4[] memory selectors = aaveV4SpokeRolesProcedureWrapper + .getSpokePositionUpdaterRoleSelectors(); + assertEq(selectors.length, 2); + assertEq(selectors[0], ISpoke.updateUserDynamicConfig.selector); + assertEq(selectors[1], ISpoke.updateUserRiskPremium.selector); + } + + function test_getSpokeConfiguratorRoleSelectors() public view { + bytes4[] memory selectors = aaveV4SpokeRolesProcedureWrapper + .getSpokeConfiguratorRoleSelectors(); + assertEq(selectors.length, 7); + 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); + } +} diff --git a/tests/deployments/procedures/deploy/spoke/AaveV4AaveOracleDeployProcedure.t.sol b/tests/deployments/procedures/deploy/spoke/AaveV4AaveOracleDeployProcedure.t.sol new file mode 100644 index 000000000..5e9c9e54d --- /dev/null +++ b/tests/deployments/procedures/deploy/spoke/AaveV4AaveOracleDeployProcedure.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/procedures/ProceduresBase.t.sol'; + +contract AaveV4AaveOracleDeployProcedureTest is ProceduresBase { + AaveV4AaveOracleDeployProcedureWrapper public aaveV4AaveOracleDeployProcedureWrapper; + function setUp() public override { + super.setUp(); + aaveV4AaveOracleDeployProcedureWrapper = new AaveV4AaveOracleDeployProcedureWrapper(); + } + + function test_deployAaveOracle() public { + address aaveOracle = aaveV4AaveOracleDeployProcedureWrapper.deployAaveOracle( + spoke, + oracleDecimals, + oracleDescription, + salt + ); + assertNotEq(aaveOracle, address(0)); + assertEq(IAaveOracle(aaveOracle).DECIMALS(), oracleDecimals); + assertEq(IAaveOracle(aaveOracle).DESCRIPTION(), oracleDescription); + } + + function test_deployAaveOracle_reverts() public { + vm.expectRevert('invalid spoke'); + aaveV4AaveOracleDeployProcedureWrapper.deployAaveOracle({ + spoke: address(0), + decimals: oracleDecimals, + description: oracleDescription, + salt: salt + }); + + vm.expectRevert('invalid oracle decimals'); + aaveV4AaveOracleDeployProcedureWrapper.deployAaveOracle({ + spoke: spoke, + decimals: 0, + description: oracleDescription, + salt: salt + }); + + vm.expectRevert('invalid oracle description'); + aaveV4AaveOracleDeployProcedureWrapper.deployAaveOracle({ + spoke: spoke, + decimals: oracleDecimals, + description: '', + salt: salt + }); + } +} diff --git a/tests/deployments/procedures/deploy/spoke/AaveV4SpokeConfiguratorDeployProcedure.t.sol b/tests/deployments/procedures/deploy/spoke/AaveV4SpokeConfiguratorDeployProcedure.t.sol new file mode 100644 index 000000000..11aa24623 --- /dev/null +++ b/tests/deployments/procedures/deploy/spoke/AaveV4SpokeConfiguratorDeployProcedure.t.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/procedures/ProceduresBase.t.sol'; + +contract AaveV4SpokeConfiguratorDeployProcedureTest is ProceduresBase { + AaveV4SpokeConfiguratorDeployProcedureWrapper + public aaveV4SpokeConfiguratorDeployProcedureWrapper; + function setUp() public override { + super.setUp(); + aaveV4SpokeConfiguratorDeployProcedureWrapper = new AaveV4SpokeConfiguratorDeployProcedureWrapper(); + } + + function test_deploySpokeConfigurator() public { + address spokeConfigurator = aaveV4SpokeConfiguratorDeployProcedureWrapper + .deploySpokeConfigurator(owner, salt); + assertNotEq(spokeConfigurator, address(0)); + assertEq(Ownable(spokeConfigurator).owner(), owner); + } + + function test_deploySpokeConfigurator_reverts() public { + vm.expectRevert('invalid owner'); + aaveV4SpokeConfiguratorDeployProcedureWrapper.deploySpokeConfigurator(address(0), salt); + } +} diff --git a/tests/deployments/procedures/deploy/spoke/AaveV4SpokeDeployProcedure.t.sol b/tests/deployments/procedures/deploy/spoke/AaveV4SpokeDeployProcedure.t.sol new file mode 100644 index 000000000..4e8c8818d --- /dev/null +++ b/tests/deployments/procedures/deploy/spoke/AaveV4SpokeDeployProcedure.t.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/procedures/ProceduresBase.t.sol'; + +contract AaveV4SpokeDeployProcedureTest is ProceduresBase { + AaveV4SpokeDeployProcedureWrapper public aaveV4SpokeDeployProcedureWrapper; + function setUp() public override { + super.setUp(); + aaveV4SpokeDeployProcedureWrapper = new AaveV4SpokeDeployProcedureWrapper(); + } + + function test_deployUpgradableSpokeInstance() public { + (address spokeProxy, address spokeImplementation) = aaveV4SpokeDeployProcedureWrapper + .deployUpgradableSpokeInstance(owner, accessManager, aaveOracle, salt); + assertNotEq(spokeProxy, address(0)); + assertNotEq(spokeImplementation, address(0)); + assertEq(Ownable(ProxyHelper.getProxyAdmin(spokeProxy)).owner(), owner); + assertEq(ProxyHelper.getImplementation(spokeProxy), spokeImplementation); + assertEq(ISpoke(spokeProxy).ORACLE(), aaveOracle); + } + + function test_deployUpgradableSpokeInstance_reverts() public { + vm.expectRevert('invalid spoke proxy admin owner'); + aaveV4SpokeDeployProcedureWrapper.deployUpgradableSpokeInstance({ + spokeProxyAdminOwner: address(0), + accessManager: accessManager, + oracle: aaveOracle, + salt: salt + }); + + vm.expectRevert('invalid access manager'); + aaveV4SpokeDeployProcedureWrapper.deployUpgradableSpokeInstance({ + spokeProxyAdminOwner: owner, + accessManager: address(0), + oracle: aaveOracle, + salt: salt + }); + + vm.expectRevert('invalid oracle'); + aaveV4SpokeDeployProcedureWrapper.deployUpgradableSpokeInstance({ + spokeProxyAdminOwner: owner, + accessManager: accessManager, + oracle: address(0), + salt: salt + }); + } +} diff --git a/tests/deployments/procedures/deploy/spoke/AaveV4TreasurySpokeDeployProcedure.t.sol b/tests/deployments/procedures/deploy/spoke/AaveV4TreasurySpokeDeployProcedure.t.sol new file mode 100644 index 000000000..3463c4112 --- /dev/null +++ b/tests/deployments/procedures/deploy/spoke/AaveV4TreasurySpokeDeployProcedure.t.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/procedures/ProceduresBase.t.sol'; + +contract AaveV4TreasurySpokeDeployProcedureTest is ProceduresBase { + AaveV4TreasurySpokeDeployProcedureWrapper public aaveV4TreasurySpokeDeployProcedureWrapper; + function setUp() public override { + super.setUp(); + aaveV4TreasurySpokeDeployProcedureWrapper = new AaveV4TreasurySpokeDeployProcedureWrapper(); + } + + function test_deployTreasurySpoke() public { + address treasurySpoke = aaveV4TreasurySpokeDeployProcedureWrapper.deployTreasurySpoke( + owner, + hub, + salt + ); + assertNotEq(treasurySpoke, address(0)); + assertEq(Ownable(treasurySpoke).owner(), owner); + assertEq(address(ITreasurySpoke(treasurySpoke).HUB()), hub); + } + + function test_deployTreasurySpoke_reverts() public { + vm.expectRevert('invalid owner'); + aaveV4TreasurySpokeDeployProcedureWrapper.deployTreasurySpoke({ + owner: address(0), + hub: hub, + salt: salt + }); + + vm.expectRevert('invalid hub'); + aaveV4TreasurySpokeDeployProcedureWrapper.deployTreasurySpoke({ + owner: owner, + hub: address(0), + salt: salt + }); + } +} diff --git a/tests/deployments/utils/libraries/Create2Utils.t.sol b/tests/deployments/utils/libraries/Create2Utils.t.sol new file mode 100644 index 000000000..571174430 --- /dev/null +++ b/tests/deployments/utils/libraries/Create2Utils.t.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {Test} from 'forge-std/Test.sol'; +import { + Create2Utils, + Create2UtilsWrapper +} from 'tests/mocks/deployments/libraries/Create2UtilsWrapper.sol'; +import {InputUtils} from 'src/deployments/utils/InputUtils.sol'; +import { + TransparentUpgradeableProxy +} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; + +contract Dummy { + constructor() {} +} + +contract Create2UtilsTest is Test, InputUtils { + Create2UtilsWrapper internal _harness; + function setUp() public { + _harness = new Create2UtilsWrapper(); + } + function testCreate2Deploy_revertsWith_missingCreate2Factory() public { + vm.expectRevert(Create2Utils.missingCreate2Factory.selector); + _harness.create2Deploy(bytes32(0), type(Dummy).creationCode); + } + + function testCreate2Deploy_revertsWith_create2AddressDerivationFailure(bytes32 salt) public { + vm.assume(salt != bytes32(0)); + vm.etch( + Create2Utils.CREATE2_FACTORY, + hex'600060005260206000f3' // runtime: mstore(0,0); return(0,32) + ); + bytes memory bytecode = type(Dummy).creationCode; + vm.expectRevert(Create2Utils.create2AddressDerivationFailure.selector); + _harness.create2Deploy(salt, bytecode); + } + + function testCreate2Deploy_revertsWith_failedCreate2FactoryCall(bytes32 salt) public { + vm.assume(salt != bytes32(0)); + _etchCreate2Factory(); + bytes memory bytecode = hex'fd'; + vm.expectRevert(Create2Utils.failedCreate2FactoryCall.selector); + _harness.create2Deploy(salt, bytecode); + } + + function testCreate2Deploy_revertsWith_contractAlreadyDeployed(bytes32 salt) public { + vm.assume(salt != bytes32(0)); + _etchCreate2Factory(); + bytes memory bytecode = type(Dummy).creationCode; + _harness.create2Deploy(salt, bytecode); + + // after already deployed, it should now revert + vm.expectRevert(Create2Utils.contractAlreadyDeployed.selector); + _harness.create2Deploy(salt, bytecode); + } + + function testCreate2Deploy_fuzz(bytes32 salt) public { + vm.assume(salt != bytes32(0)); + _etchCreate2Factory(); + bytes memory bytecode = type(Dummy).creationCode; + + assertEq( + _harness.create2Deploy(salt, bytecode), + _harness.computeCreate2Address(salt, keccak256(bytecode)) + ); + } + + function testProxify_fuzz(bytes32 salt, address initialOwner) public { + vm.assume(salt != bytes32(0)); + vm.assume(initialOwner != address(0)); + _etchCreate2Factory(); + address logic = address(new Dummy()); + bytes memory initData = bytes(''); + assertEq( + _harness.proxify(salt, logic, initialOwner, initData), + _harness.computeCreate2Address( + salt, + keccak256( + abi.encodePacked( + type(TransparentUpgradeableProxy).creationCode, + abi.encode(logic, initialOwner, initData) + ) + ) + ) + ); + } + + function testIsContractDeployed_fuzz(address addr) public view { + vm.assume(addr != address(0)); + assumeUnusedAddress(addr); + assertFalse(_harness.isContractDeployed(addr)); + } + + function testIsContractDeployed() public { + address deployed = address(new Dummy()); + assertTrue(_harness.isContractDeployed(deployed)); + } + + function testComputeCreateAddress_revertsWith_nonceNotSupported( + address deployer, + uint8 nonce + ) public { + vm.assume(deployer != address(0)); + vm.assume(nonce >= 0x80); + vm.expectRevert(Create2Utils.nonceNotSupported.selector); + _harness.computeCreateAddress(deployer, nonce); + } + + function testComputeCreateAddress_fuzz(address deployer, uint8 nonce) public view { + vm.assume(deployer != address(0)); + vm.assume(nonce < 0x80); + address expected = vm.computeCreateAddress(deployer, nonce); + assertEq(_harness.computeCreateAddress(deployer, nonce), expected); + } + + function testComputeCreate2Address_fuzz(bytes32 salt, bytes32 initcode) public view { + vm.assume(salt != bytes32(0)); + vm.assume(initcode != bytes32(0)); + address expected = _harness.computeCreate2Address(salt, initcode); + assertEq(_harness.computeCreate2Address(salt, initcode), expected); + } + + function testComputeCreate2Address_fuzz(bytes32 salt, bytes memory bytecode) public view { + vm.assume(salt != bytes32(0)); + vm.assume(bytecode.length > 0); + address expected = _harness.computeCreate2Address(salt, keccak256(abi.encodePacked(bytecode))); + assertEq(_harness.computeCreate2Address(salt, bytecode), expected); + } + + function testAddressFromLast20Bytes_fuzz(bytes32 bytesValue) public view { + vm.assume(bytesValue != bytes32(0)); + assertEq(_harness.addressFromLast20Bytes(bytesValue), address(uint160(uint256(bytesValue)))); + } +} diff --git a/tests/gas/Base.gas.t.sol b/tests/gas/Base.gas.t.sol new file mode 100644 index 000000000..d85199dca --- /dev/null +++ b/tests/gas/Base.gas.t.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/Base.t.sol'; + +/// forge-config: default.isolate = true +contract BaseGasTest is Base { + function setUp() public virtual override { + super.setUp(); + _initEnvironment(); + } +} diff --git a/tests/gas/Gateways.Operations.gas.t.sol b/tests/gas/Gateways.Operations.gas.t.sol index c2ab0743f..f19eeb5c8 100644 --- a/tests/gas/Gateways.Operations.gas.t.sol +++ b/tests/gas/Gateways.Operations.gas.t.sol @@ -2,19 +2,17 @@ // Copyright (c) 2025 Aave Labs pragma solidity ^0.8.0; -import 'tests/Base.t.sol'; +import 'tests/gas/Base.gas.t.sol'; import 'tests/unit/misc/SignatureGateway/SignatureGateway.Base.t.sol'; /// forge-config: default.isolate = true -contract NativeTokenGateway_Gas_Tests is Base { +contract NativeTokenGateway_Gas_Tests is BaseGasTest { string internal NAMESPACE = 'NativeTokenGateway.Operations'; NativeTokenGateway public nativeTokenGateway; function setUp() public virtual override { super.setUp(); - initEnvironment(); - nativeTokenGateway = new NativeTokenGateway(address(tokenList.weth), address(ADMIN)); vm.prank(SPOKE_ADMIN); diff --git a/tests/gas/Hub.Operations.gas.t.sol b/tests/gas/Hub.Operations.gas.t.sol index 6723966cd..b1fd17805 100644 --- a/tests/gas/Hub.Operations.gas.t.sol +++ b/tests/gas/Hub.Operations.gas.t.sol @@ -2,18 +2,13 @@ // Copyright (c) 2025 Aave Labs pragma solidity ^0.8.0; -import 'tests/Base.t.sol'; +import 'tests/gas/Base.gas.t.sol'; /// forge-config: default.isolate = true -contract HubOperations_Gas_Tests is Base { +contract HubOperations_Gas_Tests is BaseGasTest { using SafeCast for *; using WadRayMath for uint256; - function setUp() public override { - deployFixtures(); - initEnvironment(); - } - function test_add() public { vm.startPrank(address(spoke1)); tokenList.usdx.transferFrom(alice, address(hub1), 1000e6); diff --git a/tests/gas/Spoke.Getters.gas.t.sol b/tests/gas/Spoke.Getters.gas.t.sol index 31c23b8f5..ccfb30f86 100644 --- a/tests/gas/Spoke.Getters.gas.t.sol +++ b/tests/gas/Spoke.Getters.gas.t.sol @@ -2,15 +2,10 @@ // Copyright (c) 2025 Aave Labs pragma solidity ^0.8.0; -import 'tests/Base.t.sol'; +import 'tests/gas/Base.gas.t.sol'; /// forge-config: default.isolate = true -contract SpokeGetters_Gas_Tests is Base { - function setUp() public override { - deployFixtures(); - initEnvironment(); - } - +contract SpokeGetters_Gas_Tests is BaseGasTest { function test_getUserAccountData() external { spoke1.getUserAccountData(alice); vm.snapshotGasLastCall('Spoke.Getters', 'getUserAccountData: supplies: 0, borrows: 0'); diff --git a/tests/gas/Spoke.Operations.gas.t.sol b/tests/gas/Spoke.Operations.gas.t.sol index aa6d2b8d9..78ca96303 100644 --- a/tests/gas/Spoke.Operations.gas.t.sol +++ b/tests/gas/Spoke.Operations.gas.t.sol @@ -11,8 +11,7 @@ contract SpokeOperations_Gas_Tests is SpokeBase { ISpoke internal spoke; function setUp() public virtual override { - deployFixtures(); - initEnvironment(); + super.setUp(); spoke = spoke1; reserveId = _getReserveIds(spoke); _seed(); diff --git a/tests/mocks/GatewayBaseWrapper.sol b/tests/mocks/GatewayBaseWrapper.sol index e35515108..5821be244 100644 --- a/tests/mocks/GatewayBaseWrapper.sol +++ b/tests/mocks/GatewayBaseWrapper.sol @@ -5,5 +5,7 @@ pragma solidity 0.8.28; import {GatewayBase} from 'src/position-manager/GatewayBase.sol'; contract GatewayBaseWrapper is GatewayBase { + bool public IS_TEST = true; + constructor(address initialOwner_) GatewayBase(initialOwner_) {} } diff --git a/tests/mocks/LiquidationLogicWrapper.sol b/tests/mocks/LiquidationLogicWrapper.sol index 8a765e6be..3a06ef84c 100644 --- a/tests/mocks/LiquidationLogicWrapper.sol +++ b/tests/mocks/LiquidationLogicWrapper.sol @@ -12,6 +12,8 @@ import {LiquidationLogic} from 'src/spoke/libraries/LiquidationLogic.sol'; import {ReserveFlags, ReserveFlagsMap} from 'src/spoke/libraries/ReserveFlagsMap.sol'; contract LiquidationLogicWrapper { + bool public IS_TEST = true; + using SafeCast for *; using SafeERC20 for IERC20; using PositionStatusMap for ISpoke.PositionStatus; diff --git a/tests/mocks/deployments/libraries/Create2UtilsWrapper.sol b/tests/mocks/deployments/libraries/Create2UtilsWrapper.sol new file mode 100644 index 000000000..3349407be --- /dev/null +++ b/tests/mocks/deployments/libraries/Create2UtilsWrapper.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.20; + +import { + TransparentUpgradeableProxy +} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; +import {SpokeInstance} from 'src/spoke/instances/SpokeInstance.sol'; +import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; + +contract Create2UtilsWrapper { + function isContractDeployed(address addr) external view returns (bool) { + return Create2Utils.isContractDeployed(addr); + } + + function create2Deploy(bytes32 salt, bytes memory bytecode) external returns (address) { + return Create2Utils.create2Deploy(salt, bytecode); + } + + function proxify( + bytes32 salt, + address logic, + address initialOwner, + bytes memory data + ) external returns (address) { + return Create2Utils.proxify(salt, logic, initialOwner, data); + } + + function computeCreateAddress(address deployer, uint8 nonce) external pure returns (address) { + return Create2Utils.computeCreateAddress(deployer, nonce); + } + + function computeCreate2Address( + bytes32 salt, + bytes32 initcodeHash + ) external pure returns (address) { + return Create2Utils.computeCreate2Address(salt, initcodeHash); + } + + function computeCreate2Address( + bytes32 salt, + bytes memory bytecode + ) external pure returns (address) { + return Create2Utils.computeCreate2Address(salt, bytecode); + } + + function addressFromLast20Bytes(bytes32 bytesValue) external pure returns (address) { + return Create2Utils.addressFromLast20Bytes(bytesValue); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4AaveOracleDeployProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4AaveOracleDeployProcedureWrapper.sol new file mode 100644 index 000000000..a31f0a25b --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4AaveOracleDeployProcedureWrapper.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import { + AaveV4AaveOracleDeployProcedure +} from 'src/deployments/procedures/deploy/spoke/AaveV4AaveOracleDeployProcedure.sol'; + +contract AaveV4AaveOracleDeployProcedureWrapper is AaveV4AaveOracleDeployProcedure { + bool public IS_TEST = true; + + function deployAaveOracle( + address spoke, + uint8 decimals, + string memory description, + bytes32 salt + ) external returns (address) { + return _deployAaveOracle(spoke, decimals, description, salt); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4AccessManagerEnumerableDeployProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4AccessManagerEnumerableDeployProcedureWrapper.sol new file mode 100644 index 000000000..67fe7d353 --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4AccessManagerEnumerableDeployProcedureWrapper.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import { + AaveV4AccessManagerEnumerableDeployProcedure +} from 'src/deployments/procedures/deploy/AaveV4AccessManagerEnumerableDeployProcedure.sol'; + +contract AaveV4AccessManagerEnumerableDeployProcedureWrapper is + AaveV4AccessManagerEnumerableDeployProcedure +{ + bool public IS_TEST = true; + function deployAccessManagerEnumerable(address admin, bytes32 salt) external returns (address) { + return _deployAccessManagerEnumerable(admin, salt); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4AccessManagerRolesProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4AccessManagerRolesProcedureWrapper.sol new file mode 100644 index 000000000..994fac71e --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4AccessManagerRolesProcedureWrapper.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import { + AaveV4AccessManagerRolesProcedure +} from 'src/deployments/procedures/roles/AaveV4AccessManagerRolesProcedure.sol'; + +contract AaveV4AccessManagerRolesProcedureWrapper { + bool public IS_TEST = true; + + function grantRootAdminRole( + address accessManager, + address adminToAdd, + address adminToRemove + ) external { + AaveV4AccessManagerRolesProcedure.replaceDefaultAdminRole( + accessManager, + adminToAdd, + adminToRemove + ); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4HubConfiguratorDeployProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4HubConfiguratorDeployProcedureWrapper.sol new file mode 100644 index 000000000..4c908f675 --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4HubConfiguratorDeployProcedureWrapper.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import { + AaveV4HubConfiguratorDeployProcedure +} from 'src/deployments/procedures/deploy/hub/AaveV4HubConfiguratorDeployProcedure.sol'; + +contract AaveV4HubConfiguratorDeployProcedureWrapper is AaveV4HubConfiguratorDeployProcedure { + bool public IS_TEST = true; + + function deployHubConfigurator(address owner, bytes32 salt) external returns (address) { + return _deployHubConfigurator(owner, salt); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4HubDeployProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4HubDeployProcedureWrapper.sol new file mode 100644 index 000000000..1f576abd4 --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4HubDeployProcedureWrapper.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import { + AaveV4HubDeployProcedure +} from 'src/deployments/procedures/deploy/hub/AaveV4HubDeployProcedure.sol'; + +contract AaveV4HubDeployProcedureWrapper is AaveV4HubDeployProcedure { + bool public IS_TEST = true; + + function deployHub(address accessManager, bytes32 salt) external returns (address) { + return _deployHub(accessManager, salt); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4HubRolesProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4HubRolesProcedureWrapper.sol new file mode 100644 index 000000000..17b3a8d5c --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4HubRolesProcedureWrapper.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import { + AaveV4HubRolesProcedure +} from 'src/deployments/procedures/roles/AaveV4HubRolesProcedure.sol'; + +contract AaveV4HubRolesProcedureWrapper { + bool public IS_TEST = true; + + function grantHubAdminRole(address accessManager, address admin) external { + AaveV4HubRolesProcedure.grantHubAdminRole(accessManager, admin); + } + + function grantHubFeeMinterRole(address accessManager, address admin) external { + AaveV4HubRolesProcedure.grantHubFeeMinterRole(accessManager, admin); + } + + function grantHubConfiguratorRole(address accessManager, address admin) external { + AaveV4HubRolesProcedure.grantHubConfiguratorRole(accessManager, admin); + } + + function setupHubRoles(address accessManager, address hub) external { + AaveV4HubRolesProcedure.setupHubRoles(accessManager, hub); + } + + function setupHubFeeMinterRole(address accessManager, address hub) external { + AaveV4HubRolesProcedure.setupHubFeeMinterRole(accessManager, hub); + } + + function setupHubConfiguratorRole(address accessManager, address hub) external { + AaveV4HubRolesProcedure.setupHubConfiguratorRole(accessManager, hub); + } + + function getHubFeeMinterRoleSelectors() external pure returns (bytes4[] memory) { + return AaveV4HubRolesProcedure.getHubFeeMinterRoleSelectors(); + } + + function getHubConfiguratorRoleSelectors() external pure returns (bytes4[] memory) { + return AaveV4HubRolesProcedure.getHubConfiguratorRoleSelectors(); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4InterestRateStrategyDeployProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4InterestRateStrategyDeployProcedureWrapper.sol new file mode 100644 index 000000000..43b21854c --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4InterestRateStrategyDeployProcedureWrapper.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import { + AaveV4InterestRateStrategyDeployProcedure +} from 'src/deployments/procedures/deploy/hub/AaveV4InterestRateStrategyDeployProcedure.sol'; + +contract AaveV4InterestRateStrategyDeployProcedureWrapper is + AaveV4InterestRateStrategyDeployProcedure +{ + bool public IS_TEST = true; + + function deployInterestRateStrategy(address hub, bytes32 salt) external returns (address) { + return _deployInterestRateStrategy(hub, salt); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4NativeTokenGatewayDeployProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4NativeTokenGatewayDeployProcedureWrapper.sol new file mode 100644 index 000000000..27ef5886b --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4NativeTokenGatewayDeployProcedureWrapper.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import { + AaveV4NativeTokenGatewayDeployProcedure +} from 'src/deployments/procedures/deploy/position-manager/AaveV4NativeTokenGatewayDeployProcedure.sol'; + +contract AaveV4NativeTokenGatewayDeployProcedureWrapper is AaveV4NativeTokenGatewayDeployProcedure { + bool public IS_TEST = true; + + function deployNativeTokenGateway( + address nativeWrapper, + address owner, + bytes32 salt + ) external returns (address) { + return _deployNativeTokenGateway(nativeWrapper, owner, salt); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4SignatureGatewayDeployProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4SignatureGatewayDeployProcedureWrapper.sol new file mode 100644 index 000000000..fadc7cc84 --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4SignatureGatewayDeployProcedureWrapper.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import { + AaveV4SignatureGatewayDeployProcedure +} from 'src/deployments/procedures/deploy/position-manager/AaveV4SignatureGatewayDeployProcedure.sol'; + +contract AaveV4SignatureGatewayDeployProcedureWrapper is AaveV4SignatureGatewayDeployProcedure { + bool public IS_TEST = true; + + function deploySignatureGateway(address owner, bytes32 salt) external returns (address) { + return _deploySignatureGateway(owner, salt); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4SpokeConfiguratorDeployProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4SpokeConfiguratorDeployProcedureWrapper.sol new file mode 100644 index 000000000..20b596fe1 --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4SpokeConfiguratorDeployProcedureWrapper.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import { + AaveV4SpokeConfiguratorDeployProcedure +} from 'src/deployments/procedures/deploy/spoke/AaveV4SpokeConfiguratorDeployProcedure.sol'; + +contract AaveV4SpokeConfiguratorDeployProcedureWrapper is AaveV4SpokeConfiguratorDeployProcedure { + bool public IS_TEST = true; + + function deploySpokeConfigurator(address owner, bytes32 salt) external returns (address) { + return _deploySpokeConfigurator(owner, salt); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4SpokeDeployProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4SpokeDeployProcedureWrapper.sol new file mode 100644 index 000000000..03e0640c9 --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4SpokeDeployProcedureWrapper.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import { + AaveV4SpokeDeployProcedure +} from 'src/deployments/procedures/deploy/spoke/AaveV4SpokeDeployProcedure.sol'; + +contract AaveV4SpokeDeployProcedureWrapper is AaveV4SpokeDeployProcedure { + bool public IS_TEST = true; + + function deployUpgradableSpokeInstance( + address spokeProxyAdminOwner, + address accessManager, + address oracle, + bytes32 salt + ) external returns (address spokeProxy, address spokeImplementation) { + return _deployUpgradableSpokeInstance(spokeProxyAdminOwner, accessManager, oracle, salt); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4SpokeRolesProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4SpokeRolesProcedureWrapper.sol new file mode 100644 index 000000000..93d95e363 --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4SpokeRolesProcedureWrapper.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import { + AaveV4SpokeRolesProcedure +} from 'src/deployments/procedures/roles/AaveV4SpokeRolesProcedure.sol'; + +contract AaveV4SpokeRolesProcedureWrapper { + bool public IS_TEST = true; + + function grantSpokeAdminRole(address accessManager, address admin) external { + AaveV4SpokeRolesProcedure.grantSpokeAdminRole(accessManager, admin); + } + + function grantSpokePositionUpdaterRole(address accessManager, address admin) external { + AaveV4SpokeRolesProcedure.grantSpokePositionUpdaterRole(accessManager, admin); + } + + function grantSpokeConfiguratorRole(address accessManager, address admin) external { + AaveV4SpokeRolesProcedure.grantSpokeConfiguratorRole(accessManager, admin); + } + + function setupSpokeRoles(address accessManager, address spoke) external { + AaveV4SpokeRolesProcedure.setupSpokeRoles(accessManager, spoke); + } + + function setupSpokePositionUpdaterRole(address accessManager, address spoke) external { + AaveV4SpokeRolesProcedure.setupSpokePositionUpdaterRole(accessManager, spoke); + } + + function setupSpokeConfiguratorRole(address accessManager, address spoke) external { + AaveV4SpokeRolesProcedure.setupSpokeConfiguratorRole(accessManager, spoke); + } + function getSpokePositionUpdaterRoleSelectors() external pure returns (bytes4[] memory) { + return AaveV4SpokeRolesProcedure.getSpokePositionUpdaterRoleSelectors(); + } + + function getSpokeConfiguratorRoleSelectors() external pure returns (bytes4[] memory) { + return AaveV4SpokeRolesProcedure.getSpokeConfiguratorRoleSelectors(); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4TreasurySpokeDeployProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4TreasurySpokeDeployProcedureWrapper.sol new file mode 100644 index 000000000..4438ee36c --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4TreasurySpokeDeployProcedureWrapper.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import { + AaveV4TreasurySpokeDeployProcedure +} from 'src/deployments/procedures/deploy/spoke/AaveV4TreasurySpokeDeployProcedure.sol'; + +contract AaveV4TreasurySpokeDeployProcedureWrapper is AaveV4TreasurySpokeDeployProcedure { + bool public IS_TEST = true; + + function deployTreasurySpoke( + address owner, + address hub, + bytes32 salt + ) external returns (address) { + return _deployTreasurySpoke(owner, hub, salt); + } +} diff --git a/tests/scripts/AaveV4DeployBatchBaseScript.t.sol b/tests/scripts/AaveV4DeployBatchBaseScript.t.sol new file mode 100644 index 000000000..63e14bf5a --- /dev/null +++ b/tests/scripts/AaveV4DeployBatchBaseScript.t.sol @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {Test} from 'forge-std/Test.sol'; +import {AaveV4DeployBatchBaseScript} from 'scripts/deploy/AaveV4DeployBatchBase.s.sol'; +import {MetadataLogger} from 'src/deployments/utils/MetadataLogger.sol'; +import {InputUtils} from 'src/deployments/utils/InputUtils.sol'; +import {WETH9} from 'src/dependencies/weth/WETH9.sol'; + +contract AaveV4DeployBatchBaseScriptHarness is AaveV4DeployBatchBaseScript { + constructor() AaveV4DeployBatchBaseScript('in.json', 'out.json') {} + + function loadWarningsAndSanitizeInputs( + MetadataLogger logger, + InputUtils.FullDeployInputs memory inputs, + address deployer + ) public returns (InputUtils.FullDeployInputs memory) { + return _loadWarningsAndSanitizeInputs(logger, inputs, deployer); + } + + function logAndAppend(MetadataLogger logger, string memory warning) public { + _logAndAppend(logger, warning); + } + + function _executeUserPrompt() internal override {} +} + +contract AaveV4DeployBatchBaseScriptTest is Test { + AaveV4DeployBatchBaseScriptHarness internal _harness; + InputUtils.FullDeployInputs internal _inputs; + MetadataLogger internal _logger; + address internal _deployer; + + function setUp() public { + _harness = new AaveV4DeployBatchBaseScriptHarness(); + + _inputs.hubLabels = ['hub1', 'hub2', 'hub3']; + _inputs.spokeLabels = ['spoke1', 'spoke2', 'spoke3']; + _inputs.accessManagerAdmin = makeAddr('accessManagerAdmin'); + _inputs.hubAdmin = makeAddr('hubAdmin'); + _inputs.hubConfiguratorOwner = makeAddr('hubConfiguratorOwner'); + _inputs.treasurySpokeOwner = makeAddr('treasurySpokeOwner'); + _inputs.spokeAdmin = makeAddr('spokeAdmin'); + _inputs.spokeProxyAdminOwner = makeAddr('spokeProxyAdminOwner'); + _inputs.spokeConfiguratorOwner = makeAddr('spokeConfiguratorOwner'); + _inputs.gatewayOwner = makeAddr('gatewayOwner'); + _inputs.nativeWrapper = address(new WETH9()); + _inputs.grantRoles = true; + + _logger = new MetadataLogger('dummy/path'); + _deployer = makeAddr('deployer'); + } + + function test_loadWarningsAndSanitizeInputs() public { + InputUtils.FullDeployInputs memory expected = _inputs; + InputUtils.FullDeployInputs memory sanitized = _harness.loadWarningsAndSanitizeInputs( + _logger, + _inputs, + _deployer + ); + assertEq(sanitized, expected); + } + + function test_loadWarningsAndSanitizeInputs_withZeroAccessManagerAdmin_fuzz( + bool grantRoles + ) public { + _inputs.accessManagerAdmin = address(0); + _inputs.grantRoles = grantRoles; + InputUtils.FullDeployInputs memory sanitized = _harness.loadWarningsAndSanitizeInputs( + _logger, + _inputs, + _deployer + ); + InputUtils.FullDeployInputs memory expected = _inputs; + if (grantRoles) { + expected.accessManagerAdmin = _deployer; + } + assertEq(sanitized, expected); + } + + function test_loadWarningsAndSanitizeInputs_withZeroHubAdmin_fuzz(bool grantRoles) public { + _inputs.hubAdmin = address(0); + _inputs.grantRoles = grantRoles; + InputUtils.FullDeployInputs memory sanitized = _harness.loadWarningsAndSanitizeInputs( + _logger, + _inputs, + _deployer + ); + InputUtils.FullDeployInputs memory expected = _inputs; + if (grantRoles) { + expected.hubAdmin = _deployer; + } + assertEq(sanitized, expected); + } + + function test_loadWarningsAndSanitizeInputs_withZeroSpokeAdmin_fuzz(bool grantRoles) public { + _inputs.spokeAdmin = address(0); + _inputs.grantRoles = grantRoles; + InputUtils.FullDeployInputs memory sanitized = _harness.loadWarningsAndSanitizeInputs( + _logger, + _inputs, + _deployer + ); + InputUtils.FullDeployInputs memory expected = _inputs; + if (grantRoles) { + expected.spokeAdmin = _deployer; + } + assertEq(sanitized, expected); + } + + function test_loadWarningsAndSanitizeInputs_withZeroHubConfiguratorOwner_fuzz( + bool grantRoles + ) public { + _inputs.hubConfiguratorOwner = address(0); + _inputs.grantRoles = grantRoles; + InputUtils.FullDeployInputs memory sanitized = _harness.loadWarningsAndSanitizeInputs( + _logger, + _inputs, + _deployer + ); + InputUtils.FullDeployInputs memory expected = _inputs; + if (grantRoles) { + expected.hubConfiguratorOwner = _deployer; + } + assertEq(sanitized, expected); + } + + function test_loadWarningsAndSanitizeInputs_withZeroSpokeConfiguratorOwner_fuzz( + bool grantRoles + ) public { + _inputs.spokeConfiguratorOwner = address(0); + _inputs.grantRoles = grantRoles; + InputUtils.FullDeployInputs memory sanitized = _harness.loadWarningsAndSanitizeInputs( + _logger, + _inputs, + _deployer + ); + InputUtils.FullDeployInputs memory expected = _inputs; + if (grantRoles) { + expected.spokeConfiguratorOwner = _deployer; + } + assertEq(sanitized, expected); + } + + function test_loadWarningsAndSanitizeInputs_withZeroSpokeProxyAdminOwner_fuzz( + bool grantRoles + ) public { + _inputs.spokeProxyAdminOwner = address(0); + _inputs.grantRoles = grantRoles; + InputUtils.FullDeployInputs memory sanitized = _harness.loadWarningsAndSanitizeInputs( + _logger, + _inputs, + _deployer + ); + + InputUtils.FullDeployInputs memory expected = _inputs; + if (grantRoles) { + expected.spokeProxyAdminOwner = _deployer; + } + assertEq(sanitized, expected); + } + + function test_loadWarningsAndSanitizeInputs_withZeroTreasurySpokeOwner_fuzz( + bool grantRoles + ) public { + _inputs.treasurySpokeOwner = address(0); + _inputs.grantRoles = grantRoles; + InputUtils.FullDeployInputs memory sanitized = _harness.loadWarningsAndSanitizeInputs( + _logger, + _inputs, + _deployer + ); + InputUtils.FullDeployInputs memory expected = _inputs; + if (grantRoles) { + expected.treasurySpokeOwner = _deployer; + } + assertEq(sanitized, expected); + } + + function test_loadWarningsAndSanitizeInputs_withZeroGatewayOwner_fuzz(bool grantRoles) public { + _inputs.gatewayOwner = address(0); + _inputs.grantRoles = grantRoles; + InputUtils.FullDeployInputs memory sanitized = _harness.loadWarningsAndSanitizeInputs( + _logger, + _inputs, + _deployer + ); + InputUtils.FullDeployInputs memory expected = _inputs; + expected.gatewayOwner = _deployer; + assertEq(sanitized, expected); + } + + function test_loadWarningsAndSanitizeInputs_withZeroNativeWrapper_fuzz(bool grantRoles) public { + _inputs.nativeWrapper = address(0); + _inputs.grantRoles = grantRoles; + InputUtils.FullDeployInputs memory sanitized = _harness.loadWarningsAndSanitizeInputs( + _logger, + _inputs, + _deployer + ); + InputUtils.FullDeployInputs memory expected = _inputs; + expected.nativeWrapper = address(0); + assertEq(sanitized, expected); + } + + function assertEq( + InputUtils.FullDeployInputs memory a, + InputUtils.FullDeployInputs memory b + ) public pure { + assertEq(a.accessManagerAdmin, b.accessManagerAdmin, 'access manager admin'); + assertEq(a.hubAdmin, b.hubAdmin, 'hub admin'); + assertEq(a.hubConfiguratorOwner, b.hubConfiguratorOwner, 'hub configurator owner'); + assertEq(a.treasurySpokeOwner, b.treasurySpokeOwner, 'treasury spoke owner'); + assertEq(a.spokeProxyAdminOwner, b.spokeProxyAdminOwner, 'spoke proxy admin owner'); + assertEq(a.spokeConfiguratorOwner, b.spokeConfiguratorOwner, 'spoke configurator owner'); + assertEq(a.spokeAdmin, b.spokeAdmin, 'spoke admin'); + assertEq(a.gatewayOwner, b.gatewayOwner, 'gateway owner'); + assertEq(a.nativeWrapper, b.nativeWrapper, 'native wrapper'); + assertEq(a.grantRoles, b.grantRoles, 'grant roles'); + assertEq(a.hubLabels, b.hubLabels, 'hub labels'); + assertEq(a.spokeLabels, b.spokeLabels, 'spoke labels'); + assertEq(abi.encode(a), abi.encode(b)); + } +} diff --git a/tests/unit/AaveOracle.t.sol b/tests/unit/AaveOracle.t.sol index b5803807a..aa159ae4e 100644 --- a/tests/unit/AaveOracle.t.sol +++ b/tests/unit/AaveOracle.t.sol @@ -21,7 +21,7 @@ contract AaveOracleTest is Base { uint256 private constant reserveId2 = 1; function setUp() public override { - deployFixtures(); + super.setUp(); oracle = new AaveOracle(address(spoke1), _oracleDecimals, _description); } diff --git a/tests/unit/AssetInterestRateStrategy.t.sol b/tests/unit/AssetInterestRateStrategy.t.sol index 63777d97b..cccca25e0 100644 --- a/tests/unit/AssetInterestRateStrategy.t.sol +++ b/tests/unit/AssetInterestRateStrategy.t.sol @@ -15,7 +15,7 @@ contract AssetInterestRateStrategyTest is Base { bytes public encodedRateData; function setUp() public override { - deployFixtures(); + super.setUp(); rateStrategy = new AssetInterestRateStrategy(address(hub1)); rateData = IAssetInterestRateStrategy.InterestRateData({ diff --git a/tests/unit/Hub/Hub.Access.t.sol b/tests/unit/Hub/Hub.Access.t.sol index ca9a7af03..d83252eaf 100644 --- a/tests/unit/Hub/Hub.Access.t.sol +++ b/tests/unit/Hub/Hub.Access.t.sol @@ -159,12 +159,9 @@ contract HubAccessTest is HubBase { ); // Say addresses Alice, Bob, and Carol all have the HUB_ADMIN role, allowing them to set interest rate data. - // Grant roles with 0 delay - vm.startPrank(ADMIN); - accessManager.grantRole(Roles.HUB_ADMIN_ROLE, alice, 0); - accessManager.grantRole(Roles.HUB_ADMIN_ROLE, bob, 0); - accessManager.grantRole(Roles.HUB_ADMIN_ROLE, carol, 0); - vm.stopPrank(); + _grantHubAdminRole(hub1, alice); + _grantHubAdminRole(hub1, bob); + _grantHubAdminRole(hub1, carol); vm.prank(alice); hub1.setInterestRateData(daiAssetId, encodedIrData); @@ -212,9 +209,9 @@ contract HubAccessTest is HubBase { // Alice, Bob, and Carol currently have both HUB_ADMIN and SET_INTEREST_RATE roles. IAccessManager accessManager = IAccessManager(hub1.authority()); - assertTrue(_hasRole(accessManager, Roles.HUB_ADMIN_ROLE, alice)); - assertTrue(_hasRole(accessManager, Roles.HUB_ADMIN_ROLE, bob)); - assertTrue(_hasRole(accessManager, Roles.HUB_ADMIN_ROLE, carol)); + assertTrue(_hasRole(accessManager, Roles.HUB_FEE_MINTER_ROLE, alice)); + assertTrue(_hasRole(accessManager, Roles.HUB_FEE_MINTER_ROLE, bob)); + assertTrue(_hasRole(accessManager, Roles.HUB_FEE_MINTER_ROLE, carol)); assertTrue(_hasRole(accessManager, SET_INTEREST_RATE_ROLE, alice)); assertTrue(_hasRole(accessManager, SET_INTEREST_RATE_ROLE, bob)); @@ -222,15 +219,15 @@ contract HubAccessTest is HubBase { // We can remove HUB_ADMIN role from Alice, Bob, and Carol. vm.startPrank(ADMIN); - accessManager.revokeRole(Roles.HUB_ADMIN_ROLE, alice); - accessManager.revokeRole(Roles.HUB_ADMIN_ROLE, bob); - accessManager.revokeRole(Roles.HUB_ADMIN_ROLE, carol); + accessManager.revokeRole(Roles.HUB_FEE_MINTER_ROLE, alice); + accessManager.revokeRole(Roles.HUB_FEE_MINTER_ROLE, bob); + accessManager.revokeRole(Roles.HUB_FEE_MINTER_ROLE, carol); vm.stopPrank(); // Alice, Bob, and Carol should no longer have HUB_ADMIN role. - assertFalse(_hasRole(accessManager, Roles.HUB_ADMIN_ROLE, alice)); - assertFalse(_hasRole(accessManager, Roles.HUB_ADMIN_ROLE, bob)); - assertFalse(_hasRole(accessManager, Roles.HUB_ADMIN_ROLE, carol)); + assertFalse(_hasRole(accessManager, Roles.HUB_FEE_MINTER_ROLE, alice)); + assertFalse(_hasRole(accessManager, Roles.HUB_FEE_MINTER_ROLE, bob)); + assertFalse(_hasRole(accessManager, Roles.HUB_FEE_MINTER_ROLE, carol)); // Can still call setInterestRateData since they have SET_INTEREST_RATE role. vm.prank(alice); @@ -268,10 +265,10 @@ contract HubAccessTest is HubBase { // Set up the role for hub admin to call update asset config vm.startPrank(NEW_ADMIN); - newAuthority.grantRole(Roles.HUB_ADMIN_ROLE, HUB_ADMIN, 0); + newAuthority.grantRole(Roles.HUB_FEE_MINTER_ROLE, HUB_ADMIN, 0); bytes4[] memory selectors = new bytes4[](1); selectors[0] = IHub.updateAssetConfig.selector; - newAuthority.setTargetFunctionRole(address(hub1), selectors, Roles.HUB_ADMIN_ROLE); + newAuthority.setTargetFunctionRole(address(hub1), selectors, Roles.HUB_FEE_MINTER_ROLE); vm.stopPrank(); // Only Admin can change the authority contract @@ -304,7 +301,7 @@ contract HubAccessTest is HubBase { // Now we also give the hub admin role capability to update spoke config on new authority selectors[0] = IHub.updateSpokeConfig.selector; vm.prank(NEW_ADMIN); - newAuthority.setTargetFunctionRole(address(hub1), selectors, Roles.HUB_ADMIN_ROLE); + newAuthority.setTargetFunctionRole(address(hub1), selectors, Roles.HUB_FEE_MINTER_ROLE); // Hub admin can now call update spoke config on the hub after authority change vm.prank(HUB_ADMIN); diff --git a/tests/unit/Hub/Hub.Config.t.sol b/tests/unit/Hub/Hub.Config.t.sol index 8428a31c7..4f1d492e3 100644 --- a/tests/unit/Hub/Hub.Config.t.sol +++ b/tests/unit/Hub/Hub.Config.t.sol @@ -806,7 +806,7 @@ contract HubConfigTest is HubBase { function _assumeValidAssetConfig(IHub.AssetConfig memory newConfig) internal pure { newConfig.liquidityFee = bound(newConfig.liquidityFee, 0, PercentageMath.PERCENTAGE_FACTOR) .toUint16(); - vm.assume(address(newConfig.feeReceiver) != address(0) || newConfig.liquidityFee == 0); + vm.assume(address(newConfig.feeReceiver) != address(0)); assumeNotPrecompile(newConfig.feeReceiver); assumeNotForgeAddress(newConfig.feeReceiver); assumeNotZeroAddress(newConfig.irStrategy); diff --git a/tests/unit/Hub/Hub.Restore.t.sol b/tests/unit/Hub/Hub.Restore.t.sol index 996c0a64e..d1180cc4d 100644 --- a/tests/unit/Hub/Hub.Restore.t.sol +++ b/tests/unit/Hub/Hub.Restore.t.sol @@ -10,24 +10,9 @@ contract HubRestoreTest is HubBase { using PercentageMath for uint256; using SafeCast for *; - HubConfigurator public hubConfigurator; - address public HUB_CONFIGURATOR_ADMIN = makeAddr('HUB_CONFIGURATOR_ADMIN'); - - function setUp() public override { - super.setUp(); - - // Set up a hub configurator to test freezing and pausing assets - hubConfigurator = new HubConfigurator(HUB_CONFIGURATOR_ADMIN); - IAccessManager accessManager = IAccessManager(hub1.authority()); - // Grant hubConfigurator hub admin role with 0 delay - vm.prank(ADMIN); - accessManager.grantRole(Roles.HUB_ADMIN_ROLE, address(hubConfigurator), 0); - } - function test_restore_revertsWith_SurplusDrawnRestored() public { uint256 daiAmount = 100e18; uint256 wethAmount = 10e18; - uint256 drawAmount = daiAmount / 2; // spoke1 add weth diff --git a/tests/unit/Hub/HubAccrueInterest.t.sol b/tests/unit/Hub/HubAccrueInterest.t.sol index 0a5aaa5e0..0e4cfb548 100644 --- a/tests/unit/Hub/HubAccrueInterest.t.sol +++ b/tests/unit/Hub/HubAccrueInterest.t.sol @@ -45,7 +45,7 @@ contract HubAccrueInterestTest is Base { function setUp() public override { super.setUp(); - initEnvironment(); + _initEnvironment(); spokeMintAndApprove(); } diff --git a/tests/unit/Hub/HubBase.t.sol b/tests/unit/Hub/HubBase.t.sol index 1fe787157..ba2350b8e 100644 --- a/tests/unit/Hub/HubBase.t.sol +++ b/tests/unit/Hub/HubBase.t.sol @@ -48,7 +48,7 @@ contract HubBase is Base { function setUp() public virtual override { super.setUp(); - initEnvironment(); + _initEnvironment(); } function _updateAddCap(uint256 assetId, address spoke, uint40 newAddCap) internal { diff --git a/tests/unit/HubConfigurator.t.sol b/tests/unit/HubConfigurator.t.sol index c75ca4aa5..cbd038d75 100644 --- a/tests/unit/HubConfigurator.t.sol +++ b/tests/unit/HubConfigurator.t.sol @@ -7,9 +7,6 @@ import 'tests/unit/Hub/HubBase.t.sol'; contract HubConfiguratorTest is HubBase { using SafeCast for uint256; - HubConfigurator internal hubConfigurator; - - address internal HUB_CONFIGURATOR_ADMIN = makeAddr('HUB_CONFIGURATOR_ADMIN'); uint256 internal _assetId; bytes internal _encodedIrData; @@ -21,11 +18,7 @@ contract HubConfiguratorTest is HubBase { function setUp() public virtual override { super.setUp(); - hubConfigurator = new HubConfigurator(HUB_CONFIGURATOR_ADMIN); - IAccessManager accessManager = IAccessManager(hub1.authority()); - // Grant hubConfigurator hub admin role with 0 delay - vm.prank(ADMIN); - accessManager.grantRole(Roles.HUB_ADMIN_ROLE, address(hubConfigurator), 0); + _grantHubConfiguratorRole(hub1, address(hubConfigurator)); _assetId = daiAssetId; _encodedIrData = abi.encode( IAssetInterestRateStrategy.InterestRateData({ diff --git a/tests/unit/Rescuable.t.sol b/tests/unit/Rescuable.t.sol index 82dda5f2d..fedde75d9 100644 --- a/tests/unit/Rescuable.t.sol +++ b/tests/unit/Rescuable.t.sol @@ -9,7 +9,7 @@ contract RescuableTest is Base { function setUp() public virtual override { super.setUp(); - initEnvironment(); + _initEnvironment(); rescuable = new RescuableWrapper(ADMIN); } diff --git a/tests/unit/Spoke/Spoke.Access.t.sol b/tests/unit/Spoke/Spoke.Access.t.sol index 675ebebea..b7353f9d2 100644 --- a/tests/unit/Spoke/Spoke.Access.t.sol +++ b/tests/unit/Spoke/Spoke.Access.t.sol @@ -154,10 +154,14 @@ contract SpokeAccessTest is SpokeBase { // Set up the role for spoke admin to call update liquidation config vm.startPrank(NEW_ADMIN); - newAuthority.grantRole(Roles.SPOKE_ADMIN_ROLE, SPOKE_ADMIN, 0); + newAuthority.grantRole(Roles.SPOKE_POSITION_UPDATER_ROLE, SPOKE_ADMIN, 0); bytes4[] memory selectors = new bytes4[](1); selectors[0] = ISpoke.updateLiquidationConfig.selector; - newAuthority.setTargetFunctionRole(address(spoke1), selectors, Roles.SPOKE_ADMIN_ROLE); + newAuthority.setTargetFunctionRole( + address(spoke1), + selectors, + Roles.SPOKE_POSITION_UPDATER_ROLE + ); vm.stopPrank(); // Only Admin can change the authority contract @@ -207,7 +211,11 @@ contract SpokeAccessTest is SpokeBase { // Now we also give the spoke admin role capability to add reserve on new authority selectors[0] = ISpoke.addReserve.selector; vm.prank(NEW_ADMIN); - newAuthority.setTargetFunctionRole(address(spoke1), selectors, Roles.SPOKE_ADMIN_ROLE); + newAuthority.setTargetFunctionRole( + address(spoke1), + selectors, + Roles.SPOKE_POSITION_UPDATER_ROLE + ); // Spoke admin can now call add reserve on the spoke after authority change vm.prank(SPOKE_ADMIN); diff --git a/tests/unit/Spoke/Spoke.Config.t.sol b/tests/unit/Spoke/Spoke.Config.t.sol index 3531ed70e..0544cb5da 100644 --- a/tests/unit/Spoke/Spoke.Config.t.sol +++ b/tests/unit/Spoke/Spoke.Config.t.sol @@ -38,7 +38,9 @@ contract SpokeConfigTest is SpokeBase { address caller ) public { vm.assume( - caller != SPOKE_ADMIN && caller != ADMIN && caller != _getProxyAdminAddress(address(spoke1)) + caller != SPOKE_ADMIN && + caller != ADMIN && + caller != ProxyHelper.getProxyAdmin(address(spoke1)) ); vm.expectRevert( abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, caller) diff --git a/tests/unit/Spoke/Spoke.DynamicConfig.Triggers.t.sol b/tests/unit/Spoke/Spoke.DynamicConfig.Triggers.t.sol index 7060c33a4..c63bd8c92 100644 --- a/tests/unit/Spoke/Spoke.DynamicConfig.Triggers.t.sol +++ b/tests/unit/Spoke/Spoke.DynamicConfig.Triggers.t.sol @@ -209,10 +209,10 @@ contract SpokeDynamicConfigTriggersTest is SpokeBase { function test_updateUserDynamicConfig_reverts_when_not_authorized(address caller) public { vm.assume( caller != alice && + caller != ADMIN && caller != POSITION_MANAGER && caller != SPOKE_ADMIN && - caller != USER_POSITION_UPDATER && - caller != _getProxyAdminAddress(address(spoke1)) + caller != ProxyHelper.getProxyAdmin(address(spoke1)) ); Utils.supplyCollateral(spoke1, _usdxReserveId(spoke1), alice, 1000e6, alice); @@ -249,7 +249,6 @@ contract SpokeDynamicConfigTriggersTest is SpokeBase { _updateUserDynamicConfig({caller: alice, existingConfigs: configs}); _updateUserDynamicConfig({caller: POSITION_MANAGER, existingConfigs: configs}); _updateUserDynamicConfig({caller: SPOKE_ADMIN, existingConfigs: configs}); - _updateUserDynamicConfig({caller: USER_POSITION_UPDATER, existingConfigs: configs}); } function test_updateUserDynamicConfig_updatesRP() public { diff --git a/tests/unit/Spoke/Spoke.DynamicConfig.t.sol b/tests/unit/Spoke/Spoke.DynamicConfig.t.sol index d15c6f280..fadf6a85d 100644 --- a/tests/unit/Spoke/Spoke.DynamicConfig.t.sol +++ b/tests/unit/Spoke/Spoke.DynamicConfig.t.sol @@ -111,7 +111,9 @@ contract SpokeDynamicConfigTest is SpokeBase { address caller ) public { vm.assume( - caller != SPOKE_ADMIN && caller != ADMIN && caller != _getProxyAdminAddress(address(spoke1)) + caller != SPOKE_ADMIN && + caller != ADMIN && + caller != ProxyHelper.getProxyAdmin(address(spoke1)) ); uint256 reserveId = _randomReserveId(spoke1); uint24 dynamicConfigKey = _randomInitializedConfigKey(spoke1, reserveId); @@ -219,7 +221,9 @@ contract SpokeDynamicConfigTest is SpokeBase { address caller ) public { vm.assume( - caller != SPOKE_ADMIN && caller != ADMIN && caller != _getProxyAdminAddress(address(spoke1)) + caller != SPOKE_ADMIN && + caller != ADMIN && + caller != ProxyHelper.getProxyAdmin(address(spoke1)) ); uint256 reserveId = _randomReserveId(spoke1); uint24 dynamicConfigKey = _randomInitializedConfigKey(spoke1, reserveId); @@ -328,7 +332,9 @@ contract SpokeDynamicConfigTest is SpokeBase { spoke1, reserveId ); - dynConf.collateralFactor = _randomBps(); + dynConf.collateralFactor = _randomBps( + PercentageMath.PERCENTAGE_FACTOR.percentDivDown(dynConf.maxLiquidationBonus) + ); vm.expectEmit(address(spoke1)); emit ISpoke.AddDynamicReserveConfig(reserveId, dynamicConfigKey, dynConf); diff --git a/tests/unit/Spoke/Spoke.Getters.t.sol b/tests/unit/Spoke/Spoke.Getters.t.sol index 80d635e13..a4ba510d8 100644 --- a/tests/unit/Spoke/Spoke.Getters.t.sol +++ b/tests/unit/Spoke/Spoke.Getters.t.sol @@ -16,8 +16,9 @@ contract SpokeGettersTest is SpokeBase { super.setUp(); // Deploy new spoke without setting the liquidation config - (spoke, ) = _deploySpokeWithOracle(ADMIN, address(accessManager), 'New Spoke (USD)'); - setUpRoles(hub1, spoke, accessManager); + TestTypes.TestEnvReport memory report = _deployFixtures({numHubs: 0, numSpokes: 1}); + _setupFixturesRoles(report); + spoke = ISpoke(report.spokeReports[0].spoke); IHub.SpokeConfig memory spokeConfig = IHub.SpokeConfig({ active: true, diff --git a/tests/unit/Spoke/Spoke.MultipleHub.Base.t.sol b/tests/unit/Spoke/Spoke.MultipleHub.Base.t.sol index 9c8ed245d..012cd160b 100644 --- a/tests/unit/Spoke/Spoke.MultipleHub.Base.t.sol +++ b/tests/unit/Spoke/Spoke.MultipleHub.Base.t.sol @@ -30,10 +30,10 @@ contract SpokeMultipleHubBase is SpokeBase { bytes internal encodedIrData = abi.encode(irData); function setUp() public virtual override { - deployFixtures(); + _deployFixtures(); } - function deployFixtures() internal virtual override { + function _deployFixtures() internal virtual { vm.startPrank(ADMIN); accessManager = IAccessManager(address(new AccessManagerEnumerable(ADMIN))); // Canonical hub and spoke @@ -60,11 +60,11 @@ contract SpokeMultipleHubBase is SpokeBase { function setUpRoles() internal { vm.startPrank(ADMIN); // Grant roles with 0 delay - accessManager.grantRole(Roles.HUB_ADMIN_ROLE, ADMIN, 0); - accessManager.grantRole(Roles.SPOKE_ADMIN_ROLE, ADMIN, 0); - accessManager.grantRole(Roles.HUB_ADMIN_ROLE, HUB_ADMIN, 0); - accessManager.grantRole(Roles.SPOKE_ADMIN_ROLE, HUB_ADMIN, 0); - accessManager.grantRole(Roles.SPOKE_ADMIN_ROLE, SPOKE_ADMIN, 0); + accessManager.grantRole(Roles.HUB_FEE_MINTER_ROLE, ADMIN, 0); + accessManager.grantRole(Roles.SPOKE_POSITION_UPDATER_ROLE, ADMIN, 0); + accessManager.grantRole(Roles.HUB_FEE_MINTER_ROLE, HUB_ADMIN, 0); + accessManager.grantRole(Roles.SPOKE_POSITION_UPDATER_ROLE, HUB_ADMIN, 0); + accessManager.grantRole(Roles.SPOKE_POSITION_UPDATER_ROLE, SPOKE_ADMIN, 0); // Grant responsibilities to roles // Spoke Admin functionalities @@ -76,8 +76,16 @@ contract SpokeMultipleHubBase is SpokeBase { selectors[4] = ISpoke.addDynamicReserveConfig.selector; selectors[5] = ISpoke.updateUserRiskPremium.selector; - accessManager.setTargetFunctionRole(address(spoke1), selectors, Roles.SPOKE_ADMIN_ROLE); - accessManager.setTargetFunctionRole(address(newSpoke), selectors, Roles.SPOKE_ADMIN_ROLE); + accessManager.setTargetFunctionRole( + address(spoke1), + selectors, + Roles.SPOKE_POSITION_UPDATER_ROLE + ); + accessManager.setTargetFunctionRole( + address(newSpoke), + selectors, + Roles.SPOKE_POSITION_UPDATER_ROLE + ); // Hub Admin functionalities bytes4[] memory hubSelectors = new bytes4[](4); @@ -86,8 +94,8 @@ contract SpokeMultipleHubBase is SpokeBase { hubSelectors[2] = IHub.addSpoke.selector; hubSelectors[3] = IHub.updateSpokeConfig.selector; - accessManager.setTargetFunctionRole(address(hub1), hubSelectors, Roles.HUB_ADMIN_ROLE); - accessManager.setTargetFunctionRole(address(newHub), hubSelectors, Roles.HUB_ADMIN_ROLE); + accessManager.setTargetFunctionRole(address(hub1), hubSelectors, Roles.HUB_FEE_MINTER_ROLE); + accessManager.setTargetFunctionRole(address(newHub), hubSelectors, Roles.HUB_FEE_MINTER_ROLE); vm.stopPrank(); } } diff --git a/tests/unit/Spoke/Spoke.MultipleHub.t.sol b/tests/unit/Spoke/Spoke.MultipleHub.t.sol index 5ceb851e0..faf3e9aa3 100644 --- a/tests/unit/Spoke/Spoke.MultipleHub.t.sol +++ b/tests/unit/Spoke/Spoke.MultipleHub.t.sol @@ -23,8 +23,8 @@ contract SpokeMultipleHubTest is SpokeBase { super.setUp(); // Configure both hubs - (hub2, hub2IrStrategy) = hub2Fixture(); - (hub3, hub3IrStrategy) = hub3Fixture(); + (hub2, hub2IrStrategy) = _hub2Fixture(); + (hub3, hub3IrStrategy) = _hub3Fixture(); vm.startPrank(ADMIN); // Relist hub 2's dai on spoke1 diff --git a/tests/unit/Spoke/Spoke.Upgradeable.t.sol b/tests/unit/Spoke/Spoke.Upgradeable.t.sol index 65fc3c3c8..53ead43d6 100644 --- a/tests/unit/Spoke/Spoke.Upgradeable.t.sol +++ b/tests/unit/Spoke/Spoke.Upgradeable.t.sol @@ -5,9 +5,6 @@ pragma solidity ^0.8.0; import 'tests/unit/Spoke/SpokeBase.t.sol'; contract SpokeUpgradeableTest is SpokeBase { - bytes32 internal constant INITIALIZABLE_STORAGE = - 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; - address internal proxyAdminOwner = makeAddr('proxyAdminOwner'); address internal oracle = makeAddr('AaveOracle'); @@ -25,7 +22,7 @@ contract SpokeUpgradeableTest is SpokeBase { assertEq(address(spokeImpl), spokeImplAddress); assertEq(spokeImpl.SPOKE_REVISION(), revision); - assertEq(_getProxyInitializedVersion(spokeImplAddress), type(uint64).max); + assertEq(ProxyHelper.getProxyInitializedVersion(spokeImplAddress), type(uint64).max); vm.expectRevert(Initializable.InvalidInitialization.selector); spokeImpl.initialize(address(accessManager)); @@ -69,10 +66,10 @@ contract SpokeUpgradeableTest is SpokeBase { ); assertEq(address(spokeProxy), spokeProxyAddress); - assertEq(_getProxyAdminAddress(address(spokeProxy)), proxyAdminAddress); - assertEq(_getImplementationAddress(address(spokeProxy)), address(spokeImpl)); + assertEq(ProxyHelper.getProxyAdmin(address(spokeProxy)), proxyAdminAddress); + assertEq(ProxyHelper.getImplementation(address(spokeProxy)), address(spokeImpl)); - assertEq(_getProxyInitializedVersion(address(spokeProxy)), revision); + assertEq(ProxyHelper.getProxyInitializedVersion(address(spokeProxy)), revision); assertEq(spokeProxy.getLiquidationConfig(), expectedLiquidationConfig); } @@ -89,7 +86,14 @@ contract SpokeUpgradeableTest is SpokeBase { ) ); - setUpRoles(hub1, ISpoke(address(spokeProxy)), accessManager); + TestTypes.TestEnvReport memory report; + report.spokeReports = new TestTypes.TestSpokeReport[](1); + report.spokeReports[0].spoke = address(spokeProxy); + report.configuratorReport.hubConfigurator = address(hubConfigurator); + report.configuratorReport.spokeConfigurator = address(spokeConfigurator); + + _setupFixturesRoles(report); + uint128 targetHealthFactor = 1.05e18; _updateTargetHealthFactor(ISpoke(address(spokeProxy)), targetHealthFactor); @@ -99,7 +103,7 @@ contract SpokeUpgradeableTest is SpokeBase { vm.expectEmit(address(spokeProxy)); emit IAccessManaged.AuthorityUpdated(address(accessManager)); vm.recordLogs(); - vm.prank(_getProxyAdminAddress(address(spokeProxy))); + vm.prank(ProxyHelper.getProxyAdmin(address(spokeProxy))); spokeProxy.upgradeToAndCall( address(spokeImpl2), _getInitializeCalldata(address(accessManager)) @@ -138,13 +142,13 @@ contract SpokeUpgradeableTest is SpokeBase { ); vm.expectRevert(Initializable.InvalidInitialization.selector); - vm.prank(_getProxyAdminAddress(address(spokeProxy))); + vm.prank(ProxyHelper.getProxyAdmin(address(spokeProxy))); spokeProxy.upgradeToAndCall(address(spokeImpl), _getInitializeCalldata(address(accessManager))); uint64 secondRevision = uint64(vm.randomUint(0, initialRevision - 1)); SpokeInstance spokeImpl2 = _deployMockSpokeInstance(secondRevision); vm.expectRevert(Initializable.InvalidInitialization.selector); - vm.prank(_getProxyAdminAddress(address(spokeProxy))); + vm.prank(ProxyHelper.getProxyAdmin(address(spokeProxy))); spokeProxy.upgradeToAndCall( address(spokeImpl2), _getInitializeCalldata(address(accessManager)) @@ -175,7 +179,7 @@ contract SpokeUpgradeableTest is SpokeBase { SpokeInstance spokeImpl2 = _deployMockSpokeInstance(2); vm.expectRevert(ISpoke.InvalidAddress.selector); - vm.prank(_getProxyAdminAddress(address(spokeProxy))); + vm.prank(ProxyHelper.getProxyAdmin(address(spokeProxy))); spokeProxy.upgradeToAndCall(address(spokeImpl2), _getInitializeCalldata(address(0))); } @@ -200,11 +204,6 @@ contract SpokeUpgradeableTest is SpokeBase { ); } - function _getProxyInitializedVersion(address proxy) internal view returns (uint64) { - bytes32 slotData = vm.load(proxy, INITIALIZABLE_STORAGE); - return uint64(uint256(slotData) & ((1 << 64) - 1)); - } - function _getInitializeCalldata(address manager) internal pure returns (bytes memory) { return abi.encodeCall(Spoke.initialize, manager); } diff --git a/tests/unit/Spoke/SpokeBase.t.sol b/tests/unit/Spoke/SpokeBase.t.sol index b3044ee8a..9b9b771ed 100644 --- a/tests/unit/Spoke/SpokeBase.t.sol +++ b/tests/unit/Spoke/SpokeBase.t.sol @@ -158,7 +158,7 @@ contract SpokeBase is Base { function setUp() public virtual override { super.setUp(); - initEnvironment(); + _initEnvironment(); } /// @dev Opens a supply position for a random user @@ -1104,15 +1104,15 @@ contract SpokeBase is Base { ) internal { address mockSpoke = address(new MockSpoke(spoke.ORACLE())); - address implementation = _getImplementationAddress(address(spoke)); + address implementation = ProxyHelper.getImplementation(address(spoke)); - vm.prank(_getProxyAdminAddress(address(spoke))); + vm.prank(ProxyHelper.getProxyAdmin(address(spoke))); ITransparentUpgradeableProxy(address(spoke)).upgradeToAndCall(address(mockSpoke), ''); vm.prank(user); MockSpoke(address(spoke)).borrowWithoutHfCheck(reserveId, debtAmount, user); - vm.prank(_getProxyAdminAddress(address(spoke))); + vm.prank(ProxyHelper.getProxyAdmin(address(spoke))); ITransparentUpgradeableProxy(address(spoke)).upgradeToAndCall(implementation, ''); } diff --git a/tests/unit/SpokeConfigurator.t.sol b/tests/unit/SpokeConfigurator.t.sol index 30e97f21a..581ec5a25 100644 --- a/tests/unit/SpokeConfigurator.t.sol +++ b/tests/unit/SpokeConfigurator.t.sol @@ -7,9 +7,6 @@ import 'tests/unit/Spoke/SpokeBase.t.sol'; contract SpokeConfiguratorTest is SpokeBase { using SafeCast for uint256; - SpokeConfigurator public spokeConfigurator; - address public SPOKE_CONFIGURATOR_ADMIN = makeAddr('SPOKE_CONFIGURATOR_ADMIN'); - address public spokeAddr; ISpoke public spoke; uint256 public _reserveId; @@ -18,20 +15,10 @@ contract SpokeConfiguratorTest is SpokeBase { function setUp() public virtual override { super.setUp(); - spokeConfigurator = new SpokeConfigurator(SPOKE_CONFIGURATOR_ADMIN); spokeAddr = address(spoke1); spoke = ISpoke(spokeAddr); _reserveId = 0; invalidReserveId = spoke.getReserveCount(); - - // Grant spokeConfigurator spoke admin role with 0 delay - vm.startPrank(ADMIN); - IAccessManager(spoke1.authority()).grantRole( - Roles.SPOKE_ADMIN_ROLE, - address(spokeConfigurator), - 0 - ); - vm.stopPrank(); } function test_updateReservePriceSource_revertsWith_OwnableUnauthorizedAccount() public { diff --git a/tests/unit/libraries/LiquidationLogic/LiquidationLogic.LiquidateUser.t.sol b/tests/unit/libraries/LiquidationLogic/LiquidationLogic.LiquidateUser.t.sol index ac2ee59a3..63699feb2 100644 --- a/tests/unit/libraries/LiquidationLogic/LiquidationLogic.LiquidateUser.t.sol +++ b/tests/unit/libraries/LiquidationLogic/LiquidationLogic.LiquidateUser.t.sol @@ -28,7 +28,7 @@ contract LiquidationLogicLiquidateUserTest is LiquidationLogicBaseTest { // collateral to liquidator = 6000 - 100 = 5900 function setUp() public override { super.setUp(); - (hub2, ) = hub2Fixture(); + (hub2, ) = _hub2Fixture(); _mockInterestRateBps(hub2.getAsset(wethAssetId).irStrategy, 5_00); diff --git a/tests/unit/misc/GatewayBase.t.sol b/tests/unit/misc/GatewayBase.t.sol index 8a39b0377..a09f26e53 100644 --- a/tests/unit/misc/GatewayBase.t.sol +++ b/tests/unit/misc/GatewayBase.t.sol @@ -9,7 +9,7 @@ contract GatewayBaseTest is Base { function setUp() public virtual override { super.setUp(); - initEnvironment(); + _initEnvironment(); gateway = new GatewayBaseWrapper(address(ADMIN)); diff --git a/tests/unit/misc/SignatureGateway/SignatureGateway.Base.t.sol b/tests/unit/misc/SignatureGateway/SignatureGateway.Base.t.sol index 7fad83c12..3a5f72380 100644 --- a/tests/unit/misc/SignatureGateway/SignatureGateway.Base.t.sol +++ b/tests/unit/misc/SignatureGateway/SignatureGateway.Base.t.sol @@ -9,8 +9,7 @@ contract SignatureGatewayBaseTest is SpokeBase { uint256 public alicePk; function setUp() public virtual override { - deployFixtures(); - initEnvironment(); + super.setUp(); gateway = ISignatureGateway(new SignatureGateway(ADMIN)); (alice, alicePk) = makeAddrAndKey('alice'); diff --git a/tests/utils/BatchTestProcedures.sol b/tests/utils/BatchTestProcedures.sol new file mode 100644 index 000000000..01d23d2e9 --- /dev/null +++ b/tests/utils/BatchTestProcedures.sol @@ -0,0 +1,596 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {Test} from 'forge-std/Test.sol'; +import {console2 as console} from 'forge-std/console2.sol'; + +// dependencies +import {Ownable} from 'src/dependencies/openzeppelin/Ownable.sol'; +import {IAccessManaged} from 'src/dependencies/openzeppelin/IAccessManaged.sol'; + +// orchestration +import { + AaveV4DeployOrchestration +} from 'src/deployments/orchestration/AaveV4DeployOrchestration.sol'; +import {WETHDeployProcedure} from 'tests/deployments/procedures/WETHDeployProcedure.sol'; +import { + AaveV4SpokeRolesProcedure +} from 'src/deployments/procedures/roles/AaveV4SpokeRolesProcedure.sol'; +import { + AaveV4HubRolesProcedure +} from 'src/deployments/procedures/roles/AaveV4HubRolesProcedure.sol'; +import {AaveV4TestOrchestration} from 'tests/deployments/orchestration/AaveV4TestOrchestration.sol'; +import {AaveV4DeployProcedureBase} from 'src/deployments/procedures/AaveV4DeployProcedureBase.sol'; + +import {Logger} from 'src/deployments/utils/Logger.sol'; +import {InputUtils} from 'src/deployments/utils/InputUtils.sol'; +import {OrchestrationReports} from 'src/deployments/libraries/OrchestrationReports.sol'; +import {Constants} from 'tests/Constants.sol'; + +// libraries +import {Roles} from 'src/deployments/utils/libraries/Roles.sol'; +import {ProxyHelper} from 'tests/utils/ProxyHelper.sol'; + +// interfaces +import {IAccessManagerEnumerable} from 'src/access/interfaces/IAccessManagerEnumerable.sol'; +import {IAssetInterestRateStrategy} from 'src/hub/interfaces/IAssetInterestRateStrategy.sol'; +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {IHub} from 'src/hub/interfaces/IHub.sol'; +import {ITreasurySpoke} from 'src/spoke/interfaces/ITreasurySpoke.sol'; +import {IAaveOracle} from 'src/spoke/interfaces/IAaveOracle.sol'; + +contract BatchTestProcedures is Test, InputUtils, WETHDeployProcedure { + Logger internal _logger; + FullDeployInputs internal _inputs; + address internal _weth9; + + string[] internal _hubLabels; + string[] internal _spokeLabels; + bytes4[] internal _spokePositionUpdaterRoleSelectors; + bytes4[] internal _spokeConfiguratorRoleSelectors; + bytes4[] internal _hubFeeMinterRoleSelectors; + bytes4[] internal _hubConfiguratorRoleSelectors; + address internal _deployer = makeAddr('deployer'); + + function setUp() public virtual { + _spokePositionUpdaterRoleSelectors = AaveV4SpokeRolesProcedure + .getSpokePositionUpdaterRoleSelectors(); + _spokeConfiguratorRoleSelectors = AaveV4SpokeRolesProcedure.getSpokeConfiguratorRoleSelectors(); + + _hubFeeMinterRoleSelectors = AaveV4HubRolesProcedure.getHubFeeMinterRoleSelectors(); + _hubConfiguratorRoleSelectors = AaveV4HubRolesProcedure.getHubConfiguratorRoleSelectors(); + + _weth9 = _deployWETH(); + _logger = new Logger('dummy/path'); + _hubLabels = ['hub1', 'hub2', 'hub3']; + _spokeLabels = ['spoke1', 'spoke2', 'spoke3']; + + _etchCreate2Factory(); + } + + function checkedV4Deployment() public { + vm.startPrank(_deployer); + OrchestrationReports.FullDeploymentReport memory report = AaveV4DeployOrchestration + .deployAaveV4(_logger, _deployer, _inputs); + vm.stopPrank(); + _checkDeployment(report, _inputs); + _checkRoles(report, _inputs); + } + + function _checkDeployment( + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal view { + _checkFullReport(report, inputs); + _checkSpokeBatchDeployments(report, inputs); + _checkHubBatchDeployments(report, inputs); + } + + function _checkRoles( + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal view { + IAccessManagerEnumerable accessManager = IAccessManagerEnumerable( + report.accessBatchReport.accessManager + ); + _checkAccessManagerRoles(accessManager, inputs); + _checkSpokeRoles(accessManager, report, inputs); + _checkHubRoles(accessManager, report, inputs); + _checkConfiguratorBatchRoles(report, inputs); + _checkGatewayRoles(report, inputs); + } + + /// @dev Sanitizes the inputs by defaulting to the deployer if the address is zero. + function _sanitizeInputs( + FullDeployInputs memory inputs + ) internal view returns (FullDeployInputs memory) { + inputs.accessManagerAdmin = inputs.accessManagerAdmin != address(0) + ? inputs.accessManagerAdmin + : _deployer; + inputs.hubAdmin = inputs.hubAdmin != address(0) ? inputs.hubAdmin : _deployer; + inputs.hubConfiguratorOwner = inputs.hubConfiguratorOwner != address(0) + ? inputs.hubConfiguratorOwner + : _deployer; + inputs.treasurySpokeOwner = inputs.treasurySpokeOwner != address(0) + ? inputs.treasurySpokeOwner + : _deployer; + inputs.spokeAdmin = inputs.spokeAdmin != address(0) ? inputs.spokeAdmin : _deployer; + inputs.spokeProxyAdminOwner = inputs.spokeProxyAdminOwner != address(0) + ? inputs.spokeProxyAdminOwner + : _deployer; + inputs.spokeConfiguratorOwner = inputs.spokeConfiguratorOwner != address(0) + ? inputs.spokeConfiguratorOwner + : _deployer; + inputs.gatewayOwner = inputs.gatewayOwner != address(0) ? inputs.gatewayOwner : _deployer; + + return inputs; + } + + function _checkFullReport( + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal pure { + if (inputs.nativeWrapper != address(0)) { + assertNotEq(report.gatewaysBatchReport.nativeGateway, address(0), 'NativeGateway'); + assertNotEq(report.gatewaysBatchReport.signatureGateway, address(0), 'SignatureGateway'); + } else { + assertEq(report.gatewaysBatchReport.nativeGateway, address(0), 'Zero NativeGateway'); + assertEq(report.gatewaysBatchReport.signatureGateway, address(0), 'Zero SignatureGateway'); + } + + assertNotEq(report.accessBatchReport.accessManager, address(0), 'AccessManager'); + assertNotEq(report.configuratorBatchReport.spokeConfigurator, address(0), 'SpokeConfigurator'); + assertNotEq(report.configuratorBatchReport.hubConfigurator, address(0), 'HubConfigurator'); + for (uint256 i = 0; i < report.hubBatchReports.length; i++) { + assertNotEq(report.hubBatchReports[i].report.hub, address(0), 'Hub'); + assertNotEq(report.hubBatchReports[i].report.irStrategy, address(0), 'IRStrategy'); + assertNotEq(report.hubBatchReports[i].report.treasurySpoke, address(0), 'TreasurySpoke'); + } + for (uint256 i = 0; i < report.spokeInstanceBatchReports.length; i++) { + assertNotEq(report.spokeInstanceBatchReports[i].report.spokeProxy, address(0), 'SpokeProxy'); + assertNotEq(report.spokeInstanceBatchReports[i].report.aaveOracle, address(0), 'AaveOracle'); + } + assertEq(report.hubBatchReports.length, inputs.hubLabels.length, 'HubBatchReportsLength'); + assertEq( + report.spokeInstanceBatchReports.length, + inputs.spokeLabels.length, + 'SpokeInstanceBatchReportsLength' + ); + } + + function _checkSpokeBatchDeployments( + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal view { + string memory globalLabel = 'SpokeDeployment'; + for (uint256 i = 0; i < inputs.spokeLabels.length; i++) { + string memory label = string.concat(globalLabel, ', ', inputs.spokeLabels[i]); + OrchestrationReports.SpokeDeploymentReport memory spokeReport = report + .spokeInstanceBatchReports[i]; + _checkSpokeDeployment({ + report: spokeReport, + accessManager: report.accessBatchReport.accessManager, + label: label + }); + _checkOracleDeployment({report: spokeReport, label: label}); + } + } + + function _checkSpokeDeployment( + OrchestrationReports.SpokeDeploymentReport memory report, + address accessManager, + string memory label + ) internal view { + assertEq( + ProxyHelper.getImplementation(report.report.spokeProxy), + report.report.spokeImplementation, + string.concat(label, ' implementation') + ); + assertEq( + ISpoke(report.report.spokeProxy).ORACLE(), + report.report.aaveOracle, + string.concat(label, ' oracle on spoke') + ); + assertEq( + IAccessManaged(report.report.spokeProxy).authority(), + accessManager, + string.concat(label, ' spoke authority') + ); + } + + function _checkOracleDeployment( + OrchestrationReports.SpokeDeploymentReport memory report, + string memory label + ) internal view { + assertEq( + IAaveOracle(report.report.aaveOracle).SPOKE(), + report.report.spokeProxy, + string.concat(label, ' spoke on oracle') + ); + assertEq( + IAaveOracle(report.report.aaveOracle).DECIMALS(), + Constants.ORACLE_DECIMALS, + string.concat(label, ' oracle decimals') + ); + assertEq( + IAaveOracle(report.report.aaveOracle).DESCRIPTION(), + string.concat(report.label, Constants.ORACLE_SUFFIX), + string.concat(label, ' oracle description') + ); + } + + function _checkHubBatchDeployments( + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal view { + string memory globalLabel = 'HubDeployment'; + for (uint256 i = 0; i < inputs.hubLabels.length; i++) { + string memory label = string.concat(globalLabel, ', ', inputs.hubLabels[i]); + OrchestrationReports.HubDeploymentReport memory hubReport = report.hubBatchReports[i]; + + _checkHubDeployment({ + report: hubReport, + accessManager: report.accessBatchReport.accessManager, + label: label + }); + _checkInterestRateStrategyDeployment({report: hubReport, label: label}); + _checkTreasurySpokeDeployment({report: hubReport, label: label}); + } + } + + function _checkHubDeployment( + OrchestrationReports.HubDeploymentReport memory report, + address accessManager, + string memory label + ) internal view { + assertEq( + IAccessManaged(report.report.hub).authority(), + accessManager, + string.concat(label, ' hub authority') + ); + } + + function _checkInterestRateStrategyDeployment( + OrchestrationReports.HubDeploymentReport memory report, + string memory label + ) internal view { + assertEq( + IAssetInterestRateStrategy(report.report.irStrategy).HUB(), + report.report.hub, + string.concat(label, ' hub on interest rate strategy') + ); + } + + function _checkTreasurySpokeDeployment( + OrchestrationReports.HubDeploymentReport memory report, + string memory label + ) internal view { + assertEq( + address(ITreasurySpoke(report.report.treasurySpoke).HUB()), + report.report.hub, + string.concat(label, ' hub on treasury spoke') + ); + } + + function _checkAccessManagerRoles( + IAccessManagerEnumerable accessManager, + FullDeployInputs memory inputs + ) internal view { + address expectedAdmin = (inputs.grantRoles && inputs.accessManagerAdmin != address(0)) + ? inputs.accessManagerAdmin + : _deployer; + assertEq( + accessManager.getRoleMember(Roles.DEFAULT_ADMIN_ROLE, 0), + expectedAdmin, + 'DefaultAdminRoleMember' + ); + assertEq( + accessManager.getRoleMemberCount(Roles.DEFAULT_ADMIN_ROLE), + 1, + 'DefaultAdminRoleCount' + ); + + (bool adminHasRole, ) = accessManager.hasRole(Roles.DEFAULT_ADMIN_ROLE, expectedAdmin); + assertTrue(adminHasRole, 'access manager admin has default admin role'); + } + + function _checkSpokeRoles( + IAccessManagerEnumerable accessManager, + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal view { + _checkSpokeAdminRoles(accessManager, report, inputs); + _checkSpokeConfiguratorRoles(accessManager, report, inputs); + } + + function _checkSpokeConfiguratorRoles( + IAccessManagerEnumerable accessManager, + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal view { + if (inputs.spokeLabels.length > 0 && inputs.grantRoles) { + assertEq( + accessManager.getRoleMemberCount(Roles.SPOKE_CONFIGURATOR_ROLE), + 2, + 'SpokeConfiguratorRole member count' + ); + assertEq( + accessManager.getRoleMember(Roles.SPOKE_CONFIGURATOR_ROLE, 0), + inputs.spokeAdmin, + 'SpokeConfiguratorRole member - spoke admin' + ); + assertEq( + accessManager.getRoleMember(Roles.SPOKE_CONFIGURATOR_ROLE, 1), + report.configuratorBatchReport.spokeConfigurator, + 'SpokeConfiguratorRole member - spoke configurator' + ); + } else { + assertEq( + accessManager.getRoleMemberCount(Roles.SPOKE_CONFIGURATOR_ROLE), + 0, + 'SpokeConfiguratorRole member count' + ); + } + + for (uint256 i = 0; i < inputs.spokeLabels.length; i++) { + for (uint256 j = 0; j < _spokeConfiguratorRoleSelectors.length; j++) { + assertEq( + accessManager.getTargetFunctionRole( + report.spokeInstanceBatchReports[i].report.spokeProxy, + _spokeConfiguratorRoleSelectors[j] + ), + Roles.SPOKE_CONFIGURATOR_ROLE, + 'SpokeConfiguratorRole target function' + ); + + (bool allowed, uint32 delay) = accessManager.canCall( + report.configuratorBatchReport.spokeConfigurator, + report.spokeInstanceBatchReports[i].report.spokeProxy, + _spokeConfiguratorRoleSelectors[j] + ); + assertEq( + allowed, + inputs.grantRoles ? true : false, + 'SpokeConfiguratorRole allowed - configurator' + ); + assertEq(delay, 0, 'SpokeConfiguratorRole delay - configurator'); + + // spoke admin role encompasses spoke configurator role + (allowed, delay) = accessManager.canCall( + inputs.spokeAdmin, + report.spokeInstanceBatchReports[i].report.spokeProxy, + _spokeConfiguratorRoleSelectors[j] + ); + assertEq( + allowed, + inputs.grantRoles ? true : false, + 'SpokeConfiguratorRole allowed - spoke admin' + ); + assertEq(delay, 0, 'SpokeConfiguratorRole delay - spoke admin'); + } + } + } + + function _checkSpokeAdminRoles( + IAccessManagerEnumerable accessManager, + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal view { + if (inputs.spokeLabels.length > 0 && inputs.grantRoles) { + assertEq( + accessManager.getRoleMemberCount(Roles.SPOKE_POSITION_UPDATER_ROLE), + 1, + 'SpokeAdminRole member count' + ); + assertEq( + accessManager.getRoleMember(Roles.SPOKE_POSITION_UPDATER_ROLE, 0), + inputs.spokeAdmin, + 'SpokeAdminRole member - spoke admin' + ); + } else { + assertEq( + accessManager.getRoleMemberCount(Roles.SPOKE_POSITION_UPDATER_ROLE), + 0, + 'HubAdminRoleCount' + ); + } + + for (uint256 i = 0; i < inputs.spokeLabels.length; i++) { + address proxyAdminOwner = Ownable( + ProxyHelper.getProxyAdmin(report.spokeInstanceBatchReports[i].report.spokeProxy) + ).owner(); + assertEq( + proxyAdminOwner, + inputs.spokeProxyAdminOwner, + string.concat(inputs.spokeLabels[i], ' proxy admin owner') + ); + + for (uint256 j = 0; j < _spokePositionUpdaterRoleSelectors.length; j++) { + (bool allowed, uint32 delay) = accessManager.canCall( + inputs.spokeAdmin, + report.spokeInstanceBatchReports[i].report.spokeProxy, + _spokePositionUpdaterRoleSelectors[j] + ); + assertEq(allowed, inputs.grantRoles ? true : false, 'SpokeAdminRole allowed'); + assertEq(delay, 0, 'SpokeAdminRole delay'); + + assertEq( + accessManager.getTargetFunctionRole( + report.spokeInstanceBatchReports[i].report.spokeProxy, + _spokePositionUpdaterRoleSelectors[j] + ), + Roles.SPOKE_POSITION_UPDATER_ROLE, + 'SpokeAdminRole target function' + ); + } + } + } + + function _checkHubRoles( + IAccessManagerEnumerable accessManager, + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal view { + _checkHubBatchRoles(accessManager, report, inputs); + _checkHubConfiguratorRoles(accessManager, report, inputs); + } + + function _checkHubBatchRoles( + IAccessManagerEnumerable accessManager, + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal view { + if (inputs.hubLabels.length > 0 && inputs.grantRoles) { + assertEq(accessManager.getRoleMemberCount(Roles.HUB_FEE_MINTER_ROLE), 1, 'HubAdminRoleCount'); + assertEq( + accessManager.getRoleMember(Roles.HUB_FEE_MINTER_ROLE, 0), + inputs.hubAdmin, + 'HubAdminRole member - hub admin' + ); + } else { + assertEq(accessManager.getRoleMemberCount(Roles.HUB_FEE_MINTER_ROLE), 0, 'HubAdminRoleCount'); + } + for (uint256 i = 0; i < inputs.hubLabels.length; i++) { + _checkTreasurySpokeRoles( + report.hubBatchReports[i].report.treasurySpoke, + inputs, + inputs.hubLabels[i] + ); + for (uint256 j = 0; j < _hubFeeMinterRoleSelectors.length; j++) { + assertEq( + accessManager.getTargetFunctionRole( + report.hubBatchReports[i].report.hub, + _hubFeeMinterRoleSelectors[j] + ), + Roles.HUB_FEE_MINTER_ROLE, + 'HubAdminRole target function' + ); + + (bool allowed, uint32 delay) = accessManager.canCall( + inputs.hubAdmin, + report.hubBatchReports[i].report.hub, + _hubFeeMinterRoleSelectors[j] + ); + assertEq(allowed, inputs.grantRoles ? true : false, 'HubAdminRole allowed'); + assertEq(delay, 0, 'HubAdminRole delay'); + } + } + } + + function _checkTreasurySpokeRoles( + address treasurySpoke, + FullDeployInputs memory inputs, + string memory label + ) internal view { + assertEq( + Ownable(treasurySpoke).owner(), + inputs.treasurySpokeOwner, + string.concat(label, ' treasury spoke owner') + ); + } + + function _checkHubConfiguratorRoles( + IAccessManagerEnumerable accessManager, + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal view { + if (inputs.hubLabels.length > 0 && inputs.grantRoles) { + assertEq( + accessManager.getRoleMemberCount(Roles.HUB_CONFIGURATOR_ROLE), + 2, + 'HubConfiguratorRole member count' + ); + assertEq( + accessManager.getRoleMember(Roles.HUB_CONFIGURATOR_ROLE, 0), + inputs.hubAdmin, + 'HubConfiguratorRole member - hub admin' + ); + assertEq( + accessManager.getRoleMember(Roles.HUB_CONFIGURATOR_ROLE, 1), + report.configuratorBatchReport.hubConfigurator, + 'HubConfiguratorRole member - hub configurator' + ); + } else { + assertEq( + accessManager.getRoleMemberCount(Roles.HUB_CONFIGURATOR_ROLE), + 0, + 'HubConfiguratorRole member count' + ); + } + for (uint256 i = 0; i < inputs.hubLabels.length; i++) { + for (uint256 j = 0; j < _hubConfiguratorRoleSelectors.length; j++) { + assertEq( + accessManager.getTargetFunctionRole( + report.hubBatchReports[i].report.hub, + _hubConfiguratorRoleSelectors[j] + ), + Roles.HUB_CONFIGURATOR_ROLE, + 'HubConfiguratorRole target function' + ); + bool allowed; + uint32 delay; + + (allowed, delay) = accessManager.canCall( + report.configuratorBatchReport.hubConfigurator, + report.hubBatchReports[i].report.hub, + _hubConfiguratorRoleSelectors[j] + ); + assertEq( + allowed, + inputs.grantRoles ? true : false, + 'HubConfiguratorRole allowed - configurator' + ); + assertEq(delay, 0, 'HubConfiguratorRole delay - configurator'); + + (allowed, delay) = accessManager.canCall( + inputs.hubAdmin, + report.hubBatchReports[i].report.hub, + _hubConfiguratorRoleSelectors[j] + ); + assertEq(allowed, inputs.grantRoles ? true : false, 'HubConfiguratorRole allowed - admin'); + assertEq(delay, 0, 'HubConfiguratorRole delay - admin'); + } + } + } + + function _checkConfiguratorBatchRoles( + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal view { + if (inputs.grantRoles) { + if (inputs.hubLabels.length > 0 && inputs.hubConfiguratorOwner != address(0)) { + assertEq( + Ownable(report.configuratorBatchReport.hubConfigurator).owner(), + inputs.hubConfiguratorOwner, + 'HubConfigurator owner' + ); + } + if (inputs.spokeLabels.length > 0 && inputs.spokeConfiguratorOwner != address(0)) { + assertEq( + Ownable(report.configuratorBatchReport.spokeConfigurator).owner(), + inputs.spokeConfiguratorOwner, + 'SpokeConfigurator owner' + ); + } + } + } + + function _checkGatewayRoles( + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal view { + if (inputs.nativeWrapper != address(0)) { + assertEq( + Ownable(report.gatewaysBatchReport.nativeGateway).owner(), + inputs.gatewayOwner, + 'NativeGateway owner' + ); + assertEq( + Ownable(report.gatewaysBatchReport.signatureGateway).owner(), + inputs.gatewayOwner, + 'SignatureGateway owner' + ); + } + } +} diff --git a/tests/utils/ProxyHelper.sol b/tests/utils/ProxyHelper.sol new file mode 100644 index 000000000..1cc5b5c17 --- /dev/null +++ b/tests/utils/ProxyHelper.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {Vm} from 'forge-std/Vm.sol'; + +library ProxyHelper { + Vm internal constant vm = Vm(address(uint160(uint256(keccak256('hevm cheat code'))))); + + bytes32 internal constant ERC1967_ADMIN_SLOT = + 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + bytes32 internal constant IMPLEMENTATION_SLOT = + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + bytes32 internal constant INITIALIZABLE_STORAGE = + 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; + + function getProxyAdmin(address proxy) internal view returns (address) { + bytes32 slotData = vm.load(proxy, ERC1967_ADMIN_SLOT); + return address(uint160(uint256(slotData))); + } + + function getImplementation(address proxy) internal view returns (address) { + bytes32 slotData = vm.load(proxy, IMPLEMENTATION_SLOT); + return address(uint160(uint256(slotData))); + } + + function getProxyInitializedVersion(address proxy) internal view returns (uint64) { + bytes32 slotData = vm.load(proxy, INITIALIZABLE_STORAGE); + return uint64(uint256(slotData) & ((1 << 64) - 1)); + } +} diff --git a/tests/utils/TestTypes.sol b/tests/utils/TestTypes.sol new file mode 100644 index 000000000..c0226d145 --- /dev/null +++ b/tests/utils/TestTypes.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {WETH9} from 'src/dependencies/weth/WETH9.sol'; +import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; + +library TestTypes { + struct TokenList { + WETH9 weth; + TestnetERC20 usdx; + TestnetERC20 dai; + TestnetERC20 wbtc; + TestnetERC20 usdy; + TestnetERC20 usdz; + } + + struct SpokeReserveId { + address spoke; + uint256 reserveId; + } + + struct TestTokensBatchReport { + address weth; + address[] tokens; + } + + struct TestTokenInput { + string name; + string symbol; + uint8 decimals; + } + + struct TestHubReport { + address hub; + address irStrategy; + address treasurySpoke; + } + + struct TestSpokeReport { + address spoke; + address aaveOracle; + } + + struct TestGatewaysReport { + address signatureGateway; + address nativeGateway; + } + + struct TestConfiguratorReport { + address hubConfigurator; + address spokeConfigurator; + } + + struct TestEnvReport { + address accessManager; + TestHubReport[] hubReports; + TestSpokeReport[] spokeReports; + TestGatewaysReport gatewaysReport; + TestConfiguratorReport configuratorReport; + } + + struct TestTokensReport { + address weth; + address[] testTokens; + } +}