Skip to content

Commit fe5aa49

Browse files
committed
feat(scratch): add CSM_DEPLOYMENT_ENABLED and CMV2_DEPLOYMENT_ENABLED toggles
1 parent fba220d commit fe5aa49

6 files changed

Lines changed: 135 additions & 30 deletions

File tree

docs/scratch-deploy.md

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,33 @@ no-op, step `0150` renounces WQ/VEBO admin immediately, and step `0160` calls
161161
`LidoTemplate.finalizePermissionsWithoutDGDeployment()` (keeps Voting as
162162
permission manager) before setting the template owner to Agent.
163163

164+
The two external staking modules have one toggle each, same falsy spellings,
165+
both enabled by default: `CSM_DEPLOYMENT_ENABLED` and `CMV2_DEPLOYMENT_ENABLED`.
166+
With one off, step `0135` skips that module's deploy from
167+
`lidofinance/community-staking-module`, and step `0140` leaves it unplugged — the
168+
step keys off the module's state entry, so nothing else has to change. The two
169+
deploys are independent in that repo: separate `just` recipes, forge scripts and
170+
artifact files, and CMv2 deploys its own `Ejector`, `HashConsensus`, `Accounting`
171+
and `FeeOracle` rather than reusing CSM's. Note these are the deploy-side
172+
switches; `INTEGRATION_WITH_CSM=off` is the separate test-side one.
173+
174+
What does couple them is on this side: StakingRouter assigns module ids in plug
175+
order (NOR 1, SimpleDVT 2, then whichever of CSM / CMv2 was deployed), and
176+
`ConsolidationMigrator` takes `[consolidationMigrator].targetModuleId` as an
177+
immutable constructor argument back in step `0083`. So turning CSM off while CMv2
178+
stays on makes CMv2 module 3, not 4 — set the param (or
179+
`CONSOLIDATION_MIGRATOR_TARGET_MODULE_ID`) to match. The preflight checks that
180+
pairing up front, before the expensive external deploy; the assert at the end of
181+
`0140` is the late backstop. With CMv2 off, `ConsolidationMigrator` is still
182+
deployed and its target id simply stays unassigned (preflight warns).
183+
184+
Independently of the toggles, plugging the external modules needs Aragon Agent
185+
authority — their first admin is the Agent (`CSM_FIRST_ADMIN_ADDRESS`), so the
186+
`resume()` / `updateInitialEpoch` / `registerPauser` calls in `0140` are made
187+
through `impersonate(agent)`, i.e. `hardhat_impersonateAccount`. Unlike the DG
188+
step, that part does not work on a live network — there it would take an
189+
Agent-executed vote or DG proposal.
190+
164191
### Dual Governance configuration
165192

166193
The `[dualGovernance]` section of the deploy-params toml mirrors the structure
@@ -788,21 +815,21 @@ Two scope gaps to keep in mind:
788815

789816
### Files of interest
790817

791-
| File | Role |
792-
| ------------------------------------------------------------ | ------------------------------------------------------------- |
793-
| `scripts/dao-deploy.sh` | Entry point; wipes state file, compiles, runs `migrate.ts` |
794-
| `scripts/utils/migrate.ts` | Iterates `steps.json`, imports each step, calls `main()` |
795-
| `lib/scratch.ts` | `applyDeploySteps`, `deployScratchProtocol` (step runner) |
796-
| `lib/env-flags.ts` | `isDGDeploymentEnabled`, `isResumeEnabled`, `isTruthyEnv` |
797-
| `scripts/scratch/deploy-params-testnet.toml` | All deploy parameters (`[dualGovernance]` at the bottom) |
798-
| `scripts/scratch/steps/0145-unpause-sealables.ts` | DG prerequisite: resume WQ + VEBO pre-role-transfer |
799-
| `scripts/scratch/steps/0150-transfer-roles.ts` | Admin hand-off to Agent; defers WQ/VEBO renounce for DG |
800-
| `scripts/scratch/steps/0160-deploy-dual-governance.ts` | Forge bridge + ResealManager wiring + template finalize |
801-
| `contracts/0.4.24/template/LidoTemplate.sol` | `finalizePermissions{After,Without}DGDeployment`, `setOwner` |
802-
| `scripts/utils/upgrade.ts` | Shared `executeDGProposal` helper |
803-
| `lib/state-file.ts` | `Sk` enum, `getAddress`, `tryGetAddress`, state reset/persist |
804-
| `lib/config-schemas.ts` | Zod schema for `[dualGovernance]` |
805-
| `test/integration/dual-governance/dg-scratch.integration.ts` | Post-launch topology assertions + e2e proposal |
818+
| File | Role |
819+
| ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
820+
| `scripts/dao-deploy.sh` | Entry point; wipes state file, compiles, runs `migrate.ts` |
821+
| `scripts/utils/migrate.ts` | Iterates `steps.json`, imports each step, calls `main()` |
822+
| `lib/scratch.ts` | `applyDeploySteps`, `deployScratchProtocol` (step runner) |
823+
| `lib/env-flags.ts` | `isDGDeploymentEnabled`, `isCSMDeploymentEnabled`, `isCMv2DeploymentEnabled`, `isResumeEnabled` |
824+
| `scripts/scratch/deploy-params-testnet.toml` | All deploy parameters (`[dualGovernance]` at the bottom) |
825+
| `scripts/scratch/steps/0145-unpause-sealables.ts` | DG prerequisite: resume WQ + VEBO pre-role-transfer |
826+
| `scripts/scratch/steps/0150-transfer-roles.ts` | Admin hand-off to Agent; defers WQ/VEBO renounce for DG |
827+
| `scripts/scratch/steps/0160-deploy-dual-governance.ts` | Forge bridge + ResealManager wiring + template finalize |
828+
| `contracts/0.4.24/template/LidoTemplate.sol` | `finalizePermissions{After,Without}DGDeployment`, `setOwner` |
829+
| `scripts/utils/upgrade.ts` | Shared `executeDGProposal` helper |
830+
| `lib/state-file.ts` | `Sk` enum, `getAddress`, `tryGetAddress`, state reset/persist |
831+
| `lib/config-schemas.ts` | Zod schema for `[dualGovernance]` |
832+
| `test/integration/dual-governance/dg-scratch.integration.ts` | Post-launch topology assertions + e2e proposal |
806833

807834
## Protocol Parameters
808835

lib/env-flags.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,39 @@ export function isResumeEnabled(): boolean {
1616
return isTruthyEnv("RESUME");
1717
}
1818

19-
// `DG_DEPLOYMENT_ENABLED` is opt-out: default ON, disabled via any of the
20-
// common falsy strings ("false", "0", "off", "no" — case-insensitive). The
21-
// strict `=== "false"` check used previously rejected the rest, which
22-
// surprised users typing `DG_DEPLOYMENT_ENABLED=0`.
23-
export function isDGDeploymentEnabled(): boolean {
24-
const v = process.env.DG_DEPLOYMENT_ENABLED?.trim().toLowerCase();
19+
// Opt-out flag: default ON, disabled via any of the common falsy strings
20+
// ("false", "0", "off", "no" — case-insensitive). The strict `=== "false"` check
21+
// used previously rejected the rest, which surprised users typing `...=0`.
22+
export function isOptOutEnv(name: string): boolean {
23+
const v = process.env[name]?.trim().toLowerCase();
2524
return !v || !FALSY_VALUES.has(v);
2625
}
2726

27+
// `DG_DEPLOYMENT_ENABLED` is opt-out (default ON): when falsy, the scratch deploy
28+
// skips Dual Governance (step 0160 finalizes permissions without it).
29+
export function isDGDeploymentEnabled(): boolean {
30+
return isOptOutEnv("DG_DEPLOYMENT_ENABLED");
31+
}
32+
33+
// `CSM_DEPLOYMENT_ENABLED` / `CMV2_DEPLOYMENT_ENABLED` are opt-out (default ON): when
34+
// falsy, step 0135 skips that module's deploy from the external
35+
// lidofinance/community-staking-module repo, and step 0140 leaves it unplugged (it keys
36+
// off the module's state entry). The two are independent in that repo — separate `just`
37+
// recipes, forge scripts and deploy artifacts, and CMv2 deploys its own Ejector,
38+
// HashConsensus, Accounting and FeeOracle rather than reusing CSM's. What does couple
39+
// them is on this side: StakingRouter assigns module ids in plug order (NOR 1,
40+
// SimpleDVT 2, then whichever of CSM / CMv2 is deployed) and ConsolidationMigrator takes
41+
// `[consolidationMigrator].targetModuleId` as an immutable ctor arg in step 0083, so
42+
// turning CSM off while CMv2 stays on shifts CMv2's id. The scratch preflight checks
43+
// that pairing before the expensive external deploy runs.
44+
export function isCSMDeploymentEnabled(): boolean {
45+
return isOptOutEnv("CSM_DEPLOYMENT_ENABLED");
46+
}
47+
48+
export function isCMv2DeploymentEnabled(): boolean {
49+
return isOptOutEnv("CMV2_DEPLOYMENT_ENABLED");
50+
}
51+
2852
// `PROTOCOL_ACTIVATION_ENABLED` is opt-in (default OFF): when truthy, the scratch
2953
// deploy leaves the core protocol operationally unpaused — it calls `Lido.resume()`
3054
// and sets the staking rate limit (step 0155), and resumes WithdrawalQueue + VEBO

lib/scratch.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,13 @@ class MigrationSkipFunctionError extends Error {
3939

4040
const deployedSteps: string[] = [];
4141

42-
export { isDGDeploymentEnabled, isProtocolActivationEnabled, isResumeEnabled } from "./env-flags";
42+
export {
43+
isCMv2DeploymentEnabled,
44+
isCSMDeploymentEnabled,
45+
isDGDeploymentEnabled,
46+
isProtocolActivationEnabled,
47+
isResumeEnabled,
48+
} from "./env-flags";
4349

4450
function getCompletedStepsFromState(): string[] {
4551
if (!networkStateFileExists()) return [];

scripts/scratch/preflight.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import path from "node:path";
55
import { ethers } from "hardhat";
66
import { readScratchParameters, ScratchParameters, scratchParametersToDeploymentState } from "scripts/utils/scratch";
77

8-
import { isDGDeploymentEnabled, isResumeEnabled } from "lib/env-flags";
8+
import { isCMv2DeploymentEnabled, isCSMDeploymentEnabled, isDGDeploymentEnabled, isResumeEnabled } from "lib/env-flags";
99
import { log } from "lib/log";
1010
import { SEPOLIA_CHAIN_ID } from "lib/protocol/sepolia";
1111
import { networkStateFileExists, readNetworkState } from "lib/state-file";
@@ -168,6 +168,37 @@ export async function runScratchDeployPreflight(): Promise<void> {
168168
}
169169
}
170170

171+
// --- External staking modules: plug order vs ConsolidationMigrator ----------
172+
// StakingRouter assigns module ids in plug order (step 0140): NOR 1, SimpleDVT 2, then
173+
// CSM and CMv2 if their deploys ran. ConsolidationMigrator takes targetModuleId as an
174+
// immutable ctor arg back in step 0083, so a mismatch is only fixable by re-deploying —
175+
// catch it here rather than at the 0140 assert, which fires after the external repo
176+
// deploy and the addStakingModule txs.
177+
if (params) {
178+
const envTargetModuleId = process.env.CONSOLIDATION_MIGRATOR_TARGET_MODULE_ID?.trim();
179+
const targetModuleId = envTargetModuleId // step 0000 lets the env var override the TOML
180+
? parseInt(envTargetModuleId)
181+
: params.consolidationMigrator.targetModuleId;
182+
183+
if (!isCMv2DeploymentEnabled()) {
184+
log.warning(
185+
`CMV2_DEPLOYMENT_ENABLED is off: ConsolidationMigrator is still deployed (step 0083) with the ` +
186+
`immutable targetModuleId ${targetModuleId}, which will stay unassigned.`,
187+
);
188+
} else {
189+
const csmEnabled = isCSMDeploymentEnabled();
190+
const cmv2ModuleId = csmEnabled ? 4 : 3;
191+
if (targetModuleId !== cmv2ModuleId) {
192+
errors.push(
193+
`CMv2 will be plugged as staking module ${cmv2ModuleId} (NOR 1, SimpleDVT 2` +
194+
`${csmEnabled ? ", CSM 3" : "; CSM_DEPLOYMENT_ENABLED is off"}), but ConsolidationMigrator's ` +
195+
`targetModuleId is ${targetModuleId}. Set [consolidationMigrator].targetModuleId ` +
196+
`(or CONSOLIDATION_MIGRATOR_TARGET_MODULE_ID) to ${cmv2ModuleId}.`,
197+
);
198+
}
199+
}
200+
}
201+
171202
if (errors.length > 0) {
172203
throw new Error(`Scratch deploy preflight failed:\n - ${errors.join("\n - ")}`);
173204
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { ethers } from "hardhat";
22
import { deployStakingModules } from "scripts/utils/staking-modules";
33

4+
import { isCMv2DeploymentEnabled, isCSMDeploymentEnabled } from "lib/scratch";
45
import { readNetworkState } from "lib/state-file";
56

67
export async function main() {
78
const deployer = (await ethers.provider.getSigner()).address;
89
const state = readNetworkState({ deployer });
910

10-
await deployStakingModules(state);
11+
await deployStakingModules(state, { csm: isCSMDeploymentEnabled(), curated: isCMv2DeploymentEnabled() });
1112
}

scripts/utils/staking-modules.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -305,22 +305,38 @@ export function saveCuratedArtifact(state: DeploymentState, artifact: ExternalDe
305305
writeSubstateToToml(CM_TOML_SECTION, CM_TOML_MAP, contracts);
306306
}
307307

308+
// Which of the two external modules to deploy. Both default to true, so the upgrade flow
309+
// (scripts/upgrade/steps/0135) is unaffected; the scratch step passes the env toggles.
310+
export type RequestedStakingModules = { csm?: boolean; curated?: boolean };
311+
308312
/**
309313
* Clones the external community-staking-module repo and deploys the Community Staking Module (CSM)
310314
* and Curated Module v2 (CMv2), saving their addresses into the deployment state file.
311315
*
312316
* Shared between the scratch deploy step and the protocol upgrade step.
313317
*/
314-
export async function deployStakingModules(state: DeploymentState): Promise<void> {
318+
export async function deployStakingModules(
319+
state: DeploymentState,
320+
requested: RequestedStakingModules = {},
321+
): Promise<void> {
322+
const csmRequested = requested.csm ?? true;
323+
const curatedRequested = requested.curated ?? true;
324+
315325
// A module counts as deployed only once BOTH its proxy address and its deploy artifact are recorded.
316326
// During an upgrade the proxies are pre-written into the state file before the new implementations are
317327
// deployed, so the proxy address alone must not suppress the deploy.
318328
const csmDeployed = !!(state[Sk.sm_CSM]?.proxy?.address && state[Sk.sm_CSM]?.deployArtifact);
319329
const curatedDeployed = !!(state[Sk.sm_CM]?.proxy?.address && state[Sk.sm_CM]?.deployArtifact);
320330

321-
if (csmDeployed && curatedDeployed) {
322-
log(`Using the deployed CSM address: ${cy(state[Sk.sm_CSM].proxy.address)}`);
323-
log(`Using the deployed CMv2 address: ${cy(state[Sk.sm_CM].proxy.address)}`);
331+
if (csmDeployed) log(`Using the deployed CSM address: ${cy(state[Sk.sm_CSM].proxy.address)}`);
332+
if (curatedDeployed) log(`Using the deployed CMv2 address: ${cy(state[Sk.sm_CM].proxy.address)}`);
333+
if (!csmRequested && !csmDeployed) log("CSM deployment is disabled: skipping it.");
334+
if (!curatedRequested && !curatedDeployed) log("CMv2 deployment is disabled: skipping it.");
335+
336+
const deployCsm = csmRequested && !csmDeployed;
337+
const deployCurated = curatedRequested && !curatedDeployed;
338+
339+
if (!deployCsm && !deployCurated) {
324340
log.emptyLine();
325341
return;
326342
}
@@ -385,7 +401,7 @@ export async function deployStakingModules(state: DeploymentState): Promise<void
385401
EVM_SCRIPT_EXECUTOR_ADDRESS: state[Sk.appVoting].proxy.address,
386402
} as unknown as NodeJS.ProcessEnv;
387403

388-
if (!csmDeployed) {
404+
if (deployCsm) {
389405
log("Deploying Community Staking Module from external repo...");
390406
let artifactsFile: string;
391407
const cmdOptions: string[] = [];
@@ -406,7 +422,7 @@ export async function deployStakingModules(state: DeploymentState): Promise<void
406422
log.emptyLine();
407423
}
408424

409-
if (!curatedDeployed) {
425+
if (deployCurated) {
410426
log("Deploying Curated Module v2 from external repo...");
411427
/// @dev using deploy-curated for both scratch and upgrade, since Curated doesn't exist yet
412428
/// and there's nothing to update. Reserved for future use

0 commit comments

Comments
 (0)