diff --git a/.env.example b/.env.example index 338f500fe..1d042fa37 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,7 @@ +# Deployment +ACCOUNT=deployer +DRY=true # leave blank to broadcast + # Test rpc_endpoints RPC_MAINNET=https://eth.llamarpc.com RPC_AVALANCHE=https://api.avax.network/ext/bc/C/rpc diff --git a/.gitignore b/.gitignore index c92814bc4..8042c958f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ report/ .DS_Store .venv/ + +output/ diff --git a/Makefile b/Makefile index 9081609b9..ff9ba0f41 100644 --- a/Makefile +++ b/Makefile @@ -29,3 +29,18 @@ coverage : make coverage-clean make coverage-report make coverage-badge + +# Deployment +# Step 1:Pre-deploy LiquidationLogic library (required before deploying spokes) +# `make deploy-precompile CHAIN=mainnet` +deploy-precompile :; + FOUNDRY_PROFILE=${CHAIN} forge clean && forge script scripts/LibraryPreCompile.s.sol \ + --rpc-url ${CHAIN} --account ${ACCOUNT} --ffi \ + $(if ${DRY},, --broadcast --verify) \ + +# Step 2: Deploy contracts + grant roles to deployer +# `make deploy-contracts CHAIN=mainnet` +deploy-contracts :; + FOUNDRY_PROFILE=${CHAIN} forge clean && forge script scripts/deploy/AaveV4DeployBatch.s.sol:AaveV4DeployBatchScript \ + --rpc-url ${CHAIN} --account ${ACCOUNT} --slow \ + $(if ${DRY},, --broadcast --verify) \ diff --git a/foundry.toml b/foundry.toml index 6b956fc77..70058993c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,12 @@ 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", path = "./out" }, + { access = "read-write", path = "./output" } +] skip = ["tests/mocks/JsonBindings.sol"] solc_version = "0.8.28" evm_version = "cancun" @@ -49,6 +54,8 @@ gas_snapshot_check = true test = 'tests/gas' isolate = true +[profile.anvil] + [profile.coverage] optimizer = true optimizer_runs = 444444444444 @@ -71,6 +78,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/LibraryPreCompile.s.sol b/scripts/LibraryPreCompile.s.sol new file mode 100644 index 000000000..edbd40663 --- /dev/null +++ b/scripts/LibraryPreCompile.s.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {Script} from 'forge-std/Script.sol'; +import {console2 as console} from 'forge-std/console2.sol'; +import {SpokeDeployUtils} from 'scripts/SpokeDeployUtils.sol'; + +/** + * @dev Deploy LiquidationLogic library using CREATE2 and save the output + * to FOUNDRY_LIBRARIES env variable in .env file. + * This preprocessing step is required before running the main Deploy script, + * as SpokeInstance depends on LiquidationLogic as an external library. + * + * The script will ask you to re-execute if FOUNDRY_LIBRARIES is set but the + * library is not deployed, due to setting mutation of bytecode that could + * result in different library addresses. + * + * Usage: + * forge script scripts/LibraryPreCompile.s.sol --broadcast --fork-url $RPC --ffi + */ +contract LibraryPreCompile is Script { + function run() external { + bool found = SpokeDeployUtils._librariesPathExists(); + + if (found) { + address lastLib = SpokeDeployUtils._getLiquidationLogicAddress(); + if (lastLib.code.length > 0) { + console.log('[LibraryPreCompile] LiquidationLogic detected. Skipping re-deployment.'); + return; + } else { + SpokeDeployUtils._deleteLibrariesPath(); + console.log( + 'LibraryPreCompile: FOUNDRY_LIBRARIES was detected and removed. Please run again to deploy library with a fresh compilation.' + ); + revert('RETRY AGAIN'); + } + } + + vm.startBroadcast(); + SpokeDeployUtils._deployAndWriteLibrariesConfig(); + vm.stopBroadcast(); + + console.log('LibraryPreCompile: FOUNDRY_LIBRARIES set. Run the main deploy script.'); + } +} diff --git a/scripts/SpokeDeployUtils.sol b/scripts/SpokeDeployUtils.sol new file mode 100644 index 000000000..0f619bc3f --- /dev/null +++ b/scripts/SpokeDeployUtils.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {Vm} from 'forge-std/Vm.sol'; +import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; + +/// @title SpokeDeployUtils +/// @notice Utilities for deploying LiquidationLogic as an external library. +/// @dev LiquidationLogic must be deployed before SpokeInstance because SpokeInstance +/// is compiled with via-ir and has references to the library. +/// 1. Run LibraryPreCompile.s.sol to deploy library, writes FOUNDRY_LIBRARIES to .env +/// 2. Run the main deploy script to link via FOUNDRY_LIBRARIES +library SpokeDeployUtils { + Vm internal constant vm = Vm(address(uint160(uint256(keccak256('hevm cheat code'))))); + + // ==================== Library Deployment ==================== + + /// @notice Deploys LiquidationLogic via CREATE2 with salt=0. + /// @dev The CREATE2 factory must already be deployed on the target chain. + /// For Anvil, etch it beforehand (see scripts/deploy/AaveV4DeployBatchAnvil.s.sol). + /// @return The deployed library address. + function deployLiquidationLogic() internal returns (address) { + bytes memory bytecode = vm.getCode('src/spoke/libraries/LiquidationLogic.sol:LiquidationLogic'); + return Create2Utils.create2Deploy(bytes32(0), bytecode); + } + + /// @notice Returns the FOUNDRY_LIBRARIES-compatible string for library linking. + function getLibraryString(address liquidationLogic) internal pure returns (string memory) { + return + string( + abi.encodePacked( + 'src/spoke/libraries/LiquidationLogic.sol:LiquidationLogic:', + vm.toString(liquidationLogic) + ) + ); + } + + /// @notice Deploys LiquidationLogic and appends FOUNDRY_LIBRARIES to .env. + function _deployAndWriteLibrariesConfig() internal { + address liquidationLogic = deployLiquidationLogic(); + + string memory librariesSolcString = getLibraryString(liquidationLogic); + + string memory sedCommand = string( + abi.encodePacked('echo FOUNDRY_LIBRARIES=', librariesSolcString, ' >> .env') + ); + string[] memory command = new string[](3); + + command[0] = 'bash'; + command[1] = '-c'; + command[2] = string(abi.encodePacked('response="$(', sedCommand, ')"; $response;')); + vm.ffi(command); + } + + // ==================== .env Management (FFI) ==================== + + /// @notice Checks if .env contains a FOUNDRY_LIBRARIES entry. + function _librariesPathExists() internal returns (bool) { + string + memory checkCommand = '[ -e .env ] && grep -q "FOUNDRY_LIBRARIES" .env && echo true || echo false'; + string[] memory command = new string[](3); + + command[0] = 'bash'; + command[1] = '-c'; + command[2] = string( + abi.encodePacked( + 'response="$(', + checkCommand, + ')"; cast abi-encode "response(bool)" $response;' + ) + ); + bytes memory res = vm.ffi(command); + + return abi.decode(res, (bool)); + } + + /// @notice Deletes the FOUNDRY_LIBRARIES line from .env. + function _deleteLibrariesPath() internal { + string memory deleteCommand = "sed -i.bak -r '/FOUNDRY_LIBRARIES/d' .env && rm .env.bak"; + string[] memory delCommand = new string[](3); + + delCommand[0] = 'bash'; + delCommand[1] = '-c'; + delCommand[2] = string(abi.encodePacked('response="$(', deleteCommand, ')"; $response;')); + vm.ffi(delCommand); + } + + /// @notice Reads the LiquidationLogic address from FOUNDRY_LIBRARIES in .env. + /// @return The address, or address(0) if not found. + function _getLiquidationLogicAddress() internal returns (address) { + string memory getLibraryAddress = "sed -nr 's/.*LiquidationLogic:([^,]*).*/\\1/p' .env"; + string[] memory getAddressCommand = new string[](3); + + getAddressCommand[0] = 'bash'; + getAddressCommand[1] = '-c'; + getAddressCommand[2] = string( + abi.encodePacked( + 'response="$(', + getLibraryAddress, + ')"; [ -z "$response" ] && cast abi-encode "response(address)" 0x0000000000000000000000000000000000000000 || cast abi-encode "response(address)" $response' + ) + ); + + bytes memory res = vm.ffi(getAddressCommand); + return abi.decode(res, (address)); + } +} diff --git a/scripts/deploy/AaveV4DeployBatchBase.s.sol b/scripts/deploy/AaveV4DeployBatchBase.s.sol new file mode 100644 index 000000000..39cbd7451 --- /dev/null +++ b/scripts/deploy/AaveV4DeployBatchBase.s.sol @@ -0,0 +1,234 @@ +// 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 {BytecodeHelper} from 'src/deployments/utils/libraries/BytecodeHelper.sol'; + +import {Script} from 'forge-std/Script.sol'; + +// solhint-disable quotes +abstract contract AaveV4DeployBatchBaseScript is Script, InputUtils { + struct Lines { + string[] s; + } + + string internal constant OUTPUT_DIR = 'output/reports/deployments/'; + string internal _outputFileName; + Lines internal _promptLines; + Lines internal _summaryLines; + + constructor(string memory outputFileName_) { + _outputFileName = outputFileName_; + } + + function run() external virtual { + vm.createDir(OUTPUT_DIR, true); + MetadataLogger logger = new MetadataLogger(OUTPUT_DIR); + FullDeployInputs memory inputs = _getDeployInputs(); + + vm.startBroadcast(); + (, address deployer, ) = vm.readCallers(); + inputs = _loadWarningsAndSanitizeInputs(inputs, deployer); + + logger.log('CHAIN ID', block.chainid); + logger.log('deployer', deployer); + logger.logHeader1('starting Aave V4 batch deployment'); + + OrchestrationReports.FullDeploymentReport memory report = AaveV4DeployOrchestration + .deployAaveV4( + logger, + deployer, + inputs, + BytecodeHelper.getHubBytecode(), + BytecodeHelper.getSpokeBytecode() + ); + vm.stopBroadcast(); + logger.writeJsonReportMarket(report); + _logDeploySummary(logger); + logger.logHeader1('batch deployment completed'); + logger.logHeader1('saving logs'); + logger.save({fileName: _outputFileName, withTimestamp: true}); + } + + /// @dev Override to provide deployment inputs from any source. + function _getDeployInputs() internal virtual returns (FullDeployInputs memory); + + function _loadWarningsAndSanitizeInputs( + 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; + + // Validate label uniqueness (duplicate labels produce identical CREATE2 salts) + _validateUniqueLabels(inputs.hubLabels, 'hub'); + _validateUniqueLabels(inputs.spokeLabels, 'spoke'); + + _appendSummary('========== DEPLOYMENT SUMMARY =========='); + _logHubs(inputs); + _logSpokes(inputs); + _logNativeTokenGateway(inputs); + _logSignatureGateway(inputs); + _logPositionManagers(inputs); + _logRoles(inputs); + _appendSummary('--------------------------------------------------'); + + // Sanitize zero addresses + if (inputs.grantRoles) { + if (inputs.accessManagerAdmin == address(0)) { + _logWarning(string.concat('access manager admin', message, outcome)); + sanitizedInputs.accessManagerAdmin = deployer; + } + if (inputs.hubConfiguratorAdmin == address(0)) { + _logWarning(string.concat('hub configurator admin', message, outcome)); + sanitizedInputs.hubConfiguratorAdmin = deployer; + } + if (inputs.spokeConfiguratorAdmin == address(0)) { + _logWarning(string.concat('spoke configurator admin', message, outcome)); + sanitizedInputs.spokeConfiguratorAdmin = deployer; + } + if (inputs.hubProxyAdminOwner == address(0)) { + _logWarning(string.concat('hub proxy admin owner', message, outcome)); + sanitizedInputs.hubProxyAdminOwner = deployer; + } + if (inputs.spokeProxyAdminOwner == address(0)) { + _logWarning(string.concat('spoke proxy admin owner', message, outcome)); + sanitizedInputs.spokeProxyAdminOwner = deployer; + } + if (inputs.treasurySpokeOwner == address(0)) { + _logWarning(string.concat('treasury spoke owner', message, outcome)); + sanitizedInputs.treasurySpokeOwner = deployer; + } + if (inputs.spokeAdmin == address(0)) { + _logWarning(string.concat('spoke admin', message, outcome)); + sanitizedInputs.spokeAdmin = deployer; + } + if (inputs.hubAdmin == address(0)) { + _logWarning(string.concat('hub admin', message, outcome)); + sanitizedInputs.hubAdmin = deployer; + } + } else { + _logWarning('roles: deferred (not granted during deployment)'); + sanitizedInputs.treasurySpokeOwner = deployer; + sanitizedInputs.hubProxyAdminOwner = deployer; + sanitizedInputs.spokeProxyAdminOwner = deployer; + } + if (inputs.gatewayOwner == address(0)) { + _logWarning(string.concat('gateway owner', message, outcome)); + sanitizedInputs.gatewayOwner = deployer; + } + if (inputs.positionManagerOwner == address(0)) { + _logWarning(string.concat('position manager owner', message, outcome)); + sanitizedInputs.positionManagerOwner = deployer; + } + if (inputs.salt == bytes32(0)) { + _logWarning('salt is zero'); + } + + _executeUserPrompt(); + return sanitizedInputs; + } + + function _logHubs(FullDeployInputs memory inputs) internal { + if (inputs.hubLabels.length > 0) { + _appendSummary(string.concat('hubs to deploy: ', vm.toString(inputs.hubLabels.length))); + for (uint256 i; i < inputs.hubLabels.length; i++) { + _appendSummary(string.concat(' - ', inputs.hubLabels[i])); + } + } else { + _logWarning('no hubs will be deployed'); + } + } + + function _logSpokes(FullDeployInputs memory inputs) internal { + if (inputs.spokeLabels.length > 0) { + _appendSummary(string.concat('spokes to deploy: ', vm.toString(inputs.spokeLabels.length))); + for (uint256 i; i < inputs.spokeLabels.length; i++) { + _appendSummary(string.concat(' - ', inputs.spokeLabels[i])); + } + } else { + _logWarning('no spokes will be deployed'); + } + } + + function _logNativeTokenGateway(FullDeployInputs memory inputs) internal { + if (inputs.deployNativeTokenGateway) { + if (inputs.nativeWrapper == address(0)) { + _logWarning('deployNativeTokenGateway is true but nativeWrapper is zero address'); + } else { + _appendSummary('nativeTokenGateway will be deployed'); + } + } else { + _appendSummary('nativeTokenGateway: skipped (deployNativeTokenGateway is false)'); + } + } + + function _logSignatureGateway(FullDeployInputs memory inputs) internal { + if (inputs.deploySignatureGateway) { + _appendSummary('signatureGateway will be deployed'); + } else { + _appendSummary('signatureGateway: skipped (deploySignatureGateway is false)'); + } + } + + function _logPositionManagers(FullDeployInputs memory inputs) internal { + if (inputs.deployPositionManagers) { + _appendSummary('positionManagers (giver/taker/config) will be deployed'); + } else { + _appendSummary('positionManagers: skipped (deployPositionManagers is false)'); + } + } + + function _logRoles(FullDeployInputs memory inputs) internal { + if (inputs.grantRoles) { + _appendSummary('roles: will be granted during deployment'); + } else { + _appendSummary('roles: deferred (not granted during deployment)'); + } + } + + function _executeUserPrompt() internal virtual { + if (_promptLines.s.length > 0) { + string memory ack = vm.prompt( + string.concat(_joinLines(_promptLines), "\nenter 'y' to continue") + ); + if (keccak256(bytes(ack)) != keccak256(bytes('y'))) { + revert('user did not acknowledge. Please try again.'); + } + } + } + + function _appendSummary(string memory line) internal virtual { + _promptLines.s.push(line); + _summaryLines.s.push(line); + } + + function _logWarning(string memory warning) internal virtual { + _promptLines.s.push(string.concat('WARNING: ', warning)); + } + + /// @dev Writes the deployment summary to the logger (called after deployment). + function _logDeploySummary(MetadataLogger logger) internal virtual { + for (uint256 i; i < _summaryLines.s.length; i++) { + logger.log(_summaryLines.s[i]); + } + } + + function _joinLines(Lines storage lines) internal view virtual returns (string memory) { + uint256 n = lines.s.length; + if (n == 0) { + return ''; + } + string memory out = lines.s[0]; + for (uint256 i = 1; i < n; i++) { + out = string.concat(out, '\n', lines.s[i]); + } + return string.concat(out, '\n'); + } +} diff --git a/scripts/deploy/examples/AaveV4DeployAnvil.s.sol b/scripts/deploy/examples/AaveV4DeployAnvil.s.sol new file mode 100644 index 000000000..44533c5b6 --- /dev/null +++ b/scripts/deploy/examples/AaveV4DeployAnvil.s.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {AaveV4DeployBatchBaseScript} from 'scripts/deploy/AaveV4DeployBatchBase.s.sol'; +import {WETH9} from 'src/dependencies/weth/WETH9.sol'; +import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; + +/// @notice Anvil-only demo deploy script with hardcoded inputs for local testing. +/// @dev Requires LiquidationLogic library pre-deployed (SpokeInstance depends on it). +/// Step 1: anvil (in separate terminal) +/// Step 2: set Create2Factory on anvil: +/// Run: cast rpc anvil_setCode 0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3 --rpc-url http://127.0.0.1:8545 +/// Step 2: +/// Run: forge script scripts/LibraryPreCompile.s.sol --broadcast --rpc-url http://127.0.0.1:8545 --ffi --sender 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 --unlocked +/// Step 3: +/// Run: forge script scripts/deploy/examples/AaveV4DeployAnvil.s.sol --broadcast --rpc-url http://127.0.0.1:8545 --sender 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 --unlocked +contract AaveV4DeployAnvil is AaveV4DeployBatchBaseScript { + address public weth; + + constructor() AaveV4DeployBatchBaseScript('anvil-deploy') { + weth = address(new WETH9()); + } + + function _getDeployInputs() internal view override returns (FullDeployInputs memory inputs) { + string[] memory hubLabels = new string[](1); + hubLabels[0] = 'core'; + + string[] memory spokeLabels = new string[](1); + spokeLabels[0] = 'mainnet'; + + inputs = FullDeployInputs({ + accessManagerAdmin: address(0), + hubAdmin: address(0), + hubProxyAdminOwner: address(0), + hubConfiguratorAdmin: address(0), + treasurySpokeOwner: address(0), + spokeAdmin: address(0), + spokeProxyAdminOwner: address(0), + spokeConfiguratorAdmin: address(1), + gatewayOwner: address(2), + positionManagerOwner: address(3), + nativeWrapper: weth, + deployNativeTokenGateway: true, + deploySignatureGateway: true, + deployPositionManagers: true, + grantRoles: true, + hubLabels: hubLabels, + spokeLabels: spokeLabels, + spokeMaxReservesLimits: new uint16[](0), + salt: keccak256('anvil-test') + }); + } + + /// @dev Skip user prompt on anvil. + function _executeUserPrompt() internal override {} +} diff --git a/scripts/verification/inputs/config.json b/scripts/verification/inputs/config.json new file mode 100644 index 000000000..cbf95177f --- /dev/null +++ b/scripts/verification/inputs/config.json @@ -0,0 +1,1700 @@ +{ + "defaults": { + "spokeRegistration": { + "riskPremiumThreshold": 0, + "active": true, + "halted": false + }, + "reserve": { + "receiveSharesEnabled": true, + "frozen": false, + "paused": false, + "liquidationFee": 0, + "maxLiquidationBonus": 10000 + }, + "asset": { + "liquidityFee": 0 + }, + "tokenize": { + "enabled": true + } + }, + "tokens": { + "AAVE": { + "address": "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9", + "priceFeed": "0x547a514d5e3769680Ce22B2361c10Ea13619e8a9" + }, + "EURC": { + "address": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c", + "priceFeed": "0x04F84020Fdf10d9ee64D1dcC2986EDF2F556DA11" + }, + "GHO": { + "address": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f", + "priceFeed": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd" + }, + "LBTC": { + "address": "0x8236a87084f8B84306f72007F36F2618A5634494", + "priceFeed": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c" + }, + "LINK": { + "address": "0x514910771AF9Ca656af840dff83E8264EcF986CA", + "priceFeed": "0x2c1d072e956AFFC0D435Cb7AC38EF18d24d9127c" + }, + "PT_USDE_7MAY2026": { + "address": "0xAeBf0Bb9f57E89260d57f31AF34eB58657d96Ce0", + "priceFeed": "0x0a72df02CE3E4185b6CEDf561f0AE651E9BeE235" + }, + "PT_sUSDE_7MAY2026": { + "address": "0x3de0ff76E8b528C092d47b9DaC775931cef80F49", + "priceFeed": "0xa0dc0249c32fa79e8B9b17c735908a60b1141B40" + }, + "RLUSD": { + "address": "0x8292Bb45bf1Ee4d140127049757c2E0fF06317eD", + "priceFeed": "0xf0eaC18E908B34770FDEe46d069c846bDa866759" + }, + "USDC": { + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "priceFeed": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994" + }, + "USDG": { + "address": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D", + "priceFeed": "0x14f0737d6b705259e521EA6E9E3506AC78dBd311" + }, + "USDT": { + "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "priceFeed": "0x260326c220E469358846b187eE53328303Efe19C" + }, + "USDe": { + "address": "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3", + "priceFeed": "0xC26D4a1c46d884cfF6dE9800B6aE7A8Cf48B4Ff8" + }, + "WBTC": { + "address": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", + "priceFeed": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c" + }, + "WETH": { + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "priceFeed": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419" + }, + "XAUt": { + "address": "0x68749665FF8D2d112Fa859AA293F07A622782F38", + "priceFeed": "0x214eD9Da11D2fbe465a6fc601a91E62EbEc1a0D6" + }, + "cbBTC": { + "address": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", + "priceFeed": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c" + }, + "frxUSD": { + "address": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29", + "priceFeed": "0x25ded2f9ae6ae9416693ab63abe3ab25493861fd" + }, + "rsETH": { + "address": "0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7", + "priceFeed": "0x47f52b2e43d0386cf161e001835b03ad49889e3b" + }, + "sUSDe": { + "address": "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497", + "priceFeed": "0x42bc86f2f08419280a99d8fbEa4672e7c30a86ec" + }, + "weETH": { + "address": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee", + "priceFeed": "0xf112af6f0a332b815fbef3ff932c057e570b62d3" + }, + "wstETH": { + "address": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + "priceFeed": "0x869C9Ae2C8fbe82a8b0F768b9F791f89E083222C" + } + }, + "hubs": [ + { + "key": "PRIME_HUB" + }, + { + "key": "CORE_HUB" + }, + { + "key": "PLUS_HUB" + } + ], + "spokes": [ + { + "key": "MAIN_SPOKE", + "liquidationConfig": { + "targetHealthFactor": "1240000000000000000", + "healthFactorForMaxBonus": "900000000000000000", + "liquidationBonusFactor": 9000 + } + }, + { + "key": "LIDO_ESPOKE", + "liquidationConfig": { + "targetHealthFactor": "1013700000000000000", + "healthFactorForMaxBonus": "990000000000000000", + "liquidationBonusFactor": 10000 + } + }, + { + "key": "ETHERFI_ESPOKE", + "liquidationConfig": { + "targetHealthFactor": "1019100000000000000", + "healthFactorForMaxBonus": "990000000000000000", + "liquidationBonusFactor": 10000 + } + }, + { + "key": "KELP_ESPOKE", + "liquidationConfig": { + "targetHealthFactor": "1021800000000000000", + "healthFactorForMaxBonus": "990000000000000000", + "liquidationBonusFactor": 10000 + } + }, + { + "key": "LOMBARD_BTC_SPOKE", + "liquidationConfig": { + "targetHealthFactor": "1061500000000000000", + "healthFactorForMaxBonus": "990000000000000000", + "liquidationBonusFactor": 10000 + } + }, + { + "key": "GOLD_SPOKE", + "liquidationConfig": { + "targetHealthFactor": "1307500000000000000", + "healthFactorForMaxBonus": "900000000000000000", + "liquidationBonusFactor": 9000 + } + }, + { + "key": "FOREX_SPOKE", + "liquidationConfig": { + "targetHealthFactor": "1044200000000000000", + "healthFactorForMaxBonus": "990000000000000000", + "liquidationBonusFactor": 10000 + } + }, + { + "key": "BLUECHIP_SPOKE", + "liquidationConfig": { + "targetHealthFactor": "1174000000000000000", + "healthFactorForMaxBonus": "900000000000000000", + "liquidationBonusFactor": 9000 + } + }, + { + "key": "ETHENA_ECOSYSTEM_SPOKE", + "liquidationConfig": { + "targetHealthFactor": "1033200000000000000", + "healthFactorForMaxBonus": "990000000000000000", + "liquidationBonusFactor": 10000 + } + }, + { + "key": "ETHENA_CORRELATED_SPOKE", + "liquidationConfig": { + "targetHealthFactor": "1027700000000000000", + "healthFactorForMaxBonus": "990000000000000000", + "liquidationBonusFactor": 10000 + } + } + ], + "assets": [ + { + "tokenKey": "WETH", + "hubKey": "CORE_HUB", + "irData": { + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 235, + "rateGrowthAfterOptimal": 1400, + "optimalUsageRatio": 9200 + }, + "liquidityFee": 1500, + "tokenize": { + "name": "Wrapped Aave Core WETH", + "symbol": "waCoreWETH", + "addCap": 250 + } + }, + { + "tokenKey": "wstETH", + "hubKey": "CORE_HUB", + "irData": { + "optimalUsageRatio": 9900, + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 0, + "rateGrowthAfterOptimal": 0 + }, + "tokenize": { + "name": "Wrapped Aave Core wstETH", + "symbol": "waCorewstETH", + "addCap": 0 + } + }, + { + "tokenKey": "weETH", + "hubKey": "CORE_HUB", + "irData": { + "optimalUsageRatio": 9900, + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 0, + "rateGrowthAfterOptimal": 0 + }, + "tokenize": { + "name": "Wrapped Aave Core weETH", + "symbol": "waCoreweETH", + "addCap": 0 + } + }, + { + "tokenKey": "rsETH", + "hubKey": "CORE_HUB", + "irData": { + "optimalUsageRatio": 9900, + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 0, + "rateGrowthAfterOptimal": 0 + }, + "tokenize": { + "name": "Wrapped Aave Core rsETH", + "symbol": "waCorersETH", + "addCap": 0 + } + }, + { + "tokenKey": "USDT", + "hubKey": "CORE_HUB", + "irData": { + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 400, + "rateGrowthAfterOptimal": 2000, + "optimalUsageRatio": 9200 + }, + "liquidityFee": 1000, + "tokenize": { + "name": "Wrapped Aave Core USDT", + "symbol": "waCoreUSDT", + "addCap": 312500 + } + }, + { + "tokenKey": "USDC", + "hubKey": "CORE_HUB", + "irData": { + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 400, + "rateGrowthAfterOptimal": 2000, + "optimalUsageRatio": 9200 + }, + "liquidityFee": 1000, + "tokenize": { + "name": "Wrapped Aave Core USDC", + "symbol": "waCoreUSDC", + "addCap": 312500 + } + }, + { + "tokenKey": "GHO", + "hubKey": "CORE_HUB", + "irData": { + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 400, + "rateGrowthAfterOptimal": 3000, + "optimalUsageRatio": 9000 + }, + "liquidityFee": 1000, + "tokenize": { + "name": "Wrapped Aave Core GHO", + "symbol": "waCoreGHO", + "addCap": 125000 + } + }, + { + "tokenKey": "RLUSD", + "hubKey": "CORE_HUB", + "irData": { + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 400, + "rateGrowthAfterOptimal": 3500, + "optimalUsageRatio": 8000 + }, + "liquidityFee": 2000, + "tokenize": { + "name": "Wrapped Aave Core RLUSD", + "symbol": "waCoreRLUSD", + "addCap": 125000 + } + }, + { + "tokenKey": "USDG", + "hubKey": "CORE_HUB", + "irData": { + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 400, + "rateGrowthAfterOptimal": 3500, + "optimalUsageRatio": 9000 + }, + "liquidityFee": 2000, + "tokenize": { + "name": "Wrapped Aave Core USDG", + "symbol": "waCoreUSDG", + "addCap": 125000 + } + }, + { + "tokenKey": "frxUSD", + "hubKey": "CORE_HUB", + "irData": { + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 400, + "rateGrowthAfterOptimal": 3500, + "optimalUsageRatio": 9000 + }, + "liquidityFee": 2000, + "tokenize": { + "name": "Wrapped Aave Core frxUSD", + "symbol": "waCorefrxUSD", + "addCap": 125000 + } + }, + { + "tokenKey": "EURC", + "hubKey": "CORE_HUB", + "irData": { + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 550, + "rateGrowthAfterOptimal": 3500, + "optimalUsageRatio": 9000 + }, + "liquidityFee": 1000, + "tokenize": { + "name": "Wrapped Aave Core EURC", + "symbol": "waCoreEURC", + "addCap": 112500 + } + }, + { + "tokenKey": "WBTC", + "hubKey": "CORE_HUB", + "irData": { + "baseDrawnRate": 25, + "rateGrowthBeforeOptimal": 400, + "rateGrowthAfterOptimal": 6000, + "optimalUsageRatio": 8000 + }, + "liquidityFee": 2000, + "tokenize": { + "name": "Wrapped Aave Core WBTC", + "symbol": "waCoreWBTC", + "addCap": 0 + } + }, + { + "tokenKey": "cbBTC", + "hubKey": "CORE_HUB", + "irData": { + "baseDrawnRate": 25, + "rateGrowthBeforeOptimal": 400, + "rateGrowthAfterOptimal": 6000, + "optimalUsageRatio": 8000 + }, + "liquidityFee": 2000, + "tokenize": { + "name": "Wrapped Aave Core cbBTC", + "symbol": "waCorecbBTC", + "addCap": 0 + } + }, + { + "tokenKey": "LBTC", + "hubKey": "CORE_HUB", + "irData": { + "optimalUsageRatio": 9900, + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 0, + "rateGrowthAfterOptimal": 0 + }, + "tokenize": { + "name": "Wrapped Aave Core LBTC", + "symbol": "waCoreLBTC", + "addCap": 0 + } + }, + { + "tokenKey": "XAUt", + "hubKey": "CORE_HUB", + "irData": { + "optimalUsageRatio": 9900, + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 0, + "rateGrowthAfterOptimal": 0 + }, + "tokenize": { + "name": "Wrapped Aave Core XAUt", + "symbol": "waCoreXAUt", + "addCap": 0 + } + }, + { + "tokenKey": "AAVE", + "hubKey": "CORE_HUB", + "irData": { + "optimalUsageRatio": 9900, + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 0, + "rateGrowthAfterOptimal": 0 + }, + "tokenize": { + "name": "Wrapped Aave Core AAVE", + "symbol": "waCoreAAVE", + "addCap": 0 + } + }, + { + "tokenKey": "LINK", + "hubKey": "CORE_HUB", + "irData": { + "optimalUsageRatio": 9900, + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 0, + "rateGrowthAfterOptimal": 0 + }, + "tokenize": { + "name": "Wrapped Aave Core LINK", + "symbol": "waCoreLINK", + "addCap": 0 + } + }, + { + "tokenKey": "WETH", + "hubKey": "PRIME_HUB", + "irData": { + "optimalUsageRatio": 9900, + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 0, + "rateGrowthAfterOptimal": 0 + }, + "tokenize": { + "name": "Wrapped Aave Prime WETH", + "symbol": "waPrimeWETH", + "addCap": 0 + } + }, + { + "tokenKey": "WBTC", + "hubKey": "PRIME_HUB", + "irData": { + "optimalUsageRatio": 9900, + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 0, + "rateGrowthAfterOptimal": 0 + }, + "tokenize": { + "name": "Wrapped Aave Prime WBTC", + "symbol": "waPrimeWBTC", + "addCap": 0 + } + }, + { + "tokenKey": "cbBTC", + "hubKey": "PRIME_HUB", + "irData": { + "optimalUsageRatio": 9900, + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 0, + "rateGrowthAfterOptimal": 0 + }, + "tokenize": { + "name": "Wrapped Aave Prime cbBTC", + "symbol": "waPrimecbBTC", + "addCap": 0 + } + }, + { + "tokenKey": "wstETH", + "hubKey": "PRIME_HUB", + "irData": { + "optimalUsageRatio": 9900, + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 0, + "rateGrowthAfterOptimal": 0 + }, + "tokenize": { + "name": "Wrapped Aave Prime wstETH", + "symbol": "waPrimewstETH", + "addCap": 0 + } + }, + { + "tokenKey": "USDC", + "hubKey": "PRIME_HUB", + "irData": { + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 400, + "rateGrowthAfterOptimal": 2000, + "optimalUsageRatio": 9200 + }, + "liquidityFee": 1000, + "tokenize": { + "name": "Wrapped Aave Prime USDC", + "symbol": "waPrimeUSDC", + "addCap": 37500 + } + }, + { + "tokenKey": "USDT", + "hubKey": "PRIME_HUB", + "irData": { + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 400, + "rateGrowthAfterOptimal": 2000, + "optimalUsageRatio": 9200 + }, + "liquidityFee": 1000, + "tokenize": { + "name": "Wrapped Aave Prime USDT", + "symbol": "waPrimeUSDT", + "addCap": 37500 + } + }, + { + "tokenKey": "GHO", + "hubKey": "PRIME_HUB", + "irData": { + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 400, + "rateGrowthAfterOptimal": 3000, + "optimalUsageRatio": 9000 + }, + "liquidityFee": 1000, + "tokenize": { + "name": "Wrapped Aave Prime GHO", + "symbol": "waPrimeGHO", + "addCap": 125000 + } + }, + { + "tokenKey": "PT_sUSDE_7MAY2026", + "hubKey": "PLUS_HUB", + "irData": { + "optimalUsageRatio": 9900, + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 0, + "rateGrowthAfterOptimal": 0 + }, + "tokenize": { + "name": "Tokenized Aave Plus PT_sUSDE_7MAY2026", + "symbol": "aPlus-PT_sUSDE_7MAY2026", + "addCap": 0 + } + }, + { + "tokenKey": "PT_USDE_7MAY2026", + "hubKey": "PLUS_HUB", + "irData": { + "optimalUsageRatio": 9900, + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 0, + "rateGrowthAfterOptimal": 0 + }, + "tokenize": { + "name": "Tokenized Aave Plus PT_USDE_7MAY2026", + "symbol": "aPlus-PT_USDE_7MAY2026", + "addCap": 0 + } + }, + { + "tokenKey": "sUSDe", + "hubKey": "PLUS_HUB", + "irData": { + "optimalUsageRatio": 9900, + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 0, + "rateGrowthAfterOptimal": 0 + }, + "tokenize": { + "name": "Wrapped Aave Plus sUSDe", + "symbol": "waPlussUSDe", + "addCap": 0 + } + }, + { + "tokenKey": "USDe", + "hubKey": "PLUS_HUB", + "irData": { + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 450, + "rateGrowthAfterOptimal": 3000, + "optimalUsageRatio": 9000 + }, + "liquidityFee": 2500, + "tokenize": { + "name": "Wrapped Aave Plus USDe", + "symbol": "waPlusUSDe", + "addCap": 78000 + } + }, + { + "tokenKey": "USDC", + "hubKey": "PLUS_HUB", + "irData": { + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 450, + "rateGrowthAfterOptimal": 2000, + "optimalUsageRatio": 9200 + }, + "liquidityFee": 1500, + "tokenize": { + "name": "Wrapped Aave Plus USDC", + "symbol": "waPlusUSDC", + "addCap": 37500 + } + }, + { + "tokenKey": "GHO", + "hubKey": "PLUS_HUB", + "irData": { + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 450, + "rateGrowthAfterOptimal": 3000, + "optimalUsageRatio": 9000 + }, + "liquidityFee": 1000, + "tokenize": { + "name": "Wrapped Aave Plus GHO", + "symbol": "waPlusGHO", + "addCap": 125000 + } + }, + { + "tokenKey": "USDT", + "hubKey": "PLUS_HUB", + "irData": { + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 450, + "rateGrowthAfterOptimal": 2000, + "optimalUsageRatio": 9200 + }, + "liquidityFee": 1500, + "tokenize": { + "name": "Wrapped Aave Plus USDT", + "symbol": "waPlusUSDT", + "addCap": 37500 + } + } + ], + "spokeRegistrations": [ + { + "assetKey": "WETH", + "hubKey": "CORE_HUB", + "spokeKey": "MAIN_SPOKE", + "addCap": 1500, + "drawCap": 130 + }, + { + "assetKey": "wstETH", + "hubKey": "CORE_HUB", + "spokeKey": "MAIN_SPOKE", + "addCap": 229, + "drawCap": 0 + }, + { + "assetKey": "weETH", + "hubKey": "CORE_HUB", + "spokeKey": "MAIN_SPOKE", + "addCap": 58, + "drawCap": 0 + }, + { + "assetKey": "WBTC", + "hubKey": "CORE_HUB", + "spokeKey": "MAIN_SPOKE", + "addCap": 16, + "drawCap": 1 + }, + { + "assetKey": "cbBTC", + "hubKey": "CORE_HUB", + "spokeKey": "MAIN_SPOKE", + "addCap": 13, + "drawCap": 1 + }, + { + "assetKey": "AAVE", + "hubKey": "CORE_HUB", + "spokeKey": "MAIN_SPOKE", + "addCap": 5000, + "drawCap": 0 + }, + { + "assetKey": "LINK", + "hubKey": "CORE_HUB", + "spokeKey": "MAIN_SPOKE", + "addCap": 31250, + "drawCap": 0 + }, + { + "assetKey": "USDC", + "hubKey": "CORE_HUB", + "spokeKey": "MAIN_SPOKE", + "addCap": 1250000, + "drawCap": 1250000 + }, + { + "assetKey": "USDT", + "hubKey": "CORE_HUB", + "spokeKey": "MAIN_SPOKE", + "addCap": 1250000, + "drawCap": 1250000 + }, + { + "assetKey": "EURC", + "hubKey": "CORE_HUB", + "spokeKey": "MAIN_SPOKE", + "addCap": 225000, + "drawCap": 150000 + }, + { + "assetKey": "RLUSD", + "hubKey": "CORE_HUB", + "spokeKey": "MAIN_SPOKE", + "addCap": 500000, + "drawCap": 340000 + }, + { + "assetKey": "USDG", + "hubKey": "CORE_HUB", + "spokeKey": "MAIN_SPOKE", + "addCap": 500000, + "drawCap": 340000 + }, + { + "assetKey": "frxUSD", + "hubKey": "CORE_HUB", + "spokeKey": "MAIN_SPOKE", + "addCap": 500000, + "drawCap": 312500 + }, + { + "assetKey": "GHO", + "hubKey": "CORE_HUB", + "spokeKey": "MAIN_SPOKE", + "addCap": 500000, + "drawCap": 500000 + }, + { + "assetKey": "wstETH", + "hubKey": "CORE_HUB", + "spokeKey": "LIDO_ESPOKE", + "addCap": 406, + "drawCap": 0 + }, + { + "assetKey": "WETH", + "hubKey": "CORE_HUB", + "spokeKey": "LIDO_ESPOKE", + "addCap": 0, + "drawCap": 441 + }, + { + "assetKey": "weETH", + "hubKey": "CORE_HUB", + "spokeKey": "ETHERFI_ESPOKE", + "addCap": 500, + "drawCap": 0 + }, + { + "assetKey": "WETH", + "hubKey": "CORE_HUB", + "spokeKey": "ETHERFI_ESPOKE", + "addCap": 0, + "drawCap": 530 + }, + { + "assetKey": "rsETH", + "hubKey": "CORE_HUB", + "spokeKey": "KELP_ESPOKE", + "addCap": 563, + "drawCap": 0 + }, + { + "assetKey": "WETH", + "hubKey": "CORE_HUB", + "spokeKey": "KELP_ESPOKE", + "addCap": 0, + "drawCap": 588 + }, + { + "assetKey": "LBTC", + "hubKey": "CORE_HUB", + "spokeKey": "LOMBARD_BTC_SPOKE", + "addCap": 9, + "drawCap": 0 + }, + { + "assetKey": "WBTC", + "hubKey": "CORE_HUB", + "spokeKey": "LOMBARD_BTC_SPOKE", + "addCap": 0, + "drawCap": 5 + }, + { + "assetKey": "cbBTC", + "hubKey": "CORE_HUB", + "spokeKey": "LOMBARD_BTC_SPOKE", + "addCap": 0, + "drawCap": 3 + }, + { + "assetKey": "XAUt", + "hubKey": "CORE_HUB", + "spokeKey": "GOLD_SPOKE", + "addCap": 125, + "drawCap": 0 + }, + { + "assetKey": "USDC", + "hubKey": "CORE_HUB", + "spokeKey": "GOLD_SPOKE", + "addCap": 0, + "drawCap": 125000 + }, + { + "assetKey": "RLUSD", + "hubKey": "CORE_HUB", + "spokeKey": "GOLD_SPOKE", + "addCap": 0, + "drawCap": 62500 + }, + { + "assetKey": "USDG", + "hubKey": "CORE_HUB", + "spokeKey": "GOLD_SPOKE", + "addCap": 0, + "drawCap": 62500 + }, + { + "assetKey": "frxUSD", + "hubKey": "CORE_HUB", + "spokeKey": "GOLD_SPOKE", + "addCap": 0, + "drawCap": 62500 + }, + { + "assetKey": "EURC", + "hubKey": "CORE_HUB", + "spokeKey": "GOLD_SPOKE", + "addCap": 0, + "drawCap": 50000 + }, + { + "assetKey": "GHO", + "hubKey": "CORE_HUB", + "spokeKey": "GOLD_SPOKE", + "addCap": 0, + "drawCap": 62500 + }, + { + "assetKey": "USDT", + "hubKey": "CORE_HUB", + "spokeKey": "GOLD_SPOKE", + "addCap": 0, + "drawCap": 125000 + }, + { + "assetKey": "EURC", + "hubKey": "CORE_HUB", + "spokeKey": "FOREX_SPOKE", + "addCap": 300000, + "drawCap": 312500 + }, + { + "assetKey": "USDC", + "hubKey": "CORE_HUB", + "spokeKey": "FOREX_SPOKE", + "addCap": 187500, + "drawCap": 50000 + }, + { + "assetKey": "USDT", + "hubKey": "CORE_HUB", + "spokeKey": "FOREX_SPOKE", + "addCap": 200000, + "drawCap": 50000 + }, + { + "assetKey": "RLUSD", + "hubKey": "CORE_HUB", + "spokeKey": "FOREX_SPOKE", + "addCap": 0, + "drawCap": 90000 + }, + { + "assetKey": "USDG", + "hubKey": "CORE_HUB", + "spokeKey": "FOREX_SPOKE", + "addCap": 0, + "drawCap": 90000 + }, + { + "assetKey": "frxUSD", + "hubKey": "CORE_HUB", + "spokeKey": "FOREX_SPOKE", + "addCap": 0, + "drawCap": 62500 + }, + { + "assetKey": "GHO", + "hubKey": "CORE_HUB", + "spokeKey": "FOREX_SPOKE", + "addCap": 0, + "drawCap": 12500 + }, + { + "assetKey": "WETH", + "hubKey": "PRIME_HUB", + "spokeKey": "BLUECHIP_SPOKE", + "addCap": 130, + "drawCap": 0 + }, + { + "assetKey": "WBTC", + "hubKey": "PRIME_HUB", + "spokeKey": "BLUECHIP_SPOKE", + "addCap": 6, + "drawCap": 0 + }, + { + "assetKey": "cbBTC", + "hubKey": "PRIME_HUB", + "spokeKey": "BLUECHIP_SPOKE", + "addCap": 5, + "drawCap": 0 + }, + { + "assetKey": "wstETH", + "hubKey": "PRIME_HUB", + "spokeKey": "BLUECHIP_SPOKE", + "addCap": 114, + "drawCap": 0 + }, + { + "assetKey": "USDC", + "hubKey": "PRIME_HUB", + "spokeKey": "BLUECHIP_SPOKE", + "addCap": 150000, + "drawCap": 175000 + }, + { + "assetKey": "USDT", + "hubKey": "PRIME_HUB", + "spokeKey": "BLUECHIP_SPOKE", + "addCap": 150000, + "drawCap": 187500 + }, + { + "assetKey": "GHO", + "hubKey": "PRIME_HUB", + "spokeKey": "BLUECHIP_SPOKE", + "addCap": 500000, + "drawCap": 562500 + }, + { + "assetKey": "USDC", + "hubKey": "CORE_HUB", + "spokeKey": "BLUECHIP_SPOKE", + "addCap": 0, + "drawCap": 125000 + }, + { + "assetKey": "frxUSD", + "hubKey": "CORE_HUB", + "spokeKey": "BLUECHIP_SPOKE", + "addCap": 0, + "drawCap": 62500 + }, + { + "assetKey": "EURC", + "hubKey": "CORE_HUB", + "spokeKey": "BLUECHIP_SPOKE", + "addCap": 0, + "drawCap": 50000 + }, + { + "assetKey": "USDT", + "hubKey": "CORE_HUB", + "spokeKey": "BLUECHIP_SPOKE", + "addCap": 0, + "drawCap": 125000 + }, + { + "assetKey": "PT_USDE_7MAY2026", + "hubKey": "PLUS_HUB", + "spokeKey": "ETHENA_ECOSYSTEM_SPOKE", + "addCap": 250000, + "drawCap": 0 + }, + { + "assetKey": "PT_sUSDE_7MAY2026", + "hubKey": "PLUS_HUB", + "spokeKey": "ETHENA_ECOSYSTEM_SPOKE", + "addCap": 1400000, + "drawCap": 0 + }, + { + "assetKey": "sUSDe", + "hubKey": "PLUS_HUB", + "spokeKey": "ETHENA_ECOSYSTEM_SPOKE", + "addCap": 375000, + "drawCap": 0 + }, + { + "assetKey": "USDe", + "hubKey": "PLUS_HUB", + "spokeKey": "ETHENA_ECOSYSTEM_SPOKE", + "addCap": 312500, + "drawCap": 300000 + }, + { + "assetKey": "USDC", + "hubKey": "PLUS_HUB", + "spokeKey": "ETHENA_ECOSYSTEM_SPOKE", + "addCap": 150000, + "drawCap": 187500 + }, + { + "assetKey": "USDT", + "hubKey": "PLUS_HUB", + "spokeKey": "ETHENA_ECOSYSTEM_SPOKE", + "addCap": 150000, + "drawCap": 187500 + }, + { + "assetKey": "GHO", + "hubKey": "PLUS_HUB", + "spokeKey": "ETHENA_ECOSYSTEM_SPOKE", + "addCap": 500000, + "drawCap": 562500 + }, + { + "assetKey": "USDC", + "hubKey": "CORE_HUB", + "spokeKey": "ETHENA_ECOSYSTEM_SPOKE", + "addCap": 0, + "drawCap": 125000 + }, + { + "assetKey": "frxUSD", + "hubKey": "CORE_HUB", + "spokeKey": "ETHENA_ECOSYSTEM_SPOKE", + "addCap": 0, + "drawCap": 62500 + }, + { + "assetKey": "USDT", + "hubKey": "CORE_HUB", + "spokeKey": "ETHENA_ECOSYSTEM_SPOKE", + "addCap": 0, + "drawCap": 125000 + }, + { + "assetKey": "PT_USDE_7MAY2026", + "hubKey": "PLUS_HUB", + "spokeKey": "ETHENA_CORRELATED_SPOKE", + "addCap": 50000, + "drawCap": 0 + }, + { + "assetKey": "PT_sUSDE_7MAY2026", + "hubKey": "PLUS_HUB", + "spokeKey": "ETHENA_CORRELATED_SPOKE", + "addCap": 400000, + "drawCap": 0 + }, + { + "assetKey": "sUSDe", + "hubKey": "PLUS_HUB", + "spokeKey": "ETHENA_CORRELATED_SPOKE", + "addCap": 250000, + "drawCap": 0 + }, + { + "assetKey": "USDe", + "hubKey": "PLUS_HUB", + "spokeKey": "ETHENA_CORRELATED_SPOKE", + "addCap": 312500, + "drawCap": 325000 + } + ], + "reserves": [ + { + "spokeKey": "MAIN_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "WETH", + "borrowable": true, + "collateralFactor": 8300, + "collateralRisk": 0, + "maxLiquidationBonus": 10555, + "liquidationFee": 1000 + }, + { + "spokeKey": "MAIN_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "wstETH", + "borrowable": false, + "collateralFactor": 8000, + "collateralRisk": 0, + "maxLiquidationBonus": 10666, + "liquidationFee": 1000 + }, + { + "spokeKey": "MAIN_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "weETH", + "borrowable": false, + "collateralFactor": 8000, + "collateralRisk": 0, + "maxLiquidationBonus": 10777, + "liquidationFee": 1000 + }, + { + "spokeKey": "MAIN_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "WBTC", + "borrowable": true, + "collateralFactor": 7800, + "collateralRisk": 0, + "maxLiquidationBonus": 10555, + "liquidationFee": 1000 + }, + { + "spokeKey": "MAIN_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "cbBTC", + "borrowable": true, + "collateralFactor": 7800, + "collateralRisk": 0, + "maxLiquidationBonus": 10555, + "liquidationFee": 1000 + }, + { + "spokeKey": "MAIN_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "AAVE", + "borrowable": false, + "collateralFactor": 7600, + "collateralRisk": 0, + "maxLiquidationBonus": 10833, + "liquidationFee": 1000 + }, + { + "spokeKey": "MAIN_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "LINK", + "borrowable": false, + "collateralFactor": 7100, + "collateralRisk": 0, + "maxLiquidationBonus": 10777, + "liquidationFee": 1000 + }, + { + "spokeKey": "MAIN_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "USDC", + "borrowable": true, + "collateralFactor": 7800, + "collateralRisk": 0, + "maxLiquidationBonus": 10500, + "liquidationFee": 1000 + }, + { + "spokeKey": "MAIN_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "USDT", + "borrowable": true, + "collateralFactor": 7800, + "collateralRisk": 0, + "maxLiquidationBonus": 10500, + "liquidationFee": 1000 + }, + { + "spokeKey": "MAIN_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "EURC", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "MAIN_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "RLUSD", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "MAIN_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "USDG", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "MAIN_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "frxUSD", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "MAIN_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "GHO", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "LIDO_ESPOKE", + "hubKey": "CORE_HUB", + "assetKey": "wstETH", + "borrowable": false, + "collateralFactor": 9550, + "collateralRisk": 0, + "maxLiquidationBonus": 10100, + "liquidationFee": 1000 + }, + { + "spokeKey": "LIDO_ESPOKE", + "hubKey": "CORE_HUB", + "assetKey": "WETH", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "ETHERFI_ESPOKE", + "hubKey": "CORE_HUB", + "assetKey": "weETH", + "borrowable": false, + "collateralFactor": 9550, + "collateralRisk": 0, + "maxLiquidationBonus": 10100, + "liquidationFee": 1000 + }, + { + "spokeKey": "ETHERFI_ESPOKE", + "hubKey": "CORE_HUB", + "assetKey": "WETH", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "KELP_ESPOKE", + "hubKey": "CORE_HUB", + "assetKey": "rsETH", + "borrowable": false, + "collateralFactor": 9500, + "collateralRisk": 0, + "maxLiquidationBonus": 10100, + "liquidationFee": 1000 + }, + { + "spokeKey": "KELP_ESPOKE", + "hubKey": "CORE_HUB", + "assetKey": "WETH", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "LOMBARD_BTC_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "LBTC", + "borrowable": false, + "collateralFactor": 8600, + "collateralRisk": 0, + "maxLiquidationBonus": 10300, + "liquidationFee": 1000 + }, + { + "spokeKey": "LOMBARD_BTC_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "WBTC", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "LOMBARD_BTC_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "cbBTC", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "GOLD_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "XAUt", + "borrowable": false, + "collateralFactor": 7500, + "collateralRisk": 0, + "maxLiquidationBonus": 10666, + "liquidationFee": 1000 + }, + { + "spokeKey": "GOLD_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "USDC", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "GOLD_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "RLUSD", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "GOLD_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "USDG", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "GOLD_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "frxUSD", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "GOLD_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "EURC", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "GOLD_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "GHO", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "GOLD_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "USDT", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "FOREX_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "EURC", + "borrowable": true, + "collateralFactor": 9000, + "collateralRisk": 0, + "maxLiquidationBonus": 10200, + "liquidationFee": 1000 + }, + { + "spokeKey": "FOREX_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "USDC", + "borrowable": true, + "collateralFactor": 9000, + "collateralRisk": 0, + "maxLiquidationBonus": 10200, + "liquidationFee": 1000 + }, + { + "spokeKey": "FOREX_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "USDT", + "borrowable": true, + "collateralFactor": 9000, + "collateralRisk": 0, + "maxLiquidationBonus": 10200, + "liquidationFee": 1000 + }, + { + "spokeKey": "FOREX_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "RLUSD", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "FOREX_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "USDG", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "FOREX_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "frxUSD", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "FOREX_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "GHO", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "BLUECHIP_SPOKE", + "hubKey": "PRIME_HUB", + "assetKey": "WETH", + "borrowable": false, + "collateralFactor": 8600, + "collateralRisk": 0, + "maxLiquidationBonus": 10444, + "liquidationFee": 1000 + }, + { + "spokeKey": "BLUECHIP_SPOKE", + "hubKey": "PRIME_HUB", + "assetKey": "WBTC", + "borrowable": false, + "collateralFactor": 8450, + "collateralRisk": 0, + "maxLiquidationBonus": 10444, + "liquidationFee": 1000 + }, + { + "spokeKey": "BLUECHIP_SPOKE", + "hubKey": "PRIME_HUB", + "assetKey": "cbBTC", + "borrowable": false, + "collateralFactor": 8450, + "collateralRisk": 0, + "maxLiquidationBonus": 10444, + "liquidationFee": 1000 + }, + { + "spokeKey": "BLUECHIP_SPOKE", + "hubKey": "PRIME_HUB", + "assetKey": "wstETH", + "borrowable": false, + "collateralFactor": 8550, + "collateralRisk": 0, + "maxLiquidationBonus": 10444, + "liquidationFee": 1000 + }, + { + "spokeKey": "BLUECHIP_SPOKE", + "hubKey": "PRIME_HUB", + "assetKey": "USDC", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "BLUECHIP_SPOKE", + "hubKey": "PRIME_HUB", + "assetKey": "USDT", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "BLUECHIP_SPOKE", + "hubKey": "PRIME_HUB", + "assetKey": "GHO", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "BLUECHIP_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "USDC", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "BLUECHIP_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "frxUSD", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "BLUECHIP_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "EURC", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "BLUECHIP_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "USDT", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "ETHENA_ECOSYSTEM_SPOKE", + "hubKey": "PLUS_HUB", + "assetKey": "PT_USDE_7MAY2026", + "borrowable": false, + "collateralFactor": 9300, + "collateralRisk": 0, + "maxLiquidationBonus": 10300, + "liquidationFee": 1000 + }, + { + "spokeKey": "ETHENA_ECOSYSTEM_SPOKE", + "hubKey": "PLUS_HUB", + "assetKey": "PT_sUSDE_7MAY2026", + "borrowable": false, + "collateralFactor": 9200, + "collateralRisk": 0, + "maxLiquidationBonus": 10400, + "liquidationFee": 1000 + }, + { + "spokeKey": "ETHENA_ECOSYSTEM_SPOKE", + "hubKey": "PLUS_HUB", + "assetKey": "sUSDe", + "borrowable": false, + "collateralFactor": 9200, + "collateralRisk": 0, + "maxLiquidationBonus": 10300, + "liquidationFee": 1000 + }, + { + "spokeKey": "ETHENA_ECOSYSTEM_SPOKE", + "hubKey": "PLUS_HUB", + "assetKey": "USDe", + "borrowable": true, + "collateralFactor": 9300, + "collateralRisk": 0, + "maxLiquidationBonus": 10200, + "liquidationFee": 1000 + }, + { + "spokeKey": "ETHENA_ECOSYSTEM_SPOKE", + "hubKey": "PLUS_HUB", + "assetKey": "USDC", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "ETHENA_ECOSYSTEM_SPOKE", + "hubKey": "PLUS_HUB", + "assetKey": "USDT", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "ETHENA_ECOSYSTEM_SPOKE", + "hubKey": "PLUS_HUB", + "assetKey": "GHO", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "ETHENA_ECOSYSTEM_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "USDC", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "ETHENA_ECOSYSTEM_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "frxUSD", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "ETHENA_ECOSYSTEM_SPOKE", + "hubKey": "CORE_HUB", + "assetKey": "USDT", + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0 + }, + { + "spokeKey": "ETHENA_CORRELATED_SPOKE", + "hubKey": "PLUS_HUB", + "assetKey": "PT_USDE_7MAY2026", + "borrowable": false, + "collateralFactor": 9580, + "collateralRisk": 0, + "maxLiquidationBonus": 10200, + "liquidationFee": 1000 + }, + { + "spokeKey": "ETHENA_CORRELATED_SPOKE", + "hubKey": "PLUS_HUB", + "assetKey": "PT_sUSDE_7MAY2026", + "borrowable": false, + "collateralFactor": 9400, + "collateralRisk": 0, + "maxLiquidationBonus": 10300, + "liquidationFee": 1000 + }, + { + "spokeKey": "ETHENA_CORRELATED_SPOKE", + "hubKey": "PLUS_HUB", + "assetKey": "sUSDe", + "borrowable": false, + "collateralFactor": 9200, + "collateralRisk": 0, + "maxLiquidationBonus": 10300, + "liquidationFee": 1000 + }, + { + "spokeKey": "ETHENA_CORRELATED_SPOKE", + "hubKey": "PLUS_HUB", + "assetKey": "USDe", + "borrowable": true, + "collateralFactor": 9300, + "collateralRisk": 0, + "maxLiquidationBonus": 10200, + "liquidationFee": 1000 + } + ], + "periphery": { + "nativeTokenKey": "WETH", + "deploySignatureGateway": true, + "deployNativeTokenGateway": true, + "deployGiverPositionManager": true, + "deployTakerPositionManager": true, + "deployConfigPositionManager": true + } +} diff --git a/scripts/verification/inputs/deploy-report.json b/scripts/verification/inputs/deploy-report.json new file mode 100644 index 000000000..cf29526dd --- /dev/null +++ b/scripts/verification/inputs/deploy-report.json @@ -0,0 +1,46 @@ +{ + "accessManager": "0x08aE3BE30958cDd1847ec58fFfd4C451a87fDF01", + "configPositionManager": "0x51305839CE822a7b4b12AA7D86eA7005052d575c", + "giverPositionManager": "0x17A54b8d6D9C68e7fa1C7112AC998EA1BA51d11e", + "hub": { + "CORE_HUB": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "PLUS_HUB": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "PRIME_HUB": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931" + }, + "hubConfigurator": "0x1F0753480bB03EaA00863224602267B7E0525C3d", + "irStrategy": { + "CORE_HUB": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "PLUS_HUB": "0x31280650661b8443723fa9739b3A164E3696af48", + "PRIME_HUB": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D" + }, + "nativeTokenGateway": "0xe68ab4F90Fe026B9873F5F276eD2d7efBbbE42Be", + "oracle": { + "BLUECHIP_SPOKE": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "ETHENA_CORRELATED_SPOKE": "0x9b91a0943CADf554742E8Fb358B1cC4ae4F85F01", + "ETHENA_ECOSYSTEM_SPOKE": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", + "ETHERFI_ESPOKE": "0xd8B153FaAA8f2b1bC774916FEd333A4F3dE48792", + "FOREX_SPOKE": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", + "GOLD_SPOKE": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", + "KELP_ESPOKE": "0x37C316996C714Bf906743071e04E62220b3271ac", + "LIDO_ESPOKE": "0x664D73b6C3591333Fd79510f7ce9ef81228824F5", + "LOMBARD_BTC_SPOKE": "0x198Cac7f54FFc7d709Ac0FEc4B6454CE73e21D3D", + "MAIN_SPOKE": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127" + }, + "salt": "0x9d5fae2639ec7f127ccb5fded16e1b3a6ac8062c328a0348f43ec0bf9b01b2e0", + "signatureGateway": "0xfbC184337Dc6595D8bf62968Bda46e7De7AF9c3d", + "spoke": { + "BLUECHIP_SPOKE": "0x973a023A77420ba610f06b3858aD991Df6d85A08", + "ETHENA_CORRELATED_SPOKE": "0x58131E79531caB1d52301228d1f7b842F26B9649", + "ETHENA_ECOSYSTEM_SPOKE": "0xba1B3D55D249692b669A164024A838309B7508AF", + "ETHERFI_ESPOKE": "0xbF10BDfE177dE0336aFD7fcCF80A904E15386219", + "FOREX_SPOKE": "0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1", + "GOLD_SPOKE": "0x65407b940966954b23dfA3caA5C0702bB42984DC", + "KELP_ESPOKE": "0x3131FE68C4722e726fe6B2819ED68e514395B9a4", + "LIDO_ESPOKE": "0xe1900480ac69f0B296841Cd01cC37546d92F35Cd", + "LOMBARD_BTC_SPOKE": "0x7EC68b5695e803e98a21a9A05d744F28b0a7753D", + "MAIN_SPOKE": "0x94e7A5dCbE816e498b89aB752661904E2F56c485" + }, + "spokeConfigurator": "0x9BFFf48BFb5A7AE70c348d4d4cb97E8DEFa5389a", + "takerPositionManager": "0x6c044c0D3801499bCAbfAd458B70880bc518e9F7", + "treasurySpoke": "0xB9B0b8616f6Bf6841972a52058132BE08d723155" +} diff --git a/scripts/verification/inputs/tokenization-deploy.json b/scripts/verification/inputs/tokenization-deploy.json new file mode 100644 index 000000000..1b0eabbfd --- /dev/null +++ b/scripts/verification/inputs/tokenization-deploy.json @@ -0,0 +1,39 @@ +{ + "CORE_HUB": { + "AAVE": "0x0A65197b16C5969F92672051c9C9C0C75B369135", + "EURC": "0x6D9e2Cdd61CaF69af99b275704B6e272C41c6718", + "GHO": "0x58C14a5E061c9bC6926c5b853445290F296C2F7B", + "LBTC": "0x7961F140B570490849DB878AE222570ea838799d", + "LINK": "0xE69C2045095C8Ab3E2a7d77de2328faE5baF797c", + "RLUSD": "0xC8a125AE4275a78AADc53B46Ca10566Bc9B249E0", + "USDC": "0x531E90a2376902DE8915789Fcc1075e3B0c153E7", + "USDG": "0xAC2435E3C25e8246870D33ce0a26988A46d5DB68", + "USDT": "0x5eC44a70F309854fe04d495cFE1B5dA63DD1cc73", + "WBTC": "0x82A9CC4656784E55Ef2E78F704028B5E1Bfc1732", + "WETH": "0x7320CF22Ac095bA2a2e0a652F77efB836c2E751b", + "XAUt": "0x4E712562fcb5337011398B6C630f55b60641cd5e", + "cbBTC": "0x33B41B74366F55327d959FfF6D6b6fBc2853dbB1", + "frxUSD": "0x2226749630775ee20230Ad65214fB339087eF30D", + "rsETH": "0x45a04Ca1A5cbEeA4B44356c75EDd29b33eB2527a", + "weETH": "0x559cEc2C840D9DBB18936Afc5E5341D78bfC7Cbe", + "wstETH": "0xcb0E7dA9c635628f6d4827355AeCa75aB8d3560f" + }, + "PLUS_HUB": { + "GHO": "0xA54382db40EC602c0a173A08f9E86Ed40F9D4D10", + "PT_USDE_7MAY2026": "0xdd2Eb78BF9e6aC5068B95aD2d451e8c9Af10ac81", + "PT_sUSDE_7MAY2026": "0x90774889c22D2F2Adf44da1f04C7c95542590df4", + "USDC": "0xc94bdd83D2c7655C280655D60954e79E88D4F949", + "USDT": "0x80835EB50694EE0e519743f67e5401e6FD300006", + "USDe": "0x502Cd81da6a8F1785eb2eEE72713B7388E16A854", + "sUSDe": "0x24f8c062e1E0451736C1D6E023510DA262a41df4" + }, + "PRIME_HUB": { + "GHO": "0x900fD46d565d1ac8995928c0179052ec02a6D0E1", + "USDC": "0x486415fb1F8b062c89ED548f871cf64304AACb31", + "USDT": "0x46c588DD8453aC259c1f6a54b4C9A93C2aC3762D", + "WBTC": "0x5AE3d87De89CA6Ce501e8317887F71EABED69E18", + "WETH": "0x2087513383330B961A3753B47627Bbf149F31c70", + "cbBTC": "0xD38098faf52D8E915EdED84fBF30F81C17906938", + "wstETH": "0xFCD3D3C69cd032DE0cc78fE529B7447D2fe7F666" + } +} diff --git a/scripts/verification/inputs/tokens.json b/scripts/verification/inputs/tokens.json new file mode 100644 index 000000000..15a8e990e --- /dev/null +++ b/scripts/verification/inputs/tokens.json @@ -0,0 +1,86 @@ +{ + "AAVE": { + "address": "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9", + "priceFeed": "0x547a514d5e3769680Ce22B2361c10Ea13619e8a9" + }, + "EURC": { + "address": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c", + "priceFeed": "0x04F84020Fdf10d9ee64D1dcC2986EDF2F556DA11" + }, + "GHO": { + "address": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f", + "priceFeed": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd" + }, + "LBTC": { + "address": "0x8236a87084f8B84306f72007F36F2618A5634494", + "priceFeed": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c" + }, + "LINK": { + "address": "0x514910771AF9Ca656af840dff83E8264EcF986CA", + "priceFeed": "0x2c1d072e956AFFC0D435Cb7AC38EF18d24d9127c" + }, + "PT_USDE_7MAY2026": { + "address": "0xAeBf0Bb9f57E89260d57f31AF34eB58657d96Ce0", + "priceFeed": "0x0a72df02CE3E4185b6CEDf561f0AE651E9BeE235" + }, + "PT_sUSDE_7MAY2026": { + "address": "0x3de0ff76E8b528C092d47b9DaC775931cef80F49", + "priceFeed": "0xa0dc0249c32fa79e8B9b17c735908a60b1141B40" + }, + "RLUSD": { + "address": "0x8292Bb45bf1Ee4d140127049757c2E0fF06317eD", + "priceFeed": "0xf0eaC18E908B34770FDEe46d069c846bDa866759" + }, + "USDC": { + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "priceFeed": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994" + }, + "USDG": { + "address": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D", + "priceFeed": "0x14f0737d6b705259e521EA6E9E3506AC78dBd311" + }, + "USDT": { + "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "priceFeed": "0x260326c220E469358846b187eE53328303Efe19C" + }, + "USDe": { + "address": "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3", + "priceFeed": "0xC26D4a1c46d884cfF6dE9800B6aE7A8Cf48B4Ff8" + }, + "WBTC": { + "address": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", + "priceFeed": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c" + }, + "WETH": { + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "priceFeed": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419" + }, + "XAUt": { + "address": "0x68749665FF8D2d112Fa859AA293F07A622782F38", + "priceFeed": "0x214eD9Da11D2fbe465a6fc601a91E62EbEc1a0D6" + }, + "cbBTC": { + "address": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", + "priceFeed": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c" + }, + "frxUSD": { + "address": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29", + "priceFeed": "0x25ded2f9ae6ae9416693ab63abe3ab25493861fd" + }, + "rsETH": { + "address": "0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7", + "priceFeed": "0x47f52b2e43d0386cf161e001835b03ad49889e3b" + }, + "sUSDe": { + "address": "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497", + "priceFeed": "0x42bc86f2f08419280a99d8fbEa4672e7c30a86ec" + }, + "weETH": { + "address": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee", + "priceFeed": "0xf112af6f0a332b815fbef3ff932c057e570b62d3" + }, + "wstETH": { + "address": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + "priceFeed": "0x869C9Ae2C8fbe82a8b0F768b9F791f89E083222C" + } +} diff --git a/scripts/verification/inputs/v4-init-config.xlsx b/scripts/verification/inputs/v4-init-config.xlsx new file mode 100644 index 000000000..6567f11a8 Binary files /dev/null and b/scripts/verification/inputs/v4-init-config.xlsx differ diff --git a/scripts/verification/verify_access_manager.py b/scripts/verification/verify_access_manager.py new file mode 100644 index 000000000..9344b4180 --- /dev/null +++ b/scripts/verification/verify_access_manager.py @@ -0,0 +1,567 @@ +#!/usr/bin/env python3 +"""Reconstruct AccessManagerEnumerable roles from events and compare with on-chain getters. + +Fetches all historical events from the AccessManagerEnumerable contract, replays +them to reconstruct role state, then cross-validates against the contract's +enumerable getter functions. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from collections import defaultdict +from dataclasses import dataclass, field +from pathlib import Path + +from web3 import Web3 + +ARTIFACTS_DIR = Path(__file__).resolve().parent.parent.parent / "out" + +DEFAULT_ADDRESS = "0x08aE3BE30958cDd1847ec58fFfd4C451a87fDF01" +ADMIN_ROLE = 0 +PUBLIC_ROLE = (1 << 64) - 1 # type(uint64).max + +CHUNK_SIZE = 10_000 # blocks per getLogs request + +# Terminal colors +GREEN = "\033[92m" +RED = "\033[91m" +YELLOW = "\033[93m" +BOLD = "\033[1m" +RESET = "\033[0m" + + +def _canonical_type(param: dict) -> str: + """Resolve an ABI parameter to its canonical type, expanding tuples recursively.""" + t = param["type"] + if t == "tuple" or t.startswith("tuple["): + components = param.get("components", []) + inner = ",".join(_canonical_type(c) for c in components) + # Preserve array suffix if present, e.g. "tuple[]" -> "(uint256,address)[]" + suffix = t[5:] # everything after "tuple" + return f"({inner}){suffix}" + return t + + +def build_selector_map() -> dict[str, str]: + """Scan all ABI artifacts and build a selector -> function signature map.""" + selector_map: dict[str, str] = {} + for abi_file in ARTIFACTS_DIR.rglob("*.json"): + try: + with open(abi_file) as f: + data = json.load(f) + abi = data.get("abi") + if not abi: + continue + for item in abi: + if item.get("type") != "function": + continue + name = item["name"] + input_types = ",".join(_canonical_type(inp) for inp in item.get("inputs", [])) + sig = f"{name}({input_types})" + selector = Web3.keccak(text=sig)[:4].hex() + selector_hex = "0x" + selector + selector_map[selector_hex] = sig + except (json.JSONDecodeError, KeyError): + continue + return selector_map + + +def load_abi(sol_file: str, contract_name: str) -> list: + path = ARTIFACTS_DIR / sol_file / f"{contract_name}.json" + if not path.exists(): + sys.exit(f"Artifact not found: {path}\nRun `forge build` first.") + with open(path) as f: + return json.load(f)["abi"] + + +# --------------------------------------------------------------------------- +# Reconstructed state from events +# --------------------------------------------------------------------------- + +@dataclass +class ReconstructedState: + role_members: dict[int, set[str]] = field(default_factory=lambda: defaultdict(set)) + role_labels: dict[int, str] = field(default_factory=dict) + role_admin: dict[int, int] = field(default_factory=dict) + role_guardian: dict[int, int] = field(default_factory=dict) + # (target, selector_hex) -> roleId + target_selector_role: dict[tuple[str, str], int] = field(default_factory=dict) + + @property + def custom_roles(self) -> set[int]: + """All role IDs seen, excluding ADMIN_ROLE and PUBLIC_ROLE.""" + all_ids: set[int] = set() + all_ids.update(self.role_members.keys()) + all_ids.update(self.role_labels.keys()) + all_ids.update(self.role_admin.keys()) + all_ids.update(self.role_guardian.keys()) + for (_, _), rid in self.target_selector_role.items(): + all_ids.add(rid) + all_ids.discard(ADMIN_ROLE) + all_ids.discard(PUBLIC_ROLE) + return all_ids + + @property + def role_targets(self) -> dict[int, dict[str, set[str]]]: + """roleId -> {target_addr -> {selector_hex, ...}}""" + result: dict[int, dict[str, set[str]]] = defaultdict(lambda: defaultdict(set)) + for (target, sel), rid in self.target_selector_role.items(): + result[rid][target].add(sel) + return dict(result) + + @property + def admin_roles(self) -> set[int]: + """All admin role IDs (excluding ADMIN_ROLE itself).""" + admins: set[int] = set() + for rid, admin in self.role_admin.items(): + if admin != ADMIN_ROLE: + admins.add(admin) + return admins + + def roles_of_admin(self, admin_role_id: int) -> set[int]: + """Roles managed by a given admin role.""" + return {rid for rid, admin in self.role_admin.items() if admin == admin_role_id} + + +# --------------------------------------------------------------------------- +# Event fetching +# --------------------------------------------------------------------------- + +def fetch_all_events(w3: Web3, contract, from_block: int) -> list: + """Fetch all contract events in chunked block ranges, sorted chronologically.""" + latest = w3.eth.block_number + all_logs = [] + + event_names = [ + "RoleLabel", + "RoleGranted", + "RoleRevoked", + "RoleAdminChanged", + "RoleGuardianChanged", + "TargetFunctionRoleUpdated", + ] + + for event_name in event_names: + event = getattr(contract.events, event_name) + start = from_block + print(f" Fetching {event_name} events...", end="", flush=True) + count = 0 + while start <= latest: + end = min(start + CHUNK_SIZE - 1, latest) + try: + logs = event.get_logs(from_block=start, to_block=end) + all_logs.extend(logs) + count += len(logs) + except Exception as e: + print(f"\n {RED}Error fetching {event_name} blocks {start}-{end}: {e}{RESET}") + start = end + 1 + print(f" {count} events") + + # Sort by (blockNumber, logIndex) + all_logs.sort(key=lambda l: (l.blockNumber, l.logIndex)) + return all_logs + + +# --------------------------------------------------------------------------- +# Replay events to build state +# --------------------------------------------------------------------------- + +def replay_events(logs: list) -> ReconstructedState: + """Process events chronologically and build reconstructed state.""" + state = ReconstructedState() + + for log in logs: + name = log.event + + if name == "RoleGranted": + role_id = log.args.roleId + account = Web3.to_checksum_address(log.args.account) + new_member = log.args.newMember + if new_member: + state.role_members[role_id].add(account) + + elif name == "RoleRevoked": + role_id = log.args.roleId + account = Web3.to_checksum_address(log.args.account) + state.role_members[role_id].discard(account) + + elif name == "RoleLabel": + role_id = log.args.roleId + label = log.args.label + state.role_labels[role_id] = label + + elif name == "RoleAdminChanged": + role_id = log.args.roleId + admin = log.args.admin + state.role_admin[role_id] = admin + + elif name == "RoleGuardianChanged": + role_id = log.args.roleId + guardian = log.args.guardian + state.role_guardian[role_id] = guardian + + elif name == "TargetFunctionRoleUpdated": + target = Web3.to_checksum_address(log.args.target) + selector = log.args.selector.hex() if isinstance(log.args.selector, bytes) else log.args.selector + # Normalize to 0x-prefixed 8-char hex + if not selector.startswith("0x"): + selector = "0x" + selector + role_id = log.args.roleId + state.target_selector_role[(target, selector)] = role_id + + return state + + +# --------------------------------------------------------------------------- +# On-chain getter queries +# --------------------------------------------------------------------------- + +@dataclass +class OnChainState: + roles: list[int] = field(default_factory=list) + role_members: dict[int, list[str]] = field(default_factory=dict) + role_labels: dict[int, str] = field(default_factory=dict) + role_admin: dict[int, int] = field(default_factory=dict) + role_guardian: dict[int, int] = field(default_factory=dict) + role_targets: dict[int, dict[str, list[str]]] = field(default_factory=dict) + admin_roles: list[int] = field(default_factory=list) + admin_role_to_roles: dict[int, list[int]] = field(default_factory=dict) + labels: list[str] = field(default_factory=list) + admin_members: list[str] = field(default_factory=list) + + +def call(contract, fn_name: str, *args): + try: + return contract.functions[fn_name](*args).call() + except Exception as e: + print(f" {RED}CALL FAILED{RESET} {fn_name}({', '.join(str(a) for a in args)}): {e}") + return None + + +def query_on_chain(contract, admin_candidates: set[str]) -> OnChainState: + """Query all enumerable getters to build on-chain state.""" + oc = OnChainState() + + # Roles + role_count = call(contract, "getRoleCount") + if role_count and role_count > 0: + oc.roles = list(call(contract, "getRoles", 0, role_count)) + print(f" Roles: {len(oc.roles)}") + + # Per-role data + all_role_ids = list(oc.roles) + [ADMIN_ROLE] + for role_id in all_role_ids: + # Members + member_count = call(contract, "getRoleMemberCount", role_id) + if member_count and member_count > 0: + members = call(contract, "getRoleMembers", role_id, 0, member_count) + oc.role_members[role_id] = [Web3.to_checksum_address(m) for m in members] + else: + oc.role_members[role_id] = [] + + # Label (only for custom roles) + if role_id not in (ADMIN_ROLE, PUBLIC_ROLE): + is_labeled = call(contract, "isRoleLabeled", role_id) + if is_labeled: + label = call(contract, "getLabelOfRole", role_id) + if label is not None: + oc.role_labels[role_id] = label + + # Admin & Guardian + admin = call(contract, "getRoleAdmin", role_id) + if admin is not None: + oc.role_admin[role_id] = admin + guardian = call(contract, "getRoleGuardian", role_id) + if guardian is not None: + oc.role_guardian[role_id] = guardian + + # Targets & selectors (only for custom roles — ADMIN_ROLE excluded from enumerable) + if role_id not in (ADMIN_ROLE, PUBLIC_ROLE): + target_count = call(contract, "getRoleTargetCount", role_id) + if target_count and target_count > 0: + targets = call(contract, "getRoleTargets", role_id, 0, target_count) + oc.role_targets[role_id] = {} + for t in targets: + t = Web3.to_checksum_address(t) + sel_count = call(contract, "getRoleTargetSelectorCount", role_id, t) + if sel_count and sel_count > 0: + sels = call(contract, "getRoleTargetSelectors", role_id, t, 0, sel_count) + oc.role_targets[role_id][t] = [ + s.hex() if isinstance(s, bytes) else s for s in sels + ] + else: + oc.role_targets[role_id][t] = [] + + # Admin roles + admin_role_count = call(contract, "getAdminRoleCount") + if admin_role_count and admin_role_count > 0: + oc.admin_roles = list(call(contract, "getAdminRoles", 0, admin_role_count)) + print(f" Admin roles: {len(oc.admin_roles)}") + + # Roles managed by each admin role + for admin_id in oc.admin_roles: + count = call(contract, "getRoleOfAdminRoleCount", admin_id) + if count and count > 0: + oc.admin_role_to_roles[admin_id] = list( + call(contract, "getRolesOfAdminRole", admin_id, 0, count) + ) + + # Labels + label_count = call(contract, "getRoleLabelCount") + if label_count and label_count > 0: + oc.labels = list(call(contract, "getRoleLabels", 0, label_count)) + print(f" Labels: {len(oc.labels)}") + + return oc + + +# --------------------------------------------------------------------------- +# Comparison +# --------------------------------------------------------------------------- + +def normalize_selector(s) -> str: + """Ensure selector is a 0x-prefixed 8-char lowercase hex string.""" + if isinstance(s, bytes): + s = s.hex() + s = s.lower() + if not s.startswith("0x"): + s = "0x" + s + return s + + +def compare_states(recon: ReconstructedState, oc: OnChainState) -> int: + """Compare reconstructed vs on-chain state. Returns number of mismatches.""" + mismatches = 0 + + def check(label: str, expected, actual) -> bool: + nonlocal mismatches + if expected == actual: + print(f" {GREEN}\u2713{RESET} {label}") + return True + else: + print(f" {RED}\u2717{RESET} {label}") + print(f" Expected (events): {expected}") + print(f" Actual (getters): {actual}") + mismatches += 1 + return False + + # 1. Roles list + print(f"\n{BOLD}=== Roles ==={RESET}") + recon_roles = sorted(recon.custom_roles) + onchain_roles = sorted(oc.roles) + check("Role list", recon_roles, onchain_roles) + + # 2. Per-role comparison + all_role_ids = sorted(set(recon_roles) | set(onchain_roles)) + for role_id in all_role_ids: + label_str = recon.role_labels.get(role_id, oc.role_labels.get(role_id, f"role#{role_id}")) + print(f"\n{BOLD}--- Role {role_id} ({label_str}) ---{RESET}") + + # Members + recon_members = sorted(recon.role_members.get(role_id, set())) + onchain_members = sorted(oc.role_members.get(role_id, [])) + check("Members", recon_members, onchain_members) + + # Label + recon_label = recon.role_labels.get(role_id) + onchain_label = oc.role_labels.get(role_id) + check("Label", recon_label, onchain_label) + + # Admin + recon_admin = recon.role_admin.get(role_id, ADMIN_ROLE) + onchain_admin = oc.role_admin.get(role_id) + if onchain_admin is not None: + check("Admin role", recon_admin, onchain_admin) + + # Guardian + recon_guardian = recon.role_guardian.get(role_id, ADMIN_ROLE) + onchain_guardian = oc.role_guardian.get(role_id) + if onchain_guardian is not None: + check("Guardian role", recon_guardian, onchain_guardian) + + # Targets & selectors + recon_targets = recon.role_targets.get(role_id, {}) + onchain_targets = oc.role_targets.get(role_id, {}) + + recon_target_addrs = sorted(recon_targets.keys()) + onchain_target_addrs = sorted(onchain_targets.keys()) + check("Target contracts", recon_target_addrs, onchain_target_addrs) + + for target in sorted(set(recon_target_addrs) | set(onchain_target_addrs)): + recon_sels = sorted(normalize_selector(s) for s in recon_targets.get(target, set())) + onchain_sels = sorted(normalize_selector(s) for s in onchain_targets.get(target, [])) + check(f" Selectors on {target}", recon_sels, onchain_sels) + + # 3. ADMIN_ROLE members + print(f"\n{BOLD}--- ADMIN_ROLE (0) members ---{RESET}") + recon_admin_members = sorted(recon.role_members.get(ADMIN_ROLE, set())) + onchain_admin_members = sorted(oc.role_members.get(ADMIN_ROLE, [])) + check("ADMIN_ROLE members", recon_admin_members, onchain_admin_members) + + # 4. Admin roles enumeration + print(f"\n{BOLD}=== Admin Roles ==={RESET}") + recon_admin_roles = sorted(recon.admin_roles) + onchain_admin_roles = sorted(oc.admin_roles) + check("Admin roles list", recon_admin_roles, onchain_admin_roles) + + for admin_id in sorted(set(recon_admin_roles) | set(onchain_admin_roles)): + recon_managed = sorted(recon.roles_of_admin(admin_id)) + onchain_managed = sorted(oc.admin_role_to_roles.get(admin_id, [])) + check(f"Roles managed by admin {admin_id}", recon_managed, onchain_managed) + + # 5. Labels + print(f"\n{BOLD}=== Labels ==={RESET}") + recon_labels = sorted(recon.role_labels.values()) + onchain_labels = sorted(oc.labels) + check("Labels list", recon_labels, onchain_labels) + + return mismatches + + +# --------------------------------------------------------------------------- +# Summary table +# --------------------------------------------------------------------------- + +def print_roles(recon: ReconstructedState, selector_map: dict[str, str]): + """Print detailed per-role information from reconstructed event data.""" + separator = "=" * 80 + role_targets = recon.role_targets + + def label_for(role_id: int) -> str: + if role_id == ADMIN_ROLE: + return "ADMIN_ROLE" + if role_id == PUBLIC_ROLE: + return "PUBLIC_ROLE" + return recon.role_labels.get(role_id, f"role#{role_id}") + + def print_role(role_id: int): + label = label_for(role_id) + print(f"\n{BOLD}{separator}") + print(f"ROLE {role_id} — {label}") + print(f"{separator}{RESET}") + + # Admin + if role_id == ADMIN_ROLE: + print(f" Admin: (locked)") + else: + admin_id = recon.role_admin.get(role_id, ADMIN_ROLE) + admin_label = label_for(admin_id) + print(f" Admin: {admin_id} ({admin_label})") + + # Members + members = sorted(recon.role_members.get(role_id, set())) + print(f" Members ({len(members)}):") + if members: + for m in members: + print(f" - {m}") + else: + print(f" (none)") + + # Selectors by target + targets = role_targets.get(role_id, {}) + if targets: + print(f" Selectors:") + for target in sorted(targets.keys()): + sels = sorted(normalize_selector(s) for s in targets[target]) + print(f" Target {target}:") + for s in sels: + sig = selector_map.get(s, s) + print(f" - {sig}") + else: + print(f" Selectors: (none)") + + # ADMIN_ROLE first + print_role(ADMIN_ROLE) + + # Custom roles sorted by ID + for role_id in sorted(recon.custom_roles): + print_role(role_id) + + print() + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main(): + parser = argparse.ArgumentParser( + description="Verify AccessManagerEnumerable by comparing event reconstruction with getters." + ) + parser.add_argument("--rpc-url", required=True, help="Ethereum RPC endpoint URL") + parser.add_argument( + "--address", default=DEFAULT_ADDRESS, + help=f"AccessManagerEnumerable address (default: {DEFAULT_ADDRESS})", + ) + parser.add_argument( + "--from-block", type=int, default=0, + help="Starting block for event fetching (default: 0 = contract deploy block via binary search)", + ) + args = parser.parse_args() + + w3 = Web3(Web3.HTTPProvider(args.rpc_url)) + if not w3.is_connected(): + print(f"{RED}ERROR{RESET}: Cannot connect to {args.rpc_url}") + sys.exit(1) + print(f"Connected to chain {w3.eth.chain_id}") + + address = Web3.to_checksum_address(args.address) + abi = load_abi("IAccessManagerEnumerable.sol", "IAccessManagerEnumerable") + contract = w3.eth.contract(address=address, abi=abi) + + # Determine starting block + from_block = args.from_block + if from_block == 0: + from_block = find_deploy_block(w3, address) + print(f"Deploy block (approx): {from_block}") + + # Phase 1: Fetch events + print(f"\n{BOLD}Phase 1: Fetching events from block {from_block}...{RESET}") + logs = fetch_all_events(w3, contract, from_block) + print(f"Total events: {len(logs)}") + + # Phase 2: Reconstruct state + print(f"\n{BOLD}Phase 2: Reconstructing state from events...{RESET}") + recon = replay_events(logs) + print(f" Custom roles found: {len(recon.custom_roles)}") + print(f" Labels found: {len(recon.role_labels)}") + print(f" ADMIN_ROLE members: {len(recon.role_members.get(ADMIN_ROLE, set()))}") + + # Phase 3: Query on-chain getters + print(f"\n{BOLD}Phase 3: Querying on-chain getters...{RESET}") + admin_candidates = recon.role_members.get(ADMIN_ROLE, set()) + oc = query_on_chain(contract, admin_candidates) + + # Phase 4: Compare + print(f"\n{BOLD}Phase 4: Comparing reconstructed vs on-chain state...{RESET}") + mismatches = compare_states(recon, oc) + + # Detailed role display + selector_map = build_selector_map() + print_roles(recon, selector_map) + + if mismatches == 0: + print(f"{GREEN}{BOLD}All checks passed — event reconstruction matches on-chain getters.{RESET}") + else: + print(f"{RED}{BOLD}{mismatches} mismatch(es) found.{RESET}") + sys.exit(1) + + +def find_deploy_block(w3: Web3, address: str) -> int: + """Binary search for the block where the contract was deployed.""" + lo, hi = 0, w3.eth.block_number + while lo < hi: + mid = (lo + hi) // 2 + code = w3.eth.get_code(Web3.to_checksum_address(address), block_identifier=mid) + if len(code) > 0: + hi = mid + else: + lo = mid + 1 + return lo + + +if __name__ == "__main__": + main() diff --git a/scripts/verification/verify_bytecode.py b/scripts/verification/verify_bytecode.py new file mode 100644 index 000000000..86ad91737 --- /dev/null +++ b/scripts/verification/verify_bytecode.py @@ -0,0 +1,597 @@ +#!/usr/bin/env python3 +"""Simplified Aave V4 bytecode-only verification script. + +Reads a deployment report JSON, connects to an RPC endpoint, and verifies that +the on-chain bytecode of every deployed contract matches the local Forge +build artifacts. Does NOT require a config input — only the deploy report. +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from dataclasses import dataclass, field +from functools import lru_cache +from pathlib import Path +from typing import Any, Optional + +from web3 import Web3 + +ARTIFACTS_DIR = Path(__file__).resolve().parent.parent.parent / "out" + +GREEN = "\033[92m" +RED = "\033[91m" +BOLD = "\033[1m" +RESET = "\033[0m" + + +# --------------------------------------------------------------------------- +# Artifact mapping +# --------------------------------------------------------------------------- + +@dataclass(frozen=True) +class ArtifactInfo: + sol_file: str + contract_name: str + impl_sol_file: Optional[str] = None + impl_contract_name: Optional[str] = None + + +EIP712_IMMUTABLES: list[tuple[str, str]] = [ + ("_cachedThis", "uint256"), + ("_cachedChainId", "uint256"), + ("_cachedNameHash", "bytes32"), + ("_cachedVersionHash", "bytes32"), + ("_cachedDomainSeparator", "bytes32"), +] + +# Immutable variable labels per contract, ordered by declaration (base classes first). +# Sorted AST node IDs in immutableReferences correspond to this declaration order. +IMMUTABLE_LABELS: dict[str, list[tuple[str, str]]] = { + "TransparentUpgradeableProxy": [ + ("_admin", "address"), + ], + "AssetInterestRateStrategy": [ + ("HUB", "address"), + ], + "AaveOracle": [ + ("DECIMALS", "uint8"), + ("DEPLOYER", "address"), + ], + "NativeTokenGateway": [ + ("NATIVE_TOKEN_WRAPPER", "address"), + ], + "SpokeInstance": [ + *EIP712_IMMUTABLES, + ("MAX_USER_RESERVES_LIMIT", "uint16"), + ("ORACLE", "address"), + ], + "TokenizationSpokeInstance": [ + *EIP712_IMMUTABLES, + ("MAX_ALLOWED_SPOKE_CAP", "uint40"), + ("HUB", "address"), + ("ASSET_ID", "uint256"), + ("ASSET", "address"), + ("DECIMALS", "uint8"), + ("ASSET_UNITS", "uint256"), + ], + "SignatureGateway": [*EIP712_IMMUTABLES], + "TakerPositionManager": [*EIP712_IMMUTABLES], + "ConfigPositionManager": [*EIP712_IMMUTABLES], +} + + +ARTIFACT_MAP: dict[str, ArtifactInfo] = { + "AccessManager": ArtifactInfo("AccessManagerEnumerable.sol", "AccessManagerEnumerable"), + "HubConfigurator": ArtifactInfo("HubConfigurator.sol", "HubConfigurator"), + "SpokeConfigurator": ArtifactInfo("SpokeConfigurator.sol", "SpokeConfigurator"), + "TreasurySpoke": ArtifactInfo( + "TransparentUpgradeableProxy.sol", "TransparentUpgradeableProxy", + impl_sol_file="TreasurySpokeInstance.sol", + impl_contract_name="TreasurySpokeInstance", + ), + "Hub": ArtifactInfo( + "TransparentUpgradeableProxy.sol", "TransparentUpgradeableProxy", + impl_sol_file="HubInstance.sol", + impl_contract_name="HubInstance", + ), + "InterestRateStrategy": ArtifactInfo( + "AssetInterestRateStrategy.sol", "AssetInterestRateStrategy", + ), + "Spoke": ArtifactInfo( + "TransparentUpgradeableProxy.sol", "TransparentUpgradeableProxy", + impl_sol_file="SpokeInstance.sol", + impl_contract_name="SpokeInstance", + ), + "AaveOracle": ArtifactInfo("AaveOracle.sol", "AaveOracle"), + "SignatureGateway": ArtifactInfo("SignatureGateway.sol", "SignatureGateway"), + "NativeTokenGateway": ArtifactInfo("NativeTokenGateway.sol", "NativeTokenGateway"), + "GiverPositionManager": ArtifactInfo("GiverPositionManager.sol", "GiverPositionManager"), + "TakerPositionManager": ArtifactInfo("TakerPositionManager.sol", "TakerPositionManager"), + "ConfigPositionManager": ArtifactInfo("ConfigPositionManager.sol", "ConfigPositionManager"), + "TokenizationSpoke": ArtifactInfo( + "TransparentUpgradeableProxy.sol", "TransparentUpgradeableProxy", + impl_sol_file="TokenizationSpokeInstance.sol", + impl_contract_name="TokenizationSpokeInstance", + ), + "LiquidationLogic": ArtifactInfo("LiquidationLogic.sol", "LiquidationLogic"), +} + + +# --------------------------------------------------------------------------- +# Deploy report parsing +# --------------------------------------------------------------------------- + +@dataclass +class HubInfo: + label: str + address: str + ir_strategy: str + + +@dataclass +class SpokeInfo: + label: str + proxy: str + oracle: str + + +@dataclass +class DeployReport: + salt: str + access_manager: str + hub_configurator: str + spoke_configurator: str + treasury_spoke: str + hubs: list[HubInfo] = field(default_factory=list) + spokes: list[SpokeInfo] = field(default_factory=list) + signature_gateway: Optional[str] = None + native_token_gateway: Optional[str] = None + giver_position_manager: Optional[str] = None + taker_position_manager: Optional[str] = None + config_position_manager: Optional[str] = None + + @classmethod + def from_json(cls, data: dict) -> "DeployReport": + hubs: list[HubInfo] = [] + hub_addrs = data.get("hub", {}) + ir_addrs = data.get("irStrategy", {}) + for key, addr in hub_addrs.items(): + hubs.append(HubInfo(label=key, address=addr, ir_strategy=ir_addrs[key])) + + spokes: list[SpokeInfo] = [] + spoke_addrs = data.get("spoke", {}) + oracle_addrs = data.get("oracle", {}) + for key, addr in spoke_addrs.items(): + spokes.append(SpokeInfo(label=key, proxy=addr, oracle=oracle_addrs[key])) + + return cls( + salt=data.get("salt", ""), + access_manager=data["accessManager"], + hub_configurator=data["hubConfigurator"], + spoke_configurator=data["spokeConfigurator"], + treasury_spoke=data["treasurySpoke"], + hubs=hubs, + spokes=spokes, + signature_gateway=data.get("signatureGateway"), + native_token_gateway=data.get("nativeTokenGateway"), + giver_position_manager=data.get("giverPositionManager"), + taker_position_manager=data.get("takerPositionManager"), + config_position_manager=data.get("configPositionManager"), + ) + + def all_addresses(self) -> list[tuple[str, str]]: + pairs: list[tuple[str, str]] = [ + ("AccessManager", self.access_manager), + ("HubConfigurator", self.hub_configurator), + ("SpokeConfigurator", self.spoke_configurator), + ("TreasurySpoke", self.treasury_spoke), + ] + for h in self.hubs: + pairs.append((f"{h.label}/Hub", h.address)) + pairs.append((f"{h.label}/InterestRateStrategy", h.ir_strategy)) + for s in self.spokes: + pairs.append((f"{s.label}/Spoke", s.proxy)) + pairs.append((f"{s.label}/AaveOracle", s.oracle)) + for name, addr in [ + ("SignatureGateway", self.signature_gateway), + ("NativeTokenGateway", self.native_token_gateway), + ("GiverPositionManager", self.giver_position_manager), + ("TakerPositionManager", self.taker_position_manager), + ("ConfigPositionManager", self.config_position_manager), + ]: + if addr: + pairs.append((name, addr)) + return pairs + + +# --------------------------------------------------------------------------- +# Result tracking +# --------------------------------------------------------------------------- + +class VerificationResult: + def __init__(self) -> None: + self.passed = 0 + self.failed = 0 + + def ok(self, label: str, details: str = "") -> None: + self.passed += 1 + suffix = f" ({details})" if details else "" + print(f" {GREEN}OK{RESET} {label}{suffix}") + + def error(self, label: str, expected: Any, actual: Any) -> None: + self.failed += 1 + print(f" {RED}ERROR{RESET} {label}") + print(f" expected: {expected}") + print(f" actual: {actual}") + + def section(self, title: str) -> None: + print(f"\n{BOLD}=== {title} ==={RESET}\n") + + def summary(self) -> int: + total = self.passed + self.failed + print(f"\n{BOLD}--- Summary ---{RESET}") + print(f" {GREEN}Passed:{RESET} {self.passed}/{total}") + if self.failed: + print(f" {RED}Failed:{RESET} {self.failed}/{total}") + return 1 + print(" All checks passed.") + return 0 + + +# --------------------------------------------------------------------------- +# Bytecode helpers +# --------------------------------------------------------------------------- + +@lru_cache(maxsize=None) +def load_deployed_bytecode( + sol_file: str, contract_name: str +) -> tuple[bytes, dict, list[dict]]: + """Return (bytecode, immutable_refs, link_refs) from the Forge artifact.""" + path = ARTIFACTS_DIR / sol_file / f"{contract_name}.json" + if not path.exists(): + sys.exit(f"Artifact not found: {path}\nRun `forge build` first.") + with open(path) as f: + artifact = json.load(f) + deployed = artifact["deployedBytecode"] + bytecode_hex = deployed["object"] + if bytecode_hex.startswith("0x"): + bytecode_hex = bytecode_hex[2:] + + bytecode_hex = re.sub(r"__\$[0-9a-fA-F]+\$__", "0" * 40, bytecode_hex) + + immutable_refs: dict[str, list[dict]] = {} + for key, ranges in deployed.get("immutableReferences", {}).items(): + immutable_refs[key] = ranges + + link_refs: list[dict] = [] + for file_refs in deployed.get("linkReferences", {}).values(): + for _, ranges in file_refs.items(): + link_refs.extend(ranges) + + return bytes.fromhex(bytecode_hex), immutable_refs, link_refs + + +def link_artifact_bytecode( + artifact_bytes: bytearray, + onchain_bytes: bytes, + link_refs: list[dict], +) -> bytearray: + """Patch library placeholders in *artifact_bytes* with real addresses + extracted from *onchain_bytes*.""" + if not link_refs: + return artifact_bytes + + first = link_refs[0] + address = onchain_bytes[first["start"]:first["start"] + first["length"]] + + for ref in link_refs[1:]: + chunk = onchain_bytes[ref["start"]:ref["start"] + ref["length"]] + if chunk != address: + raise ValueError( + f"Library address mismatch in on-chain bytecode: " + f"offset {first['start']} has {address.hex()}, " + f"offset {ref['start']} has {chunk.hex()}" + ) + + for ref in link_refs: + start = ref["start"] + length = ref["length"] + artifact_bytes[start:start + length] = address + + return artifact_bytes + + +def mask_bytecode_ranges(bytecode: bytes, refs: dict) -> bytearray: + masked = bytearray(bytecode) + for ranges in refs.values(): + for ref in ranges: + start = ref["start"] + length = ref["length"] + for i in range(start, start + length): + if i < len(masked): + masked[i] = 0 + return masked + + +def _first_diff_offset(a: bytes | bytearray, b: bytes | bytearray) -> int: + for i in range(min(len(a), len(b))): + if a[i] != b[i]: + return i + return min(len(a), len(b)) + + +# --------------------------------------------------------------------------- +# On-chain helpers +# --------------------------------------------------------------------------- + +ERC1967_IMPL_SLOT = int( + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", 16 +) + + +def get_code(w3: Web3, address: str) -> bytes: + return w3.eth.get_code(Web3.to_checksum_address(address)) + + +def get_implementation(w3: Web3, proxy_address: str) -> str: + raw = w3.eth.get_storage_at( + Web3.to_checksum_address(proxy_address), ERC1967_IMPL_SLOT + ) + return Web3.to_checksum_address("0x" + raw[-20:].hex()) + + +def call_spoke_get_liquidation_logic(w3: Web3, spoke_address: str) -> str | None: + spoke_abi = _load_spoke_abi() + try: + contract = w3.eth.contract( + address=Web3.to_checksum_address(spoke_address), abi=spoke_abi + ) + return contract.functions.getLiquidationLogic().call() + except Exception as e: + print(f" {RED}CALL FAILED{RESET} getLiquidationLogic() on {spoke_address}: {e}") + return None + + +def call_owner(w3: Web3, address: str) -> str | None: + owner_abi = [{"inputs": [], "name": "owner", "outputs": [{"type": "address"}], "stateMutability": "view", "type": "function"}] + try: + contract = w3.eth.contract( + address=Web3.to_checksum_address(address), abi=owner_abi + ) + return Web3.to_checksum_address(contract.functions.owner().call()) + except Exception as e: + print(f" {RED}CALL FAILED{RESET} owner() on {address}: {e}") + return None + + +@lru_cache(maxsize=1) +def _load_spoke_abi() -> list: + path = ARTIFACTS_DIR / "ISpoke.sol" / "ISpoke.json" + if not path.exists(): + sys.exit(f"Artifact not found: {path}\nRun `forge build` first.") + with open(path) as f: + return json.load(f)["abi"] + + +# --------------------------------------------------------------------------- +# Verification +# --------------------------------------------------------------------------- + +def _verify_single_bytecode( + w3: Web3, + result: VerificationResult, + label: str, + address: str, + sol_file: str, + contract_name: str, +) -> None: + expected_bytes, immutable_refs, link_refs = load_deployed_bytecode( + sol_file, contract_name + ) + onchain_code = get_code(w3, address) + + if not onchain_code or onchain_code in (b"", b"\x00"): + result.error(label, "deployed bytecode", f"empty code at {address}") + return + + if len(expected_bytes) != len(onchain_code): + result.error( + label, + f"bytecode length {len(expected_bytes)}", + f"bytecode length {len(onchain_code)} at {address}", + ) + return + + expected = bytearray(expected_bytes) + onchain = bytes(onchain_code) + + if link_refs: + expected = link_artifact_bytecode(expected, onchain, link_refs) + + if immutable_refs: + expected_cmp = mask_bytecode_ranges(bytes(expected), immutable_refs) + onchain_cmp = mask_bytecode_ranges(onchain, immutable_refs) + tag = "masked" + else: + expected_cmp = expected + onchain_cmp = bytearray(onchain) + tag = "" + + if expected_cmp == onchain_cmp: + suffix = f"{address} ({tag})" if tag else address + result.ok(label, suffix) + else: + offset = _first_diff_offset(expected_cmp, onchain_cmp) + kind = f"matching bytecode ({tag})" if tag else "matching bytecode" + result.error( + label, + kind, + f"mismatch at byte offset {offset} at {address}", + ) + + +def _decode_immutable(raw: bytes, type_hint: str) -> str: + """Decode a 32-byte immutable slot according to its Solidity type.""" + if type_hint == "address": + return Web3.to_checksum_address("0x" + raw[-20:].hex()) + if type_hint == "bytes32": + return "0x" + raw.hex() + # uint types: big-endian integer + return str(int.from_bytes(raw, "big")) + + +def _extract_immutables( + onchain_code: bytes, + immutable_refs: dict[str, list[dict]], + contract_name: str, +) -> list[tuple[str, str, str]]: + """Return [(name, type, decoded_value)] for each immutable in the contract.""" + labels = IMMUTABLE_LABELS.get(contract_name) + sorted_ids = sorted(immutable_refs.keys(), key=lambda x: int(x)) + + entries: list[tuple[str, str, str]] = [] + for i, ast_id in enumerate(sorted_ids): + refs = immutable_refs[ast_id] + first = refs[0] + raw = onchain_code[first["start"]:first["start"] + first["length"]] + + if labels and i < len(labels): + name, type_hint = labels[i] + else: + name, type_hint = f"immutable_{ast_id}", "bytes32" + + entries.append((name, type_hint, _decode_immutable(raw, type_hint))) + return entries + + +def print_immutables( + w3: Web3, report: DeployReport, result: VerificationResult +) -> None: + result.section("Immutable Values") + for name, addr in report.all_addresses(): + suffix = name.rsplit("/", 1)[-1] + artifact = ARTIFACT_MAP.get(suffix) + if artifact is None: + continue + + for sol_file, contract_name, label_prefix in [ + (artifact.sol_file, artifact.contract_name, name), + (artifact.impl_sol_file, artifact.impl_contract_name, f"{name}/Implementation"), + ]: + if sol_file is None: + continue + + _, immutable_refs, _ = load_deployed_bytecode(sol_file, contract_name) + if not immutable_refs: + continue + + target_addr = addr + if label_prefix.endswith("/Implementation"): + target_addr = get_implementation(w3, addr) + + onchain_code = get_code(w3, target_addr) + if not onchain_code or onchain_code in (b"", b"\x00"): + continue + + entries = _extract_immutables(onchain_code, immutable_refs, contract_name) + print(f" {BOLD}{label_prefix}{RESET} ({target_addr})") + for var_name, type_hint, value in entries: + print(f" {var_name} ({type_hint}): {value}") + if var_name == "_admin" and type_hint == "address": + owner = call_owner(w3, value) + if owner: + print(f" ProxyAdmin owner (address): {owner}") + print() + + +def verify_liquidation_logic_libraries( + w3: Web3, report: DeployReport, result: VerificationResult +) -> None: + artifact = ARTIFACT_MAP["LiquidationLogic"] + addresses: dict[str, str] = {} + + for spoke in report.spokes: + lib_addr = call_spoke_get_liquidation_logic(w3, spoke.proxy) + label = f"{spoke.label}/LiquidationLogic" + + if lib_addr is None or lib_addr == "0x" + "0" * 40: + result.error(label, "non-zero library address", str(lib_addr)) + continue + + lib_addr = Web3.to_checksum_address(lib_addr) + addresses[spoke.label] = lib_addr + + _verify_single_bytecode( + w3, result, label, lib_addr, + artifact.sol_file, artifact.contract_name, + ) + + unique_addrs = set(addresses.values()) + if len(unique_addrs) == 1: + result.ok("LiquidationLogic/consistency", f"all spokes use {unique_addrs.pop()}") + elif len(unique_addrs) > 1: + details = ", ".join(f"{lbl}={addr}" for lbl, addr in addresses.items()) + result.error("LiquidationLogic/consistency", "same address across all spokes", details) + + +def verify_bytecode( + w3: Web3, report: DeployReport, result: VerificationResult +) -> None: + result.section("Bytecode Verification") + for name, addr in report.all_addresses(): + suffix = name.rsplit("/", 1)[-1] + artifact = ARTIFACT_MAP.get(suffix) + if artifact is None: + result.error(name, "known artifact mapping", f"no mapping for '{suffix}'") + continue + + _verify_single_bytecode( + w3, result, name, addr, + artifact.sol_file, artifact.contract_name, + ) + + if artifact.impl_sol_file: + impl_addr = get_implementation(w3, addr) + _verify_single_bytecode( + w3, result, f"{name}/Implementation", impl_addr, + artifact.impl_sol_file, artifact.impl_contract_name, + ) + + verify_liquidation_logic_libraries(w3, report, result) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main() -> None: + parser = argparse.ArgumentParser( + description="Verify bytecode of an Aave V4 deployment against local Forge artifacts." + ) + parser.add_argument("--rpc-url", required=True, help="RPC endpoint URL") + parser.add_argument( + "--report", required=True, help="Path to deployment report JSON" + ) + args = parser.parse_args() + + with open(args.report) as f: + report = DeployReport.from_json(json.load(f)) + + w3 = Web3(Web3.HTTPProvider(args.rpc_url)) + if not w3.is_connected(): + print(f"{RED}ERROR{RESET}: Cannot connect to {args.rpc_url}") + sys.exit(1) + + print(f"Connected to chain {w3.eth.chain_id}") + + result = VerificationResult() + verify_bytecode(w3, report, result) + print_immutables(w3, report, result) + sys.exit(result.summary()) + + +if __name__ == "__main__": + main() diff --git a/scripts/verification/verify_deployment.py b/scripts/verification/verify_deployment.py new file mode 100755 index 000000000..48dc278b6 --- /dev/null +++ b/scripts/verification/verify_deployment.py @@ -0,0 +1,2547 @@ +#!/usr/bin/env python3 +"""Aave V4 deployment verification script. + +Reads a deployment report JSON and a config input JSON, connects to an RPC +endpoint, and verifies that all on-chain state matches the expected configuration. +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +import time +from dataclasses import dataclass, field +from decimal import Decimal +from functools import lru_cache +from pathlib import Path +from typing import Any, Optional +from urllib.parse import urlencode +from urllib.request import Request, urlopen + +from web3 import Web3 + +ARTIFACTS_DIR = Path(__file__).resolve().parent.parent.parent / "out" + + +def load_abi(sol_file: str, contract_name: str) -> list: + path = ARTIFACTS_DIR / sol_file / f"{contract_name}.json" + if not path.exists(): + sys.exit(f"Artifact not found: {path}\nRun `forge build` first.") + with open(path) as f: + return json.load(f)["abi"] + + +@dataclass(frozen=True) +class ArtifactInfo: + sol_file: str + contract_name: str + impl_sol_file: Optional[str] = None + impl_contract_name: Optional[str] = None + + +ARTIFACT_MAP: dict[str, ArtifactInfo] = { + "AccessManager": ArtifactInfo("AccessManagerEnumerable.sol", "AccessManagerEnumerable"), + "HubConfigurator": ArtifactInfo("HubConfigurator.sol", "HubConfigurator"), + "SpokeConfigurator": ArtifactInfo("SpokeConfigurator.sol", "SpokeConfigurator"), + "TreasurySpoke": ArtifactInfo( + "TransparentUpgradeableProxy.sol", "TransparentUpgradeableProxy", + impl_sol_file="TreasurySpokeInstance.sol", + impl_contract_name="TreasurySpokeInstance", + ), + "Hub": ArtifactInfo( + "TransparentUpgradeableProxy.sol", "TransparentUpgradeableProxy", + impl_sol_file="HubInstance.sol", + impl_contract_name="HubInstance", + ), + "InterestRateStrategy": ArtifactInfo( + "AssetInterestRateStrategy.sol", "AssetInterestRateStrategy", + ), + "Spoke": ArtifactInfo( + "TransparentUpgradeableProxy.sol", "TransparentUpgradeableProxy", + impl_sol_file="SpokeInstance.sol", + impl_contract_name="SpokeInstance", + ), + "AaveOracle": ArtifactInfo("AaveOracle.sol", "AaveOracle"), + "SignatureGateway": ArtifactInfo("SignatureGateway.sol", "SignatureGateway"), + "NativeTokenGateway": ArtifactInfo("NativeTokenGateway.sol", "NativeTokenGateway"), + "GiverPositionManager": ArtifactInfo("GiverPositionManager.sol", "GiverPositionManager"), + "TakerPositionManager": ArtifactInfo("TakerPositionManager.sol", "TakerPositionManager"), + "ConfigPositionManager": ArtifactInfo("ConfigPositionManager.sol", "ConfigPositionManager"), + "TokenizationSpoke": ArtifactInfo( + "TransparentUpgradeableProxy.sol", "TransparentUpgradeableProxy", + impl_sol_file="TokenizationSpokeInstance.sol", + impl_contract_name="TokenizationSpokeInstance", + ), + "LiquidationLogic": ArtifactInfo("LiquidationLogic.sol", "LiquidationLogic"), +} + + +@dataclass(frozen=True) +class ExpectedCompilerSettings: + solc: str + optimizer_runs: int + via_ir: bool + evm_version: str + + +DEFAULT_COMPILER_SETTINGS = ExpectedCompilerSettings( + solc="0.8.28", + optimizer_runs=44_444_444, + via_ir=False, + evm_version="cancun", +) + +COMPILER_OVERRIDES: dict[str, ExpectedCompilerSettings] = { + "HubInstance": ExpectedCompilerSettings( + solc="0.8.28", optimizer_runs=22_300, via_ir=True, evm_version="cancun", + ), + "SpokeInstance": ExpectedCompilerSettings( + solc="0.8.28", optimizer_runs=750, via_ir=True, evm_version="cancun", + ), +} + + +def _expected_compiler(contract_name: str) -> ExpectedCompilerSettings: + return COMPILER_OVERRIDES.get(contract_name, DEFAULT_COMPILER_SETTINGS) + + +# --------------------------------------------------------------------------- +# Etherscan API helpers +# --------------------------------------------------------------------------- + +_last_etherscan_ts: float = 0.0 + + +def _etherscan_get_source(address: str, api_key: str, chain_id: int = 1) -> dict | None: + """Query Etherscan ``getsourcecode`` for *address*. + + Applies 220 ms rate-limiting between calls to stay under the free-tier + 5 req/s limit. Returns the first result dict or ``None`` on failure. + """ + global _last_etherscan_ts + elapsed = time.monotonic() - _last_etherscan_ts + if elapsed < 0.22: + time.sleep(0.22 - elapsed) + + params = urlencode({ + "chainid": str(chain_id), + "module": "contract", + "action": "getsourcecode", + "address": address, + "apikey": api_key, + }) + url = f"https://api.etherscan.io/v2/api?{params}" + try: + req = Request(url, headers={"User-Agent": "aave-v4-verify/1.0"}) + with urlopen(req, timeout=15) as resp: + data = json.loads(resp.read().decode()) + except Exception as e: + print(f" {RED}WARNING{RESET} Etherscan request failed for {address}: {e}") + return None + finally: + _last_etherscan_ts = time.monotonic() + + if data.get("status") != "1" or not data.get("result"): + msg = data.get("message", "unknown error") + detail = data.get("result", "") + print(f" {RED}WARNING{RESET} Etherscan API error for {address}: {msg} — {detail}") + return None + return data["result"][0] + + +@lru_cache(maxsize=None) +def load_deployed_bytecode( + sol_file: str, contract_name: str +) -> tuple[bytes, dict, list[dict]]: + """Return (bytecode, immutable_refs, link_refs). + + - immutable_refs: positions of constructor-set immutables (must be masked) + - link_refs: positions of library address placeholders (will be patched) + """ + path = ARTIFACTS_DIR / sol_file / f"{contract_name}.json" + if not path.exists(): + sys.exit(f"Artifact not found: {path}\nRun `forge build` first.") + with open(path) as f: + artifact = json.load(f) + deployed = artifact["deployedBytecode"] + bytecode_hex = deployed["object"] + if bytecode_hex.startswith("0x"): + bytecode_hex = bytecode_hex[2:] + + # Replace unlinked library placeholders (__$$__) with zero bytes + # so bytes.fromhex() can parse the string; the positions will be + # overwritten with real addresses before comparison. + bytecode_hex = re.sub(r"__\$[0-9a-fA-F]+\$__", "0" * 40, bytecode_hex) + + immutable_refs: dict[str, list[dict]] = {} + for key, ranges in deployed.get("immutableReferences", {}).items(): + immutable_refs[key] = ranges + + link_refs: list[dict] = [] + for file_refs in deployed.get("linkReferences", {}).values(): + for _, ranges in file_refs.items(): + link_refs.extend(ranges) + + return bytes.fromhex(bytecode_hex), immutable_refs, link_refs + + +def link_artifact_bytecode( + artifact_bytes: bytearray, + onchain_bytes: bytes, + link_refs: list[dict], +) -> bytearray: + """Patch library placeholders in *artifact_bytes* with the real addresses + extracted from *onchain_bytes*, so comparison can be a full 100% match.""" + if not link_refs: + return artifact_bytes + + # Extract the address from the first link ref position as the canonical one + first = link_refs[0] + address = onchain_bytes[first["start"]:first["start"] + first["length"]] + + # Sanity: every link ref position should contain the same address + for ref in link_refs[1:]: + chunk = onchain_bytes[ref["start"]:ref["start"] + ref["length"]] + if chunk != address: + raise ValueError( + f"Library address mismatch in on-chain bytecode: " + f"offset {first['start']} has {address.hex()}, " + f"offset {ref['start']} has {chunk.hex()}" + ) + + for ref in link_refs: + start = ref["start"] + length = ref["length"] + artifact_bytes[start:start + length] = address + + return artifact_bytes + + +def mask_bytecode_ranges(bytecode: bytes, refs: dict) -> bytearray: + masked = bytearray(bytecode) + for ranges in refs.values(): + for ref in ranges: + start = ref["start"] + length = ref["length"] + for i in range(start, start + length): + if i < len(masked): + masked[i] = 0 + return masked + + +def _first_diff_offset(a: bytes | bytearray, b: bytes | bytearray) -> int: + for i in range(min(len(a), len(b))): + if a[i] != b[i]: + return i + return min(len(a), len(b)) + + +GREEN = "\033[92m" +RED = "\033[91m" +YELLOW = "\033[93m" +BOLD = "\033[1m" +RESET = "\033[0m" + + +class VerificationResult: + def __init__(self) -> None: + self.passed = 0 + self.failed = 0 + + def ok(self, label: str, details: str = "") -> None: + self.passed += 1 + suffix = f" ({details})" if details else "" + print(f" {GREEN}OK{RESET} {label}{suffix}") + + def error(self, label: str, expected: Any, actual: Any) -> None: + self.failed += 1 + print(f" {RED}ERROR{RESET} {label}") + print(f" expected: {expected}") + print(f" actual: {actual}") + + def section(self, title: str) -> None: + print(f"\n{BOLD}=== {title} ==={RESET}\n") + + def summary(self) -> int: + total = self.passed + self.failed + print(f"\n{BOLD}--- Summary ---{RESET}") + print(f" {GREEN}Passed:{RESET} {self.passed}/{total}") + if self.failed: + print(f" {RED}Failed:{RESET} {self.failed}/{total}") + return 1 + print(" All checks passed.") + return 0 + + +@dataclass +class HubInfo: + label: str + address: str + ir_strategy: str + + +@dataclass +class SpokeInfo: + label: str + proxy: str + oracle: str + + +@dataclass +class DeployReport: + salt: str + access_manager: str + hub_configurator: str + spoke_configurator: str + treasury_spoke: str + hubs: list[HubInfo] = field(default_factory=list) + spokes: list[SpokeInfo] = field(default_factory=list) + signature_gateway: Optional[str] = None + native_token_gateway: Optional[str] = None + giver_position_manager: Optional[str] = None + taker_position_manager: Optional[str] = None + config_position_manager: Optional[str] = None + + @classmethod + def from_json(cls, data: dict) -> "DeployReport": + hubs: list[HubInfo] = [] + hub_addrs = data.get("hub", {}) + ir_addrs = data.get("irStrategy", {}) + for key, addr in hub_addrs.items(): + hubs.append( + HubInfo( + label=key, + address=addr, + ir_strategy=ir_addrs[key], + ) + ) + + spokes: list[SpokeInfo] = [] + spoke_addrs = data.get("spoke", {}) + oracle_addrs = data.get("oracle", {}) + for key, addr in spoke_addrs.items(): + spokes.append( + SpokeInfo( + label=key, + proxy=addr, + oracle=oracle_addrs[key], + ) + ) + + return cls( + salt=data.get("salt", ""), + access_manager=data["accessManager"], + hub_configurator=data["hubConfigurator"], + spoke_configurator=data["spokeConfigurator"], + treasury_spoke=data["treasurySpoke"], + hubs=hubs, + spokes=spokes, + signature_gateway=data.get("signatureGateway"), + native_token_gateway=data.get("nativeTokenGateway"), + giver_position_manager=data.get("giverPositionManager"), + taker_position_manager=data.get("takerPositionManager"), + config_position_manager=data.get("configPositionManager"), + ) + + def all_addresses(self) -> list[tuple[str, str]]: + pairs: list[tuple[str, str]] = [ + ("AccessManager", self.access_manager), + ("HubConfigurator", self.hub_configurator), + ("SpokeConfigurator", self.spoke_configurator), + ("TreasurySpoke", self.treasury_spoke), + ] + for h in self.hubs: + pairs.append((f"{h.label}/Hub", h.address)) + pairs.append((f"{h.label}/InterestRateStrategy", h.ir_strategy)) + for s in self.spokes: + pairs.append((f"{s.label}/Spoke", s.proxy)) + pairs.append((f"{s.label}/AaveOracle", s.oracle)) + for name, addr in [ + ("SignatureGateway", self.signature_gateway), + ("NativeTokenGateway", self.native_token_gateway), + ("GiverPositionManager", self.giver_position_manager), + ("TakerPositionManager", self.taker_position_manager), + ("ConfigPositionManager", self.config_position_manager), + ]: + if addr: + pairs.append((name, addr)) + return pairs + + def all_report_keys(self) -> list[tuple[str, str]]: + pairs: list[tuple[str, str]] = [ + ("accessManager", self.access_manager), + ("hubConfigurator", self.hub_configurator), + ("spokeConfigurator", self.spoke_configurator), + ("treasurySpoke", self.treasury_spoke), + ] + for h in self.hubs: + pairs.append((h.label, h.address)) + pairs.append((h.label, h.ir_strategy)) + for s in self.spokes: + pairs.append((s.label, s.proxy)) + pairs.append((s.label, s.oracle)) + for name, addr in [ + ("signatureGateway", self.signature_gateway), + ("nativeTokenGateway", self.native_token_gateway), + ("giverPositionManager", self.giver_position_manager), + ("takerPositionManager", self.taker_position_manager), + ("configPositionManager", self.config_position_manager), + ]: + if addr: + pairs.append((name, addr)) + return pairs + + def hub_by_label(self, label: str) -> HubInfo: + for h in self.hubs: + if h.label == label: + return h + raise KeyError(f"Hub '{label}' not found in report") + + def spoke_by_label(self, label: str) -> SpokeInfo: + for s in self.spokes: + if s.label == label: + return s + raise KeyError(f"Spoke '{label}' not found in report") + + +# --------------------------------------------------------------------------- +# XLSX config parser +# --------------------------------------------------------------------------- + +_ASSET_NAME_MAP: dict[str, str] = { + "ETH": "WETH", + "PT-USDE-7MAY2026": "PT_USDE_7MAY2026", + "PT-sUSDE-7MAY2026": "PT_sUSDE_7MAY2026", +} + +_HUB_NAME_MAP: dict[str, str] = { + "Core Hub": "CORE_HUB", + "Prime Hub": "PRIME_HUB", + "Plus Hub": "PLUS_HUB", +} + +_SPOKE_NAME_MAP: dict[str, str] = { + "Main Spoke": "MAIN_SPOKE", + "Lido Spoke": "LIDO_ESPOKE", + "EtherFi Spoke": "ETHERFI_ESPOKE", + "Kelp Spoke": "KELP_ESPOKE", + "Gold Spoke": "GOLD_SPOKE", + "Forex Spoke": "FOREX_SPOKE", + "Lombard BTC Spoke": "LOMBARD_BTC_SPOKE", + "Bluechip Spoke": "BLUECHIP_SPOKE", + "Ethena Ecosystem Spoke": "ETHENA_ECOSYSTEM_SPOKE", + "Ethena Correlated Spoke": "ETHENA_CORRELATED_SPOKE", +} + + +def _norm_asset(name: str) -> str: + return _ASSET_NAME_MAP.get(name, name) + + +def _norm_hub(name: str) -> str: + return _HUB_NAME_MAP.get(name, name) + + +def _norm_spoke(name: str) -> str: + return _SPOKE_NAME_MAP.get(name, name) + + +def _is_tokenization_spoke(spoke_name: str) -> bool: + return "Tokenized" in spoke_name + + +def _tokenize_name_symbol(hub_key: str, token_key: str) -> tuple[str, str]: + """Derive TokenizationSpoke name and symbol from hub and token keys.""" + hub_short = {"CORE_HUB": "Core", "PRIME_HUB": "Prime", "PLUS_HUB": "Plus"}[hub_key] + if token_key.startswith("PT_"): + return f"Tokenized Aave {hub_short} {token_key}", f"a{hub_short}-{token_key}" + return f"Wrapped Aave {hub_short} {token_key}", f"wa{hub_short}{token_key}" + + +def _to_bps(value) -> int: + """Convert a decimal value to basis points (multiply by 10000).""" + if isinstance(value, (int, float)): + return int(round(value * 10000)) + return 0 + + +def _to_wad(value: float) -> str: + """Convert a decimal to a WAD string (multiply by 1e18) without float precision loss.""" + d = Decimal(str(value)).quantize(Decimal("0.0001")) + return str(int(d * 10**18)) + + +def _parse_base_rate(value) -> int: + """Parse base rate: "0" → 0, "0.25%" → 25, numeric → bps.""" + if isinstance(value, (int, float)): + return _to_bps(value) + if isinstance(value, str): + v = value.strip() + if v == "0": + return 0 + if v.endswith("%"): + return int(round(float(v[:-1]) * 100)) + return 0 + + +def _parse_reserve_level_params(ws) -> tuple[list[dict], list[dict], dict[tuple[str, str], int]]: + """Parse 'Reserve Level Params' sheet. + + Returns (spoke_registrations, reserves, tokenize_caps). + tokenize_caps maps (hub_key, token_key) → addCap for tokenization spokes. + """ + spoke_regs: list[dict] = [] + reserves: list[dict] = [] + tokenize_caps: dict[tuple[str, str], int] = {} + + for row in ws.iter_rows(min_row=2, values_only=True): + vals = list(row) + if len(vals) < 11 or vals[0] is None: + continue + + _chain, hub_name, spoke_name, asset_name = vals[0], vals[1], vals[2], vals[3] + add_cap, draw_cap = vals[4], vals[5] + cf, mlb, borrowable, cr, lf = vals[6], vals[7], vals[8], vals[9], vals[10] + + if not hub_name or not spoke_name or not asset_name: + continue + + hub_key = _norm_hub(hub_name) + token_key = _norm_asset(asset_name) + + # Tokenization spokes → extract addCap, skip from reserves/spoke_regs + if _is_tokenization_spoke(spoke_name): + cap = int(add_cap) if isinstance(add_cap, (int, float)) else 0 + tokenize_caps[(hub_key, token_key)] = cap + continue + + spoke_key = _norm_spoke(spoke_name) + + spoke_regs.append({ + "assetKey": token_key, + "hubKey": hub_key, + "spokeKey": spoke_key, + "addCap": int(add_cap) if isinstance(add_cap, (int, float)) else 0, + "drawCap": int(draw_cap) if isinstance(draw_cap, (int, float)) else 0, + }) + + reserve = { + "spokeKey": spoke_key, + "hubKey": hub_key, + "assetKey": token_key, + "borrowable": bool(borrowable), + "collateralFactor": _to_bps(cf), + "collateralRisk": _to_bps(cr) if isinstance(cr, (int, float)) else 0, + } + if isinstance(mlb, (int, float)): + reserve["maxLiquidationBonus"] = int(round((1 + mlb) * 10000)) + if isinstance(lf, (int, float)): + reserve["liquidationFee"] = int(round(lf * 10000)) + + reserves.append(reserve) + + return spoke_regs, reserves, tokenize_caps + + +def _parse_asset_level_ir_params(ws, tokenize_caps: dict[tuple[str, str], int]) -> list[dict]: + """Parse 'Asset Level IR Params' sheet → assets list.""" + assets: list[dict] = [] + + for row in ws.iter_rows(min_row=2, values_only=True): + vals = list(row) + if len(vals) < 8 or vals[0] is None: + continue + + _chain, hub_name, asset_name = vals[0], vals[1], vals[2] + base, slope1, slope2, uoptimal, liq_fee = vals[3], vals[4], vals[5], vals[6], vals[7] + + if not hub_name or not asset_name: + continue + + hub_key = _norm_hub(hub_name) + token_key = _norm_asset(asset_name) + + entry: dict = {"tokenKey": token_key, "hubKey": hub_key} + + if base == "N/A" or slope1 == "N/A": + entry["irData"] = { + "optimalUsageRatio": 9900, + "baseDrawnRate": 0, + "rateGrowthBeforeOptimal": 0, + "rateGrowthAfterOptimal": 0, + } + else: + entry["irData"] = { + "baseDrawnRate": _parse_base_rate(base), + "rateGrowthBeforeOptimal": _to_bps(slope1), + "rateGrowthAfterOptimal": _to_bps(slope2), + "optimalUsageRatio": _to_bps(uoptimal), + } + if isinstance(liq_fee, (int, float)): + entry["liquidityFee"] = _to_bps(liq_fee) + + # Merge tokenization addCap if available + cap = tokenize_caps.get((hub_key, token_key)) + if cap is not None: + name, symbol = _tokenize_name_symbol(hub_key, token_key) + entry["tokenize"] = {"name": name, "symbol": symbol, "addCap": cap} + + assets.append(entry) + + return assets + + +def _parse_spoke_level_params(ws) -> list[dict]: + """Parse 'Spoke Level Params' sheet → spokes list.""" + spokes: list[dict] = [] + + for row in ws.iter_rows(min_row=2, values_only=True): + vals = list(row) + if len(vals) < 6 or vals[0] is None: + continue + + _chain, _hub_name, spoke_name = vals[0], vals[1], vals[2] + lbf, thf, hfmb = vals[3], vals[4], vals[5] + + if not spoke_name: + continue + + spoke_key = _norm_spoke(spoke_name) + + spokes.append({ + "key": spoke_key, + "liquidationConfig": { + "liquidationBonusFactor": _to_bps(lbf), + "targetHealthFactor": _to_wad(thf), + "healthFactorForMaxBonus": _to_wad(hfmb), + }, + }) + + return spokes + + +def load_config_from_xlsx(path: str) -> dict: + """Load configuration from an xlsx file and return a dict for ConfigInput.""" + import openpyxl + + wb = openpyxl.load_workbook(path, data_only=True) + + spoke_regs, reserves, tokenize_caps = _parse_reserve_level_params( + wb["Reserve Level Params"] + ) + assets = _parse_asset_level_ir_params( + wb["Asset Level IR Params"], tokenize_caps + ) + spokes = _parse_spoke_level_params(wb["Spoke Level Params"]) + + # Derive hubs from reserves + seen_hubs: dict[str, dict] = {} + for r in reserves: + hk = r["hubKey"] + if hk not in seen_hubs: + seen_hubs[hk] = {"key": hk} + + return { + "defaults": { + "spokeRegistration": { + "riskPremiumThreshold": 0, + "active": True, + "halted": False, + }, + "reserve": { + "receiveSharesEnabled": True, + "frozen": False, + "paused": False, + }, + }, + "tokens": {}, + "hubs": list(seen_hubs.values()), + "spokes": spokes, + "assets": assets, + "spokeRegistrations": spoke_regs, + "reserves": reserves, + } + + +class ConfigInput: + def __init__(self, data: dict) -> None: + self._raw = data + self.defaults: dict = data.get("defaults", {}) + self.tokens_by_key: dict[str, dict] = data.get("tokens", {}) + self.hubs_by_key: dict[str, dict] = { + h["key"]: h for h in data.get("hubs", []) + } + self.spokes_by_key: dict[str, dict] = { + s["key"]: s for s in data.get("spokes", []) + } + self.assets: list[dict] = data.get("assets", []) + self.spoke_registrations: list[dict] = data.get("spokeRegistrations", []) + self.reserves: list[dict] = data.get("reserves", []) + + def resolve_default( + self, entry: dict, field_name: str, defaults_section: str + ) -> Any: + if field_name in entry: + return entry[field_name] + section = self.defaults.get(defaults_section, {}) + return section[field_name] + + +class ContractCaller: + def __init__(self, w3: Web3) -> None: + self.w3 = w3 + self.hub_abi = load_abi("IHub.sol", "IHub") + self.spoke_abi = load_abi("ISpoke.sol", "ISpoke") + self.ir_strategy_abi = load_abi("IAssetInterestRateStrategy.sol", "IAssetInterestRateStrategy") + self.oracle_abi = load_abi("IAaveOracle.sol", "IAaveOracle") + self.tokenization_spoke_abi = load_abi("ITokenizationSpoke.sol", "ITokenizationSpoke") + self.access_manager_abi = load_abi( + "IAccessManagerEnumerable.sol", "IAccessManagerEnumerable" + ) + self.position_manager_base_abi = load_abi( + "IPositionManagerBase.sol", "IPositionManagerBase" + ) + self.price_oracle_abi = load_abi("IPriceOracle.sol", "IPriceOracle") + self.price_feed_abi = load_abi("IPriceFeed.sol", "IPriceFeed") + + ERC1967_IMPL_SLOT = int( + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", 16 + ) + + def get_code(self, address: str) -> bytes: + return self.w3.eth.get_code(Web3.to_checksum_address(address)) + + def get_implementation(self, proxy_address: str) -> str: + raw = self.w3.eth.get_storage_at( + Web3.to_checksum_address(proxy_address), self.ERC1967_IMPL_SLOT + ) + return Web3.to_checksum_address("0x" + raw[-20:].hex()) + + def _call(self, address: str, abi: list, fn_name: str, *args: Any, silent: bool = False) -> Any: + try: + contract = self.w3.eth.contract( + address=Web3.to_checksum_address(address), abi=abi + ) + return contract.functions[fn_name](*args).call() + except Exception as e: + if not silent: + print(f" {RED}CALL FAILED{RESET} {fn_name}({', '.join(str(a) for a in args)}) on {address}: {e}") + return None + + def call_hub(self, hub_addr: str, fn_name: str, *args: Any) -> Any: + return self._call(hub_addr, self.hub_abi, fn_name, *args) + + def call_spoke(self, spoke_addr: str, fn_name: str, *args: Any) -> Any: + return self._call(spoke_addr, self.spoke_abi, fn_name, *args) + + def call_ir_strategy(self, addr: str, fn_name: str, *args: Any) -> Any: + return self._call(addr, self.ir_strategy_abi, fn_name, *args) + + def call_oracle(self, addr: str, fn_name: str, *args: Any) -> Any: + return self._call(addr, self.oracle_abi, fn_name, *args) + + def call_tokenization_spoke(self, addr: str, fn_name: str, *args: Any, silent: bool = False) -> Any: + return self._call(addr, self.tokenization_spoke_abi, fn_name, *args, silent=silent) + + def call_access_manager(self, addr: str, fn_name: str, *args: Any) -> Any: + return self._call(addr, self.access_manager_abi, fn_name, *args) + + def call_price_feed(self, addr: str, fn_name: str, *args: Any) -> Any: + return self._call(addr, self.price_feed_abi, fn_name, *args) + + +# --------------------------------------------------------------------------- +# Batch RPC infrastructure +# --------------------------------------------------------------------------- + +MAX_BATCH_SIZE = 100 + +class BatchCallManager: + """Wraps web3.py batch_requests() to execute many eth_call requests in one + JSON-RPC batch. Accepts a list of (key, contract_call_builder) tuples, + executes them in chunks of *max_batch_size*, and returns a dict keyed by + the caller-provided keys.""" + + def __init__(self, w3: Web3, max_batch_size: int = MAX_BATCH_SIZE) -> None: + self.w3 = w3 + self.max_batch_size = max_batch_size + + def execute( + self, calls: list[tuple[str, Any]] + ) -> tuple[dict[str, Any], dict[str, str]]: + """Execute *calls* — a list of ``(key, contract_fn_call)`` where + ``contract_fn_call`` is e.g. + ``contract.functions.getAssetId(token_addr)`` (no ``.call()``). + + Returns ``(results, errors)`` dicts keyed by *key*. + """ + results: dict[str, Any] = {} + errors: dict[str, str] = {} + if not calls: + return results, errors + + # Process in chunks to respect node batch-size limits + for chunk_start in range(0, len(calls), self.max_batch_size): + chunk = calls[chunk_start : chunk_start + self.max_batch_size] + chunk_keys = [k for k, _ in chunk] + + batch = self.w3.batch_requests() + for _, fn_call in chunk: + batch.add(fn_call) + + try: + responses = batch.execute() + except Exception as e: + for key in chunk_keys: + errors[key] = str(e) + continue + + for i, key in enumerate(chunk_keys): + try: + results[key] = responses[i] + except Exception as e: + errors[key] = str(e) + + return results, errors + + +@dataclass +class DeploymentCache: + """Pre-fetched shared data used across all verify functions.""" + # (hub_addr, token_addr) -> asset_id or None + asset_ids: dict[tuple[str, str], int | None] = field(default_factory=dict) + # (spoke_addr, hub_addr, asset_id) -> reserve_id or None + reserve_ids: dict[tuple[str, str, int], int | None] = field(default_factory=dict) + + +def prefetch_shared_data( + batch_mgr: BatchCallManager, + caller: ContractCaller, + report: DeployReport, + config: ConfigInput, +) -> DeploymentCache: + """Pre-fetch all asset_ids and reserve_ids in 2 batch round-trips.""" + cache = DeploymentCache() + + if not config.tokens_by_key: + print(f" {YELLOW}SKIPPED{RESET} prefetch — no token addresses in config") + return cache + + # Collect all unique (hub_addr, token_addr) pairs + hub_token_pairs: dict[tuple[str, str], str] = {} # -> key for dedup + for entry in config.assets: + token_info = config.tokens_by_key.get(entry["tokenKey"]) + if not token_info or "address" not in token_info: + continue + hub_info = report.hub_by_label(entry["hubKey"]) + token_addr = Web3.to_checksum_address(token_info["address"]) + hub_addr = Web3.to_checksum_address(hub_info.address) + hub_token_pairs[(hub_addr, token_addr)] = f"{hub_addr}:{token_addr}" + for entry in config.spoke_registrations: + token_info = config.tokens_by_key.get(entry["assetKey"]) + if not token_info or "address" not in token_info: + continue + hub_info = report.hub_by_label(entry["hubKey"]) + token_addr = Web3.to_checksum_address(token_info["address"]) + hub_addr = Web3.to_checksum_address(hub_info.address) + hub_token_pairs[(hub_addr, token_addr)] = f"{hub_addr}:{token_addr}" + for entry in config.reserves: + token_info = config.tokens_by_key.get(entry["assetKey"]) + if not token_info or "address" not in token_info: + continue + hub_info = report.hub_by_label(entry["hubKey"]) + token_addr = Web3.to_checksum_address(token_info["address"]) + hub_addr = Web3.to_checksum_address(hub_info.address) + hub_token_pairs[(hub_addr, token_addr)] = f"{hub_addr}:{token_addr}" + + # Round 1: batch all getAssetId calls + asset_id_calls: list[tuple[str, Any]] = [] + for (hub_addr, token_addr), key in hub_token_pairs.items(): + contract = batch_mgr.w3.eth.contract( + address=hub_addr, abi=caller.hub_abi + ) + asset_id_calls.append((key, contract.functions.getAssetId(token_addr))) + + results, errors = batch_mgr.execute(asset_id_calls) + for (hub_addr, token_addr), key in hub_token_pairs.items(): + if key in errors: + print(f" {RED}CALL FAILED{RESET} getAssetId prefetch for {key}: {errors[key]}") + cache.asset_ids[(hub_addr, token_addr)] = None + else: + cache.asset_ids[(hub_addr, token_addr)] = results.get(key) + + # Collect all unique (spoke_addr, hub_addr, asset_id) for reserves + oracles + reserve_id_keys: dict[tuple[str, str, int], str] = {} + for entry in config.reserves: + token_info = config.tokens_by_key.get(entry["assetKey"]) + if not token_info or "address" not in token_info: + continue + hub_info = report.hub_by_label(entry["hubKey"]) + spoke_info = report.spoke_by_label(entry["spokeKey"]) + token_addr = Web3.to_checksum_address(token_info["address"]) + hub_addr = Web3.to_checksum_address(hub_info.address) + spoke_addr = Web3.to_checksum_address(spoke_info.proxy) + asset_id = cache.asset_ids.get((hub_addr, token_addr)) + if asset_id is None: + continue + triple = (spoke_addr, hub_addr, asset_id) + reserve_id_keys[triple] = f"{spoke_addr}:{hub_addr}:{asset_id}" + + # Round 2: batch all getReserveId calls + reserve_id_calls: list[tuple[str, Any]] = [] + for (spoke_addr, hub_addr, asset_id), key in reserve_id_keys.items(): + contract = batch_mgr.w3.eth.contract( + address=spoke_addr, abi=caller.spoke_abi + ) + reserve_id_calls.append((key, contract.functions.getReserveId(hub_addr, asset_id))) + + results, errors = batch_mgr.execute(reserve_id_calls) + for (spoke_addr, hub_addr, asset_id), key in reserve_id_keys.items(): + if key in errors: + print(f" {RED}CALL FAILED{RESET} getReserveId prefetch for {key}: {errors[key]}") + cache.reserve_ids[(spoke_addr, hub_addr, asset_id)] = None + else: + cache.reserve_ids[(spoke_addr, hub_addr, asset_id)] = results.get(key) + + return cache + + +# --------------------------------------------------------------------------- +# Verification functions +# --------------------------------------------------------------------------- + + +def _verify_single_bytecode( + caller: ContractCaller, + result: VerificationResult, + label: str, + address: str, + sol_file: str, + contract_name: str, +) -> None: + expected_bytes, immutable_refs, link_refs = load_deployed_bytecode( + sol_file, contract_name + ) + onchain_code = caller.get_code(address) + + if not onchain_code or onchain_code in (b"", b"\x00"): + result.error(label, "deployed bytecode", f"empty code at {address}") + return + + if len(expected_bytes) != len(onchain_code): + result.error( + label, + f"bytecode length {len(expected_bytes)}", + f"bytecode length {len(onchain_code)} at {address}", + ) + return + + expected = bytearray(expected_bytes) + onchain = bytes(onchain_code) + + # Bytecode verification uses up to three steps depending on the contract: + # + # 1. Library linking (link_refs): the compiled artifact contains placeholder + # slots where external library addresses will be inserted by the linker. + # We read the real addresses from the on-chain bytecode at those offsets + # and patch them into the artifact so the two can be compared directly. + # Example: SpokeInstance links against deployed library contracts. + # + # 2. Immutable masking (immutable_refs): values set in the constructor + # (e.g. a proxy admin address) are baked into deployed bytecode by the + # compiler but are absent from the compiled artifact. Since these bytes + # will always differ, we zero them out on both sides before comparing. + # Example: proxy contracts whose admin is a constructor argument. + # + # 3. Direct match: contracts with no link_refs and no immutable_refs + # (e.g. Hub implementations) are compared byte-for-byte with no masking. + # + # When masking is applied the result is tagged "(masked)" to signal that + # immutable positions were excluded — a slightly weaker guarantee than a + # full direct match, but expected for contracts with constructor immutables. + + # Patch library placeholders with real addresses from on-chain bytecode + if link_refs: + expected = link_artifact_bytecode(expected, onchain, link_refs) + + # Mask immutable positions (constructor-set values differ per deployment) + if immutable_refs: + expected_cmp = mask_bytecode_ranges(bytes(expected), immutable_refs) + onchain_cmp = mask_bytecode_ranges(onchain, immutable_refs) + tag = "masked" + else: + expected_cmp = expected + onchain_cmp = bytearray(onchain) + tag = "" + + if expected_cmp == onchain_cmp: + suffix = f"{address} ({tag})" if tag else address + result.ok(label, suffix) + else: + offset = _first_diff_offset(expected_cmp, onchain_cmp) + kind = f"matching bytecode ({tag})" if tag else "matching bytecode" + result.error( + label, + kind, + f"mismatch at byte offset {offset} at {address}", + ) + + +def verify_liquidation_logic_libraries( + caller: ContractCaller, report: DeployReport, result: VerificationResult +) -> None: + """Verify LiquidationLogic library address and bytecode for each Spoke.""" + artifact = ARTIFACT_MAP["LiquidationLogic"] + addresses: dict[str, str] = {} # spoke_label -> lib address + + for spoke in report.spokes: + spoke_addr = Web3.to_checksum_address(spoke.proxy) + lib_addr = caller.call_spoke(spoke_addr, "getLiquidationLogic") + label = f"{spoke.label}/LiquidationLogic" + + if lib_addr is None or lib_addr == "0x" + "0" * 40: + result.error(label, "non-zero library address", str(lib_addr)) + continue + + lib_addr = Web3.to_checksum_address(lib_addr) + addresses[spoke.label] = lib_addr + + _verify_single_bytecode( + caller, result, label, lib_addr, + artifact.sol_file, artifact.contract_name, + ) + + unique_addrs = set(addresses.values()) + if len(unique_addrs) == 1: + result.ok("LiquidationLogic/consistency", f"all spokes use {unique_addrs.pop()}") + elif len(unique_addrs) > 1: + details = ", ".join(f"{lbl}={addr}" for lbl, addr in addresses.items()) + result.error("LiquidationLogic/consistency", "same address across all spokes", details) + + +def verify_bytecode( + caller: ContractCaller, report: DeployReport, result: VerificationResult +) -> None: + result.section("Bytecode Verification") + for name, addr in report.all_addresses(): + suffix = name.rsplit("/", 1)[-1] + artifact = ARTIFACT_MAP.get(suffix) + if artifact is None: + result.error(name, "known artifact mapping", f"no mapping for '{suffix}'") + continue + + _verify_single_bytecode( + caller, result, name, addr, + artifact.sol_file, artifact.contract_name, + ) + + if artifact.impl_sol_file: + impl_addr = caller.get_implementation(addr) + _verify_single_bytecode( + caller, result, f"{name}/Implementation", impl_addr, + artifact.impl_sol_file, artifact.impl_contract_name, + ) + + verify_liquidation_logic_libraries(caller, report, result) + + +def verify_hub_assets( + batch_mgr: BatchCallManager, + caller: ContractCaller, + cache: DeploymentCache, + report: DeployReport, + config: ConfigInput, + result: VerificationResult, +) -> None: + result.section("Hub Assets & Interest Rate Configuration") + + if not config.tokens_by_key: + print(f" {YELLOW}SKIPPED{RESET} — no token addresses in config") + return + + # Build batch: getInterestRateData + getAssetConfig for each asset + ir_calls: list[tuple[str, Any]] = [] + cfg_calls: list[tuple[str, Any]] = [] + valid_entries: list[tuple[str, dict, HubInfo, int]] = [] + + for entry in config.assets: + hub_key = entry["hubKey"] + token_key = entry["tokenKey"] + token_info = config.tokens_by_key.get(token_key) + if not token_info or "address" not in token_info: + continue + hub_info = report.hub_by_label(hub_key) + token_addr = Web3.to_checksum_address(token_info["address"]) + hub_addr = Web3.to_checksum_address(hub_info.address) + label = f"{hub_key}/{token_key}" + + asset_id = cache.asset_ids.get((hub_addr, token_addr)) + if asset_id is None: + result.error(label, "valid assetId", "call failed") + continue + + valid_entries.append((label, entry, hub_info, asset_id)) + + ir_contract = batch_mgr.w3.eth.contract( + address=Web3.to_checksum_address(hub_info.ir_strategy), + abi=caller.ir_strategy_abi, + ) + ir_calls.append((f"{label}/IR", ir_contract.functions.getInterestRateData(asset_id))) + + hub_contract = batch_mgr.w3.eth.contract( + address=hub_addr, abi=caller.hub_abi, + ) + cfg_calls.append((f"{label}/cfg", hub_contract.functions.getAssetConfig(asset_id))) + + ir_results, ir_errors = batch_mgr.execute(ir_calls) + cfg_results, cfg_errors = batch_mgr.execute(cfg_calls) + + for label, entry, hub_info, asset_id in valid_entries: + ir_key = f"{label}/IR" + if ir_key in ir_errors: + result.error(ir_key, "interest rate data", f"call failed: {ir_errors[ir_key]}") + else: + ir_data = ir_results.get(ir_key) + if ir_data is None: + result.error(ir_key, "interest rate data", "call failed") + elif "irData" in entry: + ir_input = entry["irData"] + _check(result, f"{label}/optimalUsageRatio", ir_input["optimalUsageRatio"], ir_data[0]) + _check(result, f"{label}/baseDrawnRate", ir_input["baseDrawnRate"], ir_data[1]) + _check(result, f"{label}/rateGrowthBeforeOptimal", ir_input["rateGrowthBeforeOptimal"], ir_data[2]) + _check(result, f"{label}/rateGrowthAfterOptimal", ir_input["rateGrowthAfterOptimal"], ir_data[3]) + + cfg_key = f"{label}/cfg" + if cfg_key in cfg_errors: + result.error(f"{label}/assetConfig", "asset config", f"call failed: {cfg_errors[cfg_key]}") + else: + asset_cfg = cfg_results.get(cfg_key) + if asset_cfg is None: + result.error(f"{label}/assetConfig", "asset config", "call failed") + else: + expected_fee = entry.get( + "liquidityFee", + config.defaults.get("asset", {}).get("liquidityFee", 0), + ) + _check(result, f"{label}/liquidityFee", expected_fee, asset_cfg[1]) + + +def verify_spoke_registrations( + batch_mgr: BatchCallManager, + caller: ContractCaller, + cache: DeploymentCache, + report: DeployReport, + config: ConfigInput, + result: VerificationResult, +) -> None: + result.section("Spoke Registrations (Hub-side)") + + if not config.spoke_registrations: + print(f" {YELLOW}SKIPPED{RESET} — no spoke registration data in config") + return + + spoke_cfg_calls: list[tuple[str, Any]] = [] + valid_entries: list[tuple[str, dict]] = [] + + for entry in config.spoke_registrations: + hub_key = entry["hubKey"] + spoke_key = entry["spokeKey"] + asset_key = entry["assetKey"] + hub_info = report.hub_by_label(hub_key) + spoke_info = report.spoke_by_label(spoke_key) + token_info = config.tokens_by_key.get(asset_key) + if not token_info or "address" not in token_info: + continue + token_addr = Web3.to_checksum_address(token_info["address"]) + hub_addr = Web3.to_checksum_address(hub_info.address) + label = f"{hub_key}/{spoke_key}/{asset_key}" + + asset_id = cache.asset_ids.get((hub_addr, token_addr)) + if asset_id is None: + result.error(label, "valid assetId", "call failed") + continue + + spoke_proxy = Web3.to_checksum_address(spoke_info.proxy) + hub_contract = batch_mgr.w3.eth.contract( + address=hub_addr, abi=caller.hub_abi, + ) + spoke_cfg_calls.append((label, hub_contract.functions.getSpokeConfig(asset_id, spoke_proxy))) + valid_entries.append((label, entry)) + + results, errors = batch_mgr.execute(spoke_cfg_calls) + + for label, entry in valid_entries: + if label in errors: + result.error(f"{label}/spokeConfig", "spoke config", f"call failed: {errors[label]}") + continue + spoke_cfg = results.get(label) + if spoke_cfg is None: + result.error(f"{label}/spokeConfig", "spoke config", "call failed") + continue + + _check(result, f"{label}/addCap", entry["addCap"], spoke_cfg[0]) + _check(result, f"{label}/drawCap", entry["drawCap"], spoke_cfg[1]) + _check( + result, + f"{label}/riskPremiumThreshold", + config.resolve_default(entry, "riskPremiumThreshold", "spokeRegistration"), + spoke_cfg[2], + ) + _check( + result, + f"{label}/active", + config.resolve_default(entry, "active", "spokeRegistration"), + spoke_cfg[3], + ) + _check( + result, + f"{label}/halted", + config.resolve_default(entry, "halted", "spokeRegistration"), + spoke_cfg[4], + ) + + +def verify_reserves( + batch_mgr: BatchCallManager, + caller: ContractCaller, + cache: DeploymentCache, + report: DeployReport, + config: ConfigInput, + result: VerificationResult, +) -> None: + result.section("Reserve Configuration (Spoke-side)") + + if not config.tokens_by_key: + print(f" {YELLOW}SKIPPED{RESET} — no token addresses in config") + return + + # Batch A: getReserveConfig + getReserve for each reserve + rcfg_calls: list[tuple[str, Any]] = [] + reserve_calls: list[tuple[str, Any]] = [] + valid_entries: list[tuple[str, dict, str, int]] = [] # label, entry, spoke_addr, reserve_id + + for entry in config.reserves: + hub_key = entry["hubKey"] + spoke_key = entry["spokeKey"] + asset_key = entry["assetKey"] + hub_info = report.hub_by_label(hub_key) + spoke_info = report.spoke_by_label(spoke_key) + token_info = config.tokens_by_key.get(asset_key) + if not token_info or "address" not in token_info: + continue + token_addr = Web3.to_checksum_address(token_info["address"]) + hub_addr = Web3.to_checksum_address(hub_info.address) + spoke_addr = Web3.to_checksum_address(spoke_info.proxy) + label = f"{spoke_key}/{hub_key}/{asset_key}" + + asset_id = cache.asset_ids.get((hub_addr, token_addr)) + if asset_id is None: + result.error(label, "valid assetId", "call failed") + continue + + reserve_id = cache.reserve_ids.get((spoke_addr, hub_addr, asset_id)) + if reserve_id is None: + result.error(f"{label}/reserveId", "valid reserveId", "call failed") + continue + + valid_entries.append((label, entry, spoke_addr, reserve_id)) + + spoke_contract = batch_mgr.w3.eth.contract( + address=spoke_addr, abi=caller.spoke_abi, + ) + rcfg_calls.append((f"{label}/rcfg", spoke_contract.functions.getReserveConfig(reserve_id))) + reserve_calls.append((f"{label}/reserve", spoke_contract.functions.getReserve(reserve_id))) + + rcfg_results, rcfg_errors = batch_mgr.execute(rcfg_calls) + reserve_results, reserve_errors = batch_mgr.execute(reserve_calls) + + # Check ReserveConfig results and build Batch B for getDynamicReserveConfig + drc_calls: list[tuple[str, Any]] = [] + drc_entries: list[tuple[str, dict]] = [] + + for label, entry, spoke_addr, reserve_id in valid_entries: + rcfg_key = f"{label}/rcfg" + if rcfg_key in rcfg_errors: + result.error(f"{label}/reserveConfig", "reserve config", f"call failed: {rcfg_errors[rcfg_key]}") + else: + rcfg = rcfg_results.get(rcfg_key) + if rcfg is None: + result.error(f"{label}/reserveConfig", "reserve config", "call failed") + else: + if "collateralRisk" in entry: + _check(result, f"{label}/collateralRisk", entry["collateralRisk"], rcfg[0]) + if config.defaults.get("reserve") or "paused" in entry: + _check( + result, + f"{label}/paused", + config.resolve_default(entry, "paused", "reserve"), + rcfg[1], + ) + if config.defaults.get("reserve") or "frozen" in entry: + _check( + result, + f"{label}/frozen", + config.resolve_default(entry, "frozen", "reserve"), + rcfg[2], + ) + _check(result, f"{label}/borrowable", entry["borrowable"], rcfg[3]) + if config.defaults.get("reserve") or "receiveSharesEnabled" in entry: + _check( + result, + f"{label}/receiveSharesEnabled", + config.resolve_default(entry, "receiveSharesEnabled", "reserve"), + rcfg[4], + ) + + reserve_key = f"{label}/reserve" + if reserve_key in reserve_errors: + result.error(f"{label}/reserve", "reserve data", f"call failed: {reserve_errors[reserve_key]}") + continue + reserve = reserve_results.get(reserve_key) + if reserve is None: + result.error(f"{label}/reserve", "reserve data", "call failed") + continue + + dynamic_config_key = reserve[6] + spoke_contract = batch_mgr.w3.eth.contract( + address=spoke_addr, abi=caller.spoke_abi, + ) + drc_calls.append((f"{label}/drc", spoke_contract.functions.getDynamicReserveConfig(reserve_id, dynamic_config_key))) + drc_entries.append((label, entry)) + + # Batch B: getDynamicReserveConfig + drc_results, drc_errors = batch_mgr.execute(drc_calls) + + for label, entry in drc_entries: + drc_key = f"{label}/drc" + if drc_key in drc_errors: + result.error(f"{label}/dynamicReserveConfig", "dynamic config", f"call failed: {drc_errors[drc_key]}") + else: + drc = drc_results.get(drc_key) + if drc is None: + result.error(f"{label}/dynamicReserveConfig", "dynamic config", "call failed") + else: + cf = entry.get("collateralFactor") + if cf is not None: + _check(result, f"{label}/collateralFactor", cf, drc[0]) + if "maxLiquidationBonus" in entry or "maxLiquidationBonus" in config.defaults.get("reserve", {}): + _check( + result, + f"{label}/maxLiquidationBonus", + config.resolve_default(entry, "maxLiquidationBonus", "reserve"), + drc[1], + ) + if "liquidationFee" in entry or "liquidationFee" in config.defaults.get("reserve", {}): + _check( + result, + f"{label}/liquidationFee", + config.resolve_default(entry, "liquidationFee", "reserve"), + drc[2], + ) + + +def verify_liquidation_configs( + batch_mgr: BatchCallManager, + caller: ContractCaller, + report: DeployReport, + config: ConfigInput, + result: VerificationResult, +) -> None: + result.section("Liquidation Configuration") + + has_liq_config = any( + "liquidationConfig" in s for s in config.spokes_by_key.values() + ) + if not has_liq_config: + print(f" {YELLOW}SKIPPED{RESET} — no liquidation config data in config") + return + + calls: list[tuple[str, Any]] = [] + entries: list[tuple[str, dict]] = [] + + for spoke_entry in config.spokes_by_key.values(): + spoke_key = spoke_entry["key"] + spoke_info = report.spoke_by_label(spoke_key) + spoke_contract = batch_mgr.w3.eth.contract( + address=Web3.to_checksum_address(spoke_info.proxy), + abi=caller.spoke_abi, + ) + calls.append((spoke_key, spoke_contract.functions.getLiquidationConfig())) + entries.append((spoke_key, spoke_entry)) + + results, errors = batch_mgr.execute(calls) + + for label, spoke_entry in entries: + if label in errors: + result.error(f"{label}/liquidationConfig", "liquidation config", f"call failed: {errors[label]}") + continue + liq_cfg = results.get(label) + if liq_cfg is None: + result.error(f"{label}/liquidationConfig", "liquidation config", "call failed") + continue + + liq_input = spoke_entry.get("liquidationConfig", {}) + defaults_liq = config.defaults.get("spoke", {}).get("liquidationConfig", {}) + + expected_thf = liq_input.get( + "targetHealthFactor", defaults_liq.get("targetHealthFactor") + ) + expected_hfmb = liq_input.get( + "healthFactorForMaxBonus", defaults_liq.get("healthFactorForMaxBonus") + ) + expected_lbf = liq_input.get( + "liquidationBonusFactor", defaults_liq.get("liquidationBonusFactor") + ) + + _check(result, f"{label}/targetHealthFactor", expected_thf, liq_cfg[0]) + _check(result, f"{label}/healthFactorForMaxBonus", expected_hfmb, liq_cfg[1]) + _check(result, f"{label}/liquidationBonusFactor", expected_lbf, liq_cfg[2]) + + +def verify_oracles( + batch_mgr: BatchCallManager, + caller: ContractCaller, + cache: DeploymentCache, + report: DeployReport, + config: ConfigInput, + result: VerificationResult, +) -> None: + result.section("Oracle Configuration") + + if not config.tokens_by_key: + print(f" {YELLOW}SKIPPED{RESET} — no token addresses in config") + return + + expected_decimals = config.defaults.get("spoke", {}).get("oracleDecimals", 8) + + # Batch all decimals() + getReserveSource() calls + calls: list[tuple[str, Any]] = [] + + # Track decimals calls for output + decimals_keys: list[tuple[str, str]] = [] # (key, label) + # Track source calls + source_entries: list[tuple[str, str, str]] = [] # (key, res_label, asset_key) + + for spoke_info in report.spokes: + oracle_addr = Web3.to_checksum_address(spoke_info.oracle) + label = spoke_info.label + + oracle_contract = batch_mgr.w3.eth.contract( + address=oracle_addr, abi=caller.oracle_abi, + ) + dec_key = f"{label}/oracle/decimals" + calls.append((dec_key, oracle_contract.functions.decimals())) + decimals_keys.append((dec_key, label)) + + for entry in config.reserves: + if entry["spokeKey"] != spoke_info.label: + continue + + hub_key = entry["hubKey"] + asset_key = entry["assetKey"] + token_info = config.tokens_by_key.get(asset_key) + if not token_info or "address" not in token_info: + continue + hub_info = report.hub_by_label(hub_key) + token_addr = Web3.to_checksum_address(token_info["address"]) + hub_addr = Web3.to_checksum_address(hub_info.address) + spoke_addr = Web3.to_checksum_address(spoke_info.proxy) + res_label = f"{label}/{hub_key}/{asset_key}" + + asset_id = cache.asset_ids.get((hub_addr, token_addr)) + if asset_id is None: + result.error(f"{res_label}/oracle/source", "valid assetId", "call failed") + continue + + reserve_id = cache.reserve_ids.get((spoke_addr, hub_addr, asset_id)) + if reserve_id is None: + result.error(f"{res_label}/oracle/source", "valid reserveId", "call failed") + continue + + src_key = f"{res_label}/oracle/source" + calls.append((src_key, oracle_contract.functions.getReserveSource(reserve_id))) + source_entries.append((src_key, res_label, asset_key)) + + results, errors = batch_mgr.execute(calls) + + for dec_key, label in decimals_keys: + if dec_key in errors: + result.error(f"{label}/oracle/decimals", "decimals", f"call failed: {errors[dec_key]}") + else: + decimals = results.get(dec_key) + if decimals is None: + result.error(f"{label}/oracle/decimals", "decimals", "call failed") + else: + _check(result, f"{label}/oracle/decimals", expected_decimals, decimals) + + for src_key, res_label, asset_key in source_entries: + if src_key in errors: + result.error(f"{res_label}/oracle/source", "source address", f"call failed: {errors[src_key]}") + else: + source = results.get(src_key) + if source is None: + result.error(f"{res_label}/oracle/source", "source address", "call failed") + else: + token_info = config.tokens_by_key.get(asset_key, {}) + price_feed = token_info.get("priceFeed") + if price_feed: + expected_feed = Web3.to_checksum_address(price_feed) + _check(result, f"{res_label}/oracle/source", expected_feed, source) + + +def verify_oracle_wiring( + batch_mgr: BatchCallManager, + caller: ContractCaller, + report: DeployReport, + result: VerificationResult, +) -> None: + result.section("Oracle Wiring Verification") + + calls: list[tuple[str, Any]] = [] + expectations: list[tuple[str, str, str]] = [] # (key, direction, expected_addr) + + for spoke_info in report.spokes: + spoke_addr = Web3.to_checksum_address(spoke_info.proxy) + oracle_addr = Web3.to_checksum_address(spoke_info.oracle) + + # Spoke -> Oracle link + spoke_contract = batch_mgr.w3.eth.contract( + address=spoke_addr, abi=caller.spoke_abi, + ) + fwd_key = f"{spoke_info.label}/spoke->oracle" + calls.append((fwd_key, spoke_contract.functions.ORACLE())) + expectations.append((fwd_key, "ORACLE()", oracle_addr)) + + # Oracle -> Spoke link + oracle_contract = batch_mgr.w3.eth.contract( + address=oracle_addr, abi=caller.price_oracle_abi, + ) + rev_key = f"{spoke_info.label}/oracle->spoke" + calls.append((rev_key, oracle_contract.functions.spoke())) + expectations.append((rev_key, "spoke()", spoke_addr)) + + results, errors = batch_mgr.execute(calls) + + for key, fn_name, expected_addr in expectations: + if key in errors: + result.error(key, expected_addr, f"call failed: {errors[key]}") + else: + actual = results.get(key) + if actual is not None: + actual = Web3.to_checksum_address(actual) + _check(result, key, expected_addr, actual) + + +def verify_price_feeds( + batch_mgr: BatchCallManager, + caller: ContractCaller, + report: DeployReport, + config: ConfigInput, + result: VerificationResult, +) -> None: + result.section("Price Feed Verification") + + if not config.tokens_by_key: + print(f" {YELLOW}SKIPPED{RESET} — no token/price feed data in config") + return + + expected_decimals = config.defaults.get("spoke", {}).get("oracleDecimals", 8) + + # Collect unique price feed addresses across all reserves + feed_addresses: dict[str, list[str]] = {} # feed_addr -> [asset_keys that use it] + for entry in config.reserves: + asset_key = entry["assetKey"] + token_info = config.tokens_by_key.get(asset_key, {}) + feed_addr_raw = token_info.get("priceFeed") + if not feed_addr_raw: + continue + feed_addr = Web3.to_checksum_address(feed_addr_raw) + feed_addresses.setdefault(feed_addr, []).append(asset_key) + + if not feed_addresses: + print(" No price feeds found in config.") + return + + calls: list[tuple[str, Any]] = [] + for feed_addr in feed_addresses: + feed_contract = batch_mgr.w3.eth.contract( + address=feed_addr, abi=caller.price_feed_abi, + ) + calls.append((f"{feed_addr}/decimals", feed_contract.functions.decimals())) + calls.append((f"{feed_addr}/latestAnswer", feed_contract.functions.latestAnswer())) + + results, errors = batch_mgr.execute(calls) + + for feed_addr, asset_keys in feed_addresses.items(): + assets_str = ", ".join(asset_keys) + + dec_key = f"{feed_addr}/decimals" + if dec_key in errors: + result.error(f"priceFeed({assets_str})/decimals", expected_decimals, f"call failed: {errors[dec_key]}") + else: + decimals = results.get(dec_key) + _check(result, f"priceFeed({assets_str})/decimals", expected_decimals, decimals) + + ans_key = f"{feed_addr}/latestAnswer" + if ans_key in errors: + result.error(f"priceFeed({assets_str})/latestAnswer", "> 0", f"call failed: {errors[ans_key]}") + else: + answer = results.get(ans_key) + if answer is not None and answer > 0: + result.ok(f"priceFeed({assets_str})/latestAnswer", str(answer)) + else: + result.error(f"priceFeed({assets_str})/latestAnswer", "> 0", str(answer)) + + +def _check( + result: VerificationResult, label: str, expected: Any, actual: Any +) -> None: + # Config JSON stores large numeric values (WAD) as strings; web3.py + # returns them as ints. Coerce so the comparison works. + if isinstance(expected, str): + try: + expected = int(expected) + except ValueError: + pass + if expected == actual: + result.ok(label, str(actual)) + else: + result.error(label, expected, actual) + + +def load_method_selectors(sol_file: str, contract_name: str) -> dict[str, str]: + """Return {function_name_prefix: "0x" + hex_selector} from Forge artifact. + + Keys are the function name (part before the first '('), values are the + 4-byte hex selector prefixed with "0x". When multiple overloads share + a name, all are returned keyed by their full signature. + """ + path = ARTIFACTS_DIR / sol_file / f"{contract_name}.json" + if not path.exists(): + sys.exit(f"Artifact not found: {path}\nRun `forge build` first.") + with open(path) as f: + mids = json.load(f)["methodIdentifiers"] + result: dict[str, str] = {} + for sig, sel_hex in mids.items(): + result[sig] = "0x" + sel_hex + return result + + +def _selectors_for_names( + method_ids: dict[str, str], names: list[str] +) -> list[tuple[str, bytes]]: + """Given methodIdentifiers dict and a list of function name prefixes, + return [(full_signature, 4-byte selector)] for each match.""" + out: list[tuple[str, bytes]] = [] + for name in names: + matches = [ + (sig, bytes.fromhex(sel[2:])) + for sig, sel in method_ids.items() + if sig.split("(")[0] == name + ] + if not matches: + print(f" {RED}WARNING{RESET} selector not found for '{name}'") + out.extend(matches) + return out + + +# --------------------------------------------------------------------------- +# Role constants (mirroring Roles.sol) +# --------------------------------------------------------------------------- + +ROLE_NAMES: dict[int, str] = { + 0: "ACCESS_MANAGER_DEFAULT_ADMIN", + 100: "HUB_DOMAIN_ADMIN_ROLE", + 101: "HUB_CONFIGURATOR_ROLE", + 102: "HUB_FEE_MINTER_ROLE", + 103: "HUB_DEFICIT_ELIMINATOR_ROLE", + 200: "HUB_CONFIGURATOR_DOMAIN_ADMIN_ROLE", + 300: "SPOKE_DOMAIN_ADMIN_ROLE", + 301: "SPOKE_CONFIGURATOR_ROLE", + 302: "SPOKE_USER_POSITION_UPDATER_ROLE", + 400: "SPOKE_CONFIGURATOR_DOMAIN_ADMIN_ROLE", +} + +# role_id -> list of function name prefixes (matched against methodIdentifiers) +HUB_ROLE_FUNCTIONS: dict[int, list[str]] = { + 101: ["addAsset", "updateAssetConfig", "addSpoke", "updateSpokeConfig", "setInterestRateData"], + 102: ["mintFeeShares"], + 103: ["eliminateDeficit"], +} + +HUB_CONFIGURATOR_ROLE_FUNCTIONS: dict[int, list[str]] = { + 200: [ + "addAsset", "addAssetWithDecimals", "updateLiquidityFee", "updateFeeReceiver", + "updateFeeConfig", "updateInterestRateStrategy", "updateReinvestmentController", + "resetAssetCaps", "deactivateAsset", "haltAsset", "addSpoke", "addSpokeToAssets", + "updateSpokeActive", "updateSpokeHalted", "updateSpokeAddCap", "updateSpokeDrawCap", + "updateSpokeRiskPremiumThreshold", "updateSpokeCaps", "deactivateSpoke", "haltSpoke", + "resetSpokeCaps", "updateInterestRateData", + ], +} + +SPOKE_ROLE_FUNCTIONS: dict[int, list[str]] = { + 302: ["updateUserDynamicConfig", "updateUserRiskPremium"], + 301: [ + "updateLiquidationConfig", "addReserve", "updateReserveConfig", + "updateDynamicReserveConfig", "addDynamicReserveConfig", + "updatePositionManager", "updateReservePriceSource", + ], +} + +SPOKE_CONFIGURATOR_ROLE_FUNCTIONS: dict[int, list[str]] = { + 400: [ + "updateReservePriceSource", "updateLiquidationTargetHealthFactor", + "updateHealthFactorForMaxBonus", "updateLiquidationBonusFactor", + "updateLiquidationConfig", "addReserve", "updatePaused", "updateFrozen", + "updateBorrowable", "updateReceiveSharesEnabled", "updateCollateralRisk", + "addCollateralFactor", "updateCollateralFactor", "addMaxLiquidationBonus", + "updateMaxLiquidationBonus", "addLiquidationFee", "updateLiquidationFee", + "addDynamicReserveConfig", "updateDynamicReserveConfig", "pauseAllReserves", + "freezeAllReserves", "pauseReserve", "freezeReserve", "updatePositionManager", + ], +} + + +# --------------------------------------------------------------------------- +# TokenizationSpoke verification +# --------------------------------------------------------------------------- + + +def verify_tokenization_spokes( + batch_mgr: BatchCallManager, + caller: ContractCaller, + cache: DeploymentCache, + report: DeployReport, + config: ConfigInput, + result: VerificationResult, + tokenization_report: dict | None = None, +) -> None: + result.section("TokenizationSpoke Verification") + + if not config.tokens_by_key: + print(f" {YELLOW}SKIPPED{RESET} — no token addresses in config") + return + + # Collect assets that have a tokenize section + tokenize_entries: list[tuple[str, dict, str, int, str]] = [] # (label, tokenize_cfg, hub_addr, asset_id, hub_key) + for entry in config.assets: + tok = entry.get("tokenize") + if not tok or not tok.get("name"): + continue + hub_key = entry["hubKey"] + token_key = entry["tokenKey"] + token_info = config.tokens_by_key.get(token_key) + if not token_info or "address" not in token_info: + continue + hub_info = report.hub_by_label(hub_key) + token_addr = Web3.to_checksum_address(token_info["address"]) + hub_addr = Web3.to_checksum_address(hub_info.address) + asset_id = cache.asset_ids.get((hub_addr, token_addr)) + if asset_id is None: + result.error(f"{hub_key}/{token_key}/tokenize", "valid assetId", "call failed") + continue + label = f"{hub_key}/{token_key}" + tokenize_entries.append((label, tok, hub_addr, asset_id, hub_key)) + + if not tokenize_entries: + print(" No tokenization entries found in config.") + return + + if tokenization_report is not None: + _verify_tokenization_from_report( + batch_mgr, caller, result, tokenize_entries, tokenization_report, + ) + else: + _verify_tokenization_by_discovery( + batch_mgr, caller, result, tokenize_entries, + ) + + +def _verify_tokenization_from_report( + batch_mgr: BatchCallManager, + caller: ContractCaller, + result: VerificationResult, + tokenize_entries: list[tuple[str, dict, str, int, str]], + tokenization_report: dict, +) -> None: + """Verify TokenizationSpokes using known addresses from tokenization deploy report.""" + artifact = ARTIFACT_MAP["TokenizationSpoke"] + + # Resolve addresses and verify bytecode + entry_addrs: list[tuple[str, dict, str, int, str]] = [] # (label, tok, hub_addr, asset_id, spoke_addr) + for label, tok, hub_addr, asset_id, hub_key in tokenize_entries: + token_key = label.split("/", 1)[1] + hub_report = tokenization_report.get(hub_key) + if hub_report is None: + result.error(f"{label}/tokenize", "address in tokenization report", f"hub '{hub_key}' not found") + continue + spoke_addr_raw = hub_report.get(token_key) + if spoke_addr_raw is None: + result.error(f"{label}/tokenize", "address in tokenization report", f"token '{token_key}' not found") + continue + spoke_addr = Web3.to_checksum_address(spoke_addr_raw) + + # Bytecode: proxy + _verify_single_bytecode( + caller, result, f"{label}/tokenize/proxy", + spoke_addr, artifact.sol_file, artifact.contract_name, + ) + # Bytecode: implementation + impl_addr = caller.get_implementation(spoke_addr) + if impl_addr and int(impl_addr, 16) != 0: + _verify_single_bytecode( + caller, result, f"{label}/tokenize/impl", + impl_addr, artifact.impl_sol_file, artifact.impl_contract_name, + ) + else: + result.error(f"{label}/tokenize/impl", "implementation address", "could not read proxy impl slot") + + entry_addrs.append((label, tok, hub_addr, asset_id, spoke_addr)) + + if not entry_addrs: + return + + # Batch name() and symbol() calls + ns_calls: list[tuple[str, Any]] = [] + for label, _, _, _, spoke_addr in entry_addrs: + tok_contract = batch_mgr.w3.eth.contract( + address=spoke_addr, abi=caller.tokenization_spoke_abi, + ) + ns_calls.append((f"{label}/name", tok_contract.functions.name())) + ns_calls.append((f"{label}/symbol", tok_contract.functions.symbol())) + + ns_results, ns_errors = batch_mgr.execute(ns_calls) + + # Batch getSpokeConfig calls + cfg_calls: list[tuple[str, Any]] = [] + for label, _, hub_addr, asset_id, spoke_addr in entry_addrs: + hub_contract = batch_mgr.w3.eth.contract( + address=hub_addr, abi=caller.hub_abi, + ) + cfg_calls.append(( + f"{label}/spokeConfig", + hub_contract.functions.getSpokeConfig(asset_id, spoke_addr), + )) + + cfg_results, cfg_errors = batch_mgr.execute(cfg_calls) + + # Check results + for label, tok, hub_addr, asset_id, spoke_addr in entry_addrs: + expected_name = tok["name"] + expected_symbol = tok["symbol"] + expected_add_cap = tok["addCap"] + + # Name + name_key = f"{label}/name" + if name_key in ns_errors: + result.error(f"{label}/tokenize/name", expected_name, f"call failed: {ns_errors[name_key]}") + else: + _check(result, f"{label}/tokenize/name", expected_name, ns_results.get(name_key)) + + # Symbol + symbol_key = f"{label}/symbol" + if symbol_key in ns_errors: + result.error(f"{label}/tokenize/symbol", expected_symbol, f"call failed: {ns_errors[symbol_key]}") + else: + _check(result, f"{label}/tokenize/symbol", expected_symbol, ns_results.get(symbol_key)) + + # addCap from getSpokeConfig + cfg_key = f"{label}/spokeConfig" + if cfg_key in cfg_errors: + result.error(f"{label}/tokenize/addCap", expected_add_cap, f"call failed: {cfg_errors[cfg_key]}") + else: + spoke_cfg = cfg_results.get(cfg_key) + if spoke_cfg is not None: + _check(result, f"{label}/tokenize/addCap", expected_add_cap, spoke_cfg[0]) + else: + result.error(f"{label}/tokenize/addCap", expected_add_cap, "config call failed") + + +def _verify_tokenization_by_discovery( + batch_mgr: BatchCallManager, + caller: ContractCaller, + result: VerificationResult, + tokenize_entries: list[tuple[str, dict, str, int, str]], +) -> None: + """Discover TokenizationSpokes by probing all spokes registered on Hub.""" + # Round 1: batch getSpokeCount for each (hub, assetId) + count_calls: list[tuple[str, Any]] = [] + for label, _, hub_addr, asset_id, _ in tokenize_entries: + hub_contract = batch_mgr.w3.eth.contract( + address=hub_addr, abi=caller.hub_abi, + ) + count_calls.append((label, hub_contract.functions.getSpokeCount(asset_id))) + + count_results, count_errors = batch_mgr.execute(count_calls) + + # Round 2: batch getSpokeAddress for each index + addr_calls: list[tuple[str, Any]] = [] + addr_meta: list[tuple[str, dict, str, int, int]] = [] # label, tok, hub_addr, asset_id, index + for label, tok, hub_addr, asset_id, _ in tokenize_entries: + if label in count_errors: + result.error(f"{label}/spokeCount", "spoke count", f"call failed: {count_errors[label]}") + continue + count = count_results.get(label) + if count is None: + result.error(f"{label}/spokeCount", "spoke count", "call failed") + continue + hub_contract = batch_mgr.w3.eth.contract( + address=hub_addr, abi=caller.hub_abi, + ) + for idx in range(count): + key = f"{label}/spoke/{idx}" + addr_calls.append((key, hub_contract.functions.getSpokeAddress(asset_id, idx))) + addr_meta.append((label, tok, hub_addr, asset_id, idx)) + + addr_results, addr_errors = batch_mgr.execute(addr_calls) + + # Round 3: Probe each spoke with sequential name()/symbol() calls. + # Batching these is unsafe because name() reverts on non-TokenizationSpoke + # contracts, which poisons the entire batch chunk. + label_spokes: dict[str, list[str]] = {} + spoke_names: dict[str, str] = {} # spoke_addr -> name + spoke_symbols: dict[str, str] = {} # spoke_addr -> symbol + for label, tok, hub_addr, asset_id, idx in addr_meta: + key = f"{label}/spoke/{idx}" + if key in addr_errors: + continue + spoke_addr = addr_results.get(key) + if spoke_addr is None: + continue + spoke_addr = Web3.to_checksum_address(spoke_addr) + label_spokes.setdefault(label, []).append(spoke_addr) + + if spoke_addr not in spoke_names: + name_val = caller.call_tokenization_spoke(spoke_addr, "name", silent=True) + if name_val and isinstance(name_val, str): + spoke_names[spoke_addr] = name_val + symbol_val = caller.call_tokenization_spoke(spoke_addr, "symbol", silent=True) + spoke_symbols[spoke_addr] = symbol_val if isinstance(symbol_val, str) else "" + + # Round 4: Batch getSpokeConfig only for confirmed TokenizationSpoke candidates + config_calls: list[tuple[str, Any]] = [] + for label, tok, hub_addr, asset_id, _ in tokenize_entries: + for spoke_addr in label_spokes.get(label, []): + if spoke_addr in spoke_names: + hub_contract = batch_mgr.w3.eth.contract( + address=hub_addr, abi=caller.hub_abi, + ) + config_calls.append(( + f"{label}/{spoke_addr}/config", + hub_contract.functions.getSpokeConfig(asset_id, spoke_addr), + )) + + config_results, config_errors = batch_mgr.execute(config_calls) + + # Match discovered TokenizationSpokes against config expectations + for label, tok, hub_addr, asset_id, _ in tokenize_entries: + expected_name = tok["name"] + expected_symbol = tok["symbol"] + expected_add_cap = tok["addCap"] + spokes = label_spokes.get(label, []) + + found = False + for spoke_addr in spokes: + name_val = spoke_names.get(spoke_addr) + if not name_val: + continue + symbol_val = spoke_symbols.get(spoke_addr, "") + + if name_val == expected_name and symbol_val == expected_symbol: + found = True + result.ok(f"{label}/tokenize", f'name="{name_val}", symbol="{symbol_val}"') + config_key = f"{label}/{spoke_addr}/config" + if config_key in config_errors: + result.error(f"{label}/tokenize/addCap", expected_add_cap, "config call failed") + else: + spoke_cfg = config_results.get(config_key) + if spoke_cfg is not None: + _check(result, f"{label}/tokenize/addCap", expected_add_cap, spoke_cfg[0]) + else: + result.error(f"{label}/tokenize/addCap", expected_add_cap, "config call failed") + break + + if not found: + result.error( + f"{label}/tokenize", + f'TokenizationSpoke name="{expected_name}" symbol="{expected_symbol}"', + f"not found among {len(spokes)} spokes", + ) + + +# --------------------------------------------------------------------------- +# Role verification +# --------------------------------------------------------------------------- + + +def _build_selector_role_map( + method_ids: dict[str, str], + role_functions: dict[int, list[str]], +) -> list[tuple[str, bytes, int]]: + """Return [(function_signature, 4-byte selector, expected_role_id)].""" + out: list[tuple[str, bytes, int]] = [] + for role_id, fn_names in role_functions.items(): + for sig, sel_bytes in _selectors_for_names(method_ids, fn_names): + out.append((sig, sel_bytes, role_id)) + return out + + +def verify_roles( + batch_mgr: BatchCallManager, + caller: ContractCaller, + report: DeployReport, + config: ConfigInput, + result: VerificationResult, +) -> None: + result.section("Role Configuration") + + am_addr = Web3.to_checksum_address(report.access_manager) + am_contract = batch_mgr.w3.eth.contract( + address=am_addr, abi=caller.access_manager_abi, + ) + + hub_mids = load_method_selectors("IHub.sol", "IHub") + hub_cfg_mids = load_method_selectors("IHubConfigurator.sol", "IHubConfigurator") + spoke_mids = load_method_selectors("ISpoke.sol", "ISpoke") + spoke_cfg_mids = load_method_selectors("ISpokeConfigurator.sol", "ISpokeConfigurator") + + hub_sel_roles = _build_selector_role_map(hub_mids, HUB_ROLE_FUNCTIONS) + hub_cfg_sel_roles = _build_selector_role_map(hub_cfg_mids, HUB_CONFIGURATOR_ROLE_FUNCTIONS) + spoke_sel_roles = _build_selector_role_map(spoke_mids, SPOKE_ROLE_FUNCTIONS) + spoke_cfg_sel_roles = _build_selector_role_map(spoke_cfg_mids, SPOKE_CONFIGURATOR_ROLE_FUNCTIONS) + + # --- Part A: Selector-to-Role Mapping --- + print(f"\n{BOLD}--- Selector-to-Role Mapping ---{RESET}\n") + + role_calls: list[tuple[str, Any]] = [] + role_expectations: list[tuple[str, int]] = [] # (key, expected_role) + + # Hub targets + for hub_info in report.hubs: + target = Web3.to_checksum_address(hub_info.address) + for sig, sel_bytes, expected_role in hub_sel_roles: + fn_name = sig.split("(")[0] + key = f"Hub({hub_info.label})/{fn_name}" + role_calls.append(( + key, + am_contract.functions.getTargetFunctionRole(target, sel_bytes), + )) + role_expectations.append((key, expected_role)) + + # HubConfigurator target + hc_target = Web3.to_checksum_address(report.hub_configurator) + for sig, sel_bytes, expected_role in hub_cfg_sel_roles: + fn_name = sig.split("(")[0] + key = f"HubConfigurator/{fn_name}" + role_calls.append(( + key, + am_contract.functions.getTargetFunctionRole(hc_target, sel_bytes), + )) + role_expectations.append((key, expected_role)) + + # Spoke targets + for spoke_info in report.spokes: + target = Web3.to_checksum_address(spoke_info.proxy) + for sig, sel_bytes, expected_role in spoke_sel_roles: + fn_name = sig.split("(")[0] + key = f"Spoke({spoke_info.label})/{fn_name}" + role_calls.append(( + key, + am_contract.functions.getTargetFunctionRole(target, sel_bytes), + )) + role_expectations.append((key, expected_role)) + + # SpokeConfigurator target + sc_target = Web3.to_checksum_address(report.spoke_configurator) + for sig, sel_bytes, expected_role in spoke_cfg_sel_roles: + fn_name = sig.split("(")[0] + key = f"SpokeConfigurator/{fn_name}" + role_calls.append(( + key, + am_contract.functions.getTargetFunctionRole(sc_target, sel_bytes), + )) + role_expectations.append((key, expected_role)) + + role_results, role_errors = batch_mgr.execute(role_calls) + + for key, expected_role in role_expectations: + if key in role_errors: + result.error(key, f"role {expected_role}", f"call failed: {role_errors[key]}") + else: + actual = role_results.get(key) + role_name = ROLE_NAMES.get(expected_role, str(expected_role)) + if actual == expected_role: + result.ok(key, f"{role_name} ({expected_role})") + else: + actual_name = ROLE_NAMES.get(actual, str(actual)) if actual is not None else "None" + result.error( + key, + f"{role_name} ({expected_role})", + f"{actual_name} ({actual})", + ) + + # --- Part B: Structural Role Grants --- + print(f"\n{BOLD}--- Structural Role Grants ---{RESET}\n") + + grant_calls: list[tuple[str, Any]] = [] + grant_expectations: list[tuple[str, str]] = [] + + # HUB_CONFIGURATOR_ROLE (101) -> HubConfigurator should be member + grant_calls.append(( + "HUB_CONFIGURATOR_ROLE/HubConfigurator", + am_contract.functions.hasRole(101, hc_target), + )) + grant_expectations.append(("HUB_CONFIGURATOR_ROLE/HubConfigurator", "HubConfigurator is member")) + + # SPOKE_CONFIGURATOR_ROLE (301) -> SpokeConfigurator should be member + grant_calls.append(( + "SPOKE_CONFIGURATOR_ROLE/SpokeConfigurator", + am_contract.functions.hasRole(301, sc_target), + )) + grant_expectations.append(("SPOKE_CONFIGURATOR_ROLE/SpokeConfigurator", "SpokeConfigurator is member")) + + grant_results, grant_errors = batch_mgr.execute(grant_calls) + + for key, description in grant_expectations: + if key in grant_errors: + result.error(key, description, f"call failed: {grant_errors[key]}") + else: + val = grant_results.get(key) + # hasRole returns (isMember, executionDelay) + is_member = val[0] if isinstance(val, (tuple, list)) else val + if is_member: + result.ok(key, description) + else: + result.error(key, description, "not a member") + + # --- Part C: Print All Role Members (informational) --- + print(f"\n{BOLD}--- Role Members (informational) ---{RESET}\n") + + # Build address -> label reverse lookup from the deploy report + addr_labels: dict[str, str] = {} + for label, addr in report.all_report_keys(): + addr_labels[Web3.to_checksum_address(addr)] = label + + # Get total role count + role_count = caller.call_access_manager(am_addr, "getRoleCount") + if role_count is None or role_count == 0: + print(" Could not retrieve role count.") + return + + # Get all role IDs + roles = caller.call_access_manager(am_addr, "getRoles", 0, role_count) + if roles is None: + print(" Could not retrieve roles.") + return + + # Batch: get member count for each role + member_count_calls: list[tuple[str, Any]] = [] + for role_id in roles: + key = f"role_{role_id}/memberCount" + member_count_calls.append(( + key, + am_contract.functions.getRoleMemberCount(role_id), + )) + + mc_results, mc_errors = batch_mgr.execute(member_count_calls) + + # Batch: get members for each role + member_calls: list[tuple[str, Any]] = [] + role_member_counts: dict[int, int] = {} + for role_id in roles: + key = f"role_{role_id}/memberCount" + if key in mc_errors: + continue + count = mc_results.get(key, 0) + if count and count > 0: + role_member_counts[role_id] = count + member_calls.append(( + f"role_{role_id}/members", + am_contract.functions.getRoleMembers(role_id, 0, count), + )) + + m_results, m_errors = batch_mgr.execute(member_calls) + + for role_id in roles: + role_name = ROLE_NAMES.get(role_id, f"UNKNOWN_ROLE") + print(f" Role {role_id} ({role_name}):") + mkey = f"role_{role_id}/members" + if mkey in m_errors: + print(f" (error fetching members: {m_errors[mkey]})") + elif mkey in m_results: + members = m_results[mkey] + if members: + for addr in members: + label = addr_labels.get(Web3.to_checksum_address(addr), "") + suffix = f" ({label})" if label else "" + print(f" - {addr}{suffix}") + else: + print(f" (no members)") + else: + print(f" (no members)") + + # --- Part D: Role Labels --- + print(f"\n{BOLD}--- Role Labels ---{RESET}\n") + + # Role 0 (ADMIN_ROLE) cannot be labeled by the AccessManager contract + labelable_roles = {rid: name for rid, name in ROLE_NAMES.items() if rid != 0} + + # Batch 1: check which roles are labeled + labeled_calls: list[tuple[str, Any]] = [] + for role_id in labelable_roles: + key = f"role_{role_id}/isLabeled" + labeled_calls.append(( + key, + am_contract.functions.isRoleLabeled(role_id), + )) + + labeled_results, labeled_errors = batch_mgr.execute(labeled_calls) + + # Batch 2: get labels for roles that are labeled + label_calls: list[tuple[str, Any]] = [] + for role_id in labelable_roles: + key = f"role_{role_id}/isLabeled" + if key in labeled_errors: + continue + if labeled_results.get(key) is True: + label_key = f"role_{role_id}/label" + label_calls.append(( + label_key, + am_contract.functions.getLabelOfRole(role_id), + )) + + label_results, label_errors = batch_mgr.execute(label_calls) + + # Verify + for role_id, expected_label in labelable_roles.items(): + is_labeled_key = f"role_{role_id}/isLabeled" + if is_labeled_key in labeled_errors: + result.error( + f"role {role_id}/label", + expected_label, + f"call failed: {labeled_errors[is_labeled_key]}", + ) + continue + + if labeled_results.get(is_labeled_key) is not True: + result.error(f"role {role_id}/label", expected_label, "role not labeled") + continue + + label_key = f"role_{role_id}/label" + if label_key in label_errors: + result.error( + f"role {role_id}/label", + expected_label, + f"call failed: {label_errors[label_key]}", + ) + else: + actual_label = label_results.get(label_key) + _check(result, f"role {role_id}/label", expected_label, actual_label) + + +def verify_position_managers_and_gateways( + batch_mgr: BatchCallManager, + caller: ContractCaller, + report: DeployReport, + result: VerificationResult, +) -> None: + result.section("Position Manager & Gateway Verification") + + pm_gw_entries: list[tuple[str, str]] = [] + for name, addr in [ + ("GiverPositionManager", report.giver_position_manager), + ("TakerPositionManager", report.taker_position_manager), + ("ConfigPositionManager", report.config_position_manager), + ("SignatureGateway", report.signature_gateway), + ("NativeTokenGateway", report.native_token_gateway), + ]: + if addr: + pm_gw_entries.append((name, Web3.to_checksum_address(addr))) + + if not pm_gw_entries: + print(" No position managers or gateways found in report.") + return + + if not report.spokes: + print(" No spokes found in report.") + return + + # --- Spoke-side: isPositionManagerActive --- + print(f"\n{BOLD}--- Spoke-side (isPositionManagerActive) ---{RESET}\n") + + active_calls: list[tuple[str, Any]] = [] + for spoke_info in report.spokes: + spoke_addr = Web3.to_checksum_address(spoke_info.proxy) + spoke_contract = batch_mgr.w3.eth.contract( + address=spoke_addr, abi=caller.spoke_abi, + ) + for pm_name, pm_addr in pm_gw_entries: + key = f"{spoke_info.label}/{pm_name}" + active_calls.append(( + key, + spoke_contract.functions.isPositionManagerActive(pm_addr), + )) + + active_results, active_errors = batch_mgr.execute(active_calls) + + for spoke_info in report.spokes: + for pm_name, _ in pm_gw_entries: + key = f"{spoke_info.label}/{pm_name}" + if key in active_errors: + result.error(key, "active", f"call failed: {active_errors[key]}") + else: + val = active_results.get(key) + if val is True: + result.ok(key, "active") + else: + result.error(key, "active", f"{val}") + + # --- PM/Gateway-side: isSpokeRegistered --- + print(f"\n{BOLD}--- PM/Gateway-side (isSpokeRegistered) ---{RESET}\n") + + reg_calls: list[tuple[str, Any]] = [] + for pm_name, pm_addr in pm_gw_entries: + pm_contract = batch_mgr.w3.eth.contract( + address=pm_addr, abi=caller.position_manager_base_abi, + ) + for spoke_info in report.spokes: + spoke_addr = Web3.to_checksum_address(spoke_info.proxy) + key = f"{pm_name}/{spoke_info.label}" + reg_calls.append(( + key, + pm_contract.functions.isSpokeRegistered(spoke_addr), + )) + + reg_results, reg_errors = batch_mgr.execute(reg_calls) + + for pm_name, _ in pm_gw_entries: + for spoke_info in report.spokes: + key = f"{pm_name}/{spoke_info.label}" + if key in reg_errors: + result.error(key, "registered", f"call failed: {reg_errors[key]}") + else: + val = reg_results.get(key) + if val is True: + result.ok(key, "registered") + else: + result.error(key, "registered", f"{val}") + + +def verify_etherscan_source( + caller: ContractCaller, + report: DeployReport, + result: VerificationResult, + api_key: str, + chain_id: int = 1, +) -> dict[str, dict]: + """Check that every deployed address is source-verified on Etherscan. + + For proxy contracts the implementation is also checked. Returns a cache + mapping ``address -> etherscan result dict`` for reuse by compiler checks. + """ + result.section("Etherscan Source Verification") + + cache: dict[str, dict] = {} + + def _check_addr(label: str, addr: str) -> None: + addr = Web3.to_checksum_address(addr) + if addr in cache: + src = cache[addr] + else: + src = _etherscan_get_source(addr, api_key, chain_id) + if src is not None: + cache[addr] = src + if src is None: + result.error(label, "Etherscan response", f"no data for {addr}") + elif not src.get("SourceCode"): + result.error(label, "source verified", f"not verified at {addr}") + else: + result.ok(label, addr) + + for name, addr in report.all_addresses(): + _check_addr(name, addr) + + suffix = name.rsplit("/", 1)[-1] + artifact = ARTIFACT_MAP.get(suffix) + if artifact and artifact.impl_sol_file: + impl_addr = caller.get_implementation(addr) + _check_addr(f"{name}/Implementation", impl_addr) + + return cache + + +def verify_etherscan_compiler( + caller: ContractCaller, + report: DeployReport, + result: VerificationResult, + etherscan_cache: dict[str, dict], +) -> None: + """Compare Etherscan-reported compiler settings against expected values.""" + result.section("Compiler Settings Verification (Etherscan)") + + checked: set[str] = set() + + def _check_compiler(label: str, addr: str, contract_name: str) -> None: + addr = Web3.to_checksum_address(addr) + if addr in checked: + return + checked.add(addr) + + src = etherscan_cache.get(addr) + if src is None or not src.get("SourceCode"): + result.error(label, "compiler data", f"no verified source for {addr}") + return + + expected = _expected_compiler(contract_name) + + compiler_ver = src.get("CompilerVersion", "") + if f"v{expected.solc}" in compiler_ver: + result.ok(f"{label}/solc", compiler_ver) + else: + result.error(f"{label}/solc", f"v{expected.solc}", compiler_ver) + + opt_used = src.get("OptimizationUsed", "0") + if opt_used == "1": + result.ok(f"{label}/optimizer", "enabled") + else: + result.error(f"{label}/optimizer", "1 (enabled)", opt_used) + + runs = int(src.get("Runs", "0")) + # Etherscan may truncate very large optimizer_runs to 32-bit + expected_runs = expected.optimizer_runs + expected_runs_truncated = expected_runs & 0xFFFFFFFF + if runs == expected_runs or runs == expected_runs_truncated: + result.ok(f"{label}/runs", str(runs)) + else: + result.error(f"{label}/runs", str(expected_runs), str(runs)) + + evm = src.get("EVMVersion", "").lower() + if evm == "default": + # Etherscan reports "default" when the EVM version matches the + # compiler default; for solc 0.8.28 that is "cancun". + evm = "cancun" + if evm == expected.evm_version: + result.ok(f"{label}/evmVersion", evm) + else: + result.error(f"{label}/evmVersion", expected.evm_version, evm) + + # Extract viaIR from Standard JSON input when available + actual_via_ir = False + source_code = src.get("SourceCode", "") + if source_code.startswith("{{"): + try: + inner = json.loads(source_code[1:-1]) + actual_via_ir = inner.get("settings", {}).get("viaIR", False) + except (json.JSONDecodeError, KeyError): + pass + if actual_via_ir == expected.via_ir: + result.ok(f"{label}/viaIR", str(actual_via_ir)) + else: + result.error(f"{label}/viaIR", str(expected.via_ir), str(actual_via_ir)) + + for name, addr in report.all_addresses(): + suffix = name.rsplit("/", 1)[-1] + artifact = ARTIFACT_MAP.get(suffix) + if artifact is None: + continue + + _check_compiler(name, addr, artifact.contract_name) + + if artifact.impl_sol_file: + impl_addr = caller.get_implementation(addr) + _check_compiler( + f"{name}/Implementation", impl_addr, artifact.impl_contract_name, + ) + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Verify an Aave V4 deployment against its configuration." + ) + parser.add_argument("--rpc-url", required=True, help="RPC endpoint URL") + parser.add_argument( + "--report", required=True, help="Path to deployment report JSON" + ) + parser.add_argument("--config", required=True, help="Path to config input (.xlsx or .json)") + parser.add_argument( + "--tokenization-report", default=None, + help="Path to tokenization deployment report JSON", + ) + parser.add_argument( + "--tokens", default=None, + help="Path to JSON file with token addresses and price feeds", + ) + parser.add_argument( + "--etherscan-api-key", default=None, + help="Etherscan API key for source & compiler verification (mainnet only)", + ) + args = parser.parse_args() + + with open(args.report) as f: + report = DeployReport.from_json(json.load(f)) + + if args.config.endswith(".xlsx"): + config = ConfigInput(load_config_from_xlsx(args.config)) + else: + with open(args.config) as f: + config = ConfigInput(json.load(f)) + + if args.tokens: + with open(args.tokens) as f: + config.tokens_by_key = json.load(f) + + tokenization_report = None + if args.tokenization_report: + with open(args.tokenization_report) as f: + tokenization_report = json.load(f) + + w3 = Web3(Web3.HTTPProvider(args.rpc_url)) + if not w3.is_connected(): + print(f"{RED}ERROR{RESET}: Cannot connect to {args.rpc_url}") + sys.exit(1) + + print(f"Connected to chain {w3.eth.chain_id}") + + caller = ContractCaller(w3) + result = VerificationResult() + batch_mgr = BatchCallManager(w3) + + # Bytecode verification stays sequential (uses get_code/get_storage_at) + verify_bytecode(caller, report, result) + + chain_id = w3.eth.chain_id + if chain_id == 1: + if args.etherscan_api_key: + etherscan_cache = verify_etherscan_source( + caller, report, result, args.etherscan_api_key, chain_id, + ) + verify_etherscan_compiler(caller, report, result, etherscan_cache) + else: + print( + f"\n{BOLD}=== Etherscan Verification ==={RESET}\n" + f" {RED}WARNING{RESET} --etherscan-api-key not provided; " + "skipping source & compiler verification." + ) + + # Pre-fetch shared lookups (asset_ids, reserve_ids) in 2 batch round-trips + cache = prefetch_shared_data(batch_mgr, caller, report, config) + + verify_hub_assets(batch_mgr, caller, cache, report, config, result) + verify_spoke_registrations(batch_mgr, caller, cache, report, config, result) + verify_reserves(batch_mgr, caller, cache, report, config, result) + verify_liquidation_configs(batch_mgr, caller, report, config, result) + verify_oracles(batch_mgr, caller, cache, report, config, result) + verify_oracle_wiring(batch_mgr, caller, report, result) + verify_price_feeds(batch_mgr, caller, report, config, result) + verify_tokenization_spokes(batch_mgr, caller, cache, report, config, result, tokenization_report) + verify_roles(batch_mgr, caller, report, config, result) + # verify_position_managers_and_gateways(batch_mgr, caller, report, result) + + sys.exit(result.summary()) + + +if __name__ == "__main__": + main() diff --git a/snapshots/Hub.Operations.json b/snapshots/Hub.Operations.json index 1774f3f81..4bae269fc 100644 --- a/snapshots/Hub.Operations.json +++ b/snapshots/Hub.Operations.json @@ -1,6 +1,6 @@ { "add": "91610", - "add: with transfer": "112907", + "add: with transfer": "112946", "draw": "109072", "eliminateDeficit: full": "77541", "eliminateDeficit: partial": "87146", @@ -11,8 +11,8 @@ "remove: partial": "85702", "reportDeficit": "116908", "restore: full": "81488", - "restore: full - with transfer": "188892", + "restore: full - with transfer": "188931", "restore: partial": "90198", - "restore: partial - with transfer": "148178", + "restore: partial - with transfer": "148217", "transferShares": "74540" } \ No newline at end of file diff --git a/snapshots/SignatureGateway.Operations.json b/snapshots/SignatureGateway.Operations.json index ada11d6ea..623c91c85 100644 --- a/snapshots/SignatureGateway.Operations.json +++ b/snapshots/SignatureGateway.Operations.json @@ -1,10 +1,10 @@ { - "borrowWithSig": "222132", - "repayWithSig": "192513", + "borrowWithSig": "222144", + "repayWithSig": "192501", "setSelfAsUserPositionManagerWithSig": "75126", "setUsingAsCollateralWithSig": "85380", "supplyWithSig": "155914", "updateUserDynamicConfigWithSig": "63113", "updateUserRiskPremiumWithSig": "61995", - "withdrawWithSig": "135124" + "withdrawWithSig": "135114" } \ No newline at end of file diff --git a/snapshots/TakerPositionManager.Operations.json b/snapshots/TakerPositionManager.Operations.json index 8e6c73cef..fbaf44eb1 100644 --- a/snapshots/TakerPositionManager.Operations.json +++ b/snapshots/TakerPositionManager.Operations.json @@ -1,6 +1,6 @@ { "approveBorrow": "49813", - "approveBorrowWithSig": "65695", + "approveBorrowWithSig": "65683", "approveWithdraw": "49822", "approveWithdrawWithSig": "65649", "borrowOnBehalfOf": "332518", diff --git a/snapshots/TokenizationSpoke.Operations.json b/snapshots/TokenizationSpoke.Operations.json index 888a5d13f..d17ef6888 100644 --- a/snapshots/TokenizationSpoke.Operations.json +++ b/snapshots/TokenizationSpoke.Operations.json @@ -2,13 +2,13 @@ "deposit": "118614", "depositWithSig": "129518", "mint": "118251", - "mintWithSig": "129118", + "mintWithSig": "129130", "permit": "62766", "redeem: on behalf, full": "95212", "redeem: on behalf, partial": "119015", "redeem: self, full": "94282", "redeem: self, partial": "113482", - "redeemWithSig": "128864", + "redeemWithSig": "128852", "withdraw: on behalf, full": "95646", "withdraw: on behalf, partial": "119557", "withdraw: self, full": "94824", diff --git a/src/deployments/README.md b/src/deployments/README.md new file mode 100644 index 000000000..4f2a661a6 --- /dev/null +++ b/src/deployments/README.md @@ -0,0 +1,228 @@ +# Aave V4 Deployment Infrastructure + +Infrastructure for deploying and configuring the Aave V4 hub-and-spoke protocol. + +## Quickstart + +Deploys all contracts and grants roles basic roles. No assets are listed, no spokes are registered, no reserves are configured. + +### 1. Configure `.env` + +Copy `.env.example` and set: + +| Variable | Description | +| --------- | ---------------------------------------------------- | +| `ACCOUNT` | Foundry keystore account name for the deployer | +| `DRY` | Leave blank to broadcast; set to a value to simulate | + +The deploy script constructs a `FullDeployInputs` struct (see `src/deployments/utils/InputUtils.sol`) with admin addresses, hub/spoke labels, CREATE2 salt, and gateway flags. Override `_getDeployInputs()` in your chain-specific script (extends `AaveV4DeployBatchBase.s.sol`) to provide these values. Any zero-address admin fields default to the deployer. + +### 2. Pre-deploy LiquidationLogic (required for spokes) + +```bash +make deploy-precompile CHAIN=mainnet +``` + +This deploys `LiquidationLogic` via CREATE2 and writes `FOUNDRY_LIBRARIES` to `.env` so Foundry can link `SpokeInstance` bytecode on the next compilation. See [LiquidationLogic Pre-deployment](#liquidationlogic-pre-deployment) for details. + +### 3. Deploy Remaining Contracts + +```bash +make deploy-contracts CHAIN=mainnet +``` + +This runs `AaveV4DeployOrchestration.deployAaveV4()`, which deploys batches in order: AccessManager → Configurators → Configurator role setup → TreasurySpoke → Hubs → Spokes → Gateways → PositionManagers → role grants → DEFAULT_ADMIN transfer. + +### TokenizationSpoke + +`TokenizationSpoke` is **not** deployed by the orchestration, because it requires an asset to already be listed on a Hub and Spoke. Each `TokenizationSpoke` instance should be deployed separately after asset listing, one per asset. + +### LiquidationLogic Pre-deployment + +`LiquidationLogic` is an external Solidity library used by `Spoke.sol` (via `SpokeInstance`). Because it has `external` functions, the compiler emits it as a separate contract that `SpokeInstance` calls via `DELEGATECALL` at runtime. When Solidity compiles `SpokeInstance`, it leaves placeholder references (`__$$__`) in the bytecode where the library address should go. You cannot deploy `SpokeInstance` until those placeholders are replaced with a real on-chain address. + +This requires a **two-step deploy** because Foundry needs to re-compile with the library address baked into the bytecode: + +**Step 1 — `LibraryPreCompile.s.sol`** (separate transaction): + +1. `SpokeDeployUtils.deployLiquidationLogic()` deploys it via CREATE2 with `salt=0` +2. Writes `FOUNDRY_LIBRARIES=src/spoke/libraries/LiquidationLogic.sol:LiquidationLogic:0x
` to `.env` via FFI +3. On re-run: if the library is already deployed (has code), skips. If `FOUNDRY_LIBRARIES` exists but the library isn't deployed (wrong chain/fork), deletes the stale entry and asks you to run again + +**Step 2 — Main deploy script** (next invocation): + +1. Foundry reads `.env` at startup, sees `FOUNDRY_LIBRARIES`, and at compile time replaces all `__$$__` placeholders in `SpokeInstance`'s bytecode with the library address +2. `AaveV4SpokeInstanceBatch` deploys `SpokeInstance` with fully linked bytecode + +## Architecture + +``` +scripts/deploy/ + AaveV4DeployBatchBase.s.sol Base: deploy-only run() + +src/deployments/ + batches/ Batch constructors -- deploy related contracts together + AaveV4AuthorityBatch AccessManagerEnumerable + AaveV4ConfiguratorBatch HubConfigurator, SpokeConfigurator + AaveV4TreasurySpokeBatch TreasurySpoke (single instance, proxy + impl) + AaveV4HubInstanceBatch HubInstance (proxy + impl), InterestRateStrategy + AaveV4SpokeInstanceBatch SpokeInstance (proxy + impl), AaveOracle + AaveV4GatewayBatch NativeTokenGateway, SignatureGateway + AaveV4PositionManagerBatch GiverPositionManager, TakerPositionManager, ConfigPositionManager + + orchestration/ High-level orchestrators + AaveV4DeployOrchestration Main entry: deployAaveV4() -- calls batches in order + AaveV4DeployBase Static deploy helpers for each batch + + procedures/ Granular operations + deploy/ Individual contract deploy procedures + roles/ Role setup procedures per component + + libraries/ + BatchReports Report structs for each batch + OrchestrationReports Full deployment report aggregation + ConfigData Parameter structs for config operations + + utils/ + InputUtils FullDeployInputs struct + Roles Role ID constants (0, 100-103, 200, 300-302, 400) + Create2Utils Deterministic deployment helpers + Logger / MetadataLogger Deployment logging and JSON output +``` + +### Roles (Roles.sol) + +Roles are namespaced by contract domain: Hub (100-199), HubConfigurator (200-299), Spoke (300-399), SpokeConfigurator (400-499). See `Roles.sol` NatSpec for the full role strategy and evolution guidelines. + +#### `AccessManager` Role + +| ID | Name | Granted To | Notes | +| --- | ------------------ | ------------------ | ----------------------------------------------------------------- | +| 0 | DEFAULT_ADMIN_ROLE | accessManagerAdmin | OpenZeppelin built-in. Transferred from deployer at end of deploy | + +#### `Hub` Roles + +| ID | Name | Granted To | Functions | +| --- | --------------------------- | ---------------------------------- | ----------------------------------------------------------------------------- | +| 100 | HUB_DOMAIN_ADMIN_ROLE | hubAdmin | (reserved for future use) | +| 101 | HUB_CONFIGURATOR_ROLE | hubAdmin, HubConfigurator contract | addAsset, updateAssetConfig, addSpoke, updateSpokeConfig, setInterestRateData | +| 102 | HUB_FEE_MINTER_ROLE | hubAdmin | mintFeeShares | +| 103 | HUB_DEFICIT_ELIMINATOR_ROLE | hubAdmin | eliminateDeficit | + +#### `HubConfigurator` Roles + +| ID | Name | Granted To | Functions | +| --- | ---------------------------------- | -------------------- | ------------------------------------------------ | +| 200 | HUB_CONFIGURATOR_DOMAIN_ADMIN_ROLE | hubConfiguratorAdmin | All 22 HubConfigurator selectors (see Roles.sol) | + +Domain admin role holds all selectors initially. Granular roles (201+) are carved out as needed. + +#### `Spoke` Roles (on Spoke contract) + +| ID | Name | Granted To | Functions | +| --- | -------------------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 300 | SPOKE_DOMAIN_ADMIN_ROLE | spokeAdmin | (reserved for future use) | +| 301 | SPOKE_USER_POSITION_UPDATER_ROLE | spokeAdmin | updateUserDynamicConfig, updateUserRiskPremium | +| 302 | SPOKE_CONFIGURATOR_ROLE | spokeAdmin, SpokeConfigurator contract | updateLiquidationConfig, addReserve, updateReserveConfig, updateDynamicReserveConfig, addDynamicReserveConfig, updatePositionManager, updateReservePriceSource | + +#### `SpokeConfigurator` Roles (on SpokeConfigurator contract) + +| ID | Name | Granted To | Functions | +| --- | ------------------------------------ | ---------------------- | -------------------------------------------------- | +| 400 | SPOKE_CONFIGURATOR_DOMAIN_ADMIN_ROLE | spokeConfiguratorAdmin | All 24 SpokeConfigurator selectors (see Roles.sol) | + +Domain admin role holds all selectors initially. Granular roles (401+) are carved out as needed. + +## Data Flow + +``` +AaveV4DeployBatchBase.s.sol (Foundry script entry point) + run() + _getDeployInputs() override per chain in extended script + vm.startBroadcast() + _loadWarningsAndSanitizeInputs() validate labels, default zero addrs to deployer + | + +-- AaveV4DeployOrchestration.deployAaveV4() (library — all calls execute as deployer) + | | + | +-- _deriveSalt(deployer, salt) [deployer(160b) | hash(SALT,salt)(96b)] + | | + | +-- _deployAuthorityBatch() + | | AaveV4DeployBase.deployAuthorityBatch() + | | new AaveV4AuthorityBatch(admin, salt) + | | AaveV4AccessManagerEnumerableDeployProcedure._deployAccessManagerEnumerable() + | | Create2Utils.create2Deploy() --> AccessManagerEnumerable + | | + | +-- _deployConfiguratorBatch() + | | AaveV4DeployBase.deployConfiguratorBatch() + | | new AaveV4ConfiguratorBatch(hubAuth, spokeAuth, salt) + | | AaveV4HubConfiguratorDeployProcedure._deployHubConfigurator() + | | Create2Utils.create2Deploy() --> HubConfigurator + | | AaveV4SpokeConfiguratorDeployProcedure._deploySpokeConfigurator() + | | Create2Utils.create2Deploy() --> SpokeConfigurator + | | + | +-- _setupConfiguratorRoles() + | | AaveV4HubConfiguratorRolesProcedure.setupHubConfiguratorAllRoles() + | | AccessManager.setTargetFunctionRole() (selector -> role mappings for HubConfigurator) + | | AaveV4SpokeConfiguratorRolesProcedure.setupSpokeConfiguratorAllRoles() + | | AccessManager.setTargetFunctionRole() (selector -> role mappings for SpokeConfigurator) + | | + | +-- _deployTreasurySpokeBatch() + | | AaveV4DeployBase.deployTreasurySpokeBatch() + | | new AaveV4TreasurySpokeBatch(owner, salt) + | | Create2Utils.create2Deploy() --> TreasurySpoke + | | + | +-- _deployHubs(hubLabels) for each hub label: + | | _deployHub() + | | _deployHubInstanceBatch() + | | AaveV4DeployBase.deployHubInstanceBatch() + | | new AaveV4HubInstanceBatch(proxyAdmin, authority, hubBytecode, salt) + | | Create2Utils.proxify() --> HubInstance (proxy + impl) + | | Create2Utils.create2Deploy() --> InterestRateStrategy + | | _setupHubRoles() + | | AaveV4HubRolesProcedure.setupHubAllRoles() + | | AccessManager.setTargetFunctionRole() (selector -> role mappings for Hub) + | | + | +-- _deploySpokes(spokeLabels) for each spoke label: + | | _deploySpoke() + | | _deploySpokeInstanceBatch() + | | AaveV4DeployBase.deploySpokeInstanceBatch() + | | new AaveV4SpokeInstanceBatch(proxyAdmin, authority, bytecode, ...) + | | new AaveOracle() (non-deterministic, needs setSpoke post-deploy) + | | Create2Utils.proxify() --> SpokeInstance (proxy + impl) + | | _setupSpokeRoles() + | | AaveV4SpokeRolesProcedure.setupSpokeAllRoles() + | | AccessManager.setTargetFunctionRole() (selector -> role mappings for Spoke) + | | + | +-- _deployGatewayBatch() (if deployNativeTokenGateway || deploySignatureGateway) + | | AaveV4DeployBase.deployGatewaysBatch() + | | new AaveV4GatewayBatch(owner, nativeWrapper, flags, salt) + | | Create2Utils.create2Deploy() --> NativeTokenGateway, SignatureGateway + | | + | +-- _deployPositionManagerBatch() (if deployPositionManagers) + | | AaveV4DeployBase.deployPositionManagerBatch() + | | new AaveV4PositionManagerBatch(owner, salt) + | | Create2Utils.create2Deploy() --> GiverPositionManager + | | Create2Utils.create2Deploy() --> TakerPositionManager + | | Create2Utils.create2Deploy() --> ConfigPositionManager + | | + | +-- grantRoles (if grantRoles == true) + | | _grantHubRoles() + | | AaveV4HubRolesProcedure.grantHubAllRoles() hubAdmin gets roles 101-103 + | | AaveV4HubRolesProcedure.grantHubRole() HubConfigurator gets role 101 + | | AaveV4HubConfiguratorRolesProcedure.grantHubConfiguratorAllRoles() + | | hubConfiguratorAdmin gets role 200 + | | _grantSpokeRoles() + | | AaveV4SpokeRolesProcedure.grantSpokeAllRoles() spokeAdmin gets roles 301-302 + | | AaveV4SpokeRolesProcedure.grantSpokeRole() SpokeConfigurator gets role 302 + | | AaveV4SpokeConfiguratorRolesProcedure.grantSpokeConfiguratorAllRoles() + | | spokeConfiguratorAdmin gets role 400 + | | AaveV4AccessManagerRolesProcedure.replaceDefaultAdminRole() + | | grant role 0 to accessManagerAdmin, revoke from deployer + | | + | v + | FullDeploymentReport (all deployed addresses + salt) + | + vm.stopBroadcast() + MetadataLogger.writeJsonReportMarket() write JSON report + logger.save() save logs to output/reports/deployments/ +``` diff --git a/src/deployments/batches/AaveV4AuthorityBatch.sol b/src/deployments/batches/AaveV4AuthorityBatch.sol new file mode 100644 index 000000000..511400118 --- /dev/null +++ b/src/deployments/batches/AaveV4AuthorityBatch.sol @@ -0,0 +1,19 @@ +// 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 AaveV4AuthorityBatch is AaveV4AccessManagerEnumerableDeployProcedure { + BatchReports.AuthorityBatchReport internal _report; + + constructor(address admin_, bytes32 salt_) { + address accessManager = _deployAccessManagerEnumerable(admin_, salt_); + _report = BatchReports.AuthorityBatchReport({accessManager: accessManager}); + } + + function getReport() external view returns (BatchReports.AuthorityBatchReport memory) { + return _report; + } +} diff --git a/src/deployments/batches/AaveV4ConfiguratorBatch.sol b/src/deployments/batches/AaveV4ConfiguratorBatch.sol new file mode 100644 index 000000000..f34c142a3 --- /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 hubConfiguratorAuthority_, + address spokeConfiguratorAuthority_, + bytes32 salt_ + ) { + address hubConfigurator = _deployHubConfigurator({ + authority: hubConfiguratorAuthority_, + salt: salt_ + }); + address spokeConfigurator = _deploySpokeConfigurator({ + authority: spokeConfiguratorAuthority_, + salt: salt_ + }); + + _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..f9bf583cf --- /dev/null +++ b/src/deployments/batches/AaveV4GatewayBatch.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 {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_, + bool deployNativeTokenGateway_, + bool deploySignatureGateway_, + bytes32 salt_ + ) { + address nativeGateway; + address signatureGateway; + + if (deployNativeTokenGateway_) { + nativeGateway = _deployNativeTokenGateway({ + nativeWrapper: nativeWrapper_, + owner: owner_, + salt: salt_ + }); + } + if (deploySignatureGateway_) { + signatureGateway = _deploySignatureGateway({owner: owner_, salt: salt_}); + } + + _report = BatchReports.GatewaysBatchReport({ + signatureGateway: signatureGateway, + nativeGateway: nativeGateway + }); + } + + function getReport() external view returns (BatchReports.GatewaysBatchReport memory) { + return _report; + } +} diff --git a/src/deployments/batches/AaveV4HubInstanceBatch.sol b/src/deployments/batches/AaveV4HubInstanceBatch.sol new file mode 100644 index 000000000..b291a63a8 --- /dev/null +++ b/src/deployments/batches/AaveV4HubInstanceBatch.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 {AaveV4HubDeployProcedure} from 'src/deployments/procedures/deploy/hub/AaveV4HubDeployProcedure.sol'; +import {AaveV4InterestRateStrategyDeployProcedure} from 'src/deployments/procedures/deploy/hub/AaveV4InterestRateStrategyDeployProcedure.sol'; + +contract AaveV4HubInstanceBatch is + AaveV4HubDeployProcedure, + AaveV4InterestRateStrategyDeployProcedure +{ + BatchReports.HubInstanceBatchReport internal _report; + + constructor( + address hubProxyAdminOwner_, + address authority_, + bytes memory hubBytecode_, + bytes32 salt_ + ) { + (address hubProxy, address hubImplementation) = _deployUpgradeableHubInstance({ + hubProxyAdminOwner: hubProxyAdminOwner_, + authority: authority_, + hubBytecode: hubBytecode_, + salt: salt_ + }); + address irStrategy = _deployInterestRateStrategy({hub: hubProxy, salt: salt_}); + + _report = BatchReports.HubInstanceBatchReport({ + hubImplementation: hubImplementation, + hubProxy: hubProxy, + irStrategy: irStrategy + }); + } + + function getReport() external view returns (BatchReports.HubInstanceBatchReport memory) { + return _report; + } +} diff --git a/src/deployments/batches/AaveV4PositionManagerBatch.sol b/src/deployments/batches/AaveV4PositionManagerBatch.sol new file mode 100644 index 000000000..c5070c1c0 --- /dev/null +++ b/src/deployments/batches/AaveV4PositionManagerBatch.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; +import {AaveV4GiverPositionManagerDeployProcedure} from 'src/deployments/procedures/deploy/position-manager/AaveV4GiverPositionManagerDeployProcedure.sol'; +import {AaveV4TakerPositionManagerDeployProcedure} from 'src/deployments/procedures/deploy/position-manager/AaveV4TakerPositionManagerDeployProcedure.sol'; +import {AaveV4ConfigPositionManagerDeployProcedure} from 'src/deployments/procedures/deploy/position-manager/AaveV4ConfigPositionManagerDeployProcedure.sol'; + +contract AaveV4PositionManagerBatch is + AaveV4GiverPositionManagerDeployProcedure, + AaveV4TakerPositionManagerDeployProcedure, + AaveV4ConfigPositionManagerDeployProcedure +{ + BatchReports.PositionManagerBatchReport internal _report; + + constructor(address owner_, bytes32 salt_) { + _report = BatchReports.PositionManagerBatchReport({ + giverPositionManager: _deployGiverPositionManager({owner: owner_, salt: salt_}), + takerPositionManager: _deployTakerPositionManager({owner: owner_, salt: salt_}), + configPositionManager: _deployConfigPositionManager({owner: owner_, salt: salt_}) + }); + } + + function getReport() external view returns (BatchReports.PositionManagerBatchReport memory) { + return _report; + } +} diff --git a/src/deployments/batches/AaveV4SpokeInstanceBatch.sol b/src/deployments/batches/AaveV4SpokeInstanceBatch.sol new file mode 100644 index 000000000..a56321f54 --- /dev/null +++ b/src/deployments/batches/AaveV4SpokeInstanceBatch.sol @@ -0,0 +1,46 @@ +// 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 {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 authority_, + bytes memory spokeBytecode_, + uint8 oracleDecimals_, + uint16 maxUserReservesLimit_, + bytes32 salt_ + ) { + address aaveOracle = _deployAaveOracle(oracleDecimals_); + (address spokeProxy, address spokeImplementation) = _deployUpgradeableSpokeInstance({ + spokeProxyAdminOwner: spokeProxyAdminOwner_, + authority: authority_, + oracle: aaveOracle, + spokeBytecode: spokeBytecode_, + salt: salt_, + maxUserReservesLimit: maxUserReservesLimit_ + }); + IAaveOracle(aaveOracle).setSpoke(spokeProxy); + + require(ISpoke(spokeProxy).ORACLE() == aaveOracle, 'spoke oracle mismatch'); + require(IAaveOracle(aaveOracle).spoke() == spokeProxy, 'oracle spoke mismatch'); + + _report = BatchReports.SpokeInstanceBatchReport({ + aaveOracle: aaveOracle, + spokeImplementation: spokeImplementation, + spokeProxy: spokeProxy + }); + } + + function getReport() external view returns (BatchReports.SpokeInstanceBatchReport memory) { + return _report; + } +} diff --git a/src/deployments/batches/AaveV4TokenizationSpokeBatch.sol b/src/deployments/batches/AaveV4TokenizationSpokeBatch.sol new file mode 100644 index 000000000..0ba17f25a --- /dev/null +++ b/src/deployments/batches/AaveV4TokenizationSpokeBatch.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; +import {AaveV4TokenizationSpokeDeployProcedure} from 'src/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeDeployProcedure.sol'; +import {ITokenizationSpoke} from 'src/spoke/interfaces/ITokenizationSpoke.sol'; + +contract AaveV4TokenizationSpokeBatch is AaveV4TokenizationSpokeDeployProcedure { + BatchReports.TokenizationSpokeBatchReport internal _report; + + constructor( + address hub_, + address underlying_, + address spokeProxyAdminOwner_, + string memory shareName_, + string memory shareSymbol_, + bytes32 salt_ + ) { + ( + address tokenizationSpokeProxy, + address tokenizationSpokeImplementation + ) = _deployUpgradeableTokenizationSpokeInstance({ + hub: hub_, + underlying: underlying_, + spokeProxyAdminOwner: spokeProxyAdminOwner_, + shareName: shareName_, + shareSymbol: shareSymbol_, + salt: salt_ + }); + + _report = BatchReports.TokenizationSpokeBatchReport({ + tokenizationSpokeImplementation: tokenizationSpokeImplementation, + tokenizationSpokeProxy: tokenizationSpokeProxy + }); + } + + function getReport() external view returns (BatchReports.TokenizationSpokeBatchReport memory) { + return _report; + } +} diff --git a/src/deployments/batches/AaveV4TreasurySpokeBatch.sol b/src/deployments/batches/AaveV4TreasurySpokeBatch.sol new file mode 100644 index 000000000..045efde92 --- /dev/null +++ b/src/deployments/batches/AaveV4TreasurySpokeBatch.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; +import {AaveV4TreasurySpokeDeployProcedure} from 'src/deployments/procedures/deploy/spoke/AaveV4TreasurySpokeDeployProcedure.sol'; + +contract AaveV4TreasurySpokeBatch is AaveV4TreasurySpokeDeployProcedure { + BatchReports.TreasurySpokeBatchReport internal _report; + + constructor(address owner_, bytes32 salt_) { + address treasurySpoke = _deployTreasurySpoke({owner: owner_, salt: salt_}); + _report = BatchReports.TreasurySpokeBatchReport(treasurySpoke); + } + + function getReport() external view returns (BatchReports.TreasurySpokeBatchReport memory) { + return _report; + } +} diff --git a/src/deployments/libraries/BatchReports.sol b/src/deployments/libraries/BatchReports.sol new file mode 100644 index 000000000..4b65ae8e8 --- /dev/null +++ b/src/deployments/libraries/BatchReports.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +library BatchReports { + struct AuthorityBatchReport { + address accessManager; + } + + struct ConfiguratorBatchReport { + address hubConfigurator; + address spokeConfigurator; + } + + struct SpokeInstanceBatchReport { + address spokeImplementation; + address spokeProxy; + address aaveOracle; + } + + struct HubInstanceBatchReport { + address hubImplementation; + address hubProxy; + address irStrategy; + } + + struct TreasurySpokeBatchReport { + address treasurySpoke; + } + + struct GatewaysBatchReport { + address signatureGateway; + address nativeGateway; + } + + struct PositionManagerBatchReport { + address giverPositionManager; + address takerPositionManager; + address configPositionManager; + } + + struct TokenizationSpokeBatchReport { + address tokenizationSpokeImplementation; + address tokenizationSpokeProxy; + } +} 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..751559051 --- /dev/null +++ b/src/deployments/libraries/OrchestrationReports.sol @@ -0,0 +1,28 @@ +// 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.HubInstanceBatchReport report; + } + + struct FullDeploymentReport { + BatchReports.AuthorityBatchReport authorityBatchReport; + BatchReports.ConfiguratorBatchReport configuratorBatchReport; + BatchReports.TreasurySpokeBatchReport treasurySpokeBatchReport; + SpokeDeploymentReport[] spokeInstanceBatchReports; + HubDeploymentReport[] hubInstanceBatchReports; + BatchReports.GatewaysBatchReport gatewaysBatchReport; + BatchReports.PositionManagerBatchReport positionManagerBatchReport; + bytes32 salt; + } +} diff --git a/src/deployments/orchestration/AaveV4DeployBase.sol b/src/deployments/orchestration/AaveV4DeployBase.sol new file mode 100644 index 000000000..5c3582f4e --- /dev/null +++ b/src/deployments/orchestration/AaveV4DeployBase.sol @@ -0,0 +1,129 @@ +// 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 {AaveV4AuthorityBatch} from 'src/deployments/batches/AaveV4AuthorityBatch.sol'; +import {AaveV4ConfiguratorBatch} from 'src/deployments/batches/AaveV4ConfiguratorBatch.sol'; +import {AaveV4GatewayBatch} from 'src/deployments/batches/AaveV4GatewayBatch.sol'; +import {AaveV4HubInstanceBatch} from 'src/deployments/batches/AaveV4HubInstanceBatch.sol'; +import {AaveV4PositionManagerBatch} from 'src/deployments/batches/AaveV4PositionManagerBatch.sol'; +import {AaveV4SpokeInstanceBatch} from 'src/deployments/batches/AaveV4SpokeInstanceBatch.sol'; +import {AaveV4TokenizationSpokeBatch} from 'src/deployments/batches/AaveV4TokenizationSpokeBatch.sol'; +import {AaveV4TreasurySpokeBatch} from 'src/deployments/batches/AaveV4TreasurySpokeBatch.sol'; + +library AaveV4DeployBase { + function deployAuthorityBatch( + address admin, + bytes32 salt + ) internal returns (BatchReports.AuthorityBatchReport memory) { + AaveV4AuthorityBatch authorityBatch = new AaveV4AuthorityBatch({admin_: admin, salt_: salt}); + return authorityBatch.getReport(); + } + + function deployConfiguratorBatch( + address hubConfiguratorAuthority, + address spokeConfiguratorAuthority, + bytes32 salt + ) internal returns (BatchReports.ConfiguratorBatchReport memory) { + AaveV4ConfiguratorBatch configuratorBatch = new AaveV4ConfiguratorBatch({ + hubConfiguratorAuthority_: hubConfiguratorAuthority, + spokeConfiguratorAuthority_: spokeConfiguratorAuthority, + salt_: salt + }); + return configuratorBatch.getReport(); + } + + function deployTreasurySpokeBatch( + address owner, + bytes32 salt + ) internal returns (BatchReports.TreasurySpokeBatchReport memory) { + AaveV4TreasurySpokeBatch treasurySpokeBatch = new AaveV4TreasurySpokeBatch({ + owner_: owner, + salt_: salt + }); + return treasurySpokeBatch.getReport(); + } + + function deployHubInstanceBatch( + address hubProxyAdminOwner, + address authority, + bytes memory hubBytecode, + bytes32 salt + ) internal returns (BatchReports.HubInstanceBatchReport memory) { + AaveV4HubInstanceBatch hubInstanceBatch = new AaveV4HubInstanceBatch({ + hubProxyAdminOwner_: hubProxyAdminOwner, + authority_: authority, + hubBytecode_: hubBytecode, + salt_: salt + }); + return hubInstanceBatch.getReport(); + } + + function deploySpokeInstanceBatch( + address spokeProxyAdminOwner, + address authority, + bytes memory spokeBytecode, + uint8 oracleDecimals, + uint16 maxUserReservesLimit, + bytes32 salt + ) internal returns (BatchReports.SpokeInstanceBatchReport memory) { + AaveV4SpokeInstanceBatch spokeInstanceBatch = new AaveV4SpokeInstanceBatch({ + spokeProxyAdminOwner_: spokeProxyAdminOwner, + authority_: authority, + spokeBytecode_: spokeBytecode, + oracleDecimals_: oracleDecimals, + maxUserReservesLimit_: maxUserReservesLimit, + salt_: salt + }); + return spokeInstanceBatch.getReport(); + } + + function deployPositionManagerBatch( + address owner, + bytes32 salt + ) internal returns (BatchReports.PositionManagerBatchReport memory) { + AaveV4PositionManagerBatch positionManagerBatch = new AaveV4PositionManagerBatch({ + owner_: owner, + salt_: salt + }); + return positionManagerBatch.getReport(); + } + + function deployGatewaysBatch( + address owner, + address nativeWrapper, + bool deployNativeTokenGateway, + bool deploySignatureGateway, + bytes32 salt + ) internal returns (BatchReports.GatewaysBatchReport memory) { + AaveV4GatewayBatch gatewayBatch = new AaveV4GatewayBatch({ + owner_: owner, + nativeWrapper_: nativeWrapper, + deployNativeTokenGateway_: deployNativeTokenGateway, + deploySignatureGateway_: deploySignatureGateway, + salt_: salt + }); + return gatewayBatch.getReport(); + } + + function deployTokenizationSpokeBatch( + address hub, + address underlying, + address spokeProxyAdminOwner, + string memory shareName, + string memory shareSymbol, + bytes32 salt + ) internal returns (BatchReports.TokenizationSpokeBatchReport memory) { + AaveV4TokenizationSpokeBatch tokenizationSpokeBatch = new AaveV4TokenizationSpokeBatch({ + hub_: hub, + underlying_: underlying, + spokeProxyAdminOwner_: spokeProxyAdminOwner, + shareName_: shareName, + shareSymbol_: shareSymbol, + salt_: salt + }); + return tokenizationSpokeBatch.getReport(); + } +} diff --git a/src/deployments/orchestration/AaveV4DeployOrchestration.sol b/src/deployments/orchestration/AaveV4DeployOrchestration.sol new file mode 100644 index 000000000..398ca6017 --- /dev/null +++ b/src/deployments/orchestration/AaveV4DeployOrchestration.sol @@ -0,0 +1,499 @@ +// 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 {Roles} from 'src/deployments/utils/libraries/Roles.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 {AaveV4HubConfiguratorRolesProcedure} from 'src/deployments/procedures/roles/AaveV4HubConfiguratorRolesProcedure.sol'; +import {AaveV4SpokeConfiguratorRolesProcedure} from 'src/deployments/procedures/roles/AaveV4SpokeConfiguratorRolesProcedure.sol'; +import {InputUtils} from 'src/deployments/utils/InputUtils.sol'; +import {Logger} from 'src/deployments/utils/Logger.sol'; +import {DeployConstants} from 'src/deployments/utils/libraries/DeployConstants.sol'; + +library AaveV4DeployOrchestration { + bytes32 public constant SALT = keccak256('AAVE_V4'); + + function deployAaveV4( + Logger logger, + address deployer, + InputUtils.FullDeployInputs memory deployInputs, + bytes memory hubBytecode, + bytes memory spokeBytecode + ) internal returns (OrchestrationReports.FullDeploymentReport memory report) { + bytes32 salt = _deriveSalt({deployer: deployer, salt: deployInputs.salt}); + report.salt = deployInputs.salt; + + // Deploy Access Batch + // initialize with deployer as access manager admin + address initialAdmin = deployer; + report.authorityBatchReport = _deployAuthorityBatch({ + logger: logger, + accessManagerAdmin: initialAdmin, + salt: salt + }); + + address accessManager = report.authorityBatchReport.accessManager; + + // Deploy Configurator Batch with AccessManager as authority + report.configuratorBatchReport = _deployConfiguratorBatch({ + logger: logger, + hubConfiguratorAuthority: accessManager, + spokeConfiguratorAuthority: accessManager, + salt: salt + }); + + // Setup Configurator Roles + _setupConfiguratorRoles({logger: logger, report: report}); + + // Deploy TreasurySpoke Batch (single instance for all hubs) + report.treasurySpokeBatchReport = _deployTreasurySpokeBatch({ + logger: logger, + treasurySpokeOwner: deployInputs.treasurySpokeOwner, + salt: salt + }); + + // Deploy Hub Batches + report.hubInstanceBatchReports = _deployHubs({ + logger: logger, + hubProxyAdminOwner: deployInputs.hubProxyAdminOwner, + authority: accessManager, + hubLabels: deployInputs.hubLabels, + hubBytecode: hubBytecode, + salt: salt + }); + + // Deploy Spoke Instance Batches + report.spokeInstanceBatchReports = _deploySpokes({ + logger: logger, + authority: accessManager, + inputs: deployInputs, + spokeBytecode: spokeBytecode, + salt: salt + }); + + // Deploy Gateways Batch if either gateway flag is enabled + if (deployInputs.deployNativeTokenGateway || deployInputs.deploySignatureGateway) { + report.gatewaysBatchReport = _deployGatewayBatch({ + logger: logger, + gatewayOwner: deployInputs.gatewayOwner, + nativeWrapper: deployInputs.nativeWrapper, + deployNativeTokenGateway: deployInputs.deployNativeTokenGateway, + deploySignatureGateway: deployInputs.deploySignatureGateway, + salt: salt + }); + } + + // Deploy Position Managers Batch if flag is enabled + if (deployInputs.deployPositionManagers) { + report.positionManagerBatchReport = _deployPositionManagerBatch({ + logger: logger, + positionManagerOwner: deployInputs.positionManagerOwner, + salt: salt + }); + } + + // Set Roles if needed + if (deployInputs.grantRoles) { + if (deployInputs.hubLabels.length > 0) { + _grantHubRoles({ + logger: logger, + report: report, + hubAdmin: deployInputs.hubAdmin, + hubConfiguratorAdmin: deployInputs.hubConfiguratorAdmin + }); + } + if (deployInputs.spokeLabels.length > 0) { + _grantSpokeRoles({ + logger: logger, + report: report, + spokeAdmin: deployInputs.spokeAdmin, + spokeConfiguratorAdmin: deployInputs.spokeConfiguratorAdmin + }); + } + + if (deployInputs.accessManagerAdmin != initialAdmin) { + logger.logHeader1( + 'granting AccessManager Root Admin role to', + deployInputs.accessManagerAdmin + ); + AaveV4AccessManagerRolesProcedure.replaceDefaultAdminRole({ + accessManager: accessManager, + adminToAdd: deployInputs.accessManagerAdmin, + adminToRemove: initialAdmin + }); + } + } + + return report; + } + + function _deployHubs( + Logger logger, + address hubProxyAdminOwner, + address authority, + string[] memory hubLabels, + bytes memory hubBytecode, + bytes32 salt + ) internal returns (OrchestrationReports.HubDeploymentReport[] memory hubInstanceBatchReports) { + uint256 hubCount = hubLabels.length; + hubInstanceBatchReports = new OrchestrationReports.HubDeploymentReport[](hubCount); + for (uint256 i; i < hubCount; ++i) { + bytes32 childSalt = _deriveChildSalt(salt, 'hub', hubLabels[i]); + hubInstanceBatchReports[i] = _deployHub({ + logger: logger, + hubProxyAdminOwner: hubProxyAdminOwner, + authority: authority, + label: hubLabels[i], + hubBytecode: hubBytecode, + salt: childSalt + }); + } + logger.logNewLine(); + return hubInstanceBatchReports; + } + + function _deployHub( + Logger logger, + address hubProxyAdminOwner, + address authority, + string memory label, + bytes memory hubBytecode, + bytes32 salt + ) internal returns (OrchestrationReports.HubDeploymentReport memory) { + OrchestrationReports.HubDeploymentReport memory hubReport; + hubReport.label = label; + hubReport.report = _deployHubInstanceBatch({ + logger: logger, + hubProxyAdminOwner: hubProxyAdminOwner, + authority: authority, + hubBytecode: hubBytecode, + salt: salt + }); + + _logHubReport({logger: logger, report: hubReport.report, label: label}); + _setupHubRoles({logger: logger, report: hubReport.report, accessManager: authority}); + + return hubReport; + } + + function _deploySpokes( + Logger logger, + address authority, + InputUtils.FullDeployInputs memory inputs, + bytes memory spokeBytecode, + bytes32 salt + ) internal returns (OrchestrationReports.SpokeDeploymentReport[] memory spokeBatchReports) { + uint256 spokeCount = inputs.spokeLabels.length; + uint256 limitsLen = inputs.spokeMaxReservesLimits.length; + require(limitsLen == spokeCount || limitsLen == 0, 'spoke labels/limits length mismatch'); + spokeBatchReports = new OrchestrationReports.SpokeDeploymentReport[](spokeCount); + for (uint256 i; i < spokeCount; ++i) { + bytes32 childSalt = _deriveChildSalt(salt, 'spoke', inputs.spokeLabels[i]); + spokeBatchReports[i] = _deploySpoke({ + logger: logger, + spokeProxyAdminOwner: inputs.spokeProxyAdminOwner, + authority: authority, + label: inputs.spokeLabels[i], + spokeBytecode: spokeBytecode, + maxUserReservesLimit: limitsLen > 0 + ? inputs.spokeMaxReservesLimits[i] + : DeployConstants.MAX_ALLOWED_USER_RESERVES_LIMIT, + oracleDecimals: DeployConstants.ORACLE_DECIMALS, + salt: childSalt + }); + } + logger.logNewLine(); + return spokeBatchReports; + } + + function _deploySpoke( + Logger logger, + address spokeProxyAdminOwner, + address authority, + string memory label, + bytes memory spokeBytecode, + uint16 maxUserReservesLimit, + uint8 oracleDecimals, + bytes32 salt + ) internal returns (OrchestrationReports.SpokeDeploymentReport memory) { + OrchestrationReports.SpokeDeploymentReport memory spokeReport; + + spokeReport.label = label; + spokeReport.report = _deploySpokeInstanceBatch({ + logger: logger, + spokeProxyAdminOwner: spokeProxyAdminOwner, + authority: authority, + spokeBytecode: spokeBytecode, + oracleDecimals: oracleDecimals, + maxUserReservesLimit: maxUserReservesLimit, + salt: salt + }); + _logSpokeReport({logger: logger, report: spokeReport.report, label: label}); + _setupSpokeRoles({logger: logger, report: spokeReport.report, accessManager: authority}); + + return spokeReport; + } + + function _deployHubInstanceBatch( + Logger logger, + address hubProxyAdminOwner, + address authority, + bytes memory hubBytecode, + bytes32 salt + ) internal returns (BatchReports.HubInstanceBatchReport memory report) { + logger.logHeader1('deploying HubBatch'); + report = AaveV4DeployBase.deployHubInstanceBatch({ + hubProxyAdminOwner: hubProxyAdminOwner, + authority: authority, + hubBytecode: hubBytecode, + salt: salt + }); + return report; + } + + function _deployAuthorityBatch( + Logger logger, + address accessManagerAdmin, + bytes32 salt + ) internal returns (BatchReports.AuthorityBatchReport memory report) { + logger.logHeader1('deploying AuthorityBatch'); + + report = AaveV4DeployBase.deployAuthorityBatch({admin: accessManagerAdmin, salt: salt}); + + logger.log('AccessManager', report.accessManager); + logger.logNewLine(); + return report; + } + + function _deployConfiguratorBatch( + Logger logger, + address hubConfiguratorAuthority, + address spokeConfiguratorAuthority, + bytes32 salt + ) internal returns (BatchReports.ConfiguratorBatchReport memory report) { + logger.logHeader1('deploying ConfiguratorBatch'); + + report = AaveV4DeployBase.deployConfiguratorBatch({ + hubConfiguratorAuthority: hubConfiguratorAuthority, + spokeConfiguratorAuthority: spokeConfiguratorAuthority, + salt: salt + }); + + logger.log('HubConfigurator', report.hubConfigurator); + logger.log('SpokeConfigurator', report.spokeConfigurator); + logger.logNewLine(); + return report; + } + + function _deploySpokeInstanceBatch( + Logger logger, + address spokeProxyAdminOwner, + address authority, + bytes memory spokeBytecode, + uint8 oracleDecimals, + uint16 maxUserReservesLimit, + bytes32 salt + ) internal returns (BatchReports.SpokeInstanceBatchReport memory report) { + logger.logHeader1('deploying AaveV4SpokeInstanceBatch'); + report = AaveV4DeployBase.deploySpokeInstanceBatch({ + spokeProxyAdminOwner: spokeProxyAdminOwner, + authority: authority, + spokeBytecode: spokeBytecode, + oracleDecimals: oracleDecimals, + maxUserReservesLimit: maxUserReservesLimit, + salt: salt + }); + return report; + } + + function _deployTreasurySpokeBatch( + Logger logger, + address treasurySpokeOwner, + bytes32 salt + ) internal returns (BatchReports.TreasurySpokeBatchReport memory report) { + logger.logHeader1('deploying TreasurySpokeBatch'); + report = AaveV4DeployBase.deployTreasurySpokeBatch({owner: treasurySpokeOwner, salt: salt}); + logger.log('TreasurySpoke', report.treasurySpoke); + logger.logNewLine(); + return report; + } + + function _deployGatewayBatch( + Logger logger, + address gatewayOwner, + address nativeWrapper, + bool deployNativeTokenGateway, + bool deploySignatureGateway, + bytes32 salt + ) internal returns (BatchReports.GatewaysBatchReport memory report) { + logger.logHeader1('deploying GatewayBatch'); + report = AaveV4DeployBase.deployGatewaysBatch({ + owner: gatewayOwner, + nativeWrapper: nativeWrapper, + deployNativeTokenGateway: deployNativeTokenGateway, + deploySignatureGateway: deploySignatureGateway, + salt: salt + }); + if (deployNativeTokenGateway) { + logger.log('NativeTokenGateway', report.nativeGateway); + } + if (deploySignatureGateway) { + logger.log('SignatureGateway', report.signatureGateway); + } + return report; + } + + function _deployPositionManagerBatch( + Logger logger, + address positionManagerOwner, + bytes32 salt + ) internal returns (BatchReports.PositionManagerBatchReport memory report) { + logger.logHeader1('deploying PositionManagerBatch'); + report = AaveV4DeployBase.deployPositionManagerBatch({owner: positionManagerOwner, salt: salt}); + logger.logDetail('GiverPositionManager', report.giverPositionManager); + logger.logDetail('TakerPositionManager', report.takerPositionManager); + logger.logDetail('ConfigPositionManager', report.configPositionManager); + return report; + } + + /// @dev Setup roles for the hub and spoke configurators + function _setupConfiguratorRoles( + Logger logger, + OrchestrationReports.FullDeploymentReport memory report + ) internal { + logger.logHeader1('setting HubConfigurator roles'); + AaveV4HubConfiguratorRolesProcedure.setupHubConfiguratorAllRoles({ + accessManager: report.authorityBatchReport.accessManager, + hubConfigurator: report.configuratorBatchReport.hubConfigurator + }); + + logger.logHeader1('setting SpokeConfigurator roles'); + AaveV4SpokeConfiguratorRolesProcedure.setupSpokeConfiguratorAllRoles({ + accessManager: report.authorityBatchReport.accessManager, + spokeConfigurator: report.configuratorBatchReport.spokeConfigurator + }); + } + + function _setupSpokeRoles( + Logger logger, + BatchReports.SpokeInstanceBatchReport memory report, + address accessManager + ) internal { + logger.logHeader1('setting Spoke roles'); + AaveV4SpokeRolesProcedure.setupSpokeAllRoles({ + accessManager: accessManager, + spoke: report.spokeProxy + }); + } + + function _setupHubRoles( + Logger logger, + BatchReports.HubInstanceBatchReport memory report, + address accessManager + ) internal { + logger.logHeader1('setting Hub roles'); + AaveV4HubRolesProcedure.setupHubAllRoles({accessManager: accessManager, hub: report.hubProxy}); + } + + function _grantHubRoles( + Logger logger, + OrchestrationReports.FullDeploymentReport memory report, + address hubAdmin, + address hubConfiguratorAdmin + ) internal { + address accessManager = report.authorityBatchReport.accessManager; + + logger.logHeader1('granting Hub Admin role to', hubAdmin); + AaveV4HubRolesProcedure.grantHubAllRoles({accessManager: accessManager, admin: hubAdmin}); + + logger.logHeader1( + 'granting Hub Configurator roles to', + report.configuratorBatchReport.hubConfigurator + ); + AaveV4HubRolesProcedure.grantHubRole({ + accessManager: accessManager, + role: Roles.HUB_CONFIGURATOR_ROLE, + admin: report.configuratorBatchReport.hubConfigurator + }); + + logger.logHeader1('granting HubConfigurator Admin roles to', hubConfiguratorAdmin); + AaveV4HubConfiguratorRolesProcedure.grantHubConfiguratorAllRoles({ + accessManager: accessManager, + admin: hubConfiguratorAdmin + }); + } + + function _grantSpokeRoles( + Logger logger, + OrchestrationReports.FullDeploymentReport memory report, + address spokeAdmin, + address spokeConfiguratorAdmin + ) internal { + address accessManager = report.authorityBatchReport.accessManager; + + logger.logHeader1('granting Spoke Admin role to', spokeAdmin); + AaveV4SpokeRolesProcedure.grantSpokeAllRoles({accessManager: accessManager, admin: spokeAdmin}); + + logger.logHeader1( + 'granting Spoke Configurator roles to', + report.configuratorBatchReport.spokeConfigurator + ); + AaveV4SpokeRolesProcedure.grantSpokeRole({ + accessManager: accessManager, + role: Roles.SPOKE_CONFIGURATOR_ROLE, + admin: report.configuratorBatchReport.spokeConfigurator + }); + + logger.logHeader1('granting SpokeConfigurator Admin roles to', spokeConfiguratorAdmin); + AaveV4SpokeConfiguratorRolesProcedure.grantSpokeConfiguratorAllRoles({ + accessManager: accessManager, + admin: spokeConfiguratorAdmin + }); + } + + function _logHubReport( + Logger logger, + BatchReports.HubInstanceBatchReport memory report, + string memory label + ) internal pure { + logger.log(label); + logger.logDetail('Hub', report.hubProxy); + logger.logDetail('InterestRateStrategy', report.irStrategy); + } + + function _logSpokeReport( + Logger logger, + BatchReports.SpokeInstanceBatchReport memory report, + string memory label + ) internal pure { + logger.log(label); + logger.logDetail('SpokeInstance Proxy', report.spokeProxy); + logger.logDetail('SpokeInstance Implementation', report.spokeImplementation); + logger.logDetail('AaveOracle', report.aaveOracle); + } + + /// @dev Derives the root salt with deployer address in the first 160 bits + /// and the remaining 96 bits from the user-provided salt. + /// Layout: [deployer (160 bits) | truncated_hash (96 bits)] + function _deriveSalt(address deployer, bytes32 salt) internal pure returns (bytes32) { + return bytes32(bytes20(deployer)) | (keccak256(abi.encode(SALT, salt)) >> 160); + } + + /// @dev Derives a child salt from a base salt, contract type, and label. + /// @param baseSalt The base salt to derive the child salt from. + /// @param contractType The type of the contract (e.g. 'hub', 'spoke'). + /// @param label The label of the contract to be deployed. + /// @return The derived child salt. + function _deriveChildSalt( + bytes32 baseSalt, + string memory contractType, + string memory label + ) internal pure returns (bytes32) { + return keccak256(abi.encode(baseSalt, contractType, label)); + } +} diff --git a/src/deployments/procedures/AaveV4DeployProcedureBase.sol b/src/deployments/procedures/AaveV4DeployProcedureBase.sol new file mode 100644 index 000000000..a4bba4e32 --- /dev/null +++ b/src/deployments/procedures/AaveV4DeployProcedureBase.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {Vm} from 'forge-std/Vm.sol'; + +contract AaveV4DeployProcedureBase { + Vm internal constant vm = Vm(address(uint160(uint256(keccak256('hevm cheat code'))))); +} diff --git a/src/deployments/procedures/deploy/AaveV4AccessManagerEnumerableDeployProcedure.sol b/src/deployments/procedures/deploy/AaveV4AccessManagerEnumerableDeployProcedure.sol new file mode 100644 index 000000000..63941dcab --- /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 {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; +import {AaveV4DeployProcedureBase} from 'src/deployments/procedures/AaveV4DeployProcedureBase.sol'; +import {AccessManagerEnumerable} from 'src/access/AccessManagerEnumerable.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..7f47ed98d --- /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 authority, bytes32 salt) internal returns (address) { + require(authority != address(0), 'invalid authority'); + return + Create2Utils.create2Deploy( + salt, + abi.encodePacked(type(HubConfigurator).creationCode, abi.encode(authority)) + ); + } +} diff --git a/src/deployments/procedures/deploy/hub/AaveV4HubDeployProcedure.sol b/src/deployments/procedures/deploy/hub/AaveV4HubDeployProcedure.sol new file mode 100644 index 000000000..085e2e6a0 --- /dev/null +++ b/src/deployments/procedures/deploy/hub/AaveV4HubDeployProcedure.sol @@ -0,0 +1,27 @@ +// 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 {IHubInstance} from 'src/deployments/utils/interfaces/IHubInstance.sol'; + +contract AaveV4HubDeployProcedure is AaveV4DeployProcedureBase { + function _deployUpgradeableHubInstance( + address hubProxyAdminOwner, + address authority, + bytes memory hubBytecode, + bytes32 salt + ) internal returns (address hubProxy, address hubImplementation) { + require(hubProxyAdminOwner != address(0), 'invalid hub proxy admin owner'); + require(authority != address(0), 'invalid authority'); + hubImplementation = Create2Utils.create2Deploy({salt: salt, bytecode: hubBytecode}); + hubProxy = Create2Utils.proxify({ + salt: salt, + logic: hubImplementation, + initialOwner: hubProxyAdminOwner, + data: abi.encodeCall(IHubInstance.initialize, (authority)) + }); + return (hubProxy, hubImplementation); + } +} 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/AaveV4ConfigPositionManagerDeployProcedure.sol b/src/deployments/procedures/deploy/position-manager/AaveV4ConfigPositionManagerDeployProcedure.sol new file mode 100644 index 000000000..f8783e3b8 --- /dev/null +++ b/src/deployments/procedures/deploy/position-manager/AaveV4ConfigPositionManagerDeployProcedure.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 {ConfigPositionManager} from 'src/position-manager/ConfigPositionManager.sol'; + +contract AaveV4ConfigPositionManagerDeployProcedure is AaveV4DeployProcedureBase { + function _deployConfigPositionManager(address owner, bytes32 salt) internal returns (address) { + require(owner != address(0), 'invalid owner'); + return + Create2Utils.create2Deploy({ + salt: salt, + bytecode: abi.encodePacked(type(ConfigPositionManager).creationCode, abi.encode(owner)) + }); + } +} diff --git a/src/deployments/procedures/deploy/position-manager/AaveV4GiverPositionManagerDeployProcedure.sol b/src/deployments/procedures/deploy/position-manager/AaveV4GiverPositionManagerDeployProcedure.sol new file mode 100644 index 000000000..cc62b88d4 --- /dev/null +++ b/src/deployments/procedures/deploy/position-manager/AaveV4GiverPositionManagerDeployProcedure.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 {GiverPositionManager} from 'src/position-manager/GiverPositionManager.sol'; + +contract AaveV4GiverPositionManagerDeployProcedure is AaveV4DeployProcedureBase { + function _deployGiverPositionManager(address owner, bytes32 salt) internal returns (address) { + require(owner != address(0), 'invalid owner'); + return + Create2Utils.create2Deploy({ + salt: salt, + bytecode: abi.encodePacked(type(GiverPositionManager).creationCode, abi.encode(owner)) + }); + } +} 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..04ba3a162 --- /dev/null +++ b/src/deployments/procedures/deploy/position-manager/AaveV4NativeTokenGatewayDeployProcedure.sol @@ -0,0 +1,26 @@ +// 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: salt, + bytecode: 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..4e5101cf6 --- /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: salt, + bytecode: abi.encodePacked(type(SignatureGateway).creationCode, abi.encode(owner)) + }); + } +} diff --git a/src/deployments/procedures/deploy/position-manager/AaveV4TakerPositionManagerDeployProcedure.sol b/src/deployments/procedures/deploy/position-manager/AaveV4TakerPositionManagerDeployProcedure.sol new file mode 100644 index 000000000..48409d5de --- /dev/null +++ b/src/deployments/procedures/deploy/position-manager/AaveV4TakerPositionManagerDeployProcedure.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 {TakerPositionManager} from 'src/position-manager/TakerPositionManager.sol'; + +contract AaveV4TakerPositionManagerDeployProcedure is AaveV4DeployProcedureBase { + function _deployTakerPositionManager(address owner, bytes32 salt) internal returns (address) { + require(owner != address(0), 'invalid owner'); + return + Create2Utils.create2Deploy({ + salt: salt, + bytecode: abi.encodePacked(type(TakerPositionManager).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..393945606 --- /dev/null +++ b/src/deployments/procedures/deploy/spoke/AaveV4AaveOracleDeployProcedure.sol @@ -0,0 +1,13 @@ +// 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(uint8 decimals) internal returns (address) { + require(decimals > 0, 'invalid oracle decimals'); + // AaveOracle must be deployed via create so deployer can call setSpoke after deployment + return address(new AaveOracle({decimals_: decimals})); + } +} diff --git a/src/deployments/procedures/deploy/spoke/AaveV4SpokeConfiguratorDeployProcedure.sol b/src/deployments/procedures/deploy/spoke/AaveV4SpokeConfiguratorDeployProcedure.sol new file mode 100644 index 000000000..32123c5a0 --- /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 authority, bytes32 salt) internal returns (address) { + require(authority != address(0), 'invalid authority'); + return + Create2Utils.create2Deploy( + salt, + abi.encodePacked(type(SpokeConfigurator).creationCode, abi.encode(authority)) + ); + } +} diff --git a/src/deployments/procedures/deploy/spoke/AaveV4SpokeDeployProcedure.sol b/src/deployments/procedures/deploy/spoke/AaveV4SpokeDeployProcedure.sol new file mode 100644 index 000000000..7c0c878cf --- /dev/null +++ b/src/deployments/procedures/deploy/spoke/AaveV4SpokeDeployProcedure.sol @@ -0,0 +1,42 @@ +// 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 {ISpokeInstance} from 'src/deployments/utils/interfaces/ISpokeInstance.sol'; + +contract AaveV4SpokeDeployProcedure is AaveV4DeployProcedureBase { + function _deployUpgradeableSpokeInstance( + address spokeProxyAdminOwner, + address authority, + address oracle, + bytes memory spokeBytecode, + uint16 maxUserReservesLimit, + bytes32 salt + ) internal returns (address spokeProxy, address spokeImplementation) { + require(spokeProxyAdminOwner != address(0), 'invalid spoke proxy admin owner'); + require(authority != address(0), 'invalid authority'); + require(oracle != address(0), 'invalid oracle'); + require(maxUserReservesLimit > 0, 'invalid max user reserves limit'); + spokeImplementation = Create2Utils.create2Deploy({ + salt: salt, + bytecode: _getSpokeInstanceInitCode(spokeBytecode, oracle, maxUserReservesLimit) + }); + spokeProxy = Create2Utils.proxify({ + salt: salt, + logic: spokeImplementation, + initialOwner: spokeProxyAdminOwner, + data: abi.encodeCall(ISpokeInstance.initialize, (authority)) + }); + return (spokeProxy, spokeImplementation); + } + + function _getSpokeInstanceInitCode( + bytes memory spokeBytecode, + address oracle, + uint16 maxUserReservesLimit + ) internal pure returns (bytes memory) { + return abi.encodePacked(spokeBytecode, abi.encode(oracle, maxUserReservesLimit)); + } +} diff --git a/src/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeDeployProcedure.sol b/src/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeDeployProcedure.sol new file mode 100644 index 000000000..def7b0f5b --- /dev/null +++ b/src/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeDeployProcedure.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 {ITokenizationSpokeInstance} from 'src/deployments/utils/interfaces/ITokenizationSpokeInstance.sol'; +import {TokenizationSpokeInstance} from 'src/spoke/instances/TokenizationSpokeInstance.sol'; +import {ITokenizationSpoke} from 'src/spoke/interfaces/ITokenizationSpoke.sol'; + +contract AaveV4TokenizationSpokeDeployProcedure is AaveV4DeployProcedureBase { + function _deployUpgradeableTokenizationSpokeInstance( + address hub, + address underlying, + address spokeProxyAdminOwner, + string memory shareName, + string memory shareSymbol, + bytes32 salt + ) internal returns (address tokenizationSpokeProxy, address tokenizationSpokeImplementation) { + require(hub != address(0), 'invalid hub'); + require(spokeProxyAdminOwner != address(0), 'invalid spoke proxy admin owner'); + require(bytes(shareName).length > 0, 'invalid share name'); + require(bytes(shareSymbol).length > 0, 'invalid share symbol'); + + tokenizationSpokeImplementation = Create2Utils.create2Deploy({ + salt: salt, + bytecode: _getTokenizationSpokeInstanceInitCode(hub, underlying) + }); + + tokenizationSpokeProxy = Create2Utils.proxify({ + salt: salt, + logic: tokenizationSpokeImplementation, + initialOwner: spokeProxyAdminOwner, + data: abi.encodeCall(ITokenizationSpokeInstance.initialize, (shareName, shareSymbol)) + }); + + require( + ITokenizationSpoke(tokenizationSpokeProxy).hub() == hub, + 'tokenization spoke hub mismatch' + ); + require( + ITokenizationSpoke(tokenizationSpokeProxy).asset() == underlying, + 'tokenization spoke underlying mismatch' + ); + + return (tokenizationSpokeProxy, tokenizationSpokeImplementation); + } + function _getTokenizationSpokeInstanceInitCode( + address hub, + address underlying + ) internal pure returns (bytes memory) { + return + abi.encodePacked(type(TokenizationSpokeInstance).creationCode, abi.encode(hub, underlying)); + } +} diff --git a/src/deployments/procedures/deploy/spoke/AaveV4TreasurySpokeDeployProcedure.sol b/src/deployments/procedures/deploy/spoke/AaveV4TreasurySpokeDeployProcedure.sol new file mode 100644 index 000000000..7fdd184e4 --- /dev/null +++ b/src/deployments/procedures/deploy/spoke/AaveV4TreasurySpokeDeployProcedure.sol @@ -0,0 +1,24 @@ +// 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 {TreasurySpokeInstance} from 'src/spoke/instances/TreasurySpokeInstance.sol'; + +contract AaveV4TreasurySpokeDeployProcedure is AaveV4DeployProcedureBase { + function _deployTreasurySpoke(address owner, bytes32 salt) internal returns (address) { + require(owner != address(0), 'invalid owner'); + address implementation = Create2Utils.create2Deploy( + salt, + type(TreasurySpokeInstance).creationCode + ); + return + Create2Utils.proxify( + salt, + implementation, + owner, + abi.encodeCall(TreasurySpokeInstance.initialize, (owner)) + ); + } +} diff --git a/src/deployments/procedures/roles/AaveV4AccessManagerRolesProcedure.sol b/src/deployments/procedures/roles/AaveV4AccessManagerRolesProcedure.sol new file mode 100644 index 000000000..8cec682ea --- /dev/null +++ b/src/deployments/procedures/roles/AaveV4AccessManagerRolesProcedure.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {IAccessManager} from 'src/dependencies/openzeppelin/IAccessManager.sol'; +import {Roles} from 'src/deployments/utils/libraries/Roles.sol'; +import {RolesValidation} from 'src/deployments/utils/libraries/RolesValidation.sol'; + +library AaveV4AccessManagerRolesProcedure { + /// @notice The adminToRemove must be the current default admin, otherwise the procedure will revert. + function replaceDefaultAdminRole( + address accessManager, + address adminToAdd, + address adminToRemove + ) internal { + grantAccessManagerAdminRole(accessManager, adminToAdd); + revokeAccessManagerAdminRole(accessManager, adminToRemove); + } + + function grantAccessManagerAdminRole(address accessManager, address adminToAdd) internal { + RolesValidation.validateNonZeroAddress(accessManager); + RolesValidation.validateNonZeroAddress(adminToAdd); + IAccessManager(accessManager).grantRole({ + roleId: Roles.ACCESS_MANAGER_DEFAULT_ADMIN, + account: adminToAdd, + executionDelay: 0 + }); + } + + function revokeAccessManagerAdminRole(address accessManager, address adminToRemove) internal { + RolesValidation.validateNonZeroAddress(accessManager); + RolesValidation.validateNonZeroAddress(adminToRemove); + IAccessManager(accessManager).revokeRole({ + roleId: Roles.ACCESS_MANAGER_DEFAULT_ADMIN, + account: adminToRemove + }); + } +} diff --git a/src/deployments/procedures/roles/AaveV4HubConfiguratorRolesProcedure.sol b/src/deployments/procedures/roles/AaveV4HubConfiguratorRolesProcedure.sol new file mode 100644 index 000000000..16e45f5cd --- /dev/null +++ b/src/deployments/procedures/roles/AaveV4HubConfiguratorRolesProcedure.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {IAccessManager} from 'src/dependencies/openzeppelin/IAccessManager.sol'; +import {Roles} from 'src/deployments/utils/libraries/Roles.sol'; +import {RolesValidation} from 'src/deployments/utils/libraries/RolesValidation.sol'; + +library AaveV4HubConfiguratorRolesProcedure { + /// @notice Grants the HubConfigurator domain admin role (200) to `admin`. + function grantHubConfiguratorAllRoles(address accessManager, address admin) internal { + grantHubConfiguratorRole(accessManager, Roles.HUB_CONFIGURATOR_DOMAIN_ADMIN_ROLE, admin); + } + + function grantHubConfiguratorRole(address accessManager, uint64 role, address admin) internal { + RolesValidation.validateNonZeroAddress(accessManager); + RolesValidation.validateNonZeroAddress(admin); + IAccessManager(accessManager).grantRole({roleId: role, account: admin, executionDelay: 0}); + } + + /// @notice Sets up the HubConfigurator domain admin role with all target selectors. + function setupHubConfiguratorAllRoles(address accessManager, address hubConfigurator) internal { + setupHubConfiguratorRole( + accessManager, + hubConfigurator, + Roles.HUB_CONFIGURATOR_DOMAIN_ADMIN_ROLE, + Roles.getHubConfiguratorDomainAdminRoleSelectors() + ); + } + + function setupHubConfiguratorRole( + address accessManager, + address hubConfigurator, + uint64 role, + bytes4[] memory selectors + ) internal { + RolesValidation.validateNonZeroAddress(accessManager); + RolesValidation.validateNonZeroAddress(hubConfigurator); + IAccessManager(accessManager).setTargetFunctionRole(hubConfigurator, selectors, role); + } +} diff --git a/src/deployments/procedures/roles/AaveV4HubRolesProcedure.sol b/src/deployments/procedures/roles/AaveV4HubRolesProcedure.sol new file mode 100644 index 000000000..6e992ab17 --- /dev/null +++ b/src/deployments/procedures/roles/AaveV4HubRolesProcedure.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {IAccessManager} from 'src/dependencies/openzeppelin/IAccessManager.sol'; +import {Roles} from 'src/deployments/utils/libraries/Roles.sol'; +import {RolesValidation} from 'src/deployments/utils/libraries/RolesValidation.sol'; + +library AaveV4HubRolesProcedure { + /// @notice Grants all Hub granular roles to `admin`: + /// - HUB_CONFIGURATOR_ROLE + /// - HUB_FEE_MINTER_ROLE + /// - HUB_DEFICIT_ELIMINATOR_ROLE + function grantHubAllRoles(address accessManager, address admin) internal { + grantHubRole(accessManager, Roles.HUB_CONFIGURATOR_ROLE, admin); + grantHubRole(accessManager, Roles.HUB_FEE_MINTER_ROLE, admin); + grantHubRole(accessManager, Roles.HUB_DEFICIT_ELIMINATOR_ROLE, admin); + } + + function grantHubRole(address accessManager, uint64 role, address admin) internal { + RolesValidation.validateNonZeroAddress(accessManager); + RolesValidation.validateNonZeroAddress(admin); + IAccessManager(accessManager).grantRole({roleId: role, account: admin, executionDelay: 0}); + } + + function setupHubAllRoles(address accessManager, address hub) internal { + setupHubRole( + accessManager, + hub, + Roles.HUB_CONFIGURATOR_ROLE, + Roles.getHubConfiguratorRoleSelectors() + ); + setupHubRole( + accessManager, + hub, + Roles.HUB_FEE_MINTER_ROLE, + Roles.getHubFeeMinterRoleSelectors() + ); + setupHubRole( + accessManager, + hub, + Roles.HUB_DEFICIT_ELIMINATOR_ROLE, + Roles.getHubDeficitEliminatorRoleSelectors() + ); + } + + function setupHubRole( + address accessManager, + address hub, + uint64 roleId, + bytes4[] memory selectors + ) internal { + RolesValidation.validateNonZeroAddress(accessManager); + RolesValidation.validateNonZeroAddress(hub); + IAccessManager(accessManager).setTargetFunctionRole({ + target: hub, + selectors: selectors, + roleId: roleId + }); + } +} diff --git a/src/deployments/procedures/roles/AaveV4SpokeConfiguratorRolesProcedure.sol b/src/deployments/procedures/roles/AaveV4SpokeConfiguratorRolesProcedure.sol new file mode 100644 index 000000000..aa0277b54 --- /dev/null +++ b/src/deployments/procedures/roles/AaveV4SpokeConfiguratorRolesProcedure.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {IAccessManager} from 'src/dependencies/openzeppelin/IAccessManager.sol'; +import {Roles} from 'src/deployments/utils/libraries/Roles.sol'; +import {RolesValidation} from 'src/deployments/utils/libraries/RolesValidation.sol'; + +library AaveV4SpokeConfiguratorRolesProcedure { + /// @notice Grants the SpokeConfigurator domain admin role (400) to `admin`. + function grantSpokeConfiguratorAllRoles(address accessManager, address admin) internal { + grantSpokeConfiguratorRole(accessManager, Roles.SPOKE_CONFIGURATOR_DOMAIN_ADMIN_ROLE, admin); + } + + function grantSpokeConfiguratorRole(address accessManager, uint64 role, address admin) internal { + RolesValidation.validateNonZeroAddress(accessManager); + RolesValidation.validateNonZeroAddress(admin); + IAccessManager(accessManager).grantRole({roleId: role, account: admin, executionDelay: 0}); + } + + /// @notice Sets up the SpokeConfigurator domain admin role with all target selectors. + function setupSpokeConfiguratorAllRoles( + address accessManager, + address spokeConfigurator + ) internal { + setupSpokeConfiguratorRole( + accessManager, + spokeConfigurator, + Roles.SPOKE_CONFIGURATOR_DOMAIN_ADMIN_ROLE, + Roles.getSpokeConfiguratorDomainAdminRoleSelectors() + ); + } + + function setupSpokeConfiguratorRole( + address accessManager, + address spokeConfigurator, + uint64 role, + bytes4[] memory selectors + ) internal { + RolesValidation.validateNonZeroAddress(accessManager); + RolesValidation.validateNonZeroAddress(spokeConfigurator); + IAccessManager(accessManager).setTargetFunctionRole(spokeConfigurator, selectors, role); + } +} diff --git a/src/deployments/procedures/roles/AaveV4SpokeRolesProcedure.sol b/src/deployments/procedures/roles/AaveV4SpokeRolesProcedure.sol new file mode 100644 index 000000000..1f0074086 --- /dev/null +++ b/src/deployments/procedures/roles/AaveV4SpokeRolesProcedure.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {IAccessManager} from 'src/dependencies/openzeppelin/IAccessManager.sol'; +import {Roles} from 'src/deployments/utils/libraries/Roles.sol'; +import {RolesValidation} from 'src/deployments/utils/libraries/RolesValidation.sol'; + +library AaveV4SpokeRolesProcedure { + /// @notice Grants all Spoke granular roles to `admin`: + /// - SPOKE_USER_POSITION_UPDATER_ROLE + /// - SPOKE_CONFIGURATOR_ROLE + function grantSpokeAllRoles(address accessManager, address admin) internal { + grantSpokeRole(accessManager, Roles.SPOKE_USER_POSITION_UPDATER_ROLE, admin); + grantSpokeRole(accessManager, Roles.SPOKE_CONFIGURATOR_ROLE, admin); + } + + function grantSpokeRole(address accessManager, uint64 role, address admin) internal { + RolesValidation.validateNonZeroAddress(accessManager); + RolesValidation.validateNonZeroAddress(admin); + IAccessManager(accessManager).grantRole({roleId: role, account: admin, executionDelay: 0}); + } + + function setupSpokeAllRoles(address accessManager, address spoke) internal { + setupSpokeRole({ + accessManager: accessManager, + spoke: spoke, + roleId: Roles.SPOKE_USER_POSITION_UPDATER_ROLE, + selectors: Roles.getSpokePositionUpdaterRoleSelectors() + }); + setupSpokeRole({ + accessManager: accessManager, + spoke: spoke, + roleId: Roles.SPOKE_CONFIGURATOR_ROLE, + selectors: Roles.getSpokeConfiguratorRoleSelectors() + }); + } + + function setupSpokeRole( + address accessManager, + address spoke, + uint64 roleId, + bytes4[] memory selectors + ) internal { + RolesValidation.validateNonZeroAddress(accessManager); + RolesValidation.validateNonZeroAddress(spoke); + IAccessManager(accessManager).setTargetFunctionRole({ + target: spoke, + selectors: selectors, + roleId: roleId + }); + } +} diff --git a/src/deployments/utils/InputUtils.sol b/src/deployments/utils/InputUtils.sol new file mode 100644 index 000000000..c02cdb72a --- /dev/null +++ b/src/deployments/utils/InputUtils.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +contract InputUtils { + /// @dev accessManagerAdmin The default admin of the access manager. + /// @dev hubAdmin The admin of the hub. + /// @dev hubConfiguratorAdmin The admin granted all hub configurator roles. + /// @dev hubProxyAdminOwner The owner of the hub proxyAdmin. + /// @dev treasurySpokeOwner The owner of the treasury spoke. + /// @dev spokeAdmin The spoke admin. + /// @dev spokeProxyAdminOwner The owner of the spoke proxyAdmin. + /// @dev spokeConfiguratorAdmin The admin granted all spoke configurator roles. + /// @dev gatewayOwner The owner of the native token and signature gateways. + /// @dev positionManagerOwner The owner of the position manager contracts (giver/taker). + /// @dev nativeWrapper The address of the native wrapper (required when deployNativeTokenGateway is true). + /// @dev deployNativeTokenGateway Whether to deploy the NativeTokenGateway (from periphery config). + /// @dev deploySignatureGateway Whether to deploy the SignatureGateway (from periphery config). + /// @dev deployPositionManagers Whether to deploy the position manager batch (giver/taker). + /// @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 spokeMaxReservesLimits Per-spoke max user reserves limit (parallel to spokeLabels). + /// @dev salt Root salt for deterministic CREATE2 deployment; orchestration derives per-batch salts. + struct FullDeployInputs { + address accessManagerAdmin; + address hubAdmin; + address hubConfiguratorAdmin; + address hubProxyAdminOwner; + address treasurySpokeOwner; + address spokeAdmin; + address spokeProxyAdminOwner; + address spokeConfiguratorAdmin; + address gatewayOwner; + address positionManagerOwner; + address nativeWrapper; + bool deployNativeTokenGateway; + bool deploySignatureGateway; + bool deployPositionManagers; + bool grantRoles; + string[] hubLabels; + string[] spokeLabels; + uint16[] spokeMaxReservesLimits; + bytes32 salt; + } + + function _validateUniqueLabels(string[] memory labels, string memory kind) internal pure { + for (uint256 i; i < labels.length; i++) { + for (uint256 j = i + 1; j < labels.length; j++) { + require( + keccak256(bytes(labels[i])) != keccak256(bytes(labels[j])), + string.concat('duplicate ', kind, ' label: ', labels[i]) + ); + } + } + } +} diff --git a/src/deployments/utils/Logger.sol b/src/deployments/utils/Logger.sol new file mode 100644 index 000000000..d4df7804c --- /dev/null +++ b/src/deployments/utils/Logger.sol @@ -0,0 +1,178 @@ +// 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 _jsonKey; + string internal _json; + + constructor(string memory outputPath_) { + _jsonKey = 'root'; + _outputPath = outputPath_; + _json = _jsonKey; + } + + 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, + '.json' + ) + ); + } + + 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 logHeader1(string memory value) public pure { + _logHeader1(value); + } + + function logHeader1(string memory label, address value) public pure { + _logHeader1(label, value); + } + + function logDetail(string memory label, address value) public pure { + _logDetail(label, value); + } + + function logNewLine() public pure { + _logNewLine(); + } + + function _write(string memory label, bytes32 value) internal { + _json = vm.serializeBytes32(_jsonKey, label, value); + } + + function _write(string memory label, address value) internal { + _json = vm.serializeAddress(_jsonKey, label, value); + } + + function _write(string memory label, uint256 value) internal { + _json = vm.serializeUint(_jsonKey, label, value); + } + + function _write(string memory value) internal { + _json = vm.serializeString(_jsonKey, '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(_jsonKey, groupLabel, group); + } + + /// @dev Writes a nested group: { groupLabel: { entryLabel: { proxy: ..., implementation: ... }, ... } } + function _writeNestedProxyGroup( + string memory groupLabel, + string[] memory labels, + address[] memory proxies, + address[] memory implementations + ) internal { + string memory group; + for (uint256 i = 0; i < labels.length; i++) { + string memory inner; + inner = vm.serializeAddress(labels[i], 'proxy', proxies[i]); + inner = vm.serializeAddress(labels[i], 'implementation', implementations[i]); + group = vm.serializeString(groupLabel, labels[i], inner); + } + _json = vm.serializeString(_jsonKey, 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(_jsonKey, groupLabel, group); + } + + function _getTimestamp() internal view returns (string memory) { + return vm.toString(vm.unixTime() / 1000); + } + + 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); + } + + function _logHeader1(string memory value) internal pure { + console.log('...%s...', value); + } + + function _logHeader1(string memory label, address value) internal pure { + console.log('...%s %s...', label, value); + } + + function _logDetail(string memory label, address value) internal pure { + console.log(' %s: %s', label, value); + } + + function _logNewLine() internal pure { + console.log(); + } +} diff --git a/src/deployments/utils/MetadataLogger.sol b/src/deployments/utils/MetadataLogger.sol new file mode 100644 index 000000000..378ae519d --- /dev/null +++ b/src/deployments/utils/MetadataLogger.sol @@ -0,0 +1,68 @@ +// 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('salt', report.salt); + _write('accessManager', report.authorityBatchReport.accessManager); + _write('hubConfigurator', report.configuratorBatchReport.hubConfigurator); + _write('spokeConfigurator', report.configuratorBatchReport.spokeConfigurator); + _write('treasurySpoke', report.treasurySpokeBatchReport.treasurySpoke); + + // Group hubs by property type + uint256 hubLen = report.hubInstanceBatchReports.length; + Logger.AddressEntry[] memory hubEntries = new Logger.AddressEntry[](hubLen); + Logger.AddressEntry[] memory irEntries = new Logger.AddressEntry[](hubLen); + for (uint256 i; i < hubLen; i++) { + hubEntries[i] = Logger.AddressEntry({ + label: report.hubInstanceBatchReports[i].label, + value: report.hubInstanceBatchReports[i].report.hubProxy + }); + irEntries[i] = Logger.AddressEntry({ + label: report.hubInstanceBatchReports[i].label, + value: report.hubInstanceBatchReports[i].report.irStrategy + }); + } + _writeGroup('hub', hubEntries); + _writeGroup('irStrategy', irEntries); + + // Group spokes by property type + uint256 spokeLen = report.spokeInstanceBatchReports.length; + Logger.AddressEntry[] memory spokeEntries = new Logger.AddressEntry[](spokeLen); + Logger.AddressEntry[] memory oracleEntries = new Logger.AddressEntry[](spokeLen); + for (uint256 i; i < spokeLen; i++) { + spokeEntries[i] = Logger.AddressEntry({ + label: report.spokeInstanceBatchReports[i].label, + value: report.spokeInstanceBatchReports[i].report.spokeProxy + }); + oracleEntries[i] = Logger.AddressEntry({ + label: report.spokeInstanceBatchReports[i].label, + value: report.spokeInstanceBatchReports[i].report.aaveOracle + }); + } + _writeGroup('spoke', spokeEntries); + _writeGroup('oracle', oracleEntries); + + if (report.gatewaysBatchReport.signatureGateway != address(0)) { + _write('signatureGateway', report.gatewaysBatchReport.signatureGateway); + } + if (report.gatewaysBatchReport.nativeGateway != address(0)) { + _write('nativeTokenGateway', report.gatewaysBatchReport.nativeGateway); + } + if (report.positionManagerBatchReport.giverPositionManager != address(0)) { + _write('giverPositionManager', report.positionManagerBatchReport.giverPositionManager); + } + if (report.positionManagerBatchReport.takerPositionManager != address(0)) { + _write('takerPositionManager', report.positionManagerBatchReport.takerPositionManager); + } + if (report.positionManagerBatchReport.configPositionManager != address(0)) { + _write('configPositionManager', report.positionManagerBatchReport.configPositionManager); + } + } +} diff --git a/src/deployments/utils/interfaces/IHubInstance.sol b/src/deployments/utils/interfaces/IHubInstance.sol new file mode 100644 index 000000000..9b390cbf6 --- /dev/null +++ b/src/deployments/utils/interfaces/IHubInstance.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {IHub} from 'src/hub/interfaces/IHub.sol'; + +interface IHubInstance is IHub { + function initialize(address authority) external; + + function HUB_REVISION() external view returns (uint64); +} diff --git a/tests/mocks/ISpokeInstance.sol b/src/deployments/utils/interfaces/ISpokeInstance.sol similarity index 90% rename from tests/mocks/ISpokeInstance.sol rename to src/deployments/utils/interfaces/ISpokeInstance.sol index 5d48a132d..fee77efec 100644 --- a/tests/mocks/ISpokeInstance.sol +++ b/src/deployments/utils/interfaces/ISpokeInstance.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.0; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; diff --git a/src/deployments/utils/interfaces/ITokenizationSpokeInstance.sol b/src/deployments/utils/interfaces/ITokenizationSpokeInstance.sol new file mode 100644 index 000000000..6cb3a49ec --- /dev/null +++ b/src/deployments/utils/interfaces/ITokenizationSpokeInstance.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {ITokenizationSpoke} from 'src/spoke/interfaces/ITokenizationSpoke.sol'; + +interface ITokenizationSpokeInstance is ITokenizationSpoke { + function initialize(string memory shareName, string memory shareSymbol) external; + + function SPOKE_REVISION() external view returns (uint64); +} diff --git a/src/deployments/utils/libraries/BytecodeHelper.sol b/src/deployments/utils/libraries/BytecodeHelper.sol new file mode 100644 index 000000000..16f704e30 --- /dev/null +++ b/src/deployments/utils/libraries/BytecodeHelper.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {Vm} from 'forge-std/Vm.sol'; + +/// @title BytecodeHelper +/// @notice Library for loading contract bytecode. +library BytecodeHelper { + Vm internal constant vm = Vm(address(uint160(uint256(keccak256('hevm cheat code'))))); + + function getHubBytecode() internal view returns (bytes memory) { + return vm.getCode('src/hub/instances/HubInstance.sol:HubInstance'); + } + + function getSpokeBytecode() internal view returns (bytes memory) { + return vm.getCode('src/spoke/instances/SpokeInstance.sol:SpokeInstance'); + } +} diff --git a/src/deployments/utils/libraries/Create2Utils.sol b/src/deployments/utils/libraries/Create2Utils.sol new file mode 100644 index 000000000..730a3f1ec --- /dev/null +++ b/src/deployments/utils/libraries/Create2Utils.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.20; + +import {TransparentUpgradeableProxy} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; + +library Create2Utils { + // https://github.com/safe-global/safe-singleton-factory + address public constant CREATE2_FACTORY = 0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7; + + error MissingCreate2Factory(); + error Create2AddressDerivationFailure(); + error FailedCreate2FactoryCall(); + error ContractAlreadyDeployed(); + + function create2Deploy(bytes32 salt, bytes memory bytecode) internal returns (address) { + require(isContractDeployed(CREATE2_FACTORY), MissingCreate2Factory()); + address computed = computeCreate2Address({salt: salt, bytecode: 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 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(bytecode)); + } + + function addressFromLast20Bytes(bytes32 bytesValue) internal pure returns (address) { + return address(uint160(uint256(bytesValue))); + } +} diff --git a/src/deployments/utils/libraries/DeployConstants.sol b/src/deployments/utils/libraries/DeployConstants.sol new file mode 100644 index 000000000..9eb304a2c --- /dev/null +++ b/src/deployments/utils/libraries/DeployConstants.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +/// @title DeployConstants +/// @notice Protocol constants used by the deployment engine. +library DeployConstants { + /// @dev Default oracle decimals for AaveOracle instances. + uint8 public constant ORACLE_DECIMALS = 8; + + /// @dev Default max user reserves limit per spoke. + uint16 public constant MAX_ALLOWED_USER_RESERVES_LIMIT = type(uint16).max; +} diff --git a/src/deployments/utils/libraries/Roles.sol b/src/deployments/utils/libraries/Roles.sol new file mode 100644 index 000000000..ef5c6e129 --- /dev/null +++ b/src/deployments/utils/libraries/Roles.sol @@ -0,0 +1,168 @@ +// 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'; +import {IHubConfigurator} from 'src/hub/interfaces/IHubConfigurator.sol'; +import {ISpokeConfigurator} from 'src/spoke/interfaces/ISpokeConfigurator.sol'; + +/// @title Roles library +/// @author Aave Labs +/// @notice Defines the different roles used by the protocol and their target selectors. +/// +/// Role IDs are namespaced by domain: +/// - AccessManager: 0 (default admin) +/// - Hub: 100-199 +/// - HubConfigurator: 200-299 +/// - Spoke: 300-399 +/// - SpokeConfigurator: 400-499 +/// +/// ## Role strategy +/// +/// A single authority contract will be used to manage the roles for all applicable contracts on a given chain. +/// Role IDs, selector mappings, and overall configuration should be kept identical +/// across chains to avoid additional overhead and role divergence. +/// +/// Hub and Spoke roles remain granular (e.g. HUB_CONFIGURATOR_ROLE, +/// HUB_FEE_MINTER_ROLE, HUB_DEFICIT_ELIMINATOR_ROLE each control a distinct set +/// of selectors). +/// +/// HubConfigurator and SpokeConfigurator follow a different approach: initially, +/// a single Domain Admin role per domain (HUB_CONFIGURATOR_DOMAIN_ADMIN_ROLE = 200, +/// SPOKE_CONFIGURATOR_DOMAIN_ADMIN_ROLE = 400) holds all target selectors. +/// As more granular roles are introduced, they are added at the next available ID +/// (201, 202, ... / 401, 402, ...) and the corresponding selectors are reassigned +/// from the Domain Admin role to the new granular role: +/// - Existing role IDs should never be overwritten or reused for a different purpose. +/// - New roles are always appended with an incremented ID. +/// - The Domain Admin role (200/400) only ever has its selector set shrink over +/// time as selectors are divided into more granular roles. +/// - Addresses holding the Domain Admin role should be granted the new +/// granular role to retain their existing access. +library Roles { + // AccessManager roles + uint64 public constant ACCESS_MANAGER_DEFAULT_ADMIN = 0; + + // Hub roles + uint64 public constant HUB_DOMAIN_ADMIN_ROLE = 100; + uint64 public constant HUB_CONFIGURATOR_ROLE = 101; + uint64 public constant HUB_FEE_MINTER_ROLE = 102; + uint64 public constant HUB_DEFICIT_ELIMINATOR_ROLE = 103; + + // HubConfigurator roles — granularize as needed with new roles appended + uint64 public constant HUB_CONFIGURATOR_DOMAIN_ADMIN_ROLE = 200; + + // Spoke roles + uint64 public constant SPOKE_DOMAIN_ADMIN_ROLE = 300; + uint64 public constant SPOKE_CONFIGURATOR_ROLE = 301; + uint64 public constant SPOKE_USER_POSITION_UPDATER_ROLE = 302; + + // SpokeConfigurator roles — granularize as needed with new roles appended + uint64 public constant SPOKE_CONFIGURATOR_DOMAIN_ADMIN_ROLE = 400; + + // ─── Hub selector getters ─── + + 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 getHubFeeMinterRoleSelectors() internal pure returns (bytes4[] memory) { + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = IHub.mintFeeShares.selector; + return selectors; + } + + function getHubDeficitEliminatorRoleSelectors() internal pure returns (bytes4[] memory) { + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = IHub.eliminateDeficit.selector; + return selectors; + } + + // ─── HubConfigurator selector getters ─── + + function getHubConfiguratorDomainAdminRoleSelectors() internal pure returns (bytes4[] memory) { + bytes4[] memory selectors = new bytes4[](22); + selectors[0] = IHubConfigurator.addAsset.selector; + selectors[1] = IHubConfigurator.addAssetWithDecimals.selector; + selectors[2] = IHubConfigurator.updateLiquidityFee.selector; + selectors[3] = IHubConfigurator.updateFeeReceiver.selector; + selectors[4] = IHubConfigurator.updateFeeConfig.selector; + selectors[5] = IHubConfigurator.updateInterestRateStrategy.selector; + selectors[6] = IHubConfigurator.updateReinvestmentController.selector; + selectors[7] = IHubConfigurator.resetAssetCaps.selector; + selectors[8] = IHubConfigurator.deactivateAsset.selector; + selectors[9] = IHubConfigurator.haltAsset.selector; + selectors[10] = IHubConfigurator.addSpoke.selector; + selectors[11] = IHubConfigurator.addSpokeToAssets.selector; + selectors[12] = IHubConfigurator.updateSpokeActive.selector; + selectors[13] = IHubConfigurator.updateSpokeHalted.selector; + selectors[14] = IHubConfigurator.updateSpokeAddCap.selector; + selectors[15] = IHubConfigurator.updateSpokeDrawCap.selector; + selectors[16] = IHubConfigurator.updateSpokeRiskPremiumThreshold.selector; + selectors[17] = IHubConfigurator.updateSpokeCaps.selector; + selectors[18] = IHubConfigurator.deactivateSpoke.selector; + selectors[19] = IHubConfigurator.haltSpoke.selector; + selectors[20] = IHubConfigurator.resetSpokeCaps.selector; + selectors[21] = IHubConfigurator.updateInterestRateData.selector; + return selectors; + } + + // ─── Spoke selector getters ─── + + 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; + } + + // ─── SpokeConfigurator selector getters ─── + + function getSpokeConfiguratorDomainAdminRoleSelectors() internal pure returns (bytes4[] memory) { + bytes4[] memory selectors = new bytes4[](24); + selectors[0] = ISpokeConfigurator.updateReservePriceSource.selector; + selectors[1] = ISpokeConfigurator.updateLiquidationTargetHealthFactor.selector; + selectors[2] = ISpokeConfigurator.updateHealthFactorForMaxBonus.selector; + selectors[3] = ISpokeConfigurator.updateLiquidationBonusFactor.selector; + selectors[4] = ISpokeConfigurator.updateLiquidationConfig.selector; + selectors[5] = ISpokeConfigurator.addReserve.selector; + selectors[6] = ISpokeConfigurator.updatePaused.selector; + selectors[7] = ISpokeConfigurator.updateFrozen.selector; + selectors[8] = ISpokeConfigurator.updateBorrowable.selector; + selectors[9] = ISpokeConfigurator.updateReceiveSharesEnabled.selector; + selectors[10] = ISpokeConfigurator.updateCollateralRisk.selector; + selectors[11] = ISpokeConfigurator.addCollateralFactor.selector; + selectors[12] = ISpokeConfigurator.updateCollateralFactor.selector; + selectors[13] = ISpokeConfigurator.addMaxLiquidationBonus.selector; + selectors[14] = ISpokeConfigurator.updateMaxLiquidationBonus.selector; + selectors[15] = ISpokeConfigurator.addLiquidationFee.selector; + selectors[16] = ISpokeConfigurator.updateLiquidationFee.selector; + selectors[17] = ISpokeConfigurator.addDynamicReserveConfig.selector; + selectors[18] = ISpokeConfigurator.updateDynamicReserveConfig.selector; + selectors[19] = ISpokeConfigurator.pauseAllReserves.selector; + selectors[20] = ISpokeConfigurator.freezeAllReserves.selector; + selectors[21] = ISpokeConfigurator.pauseReserve.selector; + selectors[22] = ISpokeConfigurator.freezeReserve.selector; + selectors[23] = ISpokeConfigurator.updatePositionManager.selector; + return selectors; + } +} diff --git a/src/deployments/utils/libraries/RolesValidation.sol b/src/deployments/utils/libraries/RolesValidation.sol new file mode 100644 index 000000000..caad6d97e --- /dev/null +++ b/src/deployments/utils/libraries/RolesValidation.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +library RolesValidation { + function validateNonZeroAddress(address addr) internal pure { + require(addr != address(0), 'zero address'); + } +} diff --git a/src/libraries/types/Roles.sol b/src/libraries/types/Roles.sol deleted file mode 100644 index fd9be3c09..000000000 --- a/src/libraries/types/Roles.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-BUSL -pragma solidity ^0.8.20; - -/// @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 = 4; - uint64 public constant SPOKE_CONFIGURATOR_ROLE = 5; - uint64 public constant DEFICIT_ELIMINATOR_ROLE = 6; -} diff --git a/tests/Base.t.sol b/tests/Base.t.sol index fc75eec8c..4bf80fb34 100644 --- a/tests/Base.t.sol +++ b/tests/Base.t.sol @@ -28,7 +28,6 @@ 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 {SlotDerivation} from 'src/dependencies/openzeppelin/SlotDerivation.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'; @@ -39,7 +38,6 @@ import {IERC1967} from 'src/dependencies/openzeppelin/IERC1967.sol'; import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; import {MathUtils} from 'src/libraries/math/MathUtils.sol'; import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; -import {Roles} from 'src/libraries/types/Roles.sol'; import {Rescuable, IRescuable} from 'src/utils/Rescuable.sol'; import {NoncesKeyed, INoncesKeyed} from 'src/utils/NoncesKeyed.sol'; import {IntentConsumer, IIntentConsumer} from 'src/utils/IntentConsumer.sol'; @@ -69,6 +67,7 @@ import {PositionStatusMap} from 'src/spoke/libraries/PositionStatusMap.sol'; import {ReserveFlags, ReserveFlagsMap} from 'src/spoke/libraries/ReserveFlagsMap.sol'; import {LiquidationLogic} from 'src/spoke/libraries/LiquidationLogic.sol'; import {KeyValueList} from 'src/spoke/libraries/KeyValueList.sol'; +import {ISpokeInstance} from 'src/deployments/utils/interfaces/ISpokeInstance.sol'; // tokenization spoke import {TokenizationSpoke, ITokenizationSpoke} from 'src/spoke/TokenizationSpoke.sol'; @@ -100,9 +99,16 @@ import { // test import {Constants} from 'tests/Constants.sol'; -import {DeployUtils} from 'tests/DeployUtils.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 {AaveV4HubRolesProcedure} from 'src/deployments/procedures/roles/AaveV4HubRolesProcedure.sol'; +import {AaveV4SpokeRolesProcedure} from 'src/deployments/procedures/roles/AaveV4SpokeRolesProcedure.sol'; +import {AaveV4HubConfiguratorRolesProcedure} from 'src/deployments/procedures/roles/AaveV4HubConfiguratorRolesProcedure.sol'; +import {AaveV4SpokeConfiguratorRolesProcedure} from 'src/deployments/procedures/roles/AaveV4SpokeConfiguratorRolesProcedure.sol'; // mocks import {EIP712Types} from 'tests/mocks/EIP712Types.sol'; import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; @@ -120,12 +126,14 @@ import {MockTreasurySpokeInstance} from 'tests/mocks/MockTreasurySpokeInstance.s import {MockSkimSpoke} from 'tests/mocks/MockSkimSpoke.sol'; import {MockReentrantCaller} from 'tests/mocks/MockReentrantCaller.sol'; import {MockHubInstance} from 'tests/mocks/MockHubInstance.sol'; -import {IHubInstance} from 'tests/mocks/IHubInstance.sol'; -import {ISpokeInstance} from 'tests/mocks/ISpokeInstance.sol'; -import {DeployWrapper} from 'tests/mocks/DeployWrapper.sol'; +import {IHubInstance} from 'src/deployments/utils/interfaces/IHubInstance.sol'; +import {AaveV4TestOrchestrationWrapper} from 'tests/mocks/AaveV4TestOrchestrationWrapper.sol'; import {SpokeUtilsWrapper} from 'tests/mocks/SpokeUtilsWrapper.sol'; +import {BytecodeHelper} from 'src/deployments/utils/libraries/BytecodeHelper.sol'; + +import 'tests/utils/BatchTestProcedures.sol'; -abstract contract Base is Test { +abstract contract Base is BatchTestProcedures { using stdStorage for StdStorage; using WadRayMath for *; using SharesMath for uint256; @@ -134,24 +142,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; - bytes32 internal constant INITIALIZABLE_SLOT = - 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; - 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 = 0; uint24 internal constant MAX_COLLATERAL_RISK_BPS = 1000_00; @@ -173,6 +168,19 @@ abstract contract Base is Test { PercentageMath.PERCENTAGE_FACTOR; IHubBase.PremiumDelta internal ZERO_PREMIUM_DELTA = ZERO_PREMIUM_DELTA; + bytes32 internal constant ERC1967_ADMIN_SLOT = + 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + bytes32 internal constant IMPLEMENTATION_SLOT = + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + bytes32 internal constant INITIALIZABLE_SLOT = + 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; + + IHub[] internal _hubs; + ISpoke[] internal _spokes; + IAaveOracle[] internal _oracles; + IAssetInterestRateStrategy[] internal _irStrategies; + IAccessManager[] internal _accessManagers; + IAaveOracle internal oracle1; IAaveOracle internal oracle2; IAaveOracle internal oracle3; @@ -181,8 +189,10 @@ abstract contract Base is Test { ISpoke internal spoke1; ISpoke internal spoke2; ISpoke internal spoke3; - AssetInterestRateStrategy internal irStrategy; + IAssetInterestRateStrategy internal irStrategy; IAccessManager internal accessManager; + IHubConfigurator internal hubConfigurator; + ISpokeConfigurator internal spokeConfigurator; string internal constant ALICE = 'alice'; string internal constant BOB = 'bob'; @@ -206,10 +216,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 = makeAddr('HUB_CONFIGURATOR'); - address internal SPOKE_CONFIGURATOR = makeAddr('SPOKE_CONFIGURATOR'); + 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; @@ -224,6 +234,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 { @@ -235,15 +252,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; @@ -260,6 +268,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; @@ -316,9 +331,13 @@ 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% + baseDrawnRate: 5_00, // 5.00% + rateGrowthBeforeOptimal: 5_00, // 5.00% + rateGrowthAfterOptimal: 5_00 // 5.00% + }); function _getProxyAdminAddress(address proxy) internal view returns (address) { bytes32 slotData = vm.load(proxy, ERC1967_ADMIN_SLOT); @@ -335,194 +354,164 @@ abstract contract Base is Test { return uint64(uint256(slotData) & ((1 << 64) - 1)); } - function deployFixtures() internal virtual { - vm.startPrank(ADMIN); - accessManager = IAccessManager(address(new AccessManagerEnumerable(ADMIN))); - hub1 = DeployUtils.deployHub({authority: address(accessManager), proxyAdminOwner: ADMIN}); - irStrategy = new AssetInterestRateStrategy(address(hub1)); - (spoke1, oracle1) = _deploySpokeWithOracle(ADMIN, address(accessManager)); - (spoke2, oracle2) = _deploySpokeWithOracle(ADMIN, address(accessManager)); - (spoke3, oracle3) = _deploySpokeWithOracle(ADMIN, address(accessManager)); - TreasurySpokeInstance treasurySpokeImpl = new TreasurySpokeInstance(); - treasurySpoke = ITreasurySpoke( - DeployUtils.proxify( - address(treasurySpokeImpl), - ADMIN, - abi.encodeCall(TreasurySpokeInstance.initialize, (TREASURY_ADMIN)) - ) - ); - vm.stopPrank(); - - vm.label(address(spoke1), 'spoke1'); - vm.label(address(spoke2), 'spoke2'); - vm.label(address(spoke3), 'spoke3'); - - setUpRoles(hub1, spoke1, accessManager); - setUpRoles(hub1, spoke2, accessManager); - setUpRoles(hub1, spoke3, accessManager); - } - - 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); - - manager.grantRole(Roles.USER_POSITION_UPDATER_ROLE, SPOKE_ADMIN, 0); - manager.grantRole(Roles.USER_POSITION_UPDATER_ROLE, USER_POSITION_UPDATER, 0); - - manager.grantRole(Roles.HUB_CONFIGURATOR_ROLE, HUB_CONFIGURATOR, 0); - manager.grantRole(Roles.SPOKE_CONFIGURATOR_ROLE, SPOKE_CONFIGURATOR, 0); - - manager.grantRole(Roles.DEFICIT_ELIMINATOR_ROLE, HUB_ADMIN, 0); - manager.grantRole(Roles.DEFICIT_ELIMINATOR_ROLE, DEFICIT_ELIMINATOR, 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); - } + function setUp() public virtual override { + _etchSetup(); + _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 = IAssetInterestRateStrategy(report.hubReports[0].irStrategy); + treasurySpoke = ITreasurySpoke(report.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, + hubCount: numHubs, + spokeCount: numSpokes, + nativeWrapper: address(tokenList.weth), + hubBytecode: BytecodeHelper.getHubBytecode(), + spokeBytecode: BytecodeHelper.getSpokeBytecode(), + salt: bytes32(vm.randomBytes(32)) + }); + for (uint256 i; i < numHubs; ++i) { + _hubs.push(IHub(report.hubReports[i].hub)); + _irStrategies.push(IAssetInterestRateStrategy(report.hubReports[i].irStrategy)); - { - 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); + 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.treasurySpoke, 'treasurySpoke'); - { - 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); - } + for (uint256 i; i < numSpokes; ++i) { + _spokes.push(ISpoke(report.spokeReports[i].spoke)); + _oracles.push(IAaveOracle(report.spokeReports[i].aaveOracle)); - { - bytes4[] memory selectors = new bytes4[](1); - selectors[0] = IHub.eliminateDeficit.selector; - manager.setTargetFunctionRole(address(targetHub), selectors, Roles.DEFICIT_ELIMINATOR_ROLE); + 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)))); } - setUpHubConfiguratorRoles(HUB_CONFIGURATOR, address(manager)); - setUpSpokeConfiguratorRoles(SPOKE_CONFIGURATOR, address(manager)); + vm.label(report.configuratorReport.hubConfigurator, 'hubConfigurator'); + vm.label(report.configuratorReport.spokeConfigurator, 'spokeConfigurator'); - vm.stopPrank(); + return report; } - function setUpHubConfiguratorRoles(address hubConfigurator, address manager) internal { + function _setupFixturesRoles(TestTypes.TestEnvReport memory report) internal virtual { + if (report.accessManager == address(0)) report.accessManager = address(accessManager); + + // temporary grant admin role to address(this) to execute setAndGrantRolesTestEnv from its context vm.startPrank(ADMIN); + IAccessManager(report.accessManager).grantRole( + Roles.ACCESS_MANAGER_DEFAULT_ADMIN, + address(this), + 0 + ); + vm.stopPrank(); - // Grant HUB_ADMIN_ROLE so the configurator can call hub functions - IAccessManager(manager).grantRole(Roles.HUB_ADMIN_ROLE, hubConfigurator, 0); - - // Set up HubConfigurator function permissions - all functions callable by HUB_CONFIGURATOR_ROLE - bytes4[] memory selectors = new bytes4[](22); - selectors[0] = IHubConfigurator.updateLiquidityFee.selector; - selectors[1] = IHubConfigurator.updateFeeReceiver.selector; - selectors[2] = IHubConfigurator.updateFeeConfig.selector; - selectors[3] = IHubConfigurator.updateInterestRateStrategy.selector; - selectors[4] = IHubConfigurator.updateReinvestmentController.selector; - selectors[5] = IHubConfigurator.resetAssetCaps.selector; - selectors[6] = IHubConfigurator.deactivateAsset.selector; - selectors[7] = IHubConfigurator.haltAsset.selector; - selectors[8] = IHubConfigurator.addSpoke.selector; - selectors[9] = IHubConfigurator.addSpokeToAssets.selector; - selectors[10] = IHubConfigurator.updateSpokeActive.selector; - selectors[11] = IHubConfigurator.updateSpokeHalted.selector; - selectors[12] = IHubConfigurator.updateSpokeAddCap.selector; - selectors[13] = IHubConfigurator.updateSpokeDrawCap.selector; - selectors[14] = IHubConfigurator.updateSpokeRiskPremiumThreshold.selector; - selectors[15] = IHubConfigurator.updateSpokeCaps.selector; - selectors[16] = IHubConfigurator.deactivateSpoke.selector; - selectors[17] = IHubConfigurator.haltSpoke.selector; - selectors[18] = IHubConfigurator.resetSpokeCaps.selector; - selectors[19] = IHubConfigurator.updateInterestRateData.selector; - selectors[20] = IHubConfigurator.addAsset.selector; - selectors[21] = IHubConfigurator.addAssetWithDecimals.selector; - IAccessManager(manager).setTargetFunctionRole( - hubConfigurator, - selectors, - Roles.HUB_CONFIGURATOR_ROLE + AaveV4TestOrchestration.setRolesTestEnv(report); + AaveV4TestOrchestration.grantRolesTestEnv(report, ADMIN, HUB_ADMIN, SPOKE_ADMIN); + + // Grant HubConfigurator granular roles to HUB_CONFIGURATOR_ADMIN so it can call + // HubConfigurator functions (deactivateAsset, resetAssetCaps, haltAsset, etc.) + AaveV4HubConfiguratorRolesProcedure.grantHubConfiguratorAllRoles( + report.accessManager, + HUB_CONFIGURATOR_ADMIN ); - vm.stopPrank(); + // Grant SpokeConfigurator granular roles to SPOKE_CONFIGURATOR_ADMIN so it can call + // SpokeConfigurator functions (addReserve, updateMaxReserves, freezeReserve, etc.) + AaveV4SpokeConfiguratorRolesProcedure.grantSpokeConfiguratorAllRoles( + report.accessManager, + SPOKE_CONFIGURATOR_ADMIN + ); + + IAccessManager(report.accessManager).renounceRole( + Roles.ACCESS_MANAGER_DEFAULT_ADMIN, + address(this) + ); } - function setUpSpokeConfiguratorRoles(address spokeConfigurator, address manager) internal { + /// @dev Standalone role setup for a hub+spoke pair outside the main orchestration (e.g. upgrade tests). + function setUpRoles(IHub targetHub, ISpoke spoke, IAccessManager manager) internal virtual { vm.startPrank(ADMIN); + manager.grantRole(Roles.ACCESS_MANAGER_DEFAULT_ADMIN, address(this), 0); + vm.stopPrank(); - // Grant SPOKE_ADMIN_ROLE so the configurator can call spoke functions - IAccessManager(manager).grantRole(Roles.SPOKE_ADMIN_ROLE, spokeConfigurator, 0); - - // Set up SpokeConfigurator function permissions - all functions callable by SPOKE_CONFIGURATOR_ROLE - bytes4[] memory selectors = new bytes4[](24); - selectors[0] = ISpokeConfigurator.updateReservePriceSource.selector; - selectors[1] = ISpokeConfigurator.updateLiquidationTargetHealthFactor.selector; - selectors[2] = ISpokeConfigurator.updateHealthFactorForMaxBonus.selector; - selectors[3] = ISpokeConfigurator.updateLiquidationBonusFactor.selector; - selectors[4] = ISpokeConfigurator.updateLiquidationConfig.selector; - selectors[5] = ISpokeConfigurator.addReserve.selector; - selectors[6] = ISpokeConfigurator.updatePaused.selector; - selectors[7] = ISpokeConfigurator.updateFrozen.selector; - selectors[8] = ISpokeConfigurator.updateBorrowable.selector; - selectors[9] = ISpokeConfigurator.updateReceiveSharesEnabled.selector; - selectors[10] = ISpokeConfigurator.updateCollateralRisk.selector; - selectors[11] = ISpokeConfigurator.addCollateralFactor.selector; - selectors[12] = ISpokeConfigurator.updateCollateralFactor.selector; - selectors[13] = ISpokeConfigurator.addMaxLiquidationBonus.selector; - selectors[14] = ISpokeConfigurator.updateMaxLiquidationBonus.selector; - selectors[15] = ISpokeConfigurator.addLiquidationFee.selector; - selectors[16] = ISpokeConfigurator.updateLiquidationFee.selector; - selectors[17] = ISpokeConfigurator.addDynamicReserveConfig.selector; - selectors[18] = ISpokeConfigurator.updateDynamicReserveConfig.selector; - selectors[19] = ISpokeConfigurator.pauseAllReserves.selector; - selectors[20] = ISpokeConfigurator.freezeAllReserves.selector; - selectors[21] = ISpokeConfigurator.pauseReserve.selector; - selectors[22] = ISpokeConfigurator.freezeReserve.selector; - selectors[23] = ISpokeConfigurator.updatePositionManager.selector; - IAccessManager(manager).setTargetFunctionRole( - spokeConfigurator, - selectors, - Roles.SPOKE_CONFIGURATOR_ROLE - ); + AaveV4HubRolesProcedure.grantHubAllRoles(address(manager), ADMIN); + AaveV4HubRolesProcedure.grantHubAllRoles(address(manager), HUB_ADMIN); + AaveV4HubRolesProcedure.setupHubAllRoles(address(manager), address(targetHub)); - vm.stopPrank(); - } + AaveV4SpokeRolesProcedure.grantSpokeAllRoles(address(manager), ADMIN); + AaveV4SpokeRolesProcedure.grantSpokeAllRoles(address(manager), SPOKE_ADMIN); + AaveV4SpokeRolesProcedure.setupSpokeAllRoles(address(manager), address(spoke)); - function initEnvironment() internal { - deployMintAndApproveTokenList(); - configureTokenList(); + IAccessManager(address(manager)).renounceRole( + Roles.ACCESS_MANAGER_DEFAULT_ADMIN, + address(this) + ); } - 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) - ); + 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 + }); + + tokenList = AaveV4TestOrchestration.deployTestTokens(tokenInputs); vm.label(address(tokenList.weth), 'WETH'); vm.label(address(tokenList.usdx), 'USDX'); vm.label(address(tokenList.dai), 'DAI'); vm.label(address(tokenList.wbtc), 'WBTC'); vm.label(address(tokenList.usdy), 'USDY'); + vm.label(address(tokenList.usdz), 'USDZ'); MAX_SUPPLY_AMOUNT_USDX = MAX_SUPPLY_ASSET_UNITS * 10 ** tokenList.usdx.decimals(); MAX_SUPPLY_AMOUNT_WETH = MAX_SUPPLY_ASSET_UNITS * 10 ** tokenList.weth.decimals(); @@ -530,7 +519,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, @@ -541,13 +532,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); @@ -557,13 +541,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); + } + { + address spoke = address(treasurySpoke); + 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(); } @@ -597,7 +591,65 @@ 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 and configs 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; + + ReserveInfo storage info; + if (assetId == wethAssetId) { + info = spokeInfo[spoke].weth; + } else if (assetId == wbtcAssetId) { + info = spokeInfo[spoke].wbtc; + } else if (assetId == daiAssetId) { + info = spokeInfo[spoke].dai; + } else if (assetId == usdxAssetId) { + info = spokeInfo[spoke].usdx; + } else if (assetId == usdyAssetId) { + info = spokeInfo[spoke].usdy; + } else if (assetId == usdzAssetId) { + info = spokeInfo[spoke].usdz; + } else { + continue; + } + + info.reserveId = reserveId; + info.reserveConfig = spoke.getReserveConfig(reserveId); + info.dynReserveConfig = _getLatestDynamicReserveConfig(spoke, reserveId); + } + } + + function _getAddSpokeParams() + internal + view + returns (ConfigData.AddSpokeParams[] memory paramsList) + { IHub.SpokeConfig memory spokeConfig = IHub.SpokeConfig({ active: true, halted: false, @@ -605,373 +657,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% - baseDrawnRate: 5_00, // 5.00% - rateGrowthBeforeOptimal: 5_00, // 5.00% - rateGrowthAfterOptimal: 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: @@ -980,65 +1055,35 @@ abstract contract Base is Test { * 2: DAI * 3: WBTC */ - function hub2Fixture() internal returns (IHub, AssetInterestRateStrategy) { - IAccessManager accessManager2 = IAccessManager(address(new AccessManagerEnumerable(ADMIN))); - IHub hub2 = DeployUtils.deployHub({authority: address(accessManager2), proxyAdminOwner: ADMIN}); - 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% - baseDrawnRate: 5_00, // 5.00% - rateGrowthBeforeOptimal: 5_00, // 5.00% - rateGrowthAfterOptimal: 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 - ); - - // Add WBTC - hub2.addAsset( - address(tokenList.wbtc), - tokenList.wbtc.decimals(), - address(treasurySpoke), - address(hub2IrStrategy), - encodedIrData - ); - vm.stopPrank(); - - setUpRoles(hub2, spoke1, accessManager2); + function _hub2Fixture() internal returns (IHub, IAssetInterestRateStrategy) { + 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) + }); - return (hub2, hub2IrStrategy); + TestTypes.TestHubReport memory report = _addHubFixture('2', assetsList); + return (IHub(report.hub), IAssetInterestRateStrategy(report.irStrategy)); } /* @dev Configures Hub 3 with the following assetIds: @@ -1047,63 +1092,104 @@ abstract contract Base is Test { * 2: WBTC * 3: WETH */ - function hub3Fixture() internal returns (IHub, AssetInterestRateStrategy) { - IAccessManager accessManager3 = IAccessManager(address(new AccessManagerEnumerable(ADMIN))); - IHub hub3 = DeployUtils.deployHub({authority: address(accessManager3), proxyAdminOwner: ADMIN}); - AssetInterestRateStrategy hub3IrStrategy = new AssetInterestRateStrategy(address(hub3)); - - // Configure IR Strategy for hub 3 - bytes memory encodedIrData = abi.encode( - IAssetInterestRateStrategy.InterestRateData({ - optimalUsageRatio: 90_00, // 90.00% - baseDrawnRate: 5_00, // 5.00% - rateGrowthBeforeOptimal: 5_00, // 5.00% - rateGrowthAfterOptimal: 5_00 // 5.00% - }) - ); + function _hub3Fixture() internal returns (IHub, IAssetInterestRateStrategy) { + 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) + }); - vm.startPrank(ADMIN); - // Add DAI - hub3.addAsset( - address(tokenList.dai), - tokenList.dai.decimals(), - address(treasurySpoke), - address(hub3IrStrategy), - encodedIrData - ); + TestTypes.TestHubReport memory report = _addHubFixture('3', assetsList); + return (IHub(report.hub), IAssetInterestRateStrategy(report.irStrategy)); + } - // Add USDX - hub3.addAsset( - address(tokenList.usdx), - tokenList.usdx.decimals(), - address(treasurySpoke), - address(hub3IrStrategy), - encodedIrData + function _addHubFixture( + string memory label, + FixtureAssetList[] memory assetsList + ) internal returns (TestTypes.TestHubReport memory report) { + report = AaveV4TestOrchestration.deployTestHub( + ADMIN, + address(accessManager), + BytecodeHelper.getHubBytecode(), + label, + keccak256(abi.encodePacked(label)) ); + _hubs.push(IHub(report.hub)); + _irStrategies.push(IAssetInterestRateStrategy(report.irStrategy)); - // Add WBTC - hub3.addAsset( - address(tokenList.wbtc), - tokenList.wbtc.decimals(), - address(treasurySpoke), - address(hub3IrStrategy), - encodedIrData - ); + vm.label(report.hub, string.concat('Hub', label)); + vm.label(report.irStrategy, string.concat('IrStrategy', label)); - // Add WETH - hub3.addAsset( - address(tokenList.weth), - tokenList.weth.decimals(), - address(treasurySpoke), - address(hub3IrStrategy), - encodedIrData + ConfigData.AddAssetParams[] memory assetParams = new ConfigData.AddAssetParams[]( + assetsList.length ); + for (uint256 i; i < assetsList.length; ++i) { + assetParams[i] = ConfigData.AddAssetParams({ + hub: report.hub, + underlying: address(assetsList[i].underlying), + decimals: assetsList[i].underlying.decimals(), + feeReceiver: address(treasurySpoke), + liquidityFee: assetsList[i].liquidityFee, + irStrategy: report.irStrategy, + irData: assetsList[i].irData, + reinvestmentController: assetsList[i].reinvestmentController + }); + } + vm.startPrank(ADMIN); + accessManager.grantRole(Roles.ACCESS_MANAGER_DEFAULT_ADMIN, address(this), 0); + accessManager.grantRole(Roles.HUB_CONFIGURATOR_ROLE, address(this), 0); + + AaveV4TestOrchestration.setupHubRolesTestEnv(report, address(accessManager)); + vm.stopPrank(); + + AaveV4TestOrchestration.configureHubsAssets(assetParams); + + // Renounce temporary roles + accessManager.renounceRole(Roles.ACCESS_MANAGER_DEFAULT_ADMIN, address(this)); + accessManager.renounceRole(Roles.HUB_CONFIGURATOR_ROLE, address(this)); + + 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( @@ -1453,13 +1539,13 @@ abstract contract Base is Test { function grantDeficitEliminatorRole(IHub hub, address target) internal pausePrank { IAccessManager manager = IAccessManager(hub.authority()); vm.prank(ADMIN); - manager.grantRole(Roles.DEFICIT_ELIMINATOR_ROLE, target, 0); + manager.grantRole(Roles.HUB_DEFICIT_ELIMINATOR_ROLE, target, 0); } function revokeDeficitEliminatorRole(IHub hub, address target) internal pausePrank { IAccessManager manager = IAccessManager(hub.authority()); vm.prank(ADMIN); - manager.revokeRole(Roles.DEFICIT_ELIMINATOR_ROLE, target); + manager.revokeRole(Roles.HUB_DEFICIT_ELIMINATOR_ROLE, target); } function getUserInfo( @@ -2145,16 +2231,16 @@ abstract contract Base is Test { new MockSpoke(spoke.ORACLE(), Constants.MAX_ALLOWED_USER_RESERVES_LIMIT) ); - 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); ISpoke.UserAccountData memory userAccountData = MockSpoke(address(spoke)) .calculateUserAccountData(user, refreshConfig); - vm.prank(_getProxyAdminAddress(address(spoke))); + vm.prank(ProxyHelper.getProxyAdmin(address(spoke))); ITransparentUpgradeableProxy(address(spoke)).upgradeToAndCall(implementation, ''); vm.revertToState(snapshot); @@ -2373,10 +2459,10 @@ abstract contract Base is Test { user != address(spoke1) && user != address(spoke2) && user != address(spoke3) && - user != _getProxyAdminAddress(address(hub1)) && - user != _getProxyAdminAddress(address(spoke1)) && - user != _getProxyAdminAddress(address(spoke2)) && - user != _getProxyAdminAddress(address(spoke3)) + user != ProxyHelper.getProxyAdmin(address(hub1)) && + user != ProxyHelper.getProxyAdmin(address(spoke1)) && + user != ProxyHelper.getProxyAdmin(address(spoke2)) && + user != ProxyHelper.getProxyAdmin(address(spoke3)) ); } @@ -2438,6 +2524,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)); } @@ -2503,28 +2593,17 @@ abstract contract Base is Test { function _deploySpokeWithOracle( address proxyAdminOwner, - address _accessManager, + address accessManager_, uint16 maxUserReservesLimit ) internal pausePrank returns (ISpoke, IAaveOracle) { - address deployer = makeAddr('deployer'); - - vm.startPrank(deployer); - IAaveOracle oracle = new AaveOracle(8); - - ISpoke spoke = DeployUtils.deploySpoke( - address(oracle), - maxUserReservesLimit, - proxyAdminOwner, - abi.encodeCall(ISpokeInstance.initialize, (_accessManager)) - ); - - oracle.setSpoke(address(spoke)); - vm.stopPrank(); - - assertEq(spoke.ORACLE(), address(oracle)); - assertEq(oracle.spoke(), address(spoke)); - - return (spoke, oracle); + TestTypes.TestSpokeReport memory report = AaveV4TestOrchestration.deployTestSpoke({ + spokeProxyAdminOwner: proxyAdminOwner, + accessManager: accessManager_, + spokeBytecode: BytecodeHelper.getSpokeBytecode(), + maxUserReservesLimit: maxUserReservesLimit, + salt: keccak256(abi.encodePacked('spoke-', vm.randomBytes(32))) + }); + return (ISpoke(report.spoke), IAaveOracle(report.aaveOracle)); } function _deployTokenizationSpoke( @@ -2534,17 +2613,17 @@ abstract contract Base is Test { string memory shareSymbol, address proxyAdminOwner ) internal pausePrank returns (ITokenizationSpoke) { - address tokenizationSpokeImpl = address( - new TokenizationSpokeInstance(address(hub), underlying) - ); - ITokenizationSpoke tokenizationSpoke = ITokenizationSpoke( - DeployUtils.proxify( - tokenizationSpokeImpl, - proxyAdminOwner, - abi.encodeCall(TokenizationSpokeInstance.initialize, (shareName, shareSymbol)) - ) - ); - return tokenizationSpoke; + return + ITokenizationSpoke( + AaveV4TestOrchestration.deployTestTokenizationSpoke({ + hub: address(hub), + underlying: underlying, + spokeProxyAdminOwner: proxyAdminOwner, + shareName: shareName, + shareSymbol: shareSymbol, + salt: keccak256(abi.encodePacked('tokenization-spoke-', vm.randomBytes(32))) + }) + ); } function _registerTokenizationSpoke( diff --git a/tests/Constants.sol b/tests/Constants.sol index d58ef84e0..8c4bb81f8 100644 --- a/tests/Constants.sol +++ b/tests/Constants.sol @@ -1,7 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +import {DeployConstants} from 'src/deployments/utils/libraries/DeployConstants.sol'; + 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; @@ -11,13 +15,16 @@ library Constants { uint256 public constant VIRTUAL_SHARES = 1e6; /// @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% uint256 public constant MAX_ALLOWED_DYNAMIC_CONFIG_KEY = type(uint32).max; uint256 public constant MAX_ALLOWED_ASSET_ID = type(uint16).max; - uint16 public constant MAX_ALLOWED_USER_RESERVES_LIMIT = type(uint16).max; + uint16 public constant MAX_ALLOWED_USER_RESERVES_LIMIT = + DeployConstants.MAX_ALLOWED_USER_RESERVES_LIMIT; + + /// @dev AaveOracle Constants + uint8 public constant ORACLE_DECIMALS = DeployConstants.ORACLE_DECIMALS; /// @dev AssetInterestRateStrategy Constants uint256 internal constant MAX_ALLOWED_DRAWN_RATE = 1000_00; // 1000.00% in BPS diff --git a/tests/Create2Utils.sol b/tests/Create2Utils.sol deleted file mode 100644 index ff8f807d7..000000000 --- a/tests/Create2Utils.sol +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import {Vm} from 'forge-std/Vm.sol'; - -library Create2Utils { - error NoCreate2Factory(); - error Create2DeploymentFailed(); - - // https://github.com/safe-global/safe-singleton-factory - address public constant CREATE2_FACTORY = 0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7; - bytes internal constant CREATE2_FACTORY_BYTECODE = - hex'7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3'; - - Vm internal constant vm = Vm(address(uint160(uint256(keccak256('hevm cheat code'))))); - - function loadCreate2Factory() internal { - if (_isContractDeployed(CREATE2_FACTORY)) { - return; - } - vm.etch(CREATE2_FACTORY, CREATE2_FACTORY_BYTECODE); - } - - function create2Deploy(bytes32 salt, bytes memory bytecode) internal returns (address) { - require(_isContractDeployed(CREATE2_FACTORY), NoCreate2Factory()); - - address computed = computeCreate2Address(salt, bytecode); - - if (_isContractDeployed(computed)) { - return computed; - } else { - bytes memory creationBytecode = abi.encodePacked(salt, bytecode); - bytes memory returnData; - (, returnData) = CREATE2_FACTORY.call(creationBytecode); - - address deployedAt = address(uint160(bytes20(returnData))); - require(deployedAt == computed, Create2DeploymentFailed()); - - return deployedAt; - } - } - - function _isContractDeployed(address instance) internal view returns (bool) { - return (instance.code.length > 0); - } - - function computeCreate2Address( - bytes32 salt, - bytes32 initcodeHash - ) internal pure returns (address) { - return - address( - uint160( - uint256(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))); - } -} diff --git a/tests/DeployUtils.sol b/tests/DeployUtils.sol deleted file mode 100644 index 29f84f4f9..000000000 --- a/tests/DeployUtils.sol +++ /dev/null @@ -1,130 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import {Vm} from 'forge-std/Vm.sol'; -import {TransparentUpgradeableProxy} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; -import {IHub} from 'src/hub/interfaces/IHub.sol'; -import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; -import {IHubInstance} from 'tests/mocks/IHubInstance.sol'; -import {ISpokeInstance} from 'tests/mocks/ISpokeInstance.sol'; -import {Create2Utils} from 'tests/Create2Utils.sol'; - -library DeployUtils { - Vm internal constant vm = Vm(address(uint160(uint256(keccak256('hevm cheat code'))))); - - function deploySpokeImplementation( - address oracle, - uint16 maxUserReservesLimit - ) internal returns (ISpokeInstance) { - return deploySpokeImplementation(oracle, maxUserReservesLimit, ''); - } - - function deploySpokeImplementation( - address oracle, - uint16 maxUserReservesLimit, - bytes32 salt - ) internal returns (ISpokeInstance spoke) { - Create2Utils.loadCreate2Factory(); - return - ISpokeInstance( - Create2Utils.create2Deploy(salt, _getSpokeInstanceInitCode(oracle, maxUserReservesLimit)) - ); - } - - function deploySpoke( - address oracle, - uint16 maxUserReservesLimit, - address proxyAdminOwner, - bytes memory initData - ) internal returns (ISpoke) { - return - ISpoke( - proxify( - address(deploySpokeImplementation(oracle, maxUserReservesLimit, '')), - proxyAdminOwner, - initData - ) - ); - } - - function getDeterministicSpokeInstanceAddress( - address oracle, - uint16 maxUserReservesLimit - ) internal returns (address) { - return getDeterministicSpokeInstanceAddress(oracle, maxUserReservesLimit, ''); - } - - function getDeterministicSpokeInstanceAddress( - address oracle, - uint16 maxUserReservesLimit, - bytes32 salt - ) internal returns (address) { - bytes32 initCodeHash = keccak256(_getSpokeInstanceInitCode(oracle, maxUserReservesLimit)); - - Create2Utils.loadCreate2Factory(); - return Create2Utils.computeCreate2Address(salt, initCodeHash); - } - - function deployHubImplementation() internal returns (IHubInstance) { - return deployHubImplementation(''); - } - - function deployHubImplementation(bytes32 salt) internal returns (IHubInstance) { - Create2Utils.loadCreate2Factory(); - return IHubInstance(Create2Utils.create2Deploy(salt, _getHubInstanceInitCode())); - } - - function deployHub(address authority, address proxyAdminOwner) internal returns (IHub) { - return - IHub( - proxify( - address(deployHubImplementation()), - proxyAdminOwner, - abi.encodeCall(IHubInstance.initialize, (authority)) - ) - ); - } - - function deployHub( - address authority, - address proxyAdminOwner, - bytes32 salt - ) internal returns (IHub) { - return - IHub( - proxify( - address(deployHubImplementation(salt)), - proxyAdminOwner, - abi.encodeCall(IHubInstance.initialize, (authority)) - ) - ); - } - - function proxify( - address impl, - address proxyAdminOwner, - bytes memory initData - ) internal returns (address) { - TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( - impl, - proxyAdminOwner, - initData - ); - return address(proxy); - } - - function _getSpokeInstanceInitCode( - address oracle, - uint16 maxUserReservesLimit - ) internal view returns (bytes memory) { - return - abi.encodePacked( - vm.getCode('src/spoke/instances/SpokeInstance.sol:SpokeInstance'), - abi.encode(oracle, maxUserReservesLimit) - ); - } - - function _getHubInstanceInitCode() internal view returns (bytes memory) { - return vm.getCode('src/hub/instances/HubInstance.sol:HubInstance'); - } -} diff --git a/tests/deployments/AaveV4BatchDeployment.t.sol b/tests/deployments/AaveV4BatchDeployment.t.sol new file mode 100644 index 000000000..70a0eb845 --- /dev/null +++ b/tests/deployments/AaveV4BatchDeployment.t.sol @@ -0,0 +1,471 @@ +// 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'), + hubConfiguratorAdmin: makeAddr('hubConfiguratorAdmin'), + hubAdmin: makeAddr('hubAdmin'), + treasurySpokeOwner: makeAddr('treasurySpokeOwner'), + hubProxyAdminOwner: makeAddr('hubProxyAdminOwner'), + spokeProxyAdminOwner: makeAddr('spokeProxyAdminOwner'), + spokeConfiguratorAdmin: makeAddr('spokeConfiguratorAdmin'), + spokeAdmin: makeAddr('spokeAdmin'), + gatewayOwner: makeAddr('gatewayOwner'), + positionManagerOwner: makeAddr('positionManagerOwner'), + nativeWrapper: _weth9, + deployNativeTokenGateway: true, + deploySignatureGateway: true, + deployPositionManagers: true, + grantRoles: true, + hubLabels: _hubLabels, + spokeLabels: _spokeLabels, + spokeMaxReservesLimits: _defaultSpokeMaxReservesLimits(_spokeLabels.length), + salt: bytes32(0) + }); + } + + function testAaveV4BatchDeployment() public { + checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withoutRoles() public { + _inputs.grantRoles = false; + checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withoutGateways() public { + _inputs.deployNativeTokenGateway = false; + _inputs.deploySignatureGateway = false; + checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withoutNativeTokenGateway() public { + _inputs.deployNativeTokenGateway = false; + _inputs.deploySignatureGateway = true; + checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withoutSignatureGateway() public { + _inputs.deployNativeTokenGateway = true; + _inputs.deploySignatureGateway = false; + checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withoutHubs() public { + _inputs.hubLabels = new string[](0); + checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withoutSpokes() public { + _inputs.spokeLabels = new string[](0); + _inputs.spokeMaxReservesLimits = new uint16[](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('zero address'); + this.checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withZeroAccessManagerAdmin_withoutRoles() public { + _inputs.accessManagerAdmin = address(0); + _inputs.grantRoles = false; + + checkedV4Deployment(); + } + + /// @dev Only reverts when grantRoles is true, as hubConfiguratorAdmin is + /// now used to grant configurator roles, not as authority + function testAaveV4BatchDeployment_fuzz_withZeroHubConfiguratorAdmin(bool grantRoles) public { + _inputs.hubConfiguratorAdmin = address(0); + _inputs.grantRoles = grantRoles; + + if (grantRoles && _inputs.hubLabels.length > 0) { + vm.expectRevert('zero address'); + this.checkedV4Deployment(); + } else { + checkedV4Deployment(); + } + } + + /// @dev Reverts as treasurySpoke is always deployed and owner is required + function testAaveV4BatchDeployment_fuzz_withZeroTreasurySpokeOwner(bool grantRoles) public { + _inputs.treasurySpokeOwner = address(0); + _inputs.grantRoles = grantRoles; + + (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); + _inputs.spokeMaxReservesLimits = new uint16[](0); + } + + (bool isExpectedError, bytes memory errorMessage) = _getExpectedError(); + if (isExpectedError) { + vm.expectRevert(errorMessage); + this.checkedV4Deployment(); + } else { + checkedV4Deployment(); + } + } + + /// @dev Only reverts when grantRoles is true, as spokeConfiguratorAdmin is + /// now used to grant configurator roles, not as authority + function testAaveV4BatchDeployment_fuzz_withZeroSpokeConfiguratorAdmin(bool grantRoles) public { + _inputs.spokeConfiguratorAdmin = address(0); + _inputs.grantRoles = grantRoles; + + if (grantRoles && _inputs.spokeLabels.length > 0) { + vm.expectRevert('zero address'); + this.checkedV4Deployment(); + } else { + checkedV4Deployment(); + } + } + + function testAaveV4BatchDeployment_withZeroHubAdmin_withRoles_reverts() public { + _inputs.hubAdmin = address(0); + _inputs.grantRoles = true; + + vm.expectRevert('zero address'); + this.checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withZeroHubAdmin_withoutRoles() public { + _inputs.hubAdmin = address(0); + _inputs.grantRoles = false; + + checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withZeroSpokeAdmin_withRoles_reverts() public { + _inputs.spokeAdmin = address(0); + _inputs.grantRoles = true; + + vm.expectRevert('zero address'); + this.checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withZeroSpokeAdmin_withoutRoles() public { + _inputs.spokeAdmin = address(0); + _inputs.grantRoles = false; + + checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withZeroGatewayOwner_withGateways_reverts() public { + _inputs.gatewayOwner = address(0); + _inputs.deployNativeTokenGateway = true; + _inputs.deploySignatureGateway = true; + + vm.expectRevert('invalid owner'); + this.checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withZeroGatewayOwner_withoutGateways() public { + _inputs.gatewayOwner = address(0); + _inputs.deployNativeTokenGateway = false; + _inputs.deploySignatureGateway = false; + + checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withZeroNativeWrapper_withNativeGateway_reverts() public { + _inputs.nativeWrapper = address(0); + _inputs.deployNativeTokenGateway = true; + + vm.expectRevert('invalid native wrapper'); + this.checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withZeroNativeWrapper_withoutNativeGateway() public { + _inputs.nativeWrapper = address(0); + _inputs.deployNativeTokenGateway = false; + + checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withZeroPositionManagerOwner_withPositionManagers_reverts() + public + { + _inputs.positionManagerOwner = address(0); + _inputs.deployPositionManagers = true; + + vm.expectRevert('invalid owner'); + this.checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withZeroPositionManagerOwner_withoutPositionManagers() public { + _inputs.positionManagerOwner = address(0); + _inputs.deployPositionManagers = false; + + checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withoutPositionManagers() public { + _inputs.deployPositionManagers = false; + checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withEmptySpokeMaxReservesLimits_usesDefaults() public { + _inputs.spokeMaxReservesLimits = new uint16[](0); + checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withMismatchedSpokeMaxReservesLimits_reverts() public { + _inputs.spokeMaxReservesLimits = new uint16[](1); + _inputs.spokeMaxReservesLimits[0] = 128; + + vm.expectRevert('spoke labels/limits length mismatch'); + this.checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_accessManagerAdminTransfer() public { + address newAdmin = makeAddr('newAccessManagerAdmin'); + _inputs.accessManagerAdmin = newAdmin; + + bytes memory hubBytecode = BytecodeHelper.getHubBytecode(); + bytes memory spokeBytecode = BytecodeHelper.getSpokeBytecode(); + + vm.startPrank(_deployer); + OrchestrationReports.FullDeploymentReport memory report = AaveV4DeployOrchestration + .deployAaveV4(_logger, _deployer, _inputs, hubBytecode, spokeBytecode); + vm.stopPrank(); + + IAccessManagerEnumerable accessManager = IAccessManagerEnumerable( + report.authorityBatchReport.accessManager + ); + + // newAdmin has DEFAULT_ADMIN_ROLE + (bool newAdminHasRole, ) = accessManager.hasRole(Roles.ACCESS_MANAGER_DEFAULT_ADMIN, newAdmin); + assertTrue(newAdminHasRole, 'new admin should have DEFAULT_ADMIN_ROLE'); + + // deployer no longer has DEFAULT_ADMIN_ROLE + (bool deployerHasRole, ) = accessManager.hasRole(Roles.ACCESS_MANAGER_DEFAULT_ADMIN, _deployer); + assertFalse(deployerHasRole, 'deployer should not have DEFAULT_ADMIN_ROLE after transfer'); + + // exactly one admin + assertEq( + accessManager.getRoleMemberCount(Roles.ACCESS_MANAGER_DEFAULT_ADMIN), + 1, + 'should have exactly one DEFAULT_ADMIN' + ); + assertEq( + accessManager.getRoleMember(Roles.ACCESS_MANAGER_DEFAULT_ADMIN, 0), + newAdmin, + 'sole admin should be newAdmin' + ); + } + + function testAaveV4BatchDeployment_accessManagerAdminSameAsDeployer() public { + _inputs.accessManagerAdmin = _deployer; + + bytes memory hubBytecode = BytecodeHelper.getHubBytecode(); + bytes memory spokeBytecode = BytecodeHelper.getSpokeBytecode(); + + vm.startPrank(_deployer); + OrchestrationReports.FullDeploymentReport memory report = AaveV4DeployOrchestration + .deployAaveV4(_logger, _deployer, _inputs, hubBytecode, spokeBytecode); + vm.stopPrank(); + + IAccessManagerEnumerable accessManager = IAccessManagerEnumerable( + report.authorityBatchReport.accessManager + ); + + (bool deployerHasRole, ) = accessManager.hasRole(Roles.ACCESS_MANAGER_DEFAULT_ADMIN, _deployer); + assertTrue(deployerHasRole, 'deployer should retain DEFAULT_ADMIN_ROLE'); + assertEq( + accessManager.getRoleMemberCount(Roles.ACCESS_MANAGER_DEFAULT_ADMIN), + 1, + 'should have exactly one DEFAULT_ADMIN' + ); + } + + function testAaveV4BatchDeployment_withZeroDeployer_reverts() public { + _deployer = address(0); + + vm.expectRevert('invalid admin'); + this.checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_fuzz_withoutRoles( + FullDeployInputs memory deployInputs, + address deployer, + bool withoutHubs, + bool withoutSpokes, + bool deployNativeTokenGateway, + bool deploySignatureGateway + ) public { + deployInputs.grantRoles = false; + deployInputs.deployNativeTokenGateway = deployNativeTokenGateway; + deployInputs.deploySignatureGateway = deploySignatureGateway; + if (withoutHubs) { + deployInputs.hubLabels = new string[](0); + } else { + deployInputs.hubLabels = _inputs.hubLabels; + } + if (withoutSpokes) { + deployInputs.spokeLabels = new string[](0); + deployInputs.spokeMaxReservesLimits = new uint16[](0); + } else { + deployInputs.spokeLabels = _inputs.spokeLabels; + deployInputs.spokeMaxReservesLimits = _inputs.spokeMaxReservesLimits; + } + _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 deployNativeTokenGateway, + bool deploySignatureGateway + ) public { + deployInputs.grantRoles = true; + deployInputs.deployNativeTokenGateway = deployNativeTokenGateway; + deployInputs.deploySignatureGateway = deploySignatureGateway; + if (withoutHubs) { + deployInputs.hubLabels = new string[](0); + } else { + deployInputs.hubLabels = _inputs.hubLabels; + } + if (withoutSpokes) { + deployInputs.spokeLabels = new string[](0); + deployInputs.spokeMaxReservesLimits = new uint16[](0); + } else { + deployInputs.spokeLabels = _inputs.spokeLabels; + deployInputs.spokeMaxReservesLimits = _inputs.spokeMaxReservesLimits; + } + _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.hubConfiguratorAdmin, address(0)); + assertNotEq(deployInputs.treasurySpokeOwner, address(0)); + assertNotEq(deployInputs.hubProxyAdminOwner, address(0)); + assertNotEq(deployInputs.spokeProxyAdminOwner, address(0)); + assertNotEq(deployInputs.spokeConfiguratorAdmin, address(0)); + assertNotEq(deployInputs.gatewayOwner, address(0)); + assertNotEq(deployInputs.positionManagerOwner, address(0)); + assertNotEq(deployInputs.hubAdmin, address(0)); + assertNotEq(deployInputs.spokeAdmin, address(0)); + + _inputs = deployInputs; + checkedV4Deployment(); + } + + /// @dev Predicts the first revert error based on execution order in deployAaveV4: + /// 1. AuthorityBatch (deployer as initial admin) + /// 2. ConfiguratorBatch + /// 3. TreasurySpokeBatch (treasurySpokeOwner) + /// 4. Hubs + /// 5. Spokes (spokeProxyAdminOwner) + /// 6. Gateways (gatewayOwner, nativeWrapper) + /// 7. PositionManagers (positionManagerOwner) + /// 8. Roles (hubAdmin, hubConfiguratorAdmin, spokeAdmin, spokeConfiguratorAdmin, accessManagerAdmin) + function _getExpectedError() + internal + view + returns (bool isExpectedError, bytes memory errorMessage) + { + // 1. deployer is initial admin for access manager + if (_deployer == address(0)) return (true, bytes('invalid admin')); + + // 2. treasury spoke requires owner + if (_inputs.treasurySpokeOwner == address(0)) { + return (true, bytes('invalid owner')); + } + + // 3. hubs require proxy admin owner when deployed + if (_inputs.hubLabels.length > 0 && _inputs.hubProxyAdminOwner == address(0)) { + return (true, bytes('invalid hub proxy admin owner')); + } + + // 4. spokes require proxy admin owner when deployed + if (_inputs.spokeLabels.length > 0 && _inputs.spokeProxyAdminOwner == address(0)) { + return (true, bytes('invalid spoke proxy admin owner')); + } + + // 4. gateways: native gateway checks nativeWrapper, then owner; + // signature gateway checks owner + if (_inputs.deployNativeTokenGateway && _inputs.nativeWrapper == address(0)) { + return (true, bytes('invalid native wrapper')); + } + if ( + (_inputs.deployNativeTokenGateway || _inputs.deploySignatureGateway) && + _inputs.gatewayOwner == address(0) + ) { + return (true, bytes('invalid owner')); + } + + // 5. position managers require owner when deployed + if (_inputs.deployPositionManagers && _inputs.positionManagerOwner == address(0)) { + return (true, bytes('invalid owner')); + } + + if (_inputs.grantRoles) { + bool hasHubs = _inputs.hubLabels.length > 0; + bool hasSpokes = _inputs.spokeLabels.length > 0; + + if ( + (hasHubs && + (_inputs.hubAdmin == address(0) || _inputs.hubConfiguratorAdmin == address(0))) || + (hasSpokes && + (_inputs.spokeAdmin == address(0) || _inputs.spokeConfiguratorAdmin == address(0))) || + _inputs.accessManagerAdmin == address(0) + ) { + return (true, bytes('zero address')); + } + } + } +} diff --git a/tests/deployments/batches/AaveV4AuthorityBatch.t.sol b/tests/deployments/batches/AaveV4AuthorityBatch.t.sol new file mode 100644 index 000000000..a9607ae39 --- /dev/null +++ b/tests/deployments/batches/AaveV4AuthorityBatch.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/batches/BatchBase.t.sol'; + +contract AaveV4AuthorityBatchTest is BatchBaseTest { + AaveV4AuthorityBatch public aaveV4AuthorityBatch; + function setUp() public override { + super.setUp(); + bytes32 accessSalt = keccak256('authorityBatchSalt'); + aaveV4AuthorityBatch = new AaveV4AuthorityBatch({admin_: admin, salt_: accessSalt}); + } + + function test_getReport() public view { + BatchReports.AuthorityBatchReport memory report = aaveV4AuthorityBatch.getReport(); + assertNotEq(report.accessManager, address(0)); + + (bool hasRole, uint32 executionDelay) = IAccessManagerEnumerable(report.accessManager).hasRole( + Roles.ACCESS_MANAGER_DEFAULT_ADMIN, + admin + ); + assertTrue(hasRole); + assertEq(executionDelay, 0); + } + + function test_revert_zeroAdmin() public { + vm.expectRevert('invalid admin'); + new AaveV4AuthorityBatch({admin_: address(0), salt_: keccak256('zeroAdminSalt')}); + } + + function test_adminRoleMemberTracking() public view { + IAccessManagerEnumerable am = IAccessManagerEnumerable( + aaveV4AuthorityBatch.getReport().accessManager + ); + assertEq(am.getRoleMemberCount(Roles.ACCESS_MANAGER_DEFAULT_ADMIN), 1); + assertEq(am.getRoleMember(Roles.ACCESS_MANAGER_DEFAULT_ADMIN, 0), admin); + } + + function test_noOtherRolesInitialized() public view { + IAccessManagerEnumerable am = IAccessManagerEnumerable( + aaveV4AuthorityBatch.getReport().accessManager + ); + assertEq(am.getRoleCount(), 0); + } + + function test_differentSaltProducesDifferentAddress() public { + AaveV4AuthorityBatch newBatch = new AaveV4AuthorityBatch({ + admin_: admin, + salt_: keccak256('differentSalt') + }); + assertNotEq(aaveV4AuthorityBatch.getReport().accessManager, newBatch.getReport().accessManager); + } +} diff --git a/tests/deployments/batches/AaveV4ConfiguratorBatch.t.sol b/tests/deployments/batches/AaveV4ConfiguratorBatch.t.sol new file mode 100644 index 000000000..b36baead8 --- /dev/null +++ b/tests/deployments/batches/AaveV4ConfiguratorBatch.t.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/batches/BatchBase.t.sol'; + +contract AaveV4ConfiguratorBatchTest is BatchBaseTest { + AaveV4ConfiguratorBatch public configuratorBatch; + BatchReports.ConfiguratorBatchReport public report; + + function setUp() public override { + super.setUp(); + configuratorBatch = new AaveV4ConfiguratorBatch({ + hubConfiguratorAuthority_: accessManager, + spokeConfiguratorAuthority_: accessManager, + salt_: salt + }); + report = configuratorBatch.getReport(); + } + + function test_getReport() public view { + assertNotEq(report.hubConfigurator, address(0)); + assertNotEq(report.spokeConfigurator, address(0)); + } + + function test_hubConfiguratorAuthority() public view { + assertEq(IAccessManaged(report.hubConfigurator).authority(), accessManager); + } + + function test_spokeConfiguratorAuthority() public view { + assertEq(IAccessManaged(report.spokeConfigurator).authority(), accessManager); + } + + function test_revert_zeroHubConfiguratorAuthority() public { + vm.expectRevert('invalid authority'); + new AaveV4ConfiguratorBatch({ + hubConfiguratorAuthority_: address(0), + spokeConfiguratorAuthority_: accessManager, + salt_: salt + }); + } + + function test_revert_zeroSpokeConfiguratorAuthority() public { + vm.expectRevert('invalid authority'); + new AaveV4ConfiguratorBatch({ + hubConfiguratorAuthority_: accessManager, + spokeConfiguratorAuthority_: address(0), + salt_: keccak256('zeroSpokeCfgSalt') + }); + } + + function test_differentSaltProducesDifferentAddress() public { + AaveV4ConfiguratorBatch newBatch = new AaveV4ConfiguratorBatch({ + hubConfiguratorAuthority_: accessManager, + spokeConfiguratorAuthority_: accessManager, + salt_: keccak256('differentSalt') + }); + assertNotEq(report.hubConfigurator, newBatch.getReport().hubConfigurator); + } +} diff --git a/tests/deployments/batches/AaveV4GatewayBatch.t.sol b/tests/deployments/batches/AaveV4GatewayBatch.t.sol new file mode 100644 index 000000000..83d651150 --- /dev/null +++ b/tests/deployments/batches/AaveV4GatewayBatch.t.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/batches/BatchBase.t.sol'; + +contract AaveV4GatewayBatchTest is BatchBaseTest { + AaveV4GatewayBatch public gatewayBatch; + BatchReports.GatewaysBatchReport public report; + + function setUp() public override { + super.setUp(); + gatewayBatch = new AaveV4GatewayBatch({ + owner_: admin, + nativeWrapper_: nativeWrapper, + deployNativeTokenGateway_: true, + deploySignatureGateway_: true, + salt_: salt + }); + report = gatewayBatch.getReport(); + } + + function test_getReport() public view { + assertNotEq(report.nativeGateway, address(0)); + assertNotEq(report.signatureGateway, address(0)); + } + + function test_nativeGatewayWiring() public view { + NativeTokenGateway gateway = NativeTokenGateway(payable(report.nativeGateway)); + assertEq(gateway.owner(), admin); + assertEq(gateway.NATIVE_TOKEN_WRAPPER(), nativeWrapper); + } + + function test_signatureGatewayOwner() public view { + assertEq(Ownable(report.signatureGateway).owner(), admin); + } + + function test_onlyNativeTokenGateway() public { + AaveV4GatewayBatch batch = new AaveV4GatewayBatch({ + owner_: admin, + nativeWrapper_: nativeWrapper, + deployNativeTokenGateway_: true, + deploySignatureGateway_: false, + salt_: keccak256('nativeOnly') + }); + BatchReports.GatewaysBatchReport memory r = batch.getReport(); + assertNotEq(r.nativeGateway, address(0)); + assertEq(r.signatureGateway, address(0)); + } + + function test_onlySignatureGateway() public { + AaveV4GatewayBatch batch = new AaveV4GatewayBatch({ + owner_: admin, + nativeWrapper_: nativeWrapper, + deployNativeTokenGateway_: false, + deploySignatureGateway_: true, + salt_: keccak256('sigOnly') + }); + BatchReports.GatewaysBatchReport memory r = batch.getReport(); + assertEq(r.nativeGateway, address(0)); + assertNotEq(r.signatureGateway, address(0)); + } + + function test_noGateways() public { + AaveV4GatewayBatch batch = new AaveV4GatewayBatch({ + owner_: admin, + nativeWrapper_: nativeWrapper, + deployNativeTokenGateway_: false, + deploySignatureGateway_: false, + salt_: keccak256('none') + }); + BatchReports.GatewaysBatchReport memory r = batch.getReport(); + assertEq(r.nativeGateway, address(0)); + assertEq(r.signatureGateway, address(0)); + } + + function test_revert_zeroOwner() public { + vm.expectRevert('invalid owner'); + new AaveV4GatewayBatch({ + owner_: address(0), + nativeWrapper_: nativeWrapper, + deployNativeTokenGateway_: true, + deploySignatureGateway_: true, + salt_: salt + }); + } + + function test_revert_zeroNativeWrapper() public { + vm.expectRevert('invalid native wrapper'); + new AaveV4GatewayBatch({ + owner_: admin, + nativeWrapper_: address(0), + deployNativeTokenGateway_: true, + deploySignatureGateway_: true, + salt_: salt + }); + } + + function test_differentSaltProducesDifferentAddress() public { + AaveV4GatewayBatch newBatch = new AaveV4GatewayBatch({ + owner_: admin, + nativeWrapper_: nativeWrapper, + deployNativeTokenGateway_: true, + deploySignatureGateway_: true, + salt_: keccak256('differentSalt') + }); + assertNotEq(report.nativeGateway, newBatch.getReport().nativeGateway); + } +} diff --git a/tests/deployments/batches/AaveV4HubInstanceBatch.t.sol b/tests/deployments/batches/AaveV4HubInstanceBatch.t.sol new file mode 100644 index 000000000..620969898 --- /dev/null +++ b/tests/deployments/batches/AaveV4HubInstanceBatch.t.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/batches/BatchBase.t.sol'; + +contract AaveV4HubInstanceBatchTest is BatchBaseTest { + AaveV4HubInstanceBatch public hubInstanceBatch; + BatchReports.HubInstanceBatchReport public report; + + function setUp() public override { + super.setUp(); + hubInstanceBatch = new AaveV4HubInstanceBatch({ + hubProxyAdminOwner_: admin, + authority_: accessManager, + hubBytecode_: hubBytecode, + salt_: salt + }); + report = hubInstanceBatch.getReport(); + } + + function test_getReport() public view { + assertNotEq(report.hubProxy, address(0)); + assertNotEq(report.hubImplementation, address(0)); + assertNotEq(report.irStrategy, address(0)); + assertNotEq(report.hubProxy, report.hubImplementation); + } + + function test_hubAuthority() public view { + assertEq(IAccessManaged(report.hubProxy).authority(), accessManager); + } + + function test_irStrategyHub() public view { + assertEq(IAssetInterestRateStrategy(report.irStrategy).HUB(), report.hubProxy); + } + + function test_revert_zeroAuthority() public { + vm.expectRevert('invalid authority'); + new AaveV4HubInstanceBatch({ + hubProxyAdminOwner_: admin, + authority_: address(0), + hubBytecode_: hubBytecode, + salt_: salt + }); + } + + function test_revert_zeroHubProxyAdminOwner() public { + vm.expectRevert('invalid hub proxy admin owner'); + new AaveV4HubInstanceBatch({ + hubProxyAdminOwner_: address(0), + authority_: accessManager, + hubBytecode_: hubBytecode, + salt_: salt + }); + } + + function test_differentSaltProducesDifferentAddress() public { + AaveV4HubInstanceBatch newBatch = new AaveV4HubInstanceBatch({ + hubProxyAdminOwner_: admin, + authority_: accessManager, + hubBytecode_: hubBytecode, + salt_: keccak256('differentSalt') + }); + assertNotEq(report.hubProxy, newBatch.getReport().hubProxy); + } +} diff --git a/tests/deployments/batches/AaveV4PositionManagerBatch.t.sol b/tests/deployments/batches/AaveV4PositionManagerBatch.t.sol new file mode 100644 index 000000000..8574a067e --- /dev/null +++ b/tests/deployments/batches/AaveV4PositionManagerBatch.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/batches/BatchBase.t.sol'; +import {AaveV4PositionManagerBatch} from 'src/deployments/batches/AaveV4PositionManagerBatch.sol'; +import {GiverPositionManager} from 'src/position-manager/GiverPositionManager.sol'; +import {TakerPositionManager} from 'src/position-manager/TakerPositionManager.sol'; +import {ConfigPositionManager} from 'src/position-manager/ConfigPositionManager.sol'; + +contract AaveV4PositionManagerBatchTest is BatchBaseTest { + AaveV4PositionManagerBatch public batch; + BatchReports.PositionManagerBatchReport public report; + + function setUp() public override { + super.setUp(); + batch = new AaveV4PositionManagerBatch({owner_: admin, salt_: salt}); + report = batch.getReport(); + } + + function test_getReport() public view { + assertNotEq(report.giverPositionManager, address(0)); + assertNotEq(report.takerPositionManager, address(0)); + assertNotEq(report.configPositionManager, address(0)); + } + + function test_giverPositionManagerOwner() public view { + assertEq(Ownable(report.giverPositionManager).owner(), admin); + } + + function test_takerPositionManagerOwner() public view { + assertEq(Ownable(report.takerPositionManager).owner(), admin); + } + + function test_configPositionManagerOwner() public view { + assertEq(Ownable(report.configPositionManager).owner(), admin); + } + + function test_revert_zeroOwner() public { + vm.expectRevert('invalid owner'); + new AaveV4PositionManagerBatch({owner_: address(0), salt_: salt}); + } + + function test_differentSaltProducesDifferentAddress() public { + AaveV4PositionManagerBatch newBatch = new AaveV4PositionManagerBatch({ + owner_: admin, + salt_: keccak256('differentSalt') + }); + assertNotEq(report.giverPositionManager, newBatch.getReport().giverPositionManager); + assertNotEq(report.takerPositionManager, newBatch.getReport().takerPositionManager); + assertNotEq(report.configPositionManager, newBatch.getReport().configPositionManager); + } +} diff --git a/tests/deployments/batches/AaveV4SpokeInstanceBatch.t.sol b/tests/deployments/batches/AaveV4SpokeInstanceBatch.t.sol new file mode 100644 index 000000000..c0c5a686c --- /dev/null +++ b/tests/deployments/batches/AaveV4SpokeInstanceBatch.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/batches/BatchBase.t.sol'; + +contract AaveV4SpokeInstanceBatchTest is BatchBaseTest { + AaveV4SpokeInstanceBatch public spokeBatch; + BatchReports.SpokeInstanceBatchReport public report; + + function setUp() public override { + super.setUp(); + spokeBatch = new AaveV4SpokeInstanceBatch({ + spokeProxyAdminOwner_: admin, + authority_: accessManager, + spokeBytecode_: spokeBytecode, + oracleDecimals_: 8, + maxUserReservesLimit_: 128, + salt_: salt + }); + report = spokeBatch.getReport(); + } + + function test_getReport() public view { + assertNotEq(report.spokeProxy, address(0)); + assertNotEq(report.spokeImplementation, address(0)); + assertNotEq(report.aaveOracle, address(0)); + } + + function test_spokeAuthority() public view { + assertEq(IAccessManaged(report.spokeProxy).authority(), accessManager); + } + + function test_spokeOracle() public view { + assertEq(ISpoke(report.spokeProxy).ORACLE(), report.aaveOracle); + } + + function test_spokeMaxUserReservesLimit() public view { + assertEq(ISpoke(report.spokeProxy).MAX_USER_RESERVES_LIMIT(), 128); + } + + function test_oracleWiring() public view { + assertEq(IPriceOracle(report.aaveOracle).spoke(), report.spokeProxy); + assertEq(IPriceOracle(report.aaveOracle).decimals(), 8); + } + + function test_revert_zeroAuthority() public { + vm.expectRevert('invalid authority'); + new AaveV4SpokeInstanceBatch({ + spokeProxyAdminOwner_: admin, + authority_: address(0), + spokeBytecode_: spokeBytecode, + oracleDecimals_: 8, + maxUserReservesLimit_: 128, + salt_: salt + }); + } + + function test_revert_zeroSpokeProxyAdminOwner() public { + vm.expectRevert('invalid spoke proxy admin owner'); + new AaveV4SpokeInstanceBatch({ + spokeProxyAdminOwner_: address(0), + authority_: accessManager, + spokeBytecode_: spokeBytecode, + oracleDecimals_: 8, + maxUserReservesLimit_: 128, + salt_: salt + }); + } + + function test_revert_zeroOracleDecimals() public { + vm.expectRevert('invalid oracle decimals'); + new AaveV4SpokeInstanceBatch({ + spokeProxyAdminOwner_: admin, + authority_: accessManager, + spokeBytecode_: spokeBytecode, + oracleDecimals_: 0, + maxUserReservesLimit_: 128, + salt_: keccak256('zeroDecimalsSalt') + }); + } + + function test_revert_zeroMaxUserReservesLimit() public { + vm.expectRevert('invalid max user reserves limit'); + new AaveV4SpokeInstanceBatch({ + spokeProxyAdminOwner_: admin, + authority_: accessManager, + spokeBytecode_: spokeBytecode, + oracleDecimals_: 8, + maxUserReservesLimit_: 0, + salt_: keccak256('zeroMaxReservesSalt') + }); + } + + function test_differentSaltProducesDifferentAddress() public { + AaveV4SpokeInstanceBatch newBatch = new AaveV4SpokeInstanceBatch({ + spokeProxyAdminOwner_: admin, + authority_: accessManager, + spokeBytecode_: spokeBytecode, + oracleDecimals_: 8, + maxUserReservesLimit_: 128, + salt_: keccak256('differentSalt') + }); + assertNotEq(report.spokeProxy, newBatch.getReport().spokeProxy); + } +} diff --git a/tests/deployments/batches/AaveV4TokenizationSpokeBatch.t.sol b/tests/deployments/batches/AaveV4TokenizationSpokeBatch.t.sol new file mode 100644 index 000000000..be6f09602 --- /dev/null +++ b/tests/deployments/batches/AaveV4TokenizationSpokeBatch.t.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/batches/BatchBase.t.sol'; + +contract AaveV4TokenizationSpokeBatchTest is BatchBaseTest { + AaveV4TokenizationSpokeBatch public tokenizationSpokeBatch; + BatchReports.TokenizationSpokeBatchReport public report; + + address public hub; + address public irStrategy; + uint256 public assetId; + address public underlying; + string public shareName = 'Core Hub DAI'; + string public shareSymbol = 'chDAI'; + + function setUp() public override { + super.setUp(); + + // Deploy a Hub with asset + AaveV4HubInstanceBatch hubInstanceBatch = new AaveV4HubInstanceBatch({ + hubProxyAdminOwner_: admin, + authority_: accessManager, + hubBytecode_: hubBytecode, + salt_: salt + }); + BatchReports.HubInstanceBatchReport memory hubReport = hubInstanceBatch.getReport(); + hub = hubReport.hubProxy; + irStrategy = hubReport.irStrategy; + + // Deploy test token and add asset + TestnetERC20 testToken = new TestnetERC20('Test DAI', 'tDAI', 18); + underlying = address(testToken); + + bytes memory irData = abi.encode( + IAssetInterestRateStrategy.InterestRateData({ + optimalUsageRatio: 90_00, + baseDrawnRate: 5_00, + rateGrowthBeforeOptimal: 5_00, + rateGrowthAfterOptimal: 5_00 + }) + ); + + // Setup Hub roles and grant HUB_CONFIGURATOR_ROLE to admin + vm.startPrank(admin); + AaveV4HubRolesProcedure.setupHubAllRoles(accessManager, hub); + IAccessManagerEnumerable(accessManager).grantRole(Roles.HUB_CONFIGURATOR_ROLE, admin, 0); + + assetId = IHub(hub).addAsset({ + underlying: underlying, + decimals: 18, + feeReceiver: feeReceiver, + irStrategy: irStrategy, + irData: irData + }); + vm.stopPrank(); + + // Deploy the TokenizationSpoke batch + tokenizationSpokeBatch = new AaveV4TokenizationSpokeBatch( + hub, + underlying, + admin, + shareName, + shareSymbol, + salt + ); + report = tokenizationSpokeBatch.getReport(); + } + + function test_getReport() public view { + assertNotEq(report.tokenizationSpokeProxy, address(0)); + assertNotEq(report.tokenizationSpokeImplementation, address(0)); + } + + function test_tokenizationSpokeHub() public view { + assertEq(ITokenizationSpoke(report.tokenizationSpokeProxy).hub(), hub); + } + + function test_tokenizationSpokeAssetId() public view { + assertEq(ITokenizationSpoke(report.tokenizationSpokeProxy).assetId(), assetId); + } + + function test_tokenizationSpokeAsset() public view { + assertEq(ITokenizationSpoke(report.tokenizationSpokeProxy).asset(), underlying); + } + + function test_revert_zeroHub() public { + vm.expectRevert('invalid hub'); + new AaveV4TokenizationSpokeBatch(address(0), underlying, admin, shareName, shareSymbol, salt); + } + + function test_revert_zeroSpokeProxyAdminOwner() public { + vm.expectRevert('invalid spoke proxy admin owner'); + new AaveV4TokenizationSpokeBatch( + hub, + underlying, + address(0), + shareName, + shareSymbol, + keccak256('zeroAdminSalt') + ); + } + + function test_revert_emptyShareName() public { + vm.expectRevert('invalid share name'); + new AaveV4TokenizationSpokeBatch( + hub, + underlying, + admin, + '', + shareSymbol, + keccak256('emptyNameSalt') + ); + } + + function test_revert_emptyShareSymbol() public { + vm.expectRevert('invalid share symbol'); + new AaveV4TokenizationSpokeBatch( + hub, + underlying, + admin, + shareName, + '', + keccak256('emptySymbolSalt') + ); + } + + function test_revert_invalidUnderlying() public { + vm.expectRevert(); + new AaveV4TokenizationSpokeBatch( + hub, + makeAddr('nonExistentUnderlying'), + admin, + shareName, + shareSymbol, + keccak256('invalidAssetSalt') + ); + } + + function test_differentSaltProducesDifferentAddress() public { + AaveV4TokenizationSpokeBatch newBatch = new AaveV4TokenizationSpokeBatch( + hub, + underlying, + admin, + shareName, + shareSymbol, + keccak256('differentSalt') + ); + assertNotEq(report.tokenizationSpokeProxy, newBatch.getReport().tokenizationSpokeProxy); + } +} diff --git a/tests/deployments/batches/AaveV4TreasurySpokeBatch.t.sol b/tests/deployments/batches/AaveV4TreasurySpokeBatch.t.sol new file mode 100644 index 000000000..694d48ed5 --- /dev/null +++ b/tests/deployments/batches/AaveV4TreasurySpokeBatch.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/batches/BatchBase.t.sol'; + +contract AaveV4TreasurySpokeBatchTest is BatchBaseTest { + AaveV4TreasurySpokeBatch public treasurySpokeBatch; + BatchReports.TreasurySpokeBatchReport public report; + + function setUp() public override { + super.setUp(); + treasurySpokeBatch = new AaveV4TreasurySpokeBatch({owner_: admin, salt_: salt}); + report = treasurySpokeBatch.getReport(); + } + + function test_getReport() public view { + assertNotEq(report.treasurySpoke, address(0)); + } + + function test_treasurySpokeOwner() public view { + assertEq(Ownable(report.treasurySpoke).owner(), admin); + } + + function test_proxyAdminOwner() public view { + assertEq(Ownable(ProxyHelper.getProxyAdmin(report.treasurySpoke)).owner(), admin); + } + + function test_revert_zeroOwner() public { + vm.expectRevert('invalid owner'); + new AaveV4TreasurySpokeBatch({owner_: address(0), salt_: keccak256('zeroOwnerSalt')}); + } + + function test_differentSaltProducesDifferentAddress() public { + AaveV4TreasurySpokeBatch newBatch = new AaveV4TreasurySpokeBatch({ + owner_: admin, + salt_: keccak256('differentSalt') + }); + assertNotEq(report.treasurySpoke, newBatch.getReport().treasurySpoke); + } +} diff --git a/tests/deployments/batches/BatchBase.t.sol b/tests/deployments/batches/BatchBase.t.sol new file mode 100644 index 000000000..e6937f191 --- /dev/null +++ b/tests/deployments/batches/BatchBase.t.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {Ownable} from 'src/dependencies/openzeppelin/Ownable.sol'; +import {IAccessManaged} from 'src/dependencies/openzeppelin/IAccessManaged.sol'; + +import {WETH9} from 'src/dependencies/weth/WETH9.sol'; +import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; + +import {Create2TestHelper} from 'tests/utils/Create2TestHelper.sol'; +import {ProxyHelper} from 'tests/utils/ProxyHelper.sol'; +import {Roles} from 'src/deployments/utils/libraries/Roles.sol'; +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; +import {AaveV4AuthorityBatch} from 'src/deployments/batches/AaveV4AuthorityBatch.sol'; +import {AaveV4SpokeInstanceBatch} from 'src/deployments/batches/AaveV4SpokeInstanceBatch.sol'; +import {AaveV4HubInstanceBatch} from 'src/deployments/batches/AaveV4HubInstanceBatch.sol'; +import {AaveV4ConfiguratorBatch} from 'src/deployments/batches/AaveV4ConfiguratorBatch.sol'; +import {AaveV4TokenizationSpokeBatch} from 'src/deployments/batches/AaveV4TokenizationSpokeBatch.sol'; +import {AaveV4GatewayBatch} from 'src/deployments/batches/AaveV4GatewayBatch.sol'; +import {AaveV4PositionManagerBatch} from 'src/deployments/batches/AaveV4PositionManagerBatch.sol'; +import {AaveV4TreasurySpokeBatch} from 'src/deployments/batches/AaveV4TreasurySpokeBatch.sol'; +import {AaveV4HubRolesProcedure} from 'src/deployments/procedures/roles/AaveV4HubRolesProcedure.sol'; +import {NativeTokenGateway} from 'src/position-manager/NativeTokenGateway.sol'; + +import {IHub} from 'src/hub/interfaces/IHub.sol'; +import {ITokenizationSpoke} from 'src/spoke/interfaces/ITokenizationSpoke.sol'; +import {IAssetInterestRateStrategy} from 'src/hub/interfaces/IAssetInterestRateStrategy.sol'; + +import {AssetInterestRateStrategy} from 'src/hub/AssetInterestRateStrategy.sol'; +import {IAccessManagerEnumerable} from 'src/access/interfaces/IAccessManagerEnumerable.sol'; +import {TreasurySpoke} from 'src/spoke/TreasurySpoke.sol'; +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {IPriceOracle} from 'src/spoke/interfaces/IPriceOracle.sol'; + +contract BatchBaseTest is Create2TestHelper { + address public admin = makeAddr('admin'); + address public feeReceiver = makeAddr('feeReceiver'); + bytes32 public salt; + address public accessManager; + address public nativeWrapper; + bytes internal hubBytecode; + bytes internal spokeBytecode; + + function setUp() public virtual { + salt = keccak256('testSalt'); + _etchCreate2Factory(); + + hubBytecode = vm.getCode('src/hub/instances/HubInstance.sol:HubInstance'); + spokeBytecode = vm.getCode('src/spoke/instances/SpokeInstance.sol:SpokeInstance'); + + // used Hub, Spoke, Configurator batches + AaveV4AuthorityBatch authorityBatch = new AaveV4AuthorityBatch({admin_: admin, salt_: salt}); + accessManager = authorityBatch.getReport().accessManager; + + // used by Gateway batch + nativeWrapper = address(new WETH9()); + } +} diff --git a/tests/deployments/batches/TestTokensBatch.sol b/tests/deployments/batches/TestTokensBatch.sol new file mode 100644 index 000000000..9687ce29c --- /dev/null +++ b/tests/deployments/batches/TestTokensBatch.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {BatchReports} from 'src/deployments/libraries/BatchReports.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/fork/PostDeploymentVerificationAnvil.t.sol b/tests/deployments/fork/PostDeploymentVerificationAnvil.t.sol new file mode 100644 index 000000000..b434afb98 --- /dev/null +++ b/tests/deployments/fork/PostDeploymentVerificationAnvil.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {PostDeploymentVerificationBase} from 'tests/deployments/fork/PostDeploymentVerificationBase.t.sol'; +import {AaveV4DeployAnvil} from 'scripts/deploy/examples/AaveV4DeployAnvil.s.sol'; + +/// @title PostDeploymentVerificationAnvil +/// @notice Anvil-specific post-deployment verification test. +/// Extends the deploy script directly to read inputs and output directory — single source of truth. +/// +/// Usage: +/// 1. Deploy to anvil (see AaveV4DeployAnvil.s.sol) +/// 2. Set REPORT_FILE, DEPLOYER in this test file +/// 3. Run: forge test --mc PostDeploymentVerificationAnvil +contract PostDeploymentVerificationAnvil is PostDeploymentVerificationBase, AaveV4DeployAnvil { + // TODO: Update these constants to the test deployment params + string public constant REPORT_FILE = '1773975754-31337-anvil-deploy.json'; + address public constant DEPLOYER = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; // anvil account + + function setUp() public override(PostDeploymentVerificationBase) { + vm.skip(true, 'Anvil post-deployment test'); + _reportFile = string.concat(OUTPUT_DIR, REPORT_FILE); + _deployer = DEPLOYER; + PostDeploymentVerificationBase.setUp(); + vm.createSelectFork('anvil'); + } + + function _getSanitizedDeployInputs() internal override returns (FullDeployInputs memory) { + FullDeployInputs memory rawInputs = _getDeployInputs(); + return _loadWarningsAndSanitizeInputs(rawInputs, _deployer); + } +} diff --git a/tests/deployments/fork/PostDeploymentVerificationBase.t.sol b/tests/deployments/fork/PostDeploymentVerificationBase.t.sol new file mode 100644 index 000000000..05069f3c5 --- /dev/null +++ b/tests/deployments/fork/PostDeploymentVerificationBase.t.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {BatchTestProcedures} from 'tests/utils/BatchTestProcedures.sol'; +import {OrchestrationReports} from 'src/deployments/libraries/OrchestrationReports.sol'; +import {Roles} from 'src/deployments/utils/libraries/Roles.sol'; +import {ProxyHelper} from 'tests/utils/ProxyHelper.sol'; + +/// @title PostDeploymentVerificationBase +/// @notice Abstract base for post-deployment verification tests. +/// Reads a JSON deployment report and verifies deployed contracts on a live fork. +/// @dev Run with: forge test --match-contract --fork-url +abstract contract PostDeploymentVerificationBase is BatchTestProcedures { + /// @dev Full path to the deployment report JSON + string internal _reportFile; + + function setUp() public virtual override { + _spokePositionUpdaterRoleSelectors = Roles.getSpokePositionUpdaterRoleSelectors(); + _spokeConfiguratorRoleSelectors = Roles.getSpokeConfiguratorRoleSelectors(); + _hubFeeMinterRoleSelectors = Roles.getHubFeeMinterRoleSelectors(); + _hubConfiguratorRoleSelectors = Roles.getHubConfiguratorRoleSelectors(); + _inputs = _getSanitizedDeployInputs(); + _postDeploymentCheck = true; + } + + /// @dev Subclasses provide the expected deploy inputs (post-sanitization). + function _getSanitizedDeployInputs() internal virtual returns (FullDeployInputs memory); + + function _parseReport() + internal + view + returns (OrchestrationReports.FullDeploymentReport memory report) + { + require(bytes(_reportFile).length > 0, 'PostDeploymentVerificationBase: _reportFile not set'); + string memory json = vm.readFile(_reportFile); + + // Flat fields + report.authorityBatchReport.accessManager = vm.parseJsonAddress(json, '$.accessManager'); + report.configuratorBatchReport.hubConfigurator = vm.parseJsonAddress(json, '$.hubConfigurator'); + report.configuratorBatchReport.spokeConfigurator = vm.parseJsonAddress( + json, + '$.spokeConfigurator' + ); + report.treasurySpokeBatchReport.treasurySpoke = vm.parseJsonAddress(json, '$.treasurySpoke'); + report.salt = vm.parseJsonBytes32(json, '$.salt'); + + // Optional fields (conditionally written by MetadataLogger) + if (vm.keyExistsJson(json, '$.nativeTokenGateway')) { + report.gatewaysBatchReport.nativeGateway = vm.parseJsonAddress(json, '$.nativeTokenGateway'); + } + if (vm.keyExistsJson(json, '$.signatureGateway')) { + report.gatewaysBatchReport.signatureGateway = vm.parseJsonAddress(json, '$.signatureGateway'); + } + if (vm.keyExistsJson(json, '$.giverPositionManager')) { + report.positionManagerBatchReport.giverPositionManager = vm.parseJsonAddress( + json, + '$.giverPositionManager' + ); + } + if (vm.keyExistsJson(json, '$.takerPositionManager')) { + report.positionManagerBatchReport.takerPositionManager = vm.parseJsonAddress( + json, + '$.takerPositionManager' + ); + } + if (vm.keyExistsJson(json, '$.configPositionManager')) { + report.positionManagerBatchReport.configPositionManager = vm.parseJsonAddress( + json, + '$.configPositionManager' + ); + } + + uint256 hubCount = _inputs.hubLabels.length; + report.hubInstanceBatchReports = new OrchestrationReports.HubDeploymentReport[](hubCount); + for (uint256 i; i < hubCount; i++) { + string memory label = _inputs.hubLabels[i]; + report.hubInstanceBatchReports[i].label = label; + + report.hubInstanceBatchReports[i].report.hubProxy = vm.parseJsonAddress( + json, + string.concat('$.hub.', label) + ); + report.hubInstanceBatchReports[i].report.irStrategy = vm.parseJsonAddress( + json, + string.concat('$.irStrategy.', label) + ); + } + + uint256 spokeCount = _inputs.spokeLabels.length; + report.spokeInstanceBatchReports = new OrchestrationReports.SpokeDeploymentReport[](spokeCount); + for (uint256 i; i < spokeCount; i++) { + string memory label = _inputs.spokeLabels[i]; + report.spokeInstanceBatchReports[i].label = label; + + report.spokeInstanceBatchReports[i].report.spokeProxy = vm.parseJsonAddress( + json, + string.concat('$.spoke.', label) + ); + report.spokeInstanceBatchReports[i].report.aaveOracle = vm.parseJsonAddress( + json, + string.concat('$.oracle.', label) + ); + } + } + + function testPostDeploymentCheck() public view { + OrchestrationReports.FullDeploymentReport memory report = _parseReport(); + // Implementation addresses not included in output json report, therefore skip its checks + _checkAllAddressesHaveCode({report: report}); + _checkDeployment({report: report, inputs: _inputs}); + _checkRoles({report: report, inputs: _inputs}); + } +} diff --git a/tests/deployments/orchestration/AaveV4TestOrchestration.sol b/tests/deployments/orchestration/AaveV4TestOrchestration.sol new file mode 100644 index 000000000..9653751dd --- /dev/null +++ b/tests/deployments/orchestration/AaveV4TestOrchestration.sol @@ -0,0 +1,467 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'forge-std/Vm.sol'; + +import {TestTypes} from 'tests/utils/TestTypes.sol'; +import {Constants} from 'tests/Constants.sol'; +import {TestnetERC20} from 'tests/mocks/TestnetERC20.sol'; +import {Roles} from 'src/deployments/utils/libraries/Roles.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 {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 {AaveV4HubConfiguratorRolesProcedure} from 'src/deployments/procedures/roles/AaveV4HubConfiguratorRolesProcedure.sol'; +import {AaveV4SpokeConfiguratorRolesProcedure} from 'src/deployments/procedures/roles/AaveV4SpokeConfiguratorRolesProcedure.sol'; +import {AaveV4TreasurySpokeBatch} from 'src/deployments/batches/AaveV4TreasurySpokeBatch.sol'; +import {AaveV4AuthorityBatch} from 'src/deployments/batches/AaveV4AuthorityBatch.sol'; +import {AaveV4HubInstanceBatch} from 'src/deployments/batches/AaveV4HubInstanceBatch.sol'; +import {AaveV4SpokeInstanceBatch} from 'src/deployments/batches/AaveV4SpokeInstanceBatch.sol'; +import {TestTokensBatch} from 'tests/deployments/batches/TestTokensBatch.sol'; +import {AaveV4DeployBase} from 'src/deployments/orchestration/AaveV4DeployBase.sol'; +import {WETH9} from 'src/dependencies/weth/WETH9.sol'; +import {IHub} from 'src/hub/interfaces/IHub.sol'; +import {IHubConfigurator} from 'src/hub/interfaces/IHubConfigurator.sol'; +import {IHubInstance} from 'src/deployments/utils/interfaces/IHubInstance.sol'; +import {ISpokeInstance} from 'src/deployments/utils/interfaces/ISpokeInstance.sol'; +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; +import {TransparentUpgradeableProxy} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; + +library AaveV4TestOrchestration { + bool public constant IS_TEST = true; + bytes internal constant CREATE2_FACTORY_BYTECODE = + hex'7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3'; + Vm private constant vm = Vm(address(bytes20(uint160(uint256(keccak256('hevm cheat code')))))); + + error Create2DeploymentFailed(); + + 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, + uint256 hubCount, + uint256 spokeCount, + address nativeWrapper, + bytes memory hubBytecode, + bytes memory spokeBytecode, + 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 + .deployAuthorityBatch({admin: admin, salt: salt}) + .accessManager; + + // Deploy TreasurySpoke Batch (single instance for all hubs) + report.treasurySpoke = AaveV4DeployBase + .deployTreasurySpokeBatch({ + owner: treasuryAdmin, + salt: keccak256(abi.encodePacked(salt, 'treasurySpoke')) + }) + .treasurySpoke; + + // Deploy Hub Batches + for (uint256 i; i < hubCount; ++i) { + BatchReports.HubInstanceBatchReport memory hubReport = AaveV4DeployBase + .deployHubInstanceBatch({ + hubProxyAdminOwner: admin, + authority: report.accessManager, + hubBytecode: hubBytecode, + salt: keccak256(abi.encodePacked(salt, 'hub-', string(abi.encode(i)))) + }); + report.hubReports[i].hub = hubReport.hubProxy; + report.hubReports[i].irStrategy = hubReport.irStrategy; + } + + // Deploy Spoke Instance Batches + for (uint256 i; i < spokeCount; ++i) { + BatchReports.SpokeInstanceBatchReport memory spokeReport = AaveV4DeployBase + .deploySpokeInstanceBatch({ + spokeProxyAdminOwner: admin, + authority: report.accessManager, + spokeBytecode: spokeBytecode, + oracleDecimals: Constants.ORACLE_DECIMALS, + maxUserReservesLimit: Constants.MAX_ALLOWED_USER_RESERVES_LIMIT, + 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 with AccessManager as authority + BatchReports.ConfiguratorBatchReport memory configuratorReport = AaveV4DeployBase + .deployConfiguratorBatch({ + hubConfiguratorAuthority: report.accessManager, + spokeConfiguratorAuthority: report.accessManager, + 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, + deployNativeTokenGateway: true, + deploySignatureGateway: true, + salt: keccak256(abi.encodePacked(salt, 'gateways')) + }); + report.gatewaysReport.signatureGateway = gatewaysReport.signatureGateway; + report.gatewaysReport.nativeGateway = gatewaysReport.nativeGateway; + + return report; + } + + function deployTestHub( + address hubProxyAdminOwner, + address accessManager, + bytes memory hubBytecode, + string memory label, + bytes32 salt + ) external returns (TestTypes.TestHubReport memory) { + TestTypes.TestHubReport memory report; + BatchReports.HubInstanceBatchReport memory hubReport = AaveV4DeployBase.deployHubInstanceBatch({ + hubProxyAdminOwner: hubProxyAdminOwner, + authority: accessManager, + hubBytecode: hubBytecode, + salt: keccak256(abi.encodePacked(salt, 'hub-', label)) + }); + report.hub = hubReport.hubProxy; + report.irStrategy = hubReport.irStrategy; + + return report; + } + + function deployTestSpoke( + address spokeProxyAdminOwner, + address accessManager, + bytes memory spokeBytecode, + uint16 maxUserReservesLimit, + bytes32 salt + ) external returns (TestTypes.TestSpokeReport memory) { + TestTypes.TestSpokeReport memory report; + BatchReports.SpokeInstanceBatchReport memory spokeReport = AaveV4DeployBase + .deploySpokeInstanceBatch({ + spokeProxyAdminOwner: spokeProxyAdminOwner, + authority: accessManager, + spokeBytecode: spokeBytecode, + oracleDecimals: Constants.ORACLE_DECIMALS, + maxUserReservesLimit: maxUserReservesLimit, + salt: salt + }); + report.spoke = spokeReport.spokeProxy; + report.aaveOracle = spokeReport.aaveOracle; + return report; + } + + function deployTestTokenizationSpoke( + address hub, + address underlying, + address spokeProxyAdminOwner, + string memory shareName, + string memory shareSymbol, + bytes32 salt + ) external returns (address tokenizationSpokeProxy) { + BatchReports.TokenizationSpokeBatchReport memory report = AaveV4DeployBase + .deployTokenizationSpokeBatch({ + hub: hub, + underlying: underlying, + spokeProxyAdminOwner: spokeProxyAdminOwner, + shareName: shareName, + shareSymbol: shareSymbol, + salt: salt + }); + return report.tokenizationSpokeProxy; + } + + function deployTestTreasurySpoke( + address owner, + bytes32 salt + ) external returns (address treasurySpoke) { + return AaveV4DeployBase.deployTreasurySpokeBatch({owner: owner, salt: salt}).treasurySpoke; + } + + function configureHubsSpokes(ConfigData.AddSpokeParams[] memory paramsList) external { + for (uint256 i; i < paramsList.length; ++i) { + IHub(paramsList[i].hub).addSpoke({ + assetId: paramsList[i].assetId, + spoke: paramsList[i].spoke, + params: paramsList[i].config + }); + } + } + + function configureSpokes( + ConfigData.UpdateLiquidationConfigParams[] memory liquidationParamsList, + ConfigData.AddReserveParams[] memory reserveParamsList + ) external returns (TestTypes.SpokeReserveId[] memory) { + for (uint256 i; i < liquidationParamsList.length; ++i) { + ISpoke(liquidationParamsList[i].spoke).updateLiquidationConfig( + liquidationParamsList[i].config + ); + } + 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: ISpoke(reserveParamsList[i].spoke).addReserve({ + hub: reserveParamsList[i].hub, + assetId: reserveParamsList[i].assetId, + priceSource: reserveParamsList[i].priceSource, + config: reserveParamsList[i].config, + dynamicConfig: reserveParamsList[i].dynamicConfig + }) + }); + } + return spokeReserveIds; + } + + function setRolesTestEnv(TestTypes.TestEnvReport memory report) public { + // Set Hub Roles + for (uint256 i; i < report.hubReports.length; ++i) { + AaveV4HubRolesProcedure.setupHubAllRoles(report.accessManager, report.hubReports[i].hub); + } + + // Set Spoke Roles + for (uint256 i; i < report.spokeReports.length; ++i) { + AaveV4SpokeRolesProcedure.setupSpokeAllRoles( + report.accessManager, + report.spokeReports[i].spoke + ); + } + + // Set Configurator Roles + AaveV4HubConfiguratorRolesProcedure.setupHubConfiguratorAllRoles( + report.accessManager, + report.configuratorReport.hubConfigurator + ); + AaveV4SpokeConfiguratorRolesProcedure.setupSpokeConfiguratorAllRoles( + report.accessManager, + report.configuratorReport.spokeConfigurator + ); + } + + function setupHubRolesTestEnv( + TestTypes.TestHubReport memory report, + address accessManager + ) public { + AaveV4HubRolesProcedure.setupHubAllRoles(accessManager, report.hub); + } + + 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.grantHubAllRoles(report.accessManager, admin); + AaveV4HubRolesProcedure.grantHubAllRoles(report.accessManager, hubAdmin); + + // grant Hub Configurator role + AaveV4HubRolesProcedure.grantHubRole( + report.accessManager, + Roles.HUB_CONFIGURATOR_ROLE, + report.configuratorReport.hubConfigurator + ); + + // grant HubConfigurator Admin roles (allows admin to call HubConfigurator functions) + AaveV4HubConfiguratorRolesProcedure.grantHubConfiguratorAllRoles(report.accessManager, admin); + AaveV4HubConfiguratorRolesProcedure.grantHubConfiguratorAllRoles( + report.accessManager, + hubAdmin + ); + } + + function grantSpokeRolesTestEnv( + TestTypes.TestEnvReport memory report, + address admin, + address spokeAdmin + ) public { + // grant Spoke roles + AaveV4SpokeRolesProcedure.grantSpokeAllRoles(report.accessManager, admin); + AaveV4SpokeRolesProcedure.grantSpokeAllRoles(report.accessManager, spokeAdmin); + + // grant Spoke Configurator roles (allows SpokeConfigurator to call Spoke functions) + AaveV4SpokeRolesProcedure.grantSpokeRole( + report.accessManager, + Roles.SPOKE_CONFIGURATOR_ROLE, + report.configuratorReport.spokeConfigurator + ); + + // grant SpokeConfigurator Admin roles (allows admin to call SpokeConfigurator functions) + AaveV4SpokeConfiguratorRolesProcedure.grantSpokeConfiguratorAllRoles( + report.accessManager, + admin + ); + AaveV4SpokeConfiguratorRolesProcedure.grantSpokeConfiguratorAllRoles( + report.accessManager, + spokeAdmin + ); + } + + 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] = IHub(paramsList[i].hub).addAsset({ + underlying: paramsList[i].underlying, + decimals: paramsList[i].decimals, + feeReceiver: paramsList[i].feeReceiver, + irStrategy: paramsList[i].irStrategy, + irData: paramsList[i].irData + }); + if (paramsList[i].liquidityFee > 0 || paramsList[i].reinvestmentController != address(0)) { + IHub(paramsList[i].hub).updateAssetConfig({ + assetId: assetIds[i], + config: IHub.AssetConfig({ + liquidityFee: paramsList[i].liquidityFee, + feeReceiver: paramsList[i].feeReceiver, + irStrategy: paramsList[i].irStrategy, + reinvestmentController: paramsList[i].reinvestmentController + }), + irData: bytes('') + }); + } + } + 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] = IHubConfigurator(hubConfigurator).addAssetWithDecimals({ + hub: paramsList[i].hub, + underlying: paramsList[i].underlying, + decimals: paramsList[i].decimals, + feeReceiver: paramsList[i].feeReceiver, + liquidityFee: paramsList[i].liquidityFee, + irStrategy: paramsList[i].irStrategy, + irData: paramsList[i].irData + }); + } + 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(); + } + + function loadCreate2Factory() internal { + if (Create2Utils.isContractDeployed(Create2Utils.CREATE2_FACTORY)) return; + vm.etch(Create2Utils.CREATE2_FACTORY, CREATE2_FACTORY_BYTECODE); + } + + function _create2Deploy(bytes32 salt, bytes memory bytecode) internal returns (address) { + loadCreate2Factory(); + address computed = Create2Utils.computeCreate2Address(salt, bytecode); + if (Create2Utils.isContractDeployed(computed)) return computed; + + bytes memory creationBytecode = abi.encodePacked(salt, bytecode); + (, bytes memory returnData) = Create2Utils.CREATE2_FACTORY.call(creationBytecode); + address deployedAt = address(uint160(bytes20(returnData))); + require(deployedAt == computed, Create2DeploymentFailed()); + return deployedAt; + } + + function deploySpokeImplementation( + address oracle, + uint16 maxUserReservesLimit + ) internal returns (ISpokeInstance) { + return deploySpokeImplementation(oracle, maxUserReservesLimit, ''); + } + + function deploySpokeImplementation( + address oracle, + uint16 maxUserReservesLimit, + bytes32 salt + ) internal returns (ISpokeInstance) { + bytes memory initCode = abi.encodePacked( + vm.getCode('src/spoke/instances/SpokeInstance.sol:SpokeInstance'), + abi.encode(oracle, maxUserReservesLimit) + ); + return ISpokeInstance(_create2Deploy(salt, initCode)); + } + + function deployHubImplementation() internal returns (IHubInstance) { + return deployHubImplementation(''); + } + + function deployHubImplementation(bytes32 salt) internal returns (IHubInstance) { + bytes memory initCode = vm.getCode('src/hub/instances/HubInstance.sol:HubInstance'); + return IHubInstance(_create2Deploy(salt, initCode)); + } + function deployHub(address authority, address proxyAdminOwner) internal returns (IHub) { + return + IHub( + proxify( + address(deployHubImplementation()), + proxyAdminOwner, + abi.encodeCall(IHubInstance.initialize, (authority)) + ) + ); + } + + function proxify( + address impl, + address proxyAdminOwner, + bytes memory initData + ) internal returns (address) { + return address(new TransparentUpgradeableProxy(impl, proxyAdminOwner, initData)); + } +} diff --git a/tests/deployments/procedures/ProceduresBase.t.sol b/tests/deployments/procedures/ProceduresBase.t.sol new file mode 100644 index 000000000..985aa16a9 --- /dev/null +++ b/tests/deployments/procedures/ProceduresBase.t.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {Ownable} from 'src/dependencies/openzeppelin/Ownable.sol'; +import {IAccessManaged} from 'src/dependencies/openzeppelin/IAccessManaged.sol'; + +import {ProxyHelper} from 'tests/utils/ProxyHelper.sol'; +import {Constants} from 'tests/Constants.sol'; +import {TestnetERC20} from 'tests/mocks/TestnetERC20.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 {AaveV4HubConfiguratorRolesProcedureWrapper} from 'tests/mocks/deployments/procedures/AaveV4HubConfiguratorRolesProcedureWrapper.sol'; +import {AaveV4SpokeConfiguratorRolesProcedureWrapper} from 'tests/mocks/deployments/procedures/AaveV4SpokeConfiguratorRolesProcedureWrapper.sol'; +import {AaveV4TokenizationSpokeDeployProcedureWrapper} from 'tests/mocks/deployments/procedures/AaveV4TokenizationSpokeDeployProcedureWrapper.sol'; +import {AaveV4HubRolesProcedure} from 'src/deployments/procedures/roles/AaveV4HubRolesProcedure.sol'; +import {AaveV4DeployProcedureBase} from 'src/deployments/procedures/AaveV4DeployProcedureBase.sol'; +import {AaveV4HubInstanceBatch} from 'src/deployments/batches/AaveV4HubInstanceBatch.sol'; +import {AaveV4TreasurySpokeBatch} from 'src/deployments/batches/AaveV4TreasurySpokeBatch.sol'; +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; +import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.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 {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 {ITokenizationSpoke} from 'src/spoke/interfaces/ITokenizationSpoke.sol'; +import {Create2TestHelper} from 'tests/utils/Create2TestHelper.sol'; + +contract ProceduresBase is Create2TestHelper { + address public owner = makeAddr('owner'); + address public accessManager; + address public hub = makeAddr('hub'); + address public nativeWrapper = makeAddr('nativeWrapper'); + address public accessManagerAdmin = makeAddr('accessManagerAdmin'); + uint8 public oracleDecimals = 8; + uint16 public maxUserReservesLimit = Constants.MAX_ALLOWED_USER_RESERVES_LIMIT; + address public spoke = makeAddr('spoke'); + address public aaveOracle; + address public feeReceiver = makeAddr('feeReceiver'); + address public admin = makeAddr('admin'); + bytes32 public salt; + bytes internal hubBytecode; + bytes internal spokeBytecode; + + function setUp() public virtual { + _etchCreate2Factory(); + + hubBytecode = vm.getCode('src/hub/instances/HubInstance.sol:HubInstance'); + spokeBytecode = vm.getCode('src/spoke/instances/SpokeInstance.sol:SpokeInstance'); + accessManager = address(new AccessManagerEnumerable(accessManagerAdmin)); + aaveOracle = address(new AaveOracle(oracleDecimals)); + salt = keccak256('testSalt'); + } + + function _assertCanCall(address target, bytes4[] memory selectors) internal { + for (uint256 idx; idx < selectors.length; idx++) { + (bool allowed, uint32 delay) = IAccessManager(accessManager).canCall( + admin, + target, + selectors[idx] + ); + assertTrue(allowed); + assertEq(delay, 0); + } + + address unauthorized = makeAddr('unauthorized'); + for (uint256 idx; idx < selectors.length; idx++) { + (bool allowed, uint32 delay) = IAccessManager(accessManager).canCall( + unauthorized, + target, + selectors[idx] + ); + assertFalse(allowed); + assertEq(delay, 0); + } + } +} 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..c8732c96f --- /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(IAccessManaged(hubConfigurator).authority(), owner); + } + + function test_deployHubConfigurator_reverts() public { + vm.expectRevert('invalid authority'); + aaveV4HubConfiguratorDeployProcedureWrapper.deployHubConfigurator({ + authority: 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..d56e15af7 --- /dev/null +++ b/tests/deployments/procedures/deploy/hub/AaveV4HubDeployProcedure.t.sol @@ -0,0 +1,46 @@ +// 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 hubProxy, address hubImpl) = aaveV4HubDeployProcedureWrapper.deployHub( + admin, + accessManager, + hubBytecode, + salt + ); + assertNotEq(hubProxy, address(0)); + assertNotEq(hubImpl, address(0)); + assertNotEq(hubProxy, hubImpl); + assertEq(IHub(hubProxy).authority(), accessManager); + } + + function test_deployHub_reverts_invalidAuthority() public { + vm.expectRevert('invalid authority'); + aaveV4HubDeployProcedureWrapper.deployHub({ + hubProxyAdminOwner: admin, + authority: address(0), + hubBytecode: hubBytecode, + salt: salt + }); + } + + function test_deployHub_reverts_invalidProxyAdminOwner() public { + vm.expectRevert('invalid hub proxy admin owner'); + aaveV4HubDeployProcedureWrapper.deployHub({ + hubProxyAdminOwner: address(0), + authority: accessManager, + hubBytecode: hubBytecode, + 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..dfc4a903f --- /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_deployNativeTokenGateway() 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..2c90ea006 --- /dev/null +++ b/tests/deployments/procedures/deploy/roles/AaveV4AccessManagerRolesProcedure.t.sol @@ -0,0 +1,106 @@ +// 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_replaceDefaultAdminRole() public { + address newAdmin = makeAddr('newAdmin'); + + _replaceDefaultAdminRole(newAdmin); + (bool hasRole, uint32 executionDelay) = IAccessManagerEnumerable(accessManager).hasRole( + Roles.ACCESS_MANAGER_DEFAULT_ADMIN, + newAdmin + ); + assertTrue(hasRole); + assertEq(executionDelay, 0); + } + + function test_replaceDefaultAdminRole_reverts() public { + address newAdmin = makeAddr('newAdmin'); + vm.expectRevert('zero address'); + aaveV4AccessManagerRolesProcedureWrapper.replaceDefaultAdminRole({ + accessManager: address(0), + adminToAdd: newAdmin, + adminToRemove: accessManagerAdmin + }); + + vm.expectRevert('zero address'); + aaveV4AccessManagerRolesProcedureWrapper.replaceDefaultAdminRole({ + accessManager: accessManager, + adminToAdd: address(0), + adminToRemove: newAdmin + }); + + vm.prank(accessManagerAdmin); + IAccessManager(accessManager).grantRole( + Roles.ACCESS_MANAGER_DEFAULT_ADMIN, + address(aaveV4AccessManagerRolesProcedureWrapper), + 0 + ); + vm.expectRevert('zero address'); + aaveV4AccessManagerRolesProcedureWrapper.replaceDefaultAdminRole({ + accessManager: accessManager, + adminToAdd: newAdmin, + adminToRemove: address(0) + }); + } + + function test_grantAccessManagerAdminRole() public { + address newAdmin = makeAddr('newAdmin'); + vm.prank(accessManagerAdmin); + IAccessManager(accessManager).grantRole( + Roles.ACCESS_MANAGER_DEFAULT_ADMIN, + address(aaveV4AccessManagerRolesProcedureWrapper), + 0 + ); + aaveV4AccessManagerRolesProcedureWrapper.grantAccessManagerAdminRole({ + accessManager: accessManager, + admin: newAdmin + }); + + (bool hasRole, uint32 executionDelay) = IAccessManagerEnumerable(accessManager).hasRole( + Roles.ACCESS_MANAGER_DEFAULT_ADMIN, + newAdmin + ); + assertTrue(hasRole); + assertEq(executionDelay, 0); + } + + function test_grantAccessManagerAdminRole_reverts() public { + vm.expectRevert('zero address'); + aaveV4AccessManagerRolesProcedureWrapper.grantAccessManagerAdminRole({ + accessManager: address(0), + admin: admin + }); + + vm.expectRevert('zero address'); + aaveV4AccessManagerRolesProcedureWrapper.grantAccessManagerAdminRole({ + accessManager: accessManager, + admin: address(0) + }); + } + + /// @dev Grants a temporary root admin role to the wrapper contract to execute the procedure. + function _replaceDefaultAdminRole(address newAdmin) internal { + vm.startPrank(accessManagerAdmin); + IAccessManager(accessManager).grantRole( + Roles.ACCESS_MANAGER_DEFAULT_ADMIN, + address(aaveV4AccessManagerRolesProcedureWrapper), + 0 + ); + aaveV4AccessManagerRolesProcedureWrapper.replaceDefaultAdminRole({ + accessManager: accessManager, + adminToAdd: newAdmin, + adminToRemove: address(aaveV4AccessManagerRolesProcedureWrapper) + }); + vm.stopPrank(); + } +} diff --git a/tests/deployments/procedures/deploy/roles/AaveV4HubConfiguratorRolesProcedure.t.sol b/tests/deployments/procedures/deploy/roles/AaveV4HubConfiguratorRolesProcedure.t.sol new file mode 100644 index 000000000..f7a9671b8 --- /dev/null +++ b/tests/deployments/procedures/deploy/roles/AaveV4HubConfiguratorRolesProcedure.t.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/procedures/ProceduresBase.t.sol'; + +import {IHubConfigurator} from 'src/hub/interfaces/IHubConfigurator.sol'; + +contract AaveV4HubConfiguratorRolesProcedureTest is ProceduresBase { + AaveV4HubConfiguratorRolesProcedureWrapper public wrapper; + address public hubConfigurator = makeAddr('hubConfigurator'); + + function setUp() public override { + super.setUp(); + wrapper = new AaveV4HubConfiguratorRolesProcedureWrapper(); + } + + function test_grantHubConfiguratorRole_reverts() public { + vm.expectRevert('zero address'); + wrapper.grantHubConfiguratorRole({ + accessManager: address(0), + role: Roles.HUB_CONFIGURATOR_DOMAIN_ADMIN_ROLE, + admin: admin + }); + + vm.expectRevert('zero address'); + wrapper.grantHubConfiguratorRole({ + accessManager: accessManager, + role: Roles.HUB_CONFIGURATOR_DOMAIN_ADMIN_ROLE, + admin: address(0) + }); + } + + function test_setupHubConfiguratorAllRoles_reverts() public { + vm.expectRevert('zero address'); + wrapper.setupHubConfiguratorAllRoles({ + accessManager: address(0), + hubConfigurator: hubConfigurator + }); + + vm.expectRevert('zero address'); + wrapper.setupHubConfiguratorAllRoles({ + accessManager: accessManager, + hubConfigurator: address(0) + }); + } + + function test_setupHubConfiguratorRole_reverts() public { + bytes4[] memory selectors = wrapper.getHubConfiguratorDomainAdminRoleSelectors(); + + vm.expectRevert('zero address'); + wrapper.setupHubConfiguratorRole({ + accessManager: address(0), + hubConfigurator: hubConfigurator, + role: Roles.HUB_CONFIGURATOR_DOMAIN_ADMIN_ROLE, + selectors: selectors + }); + + vm.expectRevert('zero address'); + wrapper.setupHubConfiguratorRole({ + accessManager: accessManager, + hubConfigurator: address(0), + role: Roles.HUB_CONFIGURATOR_DOMAIN_ADMIN_ROLE, + selectors: selectors + }); + } + + function test_grantHubConfiguratorAllRoles() public { + _grantAdminToWrapper(address(wrapper)); + wrapper.grantHubConfiguratorAllRoles({accessManager: accessManager, admin: admin}); + + (bool hasRole, ) = IAccessManager(accessManager).hasRole( + Roles.HUB_CONFIGURATOR_DOMAIN_ADMIN_ROLE, + admin + ); + assertTrue(hasRole); + } + + function test_setupHubConfiguratorAllRoles() public { + _grantAdminToWrapper(address(wrapper)); + wrapper.setupHubConfiguratorAllRoles({ + accessManager: accessManager, + hubConfigurator: hubConfigurator + }); + + bytes4[] memory selectors = wrapper.getHubConfiguratorDomainAdminRoleSelectors(); + for (uint256 i; i < selectors.length; i++) { + assertEq( + IAccessManager(accessManager).getTargetFunctionRole(hubConfigurator, selectors[i]), + Roles.HUB_CONFIGURATOR_DOMAIN_ADMIN_ROLE + ); + } + } + + function _grantAdminToWrapper(address wrapperAddr) internal { + vm.prank(accessManagerAdmin); + IAccessManager(accessManager).grantRole(Roles.ACCESS_MANAGER_DEFAULT_ADMIN, wrapperAddr, 0); + } + + function test_getHubConfiguratorDomainAdminRoleSelectors() public view { + bytes4[] memory selectors = wrapper.getHubConfiguratorDomainAdminRoleSelectors(); + assertEq(selectors.length, 22); + assertEq(selectors[0], IHubConfigurator.addAsset.selector); + assertEq(selectors[1], IHubConfigurator.addAssetWithDecimals.selector); + assertEq(selectors[2], IHubConfigurator.updateLiquidityFee.selector); + assertEq(selectors[3], IHubConfigurator.updateFeeReceiver.selector); + assertEq(selectors[4], IHubConfigurator.updateFeeConfig.selector); + assertEq(selectors[5], IHubConfigurator.updateInterestRateStrategy.selector); + assertEq(selectors[6], IHubConfigurator.updateReinvestmentController.selector); + assertEq(selectors[7], IHubConfigurator.resetAssetCaps.selector); + assertEq(selectors[8], IHubConfigurator.deactivateAsset.selector); + assertEq(selectors[9], IHubConfigurator.haltAsset.selector); + assertEq(selectors[10], IHubConfigurator.addSpoke.selector); + assertEq(selectors[11], IHubConfigurator.addSpokeToAssets.selector); + assertEq(selectors[12], IHubConfigurator.updateSpokeActive.selector); + assertEq(selectors[13], IHubConfigurator.updateSpokeHalted.selector); + assertEq(selectors[14], IHubConfigurator.updateSpokeAddCap.selector); + assertEq(selectors[15], IHubConfigurator.updateSpokeDrawCap.selector); + assertEq(selectors[16], IHubConfigurator.updateSpokeRiskPremiumThreshold.selector); + assertEq(selectors[17], IHubConfigurator.updateSpokeCaps.selector); + assertEq(selectors[18], IHubConfigurator.deactivateSpoke.selector); + assertEq(selectors[19], IHubConfigurator.haltSpoke.selector); + assertEq(selectors[20], IHubConfigurator.resetSpokeCaps.selector); + assertEq(selectors[21], IHubConfigurator.updateInterestRateData.selector); + } + + function test_canCall_hubConfiguratorAllRoles() public { + _grantAdminToWrapper(address(wrapper)); + wrapper.grantHubConfiguratorAllRoles({accessManager: accessManager, admin: admin}); + wrapper.setupHubConfiguratorAllRoles({ + accessManager: accessManager, + hubConfigurator: hubConfigurator + }); + + _assertCanCall(hubConfigurator, wrapper.getHubConfiguratorDomainAdminRoleSelectors()); + } +} 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..0995d81ff --- /dev/null +++ b/tests/deployments/procedures/deploy/roles/AaveV4HubRolesProcedure.t.sol @@ -0,0 +1,206 @@ +// 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_grantHubAllRoles_reverts() public { + vm.expectRevert('zero address'); + aaveV4HubRolesProcedureWrapper.grantHubAllRoles({accessManager: address(0), admin: admin}); + + vm.expectRevert('zero address'); + aaveV4HubRolesProcedureWrapper.grantHubAllRoles({ + accessManager: accessManager, + admin: address(0) + }); + } + + function test_grantHubRole_reverts() public { + vm.expectRevert('zero address'); + aaveV4HubRolesProcedureWrapper.grantHubRole({ + accessManager: address(0), + role: Roles.HUB_FEE_MINTER_ROLE, + admin: admin + }); + + vm.expectRevert('zero address'); + aaveV4HubRolesProcedureWrapper.grantHubRole({ + accessManager: accessManager, + role: Roles.HUB_FEE_MINTER_ROLE, + admin: address(0) + }); + } + + function test_setupHubRoles_reverts() public { + vm.expectRevert('zero address'); + aaveV4HubRolesProcedureWrapper.setupHubRoles({accessManager: address(0), hub: hub}); + + vm.expectRevert('zero address'); + aaveV4HubRolesProcedureWrapper.setupHubRoles({accessManager: accessManager, hub: address(0)}); + } + + function test_setupHubFeeMinterRole_reverts() public { + vm.expectRevert('zero address'); + aaveV4HubRolesProcedureWrapper.setupHubFeeMinterRole({accessManager: address(0), hub: hub}); + + vm.expectRevert('zero address'); + aaveV4HubRolesProcedureWrapper.setupHubFeeMinterRole({ + accessManager: accessManager, + hub: address(0) + }); + } + + function test_setupHubConfiguratorRole_reverts() public { + vm.expectRevert('zero address'); + aaveV4HubRolesProcedureWrapper.setupHubConfiguratorRole({accessManager: address(0), hub: hub}); + + vm.expectRevert('zero address'); + aaveV4HubRolesProcedureWrapper.setupHubConfiguratorRole({ + accessManager: accessManager, + hub: address(0) + }); + } + + function test_grantHubAllRoles() public { + _grantAdminToWrapper(address(aaveV4HubRolesProcedureWrapper)); + aaveV4HubRolesProcedureWrapper.grantHubAllRoles({accessManager: accessManager, admin: admin}); + + (bool hasConfigurator, ) = IAccessManager(accessManager).hasRole( + Roles.HUB_CONFIGURATOR_ROLE, + admin + ); + assertTrue(hasConfigurator); + + (bool hasFeeMinter, ) = IAccessManager(accessManager).hasRole(Roles.HUB_FEE_MINTER_ROLE, admin); + assertTrue(hasFeeMinter); + + (bool hasDeficitEliminator, ) = IAccessManager(accessManager).hasRole( + Roles.HUB_DEFICIT_ELIMINATOR_ROLE, + admin + ); + assertTrue(hasDeficitEliminator); + } + + function test_setupHubRoles() public { + _grantAdminToWrapper(address(aaveV4HubRolesProcedureWrapper)); + aaveV4HubRolesProcedureWrapper.setupHubRoles({accessManager: accessManager, hub: hub}); + + assertEq( + IAccessManager(accessManager).getTargetFunctionRole(hub, IHub.mintFeeShares.selector), + Roles.HUB_FEE_MINTER_ROLE + ); + assertEq( + IAccessManager(accessManager).getTargetFunctionRole(hub, IHub.addAsset.selector), + Roles.HUB_CONFIGURATOR_ROLE + ); + assertEq( + IAccessManager(accessManager).getTargetFunctionRole(hub, IHub.eliminateDeficit.selector), + Roles.HUB_DEFICIT_ELIMINATOR_ROLE + ); + } + + function _grantAdminToWrapper(address wrapper) internal { + vm.prank(accessManagerAdmin); + IAccessManager(accessManager).grantRole(Roles.ACCESS_MANAGER_DEFAULT_ADMIN, wrapper, 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); + } + + function test_canCall_hubFeeMinterRole() public { + _grantAdminToWrapper(address(aaveV4HubRolesProcedureWrapper)); + aaveV4HubRolesProcedureWrapper.grantHubRole({ + accessManager: accessManager, + role: Roles.HUB_FEE_MINTER_ROLE, + admin: admin + }); + aaveV4HubRolesProcedureWrapper.setupHubFeeMinterRole({accessManager: accessManager, hub: hub}); + + bytes4[] memory selectors = aaveV4HubRolesProcedureWrapper.getHubFeeMinterRoleSelectors(); + _assertCanCall(hub, selectors); + } + + function test_canCall_hubConfiguratorRole() public { + _grantAdminToWrapper(address(aaveV4HubRolesProcedureWrapper)); + aaveV4HubRolesProcedureWrapper.grantHubRole({ + accessManager: accessManager, + role: Roles.HUB_CONFIGURATOR_ROLE, + admin: admin + }); + aaveV4HubRolesProcedureWrapper.setupHubConfiguratorRole({ + accessManager: accessManager, + hub: hub + }); + + bytes4[] memory selectors = aaveV4HubRolesProcedureWrapper.getHubConfiguratorRoleSelectors(); + _assertCanCall(hub, selectors); + } + + function test_setupHubDeficitEliminatorRole_reverts() public { + vm.expectRevert('zero address'); + aaveV4HubRolesProcedureWrapper.setupHubDeficitEliminatorRole({ + accessManager: address(0), + hub: hub + }); + + vm.expectRevert('zero address'); + aaveV4HubRolesProcedureWrapper.setupHubDeficitEliminatorRole({ + accessManager: accessManager, + hub: address(0) + }); + } + + function test_getHubDeficitEliminatorRoleSelectors() public view { + bytes4[] memory selectors = aaveV4HubRolesProcedureWrapper + .getHubDeficitEliminatorRoleSelectors(); + assertEq(selectors.length, 1); + assertEq(selectors[0], IHub.eliminateDeficit.selector); + } + + function test_canCall_hubDeficitEliminatorRole() public { + _grantAdminToWrapper(address(aaveV4HubRolesProcedureWrapper)); + aaveV4HubRolesProcedureWrapper.grantHubRole({ + accessManager: accessManager, + role: Roles.HUB_DEFICIT_ELIMINATOR_ROLE, + admin: admin + }); + aaveV4HubRolesProcedureWrapper.setupHubDeficitEliminatorRole({ + accessManager: accessManager, + hub: hub + }); + + bytes4[] memory selectors = aaveV4HubRolesProcedureWrapper + .getHubDeficitEliminatorRoleSelectors(); + _assertCanCall(hub, selectors); + } + + function test_canCall_hubAllRoles() public { + _grantAdminToWrapper(address(aaveV4HubRolesProcedureWrapper)); + aaveV4HubRolesProcedureWrapper.grantHubAllRoles({accessManager: accessManager, admin: admin}); + aaveV4HubRolesProcedureWrapper.setupHubRoles({accessManager: accessManager, hub: hub}); + + _assertCanCall(hub, aaveV4HubRolesProcedureWrapper.getHubFeeMinterRoleSelectors()); + _assertCanCall(hub, aaveV4HubRolesProcedureWrapper.getHubConfiguratorRoleSelectors()); + _assertCanCall(hub, aaveV4HubRolesProcedureWrapper.getHubDeficitEliminatorRoleSelectors()); + } +} diff --git a/tests/deployments/procedures/deploy/roles/AaveV4SpokeConfiguratorRolesProcedure.t.sol b/tests/deployments/procedures/deploy/roles/AaveV4SpokeConfiguratorRolesProcedure.t.sol new file mode 100644 index 000000000..ca9bb1cc7 --- /dev/null +++ b/tests/deployments/procedures/deploy/roles/AaveV4SpokeConfiguratorRolesProcedure.t.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/procedures/ProceduresBase.t.sol'; + +import {ISpokeConfigurator} from 'src/spoke/interfaces/ISpokeConfigurator.sol'; + +contract AaveV4SpokeConfiguratorRolesProcedureTest is ProceduresBase { + AaveV4SpokeConfiguratorRolesProcedureWrapper public wrapper; + address public spokeConfigurator = makeAddr('spokeConfigurator'); + + function setUp() public override { + super.setUp(); + wrapper = new AaveV4SpokeConfiguratorRolesProcedureWrapper(); + } + + function test_grantSpokeConfiguratorRole_reverts() public { + vm.expectRevert('zero address'); + wrapper.grantSpokeConfiguratorRole({ + accessManager: address(0), + role: Roles.SPOKE_CONFIGURATOR_DOMAIN_ADMIN_ROLE, + admin: admin + }); + + vm.expectRevert('zero address'); + wrapper.grantSpokeConfiguratorRole({ + accessManager: accessManager, + role: Roles.SPOKE_CONFIGURATOR_DOMAIN_ADMIN_ROLE, + admin: address(0) + }); + } + + function test_setupSpokeConfiguratorRoles_reverts() public { + vm.expectRevert('zero address'); + wrapper.setupSpokeConfiguratorRoles({ + accessManager: address(0), + spokeConfigurator: spokeConfigurator + }); + + vm.expectRevert('zero address'); + wrapper.setupSpokeConfiguratorRoles({ + accessManager: accessManager, + spokeConfigurator: address(0) + }); + } + + function test_setupSpokeConfiguratorRole_reverts() public { + bytes4[] memory selectors = wrapper.getSpokeConfiguratorDomainAdminRoleSelectors(); + + vm.expectRevert('zero address'); + wrapper.setupSpokeConfiguratorRole({ + accessManager: address(0), + spokeConfigurator: spokeConfigurator, + role: Roles.SPOKE_CONFIGURATOR_DOMAIN_ADMIN_ROLE, + selectors: selectors + }); + + vm.expectRevert('zero address'); + wrapper.setupSpokeConfiguratorRole({ + accessManager: accessManager, + spokeConfigurator: address(0), + role: Roles.SPOKE_CONFIGURATOR_DOMAIN_ADMIN_ROLE, + selectors: selectors + }); + } + + function test_grantSpokeConfiguratorAllRoles() public { + _grantAdminToWrapper(address(wrapper)); + wrapper.grantSpokeConfiguratorAllRoles({accessManager: accessManager, admin: admin}); + + (bool hasRole, ) = IAccessManager(accessManager).hasRole( + Roles.SPOKE_CONFIGURATOR_DOMAIN_ADMIN_ROLE, + admin + ); + assertTrue(hasRole); + } + + function test_setupSpokeConfiguratorRoles() public { + _grantAdminToWrapper(address(wrapper)); + wrapper.setupSpokeConfiguratorRoles({ + accessManager: accessManager, + spokeConfigurator: spokeConfigurator + }); + + bytes4[] memory selectors = wrapper.getSpokeConfiguratorDomainAdminRoleSelectors(); + for (uint256 i; i < selectors.length; i++) { + assertEq( + IAccessManager(accessManager).getTargetFunctionRole(spokeConfigurator, selectors[i]), + Roles.SPOKE_CONFIGURATOR_DOMAIN_ADMIN_ROLE + ); + } + } + + function _grantAdminToWrapper(address _wrapper) internal { + vm.prank(accessManagerAdmin); + IAccessManager(accessManager).grantRole(Roles.ACCESS_MANAGER_DEFAULT_ADMIN, _wrapper, 0); + } + + function test_getSpokeConfiguratorDomainAdminRoleSelectors() public view { + bytes4[] memory selectors = wrapper.getSpokeConfiguratorDomainAdminRoleSelectors(); + assertEq(selectors.length, 24); + assertEq(selectors[0], ISpokeConfigurator.updateReservePriceSource.selector); + assertEq(selectors[1], ISpokeConfigurator.updateLiquidationTargetHealthFactor.selector); + assertEq(selectors[2], ISpokeConfigurator.updateHealthFactorForMaxBonus.selector); + assertEq(selectors[3], ISpokeConfigurator.updateLiquidationBonusFactor.selector); + assertEq(selectors[4], ISpokeConfigurator.updateLiquidationConfig.selector); + assertEq(selectors[5], ISpokeConfigurator.addReserve.selector); + assertEq(selectors[6], ISpokeConfigurator.updatePaused.selector); + assertEq(selectors[7], ISpokeConfigurator.updateFrozen.selector); + assertEq(selectors[8], ISpokeConfigurator.updateBorrowable.selector); + assertEq(selectors[9], ISpokeConfigurator.updateReceiveSharesEnabled.selector); + assertEq(selectors[10], ISpokeConfigurator.updateCollateralRisk.selector); + assertEq(selectors[11], ISpokeConfigurator.addCollateralFactor.selector); + assertEq(selectors[12], ISpokeConfigurator.updateCollateralFactor.selector); + assertEq(selectors[13], ISpokeConfigurator.addMaxLiquidationBonus.selector); + assertEq(selectors[14], ISpokeConfigurator.updateMaxLiquidationBonus.selector); + assertEq(selectors[15], ISpokeConfigurator.addLiquidationFee.selector); + assertEq(selectors[16], ISpokeConfigurator.updateLiquidationFee.selector); + assertEq(selectors[17], ISpokeConfigurator.addDynamicReserveConfig.selector); + assertEq(selectors[18], ISpokeConfigurator.updateDynamicReserveConfig.selector); + assertEq(selectors[19], ISpokeConfigurator.pauseAllReserves.selector); + assertEq(selectors[20], ISpokeConfigurator.freezeAllReserves.selector); + assertEq(selectors[21], ISpokeConfigurator.pauseReserve.selector); + assertEq(selectors[22], ISpokeConfigurator.freezeReserve.selector); + assertEq(selectors[23], ISpokeConfigurator.updatePositionManager.selector); + } + + function test_canCall_spokeConfiguratorAllRoles() public { + _grantAdminToWrapper(address(wrapper)); + wrapper.grantSpokeConfiguratorAllRoles({accessManager: accessManager, admin: admin}); + wrapper.setupSpokeConfiguratorRoles({ + accessManager: accessManager, + spokeConfigurator: spokeConfigurator + }); + + _assertCanCall(spokeConfigurator, wrapper.getSpokeConfiguratorDomainAdminRoleSelectors()); + } +} 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..7401ce8bd --- /dev/null +++ b/tests/deployments/procedures/deploy/roles/AaveV4SpokeRolesProcedure.t.sol @@ -0,0 +1,237 @@ +// 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_grantSpokeAllRoles_reverts() public { + vm.expectRevert('zero address'); + aaveV4SpokeRolesProcedureWrapper.grantSpokeAllRoles({accessManager: address(0), admin: admin}); + + vm.expectRevert('zero address'); + aaveV4SpokeRolesProcedureWrapper.grantSpokeAllRoles({ + accessManager: accessManager, + admin: address(0) + }); + } + + function test_grantSpokeRole_reverts() public { + vm.expectRevert('zero address'); + aaveV4SpokeRolesProcedureWrapper.grantSpokeRole({ + accessManager: address(0), + role: Roles.SPOKE_USER_POSITION_UPDATER_ROLE, + admin: admin + }); + + vm.expectRevert('zero address'); + aaveV4SpokeRolesProcedureWrapper.grantSpokeRole({ + accessManager: accessManager, + role: Roles.SPOKE_USER_POSITION_UPDATER_ROLE, + admin: address(0) + }); + } + + function test_setupSpokeRoles_reverts() public { + vm.expectRevert('zero address'); + aaveV4SpokeRolesProcedureWrapper.setupSpokeRoles({accessManager: address(0), spoke: spoke}); + + vm.expectRevert('zero address'); + aaveV4SpokeRolesProcedureWrapper.setupSpokeRoles({ + accessManager: accessManager, + spoke: address(0) + }); + } + + function test_setupSpokePositionUpdaterRole_reverts() public { + vm.expectRevert('zero address'); + aaveV4SpokeRolesProcedureWrapper.setupSpokePositionUpdaterRole({ + accessManager: address(0), + spoke: spoke + }); + + vm.expectRevert('zero address'); + aaveV4SpokeRolesProcedureWrapper.setupSpokePositionUpdaterRole({ + accessManager: accessManager, + spoke: address(0) + }); + } + + function test_setupSpokeConfiguratorRole_reverts() public { + vm.expectRevert('zero address'); + aaveV4SpokeRolesProcedureWrapper.setupSpokeConfiguratorRole({ + accessManager: address(0), + spoke: spoke + }); + + vm.expectRevert('zero address'); + aaveV4SpokeRolesProcedureWrapper.setupSpokeConfiguratorRole({ + accessManager: accessManager, + spoke: address(0) + }); + } + + function test_grantSpokeAllRoles() public { + _grantAdminToWrapper(address(aaveV4SpokeRolesProcedureWrapper)); + aaveV4SpokeRolesProcedureWrapper.grantSpokeAllRoles({ + accessManager: accessManager, + admin: admin + }); + + (bool hasPositionUpdater, ) = IAccessManager(accessManager).hasRole( + Roles.SPOKE_USER_POSITION_UPDATER_ROLE, + admin + ); + assertTrue(hasPositionUpdater); + + (bool hasConfigurator, ) = IAccessManager(accessManager).hasRole( + Roles.SPOKE_CONFIGURATOR_ROLE, + admin + ); + assertTrue(hasConfigurator); + } + + function test_setupSpokeRoles() public { + _grantAdminToWrapper(address(aaveV4SpokeRolesProcedureWrapper)); + aaveV4SpokeRolesProcedureWrapper.setupSpokeRoles({accessManager: accessManager, spoke: spoke}); + + assertEq( + IAccessManager(accessManager).getTargetFunctionRole( + spoke, + ISpoke.updateUserDynamicConfig.selector + ), + Roles.SPOKE_USER_POSITION_UPDATER_ROLE + ); + assertEq( + IAccessManager(accessManager).getTargetFunctionRole(spoke, ISpoke.addReserve.selector), + Roles.SPOKE_CONFIGURATOR_ROLE + ); + } + + function _grantAdminToWrapper(address wrapper) internal { + vm.prank(accessManagerAdmin); + IAccessManager(accessManager).grantRole(Roles.ACCESS_MANAGER_DEFAULT_ADMIN, wrapper, 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); + } + + function test_canCall_spokePositionUpdaterRole() public { + _grantAdminToWrapper(address(aaveV4SpokeRolesProcedureWrapper)); + aaveV4SpokeRolesProcedureWrapper.grantSpokeRole({ + accessManager: accessManager, + role: Roles.SPOKE_USER_POSITION_UPDATER_ROLE, + admin: admin + }); + aaveV4SpokeRolesProcedureWrapper.setupSpokePositionUpdaterRole({ + accessManager: accessManager, + spoke: spoke + }); + + bytes4[] memory selectors = aaveV4SpokeRolesProcedureWrapper + .getSpokePositionUpdaterRoleSelectors(); + for (uint256 i = 0; i < selectors.length; i++) { + (bool allowed, uint32 delay) = IAccessManager(accessManager).canCall( + admin, + spoke, + selectors[i] + ); + assertTrue(allowed); + assertEq(delay, 0); + } + + address unauthorized = makeAddr('unauthorized'); + for (uint256 i = 0; i < selectors.length; i++) { + (bool allowed, ) = IAccessManager(accessManager).canCall(unauthorized, spoke, selectors[i]); + assertFalse(allowed); + } + } + + function test_canCall_spokeConfiguratorRole() public { + _grantAdminToWrapper(address(aaveV4SpokeRolesProcedureWrapper)); + aaveV4SpokeRolesProcedureWrapper.grantSpokeRole({ + accessManager: accessManager, + role: Roles.SPOKE_CONFIGURATOR_ROLE, + admin: admin + }); + aaveV4SpokeRolesProcedureWrapper.setupSpokeConfiguratorRole({ + accessManager: accessManager, + spoke: spoke + }); + + bytes4[] memory selectors = aaveV4SpokeRolesProcedureWrapper + .getSpokeConfiguratorRoleSelectors(); + for (uint256 i = 0; i < selectors.length; i++) { + (bool allowed, uint32 delay) = IAccessManager(accessManager).canCall( + admin, + spoke, + selectors[i] + ); + assertTrue(allowed); + assertEq(delay, 0); + } + + address unauthorized = makeAddr('unauthorized'); + for (uint256 i = 0; i < selectors.length; i++) { + (bool allowed, ) = IAccessManager(accessManager).canCall(unauthorized, spoke, selectors[i]); + assertFalse(allowed); + } + } + + function test_canCall_spokeAllRoles() public { + _grantAdminToWrapper(address(aaveV4SpokeRolesProcedureWrapper)); + aaveV4SpokeRolesProcedureWrapper.grantSpokeAllRoles({ + accessManager: accessManager, + admin: admin + }); + aaveV4SpokeRolesProcedureWrapper.setupSpokeRoles({accessManager: accessManager, spoke: spoke}); + + bytes4[] memory positionUpdaterSelectors = aaveV4SpokeRolesProcedureWrapper + .getSpokePositionUpdaterRoleSelectors(); + for (uint256 i = 0; i < positionUpdaterSelectors.length; i++) { + (bool allowed, uint32 delay) = IAccessManager(accessManager).canCall( + admin, + spoke, + positionUpdaterSelectors[i] + ); + assertTrue(allowed); + assertEq(delay, 0); + } + + bytes4[] memory configuratorSelectors = aaveV4SpokeRolesProcedureWrapper + .getSpokeConfiguratorRoleSelectors(); + for (uint256 i = 0; i < configuratorSelectors.length; i++) { + (bool allowed, uint32 delay) = IAccessManager(accessManager).canCall( + admin, + spoke, + configuratorSelectors[i] + ); + assertTrue(allowed); + assertEq(delay, 0); + } + } +} 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..0b4cdfd57 --- /dev/null +++ b/tests/deployments/procedures/deploy/spoke/AaveV4AaveOracleDeployProcedure.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 AaveV4AaveOracleDeployProcedureTest is ProceduresBase { + AaveV4AaveOracleDeployProcedureWrapper public aaveV4AaveOracleDeployProcedureWrapper; + function setUp() public override { + super.setUp(); + aaveV4AaveOracleDeployProcedureWrapper = new AaveV4AaveOracleDeployProcedureWrapper(); + } + + function test_deployAaveOracle() public { + address oracle = aaveV4AaveOracleDeployProcedureWrapper.deployAaveOracle(oracleDecimals); + assertNotEq(oracle, address(0)); + assertEq(IAaveOracle(oracle).decimals(), oracleDecimals); + } + + function test_deployAaveOracle_reverts_inputValidation() public { + vm.expectRevert('invalid oracle decimals'); + aaveV4AaveOracleDeployProcedureWrapper.deployAaveOracle({decimals: 0}); + } +} 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..7d010af58 --- /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(IAccessManaged(spokeConfigurator).authority(), owner); + } + + function test_deploySpokeConfigurator_reverts() public { + vm.expectRevert('invalid authority'); + 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..411f7397b --- /dev/null +++ b/tests/deployments/procedures/deploy/spoke/AaveV4SpokeDeployProcedure.t.sol @@ -0,0 +1,72 @@ +// 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_deployUpgradeableSpokeInstance() public { + (address spokeProxy, address spokeImplementation) = aaveV4SpokeDeployProcedureWrapper + .deployUpgradeableSpokeInstance( + owner, + accessManager, + aaveOracle, + spokeBytecode, + maxUserReservesLimit, + 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_deployUpgradeableSpokeInstance_reverts() public { + vm.expectRevert('invalid spoke proxy admin owner'); + aaveV4SpokeDeployProcedureWrapper.deployUpgradeableSpokeInstance({ + spokeProxyAdminOwner: address(0), + authority: accessManager, + oracle: aaveOracle, + spokeBytecode: spokeBytecode, + maxUserReservesLimit: maxUserReservesLimit, + salt: salt + }); + + vm.expectRevert('invalid authority'); + aaveV4SpokeDeployProcedureWrapper.deployUpgradeableSpokeInstance({ + spokeProxyAdminOwner: owner, + authority: address(0), + oracle: aaveOracle, + spokeBytecode: spokeBytecode, + maxUserReservesLimit: maxUserReservesLimit, + salt: salt + }); + + vm.expectRevert('invalid oracle'); + aaveV4SpokeDeployProcedureWrapper.deployUpgradeableSpokeInstance({ + spokeProxyAdminOwner: owner, + authority: accessManager, + oracle: address(0), + spokeBytecode: spokeBytecode, + maxUserReservesLimit: maxUserReservesLimit, + salt: salt + }); + + vm.expectRevert('invalid max user reserves limit'); + aaveV4SpokeDeployProcedureWrapper.deployUpgradeableSpokeInstance({ + spokeProxyAdminOwner: owner, + authority: accessManager, + oracle: aaveOracle, + spokeBytecode: spokeBytecode, + maxUserReservesLimit: 0, + salt: salt + }); + } +} diff --git a/tests/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeDeployProcedure.t.sol b/tests/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeDeployProcedure.t.sol new file mode 100644 index 000000000..ae1b2b8dc --- /dev/null +++ b/tests/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeDeployProcedure.t.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/deployments/procedures/ProceduresBase.t.sol'; + +contract AaveV4TokenizationSpokeDeployProcedureTest is ProceduresBase { + AaveV4TokenizationSpokeDeployProcedureWrapper public wrapper; + address public deployedHub; + uint256 public assetId; + address public underlying; + string public shareName = 'Test Vault Share'; + string public shareSymbol = 'tvDAI'; + + function setUp() public override { + super.setUp(); + wrapper = new AaveV4TokenizationSpokeDeployProcedureWrapper(); + + // TokenizationSpokeInstance constructor requires hub + AaveV4HubInstanceBatch hubInstanceBatch = new AaveV4HubInstanceBatch({ + hubProxyAdminOwner_: admin, + authority_: accessManager, + hubBytecode_: hubBytecode, + salt_: salt + }); + BatchReports.HubInstanceBatchReport memory hubReport = hubInstanceBatch.getReport(); + deployedHub = hubReport.hubProxy; + + // Deploy test ERC20 + TestnetERC20 testToken = new TestnetERC20('Test DAI', 'tDAI', 18); + underlying = address(testToken); + + // Setup Hub roles and add asset + vm.startPrank(accessManagerAdmin); + AaveV4HubRolesProcedure.setupHubAllRoles(accessManager, deployedHub); + IAccessManagerEnumerable(accessManager).grantRole(Roles.HUB_CONFIGURATOR_ROLE, admin, 0); + vm.stopPrank(); + + bytes memory irData = abi.encode( + IAssetInterestRateStrategy.InterestRateData({ + optimalUsageRatio: 90_00, + baseDrawnRate: 5_00, + rateGrowthBeforeOptimal: 5_00, + rateGrowthAfterOptimal: 5_00 + }) + ); + + vm.prank(admin); + assetId = IHub(deployedHub).addAsset({ + underlying: underlying, + decimals: 18, + feeReceiver: feeReceiver, + irStrategy: hubReport.irStrategy, + irData: irData + }); + } + + function test_deployUpgradeableTokenizationSpokeInstance() public { + (address tokenizationSpokeProxy, address tokenizationSpokeImplementation) = wrapper + .deployUpgradeableTokenizationSpokeInstance( + deployedHub, + underlying, + owner, + shareName, + shareSymbol, + salt + ); + assertNotEq(tokenizationSpokeProxy, address(0)); + assertNotEq(tokenizationSpokeImplementation, address(0)); + assertEq(Ownable(ProxyHelper.getProxyAdmin(tokenizationSpokeProxy)).owner(), owner); + assertEq( + ProxyHelper.getImplementation(tokenizationSpokeProxy), + tokenizationSpokeImplementation + ); + assertEq(ITokenizationSpoke(tokenizationSpokeProxy).hub(), deployedHub); + assertEq(ITokenizationSpoke(tokenizationSpokeProxy).assetId(), assetId); + assertEq(ITokenizationSpoke(tokenizationSpokeProxy).asset(), underlying); + } + + function test_deployUpgradeableTokenizationSpokeInstance_reverts() public { + vm.expectRevert('invalid hub'); + wrapper.deployUpgradeableTokenizationSpokeInstance({ + hub: address(0), + underlying: underlying, + spokeProxyAdminOwner: owner, + shareName: shareName, + shareSymbol: shareSymbol, + salt: salt + }); + + vm.expectRevert('invalid spoke proxy admin owner'); + wrapper.deployUpgradeableTokenizationSpokeInstance({ + hub: deployedHub, + underlying: underlying, + spokeProxyAdminOwner: address(0), + shareName: shareName, + shareSymbol: shareSymbol, + salt: keccak256('zeroAdminSalt') + }); + + vm.expectRevert('invalid share name'); + wrapper.deployUpgradeableTokenizationSpokeInstance({ + hub: deployedHub, + underlying: underlying, + spokeProxyAdminOwner: owner, + shareName: '', + shareSymbol: shareSymbol, + salt: keccak256('emptyNameSalt') + }); + + vm.expectRevert('invalid share symbol'); + wrapper.deployUpgradeableTokenizationSpokeInstance({ + hub: deployedHub, + underlying: underlying, + spokeProxyAdminOwner: owner, + shareName: shareName, + shareSymbol: '', + salt: keccak256('emptySymbolSalt') + }); + } + + function test_deployUpgradeableTokenizationSpokeInstance_revertsWith_failedCreate2FactoryCall() + public + { + vm.expectRevert(Create2Utils.FailedCreate2FactoryCall.selector); + wrapper.deployUpgradeableTokenizationSpokeInstance({ + hub: deployedHub, + underlying: makeAddr('nonExistentUnderlying'), + spokeProxyAdminOwner: owner, + shareName: shareName, + shareSymbol: shareSymbol, + salt: keccak256('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..a06734f1c --- /dev/null +++ b/tests/deployments/procedures/deploy/spoke/AaveV4TreasurySpokeDeployProcedure.t.sol @@ -0,0 +1,27 @@ +// 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, + salt + ); + assertEq(Ownable(treasurySpoke).owner(), owner); + assertEq(Ownable(ProxyHelper.getProxyAdmin(treasurySpoke)).owner(), owner); + } + + function test_deployTreasurySpoke_reverts() public { + vm.expectRevert('invalid owner'); + aaveV4TreasurySpokeDeployProcedureWrapper.deployTreasurySpoke({owner: 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..f1d032ef2 --- /dev/null +++ b/tests/deployments/utils/libraries/Create2Utils.t.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import { + Create2Utils, + Create2UtilsWrapper +} from 'tests/mocks/deployments/libraries/Create2UtilsWrapper.sol'; +import {Create2TestHelper} from 'tests/utils/Create2TestHelper.sol'; +import {TransparentUpgradeableProxy} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; + +contract Dummy { + constructor() {} +} + +contract Create2UtilsTest is Create2TestHelper { + Create2UtilsWrapper internal _create2UtilsWrapper; + function setUp() public { + _create2UtilsWrapper = new Create2UtilsWrapper(); + } + function testCreate2Deploy_revertsWith_missingCreate2Factory() public { + vm.expectRevert(Create2Utils.MissingCreate2Factory.selector); + _create2UtilsWrapper.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); + _create2UtilsWrapper.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); + _create2UtilsWrapper.create2Deploy(salt, bytecode); + } + + function testCreate2Deploy_revertsWith_contractAlreadyDeployed(bytes32 salt) public { + vm.assume(salt != bytes32(0)); + _etchCreate2Factory(); + bytes memory bytecode = type(Dummy).creationCode; + _create2UtilsWrapper.create2Deploy(salt, bytecode); + + // after already deployed, it should now revert + vm.expectRevert(Create2Utils.ContractAlreadyDeployed.selector); + _create2UtilsWrapper.create2Deploy(salt, bytecode); + } + + function testCreate2Deploy_fuzz(bytes32 salt) public { + vm.assume(salt != bytes32(0)); + _etchCreate2Factory(); + bytes memory bytecode = type(Dummy).creationCode; + + assertEq( + _create2UtilsWrapper.create2Deploy(salt, bytecode), + _create2UtilsWrapper.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( + _create2UtilsWrapper.proxify(salt, logic, initialOwner, initData), + _create2UtilsWrapper.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(_create2UtilsWrapper.isContractDeployed(addr)); + } + + function testIsContractDeployed() public { + address deployed = address(new Dummy()); + assertTrue(_create2UtilsWrapper.isContractDeployed(deployed)); + } + + function testComputeCreate2Address_fuzz(bytes32 salt, bytes32 initcode) public view { + vm.assume(salt != bytes32(0)); + vm.assume(initcode != bytes32(0)); + address expected = _create2UtilsWrapper.computeCreate2Address(salt, initcode); + assertEq(_create2UtilsWrapper.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 = _create2UtilsWrapper.computeCreate2Address( + salt, + keccak256(abi.encodePacked(bytecode)) + ); + assertEq(_create2UtilsWrapper.computeCreate2Address(salt, bytecode), expected); + } + + function testAddressFromLast20Bytes_fuzz(bytes32 bytesValue) public view { + vm.assume(bytesValue != bytes32(0)); + assertEq( + _create2UtilsWrapper.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 4dddbb1ae..dd9bb369b 100644 --- a/tests/gas/Gateways.Operations.gas.t.sol +++ b/tests/gas/Gateways.Operations.gas.t.sol @@ -12,8 +12,7 @@ contract NativeTokenGateway_Gas_Tests is Base { function setUp() public virtual override { super.setUp(); - initEnvironment(); - + _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 fd81ba4c1..26f0bfdb8 100644 --- a/tests/gas/Hub.Operations.gas.t.sol +++ b/tests/gas/Hub.Operations.gas.t.sol @@ -1,18 +1,13 @@ // SPDX-License-Identifier: MIT 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/PositionManagers.Operations.gas.t.sol b/tests/gas/PositionManagers.Operations.gas.t.sol index 33aab3121..2d7e151d8 100644 --- a/tests/gas/PositionManagers.Operations.gas.t.sol +++ b/tests/gas/PositionManagers.Operations.gas.t.sol @@ -11,8 +11,7 @@ contract PositionManager_Gas_Tests is SpokeBase { uint192 internal nonceKey = 0; function setUp() public virtual override { - deployFixtures(); - initEnvironment(); + super.setUp(); positionManager = new PositionManagerBaseWrapper(address(ADMIN)); @@ -60,8 +59,7 @@ contract GiverPositionManager_Gas_Tests is SpokeBase { GiverPositionManager public positionManager; function setUp() public virtual override { - deployFixtures(); - initEnvironment(); + super.setUp(); positionManager = new GiverPositionManager(address(ADMIN)); vm.prank(SPOKE_ADMIN); @@ -109,8 +107,7 @@ contract TakerPositionManager_Gas_Tests is SpokeBase { uint192 internal creditNonceKey = 1; function setUp() public virtual override { - deployFixtures(); - initEnvironment(); + super.setUp(); positionManager = new TakerPositionManager(address(ADMIN)); vm.prank(SPOKE_ADMIN); @@ -262,8 +259,7 @@ contract ConfigPositionManager_Gas_Tests is SpokeBase { ConfigPositionManager public positionManager; function setUp() public virtual override { - deployFixtures(); - initEnvironment(); + super.setUp(); positionManager = new ConfigPositionManager(address(ADMIN)); diff --git a/tests/gas/Spoke.Getters.gas.t.sol b/tests/gas/Spoke.Getters.gas.t.sol index 8855629fb..9a8654211 100644 --- a/tests/gas/Spoke.Getters.gas.t.sol +++ b/tests/gas/Spoke.Getters.gas.t.sol @@ -1,15 +1,10 @@ // SPDX-License-Identifier: MIT 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 1c7a13cb6..c139ab129 100644 --- a/tests/gas/Spoke.Operations.gas.t.sol +++ b/tests/gas/Spoke.Operations.gas.t.sol @@ -10,8 +10,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/AaveV4TestOrchestrationWrapper.sol b/tests/mocks/AaveV4TestOrchestrationWrapper.sol new file mode 100644 index 000000000..3b5c3e817 --- /dev/null +++ b/tests/mocks/AaveV4TestOrchestrationWrapper.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {AaveV4TestOrchestration} from 'tests/deployments/orchestration/AaveV4TestOrchestration.sol'; + +contract AaveV4TestOrchestrationWrapper { + function deploySpokeImplementation( + address oracle, + uint16 maxUserReservesLimit + ) external returns (address) { + return address(AaveV4TestOrchestration.deploySpokeImplementation(oracle, maxUserReservesLimit)); + } + + function deployHub(address authority, address proxyAdminOwner) external returns (address) { + return address(AaveV4TestOrchestration.deployHub(authority, proxyAdminOwner)); + } +} diff --git a/tests/mocks/DeployWrapper.sol b/tests/mocks/DeployWrapper.sol deleted file mode 100644 index d77f09b41..000000000 --- a/tests/mocks/DeployWrapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import {DeployUtils} from 'tests/DeployUtils.sol'; - -contract DeployWrapper { - function deploySpokeImplementation( - address oracle, - uint16 maxUserReservesLimit - ) external returns (address) { - return address(DeployUtils.deploySpokeImplementation(oracle, maxUserReservesLimit, '')); - } - - function deploySpoke( - address oracle, - uint16 maxUserReservesLimit, - address proxyAdminOwner, - bytes calldata initData - ) external returns (address) { - return - address(DeployUtils.deploySpoke(oracle, maxUserReservesLimit, proxyAdminOwner, initData)); - } - - function deployHub(address authority, address proxyAdminOwner) external returns (address) { - return address(DeployUtils.deployHub({authority: authority, proxyAdminOwner: proxyAdminOwner})); - } -} diff --git a/tests/mocks/JsonBindings.sol b/tests/mocks/JsonBindings.sol index 42f360b5e..454b218ea 100644 --- a/tests/mocks/JsonBindings.sol +++ b/tests/mocks/JsonBindings.sol @@ -37,6 +37,7 @@ interface Vm { ) external returns (string memory json); } +// solhint-disable quotes library JsonBindings { Vm constant vm = Vm(address(uint160(uint256(keccak256('hevm cheat code'))))); diff --git a/tests/mocks/LiquidationLogicWrapper.sol b/tests/mocks/LiquidationLogicWrapper.sol index 88cbaaca8..91d0d066d 100644 --- a/tests/mocks/LiquidationLogicWrapper.sol +++ b/tests/mocks/LiquidationLogicWrapper.sol @@ -11,6 +11,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..276c05466 --- /dev/null +++ b/tests/mocks/deployments/libraries/Create2UtilsWrapper.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.20; + +import {TransparentUpgradeableProxy} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.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 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..683a2ff58 --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4AaveOracleDeployProcedureWrapper.sol @@ -0,0 +1,13 @@ +// 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(uint8 decimals) external returns (address) { + return _deployAaveOracle(decimals); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4AccessManagerEnumerableDeployProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4AccessManagerEnumerableDeployProcedureWrapper.sol new file mode 100644 index 000000000..39bfad667 --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4AccessManagerEnumerableDeployProcedureWrapper.sol @@ -0,0 +1,14 @@ +// 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..6afec74af --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4AccessManagerRolesProcedureWrapper.sol @@ -0,0 +1,25 @@ +// 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 replaceDefaultAdminRole( + address accessManager, + address adminToAdd, + address adminToRemove + ) external { + AaveV4AccessManagerRolesProcedure.replaceDefaultAdminRole( + accessManager, + adminToAdd, + adminToRemove + ); + } + + function grantAccessManagerAdminRole(address accessManager, address admin) external { + AaveV4AccessManagerRolesProcedure.grantAccessManagerAdminRole(accessManager, admin); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4HubConfiguratorDeployProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4HubConfiguratorDeployProcedureWrapper.sol new file mode 100644 index 000000000..ed81354d9 --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4HubConfiguratorDeployProcedureWrapper.sol @@ -0,0 +1,13 @@ +// 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 authority, bytes32 salt) external returns (address) { + return _deployHubConfigurator(authority, salt); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4HubConfiguratorRolesProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4HubConfiguratorRolesProcedureWrapper.sol new file mode 100644 index 000000000..723aa0107 --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4HubConfiguratorRolesProcedureWrapper.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {AaveV4HubConfiguratorRolesProcedure} from 'src/deployments/procedures/roles/AaveV4HubConfiguratorRolesProcedure.sol'; +import {Roles} from 'src/deployments/utils/libraries/Roles.sol'; + +contract AaveV4HubConfiguratorRolesProcedureWrapper { + bool public IS_TEST = true; + + function grantHubConfiguratorAllRoles(address accessManager, address admin) external { + AaveV4HubConfiguratorRolesProcedure.grantHubConfiguratorAllRoles(accessManager, admin); + } + + function grantHubConfiguratorRole(address accessManager, uint64 role, address admin) external { + AaveV4HubConfiguratorRolesProcedure.grantHubConfiguratorRole(accessManager, role, admin); + } + + function setupHubConfiguratorAllRoles(address accessManager, address hubConfigurator) external { + AaveV4HubConfiguratorRolesProcedure.setupHubConfiguratorAllRoles( + accessManager, + hubConfigurator + ); + } + + function setupHubConfiguratorRole( + address accessManager, + address hubConfigurator, + uint64 role, + bytes4[] memory selectors + ) external { + AaveV4HubConfiguratorRolesProcedure.setupHubConfiguratorRole( + accessManager, + hubConfigurator, + role, + selectors + ); + } + + function getHubConfiguratorDomainAdminRoleSelectors() external pure returns (bytes4[] memory) { + return Roles.getHubConfiguratorDomainAdminRoleSelectors(); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4HubDeployProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4HubDeployProcedureWrapper.sol new file mode 100644 index 000000000..ddba1230d --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4HubDeployProcedureWrapper.sol @@ -0,0 +1,18 @@ +// 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 hubProxyAdminOwner, + address authority, + bytes memory hubBytecode, + bytes32 salt + ) external returns (address hubProxy, address hubImplementation) { + return _deployUpgradeableHubInstance(hubProxyAdminOwner, authority, hubBytecode, salt); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4HubRolesProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4HubRolesProcedureWrapper.sol new file mode 100644 index 000000000..1b3e06d7b --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4HubRolesProcedureWrapper.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {AaveV4HubRolesProcedure} from 'src/deployments/procedures/roles/AaveV4HubRolesProcedure.sol'; +import {Roles} from 'src/deployments/utils/libraries/Roles.sol'; + +contract AaveV4HubRolesProcedureWrapper { + bool public IS_TEST = true; + + function grantHubAllRoles(address accessManager, address admin) external { + AaveV4HubRolesProcedure.grantHubAllRoles(accessManager, admin); + } + + function grantHubRole(address accessManager, uint64 role, address admin) external { + AaveV4HubRolesProcedure.grantHubRole(accessManager, role, admin); + } + + function setupHubRoles(address accessManager, address hub) external { + AaveV4HubRolesProcedure.setupHubAllRoles(accessManager, hub); + } + + function setupHubFeeMinterRole(address accessManager, address hub) external { + AaveV4HubRolesProcedure.setupHubRole( + accessManager, + hub, + Roles.HUB_FEE_MINTER_ROLE, + Roles.getHubFeeMinterRoleSelectors() + ); + } + + function setupHubConfiguratorRole(address accessManager, address hub) external { + AaveV4HubRolesProcedure.setupHubRole( + accessManager, + hub, + Roles.HUB_CONFIGURATOR_ROLE, + Roles.getHubConfiguratorRoleSelectors() + ); + } + + function getHubFeeMinterRoleSelectors() external pure returns (bytes4[] memory) { + return Roles.getHubFeeMinterRoleSelectors(); + } + + function setupHubDeficitEliminatorRole(address accessManager, address hub) external { + AaveV4HubRolesProcedure.setupHubRole( + accessManager, + hub, + Roles.HUB_DEFICIT_ELIMINATOR_ROLE, + Roles.getHubDeficitEliminatorRoleSelectors() + ); + } + + function getHubConfiguratorRoleSelectors() external pure returns (bytes4[] memory) { + return Roles.getHubConfiguratorRoleSelectors(); + } + + function getHubDeficitEliminatorRoleSelectors() external pure returns (bytes4[] memory) { + return Roles.getHubDeficitEliminatorRoleSelectors(); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4InterestRateStrategyDeployProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4InterestRateStrategyDeployProcedureWrapper.sol new file mode 100644 index 000000000..d3296d95e --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4InterestRateStrategyDeployProcedureWrapper.sol @@ -0,0 +1,15 @@ +// 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..b184c9ae1 --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4NativeTokenGatewayDeployProcedureWrapper.sol @@ -0,0 +1,17 @@ +// 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..5eccc0b45 --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4SignatureGatewayDeployProcedureWrapper.sol @@ -0,0 +1,13 @@ +// 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..a59aa4255 --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4SpokeConfiguratorDeployProcedureWrapper.sol @@ -0,0 +1,13 @@ +// 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 authority, bytes32 salt) external returns (address) { + return _deploySpokeConfigurator(authority, salt); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4SpokeConfiguratorRolesProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4SpokeConfiguratorRolesProcedureWrapper.sol new file mode 100644 index 000000000..095f9c91a --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4SpokeConfiguratorRolesProcedureWrapper.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {AaveV4SpokeConfiguratorRolesProcedure} from 'src/deployments/procedures/roles/AaveV4SpokeConfiguratorRolesProcedure.sol'; +import {Roles} from 'src/deployments/utils/libraries/Roles.sol'; + +contract AaveV4SpokeConfiguratorRolesProcedureWrapper { + bool public IS_TEST = true; + + function grantSpokeConfiguratorAllRoles(address accessManager, address admin) external { + AaveV4SpokeConfiguratorRolesProcedure.grantSpokeConfiguratorAllRoles(accessManager, admin); + } + + function grantSpokeConfiguratorRole(address accessManager, uint64 role, address admin) external { + AaveV4SpokeConfiguratorRolesProcedure.grantSpokeConfiguratorRole(accessManager, role, admin); + } + + function setupSpokeConfiguratorRoles(address accessManager, address spokeConfigurator) external { + AaveV4SpokeConfiguratorRolesProcedure.setupSpokeConfiguratorAllRoles( + accessManager, + spokeConfigurator + ); + } + + function setupSpokeConfiguratorRole( + address accessManager, + address spokeConfigurator, + uint64 role, + bytes4[] memory selectors + ) external { + AaveV4SpokeConfiguratorRolesProcedure.setupSpokeConfiguratorRole( + accessManager, + spokeConfigurator, + role, + selectors + ); + } + + function getSpokeConfiguratorDomainAdminRoleSelectors() external pure returns (bytes4[] memory) { + return Roles.getSpokeConfiguratorDomainAdminRoleSelectors(); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4SpokeDeployProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4SpokeDeployProcedureWrapper.sol new file mode 100644 index 000000000..6594f6d72 --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4SpokeDeployProcedureWrapper.sol @@ -0,0 +1,28 @@ +// 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 deployUpgradeableSpokeInstance( + address spokeProxyAdminOwner, + address authority, + address oracle, + bytes memory spokeBytecode, + uint16 maxUserReservesLimit, + bytes32 salt + ) external returns (address spokeProxy, address spokeImplementation) { + return + _deployUpgradeableSpokeInstance( + spokeProxyAdminOwner, + authority, + oracle, + spokeBytecode, + maxUserReservesLimit, + salt + ); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4SpokeRolesProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4SpokeRolesProcedureWrapper.sol new file mode 100644 index 000000000..a90cd5e2b --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4SpokeRolesProcedureWrapper.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {AaveV4SpokeRolesProcedure} from 'src/deployments/procedures/roles/AaveV4SpokeRolesProcedure.sol'; +import {Roles} from 'src/deployments/utils/libraries/Roles.sol'; + +contract AaveV4SpokeRolesProcedureWrapper { + bool public IS_TEST = true; + + function grantSpokeAllRoles(address accessManager, address admin) external { + AaveV4SpokeRolesProcedure.grantSpokeAllRoles(accessManager, admin); + } + + function grantSpokeRole(address accessManager, uint64 role, address admin) external { + AaveV4SpokeRolesProcedure.grantSpokeRole(accessManager, role, admin); + } + + function setupSpokeRoles(address accessManager, address spoke) external { + AaveV4SpokeRolesProcedure.setupSpokeAllRoles(accessManager, spoke); + } + + function setupSpokePositionUpdaterRole(address accessManager, address spoke) external { + AaveV4SpokeRolesProcedure.setupSpokeRole( + accessManager, + spoke, + Roles.SPOKE_USER_POSITION_UPDATER_ROLE, + Roles.getSpokePositionUpdaterRoleSelectors() + ); + } + + function setupSpokeConfiguratorRole(address accessManager, address spoke) external { + AaveV4SpokeRolesProcedure.setupSpokeRole( + accessManager, + spoke, + Roles.SPOKE_CONFIGURATOR_ROLE, + Roles.getSpokeConfiguratorRoleSelectors() + ); + } + + function getSpokePositionUpdaterRoleSelectors() external pure returns (bytes4[] memory) { + return Roles.getSpokePositionUpdaterRoleSelectors(); + } + + function getSpokeConfiguratorRoleSelectors() external pure returns (bytes4[] memory) { + return Roles.getSpokeConfiguratorRoleSelectors(); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4TokenizationSpokeDeployProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4TokenizationSpokeDeployProcedureWrapper.sol new file mode 100644 index 000000000..e6689f827 --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4TokenizationSpokeDeployProcedureWrapper.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {AaveV4TokenizationSpokeDeployProcedure} from 'src/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeDeployProcedure.sol'; + +contract AaveV4TokenizationSpokeDeployProcedureWrapper is AaveV4TokenizationSpokeDeployProcedure { + bool public IS_TEST = true; + + function deployUpgradeableTokenizationSpokeInstance( + address hub, + address underlying, + address spokeProxyAdminOwner, + string memory shareName, + string memory shareSymbol, + bytes32 salt + ) external returns (address tokenizationSpokeProxy, address tokenizationSpokeImplementation) { + return + _deployUpgradeableTokenizationSpokeInstance( + hub, + underlying, + spokeProxyAdminOwner, + shareName, + shareSymbol, + salt + ); + } +} diff --git a/tests/mocks/deployments/procedures/AaveV4TreasurySpokeDeployProcedureWrapper.sol b/tests/mocks/deployments/procedures/AaveV4TreasurySpokeDeployProcedureWrapper.sol new file mode 100644 index 000000000..f001fb91d --- /dev/null +++ b/tests/mocks/deployments/procedures/AaveV4TreasurySpokeDeployProcedureWrapper.sol @@ -0,0 +1,13 @@ +// 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, bytes32 salt) external returns (address) { + return _deployTreasurySpoke(owner, salt); + } +} diff --git a/tests/scripts/AaveV4DeployBatchBaseScript.t.sol b/tests/scripts/AaveV4DeployBatchBaseScript.t.sol new file mode 100644 index 000000000..772a76501 --- /dev/null +++ b/tests/scripts/AaveV4DeployBatchBaseScript.t.sol @@ -0,0 +1,353 @@ +// 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 {InputUtils} from 'src/deployments/utils/InputUtils.sol'; +import {Constants} from 'tests/Constants.sol'; +import {WETH9} from 'src/dependencies/weth/WETH9.sol'; + +contract AaveV4DeployBatchBaseScriptHarness is AaveV4DeployBatchBaseScript { + // use harness to expose internal functions for testing + + constructor() AaveV4DeployBatchBaseScript('out.json') {} + + function loadWarningsAndSanitizeInputs( + InputUtils.FullDeployInputs memory inputs, + address deployer + ) public returns (InputUtils.FullDeployInputs memory) { + return _loadWarningsAndSanitizeInputs(inputs, deployer); + } + + function logWarning(string memory warning) public { + _logWarning(warning); + } + + function _getDeployInputs() internal pure override returns (InputUtils.FullDeployInputs memory) { + revert('not implemented'); + } + + function _executeUserPrompt() internal override {} +} + +contract AaveV4DeployBatchBaseScriptTest is Test { + AaveV4DeployBatchBaseScriptHarness internal _harness; + InputUtils.FullDeployInputs internal _inputs; + address internal _deployer; + + function setUp() public { + _harness = new AaveV4DeployBatchBaseScriptHarness(); + + _inputs = InputUtils.FullDeployInputs({ + accessManagerAdmin: makeAddr('accessManagerAdmin'), + hubAdmin: makeAddr('hubAdmin'), + hubConfiguratorAdmin: makeAddr('hubConfiguratorAdmin'), + treasurySpokeOwner: makeAddr('treasurySpokeOwner'), + spokeAdmin: makeAddr('spokeAdmin'), + hubProxyAdminOwner: makeAddr('hubProxyAdminOwner'), + spokeProxyAdminOwner: makeAddr('spokeProxyAdminOwner'), + spokeConfiguratorAdmin: makeAddr('spokeConfiguratorAdmin'), + gatewayOwner: makeAddr('gatewayOwner'), + positionManagerOwner: makeAddr('positionManagerOwner'), + nativeWrapper: address(new WETH9()), + deployNativeTokenGateway: true, + deploySignatureGateway: true, + deployPositionManagers: true, + grantRoles: true, + hubLabels: _toArray('hub1', 'hub2', 'hub3'), + spokeLabels: _toArray('spoke1', 'spoke2', 'spoke3'), + spokeMaxReservesLimits: _defaultSpokeMaxReservesLimits(3), + salt: bytes32(0) + }); + + _deployer = makeAddr('deployer'); + } + + function test_loadWarningsAndSanitizeInputs() public { + InputUtils.FullDeployInputs memory expected = _inputs; + InputUtils.FullDeployInputs memory sanitized = _harness.loadWarningsAndSanitizeInputs( + _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( + _inputs, + _deployer + ); + InputUtils.FullDeployInputs memory expected = _inputs; + if (grantRoles) { + expected.accessManagerAdmin = _deployer; + } else { + expected.treasurySpokeOwner = _deployer; + expected.hubProxyAdminOwner = _deployer; + expected.spokeProxyAdminOwner = _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( + _inputs, + _deployer + ); + InputUtils.FullDeployInputs memory expected = _inputs; + if (grantRoles) { + expected.hubAdmin = _deployer; + } else { + // when grantRoles=false, treasurySpokeOwner and spokeProxyAdminOwner always default to deployer + expected.treasurySpokeOwner = _deployer; + expected.hubProxyAdminOwner = _deployer; + expected.spokeProxyAdminOwner = _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( + _inputs, + _deployer + ); + InputUtils.FullDeployInputs memory expected = _inputs; + if (grantRoles) { + expected.spokeAdmin = _deployer; + } else { + expected.treasurySpokeOwner = _deployer; + expected.hubProxyAdminOwner = _deployer; + expected.spokeProxyAdminOwner = _deployer; + } + assertEq(sanitized, expected); + } + + function test_loadWarningsAndSanitizeInputs_withZeroHubConfiguratorAdmin_fuzz( + bool grantRoles + ) public { + _inputs.hubConfiguratorAdmin = address(0); + _inputs.grantRoles = grantRoles; + InputUtils.FullDeployInputs memory sanitized = _harness.loadWarningsAndSanitizeInputs( + _inputs, + _deployer + ); + InputUtils.FullDeployInputs memory expected = _inputs; + if (grantRoles) { + expected.hubConfiguratorAdmin = _deployer; + } else { + expected.treasurySpokeOwner = _deployer; + expected.hubProxyAdminOwner = _deployer; + expected.spokeProxyAdminOwner = _deployer; + } + assertEq(sanitized, expected); + } + + function test_loadWarningsAndSanitizeInputs_withZeroSpokeConfiguratorAdmin_fuzz( + bool grantRoles + ) public { + _inputs.spokeConfiguratorAdmin = address(0); + _inputs.grantRoles = grantRoles; + InputUtils.FullDeployInputs memory sanitized = _harness.loadWarningsAndSanitizeInputs( + _inputs, + _deployer + ); + InputUtils.FullDeployInputs memory expected = _inputs; + if (grantRoles) { + expected.spokeConfiguratorAdmin = _deployer; + } else { + expected.treasurySpokeOwner = _deployer; + expected.hubProxyAdminOwner = _deployer; + expected.spokeProxyAdminOwner = _deployer; + } + assertEq(sanitized, expected); + } + + function test_loadWarningsAndSanitizeInputs_withZeroHubProxyAdminOwner_fuzz( + bool grantRoles + ) public { + _inputs.hubProxyAdminOwner = address(0); + _inputs.grantRoles = grantRoles; + InputUtils.FullDeployInputs memory sanitized = _harness.loadWarningsAndSanitizeInputs( + _inputs, + _deployer + ); + + InputUtils.FullDeployInputs memory expected = _inputs; + // hubProxyAdminOwner always defaults to deployer (in both grantRoles branches) + expected.hubProxyAdminOwner = _deployer; + if (!grantRoles) { + expected.treasurySpokeOwner = _deployer; + expected.hubProxyAdminOwner = _deployer; + expected.spokeProxyAdminOwner = _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( + _inputs, + _deployer + ); + + InputUtils.FullDeployInputs memory expected = _inputs; + // spokeProxyAdminOwner always defaults to deployer (in both grantRoles branches) + expected.spokeProxyAdminOwner = _deployer; + if (!grantRoles) { + expected.treasurySpokeOwner = _deployer; + expected.hubProxyAdminOwner = _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( + _inputs, + _deployer + ); + InputUtils.FullDeployInputs memory expected = _inputs; + // treasurySpokeOwner always defaults to deployer (in both grantRoles branches) + expected.treasurySpokeOwner = _deployer; + if (!grantRoles) { + expected.hubProxyAdminOwner = _deployer; + expected.spokeProxyAdminOwner = _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( + _inputs, + _deployer + ); + InputUtils.FullDeployInputs memory expected = _inputs; + expected.gatewayOwner = _deployer; + if (!grantRoles) { + expected.treasurySpokeOwner = _deployer; + expected.hubProxyAdminOwner = _deployer; + expected.spokeProxyAdminOwner = _deployer; + } + assertEq(sanitized, expected); + } + + function test_loadWarningsAndSanitizeInputs_withZeroPositionManagerOwner_fuzz( + bool grantRoles + ) public { + _inputs.positionManagerOwner = address(0); + _inputs.grantRoles = grantRoles; + InputUtils.FullDeployInputs memory sanitized = _harness.loadWarningsAndSanitizeInputs( + _inputs, + _deployer + ); + InputUtils.FullDeployInputs memory expected = _inputs; + expected.positionManagerOwner = _deployer; + if (!grantRoles) { + expected.treasurySpokeOwner = _deployer; + expected.hubProxyAdminOwner = _deployer; + expected.spokeProxyAdminOwner = _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( + _inputs, + _deployer + ); + InputUtils.FullDeployInputs memory expected = _inputs; + expected.nativeWrapper = address(0); + if (!grantRoles) { + expected.treasurySpokeOwner = _deployer; + expected.hubProxyAdminOwner = _deployer; + expected.spokeProxyAdminOwner = _deployer; + } + assertEq(sanitized, expected); + } + + function test_loadWarningsAndSanitizeInputs_revertsWith_duplicateHubLabel() public { + _inputs.hubLabels = ['hub1', 'hub2', 'hub1']; + vm.expectRevert('duplicate hub label: hub1'); + _harness.loadWarningsAndSanitizeInputs(_inputs, _deployer); + } + + function test_loadWarningsAndSanitizeInputs_revertsWith_duplicateSpokeLabel() public { + _inputs.spokeLabels = ['spoke1', 'spoke1']; + _inputs.spokeMaxReservesLimits = _defaultSpokeMaxReservesLimits(2); + + vm.expectRevert('duplicate spoke label: spoke1'); + _harness.loadWarningsAndSanitizeInputs(_inputs, _deployer); + } + + function test_loadWarningsAndSanitizeInputs_withZeroSalt() public { + _inputs.salt = bytes32(0); + InputUtils.FullDeployInputs memory sanitized = _harness.loadWarningsAndSanitizeInputs( + _inputs, + _deployer + ); + InputUtils.FullDeployInputs memory expected = _inputs; + 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.hubConfiguratorAdmin, b.hubConfiguratorAdmin, 'hub configurator admin'); + assertEq(a.treasurySpokeOwner, b.treasurySpokeOwner, 'treasury spoke owner'); + assertEq(a.hubProxyAdminOwner, b.hubProxyAdminOwner, 'hub proxy admin owner'); + assertEq(a.spokeProxyAdminOwner, b.spokeProxyAdminOwner, 'spoke proxy admin owner'); + assertEq(a.spokeConfiguratorAdmin, b.spokeConfiguratorAdmin, 'spoke configurator admin'); + assertEq(a.spokeAdmin, b.spokeAdmin, 'spoke admin'); + assertEq(a.gatewayOwner, b.gatewayOwner, 'gateway owner'); + assertEq(a.positionManagerOwner, b.positionManagerOwner, 'position manager owner'); + assertEq(a.nativeWrapper, b.nativeWrapper, 'native wrapper'); + assertEq(a.deployNativeTokenGateway, b.deployNativeTokenGateway, 'deploy native token gateway'); + assertEq(a.deploySignatureGateway, b.deploySignatureGateway, 'deploy signature gateway'); + assertEq(a.deployPositionManagers, b.deployPositionManagers, 'deploy position managers'); + assertEq(a.grantRoles, b.grantRoles, 'grant roles'); + assertEq(a.hubLabels, b.hubLabels, 'hub labels'); + assertEq(a.spokeLabels, b.spokeLabels, 'spoke labels'); + assertEq(a.salt, b.salt, 'salt'); + assertEq(abi.encode(a), abi.encode(b)); + } + + function _defaultSpokeMaxReservesLimits( + uint256 count + ) internal pure returns (uint16[] memory limits) { + limits = new uint16[](count); + for (uint256 i; i < count; i++) { + limits[i] = Constants.MAX_ALLOWED_USER_RESERVES_LIMIT; + } + } + + function _toArray( + string memory a, + string memory b, + string memory c + ) internal pure returns (string[] memory arr) { + arr = new string[](3); + arr[0] = a; + arr[1] = b; + arr[2] = c; + } +} diff --git a/tests/unit/AaveOracle.t.sol b/tests/unit/AaveOracle.t.sol index e54ffd49b..83ee29d85 100644 --- a/tests/unit/AaveOracle.t.sol +++ b/tests/unit/AaveOracle.t.sol @@ -22,13 +22,13 @@ contract AaveOracleTest is Base { uint256 private constant reserveId2 = 1; function setUp() public override { - deployFixtures(); + super.setUp(); vm.startPrank(deployer); oracle = new AaveOracle(_oracleDecimals); spoke1 = ISpoke( address( - DeployUtils.deploySpokeImplementation( + AaveV4TestOrchestration.deploySpokeImplementation( address(oracle), Constants.MAX_ALLOWED_USER_RESERVES_LIMIT ) @@ -84,7 +84,7 @@ contract AaveOracleTest is Base { oracle = new AaveOracle(_oracleDecimals); address newSpoke = address( - DeployUtils.deploySpokeImplementation( + AaveV4TestOrchestration.deploySpokeImplementation( address(oracle), Constants.MAX_ALLOWED_USER_RESERVES_LIMIT ) @@ -149,7 +149,7 @@ contract AaveOracleTest is Base { // set new spoke to a separate oracle address mismatchOracle = address(new AaveOracle(_oracleDecimals)); address newSpoke = address( - DeployUtils.deploySpokeImplementation( + AaveV4TestOrchestration.deploySpokeImplementation( mismatchOracle, Constants.MAX_ALLOWED_USER_RESERVES_LIMIT ) diff --git a/tests/unit/AssetInterestRateStrategy.t.sol b/tests/unit/AssetInterestRateStrategy.t.sol index 317efd900..b90e871af 100644 --- a/tests/unit/AssetInterestRateStrategy.t.sol +++ b/tests/unit/AssetInterestRateStrategy.t.sol @@ -14,7 +14,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 e0ceacd0b..5cdaee008 100644 --- a/tests/unit/Hub/Hub.Access.t.sol +++ b/tests/unit/Hub/Hub.Access.t.sol @@ -124,7 +124,11 @@ contract HubAccessTest is HubBase { bytes4[] memory hubSelectors = new bytes4[](1); hubSelectors[0] = IHub.setInterestRateData.selector; vm.prank(ADMIN); - accessManager.setTargetFunctionRole(address(hub1), hubSelectors, Roles.DEFAULT_ADMIN_ROLE); + accessManager.setTargetFunctionRole( + address(hub1), + hubSelectors, + Roles.ACCESS_MANAGER_DEFAULT_ADMIN + ); // The old role (HUB_ADMIN) should no longer have access vm.expectRevert( @@ -163,13 +167,10 @@ contract HubAccessTest is HubBase { }) ); - // Say addresses Alice, Bob, and Carol all have the HUB_ADMIN role, allowing them to set drawn 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(); + // Say addresses Alice, Bob, and Carol all have the HUB_ADMIN role, allowing them to set interest rate data. + _grantHubAdminRole(hub1, alice); + _grantHubAdminRole(hub1, bob); + _grantHubAdminRole(hub1, carol); vm.prank(alice); hub1.setInterestRateData(daiAssetId, encodedIrData); @@ -178,8 +179,8 @@ contract HubAccessTest is HubBase { vm.prank(carol); hub1.setInterestRateData(daiAssetId, encodedIrData); - // Now, we change the role responsible for setting drawn rate data to SET_INTEREST_RATE role. - uint64 SET_INTEREST_RATE_ROLE = 4; + // Now, we change the role responsible for setting interest rate data to SET_INTEREST_RATE role. + uint64 SET_INTEREST_RATE_ROLE = 100; bytes4[] memory hubSelectors = new bytes4[](1); hubSelectors[0] = IHub.setInterestRateData.selector; vm.prank(ADMIN); @@ -215,27 +216,27 @@ contract HubAccessTest is HubBase { vm.prank(carol); hub1.setInterestRateData(daiAssetId, encodedIrData); - // Alice, Bob, and Carol currently have both HUB_ADMIN and SET_INTEREST_RATE roles. + // Alice, Bob, and Carol currently have both HUB_CONFIGURATOR 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_CONFIGURATOR_ROLE, alice)); + assertTrue(_hasRole(accessManager, Roles.HUB_CONFIGURATOR_ROLE, bob)); + assertTrue(_hasRole(accessManager, Roles.HUB_CONFIGURATOR_ROLE, carol)); assertTrue(_hasRole(accessManager, SET_INTEREST_RATE_ROLE, alice)); assertTrue(_hasRole(accessManager, SET_INTEREST_RATE_ROLE, bob)); assertTrue(_hasRole(accessManager, SET_INTEREST_RATE_ROLE, carol)); - // We can remove HUB_ADMIN role from Alice, Bob, and Carol. + // We can remove HUB_CONFIGURATOR 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_CONFIGURATOR_ROLE, alice); + accessManager.revokeRole(Roles.HUB_CONFIGURATOR_ROLE, bob); + accessManager.revokeRole(Roles.HUB_CONFIGURATOR_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)); + // Alice, Bob, and Carol should no longer have HUB_CONFIGURATOR role. + assertFalse(_hasRole(accessManager, Roles.HUB_CONFIGURATOR_ROLE, alice)); + assertFalse(_hasRole(accessManager, Roles.HUB_CONFIGURATOR_ROLE, bob)); + assertFalse(_hasRole(accessManager, Roles.HUB_CONFIGURATOR_ROLE, carol)); // Can still call setInterestRateData since they have SET_INTEREST_RATE role. vm.prank(alice); @@ -273,10 +274,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_CONFIGURATOR_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_CONFIGURATOR_ROLE); vm.stopPrank(); // Only Admin can change the authority contract @@ -284,7 +285,7 @@ contract HubAccessTest is HubBase { abi.encodeWithSelector( IAccessManager.AccessManagerUnauthorizedAccount.selector, address(this), - Roles.DEFAULT_ADMIN_ROLE + Roles.ACCESS_MANAGER_DEFAULT_ADMIN ) ); authority.updateAuthority(address(hub1), address(newAuthority)); @@ -309,7 +310,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_CONFIGURATOR_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 889aab014..aaaa9fa06 100644 --- a/tests/unit/Hub/Hub.Config.t.sol +++ b/tests/unit/Hub/Hub.Config.t.sol @@ -23,7 +23,7 @@ contract HubConfigTest is HubBase { } function test_hub_deploy_reverts_on_InvalidConstructorInput() public { - DeployWrapper deployer = new DeployWrapper(); + AaveV4TestOrchestrationWrapper deployer = new AaveV4TestOrchestrationWrapper(); vm.expectRevert(IHub.InvalidAddress.selector); deployer.deployHub({authority: address(0), proxyAdminOwner: ADMIN}); diff --git a/tests/unit/Hub/Hub.Restore.t.sol b/tests/unit/Hub/Hub.Restore.t.sol index 0b6ed4cf8..c84af2835 100644 --- a/tests/unit/Hub/Hub.Restore.t.sol +++ b/tests/unit/Hub/Hub.Restore.t.sol @@ -9,20 +9,9 @@ contract HubRestoreTest is HubBase { using PercentageMath for uint256; using SafeCast for *; - HubConfigurator public hubConfigurator; - - function setUp() public override { - super.setUp(); - - // Set up a hub configurator to test resetting asset caps and pausing assets - hubConfigurator = new HubConfigurator(hub1.authority()); - setUpHubConfiguratorRoles(address(hubConfigurator), hub1.authority()); - } - function test_restore_revertsWith_SurplusDrawnRestored() public { uint256 daiAmount = 100e18; uint256 wethAmount = 10e18; - uint256 drawAmount = daiAmount / 2; // spoke1 add weth @@ -118,7 +107,7 @@ contract HubRestoreTest is HubBase { } function test_restore_revertsWith_SpokeNotActive_whenPaused() public { - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.deactivateAsset(address(hub1), daiAssetId); IHubBase.PremiumDelta memory premiumDelta = _getExpectedPremiumDelta( @@ -206,7 +195,7 @@ contract HubRestoreTest is HubBase { }); // Reset asset caps - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.resetAssetCaps(address(hub1), daiAssetId); (uint256 drawn, uint256 premium) = hub1.getSpokeOwed(daiAssetId, address(spoke1)); diff --git a/tests/unit/Hub/Hub.Sweep.t.sol b/tests/unit/Hub/Hub.Sweep.t.sol index 649a2f7d4..871ce7af5 100644 --- a/tests/unit/Hub/Hub.Sweep.t.sol +++ b/tests/unit/Hub/Hub.Sweep.t.sol @@ -20,7 +20,6 @@ contract HubSweepTest is HubBase { function test_sweep_revertsWith_OnlyReinvestmentController(address caller) public { vm.assume(caller != reinvestmentController && caller != _getProxyAdminAddress(address(hub1))); - updateAssetReinvestmentController(hub1, daiAssetId, reinvestmentController); vm.expectRevert(IHub.OnlyReinvestmentController.selector); diff --git a/tests/unit/Hub/Hub.Upgradeable.t.sol b/tests/unit/Hub/Hub.Upgradeable.t.sol index 7fa4d8420..b0ba47175 100644 --- a/tests/unit/Hub/Hub.Upgradeable.t.sol +++ b/tests/unit/Hub/Hub.Upgradeable.t.sol @@ -133,7 +133,7 @@ contract HubUpgradeableTest is HubBase { } function test_proxy_constructor_revertsWith_InvalidAddress() public { - IHubInstance hubImpl = DeployUtils.deployHubImplementation(); + IHubInstance hubImpl = AaveV4TestOrchestration.deployHubImplementation(); vm.expectRevert(IHub.InvalidAddress.selector); new TransparentUpgradeableProxy( address(hubImpl), @@ -167,7 +167,7 @@ contract HubUpgradeableTest is HubBase { } function test_hub_revision_accessible() public { - IHubInstance hubImpl = DeployUtils.deployHubImplementation(); + IHubInstance hubImpl = AaveV4TestOrchestration.deployHubImplementation(); IHubInstance hubProxy = IHubInstance(address(_deployHubProxy(address(hubImpl)))); assertEq(hubProxy.HUB_REVISION(), 1); @@ -206,8 +206,8 @@ contract HubUpgradeableTest is HubBase { bytes4[] memory hubSelectors = new bytes4[](1); hubSelectors[0] = IHub.addAsset.selector; vm.startPrank(ADMIN); - accessManager.grantRole(Roles.HUB_ADMIN_ROLE, address(this), 0); - accessManager.setTargetFunctionRole(address(hub), hubSelectors, Roles.HUB_ADMIN_ROLE); + accessManager.grantRole(Roles.HUB_CONFIGURATOR_ROLE, address(this), 0); + accessManager.setTargetFunctionRole(address(hub), hubSelectors, Roles.HUB_CONFIGURATOR_ROLE); vm.stopPrank(); assetId = hub.addAsset(underlying, 18, feeReceiver, address(irStrat), abi.encode(irDataLocal)); diff --git a/tests/unit/Hub/HubAccrueInterest.t.sol b/tests/unit/Hub/HubAccrueInterest.t.sol index 7698a2168..706530483 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 78af71a99..847a1b9dd 100644 --- a/tests/unit/Hub/HubBase.t.sol +++ b/tests/unit/Hub/HubBase.t.sol @@ -47,7 +47,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.GranularAccessControl.t.sol b/tests/unit/HubConfigurator.GranularAccessControl.t.sol index a73054686..ccd7d2b43 100644 --- a/tests/unit/HubConfigurator.GranularAccessControl.t.sol +++ b/tests/unit/HubConfigurator.GranularAccessControl.t.sol @@ -6,15 +6,14 @@ import 'tests/unit/Hub/HubBase.t.sol'; contract HubConfiguratorGranularAccessControlTest is HubBase { using SafeCast for uint256; - // Granular role constants - uint64 constant ASSET_MANAGER_ROLE = 100; - uint64 constant SPOKE_MANAGER_ROLE = 101; + // Granular role constants (must not collide with Roles.sol IDs 0-113, 200-309) + uint64 constant ASSET_MANAGER_ROLE = 1000; + uint64 constant SPOKE_MANAGER_ROLE = 1001; // Role holders address ASSET_MANAGER = makeAddr('ASSET_MANAGER'); address SPOKE_MANAGER = makeAddr('SPOKE_MANAGER'); - HubConfigurator hubConfigurator; IAccessManager manager; uint256 assetId; @@ -31,9 +30,9 @@ contract HubConfiguratorGranularAccessControlTest is HubBase { manager = IAccessManager(hub1.authority()); hubConfigurator = new HubConfigurator(address(manager)); - // Grant HUB_ADMIN_ROLE to hubConfigurator so it can call hub functions + // Grant HUB_CONFIGURATOR_ROLE to hubConfigurator so it can call hub functions vm.startPrank(ADMIN); - manager.grantRole(Roles.HUB_ADMIN_ROLE, address(hubConfigurator), 0); + manager.grantRole(Roles.HUB_CONFIGURATOR_ROLE, address(hubConfigurator), 0); // Grant granular roles to role holders manager.grantRole(ASSET_MANAGER_ROLE, ASSET_MANAGER, 0); @@ -190,8 +189,7 @@ contract HubConfiguratorGranularAccessControlTest is HubBase { } function test_fuzz_unauthorized_cannotCall_assetManagerMethods(address caller) public { - vm.assume(caller != ASSET_MANAGER); - vm.assume(caller != address(0)); + vm.assume(caller != ASSET_MANAGER && caller != address(0) && caller != address(manager)); for (uint256 i = 0; i < assetManagerCalldata.length; ++i) { vm.prank(caller); @@ -205,8 +203,7 @@ contract HubConfiguratorGranularAccessControlTest is HubBase { } function test_fuzz_unauthorized_cannotCall_spokeManagerMethods(address caller) public { - vm.assume(caller != SPOKE_MANAGER); - vm.assume(caller != address(0)); + vm.assume(caller != SPOKE_MANAGER && caller != address(0) && caller != address(manager)); for (uint256 i = 0; i < spokeManagerCalldata.length; ++i) { vm.prank(caller); diff --git a/tests/unit/HubConfigurator.t.sol b/tests/unit/HubConfigurator.t.sol index eed9d3820..229e384d6 100644 --- a/tests/unit/HubConfigurator.t.sol +++ b/tests/unit/HubConfigurator.t.sol @@ -6,22 +6,18 @@ import 'tests/unit/Hub/HubBase.t.sol'; contract HubConfiguratorTest is HubBase { using SafeCast for uint256; - HubConfigurator internal hubConfigurator; - uint256 internal _assetId; bytes internal _encodedIrData; address[4] public spokeAddresses; address spoke; - mapping(address => uint24) public riskPremiumThresholdsPerSpoke; // spoke address => risk premium threshold - mapping(uint256 => uint24) public riskPremiumThresholdsPerAsset; // assetId => risk premium threshold + mapping(address spoke => uint24 riskPremiumThreshold) public riskPremiumThresholdsPerSpoke; + mapping(uint256 assetId => uint24 riskPremiumThreshold) public riskPremiumThresholdsPerAsset; function setUp() public virtual override { super.setUp(); - hubConfigurator = new HubConfigurator(hub1.authority()); - setUpHubConfiguratorRoles(address(hubConfigurator), hub1.authority()); - + _grantHubConfiguratorRole(hub1, address(hubConfigurator)); _assetId = daiAssetId; _encodedIrData = abi.encode( IAssetInterestRateStrategy.InterestRateData({ @@ -36,7 +32,7 @@ contract HubConfiguratorTest is HubBase { } function test_addAsset_fuzz_revertsWith_AccessManagedUnauthorized(address caller) public { - vm.assume(caller != HUB_CONFIGURATOR); + _assumeNonHubConfiguratorAdmin(caller); vm.expectRevert( abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, caller) @@ -60,7 +56,7 @@ contract HubConfiguratorTest is HubBase { function test_addAsset_reverts_invalidIrData() public { vm.expectRevert(); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); _addAsset({ fetchErc20Decimals: vm.randomBool(), underlying: vm.randomAddress(), @@ -89,7 +85,7 @@ contract HubConfiguratorTest is HubBase { liquidityFee = bound(liquidityFee, 0, PercentageMath.PERCENTAGE_FACTOR); vm.expectRevert(IHub.InvalidAssetDecimals.selector, address(hub1)); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); _addAsset( fetchErc20Decimals, underlying, @@ -113,7 +109,7 @@ contract HubConfiguratorTest is HubBase { uint256 liquidityFee = vm.randomUint(0, PercentageMath.PERCENTAGE_FACTOR); vm.expectRevert(IHub.InvalidAddress.selector, address(hub1)); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); _addAsset(true, address(0), decimals, feeReceiver, liquidityFee, irStrategy, _encodedIrData); } @@ -129,7 +125,7 @@ contract HubConfiguratorTest is HubBase { uint256 liquidityFee = vm.randomUint(0, PercentageMath.PERCENTAGE_FACTOR); vm.expectRevert(IHub.InvalidAddress.selector, address(hub1)); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); _addAsset(true, underlying, decimals, feeReceiver, liquidityFee, address(0), _encodedIrData); } @@ -146,7 +142,7 @@ contract HubConfiguratorTest is HubBase { uint256 liquidityFee = vm.randomUint(PercentageMath.PERCENTAGE_FACTOR + 1, type(uint16).max); vm.expectRevert(IHub.InvalidLiquidityFee.selector, address(hub1)); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); _addAsset(false, underlying, decimals, feeReceiver, liquidityFee, irStrategy, _encodedIrData); } @@ -221,7 +217,7 @@ contract HubConfiguratorTest is HubBase { abi.encodeCall(IHub.updateAssetConfig, (hub1.getAssetCount(), expectedConfig, new bytes(0))) ); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); _assetId = _addAsset( fetchErc20Decimals, underlying, @@ -255,7 +251,7 @@ contract HubConfiguratorTest is HubBase { ); vm.expectRevert(IHub.InvalidLiquidityFee.selector); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.updateLiquidityFee(address(hub1), _assetId, liquidityFee); } @@ -271,7 +267,7 @@ contract HubConfiguratorTest is HubBase { abi.encodeCall(IHub.updateAssetConfig, (_assetId, expectedConfig, new bytes(0))) ); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.updateLiquidityFee(address(hub1), _assetId, expectedConfig.liquidityFee); assertEq(hub1.getAssetConfig(_assetId), expectedConfig); @@ -280,7 +276,7 @@ contract HubConfiguratorTest is HubBase { function test_updateFeeReceiver_fuzz_revertsWith_AccessManagedUnauthorized( address caller ) public { - vm.assume(caller != HUB_CONFIGURATOR); + _assumeNonHubConfiguratorAdmin(caller); vm.expectRevert( abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, caller) ); @@ -292,7 +288,7 @@ contract HubConfiguratorTest is HubBase { _assetId = vm.randomUint(0, hub1.getAssetCount() - 1); vm.expectRevert(IHub.InvalidAddress.selector, address(hub1)); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.updateFeeReceiver(address(hub1), _assetId, address(0)); } @@ -314,7 +310,7 @@ contract HubConfiguratorTest is HubBase { vm.expectRevert(IHub.SpokeAlreadyListed.selector, address(hub1)); } } - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.updateFeeReceiver(address(hub1), _assetId, feeReceiver); assertEq(hub1.getAssetConfig(_assetId), expectedConfig); @@ -326,7 +322,7 @@ contract HubConfiguratorTest is HubBase { // set feeReceiver as an existing spoke address feeReceiver = address(spoke1); vm.expectRevert(IHub.SpokeAlreadyListed.selector, address(hub1)); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.updateFeeReceiver(address(hub1), _assetId, feeReceiver); } @@ -354,20 +350,16 @@ contract HubConfiguratorTest is HubBase { assertGe(hub1.getSpokeAddedShares(daiAssetId, address(treasurySpoke)), 0); // Change the fee receiver - TreasurySpokeInstance newTreasurySpokeImpl = new TreasurySpokeInstance(); - ITreasurySpoke newTreasurySpoke = ITreasurySpoke( - DeployUtils.proxify( - address(newTreasurySpokeImpl), - ADMIN, - abi.encodeCall(TreasurySpokeInstance.initialize, (HUB_ADMIN)) - ) - ); - vm.prank(HUB_CONFIGURATOR); - hubConfigurator.updateFeeReceiver(address(hub1), daiAssetId, address(newTreasurySpoke)); + address newTreasurySpoke = AaveV4TestOrchestration.deployTestTreasurySpoke({ + owner: HUB_ADMIN, + salt: bytes32('newTreasurySpoke1') + }); + vm.prank(HUB_CONFIGURATOR_ADMIN); + hubConfigurator.updateFeeReceiver(address(hub1), daiAssetId, newTreasurySpoke); assertEq( hub1.getAssetConfig(daiAssetId).feeReceiver, - address(newTreasurySpoke), + newTreasurySpoke, 'new fee receiver updated' ); assertTrue( @@ -391,7 +383,7 @@ contract HubConfiguratorTest is HubBase { Utils.mintFeeShares(hub1, daiAssetId, ADMIN); assertGt( - hub1.getSpokeAddedAssets(daiAssetId, address(newTreasurySpoke)), + hub1.getSpokeAddedAssets(daiAssetId, newTreasurySpoke), 0, 'new fee receiver should have accrued fees' ); @@ -429,21 +421,17 @@ contract HubConfiguratorTest is HubBase { uint256 feeShares = hub1.getSpokeAddedShares(daiAssetId, address(treasurySpoke)); // Change the fee receiver - TreasurySpokeInstance newTreasurySpokeImpl2 = new TreasurySpokeInstance(); - ITreasurySpoke newTreasurySpoke = ITreasurySpoke( - DeployUtils.proxify( - address(newTreasurySpokeImpl2), - ADMIN, - abi.encodeCall(TreasurySpokeInstance.initialize, (HUB_ADMIN)) - ) - ); - vm.prank(HUB_CONFIGURATOR); - hubConfigurator.updateFeeReceiver(address(hub1), daiAssetId, address(newTreasurySpoke)); + address newTreasurySpoke = AaveV4TestOrchestration.deployTestTreasurySpoke({ + owner: HUB_ADMIN, + salt: bytes32('newTreasurySpoke2') + }); + vm.prank(HUB_CONFIGURATOR_ADMIN); + hubConfigurator.updateFeeReceiver(address(hub1), daiAssetId, newTreasurySpoke); // Ensure fee receiver was updated assertEq( hub1.getAssetConfig(daiAssetId).feeReceiver, - address(newTreasurySpoke), + newTreasurySpoke, 'new fee receiver mismatch' ); @@ -467,7 +455,7 @@ contract HubConfiguratorTest is HubBase { // Check that new fee receiver is getting the fees, and not old treasury spoke assertGt( - hub1.getSpokeAddedAssets(daiAssetId, address(newTreasurySpoke)), + hub1.getSpokeAddedAssets(daiAssetId, newTreasurySpoke), 0, 'new fee receiver should have accrued fees' ); @@ -500,7 +488,7 @@ contract HubConfiguratorTest is HubBase { } function test_updateFeeConfig_fuzz_revertsWith_AccessManagedUnauthorized(address caller) public { - vm.assume(caller != HUB_CONFIGURATOR); + _assumeNonHubConfiguratorAdmin(caller); vm.expectRevert( abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, caller) ); @@ -518,7 +506,7 @@ contract HubConfiguratorTest is HubBase { uint256 liquidityFee = vm.randomUint(1, PercentageMath.PERCENTAGE_FACTOR); vm.expectRevert(IHub.InvalidAddress.selector, address(hub1)); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.updateFeeConfig(address(hub1), assetId, liquidityFee, address(0)); } @@ -530,7 +518,7 @@ contract HubConfiguratorTest is HubBase { address feeReceiver = hub1.getAssetConfig(assetId).feeReceiver; vm.expectRevert(IHub.InvalidLiquidityFee.selector, address(hub1)); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.updateFeeConfig(address(hub1), assetId, liquidityFee, feeReceiver); } @@ -560,7 +548,7 @@ contract HubConfiguratorTest is HubBase { vm.expectRevert(IHub.SpokeAlreadyListed.selector, address(hub1)); } } - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.updateFeeConfig(address(hub1), assetId_, liquidityFee, feeReceiver); assertEq(hub1.getAssetConfig(assetId_), expectedConfig); } @@ -577,7 +565,7 @@ contract HubConfiguratorTest is HubBase { function test_updateInterestRateStrategy_fuzz_revertsWith_AccessManagedUnauthorized( address caller ) public { - vm.assume(caller != HUB_CONFIGURATOR); + _assumeNonHubConfiguratorAdmin(caller); vm.expectRevert( abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, caller) ); @@ -602,7 +590,7 @@ contract HubConfiguratorTest is HubBase { abi.encodeCall(IHub.updateAssetConfig, (_assetId, expectedConfig, _encodedIrData)) ); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.updateInterestRateStrategy(address(hub1), _assetId, irStrategy, _encodedIrData); assertEq(hub1.getAssetConfig(_assetId), expectedConfig); @@ -612,7 +600,7 @@ contract HubConfiguratorTest is HubBase { _assetId = vm.randomUint(0, hub1.getAssetCount() - 1); vm.expectRevert(IHub.InvalidAddress.selector, address(hub1)); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.updateInterestRateStrategy(address(hub1), _assetId, address(0), _encodedIrData); } @@ -621,13 +609,13 @@ contract HubConfiguratorTest is HubBase { address irStrategy = makeAddr('newDrawnRateStrategy'); vm.expectRevert(); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.updateInterestRateStrategy(address(hub1), _assetId, irStrategy, _encodedIrData); } function test_updateInterestRateStrategy_revertsWith_InvalidInterestRateStrategy() public { vm.expectRevert(IHub.InvalidInterestRateStrategy.selector, address(hub1)); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.updateInterestRateStrategy( address(hub1), _assetId, @@ -639,7 +627,7 @@ contract HubConfiguratorTest is HubBase { function test_updateReinvestmentController_fuzz_revertsWith_AccessManagedUnauthorized( address caller ) public { - vm.assume(caller != HUB_CONFIGURATOR); + _assumeNonHubConfiguratorAdmin(caller); vm.expectRevert( abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, caller) ); @@ -659,7 +647,7 @@ contract HubConfiguratorTest is HubBase { address(hub1), abi.encodeCall(IHub.updateAssetConfig, (_assetId, expectedConfig, new bytes(0))) ); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.updateReinvestmentController(address(hub1), _assetId, reinvestmentController); assertEq(hub1.getAssetConfig(_assetId), expectedConfig); @@ -686,7 +674,7 @@ contract HubConfiguratorTest is HubBase { riskPremiumThresholdsPerSpoke[spokeAddresses[i]] = spokeConfig.riskPremiumThreshold; } - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.resetAssetCaps(address(hub1), _assetId); for (uint256 i; i < spokeAddresses.length; i++) { @@ -715,7 +703,7 @@ contract HubConfiguratorTest is HubBase { ); } - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.deactivateAsset(address(hub1), _assetId); for (uint256 i; i < spokeAddresses.length; i++) { @@ -742,7 +730,7 @@ contract HubConfiguratorTest is HubBase { ); } - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.haltAsset(address(hub1), _assetId); for (uint256 i; i < spokeAddresses.length; i++) { @@ -773,7 +761,7 @@ contract HubConfiguratorTest is HubBase { vm.expectEmit(address(hub1)); emit IHub.AddSpoke(daiAssetId, newSpoke); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.addSpoke(address(hub1), newSpoke, daiAssetId, daiSpokeConfig); assertEq(hub1.getSpokeConfig(daiAssetId, newSpoke), daiSpokeConfig); @@ -821,7 +809,7 @@ contract HubConfiguratorTest is HubBase { }); vm.expectRevert(IHubConfigurator.MismatchedConfigs.selector); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.addSpokeToAssets(address(hub1), spoke, assetIds, spokeConfigs); } @@ -855,7 +843,7 @@ contract HubConfiguratorTest is HubBase { emit IHub.AddSpoke(daiAssetId, newSpoke); vm.expectEmit(address(hub1)); emit IHub.AddSpoke(wethAssetId, newSpoke); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.addSpokeToAssets(address(hub1), newSpoke, assetIds, spokeConfigs); IHub.SpokeConfig memory daiSpokeData = hub1.getSpokeConfig(daiAssetId, newSpoke); @@ -882,7 +870,7 @@ contract HubConfiguratorTest is HubBase { address(hub1), abi.encodeCall(IHub.updateSpokeConfig, (_assetId, spoke, expectedSpokeConfig)) ); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.updateSpokeHalted(address(hub1), _assetId, spoke, halted); assertEq(hub1.getSpokeConfig(_assetId, spoke), expectedSpokeConfig); } @@ -905,7 +893,7 @@ contract HubConfiguratorTest is HubBase { address(hub1), abi.encodeCall(IHub.updateSpokeConfig, (_assetId, spoke, expectedSpokeConfig)) ); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.updateSpokeActive(address(hub1), _assetId, spoke, active); assertEq(hub1.getSpokeConfig(_assetId, spoke), expectedSpokeConfig); } @@ -927,7 +915,7 @@ contract HubConfiguratorTest is HubBase { address(hub1), abi.encodeCall(IHub.updateSpokeConfig, (_assetId, spoke, expectedSpokeConfig)) ); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.updateSpokeAddCap(address(hub1), _assetId, spoke, newAddCap); assertEq(hub1.getSpokeConfig(_assetId, spoke), expectedSpokeConfig); } @@ -948,7 +936,7 @@ contract HubConfiguratorTest is HubBase { address(hub1), abi.encodeCall(IHub.updateSpokeConfig, (_assetId, spoke, expectedSpokeConfig)) ); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.updateSpokeDrawCap(address(hub1), _assetId, spoke, newDrawCap); assertEq(hub1.getSpokeConfig(_assetId, spoke), expectedSpokeConfig); } @@ -974,7 +962,7 @@ contract HubConfiguratorTest is HubBase { address(hub1), abi.encodeCall(IHub.updateSpokeConfig, (_assetId, spoke, expectedSpokeConfig)) ); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.updateSpokeRiskPremiumThreshold( address(hub1), _assetId, @@ -1002,7 +990,7 @@ contract HubConfiguratorTest is HubBase { address(hub1), abi.encodeCall(IHub.updateSpokeConfig, (_assetId, spoke, expectedSpokeConfig)) ); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.updateSpokeCaps(address(hub1), _assetId, spoke, newAddCap, newDrawCap); assertEq(hub1.getSpokeConfig(_assetId, spoke), expectedSpokeConfig); } @@ -1034,7 +1022,7 @@ contract HubConfiguratorTest is HubBase { vm.expectCall(address(hub1), abi.encodeCall(IHub.isSpokeListed, (assetId, address(spoke3)))); } - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.deactivateSpoke(address(hub1), address(spoke3)); for (uint256 assetId = 0; assetId < 4; ++assetId) { @@ -1070,7 +1058,7 @@ contract HubConfiguratorTest is HubBase { vm.expectCall(address(hub1), abi.encodeCall(IHub.isSpokeListed, (assetId, address(spoke3)))); } - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.haltSpoke(address(hub1), address(spoke3)); for (uint256 assetId = 0; assetId < 4; ++assetId) { @@ -1109,7 +1097,7 @@ contract HubConfiguratorTest is HubBase { vm.expectCall(address(hub1), abi.encodeCall(IHub.isSpokeListed, (assetId, address(spoke3)))); } - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.resetSpokeCaps(address(hub1), address(spoke3)); for (uint256 assetId = 0; assetId < 4; ++assetId) { @@ -1141,7 +1129,7 @@ contract HubConfiguratorTest is HubBase { address(hub1), abi.encodeCall(IHub.setInterestRateData, (_assetId, abi.encode(newIrData))) ); - vm.prank(HUB_CONFIGURATOR); + vm.prank(HUB_CONFIGURATOR_ADMIN); hubConfigurator.updateInterestRateData(address(hub1), _assetId, abi.encode(newIrData)); assertEq(irStrategy.getInterestRateData(_assetId), newIrData); @@ -1180,4 +1168,13 @@ contract HubConfiguratorTest is HubBase { ); } } + + function _assumeNonHubConfiguratorAdmin(address caller) internal { + vm.assume( + caller != HUB_CONFIGURATOR_ADMIN && + caller != address(accessManager) && + caller != ADMIN && + caller != HUB_ADMIN + ); + } } diff --git a/tests/unit/Rescuable.t.sol b/tests/unit/Rescuable.t.sol index 5726cdf31..433aa7d3b 100644 --- a/tests/unit/Rescuable.t.sol +++ b/tests/unit/Rescuable.t.sol @@ -8,7 +8,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 de5e1a1ea..b01333207 100644 --- a/tests/unit/Spoke/Spoke.Access.t.sol +++ b/tests/unit/Spoke/Spoke.Access.t.sol @@ -153,10 +153,10 @@ 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_CONFIGURATOR_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_CONFIGURATOR_ROLE); vm.stopPrank(); // Only Admin can change the authority contract @@ -164,7 +164,7 @@ contract SpokeAccessTest is SpokeBase { abi.encodeWithSelector( IAccessManager.AccessManagerUnauthorizedAccount.selector, address(this), - Roles.DEFAULT_ADMIN_ROLE + Roles.ACCESS_MANAGER_DEFAULT_ADMIN ) ); authority.updateAuthority(address(spoke1), address(newAuthority)); @@ -203,10 +203,10 @@ contract SpokeAccessTest is SpokeBase { }) ); - // Now we also give the spoke admin role capability to add reserve on new authority + // Now we also give the spoke configurator 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_CONFIGURATOR_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 8abd5cf9c..28dce92b2 100644 --- a/tests/unit/Spoke/Spoke.Config.t.sol +++ b/tests/unit/Spoke/Spoke.Config.t.sol @@ -13,7 +13,10 @@ contract SpokeConfigTest is SpokeBase { vm.mockCall(oracle, abi.encodeCall(IPriceOracle.decimals, ()), abi.encode(8)); ISpoke instance = ISpoke( address( - DeployUtils.deploySpokeImplementation(oracle, Constants.MAX_ALLOWED_USER_RESERVES_LIMIT) + AaveV4TestOrchestration.deploySpokeImplementation( + oracle, + Constants.MAX_ALLOWED_USER_RESERVES_LIMIT + ) ) ); assertEq(instance.ORACLE(), oracle); @@ -22,14 +25,14 @@ contract SpokeConfigTest is SpokeBase { } function test_spoke_deploy_reverts_on_InvalidConstructorInput() public { - DeployWrapper deployer = new DeployWrapper(); + AaveV4TestOrchestrationWrapper deployer = new AaveV4TestOrchestrationWrapper(); vm.expectRevert(); deployer.deploySpokeImplementation(address(0), Constants.MAX_ALLOWED_USER_RESERVES_LIMIT); } function test_spoke_deploy_reverts_on_InvalidOracleDecimals() public { - DeployWrapper deployer = new DeployWrapper(); + AaveV4TestOrchestrationWrapper deployer = new AaveV4TestOrchestrationWrapper(); address oracle = makeAddr('AaveOracle'); vm.mockCall(oracle, abi.encodeCall(IPriceOracle.decimals, ()), abi.encode(7)); @@ -38,7 +41,7 @@ contract SpokeConfigTest is SpokeBase { } function test_spoke_deploy_reverts_on_InvalidMaxUserReservesLimit() public { - DeployWrapper deployer = new DeployWrapper(); + AaveV4TestOrchestrationWrapper deployer = new AaveV4TestOrchestrationWrapper(); address oracle = makeAddr('AaveOracle'); vm.mockCall(oracle, abi.encodeCall(IPriceOracle.decimals, ()), abi.encode(8)); @@ -52,8 +55,9 @@ contract SpokeConfigTest is SpokeBase { vm.assume( caller != SPOKE_ADMIN && caller != ADMIN && - caller != SPOKE_CONFIGURATOR && - caller != _getProxyAdminAddress(address(spoke1)) + caller != SPOKE_CONFIGURATOR_ADMIN && + caller != address(spokeConfigurator) && + caller != ProxyHelper.getProxyAdmin(address(spoke1)) ); vm.expectRevert( abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, caller) @@ -309,8 +313,8 @@ contract SpokeConfigTest is SpokeBase { } function test_getReserveId_fuzz_multipleHubs(uint256 reserveId) public { - (IHub hub2, ) = hub2Fixture(); - (IHub hub3, ) = hub3Fixture(); + (IHub hub2, ) = _hub2Fixture(); + (IHub hub3, ) = _hub3Fixture(); vm.startPrank(ADMIN); spoke1.addReserve( diff --git a/tests/unit/Spoke/Spoke.DynamicConfig.Triggers.t.sol b/tests/unit/Spoke/Spoke.DynamicConfig.Triggers.t.sol index c198a9b44..4f87d732f 100644 --- a/tests/unit/Spoke/Spoke.DynamicConfig.Triggers.t.sol +++ b/tests/unit/Spoke/Spoke.DynamicConfig.Triggers.t.sol @@ -240,10 +240,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); @@ -280,7 +280,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 f1eb09505..5333395a5 100644 --- a/tests/unit/Spoke/Spoke.DynamicConfig.t.sol +++ b/tests/unit/Spoke/Spoke.DynamicConfig.t.sol @@ -114,8 +114,9 @@ contract SpokeDynamicConfigTest is SpokeBase { vm.assume( caller != SPOKE_ADMIN && caller != ADMIN && - caller != SPOKE_CONFIGURATOR && - caller != _getProxyAdminAddress(address(spoke1)) + caller != SPOKE_CONFIGURATOR_ADMIN && + caller != address(spokeConfigurator) && + caller != ProxyHelper.getProxyAdmin(address(spoke1)) ); uint256 reserveId = _randomReserveId(spoke1); uint32 dynamicConfigKey = _randomInitializedConfigKey(spoke1, reserveId); @@ -225,8 +226,9 @@ contract SpokeDynamicConfigTest is SpokeBase { vm.assume( caller != SPOKE_ADMIN && caller != ADMIN && - caller != SPOKE_CONFIGURATOR && - caller != _getProxyAdminAddress(address(spoke1)) + caller != SPOKE_CONFIGURATOR_ADMIN && + caller != address(spokeConfigurator) && + caller != ProxyHelper.getProxyAdmin(address(spoke1)) ); uint256 reserveId = _randomReserveId(spoke1); uint32 dynamicConfigKey = _randomInitializedConfigKey(spoke1, reserveId); diff --git a/tests/unit/Spoke/Spoke.Getters.t.sol b/tests/unit/Spoke/Spoke.Getters.t.sol index 24408edf1..8eec53a61 100644 --- a/tests/unit/Spoke/Spoke.Getters.t.sol +++ b/tests/unit/Spoke/Spoke.Getters.t.sol @@ -15,8 +15,9 @@ contract SpokeGettersTest is SpokeBase { super.setUp(); // Deploy new spoke without setting the liquidation config - (spoke, ) = _deploySpokeWithOracle(ADMIN, address(accessManager)); - 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 4f4489f36..9f5a597a9 100644 --- a/tests/unit/Spoke/Spoke.MultipleHub.Base.t.sol +++ b/tests/unit/Spoke/Spoke.MultipleHub.Base.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import 'tests/unit/Spoke/SpokeBase.t.sol'; +import {BytecodeHelper} from 'src/deployments/utils/libraries/BytecodeHelper.sol'; contract SpokeMultipleHubBase is SpokeBase { // New hub and spoke @@ -29,75 +30,61 @@ contract SpokeMultipleHubBase is SpokeBase { bytes internal encodedIrData = abi.encode(irData); function setUp() public virtual override { - deployFixtures(); + _deployFixtures(); } - function deployFixtures() internal virtual override { - vm.startPrank(ADMIN); - accessManager = IAccessManager(address(new AccessManagerEnumerable(ADMIN))); - // Canonical hub and spoke - hub1 = DeployUtils.deployHub({ - authority: address(accessManager), - proxyAdminOwner: ADMIN, - salt: hex'01' + function _deployFixtures() internal virtual { + _etchSetup(); + + TestTypes.TestEnvReport memory report = AaveV4TestOrchestration.deployTestEnv({ + admin: ADMIN, + treasuryAdmin: ADMIN, + hubCount: 2, + spokeCount: 2, + nativeWrapper: makeAddr('nativeWrapper'), + hubBytecode: BytecodeHelper.getHubBytecode(), + spokeBytecode: BytecodeHelper.getSpokeBytecode(), + salt: bytes32('multiHubTest') }); - (spoke1, oracle1) = _deploySpokeWithOracle(ADMIN, address(accessManager)); - TreasurySpokeInstance treasurySpokeImpl = new TreasurySpokeInstance(); - treasurySpoke = ITreasurySpoke( - DeployUtils.proxify( - address(treasurySpokeImpl), - ADMIN, - abi.encodeCall(TreasurySpokeInstance.initialize, (ADMIN)) - ) - ); - irStrategy = new AssetInterestRateStrategy(address(hub1)); + + // Canonical hub and spoke + accessManager = IAccessManager(report.accessManager); + hub1 = IHub(report.hubReports[0].hub); + irStrategy = IAssetInterestRateStrategy(report.hubReports[0].irStrategy); + spoke1 = ISpoke(report.spokeReports[0].spoke); + oracle1 = IAaveOracle(report.spokeReports[0].aaveOracle); + treasurySpoke = ITreasurySpoke(report.treasurySpoke); // New hub and spoke - newHub = DeployUtils.deployHub({ - authority: address(accessManager), - proxyAdminOwner: ADMIN, - salt: hex'02' - }); - (newSpoke, newOracle) = _deploySpokeWithOracle(ADMIN, address(accessManager)); - newIrStrategy = new AssetInterestRateStrategy(address(newHub)); + newHub = IHub(report.hubReports[1].hub); + newIrStrategy = IAssetInterestRateStrategy(report.hubReports[1].irStrategy); + newSpoke = ISpoke(report.spokeReports[1].spoke); + newOracle = IAaveOracle(report.spokeReports[1].aaveOracle); + // Deploy test tokens + vm.startPrank(ADMIN); assetA = new TestnetERC20('Asset A', 'A', 18); assetB = new TestnetERC20('Asset B', 'B', 18); vm.stopPrank(); - setUpRoles(); + + _setupMultiHubRoles(report); } - function setUpRoles() internal { + function _setupMultiHubRoles(TestTypes.TestEnvReport memory report) 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); - - // Grant responsibilities to roles - // Spoke Admin functionalities - bytes4[] memory selectors = new bytes4[](6); - selectors[0] = ISpoke.updateReservePriceSource.selector; - selectors[1] = ISpoke.updateLiquidationConfig.selector; - selectors[2] = ISpoke.addReserve.selector; - selectors[3] = ISpoke.updateReserveConfig.selector; - 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); + IAccessManager(report.accessManager).grantRole( + Roles.ACCESS_MANAGER_DEFAULT_ADMIN, + address(this), + 0 + ); + vm.stopPrank(); - // Hub Admin functionalities - bytes4[] memory hubSelectors = new bytes4[](4); - hubSelectors[0] = IHub.addAsset.selector; - hubSelectors[1] = IHub.updateAssetConfig.selector; - hubSelectors[2] = IHub.addSpoke.selector; - hubSelectors[3] = IHub.updateSpokeConfig.selector; + AaveV4TestOrchestration.setRolesTestEnv(report); + AaveV4TestOrchestration.grantRolesTestEnv(report, ADMIN, HUB_ADMIN, SPOKE_ADMIN); - accessManager.setTargetFunctionRole(address(hub1), hubSelectors, Roles.HUB_ADMIN_ROLE); - accessManager.setTargetFunctionRole(address(newHub), hubSelectors, Roles.HUB_ADMIN_ROLE); - vm.stopPrank(); + IAccessManager(report.accessManager).renounceRole( + Roles.ACCESS_MANAGER_DEFAULT_ADMIN, + address(this) + ); } } diff --git a/tests/unit/Spoke/Spoke.MultipleHub.t.sol b/tests/unit/Spoke/Spoke.MultipleHub.t.sol index 94b046618..34ed16dda 100644 --- a/tests/unit/Spoke/Spoke.MultipleHub.t.sol +++ b/tests/unit/Spoke/Spoke.MultipleHub.t.sol @@ -6,8 +6,8 @@ import 'tests/unit/Spoke/SpokeBase.t.sol'; contract SpokeMultipleHubTest is SpokeBase { IHub internal hub2; IHub internal hub3; - AssetInterestRateStrategy internal hub2IrStrategy; - AssetInterestRateStrategy internal hub3IrStrategy; + IAssetInterestRateStrategy internal hub2IrStrategy; + IAssetInterestRateStrategy internal hub3IrStrategy; uint256 internal daiHub2ReserveId; uint256 internal daiHub3ReserveId; @@ -22,8 +22,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 fb68f97cb..39318b1fa 100644 --- a/tests/unit/Spoke/Spoke.Upgradeable.t.sol +++ b/tests/unit/Spoke/Spoke.Upgradeable.t.sol @@ -21,7 +21,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)); @@ -57,9 +57,8 @@ contract SpokeUpgradeableTest is SpokeBase { ISpoke spokeProxy = _deploySpokeProxy(address(spokeImpl)); 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(IAccessManaged(address(spokeProxy)).authority(), address(accessManager)); assertEq(spokeProxy.getLiquidationConfig(), expectedLiquidationConfig); @@ -73,6 +72,7 @@ contract SpokeUpgradeableTest is SpokeBase { ISpoke spokeProxy = _deploySpokeProxy(address(spokeImpl)); setUpRoles(hub1, spokeProxy, accessManager); + uint128 targetHealthFactor = 1.05e18; _updateTargetHealthFactor(spokeProxy, targetHealthFactor); @@ -82,7 +82,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))); ITransparentUpgradeableProxy(address(spokeProxy)).upgradeToAndCall( address(spokeImpl2), _getInitializeCalldata(address(accessManager)) @@ -115,13 +115,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)); ISpokeInstance 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)) @@ -129,7 +129,7 @@ contract SpokeUpgradeableTest is SpokeBase { } function test_proxy_constructor_revertsWith_InvalidAddress() public { - ISpokeInstance spokeImpl = DeployUtils.deploySpokeImplementation( + ISpokeInstance spokeImpl = AaveV4TestOrchestration.deploySpokeImplementation( oracle, Constants.MAX_ALLOWED_USER_RESERVES_LIMIT ); @@ -142,7 +142,7 @@ contract SpokeUpgradeableTest is SpokeBase { } function test_proxy_reinitialization_revertsWith_InvalidAddress() public { - ISpokeInstance spokeImpl = DeployUtils.deploySpokeImplementation( + ISpokeInstance spokeImpl = AaveV4TestOrchestration.deploySpokeImplementation( oracle, Constants.MAX_ALLOWED_USER_RESERVES_LIMIT ); @@ -152,12 +152,12 @@ contract SpokeUpgradeableTest is SpokeBase { ISpokeInstance 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))); } function test_proxy_reinitialization_revertsWith_CallerNotProxyAdmin() public { - ISpokeInstance spokeImpl = DeployUtils.deploySpokeImplementation( + ISpokeInstance spokeImpl = AaveV4TestOrchestration.deploySpokeImplementation( oracle, Constants.MAX_ALLOWED_USER_RESERVES_LIMIT ); @@ -199,7 +199,7 @@ contract SpokeUpgradeableTest is SpokeBase { } function test_spoke_revision_accessible() public { - ISpokeInstance spokeImpl = DeployUtils.deploySpokeImplementation( + ISpokeInstance spokeImpl = AaveV4TestOrchestration.deploySpokeImplementation( oracle, Constants.MAX_ALLOWED_USER_RESERVES_LIMIT ); diff --git a/tests/unit/Spoke/SpokeBase.t.sol b/tests/unit/Spoke/SpokeBase.t.sol index c623ca285..cd3567d32 100644 --- a/tests/unit/Spoke/SpokeBase.t.sol +++ b/tests/unit/Spoke/SpokeBase.t.sol @@ -157,7 +157,7 @@ contract SpokeBase is Base { function setUp() public virtual override { super.setUp(); - initEnvironment(); + _initEnvironment(); } /// @dev Opens a supply position for a random user @@ -1146,15 +1146,15 @@ contract SpokeBase is Base { new MockSpoke(spoke.ORACLE(), Constants.MAX_ALLOWED_USER_RESERVES_LIMIT) ); - 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, ''); } @@ -1170,8 +1170,11 @@ contract SpokeBase is Base { /// @dev Helper to etch spoke's implementation with a new maxUserReservesLimit function _updateMaxUserReservesLimit(ISpoke spoke, uint16 newLimit) internal { - address currentImpl = _getImplementationAddress(address(spoke)); - ISpokeInstance newImpl = DeployUtils.deploySpokeImplementation(spoke.ORACLE(), newLimit); + address currentImpl = ProxyHelper.getImplementation(address(spoke)); + ISpokeInstance newImpl = AaveV4TestOrchestration.deploySpokeImplementation( + spoke.ORACLE(), + newLimit + ); vm.etch(currentImpl, address(newImpl).code); } } diff --git a/tests/unit/Spoke/TreasurySpoke.Upgradeable.t.sol b/tests/unit/Spoke/TreasurySpoke.Upgradeable.t.sol index 3a8fe38d3..37ed0a578 100644 --- a/tests/unit/Spoke/TreasurySpoke.Upgradeable.t.sol +++ b/tests/unit/Spoke/TreasurySpoke.Upgradeable.t.sol @@ -44,7 +44,7 @@ contract TreasurySpokeUpgradeableTest is SpokeBase { emit IERC1967.AdminChanged(address(0), proxyAdminAddress); ITreasurySpoke proxy = ITreasurySpoke( - DeployUtils.proxify( + AaveV4TestOrchestration.proxify( address(impl), proxyAdminOwner, abi.encodeCall(MockTreasurySpokeInstance.initialize, (TREASURY_ADMIN)) @@ -63,7 +63,7 @@ contract TreasurySpokeUpgradeableTest is SpokeBase { initialRevision = uint64(bound(initialRevision, 1, type(uint64).max - 1)); MockTreasurySpokeInstance impl = _deployMockTreasurySpokeInstance(initialRevision); ITransparentUpgradeableProxy proxy = ITransparentUpgradeableProxy( - DeployUtils.proxify( + AaveV4TestOrchestration.proxify( address(impl), proxyAdminOwner, abi.encodeCall(MockTreasurySpokeInstance.initialize, (TREASURY_ADMIN)) @@ -88,7 +88,7 @@ contract TreasurySpokeUpgradeableTest is SpokeBase { MockTreasurySpokeInstance impl = _deployMockTreasurySpokeInstance(0); vm.expectRevert(Initializable.InvalidInitialization.selector); - DeployUtils.proxify( + AaveV4TestOrchestration.proxify( address(impl), proxyAdminOwner, abi.encodeCall(MockTreasurySpokeInstance.initialize, (TREASURY_ADMIN)) @@ -102,7 +102,7 @@ contract TreasurySpokeUpgradeableTest is SpokeBase { MockTreasurySpokeInstance impl = _deployMockTreasurySpokeInstance(initialRevision); ITransparentUpgradeableProxy proxy = ITransparentUpgradeableProxy( - DeployUtils.proxify( + AaveV4TestOrchestration.proxify( address(impl), proxyAdminOwner, abi.encodeCall(MockTreasurySpokeInstance.initialize, (TREASURY_ADMIN)) @@ -131,7 +131,7 @@ contract TreasurySpokeUpgradeableTest is SpokeBase { vm.expectRevert( abi.encodeWithSelector(OwnableUpgradeable.OwnableInvalidOwner.selector, address(0)) ); - DeployUtils.proxify( + AaveV4TestOrchestration.proxify( address(impl), proxyAdminOwner, abi.encodeCall(TreasurySpokeInstance.initialize, (address(0))) @@ -141,7 +141,7 @@ contract TreasurySpokeUpgradeableTest is SpokeBase { function test_proxy_reinitialization_revertsWith_CallerNotProxyAdmin() public { TreasurySpokeInstance impl = new TreasurySpokeInstance(); ITransparentUpgradeableProxy proxy = ITransparentUpgradeableProxy( - DeployUtils.proxify( + AaveV4TestOrchestration.proxify( address(impl), proxyAdminOwner, abi.encodeCall(TreasurySpokeInstance.initialize, (TREASURY_ADMIN)) diff --git a/tests/unit/Spoke/TreasurySpoke.t.sol b/tests/unit/Spoke/TreasurySpoke.t.sol index 75b5b5cd2..8fd403ee7 100644 --- a/tests/unit/Spoke/TreasurySpoke.t.sol +++ b/tests/unit/Spoke/TreasurySpoke.t.sol @@ -11,7 +11,7 @@ contract TreasurySpokeTest is SpokeBase { function setUp() public virtual override { super.setUp(); _testToken = new MockERC20(); - (hub2, ) = hub2Fixture(); + (hub2, ) = _hub2Fixture(); // Add a reserve on spoke1 for hub2 vm.startPrank(ADMIN); @@ -52,7 +52,7 @@ contract TreasurySpokeTest is SpokeBase { vm.expectRevert( abi.encodeWithSelector(OwnableUpgradeable.OwnableInvalidOwner.selector, address(0)) ); - DeployUtils.proxify( + AaveV4TestOrchestration.proxify( address(impl), ADMIN, abi.encodeCall(TreasurySpokeInstance.initialize, (address(0))) @@ -506,8 +506,4 @@ contract TreasurySpokeTest is SpokeBase { 'hub2 reserve supplied shares' ); } - - function _treasurySpoke() internal view returns (ISpoke) { - return ISpoke(address(treasurySpoke)); - } } diff --git a/tests/unit/SpokeConfigurator.GranularAccessControl.t.sol b/tests/unit/SpokeConfigurator.GranularAccessControl.t.sol index 35a3b2f1f..1ed52d96e 100644 --- a/tests/unit/SpokeConfigurator.GranularAccessControl.t.sol +++ b/tests/unit/SpokeConfigurator.GranularAccessControl.t.sol @@ -6,17 +6,16 @@ import 'tests/unit/Spoke/SpokeBase.t.sol'; contract SpokeConfiguratorGranularAccessControlTest is SpokeBase { using SafeCast for uint256; - // Granular role constants - uint64 constant RESERVE_MANAGER_ROLE = 102; - uint64 constant LIQUIDATION_CONFIG_MANAGER_ROLE = 103; - uint64 constant POSITION_MANAGER_ADMIN_ROLE = 104; + // Granular role constants (must not collide with Roles.sol IDs 0-113, 200-309) + uint64 constant RESERVE_MANAGER_ROLE = 1002; + uint64 constant LIQUIDATION_CONFIG_MANAGER_ROLE = 1003; + uint64 constant POSITION_MANAGER_ADMIN_ROLE = 1004; // Role holders address RESERVE_MANAGER = makeAddr('RESERVE_MANAGER'); address LIQUIDATION_CONFIG_MANAGER = makeAddr('LIQUIDATION_CONFIG_MANAGER'); address POSITION_MANAGER_ADMIN = makeAddr('POSITION_MANAGER_ADMIN'); - SpokeConfigurator spokeConfigurator; IAccessManager manager; address spokeAddr; @@ -34,9 +33,9 @@ contract SpokeConfiguratorGranularAccessControlTest is SpokeBase { manager = IAccessManager(spoke1.authority()); spokeConfigurator = new SpokeConfigurator(address(manager)); - // Grant SPOKE_ADMIN_ROLE to spokeConfigurator so it can call spoke functions + // Grant SPOKE_CONFIGURATOR_ROLE to spokeConfigurator so it can call spoke functions vm.startPrank(ADMIN); - manager.grantRole(Roles.SPOKE_ADMIN_ROLE, address(spokeConfigurator), 0); + manager.grantRole(Roles.SPOKE_CONFIGURATOR_ROLE, address(spokeConfigurator), 0); // Grant granular roles to role holders manager.grantRole(RESERVE_MANAGER_ROLE, RESERVE_MANAGER, 0); @@ -206,6 +205,7 @@ contract SpokeConfiguratorGranularAccessControlTest is SpokeBase { function test_fuzz_unauthorized_cannotCall_reserveManagerMethods(address caller) public { vm.assume(caller != RESERVE_MANAGER); + vm.assume(caller != ADMIN); vm.assume(caller != address(0)); for (uint256 i = 0; i < reserveManagerCalldata.length; ++i) { @@ -223,6 +223,7 @@ contract SpokeConfiguratorGranularAccessControlTest is SpokeBase { address caller ) public { vm.assume(caller != LIQUIDATION_CONFIG_MANAGER); + vm.assume(caller != ADMIN); vm.assume(caller != address(0)); for (uint256 i = 0; i < liquidationConfigManagerCalldata.length; ++i) { @@ -240,6 +241,7 @@ contract SpokeConfiguratorGranularAccessControlTest is SpokeBase { function test_fuzz_unauthorized_cannotCall_positionManagerAdminMethods(address caller) public { vm.assume(caller != POSITION_MANAGER_ADMIN); + vm.assume(caller != ADMIN); vm.assume(caller != address(0)); for (uint256 i = 0; i < positionManagerAdminCalldata.length; ++i) { diff --git a/tests/unit/SpokeConfigurator.t.sol b/tests/unit/SpokeConfigurator.t.sol index f159ccc37..0132d95fd 100644 --- a/tests/unit/SpokeConfigurator.t.sol +++ b/tests/unit/SpokeConfigurator.t.sol @@ -6,8 +6,6 @@ import 'tests/unit/Spoke/SpokeBase.t.sol'; contract SpokeConfiguratorTest is SpokeBase { using SafeCast for uint256; - SpokeConfigurator public spokeConfigurator; - address public spokeAddr; ISpoke public spoke; uint256 public reserveId; @@ -15,12 +13,9 @@ contract SpokeConfiguratorTest is SpokeBase { function setUp() public virtual override { super.setUp(); - - spokeConfigurator = new SpokeConfigurator(spoke1.authority()); - setUpSpokeConfiguratorRoles(address(spokeConfigurator), spoke1.authority()); - spokeAddr = address(spoke1); spoke = ISpoke(spokeAddr); + _grantSpokeConfiguratorRole(spoke, address(spokeConfigurator)); reserveId = 0; invalidReserveId = spoke.getReserveCount(); } @@ -41,7 +36,7 @@ contract SpokeConfiguratorTest is SpokeBase { ); vm.expectEmit(address(spoke)); emit ISpoke.UpdateReservePriceSource(reserveId, newPriceSource); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); spokeConfigurator.updateReservePriceSource(spokeAddr, reserveId, newPriceSource); } @@ -65,7 +60,7 @@ contract SpokeConfiguratorTest is SpokeBase { ); vm.expectEmit(address(spoke)); emit ISpoke.UpdateLiquidationConfig(expectedLiquidationConfig); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); spokeConfigurator.updateLiquidationTargetHealthFactor(spokeAddr, newTargetHealthFactor); assertEq(spoke.getLiquidationConfig(), expectedLiquidationConfig); @@ -91,7 +86,7 @@ contract SpokeConfiguratorTest is SpokeBase { ); vm.expectEmit(address(spoke)); emit ISpoke.UpdateLiquidationConfig(expectedLiquidationConfig); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); spokeConfigurator.updateHealthFactorForMaxBonus(spokeAddr, newHealthFactorForMaxBonus); assertEq(spoke.getLiquidationConfig().healthFactorForMaxBonus, newHealthFactorForMaxBonus); @@ -117,7 +112,7 @@ contract SpokeConfiguratorTest is SpokeBase { ); vm.expectEmit(address(spoke)); emit ISpoke.UpdateLiquidationConfig(expectedLiquidationConfig); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); spokeConfigurator.updateLiquidationBonusFactor(spokeAddr, newLiquidationBonusFactor); assertEq(spoke.getLiquidationConfig(), expectedLiquidationConfig); @@ -151,7 +146,7 @@ contract SpokeConfiguratorTest is SpokeBase { ); vm.expectEmit(address(spoke)); emit ISpoke.UpdateLiquidationConfig(newLiquidationConfig); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); spokeConfigurator.updateLiquidationConfig(spokeAddr, newLiquidationConfig); assertEq(spoke.getLiquidationConfig(), newLiquidationConfig); @@ -200,7 +195,7 @@ contract SpokeConfiguratorTest is SpokeBase { emit ISpoke.UpdateReserveConfig(expectedReserveId, config); vm.expectEmit(address(spoke)); emit ISpoke.AddDynamicReserveConfig(expectedReserveId, 0, dynamicConfig); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); uint256 actualReserveId = spokeConfigurator.addReserve({ spoke: spokeAddr, hub: address(hub1), @@ -233,7 +228,7 @@ contract SpokeConfiguratorTest is SpokeBase { ); vm.expectEmit(address(spoke)); emit ISpoke.UpdateReserveConfig(reserveId, expectedReserveConfig); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); spokeConfigurator.updatePaused(spokeAddr, reserveId, expectedReserveConfig.paused); assertEq(spoke.getReserveConfig(reserveId), expectedReserveConfig); @@ -260,7 +255,7 @@ contract SpokeConfiguratorTest is SpokeBase { ); vm.expectEmit(address(spoke)); emit ISpoke.UpdateReserveConfig(reserveId, expectedReserveConfig); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); spokeConfigurator.updateFrozen(spokeAddr, reserveId, expectedReserveConfig.frozen); assertEq(spoke.getReserveConfig(reserveId), expectedReserveConfig); @@ -287,7 +282,7 @@ contract SpokeConfiguratorTest is SpokeBase { ); vm.expectEmit(address(spoke)); emit ISpoke.UpdateReserveConfig(reserveId, expectedReserveConfig); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); spokeConfigurator.updateBorrowable(spokeAddr, reserveId, expectedReserveConfig.borrowable); assertEq(spoke.getReserveConfig(reserveId), expectedReserveConfig); @@ -314,7 +309,7 @@ contract SpokeConfiguratorTest is SpokeBase { ); vm.expectEmit(address(spoke)); emit ISpoke.UpdateReserveConfig(reserveId, expectedReserveConfig); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); spokeConfigurator.updateReceiveSharesEnabled( spokeAddr, reserveId, @@ -345,7 +340,7 @@ contract SpokeConfiguratorTest is SpokeBase { ); vm.expectEmit(address(spoke)); emit ISpoke.UpdateReserveConfig(reserveId, expectedReserveConfig); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); spokeConfigurator.updateCollateralRisk(spokeAddr, reserveId, newCollateralRisk); assertEq(spoke.getReserveConfig(reserveId), expectedReserveConfig); @@ -374,7 +369,7 @@ contract SpokeConfiguratorTest is SpokeBase { ); vm.expectEmit(address(spoke)); emit ISpoke.AddDynamicReserveConfig(reserveId, expectedConfigKey, expectedDynamicReserveConfig); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); uint32 dynamicConfigKey = spokeConfigurator.addCollateralFactor( spokeAddr, reserveId, @@ -418,7 +413,7 @@ contract SpokeConfiguratorTest is SpokeBase { dynamicConfigKey, expectedDynamicReserveConfig ); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); spokeConfigurator.updateCollateralFactor( spokeAddr, reserveId, @@ -455,7 +450,7 @@ contract SpokeConfiguratorTest is SpokeBase { ); vm.expectEmit(address(spoke)); emit ISpoke.AddDynamicReserveConfig(reserveId, expectedConfigKey, expectedDynamicReserveConfig); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); uint32 dynamicConfigKey = spokeConfigurator.addMaxLiquidationBonus( spokeAddr, reserveId, @@ -499,7 +494,7 @@ contract SpokeConfiguratorTest is SpokeBase { dynamicConfigKey, expectedDynamicReserveConfig ); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); spokeConfigurator.updateMaxLiquidationBonus( spokeAddr, reserveId, @@ -536,7 +531,7 @@ contract SpokeConfiguratorTest is SpokeBase { ); vm.expectEmit(address(spoke)); emit ISpoke.AddDynamicReserveConfig(reserveId, expectedConfigKey, expectedDynamicReserveConfig); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); uint32 dynamicConfigKey = spokeConfigurator.addLiquidationFee( spokeAddr, reserveId, @@ -580,7 +575,7 @@ contract SpokeConfiguratorTest is SpokeBase { dynamicConfigKey, expectedDynamicReserveConfig ); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); spokeConfigurator.updateLiquidationFee( spokeAddr, reserveId, @@ -625,7 +620,7 @@ contract SpokeConfiguratorTest is SpokeBase { ); vm.expectEmit(address(spoke)); emit ISpoke.AddDynamicReserveConfig(reserveId, expectedConfigKey, newDynamicReserveConfig); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); uint32 actualConfigKey = spokeConfigurator.addDynamicReserveConfig( spokeAddr, reserveId, @@ -675,7 +670,7 @@ contract SpokeConfiguratorTest is SpokeBase { vm.expectEmit(address(spoke)); emit ISpoke.UpdateDynamicReserveConfig(reserveId, configKeyToUpdate, newDynamicReserveConfig); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); spokeConfigurator.updateDynamicReserveConfig( spokeAddr, reserveId, @@ -706,7 +701,7 @@ contract SpokeConfiguratorTest is SpokeBase { emit ISpoke.UpdateReserveConfig(reserveIdx, reserveConfig); } - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); spokeConfigurator.pauseAllReserves(spokeAddr); for (uint256 reserveIdx; reserveIdx < spoke.getReserveCount(); ++reserveIdx) { @@ -731,7 +726,7 @@ contract SpokeConfiguratorTest is SpokeBase { ); vm.expectEmit(address(spoke)); emit ISpoke.UpdateReserveConfig(reserveId, reserveConfig); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); spokeConfigurator.pauseReserve(spokeAddr, reserveId); assertTrue(spoke.getReserveConfig(reserveId).paused); @@ -754,7 +749,7 @@ contract SpokeConfiguratorTest is SpokeBase { emit ISpoke.UpdateReserveConfig(id, reserveConfig); } - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); spokeConfigurator.freezeAllReserves(spokeAddr); for (uint256 id; id < spoke.getReserveCount(); ++id) { @@ -779,7 +774,7 @@ contract SpokeConfiguratorTest is SpokeBase { ); vm.expectEmit(address(spoke)); emit ISpoke.UpdateReserveConfig(reserveId, reserveConfig); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); spokeConfigurator.freezeReserve(spokeAddr, reserveId); assertTrue(spoke.getReserveConfig(reserveId).frozen); @@ -803,7 +798,7 @@ contract SpokeConfiguratorTest is SpokeBase { ); vm.expectEmit(address(spoke)); emit ISpoke.UpdatePositionManager(newPositionManager, active); - vm.prank(SPOKE_CONFIGURATOR); + vm.prank(SPOKE_CONFIGURATOR_ADMIN); spokeConfigurator.updatePositionManager(spokeAddr, newPositionManager, active); assertEq(spoke.isPositionManagerActive(newPositionManager), active); } diff --git a/tests/unit/TokenizationSpoke/TokenizationSpoke.Base.t.sol b/tests/unit/TokenizationSpoke/TokenizationSpoke.Base.t.sol index 79706dbb5..791473d32 100644 --- a/tests/unit/TokenizationSpoke/TokenizationSpoke.Base.t.sol +++ b/tests/unit/TokenizationSpoke/TokenizationSpoke.Base.t.sol @@ -9,8 +9,8 @@ contract TokenizationSpokeBaseTest is Base { string public constant SHARE_SYMBOL = 'chDAI'; function setUp() public virtual override { - deployFixtures(); - initEnvironment(); + super.setUp(); + _initEnvironment(); daiVault = _deployTokenizationSpoke( hub1, address(tokenList.dai), diff --git a/tests/unit/TokenizationSpoke/TokenizationSpoke.Config.t.sol b/tests/unit/TokenizationSpoke/TokenizationSpoke.Config.t.sol index 1d5a7239e..295dc9247 100644 --- a/tests/unit/TokenizationSpoke/TokenizationSpoke.Config.t.sol +++ b/tests/unit/TokenizationSpoke/TokenizationSpoke.Config.t.sol @@ -43,14 +43,14 @@ contract TokenizationSpokeConfigTest is TokenizationSpokeBaseTest { } function test_configuration() public view { - ProxyAdmin proxyAdmin = ProxyAdmin(_getProxyAdminAddress(address(daiVault))); + ProxyAdmin proxyAdmin = ProxyAdmin(ProxyHelper.getProxyAdmin(address(daiVault))); assertEq(proxyAdmin.owner(), ADMIN); assertEq(proxyAdmin.UPGRADE_INTERFACE_VERSION(), '5.0.0'); assertEq( - _getProxyInitializedVersion(address(daiVault)), + ProxyHelper.getProxyInitializedVersion(address(daiVault)), TokenizationSpokeInstance(address(daiVault)).SPOKE_REVISION() ); - address implementation = _getImplementationAddress(address(daiVault)); - assertEq(_getProxyInitializedVersion(implementation), type(uint64).max); + address implementation = ProxyHelper.getImplementation(address(daiVault)); + assertEq(ProxyHelper.getProxyInitializedVersion(implementation), type(uint64).max); } } diff --git a/tests/unit/TokenizationSpoke/TokenizationSpoke.Upgradeable.t.sol b/tests/unit/TokenizationSpoke/TokenizationSpoke.Upgradeable.t.sol index fc3e9d501..daa39a209 100644 --- a/tests/unit/TokenizationSpoke/TokenizationSpoke.Upgradeable.t.sol +++ b/tests/unit/TokenizationSpoke/TokenizationSpoke.Upgradeable.t.sol @@ -16,7 +16,7 @@ contract TokenizationSpokeUpgradeableTest is TokenizationSpokeBaseTest { assertEq(address(vaultImpl), vaultImplAddress); assertEq(vaultImpl.SPOKE_REVISION(), revision); - assertEq(_getProxyInitializedVersion(vaultImplAddress), type(uint64).max); + assertEq(ProxyHelper.getProxyInitializedVersion(vaultImplAddress), type(uint64).max); vm.expectRevert(Initializable.InvalidInitialization.selector); vaultImpl.initialize(SHARE_NAME, SHARE_SYMBOL); @@ -50,10 +50,10 @@ contract TokenizationSpokeUpgradeableTest is TokenizationSpokeBaseTest { ); assertEq(address(vaultProxy), vaultProxyAddress); - assertEq(_getProxyAdminAddress(address(vaultProxy)), proxyAdminAddress); - assertEq(_getImplementationAddress(address(vaultProxy)), address(vaultImpl)); + assertEq(ProxyHelper.getProxyAdmin(address(vaultProxy)), proxyAdminAddress); + assertEq(ProxyHelper.getImplementation(address(vaultProxy)), address(vaultImpl)); - assertEq(_getProxyInitializedVersion(address(vaultProxy)), revision); + assertEq(ProxyHelper.getProxyInitializedVersion(address(vaultProxy)), revision); assertEq(vaultProxy.name(), SHARE_NAME); assertEq(vaultProxy.symbol(), SHARE_SYMBOL); } @@ -83,7 +83,7 @@ contract TokenizationSpokeUpgradeableTest is TokenizationSpokeBaseTest { vm.expectEmit(address(vaultProxy)); emit Initializable.Initialized(secondRevision); vm.recordLogs(); - vm.prank(_getProxyAdminAddress(address(vaultProxy))); + vm.prank(ProxyHelper.getProxyAdmin(address(vaultProxy))); vaultProxy.upgradeToAndCall( address(vaultImpl2), _getInitializeCalldata(newShareName, newShareSymbol) @@ -122,7 +122,7 @@ contract TokenizationSpokeUpgradeableTest is TokenizationSpokeBaseTest { ); vm.expectRevert(Initializable.InvalidInitialization.selector); - vm.prank(_getProxyAdminAddress(address(vaultProxy))); + vm.prank(ProxyHelper.getProxyAdmin(address(vaultProxy))); vaultProxy.upgradeToAndCall( address(vaultImpl), _getInitializeCalldata(SHARE_NAME, SHARE_SYMBOL) @@ -131,7 +131,7 @@ contract TokenizationSpokeUpgradeableTest is TokenizationSpokeBaseTest { uint64 secondRevision = uint64(vm.randomUint(0, initialRevision - 1)); TokenizationSpokeInstance vaultImpl2 = _deployMockTokenizationSpokeInstance(secondRevision); vm.expectRevert(Initializable.InvalidInitialization.selector); - vm.prank(_getProxyAdminAddress(address(vaultProxy))); + vm.prank(ProxyHelper.getProxyAdmin(address(vaultProxy))); vaultProxy.upgradeToAndCall( address(vaultImpl2), _getInitializeCalldata(SHARE_NAME, SHARE_SYMBOL) diff --git a/tests/unit/libraries/LiquidationLogic/LiquidationLogic.ExecuteLiquidation.t.sol b/tests/unit/libraries/LiquidationLogic/LiquidationLogic.ExecuteLiquidation.t.sol index 22a32eaa9..c62370016 100644 --- a/tests/unit/libraries/LiquidationLogic/LiquidationLogic.ExecuteLiquidation.t.sol +++ b/tests/unit/libraries/LiquidationLogic/LiquidationLogic.ExecuteLiquidation.t.sol @@ -29,7 +29,7 @@ contract LiquidationLogicExecuteLiquidationTest is LiquidationLogicBaseTest { super.setUp(); IHub collateralReserveHub = hub1; _mockSupplySharePrice(collateralReserveHub, usdxAssetId, 12_500.25e6, 10_000e6); - (IHub debtReserveHub, ) = hub2Fixture(); + (IHub debtReserveHub, ) = _hub2Fixture(); _mockDrawnRateBps(debtReserveHub.getAsset(wethAssetId).irStrategy, 5_00); // Mock params diff --git a/tests/unit/libraries/LiquidationLogic/LiquidationLogic.LiquidateUser.t.sol b/tests/unit/libraries/LiquidationLogic/LiquidationLogic.LiquidateUser.t.sol index d591c13a9..c4ad84969 100644 --- a/tests/unit/libraries/LiquidationLogic/LiquidationLogic.LiquidateUser.t.sol +++ b/tests/unit/libraries/LiquidationLogic/LiquidationLogic.LiquidateUser.t.sol @@ -30,7 +30,7 @@ contract LiquidationLogicLiquidateUserTest is LiquidationLogicBaseTest { super.setUp(); collateralReserveHub = hub1; _mockSupplySharePrice(collateralReserveHub, usdxAssetId, 12_500.25e6, 10_000e6); - (debtReserveHub, ) = hub2Fixture(); + (debtReserveHub, ) = _hub2Fixture(); _mockDrawnRateBps(debtReserveHub.getAsset(wethAssetId).irStrategy, 5_00); // Mock params diff --git a/tests/unit/position-manager/SignatureGateway/SignatureGateway.Base.t.sol b/tests/unit/position-manager/SignatureGateway/SignatureGateway.Base.t.sol index 3003e9689..d2b81d95c 100644 --- a/tests/unit/position-manager/SignatureGateway/SignatureGateway.Base.t.sol +++ b/tests/unit/position-manager/SignatureGateway/SignatureGateway.Base.t.sol @@ -7,8 +7,7 @@ contract SignatureGatewayBaseTest is SpokeBase { ISignatureGateway public gateway; function setUp() public virtual override { - deployFixtures(); - initEnvironment(); + super.setUp(); gateway = ISignatureGateway(new SignatureGateway(ADMIN)); vm.prank(address(ADMIN)); diff --git a/tests/utils/BatchTestProcedures.sol b/tests/utils/BatchTestProcedures.sol new file mode 100644 index 000000000..beeb5b5d6 --- /dev/null +++ b/tests/utils/BatchTestProcedures.sol @@ -0,0 +1,926 @@ +// 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 {AaveV4TestOrchestration} from 'tests/deployments/orchestration/AaveV4TestOrchestration.sol'; +import {AaveV4DeployProcedureBase} from 'src/deployments/procedures/AaveV4DeployProcedureBase.sol'; +import {Roles} from 'src/deployments/utils/libraries/Roles.sol'; +import {Logger} from 'src/deployments/utils/Logger.sol'; +import {InputUtils} from 'src/deployments/utils/InputUtils.sol'; +import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; +import {Create2TestHelper} from 'tests/utils/Create2TestHelper.sol'; +import {OrchestrationReports} from 'src/deployments/libraries/OrchestrationReports.sol'; +import {Constants} from 'tests/Constants.sol'; + +// libraries +import {ProxyHelper} from 'tests/utils/ProxyHelper.sol'; +import {BytecodeHelper} from 'src/deployments/utils/libraries/BytecodeHelper.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'; +import {INativeTokenGateway} from 'src/position-manager/interfaces/INativeTokenGateway.sol'; + +contract BatchTestProcedures is Test, InputUtils, Create2TestHelper, 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'); + // if post-deployment, skip impl checks, native wrapper, as they aren't included in the report + bool internal _postDeploymentCheck; + + function setUp() public virtual { + _spokePositionUpdaterRoleSelectors = Roles.getSpokePositionUpdaterRoleSelectors(); + _spokeConfiguratorRoleSelectors = Roles.getSpokeConfiguratorRoleSelectors(); + + _hubFeeMinterRoleSelectors = Roles.getHubFeeMinterRoleSelectors(); + _hubConfiguratorRoleSelectors = Roles.getHubConfiguratorRoleSelectors(); + + _weth9 = _deployWETH(); + _logger = new Logger('dummy/path'); + _hubLabels = ['hub1', 'hub2', 'hub3']; + _spokeLabels = ['spoke1', 'spoke2', 'spoke3']; + + _etchCreate2Factory(); + } + + function checkedV4Deployment() public { + bytes memory hubBytecode = BytecodeHelper.getHubBytecode(); + bytes memory spokeBytecode = BytecodeHelper.getSpokeBytecode(); + + vm.startPrank(_deployer); + OrchestrationReports.FullDeploymentReport memory report = AaveV4DeployOrchestration + .deployAaveV4(_logger, _deployer, _inputs, hubBytecode, spokeBytecode); + vm.stopPrank(); + _checkDeployment({report: report, inputs: _inputs}); + } + + function _checkDeployment( + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal view { + _checkFullReport({report: report, inputs: inputs}); + _checkBatchDeployments({report: report, inputs: inputs}); + _checkRoles(report, _inputs); + } + + function _checkBatchDeployments( + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal view { + _checkSpokeBatchDeployments({report: report, inputs: inputs}); + _checkHubBatchDeployments({report: report, inputs: inputs}); + _checkConfiguratorBatchDeployments({report: report}); + _checkGatewayBatchDeployments({report: report, inputs: inputs}); + } + + function _checkConfiguratorBatchDeployments( + OrchestrationReports.FullDeploymentReport memory report + ) internal view { + assertEq( + IAccessManaged(report.configuratorBatchReport.hubConfigurator).authority(), + report.authorityBatchReport.accessManager, + 'HubConfigurator authority' + ); + assertEq( + IAccessManaged(report.configuratorBatchReport.spokeConfigurator).authority(), + report.authorityBatchReport.accessManager, + 'SpokeConfigurator authority' + ); + } + + function _checkGatewayBatchDeployments( + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal view { + if (inputs.deployNativeTokenGateway && !_postDeploymentCheck) { + assertEq( + INativeTokenGateway(report.gatewaysBatchReport.nativeGateway).NATIVE_TOKEN_WRAPPER(), + inputs.nativeWrapper, + 'NativeGateway NATIVE_TOKEN_WRAPPER' + ); + } + } + + function _checkRoles( + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal view { + IAccessManagerEnumerable accessManager = IAccessManagerEnumerable( + report.authorityBatchReport.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.hubConfiguratorAdmin = inputs.hubConfiguratorAdmin != address(0) + ? inputs.hubConfiguratorAdmin + : _deployer; + inputs.treasurySpokeOwner = inputs.treasurySpokeOwner != address(0) + ? inputs.treasurySpokeOwner + : _deployer; + inputs.spokeAdmin = inputs.spokeAdmin != address(0) ? inputs.spokeAdmin : _deployer; + inputs.hubProxyAdminOwner = inputs.hubProxyAdminOwner != address(0) + ? inputs.hubProxyAdminOwner + : _deployer; + inputs.spokeProxyAdminOwner = inputs.spokeProxyAdminOwner != address(0) + ? inputs.spokeProxyAdminOwner + : _deployer; + inputs.spokeConfiguratorAdmin = inputs.spokeConfiguratorAdmin != address(0) + ? inputs.spokeConfiguratorAdmin + : _deployer; + inputs.gatewayOwner = inputs.gatewayOwner != address(0) ? inputs.gatewayOwner : _deployer; + inputs.positionManagerOwner = inputs.positionManagerOwner != address(0) + ? inputs.positionManagerOwner + : _deployer; + + // Sync parallel arrays with spokeLabels length + inputs.hubLabels = _hubLabels; + inputs.spokeLabels = _spokeLabels; + inputs.spokeMaxReservesLimits = _defaultSpokeMaxReservesLimits(_spokeLabels.length); + inputs.nativeWrapper = _weth9; + inputs.deployNativeTokenGateway = true; + inputs.deploySignatureGateway = true; + inputs.deployPositionManagers = true; + + return inputs; + } + + function _defaultSpokeMaxReservesLimits( + uint256 count + ) internal pure returns (uint16[] memory limits) { + limits = new uint16[](count); + for (uint256 i; i < count; i++) { + limits[i] = Constants.MAX_ALLOWED_USER_RESERVES_LIMIT; + } + } + + function _checkFullReport( + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal view { + if (inputs.deployNativeTokenGateway) { + assertNotEq(report.gatewaysBatchReport.nativeGateway, address(0), 'NativeGateway'); + } else { + assertEq(report.gatewaysBatchReport.nativeGateway, address(0), 'Zero NativeGateway'); + } + if (inputs.deploySignatureGateway) { + assertNotEq(report.gatewaysBatchReport.signatureGateway, address(0), 'SignatureGateway'); + } else { + assertEq(report.gatewaysBatchReport.signatureGateway, address(0), 'Zero SignatureGateway'); + } + if (inputs.deployPositionManagers) { + assertNotEq( + report.positionManagerBatchReport.giverPositionManager, + address(0), + 'GiverPositionManager' + ); + assertNotEq( + report.positionManagerBatchReport.takerPositionManager, + address(0), + 'TakerPositionManager' + ); + assertNotEq( + report.positionManagerBatchReport.configPositionManager, + address(0), + 'ConfigPositionManager' + ); + } else { + assertEq( + report.positionManagerBatchReport.giverPositionManager, + address(0), + 'Zero GiverPositionManager' + ); + assertEq( + report.positionManagerBatchReport.takerPositionManager, + address(0), + 'Zero TakerPositionManager' + ); + assertEq( + report.positionManagerBatchReport.configPositionManager, + address(0), + 'Zero ConfigPositionManager' + ); + } + + assertNotEq(report.authorityBatchReport.accessManager, address(0), 'AccessManager'); + assertNotEq(report.configuratorBatchReport.spokeConfigurator, address(0), 'SpokeConfigurator'); + assertNotEq(report.configuratorBatchReport.hubConfigurator, address(0), 'HubConfigurator'); + assertNotEq(report.treasurySpokeBatchReport.treasurySpoke, address(0), 'TreasurySpoke'); + for (uint256 i = 0; i < report.hubInstanceBatchReports.length; i++) { + assertNotEq(report.hubInstanceBatchReports[i].report.hubProxy, address(0), 'Hub'); + if (!_postDeploymentCheck) { + assertNotEq( + report.hubInstanceBatchReports[i].report.hubImplementation, + address(0), + 'HubImplementation' + ); + } + assertNotEq(report.hubInstanceBatchReports[i].report.irStrategy, address(0), 'IRStrategy'); + } + for (uint256 i = 0; i < report.spokeInstanceBatchReports.length; i++) { + assertNotEq(report.spokeInstanceBatchReports[i].report.spokeProxy, address(0), 'SpokeProxy'); + if (!_postDeploymentCheck) { + assertNotEq( + report.spokeInstanceBatchReports[i].report.spokeImplementation, + address(0), + 'SpokeImplementation' + ); + } + assertNotEq(report.spokeInstanceBatchReports[i].report.aaveOracle, address(0), 'AaveOracle'); + } + assertEq( + report.hubInstanceBatchReports.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.authorityBatchReport.accessManager, + expectedMaxReservesLimit: inputs.spokeMaxReservesLimits.length > i + ? inputs.spokeMaxReservesLimits[i] + : Constants.MAX_ALLOWED_USER_RESERVES_LIMIT, + label: label + }); + _checkOracleDeployment({report: spokeReport, label: label}); + } + } + + function _checkSpokeDeployment( + OrchestrationReports.SpokeDeploymentReport memory report, + address accessManager, + uint16 expectedMaxReservesLimit, + string memory label + ) internal view { + if (!_postDeploymentCheck) { + 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') + ); + assertEq( + ISpoke(report.report.spokeProxy).MAX_USER_RESERVES_LIMIT(), + expectedMaxReservesLimit, + string.concat(label, ' max user reserves limit') + ); + assertEq( + ProxyHelper.getProxyInitializedVersion( + ProxyHelper.getImplementation(report.report.spokeProxy) + ), + type(uint64).max, + string.concat(label, ' implementation initializers disabled') + ); + // verify the non-immutable portions match + _assertBytecodeMatchExcludingImmutables( + ProxyHelper.getImplementation(report.report.spokeProxy).code, + vm.getDeployedCode('src/spoke/instances/SpokeInstance.sol:SpokeInstance'), + string.concat(label, ' spoke implementation bytecode') + ); + } + + 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') + ); + } + + 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.hubInstanceBatchReports[i]; + + _checkHubDeployment({ + report: hubReport, + accessManager: report.authorityBatchReport.accessManager, + expectedHubProxyAdminOwner: inputs.hubProxyAdminOwner, + label: label + }); + _checkInterestRateStrategyDeployment({report: hubReport, label: label}); + } + _checkTreasurySpokeDeployment(report); + } + + function _checkHubDeployment( + OrchestrationReports.HubDeploymentReport memory report, + address accessManager, + address expectedHubProxyAdminOwner, + string memory label + ) internal view { + if (!_postDeploymentCheck) { + assertEq( + ProxyHelper.getImplementation(report.report.hubProxy), + report.report.hubImplementation, + string.concat(label, ' implementation') + ); + address proxyAdminOwner = Ownable(ProxyHelper.getProxyAdmin(report.report.hubProxy)).owner(); + assertEq( + proxyAdminOwner, + expectedHubProxyAdminOwner, + string.concat(label, ' hub proxy admin owner') + ); + } + assertEq( + IAccessManaged(report.report.hubProxy).authority(), + accessManager, + string.concat(label, ' hub authority') + ); + assertEq( + ProxyHelper.getProxyInitializedVersion(ProxyHelper.getImplementation(report.report.hubProxy)), + type(uint64).max, + string.concat(label, ' implementation initializers disabled') + ); + assertEq( + ProxyHelper.getImplementation(report.report.hubProxy).codehash, + keccak256(vm.getDeployedCode('src/hub/instances/HubInstance.sol:HubInstance')), + string.concat(label, ' hub implementation bytecode') + ); + } + + function _checkInterestRateStrategyDeployment( + OrchestrationReports.HubDeploymentReport memory report, + string memory label + ) internal view { + assertEq( + IAssetInterestRateStrategy(report.report.irStrategy).HUB(), + report.report.hubProxy, + string.concat(label, ' hub on interest rate strategy') + ); + } + + function _checkTreasurySpokeDeployment( + OrchestrationReports.FullDeploymentReport memory report + ) internal pure { + assertNotEq( + report.treasurySpokeBatchReport.treasurySpoke, + address(0), + 'treasury spoke deployed' + ); + } + + function _checkAccessManagerRoles( + IAccessManagerEnumerable accessManager, + FullDeployInputs memory inputs + ) internal view { + address expectedAdmin = (inputs.grantRoles && inputs.accessManagerAdmin != address(0)) + ? inputs.accessManagerAdmin + : _deployer; + assertEq( + accessManager.getRoleMember(Roles.ACCESS_MANAGER_DEFAULT_ADMIN, 0), + expectedAdmin, + 'DefaultAdminRoleMember' + ); + assertEq( + accessManager.getRoleMemberCount(Roles.ACCESS_MANAGER_DEFAULT_ADMIN), + 1, + 'DefaultAdminRoleCount' + ); + + (bool adminHasRole, ) = accessManager.hasRole( + Roles.ACCESS_MANAGER_DEFAULT_ADMIN, + 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_USER_POSITION_UPDATER_ROLE), + 1, + 'SpokePositionUpdaterRole member count' + ); + assertEq( + accessManager.getRoleMember(Roles.SPOKE_USER_POSITION_UPDATER_ROLE, 0), + inputs.spokeAdmin, + 'SpokePositionUpdaterRole member - spoke admin' + ); + } else { + assertEq( + accessManager.getRoleMemberCount(Roles.SPOKE_USER_POSITION_UPDATER_ROLE), + 0, + 'SpokePositionUpdaterRoleCount' + ); + } + + 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, 'SpokePositionUpdaterRole allowed'); + assertEq(delay, 0, 'SpokePositionUpdaterRole delay'); + + assertEq( + accessManager.getTargetFunctionRole( + report.spokeInstanceBatchReports[i].report.spokeProxy, + _spokePositionUpdaterRoleSelectors[j] + ), + Roles.SPOKE_USER_POSITION_UPDATER_ROLE, + 'SpokePositionUpdaterRole target function' + ); + } + } + } + + function _checkHubRoles( + IAccessManagerEnumerable accessManager, + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal view { + _checkHubBatchRoles(accessManager, report, inputs); + _checkHubSelectorRoles(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, + 'HubFeeMinterRoleCount' + ); + assertEq( + accessManager.getRoleMember(Roles.HUB_FEE_MINTER_ROLE, 0), + inputs.hubAdmin, + 'HubFeeMinterRole member - hub admin' + ); + } else { + assertEq( + accessManager.getRoleMemberCount(Roles.HUB_FEE_MINTER_ROLE), + 0, + 'HubFeeMinterRoleCount' + ); + } + _checkTreasurySpokeRoles(report.treasurySpokeBatchReport.treasurySpoke, inputs); + for (uint256 i = 0; i < inputs.hubLabels.length; i++) { + for (uint256 j = 0; j < _hubFeeMinterRoleSelectors.length; j++) { + assertEq( + accessManager.getTargetFunctionRole( + report.hubInstanceBatchReports[i].report.hubProxy, + _hubFeeMinterRoleSelectors[j] + ), + Roles.HUB_FEE_MINTER_ROLE, + 'HubFeeMinterRole target function' + ); + + (bool allowed, uint32 delay) = accessManager.canCall( + inputs.hubAdmin, + report.hubInstanceBatchReports[i].report.hubProxy, + _hubFeeMinterRoleSelectors[j] + ); + assertEq(allowed, inputs.grantRoles ? true : false, 'HubFeeMinterRole allowed'); + assertEq(delay, 0, 'HubFeeMinterRole delay'); + } + } + } + + function _checkTreasurySpokeRoles( + address treasurySpoke, + FullDeployInputs memory inputs + ) internal view { + assertEq(Ownable(treasurySpoke).owner(), inputs.treasurySpokeOwner, 'treasury spoke owner'); + } + + function _checkHubSelectorRoles( + 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.hubInstanceBatchReports[i].report.hubProxy, + _hubConfiguratorRoleSelectors[j] + ), + Roles.HUB_CONFIGURATOR_ROLE, + 'HubConfiguratorRole target function' + ); + bool allowed; + uint32 delay; + + (allowed, delay) = accessManager.canCall( + report.configuratorBatchReport.hubConfigurator, + report.hubInstanceBatchReports[i].report.hubProxy, + _hubConfiguratorRoleSelectors[j] + ); + assertEq( + allowed, + inputs.grantRoles ? true : false, + 'HubConfiguratorRole allowed - configurator' + ); + assertEq(delay, 0, 'HubConfiguratorRole delay - configurator'); + + (allowed, delay) = accessManager.canCall( + inputs.hubAdmin, + report.hubInstanceBatchReports[i].report.hubProxy, + _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 { + assertEq( + IAccessManaged(report.configuratorBatchReport.hubConfigurator).authority(), + report.authorityBatchReport.accessManager, + 'HubConfigurator authority' + ); + assertEq( + IAccessManaged(report.configuratorBatchReport.spokeConfigurator).authority(), + report.authorityBatchReport.accessManager, + 'SpokeConfigurator authority' + ); + + IAccessManagerEnumerable accessManager = IAccessManagerEnumerable( + report.authorityBatchReport.accessManager + ); + + _checkHubConfiguratorBatchRoles(accessManager, report, inputs); + _checkSpokeConfiguratorBatchRoles(accessManager, report, inputs); + } + + function _checkHubConfiguratorBatchRoles( + IAccessManagerEnumerable accessManager, + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal view { + address hubConfigurator = report.configuratorBatchReport.hubConfigurator; + bytes4[] memory selectors = Roles.getHubConfiguratorDomainAdminRoleSelectors(); + + for (uint256 i; i < selectors.length; i++) { + assertEq( + accessManager.getTargetFunctionRole(hubConfigurator, selectors[i]), + Roles.HUB_CONFIGURATOR_DOMAIN_ADMIN_ROLE, + 'HubConfigurator domain admin selector role mapping' + ); + } + + if (inputs.grantRoles && inputs.hubLabels.length > 0) { + for (uint256 i; i < selectors.length; i++) { + (bool allowed, ) = accessManager.canCall( + inputs.hubConfiguratorAdmin, + hubConfigurator, + selectors[i] + ); + assertTrue(allowed, 'HubConfigurator admin canCall selector'); + } + } + } + + function _checkSpokeConfiguratorBatchRoles( + IAccessManagerEnumerable accessManager, + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal view { + address spokeConfigurator = report.configuratorBatchReport.spokeConfigurator; + bytes4[] memory selectors = Roles.getSpokeConfiguratorDomainAdminRoleSelectors(); + + for (uint256 i; i < selectors.length; i++) { + assertEq( + accessManager.getTargetFunctionRole(spokeConfigurator, selectors[i]), + Roles.SPOKE_CONFIGURATOR_DOMAIN_ADMIN_ROLE, + 'SpokeConfigurator domain admin selector role mapping' + ); + } + + if (inputs.grantRoles && inputs.spokeLabels.length > 0) { + for (uint256 i; i < selectors.length; i++) { + (bool allowed, ) = accessManager.canCall( + inputs.spokeConfiguratorAdmin, + spokeConfigurator, + selectors[i] + ); + assertTrue(allowed, 'SpokeConfigurator admin canCall selector'); + } + } + } + + function _checkGatewayRoles( + OrchestrationReports.FullDeploymentReport memory report, + FullDeployInputs memory inputs + ) internal view { + if (inputs.deployNativeTokenGateway) { + assertEq( + Ownable(report.gatewaysBatchReport.nativeGateway).owner(), + inputs.gatewayOwner, + 'NativeGateway owner' + ); + } + if (inputs.deploySignatureGateway) { + assertEq( + Ownable(report.gatewaysBatchReport.signatureGateway).owner(), + inputs.gatewayOwner, + 'SignatureGateway owner' + ); + } + if (inputs.deployPositionManagers) { + assertEq( + Ownable(report.positionManagerBatchReport.giverPositionManager).owner(), + inputs.positionManagerOwner, + 'GiverPositionManager owner' + ); + assertEq( + Ownable(report.positionManagerBatchReport.takerPositionManager).owner(), + inputs.positionManagerOwner, + 'TakerPositionManager owner' + ); + assertEq( + Ownable(report.positionManagerBatchReport.configPositionManager).owner(), + inputs.positionManagerOwner, + 'ConfigPositionManager owner' + ); + } + } + + function _etchSetup() internal { + _etchCreate2Factory(); + } + + function _checkAllAddressesHaveCode( + OrchestrationReports.FullDeploymentReport memory report + ) internal view { + _assertHasCode(report.authorityBatchReport.accessManager, 'accessManager'); + _assertHasCode(report.configuratorBatchReport.hubConfigurator, 'hubConfigurator'); + _assertHasCode(report.configuratorBatchReport.spokeConfigurator, 'spokeConfigurator'); + _assertHasCode(report.treasurySpokeBatchReport.treasurySpoke, 'treasurySpoke'); + + for (uint256 i; i < report.hubInstanceBatchReports.length; i++) { + string memory label = report.hubInstanceBatchReports[i].label; + _assertHasCode( + report.hubInstanceBatchReports[i].report.hubProxy, + string.concat('hub proxy: ', label) + ); + if (!_postDeploymentCheck) { + _assertHasCode( + report.hubInstanceBatchReports[i].report.hubImplementation, + string.concat('hub impl: ', label) + ); + } + _assertHasCode( + report.hubInstanceBatchReports[i].report.irStrategy, + string.concat('irStrategy: ', label) + ); + } + + for (uint256 i; i < report.spokeInstanceBatchReports.length; i++) { + string memory label = report.spokeInstanceBatchReports[i].label; + _assertHasCode( + report.spokeInstanceBatchReports[i].report.spokeProxy, + string.concat('spoke proxy: ', label) + ); + if (!_postDeploymentCheck) { + _assertHasCode( + report.spokeInstanceBatchReports[i].report.spokeImplementation, + string.concat('spoke impl: ', label) + ); + } + _assertHasCode( + report.spokeInstanceBatchReports[i].report.aaveOracle, + string.concat('oracle: ', label) + ); + } + + if (report.gatewaysBatchReport.nativeGateway != address(0)) { + _assertHasCode(report.gatewaysBatchReport.nativeGateway, 'nativeTokenGateway'); + } + if (report.gatewaysBatchReport.signatureGateway != address(0)) { + _assertHasCode(report.gatewaysBatchReport.signatureGateway, 'signatureGateway'); + } + if (report.positionManagerBatchReport.giverPositionManager != address(0)) { + _assertHasCode( + report.positionManagerBatchReport.giverPositionManager, + 'giverPositionManager' + ); + } + if (report.positionManagerBatchReport.takerPositionManager != address(0)) { + _assertHasCode( + report.positionManagerBatchReport.takerPositionManager, + 'takerPositionManager' + ); + } + if (report.positionManagerBatchReport.configPositionManager != address(0)) { + _assertHasCode( + report.positionManagerBatchReport.configPositionManager, + 'configPositionManager' + ); + } + } + + function _assertHasCode(address addr, string memory label) internal view { + assertTrue(addr.code.length > 0, string.concat('no code at ', label, ': ', vm.toString(addr))); + } + + /// @dev Assert that actual bytecode matches artifact bytecode, ignoring immutable slots + function _assertBytecodeMatchExcludingImmutables( + bytes memory actual, + bytes memory artifact, + string memory label + ) internal pure { + assertEq(actual.length, artifact.length, string.concat(label, ': code size mismatch')); + + // Copy on-chain bytecode; zero out positions where artifact has zeros but on-chain doesn't. + // These are immutable slots (values are validated separately) + bytes memory masked = new bytes(actual.length); + for (uint256 i; i < actual.length; i++) { + masked[i] = (artifact[i] == 0x00) ? bytes1(0x00) : actual[i]; + } + assertEq(keccak256(masked), keccak256(artifact), string.concat(label, ': bytecode mismatch')); + } +} diff --git a/tests/utils/Create2TestHelper.sol b/tests/utils/Create2TestHelper.sol new file mode 100644 index 000000000..c3c3e2634 --- /dev/null +++ b/tests/utils/Create2TestHelper.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import {Test} from 'forge-std/Test.sol'; +import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; + +abstract contract Create2TestHelper is Test { + function _etchCreate2Factory() internal { + vm.etch( + Create2Utils.CREATE2_FACTORY, + hex'7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3' + ); + } +} 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..6f0f83f4e --- /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; + } + + struct TestSpokeReport { + address spoke; + address aaveOracle; + } + + struct TestGatewaysReport { + address signatureGateway; + address nativeGateway; + } + + struct TestConfiguratorReport { + address hubConfigurator; + address spokeConfigurator; + } + + struct TestEnvReport { + address accessManager; + address treasurySpoke; + TestHubReport[] hubReports; + TestSpokeReport[] spokeReports; + TestGatewaysReport gatewaysReport; + TestConfiguratorReport configuratorReport; + } + + struct TestTokensReport { + address weth; + address[] testTokens; + } +}