Skip to content

Commit 466b5f6

Browse files
committed
feat: add Dual Governance to scratch deploy
1 parent 19dc7cd commit 466b5f6

File tree

7 files changed

+361
-1
lines changed

7 files changed

+361
-1
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,8 @@ GENESIS_TIME=1639659600
5959
GAS_PRIORITY_FEE=1
6060
GAS_MAX_FEE=100
6161

62+
# Dual Governance deployment
63+
DG_DEPLOYER_ACCOUNT_NAME=
64+
6265
# Etherscan API key for verifying contracts
6366
ETHERSCAN_API_KEY=

hardhat.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const config: HardhatUserConfig = {
4848
},
4949
"local": {
5050
url: process.env.LOCAL_RPC_URL || RPC_URL,
51+
timeout: 120000,
5152
},
5253
"local-devnet": {
5354
url: process.env.LOCAL_RPC_URL || RPC_URL,

lib/dg-installation.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import child_process from "node:child_process";
2+
import fs from "node:fs/promises";
3+
import util from "node:util";
4+
5+
async function main() {
6+
const dgDir = `${process.cwd()}/node_modules/@lido/dual-governance`;
7+
const gitmodulesPath = `${dgDir}/.gitmodules`;
8+
9+
const gitmodulesExists = await checkFileExists(gitmodulesPath);
10+
if (!gitmodulesExists) {
11+
console.log(`.gitmodules file not found at ${gitmodulesPath}`);
12+
return;
13+
}
14+
15+
console.log(`.gitmodules file found at ${gitmodulesPath}`);
16+
17+
const gitmodulesFile = (await fs.readFile(gitmodulesPath)).toString().replaceAll(/\t/g, "");
18+
19+
const submodules = parseGitmodules(gitmodulesFile);
20+
console.log(submodules);
21+
22+
await fs.rm(`${dgDir}/lib`, { force: true, recursive: true });
23+
24+
await cloneSubmodules(submodules);
25+
26+
console.log("Building DualGovernance contracts");
27+
await runForgeBuild(dgDir);
28+
29+
console.log("Running unit tests");
30+
await runUnitTests(dgDir);
31+
}
32+
33+
type GitSubmodule = {
34+
path: string;
35+
url: string;
36+
branch?: string;
37+
};
38+
39+
/**
40+
* @param {String} gitmodulesFile .gitmodules file content
41+
*/
42+
function parseGitmodules(gitmodulesFile: string) {
43+
const submoduleSectionRe = /\[submodule(\s+)('|")(.+)('|")\]([^\[]+)/gm;
44+
const submodulePropertyRe = /(path)(.+)|(url)(.+)|(branch)(.+)/g;
45+
const submodulesList = [...gitmodulesFile.matchAll(submoduleSectionRe)];
46+
const result: Record<string, GitSubmodule> = {};
47+
48+
if (!submodulesList.length) {
49+
return result;
50+
}
51+
52+
submodulesList.forEach((submoduleText) => {
53+
const item: GitSubmodule = {
54+
path: "",
55+
url: ""
56+
};
57+
const submodulePropertiesList = [...(submoduleText[5] || "").matchAll(submodulePropertyRe)];
58+
submodulePropertiesList.forEach((conf) => {
59+
const [key = "", value = ""] = (conf[0] ?? "").split("=");
60+
const pureKey = key.trim() as "path" | "url" | "branch";
61+
if (pureKey) {
62+
item[pureKey] = value.trim();
63+
}
64+
});
65+
result[item.path] = item;
66+
})
67+
return result;
68+
}
69+
70+
async function cloneSubmodules(submodules: Record<string, GitSubmodule>) {
71+
for (const key of Object.keys(submodules)) {
72+
let branch = submodules[key].branch || "";
73+
if (branch.length && branch.indexOf("tag") != -1) {
74+
branch = branch.replace("tags/", "");
75+
}
76+
await runCommand(`git clone ${branch.length ? `--branch ${branch}` : ""} --single-branch ${submodules[key].url} ${process.cwd()}/node_modules/@lido/dual-governance/${submodules[key].path}`, process.cwd());
77+
}
78+
}
79+
80+
async function runForgeBuild(workingDirectory: string) {
81+
await runCommand("forge build", workingDirectory);
82+
}
83+
84+
async function runUnitTests(workingDirectory: string) {
85+
await runCommand("npm run test:unit", workingDirectory);
86+
}
87+
88+
async function runCommand(command: string, workingDirectory: string) {
89+
const exec = util.promisify(child_process.exec);
90+
91+
try {
92+
const { stdout } = await exec(command, { cwd: workingDirectory });
93+
console.log("stdout:", stdout);
94+
} catch (error) {
95+
console.error(`Error running command ${command}`, `${error}`);
96+
throw error;
97+
}
98+
}
99+
100+
async function checkFileExists(path: string) {
101+
return fs.access(path)
102+
.then(() => true)
103+
.catch(() => false);
104+
}
105+
106+
main()
107+
.then(() => process.exit(0))
108+
.catch((error) => {
109+
console.error(error);
110+
process.exit(1);
111+
});

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"test:integration:fork:mainnet:custom": "MODE=forking hardhat test --network mainnet-fork",
3434
"typecheck": "tsc --noEmit",
3535
"prepare": "husky",
36+
"postinstall": "yarn ts-node lib/dg-installation.ts",
3637
"abis:extract": "hardhat abis:extract",
3738
"verify:deployed": "hardhat verify:deployed"
3839
},
@@ -106,6 +107,7 @@
106107
"@aragon/id": "2.1.1",
107108
"@aragon/minime": "1.0.0",
108109
"@aragon/os": "4.4.0",
110+
"@lido/dual-governance": "git+ssh://[email protected]/lidofinance/dual-governance.git#feature/scratch-deploy-support",
109111
"@openzeppelin/contracts": "3.4.0",
110112
"@openzeppelin/contracts-v4.4": "npm:@openzeppelin/[email protected]",
111113
"openzeppelin-solidity": "2.0.0"

scripts/scratch/steps.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"scratch/steps/0120-initialize-non-aragon-contracts",
1717
"scratch/steps/0130-grant-roles",
1818
"scratch/steps/0140-plug-staking-modules",
19-
"scratch/steps/0150-transfer-roles"
19+
"scratch/steps/0150-transfer-roles",
20+
"scratch/steps/0160-deploy-dual-governance"
2021
]
2122
}
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import child_process from "node:child_process";
2+
import fs from "node:fs/promises";
3+
import util from "node:util";
4+
5+
import { ethers } from "hardhat";
6+
7+
import { log } from "lib";
8+
import { readNetworkState, Sk, getAddress, DeploymentState } from "lib/state-file";
9+
10+
export async function main() {
11+
// TODO: consider making DG installation optional, for example with env var
12+
log.header("Deploy DG");
13+
log.emptyLine();
14+
15+
const deployerAccountName = process.env.DG_DEPLOYER_ACCOUNT_NAME || "";
16+
if (!deployerAccountName.length) {
17+
log.error(`You need to set the env variable DG_DEPLOYER_ACCOUNT_NAME to run DG deployment.
18+
To do so, please create a new cast wallet account (see https://getfoundry.sh/cast/reference/wallet/) with the current deployer private key:
19+
> cast wallet import <DEPLOYER ACCOUNT NAME>
20+
21+
Then set DG_DEPLOYER_ACCOUNT_NAME=<DEPLOYER ACCOUNT NAME> in the .env file.
22+
`);
23+
throw new Error("Env variable DG_DEPLOYER_ACCOUNT_NAME is not set.");
24+
}
25+
26+
log.warning(`To run the deployment with the local Hardhat node you need to increase allowed memory usage to 16Gb.
27+
> yarn hardhat node --fork <YOUR RPC URL> --port 8555 --max-memory 16384
28+
29+
AND
30+
31+
> export NODE_OPTIONS=--max_old_space_size=16384
32+
`);
33+
34+
const deployer = (await ethers.provider.getSigner()).address;
35+
const state = readNetworkState({ deployer });
36+
37+
const network = await ethers.getDefaultProvider(process.env.LOCAL_RPC_URL).getNetwork();
38+
const chainId = `${network.chainId}`;
39+
40+
const config = getDGConfig(chainId, state);
41+
42+
const timestamp = `${Date.now()}`;
43+
const dgDeployConfigFilename = `deploy-config-scratch-${timestamp}.json`;
44+
await writeDGConfigFile(JSON.stringify(config, null, 2), dgDeployConfigFilename);
45+
46+
await runCommand(`DEPLOY_CONFIG_FILE_NAME="${dgDeployConfigFilename}" forge script scripts/deploy/DeployConfigurable.s.sol:DeployConfigurable --rpc-url "${process.env.LOCAL_RPC_URL}" --force --account "${deployerAccountName}" --sender "${deployer}" --broadcast --slow`, `${process.cwd()}/node_modules/@lido/dual-governance`);
47+
48+
await runDGRegressionTests(chainId, state, process.env.LOCAL_RPC_URL);
49+
}
50+
51+
async function runDGRegressionTests(networkChainId: string, networkState: DeploymentState, rpcUrl: string) {
52+
log.header("Run DG regression tests");
53+
54+
const deployArtifactFilename = await getLatestDGDeployArtifactFilename(networkChainId);
55+
56+
const dotEnvFile = getDGDotEnvFile(deployArtifactFilename, networkState, rpcUrl);
57+
await writeDGDotEnvFile(dotEnvFile);
58+
59+
try {
60+
await runCommand("npm run test:regressions", `${process.cwd()}/node_modules/@lido/dual-governance`);
61+
} catch (error) {
62+
// TODO: some of regression tests don't work at the moment, need to fix it.
63+
log.error("DG regression tests run failed");
64+
log(`${error}`);
65+
}
66+
}
67+
68+
async function runCommand(command: string, workingDirectory: string) {
69+
const exec = util.promisify(child_process.exec);
70+
71+
try {
72+
const { stdout } = await exec(command, { cwd: workingDirectory });
73+
log("stdout:", stdout);
74+
} catch (error) {
75+
log.error(`Error running command ${command}`, `${error}`);
76+
throw error;
77+
}
78+
}
79+
80+
async function writeDGConfigFile(dgConfig: string, filename: string) {
81+
const dgConfigDirPath = `${process.cwd()}/node_modules/@lido/dual-governance/deploy-config`;
82+
const dgConfigFilePath = `${dgConfigDirPath}/${filename}`;
83+
84+
return writeFile(dgConfig, dgConfigFilePath, "config");
85+
}
86+
87+
async function writeDGDotEnvFile(fileContent: string) {
88+
const dgDirPath = `${process.cwd()}/node_modules/@lido/dual-governance`;
89+
const dgDotEnvFilePath = `${dgDirPath}/.env`;
90+
91+
return writeFile(fileContent, dgDotEnvFilePath, ".env");
92+
}
93+
94+
async function writeFile(fileContent: string, filePath: string, fileKind: string) {
95+
try {
96+
await fs.writeFile(filePath, fileContent, "utf8");
97+
log.success(`${fileKind} file successfully saved to ${filePath}`);
98+
} catch (error) {
99+
log.error(`An error has occurred while writing DG ${filePath} file`, `${error}`);
100+
throw error;
101+
}
102+
}
103+
104+
async function getLatestDGDeployArtifactFilename(networkChainId: string) {
105+
const dgDeployArtifactsDirPath = `${process.cwd()}/node_modules/@lido/dual-governance/deploy-artifacts`;
106+
const deployArtifactFilenameRe = new RegExp(`deploy-artifact-${networkChainId}-\\d+.toml`, "ig");
107+
108+
let files = [];
109+
try {
110+
files = await fs.readdir(dgDeployArtifactsDirPath);
111+
} catch (error) {
112+
log.error("An error has occurred while reading directory:", `${error}`);
113+
throw error;
114+
}
115+
116+
files = files.filter((file) => file.match(deployArtifactFilenameRe)).sort();
117+
118+
if (files.length === 0) {
119+
throw new Error("No deploy artifact file found");
120+
}
121+
122+
return files[files.length - 1];
123+
}
124+
125+
function getDGConfig(chainId: string, networkState: DeploymentState) {
126+
const daoVoting = getAddress(Sk.appVoting, networkState);
127+
const withdrawalQueue = getAddress(Sk.withdrawalQueueERC721, networkState);
128+
const stEth = getAddress(Sk.appLido, networkState);
129+
const wstEth = getAddress(Sk.wstETH, networkState);
130+
131+
return {
132+
chain_id: chainId,
133+
dual_governance: {
134+
admin_proposer: daoVoting,
135+
proposals_canceller: daoVoting,
136+
sealable_withdrawal_blockers: [], // TODO: add withdrawalQueue
137+
reseal_committee: daoVoting,
138+
tiebreaker_activation_timeout: 31536000,
139+
140+
signalling_tokens: {
141+
st_eth: stEth,
142+
wst_eth: wstEth,
143+
withdrawal_queue: withdrawalQueue,
144+
},
145+
sanity_check_params: {
146+
max_min_assets_lock_duration: 4147200,
147+
max_sealable_withdrawal_blockers_count: 255,
148+
max_tiebreaker_activation_timeout: 63072000,
149+
min_tiebreaker_activation_timeout: 15768000,
150+
min_withdrawals_batch_size: 4,
151+
}
152+
},
153+
dual_governance_config_provider: {
154+
first_seal_rage_quit_support: "10000000000000000",
155+
second_seal_rage_quit_support: "100000000000000000",
156+
min_assets_lock_duration: 18000,
157+
rage_quit_eth_withdrawals_delay_growth: 1296000,
158+
rage_quit_eth_withdrawals_min_delay: 5184000,
159+
rage_quit_eth_withdrawals_max_delay: 15552000,
160+
rage_quit_extension_period_duration: 604800,
161+
veto_cooldown_duration: 18000,
162+
veto_signalling_deactivation_max_duration: 259200,
163+
veto_signalling_min_active_duration: 18000,
164+
veto_signalling_min_duration: 432000,
165+
veto_signalling_max_duration: 3888000,
166+
},
167+
timelock: {
168+
after_submit_delay: 259200,
169+
after_schedule_delay: 86400,
170+
sanity_check_params: {
171+
min_execution_delay: 259200,
172+
max_after_submit_delay: 2592000,
173+
max_after_schedule_delay: 864000,
174+
max_emergency_mode_duration: 31536000,
175+
max_emergency_protection_duration: 94608000,
176+
},
177+
emergency_protection: {
178+
emergency_activation_committee: daoVoting,
179+
emergency_execution_committee: daoVoting,
180+
emergency_governance_proposer: daoVoting,
181+
emergency_mode_duration: 2592000,
182+
emergency_protection_end_date: 1781913600,
183+
},
184+
},
185+
tiebreaker: {
186+
execution_delay: 2592000,
187+
committees_count: 1,
188+
quorum: 1,
189+
committees: [
190+
{
191+
members: [
192+
daoVoting,
193+
],
194+
quorum: 1,
195+
}
196+
]
197+
}
198+
};
199+
}
200+
201+
function getDGDotEnvFile(deployArtifactFilename: string, networkState: DeploymentState, rpcUrl: string) {
202+
const stEth = getAddress(Sk.appLido, networkState);
203+
const wstEth = getAddress(Sk.wstETH, networkState);
204+
const withdrawalQueue = getAddress(Sk.withdrawalQueueERC721, networkState);
205+
const hashConsensus = getAddress(Sk.hashConsensusForAccountingOracle, networkState);
206+
const burner = getAddress(Sk.burner, networkState);
207+
const accountingOracle = getAddress(Sk.accountingOracle, networkState);
208+
const elRewardsVault = getAddress(Sk.executionLayerRewardsVault, networkState);
209+
const withdrawalVault = getAddress(Sk.withdrawalVault, networkState);
210+
const oracleReportSanityChecker = getAddress(Sk.oracleReportSanityChecker, networkState);
211+
const acl = getAddress(Sk.aragonAcl, networkState);
212+
const ldo = getAddress(Sk.ldo, networkState);
213+
const daoAgent = getAddress(Sk.appAgent, networkState);
214+
const daoVoting = getAddress(Sk.appVoting, networkState);
215+
const daoTokenManager = getAddress(Sk.appTokenManager, networkState);
216+
217+
return `MAINNET_RPC_URL=${rpcUrl}
218+
DEPLOY_ARTIFACT_FILE_NAME=${deployArtifactFilename}
219+
DG_TESTS_LIDO_ST_ETH=${stEth}
220+
DG_TESTS_LIDO_WST_ETH=${wstEth}
221+
DG_TESTS_LIDO_WITHDRAWAL_QUEUE=${withdrawalQueue}
222+
DG_TESTS_LIDO_HASH_CONSENSUS=${hashConsensus}
223+
DG_TESTS_LIDO_BURNER=${burner}
224+
DG_TESTS_LIDO_ACCOUNTING_ORACLE=${accountingOracle}
225+
DG_TESTS_LIDO_EL_REWARDS_VAULT=${elRewardsVault}
226+
DG_TESTS_LIDO_WITHDRAWAL_VAULT=${withdrawalVault}
227+
DG_TESTS_LIDO_ORACLE_REPORT_SANITY_CHECKER=${oracleReportSanityChecker}
228+
DG_TESTS_LIDO_DAO_ACL=${acl}
229+
DG_TESTS_LIDO_LDO_TOKEN=${ldo}
230+
DG_TESTS_LIDO_DAO_AGENT=${daoAgent}
231+
DG_TESTS_LIDO_DAO_VOTING=${daoVoting}
232+
DG_TESTS_LIDO_DAO_TOKEN_MANAGER=${daoTokenManager}
233+
`;
234+
}

0 commit comments

Comments
 (0)