diff --git a/.env.sample b/.env.sample index 814ab63..e9b3a85 100644 --- a/.env.sample +++ b/.env.sample @@ -26,7 +26,7 @@ BASE_RPC_URL= ADMIN_ADDRESS= # [REQUIRED] Operator address that can call executeDebtSwap() and other SafeDebtManager/LeveragedPosition functions -# This address is stored in ProtocolRegistry and can only be changed via TimelockController (2-day delay) +# This address is stored in ProtocolRegistry and can only be changed via TimelockController (8-hour delay by default) SAFE_OPERATOR_ADDRESS= # [REQUIRED] Pauser address that can pause/unpause SafeDebtManager and LeveragedPosition in emergencies @@ -42,7 +42,7 @@ PAUSER_ADDRESS= TIMELOCK_ADMIN= # [OPTIONAL] Minimum delay (seconds) before queued timelock ops can execute. -# Defaults to 172800 (2 days). Lower on testnets only. +# Defaults to 28800 (8 hours). TIMELOCK_DELAY= # ============================================================================= @@ -54,7 +54,7 @@ TIMELOCK_DELAY= RHP_TREASURY= # [OPTIONAL] ProtocolRegistry to wire RHP to. If unset, falls back to -# PROTOCOL_REGISTRY_ADDRESS in contractAddresses.ts (auto-synced by deploy:1_core). +# PROTOCOL_REGISTRY_ADDRESS in contractAddresses.ts (auto-synced by registry/core deploys). RHP_REGISTRY= # [OPTIONAL] DEFAULT_ADMIN_ROLE holder on RatehopperUniV3Positions. Falls back to ADMIN_ADDRESS. @@ -122,4 +122,4 @@ OPERATION_ID= # REPORT_GAS=true # [OPTIONAL] Override the chain id used by scripts/syncRegistryAddress.js. Default 8453 (Base). -# CHAIN_ID=8453 \ No newline at end of file +# CHAIN_ID=8453 diff --git a/README.md b/README.md index 51e4187..883b9ca 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ The system consists of several key components: 1. **Governance & Access Control**: - - `TimelockController`: OpenZeppelin's timelock implementation with 2-day delay for critical operations + - `TimelockController`: OpenZeppelin's timelock implementation with 8-hour delay by default for critical operations - `ProtocolRegistry.sol`: Central registry with hybrid access control: - `DEFAULT_ADMIN_ROLE`: For routine operations (whitelist, token mappings) - immediate execution - `CRITICAL_ROLE`: For critical operations (setParaswapV6, setOperator) - requires timelock @@ -193,7 +193,7 @@ RHP_MAX_FEE_BPS=2000 # Hard upper bound on BOTH fees (bps). Default 200 # Optional — TimelockController inside 2_DeployUniV3Helper TIMELOCK_ADMIN=0x... # Proposer + executor on the new timelock. Falls back to ADMIN_ADDRESS -TIMELOCK_DELAY=172800 # Min delay before queued ops execute (seconds). Default 172800 (2 days) +TIMELOCK_DELAY=28800 # Min delay before queued ops execute (seconds). Default 28800 (8 hours) ``` ## Setup and Development @@ -252,30 +252,54 @@ Hardhat Ignition is a declarative deployment framework. Instead of writing imper - **Tracks state** in `ignition/deployments/chain-/` so deployments can be resumed if interrupted - **Records constructor args** in `journal.jsonl` for reproducibility and verification - **Supports `--verify`** to automatically submit contracts to Etherscan after deployment -- **Supports `--reset`** to wipe previous state and redeploy from scratch +- **Supports per-future wipes** with `hardhat ignition wipe ` The core contracts are defined in a single module at `ignition/modules/1_DeployCore.ts`. Every step is chained sequentially via `after` dependencies to avoid nonce race conditions. +### Export ABIs + +ABI files for the four core integration contracts are exported to `abis/` from Hardhat artifacts: + +```bash +yarn abis +``` + +This compiles the contracts and writes: + +- `abis/LeveragedPosition.json` +- `abis/RatehopperUniV3Positions.json` +- `abis/SafeDebtManager.json` +- `abis/SafeExecTransactionWrapper.json` + +The deploy scripts below run the ABI exporter automatically after a successful deployment. + +### Deploy Registry Only + +Deploy only `ProtocolRegistry`: + +```bash +yarn deploy:0_registry +``` + +This deploys and configures `ProtocolRegistry`, syncs the registry address into `contractAddresses.ts`, and refreshes the ABI files in `abis/`. + ### Deploy All Contracts -Deploy everything in one command: +Deploy the core debt-management contracts: ```bash yarn deploy:1_core -# or equivalently: -npx hardhat ignition deploy ignition/modules/1_DeployCore.ts --network base --verify --reset ``` +This deploys `ignition/modules/1_DeployCore.ts` to Base with verification enabled, reusing the configured registry from `deploy:0_registry`, syncs the registry address into `contractAddresses.ts`, and refreshes the ABI files in `abis/`. + This deploys all contracts sequentially in a single transaction chain: -1. **TimelockController** (2-day delay) -2. **ProtocolRegistry** (with timelock, operator, paraswap configured) -3. **Handlers**: AaveV3 → Compound → Morpho → FluidSafe → Moonwell -4. **Registry config**: token mappings, Fluid resolver, whitelist -5. **Admin transfer**: grant `ADMIN_ADDRESS` admin role, revoke deployer -6. **SafeDebtManager** → `transferOwnership` to `ADMIN_ADDRESS` -7. **LeveragedPosition** → `transferOwnership` to `ADMIN_ADDRESS` -8. **SafeExecTransactionWrapper** +1. **Configured ProtocolRegistry** from `DeployRegistryOnly` +2. **Handlers**: AaveV3 → Compound → Morpho → FluidSafe → Moonwell +3. **SafeDebtManager** → `transferOwnership` to `ADMIN_ADDRESS` +4. **LeveragedPosition** → `transferOwnership` to `ADMIN_ADDRESS` +5. **SafeExecTransactionWrapper** ### Deploy RatehopperUniV3Positions (Standalone) @@ -283,13 +307,13 @@ This deploys all contracts sequentially in a single transaction chain: ```bash yarn deploy:2_univ3_helper -# or equivalently: -npx hardhat ignition deploy ignition/modules/2_DeployUniV3Helper.ts --network base --verify ``` +This deploys `ignition/modules/2_DeployUniV3Helper.ts` to Base with verification enabled and refreshes the ABI files in `abis/`. + This module by default deploys: -1. **TimelockController** (proposer + executor = `TIMELOCK_ADMIN` ?? `ADMIN_ADDRESS`; delay = `TIMELOCK_DELAY` ?? 2 days) +1. **TimelockController** (proposer + executor = `TIMELOCK_ADMIN` ?? `ADMIN_ADDRESS`; delay = `TIMELOCK_DELAY` ?? 8 hours) 2. **RatehopperUniV3Positions** (wired to the registry from `RHP_REGISTRY` ?? `PROTOCOL_REGISTRY_ADDRESS`; CRITICAL_ROLE granted to the timelock from step 1) To reuse an existing TimelockController instead of deploying a new one, set `RHP_TIMELOCK=0x...` — the module skips step 1 and points RHP at the supplied address. See `## Environment Variables` for the full list of optional knobs. @@ -311,6 +335,27 @@ Inspect deployed addresses: cat ignition/deployments/chain-8453/deployed_addresses.json ``` +### Wiping Deployments + +Use `wipe:all` when you want to clear all local Ignition state for Base and redeploy everything from scratch: + +```bash +yarn wipe:all +``` + +This removes `ignition/deployments/chain-8453`. It does not delete on-chain contracts; it only resets this repo's local Ignition deployment journal and generated deployment artifacts for Base. + +Shortcut scripts are available for common futures: + +```bash +yarn wipe:leveraged-position +yarn wipe:univ3-positions +yarn wipe:safe-debt-manager +yarn wipe:safe-wrapper +``` + +In short: use `wipe:all` for a clean redeploy of the whole Base deployment, and use `wipe` or a `wipe:*` shortcut only when you intentionally want to rerun one named future. + ### Contract Verification The `--verify` flag on `yarn deploy:1_core` may fail due to a known `hardhat-verify` v2.x bug with Etherscan's V2 API (the plugin's GET requests strip the `chainid` parameter). Use the standalone verification script instead: @@ -331,7 +376,7 @@ This script: ### Timelock Operations -Critical `ProtocolRegistry` setters (`setParaswapV6`, `setOperator`) carry `CRITICAL_ROLE` and revert unless `msg.sender` is the timelock, so they must be scheduled and executed through the `TimelockController` (2-day delay). Each script is a two-step flow: schedule, wait for the delay, then re-run with `EXECUTE=true` reusing the same `OPERATION_ID` printed during scheduling. +Critical `ProtocolRegistry` setters (`setParaswapV6`, `setOperator`) carry `CRITICAL_ROLE` and revert unless `msg.sender` is the timelock, so they must be scheduled and executed through the `TimelockController` (8-hour delay by default). Each script is a two-step flow: schedule, wait for the delay, then re-run with `EXECUTE=true` reusing the same `OPERATION_ID` printed during scheduling. #### Finding the TimelockController address @@ -365,7 +410,7 @@ Use the resulting address as `TIMELOCK_ADDRESS` in the commands below. TIMELOCK_ADDRESS=0x... PROTOCOL_REGISTRY_ADDRESS=0x... NEW_PARASWAP_ADDRESS=0x... \ yarn hardhat run scripts/timelockUpdateParaswap.ts --network base -# Wait 2 days, then execute +# Wait for the timelock delay, then execute EXECUTE=true OPERATION_ID="..." TIMELOCK_ADDRESS=0x... PROTOCOL_REGISTRY_ADDRESS=0x... NEW_PARASWAP_ADDRESS=0x... \ yarn hardhat run scripts/timelockUpdateParaswap.ts --network base ``` @@ -377,7 +422,7 @@ yarn hardhat run scripts/timelockUpdateParaswap.ts --network base TIMELOCK_ADDRESS=0x... PROTOCOL_REGISTRY_ADDRESS=0x... NEW_OPERATOR_ADDRESS=0x... \ yarn hardhat run scripts/timelockUpdateOperator.ts --network base -# Wait 2 days, then execute (reuse the OPERATION_ID printed during scheduling) +# Wait for the timelock delay, then execute (reuse the OPERATION_ID printed during scheduling) EXECUTE=true OPERATION_ID="..." TIMELOCK_ADDRESS=0x... PROTOCOL_REGISTRY_ADDRESS=0x... NEW_OPERATOR_ADDRESS=0x... \ yarn hardhat run scripts/timelockUpdateOperator.ts --network base ``` @@ -389,7 +434,7 @@ The contracts include several security features: ### Access Control & Governance -- **Timelock Controller**: 2-day delay for critical operations (Paraswap and operator updates) +- **Timelock Controller**: 8-hour delay by default for critical operations (Paraswap and operator updates) - **Hybrid Access Control**: - `DEFAULT_ADMIN_ROLE`: For routine operations (immediate execution) - `CRITICAL_ROLE`: For critical operations (requires timelock) diff --git a/contractAddresses.ts b/contractAddresses.ts index f68718a..7908f87 100644 --- a/contractAddresses.ts +++ b/contractAddresses.ts @@ -49,11 +49,11 @@ export const ADMIN_ADDRESS = "0xc74fc973A0740Ca1ED6f8F31Ed56003A13D4F5F1"; // (e.g. 2_DeployUniV3Helper reads PROTOCOL_REGISTRY_ADDRESS to wire RHP // to the existing registry). // -// AUTO-MANAGED: `yarn deploy:1_core` runs scripts/syncRegistryAddress.js as its -// final step, rewriting the address below to the freshly deployed +// AUTO-MANAGED: registry/core deploy scripts run scripts/syncRegistryAddress.js, +// rewriting the address below to the configured // ProtocolRegistry. Commit the resulting diff. Only edit by hand if pointing at // a registry deployed outside this repo's Ignition flow. -export const PROTOCOL_REGISTRY_ADDRESS = "0x20b2003CEF180DBbcca9fAf33Ddb1D635fdD2F6c"; +export const PROTOCOL_REGISTRY_ADDRESS = "0x2f1331Df43E2f63e01298f570F9e467375077d7d"; // Protocol enum export enum Protocol { diff --git a/ignition/modules/0_DeployRegistryOnly.ts b/ignition/modules/0_DeployRegistryOnly.ts index b9658b8..4469960 100644 --- a/ignition/modules/0_DeployRegistryOnly.ts +++ b/ignition/modules/0_DeployRegistryOnly.ts @@ -1,9 +1,32 @@ import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; import TimelockControllerModule from "./TimelockControllerModule"; import { + AERO_ADDRESS, + cbBTC_ADDRESS, + cbETH_ADDRESS, + DAI_ADDRESS, + eUSD_ADDRESS, + EURC_ADDRESS, + FLUID_VAULT_RESOLVER, + GHO_ADDRESS, + getCTokenMappingArrays, + getMTokenMappingArrays, + LBTC_ADDRESS, + MAI_ADDRESS, PARASWAP_V6_CONTRACT_ADDRESS, + rETH_ADDRESS, + sUSDS_ADDRESS, + tBTC_ADDRESS, UNISWAP_V3_FACTORY_ADDRESS, + USDbC_ADDRESS, + USDC_ADDRESS, + USDS_ADDRESS, + VIRTUAL_ADDRESS, + weETH_ADDRESS, + WELL_ADDRESS, WETH_ADDRESS, + wrsETH_ADDRESS, + wstETH_ADDRESS, } from "../../contractAddresses"; const ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/; @@ -18,19 +41,19 @@ function requireAddress(label: string, value: string): void { } /** - * Deploys only ProtocolRegistry. + * Deploys and configures ProtocolRegistry. * * If REGISTRY_TIMELOCK is set, that existing TimelockController address is used. * If unset, this module deploys/reuses the shared TimelockControllerModule. * * Environment variables: - * - REGISTRY_INITIAL_ADMIN: DEFAULT_ADMIN_ROLE holder. Falls back to ADMIN_ADDRESS. + * - REGISTRY_INITIAL_ADMIN: Final DEFAULT_ADMIN_ROLE holder. Falls back to ADMIN_ADDRESS. * - SAFE_OPERATOR_ADDRESS: Initial registry safeOperator. * - REGISTRY_TIMELOCK: Optional existing timelock to use for CRITICAL_ROLE. * - TIMELOCK_ADMIN: Used by TimelockControllerModule when REGISTRY_TIMELOCK is unset. * Falls back to ADMIN_ADDRESS. * - TIMELOCK_DELAY: Used by TimelockControllerModule when REGISTRY_TIMELOCK is unset. - * Defaults to 172800 seconds. + * Defaults to 28800 seconds. * - DEPLOYER_PRIVATE_KEY: Deployer key (set in hardhat.config.ts). * * Usage: @@ -38,18 +61,21 @@ function requireAddress(label: string, value: string): void { * --network base --verify */ export default buildModule("DeployRegistryOnly", (m) => { - const initialAdminAddr = process.env.REGISTRY_INITIAL_ADMIN ?? process.env.ADMIN_ADDRESS ?? ""; + const finalAdminAddr = process.env.REGISTRY_INITIAL_ADMIN ?? process.env.ADMIN_ADDRESS ?? ""; const operatorAddr = process.env.SAFE_OPERATOR_ADDRESS ?? ""; const reuseTimelockAddr = process.env.REGISTRY_TIMELOCK ?? ""; - requireAddress("initialAdmin (REGISTRY_INITIAL_ADMIN / ADMIN_ADDRESS)", initialAdminAddr); + requireAddress("finalAdmin (REGISTRY_INITIAL_ADMIN / ADMIN_ADDRESS)", finalAdminAddr); requireAddress("operator (SAFE_OPERATOR_ADDRESS)", operatorAddr); if (reuseTimelockAddr) requireAddress("timelock (REGISTRY_TIMELOCK)", reuseTimelockAddr); + const deployer = m.getAccount(0); + const DEFAULT_ADMIN_ROLE = "0x0000000000000000000000000000000000000000000000000000000000000000"; + const timelock = reuseTimelockAddr ? undefined : m.useModule(TimelockControllerModule).timelock; const timelockArg: any = timelock ?? reuseTimelockAddr; - const initialAdmin = m.getParameter("initialAdmin", initialAdminAddr); + const finalAdmin = m.getParameter("finalAdmin", finalAdminAddr); const operator = m.getParameter("operator", operatorAddr); const registry = m.contract( @@ -57,7 +83,7 @@ export default buildModule("DeployRegistryOnly", (m) => { [ WETH_ADDRESS, UNISWAP_V3_FACTORY_ADDRESS, - initialAdmin, + deployer, timelockArg, operator, PARASWAP_V6_CONTRACT_ADDRESS, @@ -65,5 +91,65 @@ export default buildModule("DeployRegistryOnly", (m) => { timelock ? { after: [timelock] } : undefined, ); - return { registry, ...(timelock ? { timelock } : {}) }; + const [mTokens, mContracts] = getMTokenMappingArrays(); + const setMoonwellMappings = m.call(registry, "batchSetTokenMContracts", [mTokens, mContracts], { + id: "registry_setMoonwellMappings", + }); + + const [cTokens, cContracts] = getCTokenMappingArrays(); + const setCompoundMappings = m.call(registry, "batchSetTokenCContracts", [cTokens, cContracts], { + id: "registry_setCompoundMappings", + after: [setMoonwellMappings], + }); + + const setFluidResolver = m.call(registry, "setFluidVaultResolver", [FLUID_VAULT_RESOLVER], { + id: "registry_setFluidVaultResolver", + after: [setCompoundMappings], + }); + + const whitelistTokens = [ + USDC_ADDRESS, + cbETH_ADDRESS, + WETH_ADDRESS, + USDbC_ADDRESS, + cbBTC_ADDRESS, + eUSD_ADDRESS, + MAI_ADDRESS, + DAI_ADDRESS, + sUSDS_ADDRESS, + AERO_ADDRESS, + wstETH_ADDRESS, + rETH_ADDRESS, + weETH_ADDRESS, + EURC_ADDRESS, + GHO_ADDRESS, + wrsETH_ADDRESS, + WELL_ADDRESS, + USDS_ADDRESS, + tBTC_ADDRESS, + LBTC_ADDRESS, + VIRTUAL_ADDRESS, + ]; + const addToWhitelist = m.call(registry, "addToWhitelistBatch", [whitelistTokens], { + id: "registry_addToWhitelistBatch", + after: [setFluidResolver], + }); + + const grantAdminRole = m.call(registry, "grantRole", [DEFAULT_ADMIN_ROLE, finalAdmin], { + id: "registry_grantAdminToFinalAdmin", + after: [addToWhitelist], + }); + + const revokeDeployerRole = m.call(registry, "revokeRole", [DEFAULT_ADMIN_ROLE, deployer], { + id: "registry_revokeAdminFromDeployer", + after: [grantAdminRole], + }); + + // `registryConfigured` is the final config call (revoke), exposed so 1_DeployCore + // can gate handler deployment on registry configuration completing. Ignition's + // result type only models contract deployment futures, so cast the call future to + // satisfy the type while keeping the real future for `after` dependencies. + const registryConfigured = revokeDeployerRole as unknown as ReturnType; + + return { registry, registryConfigured, ...(timelock ? { timelock } : {}) }; }); diff --git a/ignition/modules/1_DeployCore.ts b/ignition/modules/1_DeployCore.ts index a0735a8..4c1727a 100644 --- a/ignition/modules/1_DeployCore.ts +++ b/ignition/modules/1_DeployCore.ts @@ -1,37 +1,12 @@ import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; -import TimelockControllerModule from "./TimelockControllerModule"; +import DeployRegistryOnlyModule from "./0_DeployRegistryOnly"; import { - getCTokenMappingArrays, - getMTokenMappingArrays, UNISWAP_V3_FACTORY_ADDRESS, - FLUID_VAULT_RESOLVER, AAVE_V3_POOL_ADDRESS, AAVE_V3_DATA_PROVIDER_ADDRESS, MORPHO_ADDRESS, COMPTROLLER_ADDRESS, - PARASWAP_V6_CONTRACT_ADDRESS, Protocol, - WETH_ADDRESS, - USDC_ADDRESS, - USDbC_ADDRESS, - cbETH_ADDRESS, - cbBTC_ADDRESS, - eUSD_ADDRESS, - MAI_ADDRESS, - DAI_ADDRESS, - sUSDS_ADDRESS, - AERO_ADDRESS, - wstETH_ADDRESS, - rETH_ADDRESS, - weETH_ADDRESS, - EURC_ADDRESS, - GHO_ADDRESS, - wrsETH_ADDRESS, - WELL_ADDRESS, - USDS_ADDRESS, - tBTC_ADDRESS, - LBTC_ADDRESS, - VIRTUAL_ADDRESS, } from "../../contractAddresses"; /** @@ -40,14 +15,11 @@ import { * All steps are chained sequentially via `after` to avoid nonce race conditions. * * Deployment order: - * 1. TimelockController (2-day delay) - * 2. ProtocolRegistry (with timelock, operator, paraswap set in constructor) - * 3. Handlers: AaveV3 → Compound → Morpho → FluidSafe → Moonwell - * 4. Registry config: Moonwell mappings → Compound mappings → Fluid resolver → Whitelist - * 5. Registry admin transfer: grant ADMIN_ADDRESS → revoke deployer - * 6. SafeDebtManager → transferOwnership to ADMIN_ADDRESS - * 7. LeveragedPosition → transferOwnership to ADMIN_ADDRESS - * 8. SafeExecTransactionWrapper + * 1. Deploy/reuse configured ProtocolRegistry from DeployRegistryOnly + * 2. Handlers: AaveV3 → Compound → Morpho → FluidSafe → Moonwell + * 3. SafeDebtManager → transferOwnership to ADMIN_ADDRESS + * 4. LeveragedPosition → transferOwnership to ADMIN_ADDRESS + * 5. SafeExecTransactionWrapper * * Environment Variables Required: * - ADMIN_ADDRESS: Admin / timelock proposer+executor / final owner @@ -56,7 +28,7 @@ import { * - DEPLOYER_PRIVATE_KEY: Deployer private key (in hardhat.config.ts) * * Usage: - * npx hardhat ignition deploy ignition/modules/1_DeployCore.ts --network base --verify --reset + * npx hardhat ignition deploy ignition/modules/1_DeployCore.ts --network base --verify */ export default buildModule("DeployCore", (m) => { // ── Validate env vars ────────────────────────────────────────────── @@ -64,38 +36,18 @@ export default buildModule("DeployCore", (m) => { if (!adminAddress) { throw new Error("Please set ADMIN_ADDRESS environment variable"); } - const operatorAddress = process.env.SAFE_OPERATOR_ADDRESS; - if (!operatorAddress) { - throw new Error("Please set SAFE_OPERATOR_ADDRESS environment variable"); - } const pauserAddress = process.env.PAUSER_ADDRESS; if (!pauserAddress) { throw new Error("Please set PAUSER_ADDRESS environment variable"); } - const deployer = m.getAccount(0); - const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; - const DEFAULT_ADMIN_ROLE = "0x0000000000000000000000000000000000000000000000000000000000000000"; - - // ── 1. TimelockController (shared sub-module — Ignition deduplicates - // the future across runs, so `yarn deploy:2_univ3_helper` later reuses this - // same address). Params come from TIMELOCK_ADMIN / TIMELOCK_DELAY - // env vars in TimelockControllerModule, both with fallbacks to - // ADMIN_ADDRESS / 2 days. - const { timelock } = m.useModule(TimelockControllerModule); + const { registry, registryConfigured } = m.useModule(DeployRegistryOnlyModule); - // ── 2. ProtocolRegistry ──────────────────────────────────────────── - const registry = m.contract( - "ProtocolRegistry", - [WETH_ADDRESS, UNISWAP_V3_FACTORY_ADDRESS, deployer, timelock, operatorAddress, PARASWAP_V6_CONTRACT_ADDRESS], - { after: [timelock] }, - ); - - // ── 3. Handlers (sequential) ────────────────────────────────────── + // ── 1. Handlers (sequential) ────────────────────────────────────── const aaveV3Handler = m.contract( "AaveV3Handler", [AAVE_V3_POOL_ADDRESS, AAVE_V3_DATA_PROVIDER_ADDRESS, UNISWAP_V3_FACTORY_ADDRESS, registry], - { after: [registry] }, + { after: [registryConfigured] }, ); const compoundHandler = m.contract("CompoundHandler", [registry, UNISWAP_V3_FACTORY_ADDRESS], { @@ -114,76 +66,19 @@ export default buildModule("DeployCore", (m) => { after: [fluidSafeHandler], }); - // ── 4. Registry configuration ───────────────────────────────────── - const [mTokens, mContracts] = getMTokenMappingArrays(); - const setMoonwellMappings = m.call(registry, "batchSetTokenMContracts", [mTokens, mContracts], { - id: "registry_setMoonwellMappings", - after: [moonwellHandler], - }); - - const [cTokens, cContracts] = getCTokenMappingArrays(); - const setCompoundMappings = m.call(registry, "batchSetTokenCContracts", [cTokens, cContracts], { - id: "registry_setCompoundMappings", - after: [setMoonwellMappings], - }); - - const setFluidResolver = m.call(registry, "setFluidVaultResolver", [FLUID_VAULT_RESOLVER], { - id: "registry_setFluidVaultResolver", - after: [setCompoundMappings], - }); - - const whitelistTokens = [ - USDC_ADDRESS, - cbETH_ADDRESS, - WETH_ADDRESS, - USDbC_ADDRESS, - cbBTC_ADDRESS, - eUSD_ADDRESS, - MAI_ADDRESS, - DAI_ADDRESS, - sUSDS_ADDRESS, - AERO_ADDRESS, - wstETH_ADDRESS, - rETH_ADDRESS, - weETH_ADDRESS, - EURC_ADDRESS, - GHO_ADDRESS, - wrsETH_ADDRESS, - WELL_ADDRESS, - USDS_ADDRESS, - tBTC_ADDRESS, - LBTC_ADDRESS, - VIRTUAL_ADDRESS, - ]; - const addToWhitelist = m.call(registry, "addToWhitelistBatch", [whitelistTokens], { - id: "registry_addToWhitelistBatch", - after: [setFluidResolver], - }); - - // ── 5. Transfer registry admin role ──────────────────────────────── - const grantAdminRole = m.call(registry, "grantRole", [DEFAULT_ADMIN_ROLE, adminAddress], { - id: "registry_grantAdminToFinalAdmin", - after: [addToWhitelist], - }); - - const revokeDeployerRole = m.call(registry, "revokeRole", [DEFAULT_ADMIN_ROLE, deployer], { - id: "registry_revokeAdminFromDeployer", - after: [grantAdminRole], - }); - - // ── 6. SafeDebtManager ───────────────────────────────────────────── + // ── 2. SafeDebtManager ───────────────────────────────────────────── const protocols = [Protocol.AAVE_V3, Protocol.COMPOUND, Protocol.MORPHO, Protocol.FLUID, Protocol.MOONWELL]; const handlers = [aaveV3Handler, compoundHandler, morphoHandler, fluidSafeHandler, moonwellHandler]; const safeDebtManager = m.contract("SafeDebtManager", [registry, protocols, handlers, pauserAddress], { - after: [revokeDeployerRole], + after: [moonwellHandler], }); const safeDebtManagerTransfer = m.call(safeDebtManager, "transferOwnership", [adminAddress], { id: "safeDebtManager_transferOwnership", }); - // ── 7. LeveragedPosition ─────────────────────────────────────────── + // ── 3. LeveragedPosition ─────────────────────────────────────────── const leveragedPosition = m.contract("LeveragedPosition", [registry, protocols, handlers, pauserAddress], { after: [safeDebtManagerTransfer], }); @@ -192,13 +87,12 @@ export default buildModule("DeployCore", (m) => { id: "leveragedPosition_transferOwnership", }); - // ── 8. SafeExecTransactionWrapper ────────────────────────────────── + // ── 4. SafeExecTransactionWrapper ────────────────────────────────── const safeExecTransactionWrapper = m.contract("SafeExecTransactionWrapper", [], { after: [leveragedPositionTransfer], }); return { - timelock, registry, aaveV3Handler, compoundHandler, diff --git a/ignition/modules/2_DeployUniV3Helper.ts b/ignition/modules/2_DeployUniV3Helper.ts index 594c1bd..3d1b990 100644 --- a/ignition/modules/2_DeployUniV3Helper.ts +++ b/ignition/modules/2_DeployUniV3Helper.ts @@ -49,7 +49,7 @@ function requireAddress(label: string, value: string): void { * Falls back to ADMIN_ADDRESS. Required when * RHP_TIMELOCK is unset. * - TIMELOCK_DELAY: (consumed by TimelockControllerModule) Minimum - * delay in seconds. Defaults to 172800 (2 days). + * delay in seconds. Defaults to 28800 (8 hours). * - RHP_PERFORMANCE_FEE_BPS: Performance fee on net profit at closeLp in * bps. Defaults to 1000 (10%). * - RHP_FEE_COLLECT_BPS: Fee on harvested LP fees in bps. Defaults to diff --git a/ignition/modules/TimelockControllerModule.ts b/ignition/modules/TimelockControllerModule.ts index 09dd60b..16d3755 100644 --- a/ignition/modules/TimelockControllerModule.ts +++ b/ignition/modules/TimelockControllerModule.ts @@ -21,10 +21,10 @@ import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; * - TIMELOCK_ADMIN: EOA / multisig granted BOTH proposer and executor * roles. Falls back to ADMIN_ADDRESS. REQUIRED. * - TIMELOCK_DELAY: Minimum delay (seconds) before queued ops can execute. - * Defaults to 172800 (2 days). Lower on testnets only. + * Defaults to 28800 (8 hours). */ const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; -const TWO_DAYS = 2 * 24 * 60 * 60; +const EIGHT_HOURS = 8 * 60 * 60; export default buildModule("TimelockControllerModule", (m) => { const admin = m.getParameter( @@ -33,7 +33,7 @@ export default buildModule("TimelockControllerModule", (m) => { ); const delay = m.getParameter( "delay", - Number(process.env.TIMELOCK_DELAY ?? TWO_DAYS), + Number(process.env.TIMELOCK_DELAY ?? EIGHT_HOURS), ); const timelock = m.contract("TimelockController", [ diff --git a/package.json b/package.json index 0847d6b..8a323d2 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,17 @@ "format": "npx prettier --write 'contracts/**/*.sol'", "format:check": "npx prettier --check 'contracts/**/*.sol'", "clean": "npx hardhat clean", - "deploy:0_registry": "npx hardhat ignition deploy ignition/modules/0_DeployRegistryOnly.ts --network base --verify", - "deploy:1_core": "rm -rf ignition/deployments/chain-8453 && npx hardhat ignition deploy ignition/modules/1_DeployCore.ts --network base --verify --reset && node scripts/syncRegistryAddress.js", - "deploy:2_univ3_helper": "npx hardhat ignition deploy ignition/modules/2_DeployUniV3Helper.ts --network base --verify", + "abis:export": "node scripts/exportAbis.js", + "abis": "npx hardhat compile && node scripts/exportAbis.js", + "deploy:0_registry": "npx hardhat ignition deploy ignition/modules/0_DeployRegistryOnly.ts --network base --verify && node scripts/syncRegistryAddress.js && node scripts/exportAbis.js", + "deploy:1_core": "npx hardhat ignition deploy ignition/modules/1_DeployCore.ts --network base --verify && node scripts/syncRegistryAddress.js && node scripts/exportAbis.js", + "deploy:2_univ3_helper": "npx hardhat ignition deploy ignition/modules/2_DeployUniV3Helper.ts --network base --verify && node scripts/exportAbis.js", "verify": "npx hardhat run scripts/verifyAll.ts --network base", - "wipe": "npx hardhat ignition wipe chain-8453" + "wipe:all": "rm -rf ignition/deployments/chain-8453", + "wipe:leveraged-position": "npx hardhat ignition wipe chain-8453 DeployCore#LeveragedPosition", + "wipe:univ3-positions": "npx hardhat ignition wipe chain-8453 DeployUniV3Helper#RatehopperUniV3Positions", + "wipe:safe-debt-manager": "npx hardhat ignition wipe chain-8453 DeployCore#SafeDebtManager", + "wipe:safe-wrapper": "npx hardhat ignition wipe chain-8453 DeployCore#SafeExecTransactionWrapper" }, "author": "Ratehopper AI", "license": "BUSL-1.1", diff --git a/scripts/exportAbis.js b/scripts/exportAbis.js new file mode 100644 index 0000000..bc4b2c8 --- /dev/null +++ b/scripts/exportAbis.js @@ -0,0 +1,29 @@ +const fs = require("fs"); +const path = require("path"); + +const contracts = [ + "LeveragedPosition", + "RatehopperUniV3Positions", + "SafeDebtManager", + "SafeExecTransactionWrapper", +]; + +const rootDir = path.join(__dirname, ".."); +const abisDir = path.join(rootDir, "abis"); + +fs.mkdirSync(abisDir, { recursive: true }); + +for (const contractName of contracts) { + const artifactPath = path.join( + rootDir, + "artifacts", + "contracts", + `${contractName}.sol`, + `${contractName}.json`, + ); + const artifact = JSON.parse(fs.readFileSync(artifactPath, "utf8")); + const abiPath = path.join(abisDir, `${contractName}.json`); + + fs.writeFileSync(abiPath, `${JSON.stringify(artifact.abi, null, 4)}\n`); + console.log(`Exported ${path.relative(rootDir, abiPath)}`); +} diff --git a/scripts/syncRegistryAddress.js b/scripts/syncRegistryAddress.js index 3268c67..831a7c2 100644 --- a/scripts/syncRegistryAddress.js +++ b/scripts/syncRegistryAddress.js @@ -1,20 +1,20 @@ // Post-deploy sync: rewrites PROTOCOL_REGISTRY_ADDRESS in contractAddresses.ts -// to the ProtocolRegistry address just deployed by 1_DeployCore.ts. +// to the ProtocolRegistry address deployed by 0_DeployRegistryOnly.ts. // -// Runs automatically as the last step of `yarn deploy:1_core` so the +// Runs automatically after registry/core deploys so the // `yarn deploy:2_univ3_helper` fallback (which reads PROTOCOL_REGISTRY_ADDRESS) -// always points at the freshly deployed registry — a dev can't forget to bump +// always points at the configured registry — a dev can't forget to bump // it manually. // // Reads ignition/deployments/chain-/deployed_addresses.json (Ignition // keys futures as `#`, so the registry is -// `DeployCore#ProtocolRegistry`). +// `DeployRegistryOnly#ProtocolRegistry`). const fs = require("fs"); const path = require("path"); const CHAIN_ID = process.env.CHAIN_ID || "8453"; -const REGISTRY_KEY = "DeployCore#ProtocolRegistry"; +const REGISTRY_KEY = "DeployRegistryOnly#ProtocolRegistry"; const ADDRESSES_FILE = path.join( __dirname, diff --git a/scripts/timelockUpdateOperator.ts b/scripts/timelockUpdateOperator.ts index 2bd7247..37e389c 100644 --- a/scripts/timelockUpdateOperator.ts +++ b/scripts/timelockUpdateOperator.ts @@ -6,7 +6,7 @@ import { ethers } from "hardhat"; * `setOperator` is a CRITICAL_ROLE function that can ONLY be called by the timelock, * so it must go through the two-step process: * 1. Schedule the operation (requires PROPOSER_ROLE) - * 2. Wait for the timelock delay (2 days) + * 2. Wait for the timelock delay (8 hours by default) * 3. Execute the operation (requires EXECUTOR_ROLE) * * Usage: @@ -14,7 +14,7 @@ import { ethers } from "hardhat"; * TIMELOCK_ADDRESS=0x... PROTOCOL_REGISTRY_ADDRESS=0x... NEW_OPERATOR_ADDRESS=0x... \ * npx hardhat run scripts/timelockUpdateOperator.ts --network base * - * STEP 2 - Execute (run after 2 days, reuse the SAME OPERATION_ID printed in step 1): + * STEP 2 - Execute (run after the timelock delay, reuse the SAME OPERATION_ID printed in step 1): * EXECUTE=true OPERATION_ID="..." TIMELOCK_ADDRESS=0x... PROTOCOL_REGISTRY_ADDRESS=0x... NEW_OPERATOR_ADDRESS=0x... \ * npx hardhat run scripts/timelockUpdateOperator.ts --network base */ diff --git a/scripts/timelockUpdateParaswap.ts b/scripts/timelockUpdateParaswap.ts index f17638f..5c2aa59 100644 --- a/scripts/timelockUpdateParaswap.ts +++ b/scripts/timelockUpdateParaswap.ts @@ -6,7 +6,7 @@ import { ethers } from "hardhat"; * * This demonstrates the two-step process: * 1. Schedule the operation (requires PROPOSER_ROLE) - * 2. Wait 2 days + * 2. Wait for the timelock delay (8 hours by default) * 3. Execute the operation (requires EXECUTOR_ROLE) * * Usage: @@ -14,7 +14,7 @@ import { ethers } from "hardhat"; * TIMELOCK_ADDRESS=0x... PROTOCOL_REGISTRY_ADDRESS=0x... NEW_PARASWAP_ADDRESS=0x... \ * npx hardhat run scripts/timelock-update-paraswap.ts --network base * - * STEP 2 - Execute (run after 2 days): + * STEP 2 - Execute (run after the timelock delay): * EXECUTE=true TIMELOCK_ADDRESS=0x... PROTOCOL_REGISTRY_ADDRESS=0x... NEW_PARASWAP_ADDRESS=0x... \ * npx hardhat run scripts/timelock-update-paraswap.ts --network base */