diff --git a/.gitignore b/.gitignore index a466073..e787b77 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,9 @@ node_modules .DS_Store reports/* -wallets/* \ No newline at end of file +wallets/* + +/configs/deployed-local-devnet.json +/configs/extra-deployed-local-devnet.json + +generated-keys diff --git a/configs/envs.ts b/configs/envs.ts index f55dd4d..c95f51c 100644 --- a/configs/envs.ts +++ b/configs/envs.ts @@ -2,4 +2,4 @@ import * as dotenv from 'dotenv'; const { parsed } = dotenv.config(); -export const envs = parsed; +export const envs = { ...parsed, ...process.env }; diff --git a/programs/devnet.ts b/programs/devnet.ts new file mode 100644 index 0000000..7261d56 --- /dev/null +++ b/programs/devnet.ts @@ -0,0 +1,166 @@ +import { program } from '@command'; +import { votingNewVote } from '@scripts'; +import { + CallScriptActionWithDescription, + encodeCallScript, + forwardVoteFromTm, + splitAddresses, + logger, + compareContractCalls, + authorizedCall, +} from '@utils'; +import chalk from 'chalk'; +import { + encodeScriptsAO, + encodeScriptsDSM, + encodeScriptsLidoResumeIfStopped, + encodeScriptsRoles, + encodeScriptsVEBO, + encodeScriptsWQResumeIfPaused, + joinVotingDesc, + DEFAULT_DEVNET_CONFIG, +} from './omnibus-scripts/generators'; +import { getLocatorContract, getProxyContract, locatorContract } from '@contracts'; +import { getCreateAddress } from 'ethers'; +import { wallet } from '@providers'; +import { findDeploymentTransaction } from 'utils'; + +const devnet = program.command('devnet').description('scripts for devnet'); + +const head = chalk.blue.bold; +const bold = chalk.white.bold; + +devnet + .command('setup') + .option('-l, --staking-limit ', 'staking limit in eth', String(DEFAULT_DEVNET_CONFIG.STAKING_LIMIT)) + .option('-m, --oracles-members ', 'VEBO and AO members separated by comma', '') + .option('-q, --oracles-quorum ', 'VEBO and AO members quorum, should be > 50%', '1') + .option('-e, --oracles-initial-epoch ', 'initial epoch for VEBO and AO') + .option('-g, --dsm-guardians ', 'DSM guardians separated by comma', '') + .option('-d, --dsm-quorum ', 'DSM guardians quorum', '1') + .option('-b, --roles-beneficiary ', 'roles beneficiary', DEFAULT_DEVNET_CONFIG.ROLES_BENEFICIARY) + .action(async (options) => { + // Lido + logger.log(head('Lido')); + const stakingLimit = Number(options.stakingLimit); + const lidoResumeScripts = await encodeScriptsLidoResumeIfStopped(stakingLimit); + logger.log(); + + // Withdrawal Queue + logger.log(head('Withdrawal Queue')); + const wqResumeScripts = await encodeScriptsWQResumeIfPaused(); + logger.log(); + + // Oracles + logger.log(head('Oracles')); + + const oraclesInitialEpoch = options.oraclesInitialEpoch ? Number(options.oraclesInitialEpoch) : undefined; + const oraclesMembers = splitAddresses(options.oraclesMembers); + const oraclesQuorum = Number(options.oraclesQuorum); + + /**/ logger.log(bold('VEBO')); + /**/ const veboScripts = await encodeScriptsVEBO(oraclesMembers, oraclesQuorum, oraclesInitialEpoch); + /**/ logger.log(); + + /**/ logger.log(bold('AO')); + /**/ const aoScripts = await encodeScriptsAO(oraclesMembers, oraclesQuorum, oraclesInitialEpoch); + /**/ logger.log(); + + // DSM + logger.log(head('DSM')); + const guardians = splitAddresses(options.dsmGuardians); + const dsmQuorum = Number(options.dsmQuorum); + const dsmScripts = await encodeScriptsDSM(guardians, dsmQuorum); + logger.log(); + + // Roles + logger.log(head('Roles')); + const rolesBeneficiary = options.rolesBeneficiary; + const rolesScripts = await encodeScriptsRoles(rolesBeneficiary); + logger.log(); + + // Voting calls + const votingCalls: CallScriptActionWithDescription[] = [ + ...lidoResumeScripts, + ...wqResumeScripts, + ...veboScripts, + ...aoScripts, + ...dsmScripts, + ...rolesScripts, + ]; + + // Voting description + const description = joinVotingDesc(votingCalls); + logger.log(head('Voting description:')); + logger.log(description); + logger.log(); + + // Voting start + const voteEvmScript = encodeCallScript(votingCalls); + const [newVoteCalldata] = votingNewVote(voteEvmScript, description); + await forwardVoteFromTm(newVoteCalldata); + }); + +devnet + .command('replace-dsm-with-eoa') + .argument('', 'EOA address') + .action(async (eoa) => { + const getProxyAddress = async () => await locatorContract.getAddress(); + const locatorProxyContract = getProxyContract(getProxyAddress); + const curLocatorImplementationAddress = await locatorProxyContract.proxy__getImplementation(); + + const currentDSMAddress = await locatorContract.depositSecurityModule(); + const currentDSMAddressBytes = currentDSMAddress.slice(2).toLowerCase(); + const eoaBytes = eoa.slice(2).toLowerCase(); + + if (eoaBytes.length !== 40) { + logger.error('Invalid EOA address'); + return; + } + + const currentLocatorImplementationDeploymentTx = await findDeploymentTransaction(curLocatorImplementationAddress); + const newLocatorDeploymentData = currentLocatorImplementationDeploymentTx.data.replaceAll( + currentDSMAddressBytes, + eoaBytes, + ); + + const newLocatorDeployTx = { + data: newLocatorDeploymentData, + gasLimit: 5000000, + }; + + const txResponse = await wallet.sendTransaction(newLocatorDeployTx); + logger.log('New Locator deployment tx hash:', txResponse.hash); + + await txResponse.wait(); + + const newLocatorImplementationAddress = getCreateAddress(txResponse); + logger.log('New Locator implementation address:', newLocatorImplementationAddress); + + logger.log('Locator implementations diff'); + + const curLocatorImplementationContract = getLocatorContract(curLocatorImplementationAddress); + const newLocatorImplementationContract = getLocatorContract(newLocatorImplementationAddress); + + await compareContractCalls( + [curLocatorImplementationContract, newLocatorImplementationContract], + [ + { method: 'accountingOracle' }, + { method: 'depositSecurityModule' }, + { method: 'elRewardsVault' }, + { method: 'legacyOracle' }, + { method: 'lido' }, + { method: 'oracleReportSanityChecker' }, + { method: 'postTokenRebaseReceiver' }, + { method: 'burner' }, + { method: 'stakingRouter' }, + { method: 'treasury' }, + { method: 'validatorsExitBusOracle' }, + { method: 'withdrawalQueue' }, + { method: 'withdrawalVault' }, + { method: 'oracleDaemonConfig' }, + ], + ); + + await authorizedCall(locatorProxyContract, 'proxy__upgradeTo', [newLocatorImplementationAddress]); + }); diff --git a/programs/index.ts b/programs/index.ts index bbadb7a..ea5a3a8 100644 --- a/programs/index.ts +++ b/programs/index.ts @@ -9,6 +9,7 @@ export * from './csm-oracle'; export * from './csm'; export * from './deposit-contract'; export * from './deposit-data'; +export * from './devnet'; export * from './dsm'; export * from './el-requests'; export * from './eth'; diff --git a/programs/omnibus-scripts/devnet-csm-start.ts b/programs/omnibus-scripts/devnet-csm-start.ts new file mode 100644 index 0000000..2cad75d --- /dev/null +++ b/programs/omnibus-scripts/devnet-csm-start.ts @@ -0,0 +1,160 @@ +import { + aragonAgentAddress, + burnerAddress, + burnerContract, + stakingRouterAddress, + stakingRouterContract, +} from '@contracts'; +import { provider } from '@providers'; +import { encodeFromAgent, votingNewVote } from '@scripts'; +import { CallScriptAction, encodeCallScript, forwardVoteFromTm, getRoleHash } from '@utils'; +import { Contract, Interface } from 'ethers'; + +export const devnetCSMStart = async () => { + // Module address: 0x26aBc20a47f7e8991F1d26Bf0fC2bE8f24E9eF2A + const CS_MODULE_ADDRESS = process.env.CS_MODULE_ADDRESS as string; + // Accounting address: 0x782c7c96959bE2258a7b67439435885b39946c9E + const CS_ACCOUNTING_ADDRESS = process.env.CS_ACCOUNTING_ADDRESS as string; + // Oracle hash consensus address: 0x64c48254123A4c62278Fa671BEA32B45099Aeb9b + const CS_ORACLE_HASH_CONSENSUS_ADDRESS = process.env.CS_ORACLE_HASH_CONSENSUS_ADDRESS as string; + // Settle EL stealing address: 0xC86D7B14BE8c1E8718EaC65F95306A71d0E75aA8 + + const CS_MODULE_NAME = process.env.CS_MODULE_NAME ?? 'CommunityStaking'; + const CS_STAKE_SHARE_LIMIT = process.env.CS_STAKE_SHARE_LIMIT ?? 2000; // 20% + const CS_PRIORITY_EXIT_SHARE_THRESHOLD = process.env.CS_PRIORITY_EXIT_SHARE_THRESHOLD ?? 2500; // 25% + const CS_STAKING_MODULE_FEE = process.env.CS_STAKING_MODULE_FEE ?? 800; // 8% + const CS_TREASURY_FEE = process.env.CS_TREASURY_FEE ?? 200; // 2% + const CS_MAX_DEPOSITS_PER_BLOCK = process.env.CS_MAX_DEPOSITS_PER_BLOCK ?? 30; + const CS_MIN_DEPOSIT_BLOCK_DISTANCE = process.env.CS_MIN_DEPOSIT_BLOCK_DISTANCE ?? 25; + // 60 (50 + 10) + // https://github.com/lidofinance/community-staking-module/blob/e1bbb4133d18206fc3a1a63ae660a670be08b6ea/script/DeployLocalDevNet.s.sol#L22 + const CS_ORACLE_INITIAL_EPOCH = process.env.CS_ORACLE_INITIAL_EPOCH ?? 60; + + const iface = new Interface([ + 'function proxy__upgradeTo(address)', + 'function finalizeUpgrade_v2(uint256[],uint256[],uint256[])', + 'function finalizeUpgrade_v2(uint256)', + 'function finalizeUpgrade_v3()', + 'function revokeRole(bytes32,address)', + 'function grantRole(bytes32,address)', + 'function STAKING_MODULE_UNVETTING_ROLE() view returns (bytes32)', + 'function RESUME_ROLE() view returns (bytes32)', + 'function MODULE_MANAGER_ROLE() view returns (bytes32)', + 'function resume()', + 'function activatePublicRelease()', + 'function updateInitialEpoch(uint256)', + 'function addStakingModule(string,address,uint256,uint256,uint256,uint256,uint256,uint256)', + 'function setConsensusVersion(uint256)', + 'function addEVMScriptFactory(address,bytes)', + ]); + + /** + * CSM + */ + + // 1. Grant staking module manage role to agent + const srModuleManageRoleHash = await getRoleHash(stakingRouterContract, 'STAKING_MODULE_MANAGE_ROLE'); + const [, moduleManageRoleGrantToAgentScript] = encodeFromAgent({ + to: stakingRouterAddress, + data: iface.encodeFunctionData('grantRole', [srModuleManageRoleHash, aragonAgentAddress]), + }); + + // 2. Add staking module + const [, addStakingModuleScript] = encodeFromAgent({ + to: stakingRouterAddress, + data: iface.encodeFunctionData('addStakingModule', [ + CS_MODULE_NAME, + CS_MODULE_ADDRESS, + CS_STAKE_SHARE_LIMIT, + CS_PRIORITY_EXIT_SHARE_THRESHOLD, + CS_STAKING_MODULE_FEE, + CS_TREASURY_FEE, + CS_MAX_DEPOSITS_PER_BLOCK, + CS_MIN_DEPOSIT_BLOCK_DISTANCE, + ]), + }); + + // 3. Grant request burn role to CSAccounting contract + const burnerRequestBurnRoleHash = await getRoleHash(burnerContract, 'REQUEST_BURN_SHARES_ROLE'); + const [, requestBurnRoleGrantScript] = encodeFromAgent({ + to: burnerAddress, + data: iface.encodeFunctionData('grantRole', [burnerRequestBurnRoleHash, CS_ACCOUNTING_ADDRESS]), + }); + + // 4. Grant resume role to agent + const csModuleContract = new Contract(CS_MODULE_ADDRESS, iface, provider); + const csmResumeRoleHash = await getRoleHash(csModuleContract, 'RESUME_ROLE'); + const [, resumeRoleGrantScript] = encodeFromAgent({ + to: CS_MODULE_ADDRESS, + data: iface.encodeFunctionData('grantRole', [csmResumeRoleHash, aragonAgentAddress]), + }); + + // 5. Grant csmModuleManager role to agent + const csmModuleManagerRoleHash = await getRoleHash(csModuleContract, 'MODULE_MANAGER_ROLE'); + const [, csmModuleManagerRoleGrantScript] = encodeFromAgent({ + to: CS_MODULE_ADDRESS, + data: iface.encodeFunctionData('grantRole', [csmModuleManagerRoleHash, aragonAgentAddress]), + }); + + // 6. Resume staking module + const [, resumeScript] = encodeFromAgent({ + to: CS_MODULE_ADDRESS, + data: iface.encodeFunctionData('resume', []), + }); + + // 7. Activate public release + const [, activatePublicReleaseScript] = encodeFromAgent({ + to: CS_MODULE_ADDRESS, + data: iface.encodeFunctionData('activatePublicRelease', []), + }); + + // 8. Revoke resume role from agent + const [, resumeRoleRevokeScript] = encodeFromAgent({ + to: CS_MODULE_ADDRESS, + data: iface.encodeFunctionData('revokeRole', [csmResumeRoleHash, aragonAgentAddress]), + }); + + // 9. Revoke csmModuleManager role from agent + const [, resumeCsmModuleManagerRoleRevokeScript] = encodeFromAgent({ + to: CS_MODULE_ADDRESS, + data: iface.encodeFunctionData('revokeRole', [csmModuleManagerRoleHash, aragonAgentAddress]), + }); + + // 10. Update initial epoch + const [, updateInitialEpochScript] = encodeFromAgent({ + to: CS_ORACLE_HASH_CONSENSUS_ADDRESS, + data: iface.encodeFunctionData('updateInitialEpoch', [CS_ORACLE_INITIAL_EPOCH]), + }); + + // Collect all calls + const calls: CallScriptAction[] = [ + moduleManageRoleGrantToAgentScript, + addStakingModuleScript, + requestBurnRoleGrantScript, + resumeRoleGrantScript, + csmModuleManagerRoleGrantScript, + resumeScript, + activatePublicReleaseScript, + resumeRoleRevokeScript, + resumeCsmModuleManagerRoleRevokeScript, + updateInitialEpochScript, + ]; + + const description = [ + `1. Grant staking module manage role to agent ${aragonAgentAddress}`, + `2. Add staking module ${CS_MODULE_NAME} with address ${CS_MODULE_ADDRESS}`, + `3. Grant request burn shares role to CSAccounting contract with address ${CS_ACCOUNTING_ADDRESS}`, + `4. Grant resume role to agent ${aragonAgentAddress}`, + `5. Grant csmModuleManager role to agent ${aragonAgentAddress}`, + `6. Resume staking module`, + `7. Activate public release`, + `8. Revoke resume role from agent ${aragonAgentAddress}`, + `9. Revoke csmModuleManager role from agent ${aragonAgentAddress}`, + `10. Update initial epoch to ${CS_ORACLE_INITIAL_EPOCH}`, + ].join('\n'); + + const voteEvmScript = encodeCallScript(calls); + const [newVoteCalldata] = votingNewVote(voteEvmScript, description); + + await forwardVoteFromTm(newVoteCalldata); +}; diff --git a/programs/omnibus-scripts/devnet-start.ts b/programs/omnibus-scripts/devnet-start.ts index 9168f5c..94111f4 100644 --- a/programs/omnibus-scripts/devnet-start.ts +++ b/programs/omnibus-scripts/devnet-start.ts @@ -1,61 +1,24 @@ -import { - consensusForAccountingContract, - consensusForExitBusContract, - norContract, - oracleConfigContract, - sanityCheckerContract, - stakingRouterContract, -} from '@contracts'; -import { wallet } from '@providers'; import { votingNewVote } from '@scripts'; import { CallScriptActionWithDescription, encodeCallScript, forwardVoteFromTm, logger } from '@utils'; import { - encodeFromVotingGrantRolesAragonWithConfirm, - encodeFromAgentGrantRolesAccessControlWithConfirmed, - encodeScriptsVEBOResumeIfPaused, encodeScriptsWQResumeIfPaused, - promptScriptsAddingGuardiansFromAgentIfEmpty, - promptScriptsAOInitialEpoch, - promptScriptsAOMembers, promptScriptsLidoResumeIfStopped, - promptScriptsVEBOInitialEpoch, - promptScriptsVEBOMembers, - promptRolesBeneficiary, joinVotingDesc, + DEFAULT_DEVNET_CONFIG, + promptScriptsVEBO, + promptScriptsAO, + promptScriptsDSM, + promptScriptsRoles, } from './generators'; import chalk from 'chalk'; -const DEFAULT_CONFIG = { - STAKING_LIMIT: 150_000, - ORACLE_MEMBERS: 2, - DSM_GUARDIANS_MEMBERS: 2, - ROLES_BENEFICIARY: wallet.address, -}; - -const NOR_ROLES = [ - 'STAKING_ROUTER_ROLE', - 'MANAGE_SIGNING_KEYS', - 'SET_NODE_OPERATOR_LIMIT_ROLE', - 'MANAGE_NODE_OPERATOR_ROLE', -]; - -const HASH_CONSENSUS_ROLES = [ - 'MANAGE_MEMBERS_AND_QUORUM_ROLE', - 'MANAGE_FRAME_CONFIG_ROLE', - 'MANAGE_FAST_LANE_CONFIG_ROLE', -]; - -const ORACLE_CONFIG_ROLES = ['CONFIG_MANAGER_ROLE']; -const STAKING_ROUTER_ROLES = ['STAKING_MODULE_MANAGE_ROLE']; -const SANITY_CHECKER_ROLES = ['ALL_LIMITS_MANAGER_ROLE']; - const head = chalk.blue.bold; const bold = chalk.white.bold; export const devnetStart = async () => { // Lido logger.log(head('Lido')); - const lidoResumeScripts = await promptScriptsLidoResumeIfStopped(DEFAULT_CONFIG.STAKING_LIMIT); + const lidoResumeScripts = await promptScriptsLidoResumeIfStopped(DEFAULT_DEVNET_CONFIG.STAKING_LIMIT); logger.log(); // Withdrawal Queue @@ -105,57 +68,3 @@ export const devnetStart = async () => { const [newVoteCalldata] = votingNewVote(voteEvmScript, description); await forwardVoteFromTm(newVoteCalldata); }; - -const promptScriptsVEBO = async () => { - const veboResumeScripts = await encodeScriptsVEBOResumeIfPaused(); - const veboMembersScripts = await promptScriptsVEBOMembers(DEFAULT_CONFIG.ORACLE_MEMBERS); - const veboInitialEpochScripts = await promptScriptsVEBOInitialEpoch(); - return [...veboResumeScripts, ...veboMembersScripts, ...veboInitialEpochScripts]; -}; - -const promptScriptsAO = async () => { - const aoMembersScripts = await promptScriptsAOMembers(DEFAULT_CONFIG.ORACLE_MEMBERS); - const aoInitialEpochScripts = await promptScriptsAOInitialEpoch(); - return [...aoMembersScripts, ...aoInitialEpochScripts]; -}; - -const promptScriptsDSM = async () => { - const guardiansScripts = await promptScriptsAddingGuardiansFromAgentIfEmpty(DEFAULT_CONFIG.DSM_GUARDIANS_MEMBERS); - return [...guardiansScripts]; -}; - -const promptScriptsRoles = async () => { - const beneficiary = await promptRolesBeneficiary(DEFAULT_CONFIG.ROLES_BENEFICIARY); - const srScripts = await encodeFromAgentGrantRolesAccessControlWithConfirmed( - 'SR', - STAKING_ROUTER_ROLES, - stakingRouterContract, - beneficiary, - ); - const norScripts = await encodeFromVotingGrantRolesAragonWithConfirm('NOR', NOR_ROLES, norContract, beneficiary); - const aoScripts = await encodeFromAgentGrantRolesAccessControlWithConfirmed( - 'AO consensus', - HASH_CONSENSUS_ROLES, - consensusForAccountingContract, - beneficiary, - ); - const veboScripts = await encodeFromAgentGrantRolesAccessControlWithConfirmed( - 'VEBO consensus', - HASH_CONSENSUS_ROLES, - consensusForExitBusContract, - beneficiary, - ); - const oracleConfigScripts = await encodeFromAgentGrantRolesAccessControlWithConfirmed( - 'Oracle daemon config', - ORACLE_CONFIG_ROLES, - oracleConfigContract, - beneficiary, - ); - const sanityCheckerScripts = await encodeFromAgentGrantRolesAccessControlWithConfirmed( - 'Sanity checker', - SANITY_CHECKER_ROLES, - sanityCheckerContract, - beneficiary, - ); - return [...srScripts, ...norScripts, ...aoScripts, ...veboScripts, ...oracleConfigScripts, ...sanityCheckerScripts]; -}; diff --git a/programs/omnibus-scripts/generators/access-control.ts b/programs/omnibus-scripts/generators/access-control.ts index 5dcbcbf..7b18732 100644 --- a/programs/omnibus-scripts/generators/access-control.ts +++ b/programs/omnibus-scripts/generators/access-control.ts @@ -18,7 +18,7 @@ export const encodeFromAgentGrantRole = async ( }); }; -export const encodeFromAgentGrantRolesAccessControlWithConfirmed = async ( +export const encodeFromAgentGrantRolesAccessControlWithConfirm = async ( contractName: string, rolesToGrant: string[], contract: Contract, diff --git a/programs/omnibus-scripts/generators/accounting-oracle.ts b/programs/omnibus-scripts/generators/accounting-oracle.ts index 02cdc1d..5af3531 100644 --- a/programs/omnibus-scripts/generators/accounting-oracle.ts +++ b/programs/omnibus-scripts/generators/accounting-oracle.ts @@ -1,11 +1,37 @@ import { accountingOracleContract, consensusForAccountingContract } from '@contracts'; import { encodeFromAgentGrantRole } from './access-control'; -import { promptScriptsOracleInitialEpochIfNotSet, promptScriptsOracleMembersIfEmpty } from './oracles'; +import { + encodeScriptsOracleInitialEpochIfPassed, + encodeScriptsOracleMembers, + promptScriptsOracleInitialEpochIfNotSet, + promptScriptsOracleMembersIfEmpty, +} from './oracles'; +import { DEFAULT_DEVNET_CONFIG } from './devnet'; + +export const encodeScriptsAO = async (oracleMembers: string[], oracleQuorum: number, initialEpoch?: number) => { + const aoMembersScripts = await encodeScriptsAOMembers(oracleMembers, oracleQuorum); + const aoInitialEpochScripts = await encodeScriptsAOInitialEpoch(initialEpoch); + return [...aoMembersScripts, ...aoInitialEpochScripts]; +}; + +export const promptScriptsAO = async () => { + const aoMembersScripts = await promptScriptsAOMembers(DEFAULT_DEVNET_CONFIG.ORACLE_MEMBERS); + const aoInitialEpochScripts = await promptScriptsAOInitialEpoch(); + return [...aoMembersScripts, ...aoInitialEpochScripts]; +}; export const encodeFromAgentAOGrantRole = async (role: string, account: string) => { return await encodeFromAgentGrantRole('AO', accountingOracleContract, role, account); }; +export const encodeScriptsAOMembers = async (members: string[], quorum: number) => { + return await encodeScriptsOracleMembers('AO', consensusForAccountingContract, members, quorum); +}; + +export const encodeScriptsAOInitialEpoch = async (initialEpoch?: number) => { + return await encodeScriptsOracleInitialEpochIfPassed('AO', consensusForAccountingContract, initialEpoch); +}; + export const promptScriptsAOMembers = async (initialMembers: number) => { return await promptScriptsOracleMembersIfEmpty('AO', consensusForAccountingContract, initialMembers); }; diff --git a/programs/omnibus-scripts/generators/devnet.ts b/programs/omnibus-scripts/generators/devnet.ts new file mode 100644 index 0000000..78af82c --- /dev/null +++ b/programs/omnibus-scripts/generators/devnet.ts @@ -0,0 +1,8 @@ +import { wallet } from '@providers'; + +export const DEFAULT_DEVNET_CONFIG = { + STAKING_LIMIT: 150_000, + ORACLE_MEMBERS: 2, + DSM_GUARDIANS_MEMBERS: 2, + ROLES_BENEFICIARY: wallet.address, +}; diff --git a/programs/omnibus-scripts/generators/dsm.ts b/programs/omnibus-scripts/generators/dsm.ts index 522b589..13b7d72 100644 --- a/programs/omnibus-scripts/generators/dsm.ts +++ b/programs/omnibus-scripts/generators/dsm.ts @@ -3,6 +3,41 @@ import { encodeFromAgent } from '@scripts'; import { CallScriptActionWithDescription, logger } from '@utils'; import { isAddress } from 'ethers'; import prompts from 'prompts'; +import { DEFAULT_DEVNET_CONFIG } from './devnet'; + +export const encodeScriptsDSM = async (guardians: string[], quorum: number) => { + const guardiansScripts = await encodeScriptsAddingGuardiansFromAgent(guardians, quorum); + return [...guardiansScripts]; +}; + +export const encodeScriptsAddingGuardiansFromAgent = async (guardians: string[], quorum: number) => { + const total = guardians.length; + + if (total === 0) { + logger.warn('No guardians to add. Skipping adding guardians'); + return []; + } + + logger.log('Preparing scripts to add guardians'); + + const calls: CallScriptActionWithDescription[] = []; + + for (let i = 0; i < total; i++) { + const quorumForIteration = Math.min(i + 1, quorum); + const [, addGuardianCall] = encodeFromAgentAddGuardian(guardians[i], quorumForIteration); + + calls.push(addGuardianCall); + } + + return calls; +}; + +export const promptScriptsDSM = async () => { + const guardiansScripts = await promptScriptsAddingGuardiansFromAgentIfEmpty( + DEFAULT_DEVNET_CONFIG.DSM_GUARDIANS_MEMBERS, + ); + return [...guardiansScripts]; +}; export const promptScriptsAddingGuardiansFromAgentIfEmpty = async (initialTotal: number) => { const guardians = await dsmContract.getGuardians(); diff --git a/programs/omnibus-scripts/generators/exit-bus-oracle.ts b/programs/omnibus-scripts/generators/exit-bus-oracle.ts index eca0e78..49a1db3 100644 --- a/programs/omnibus-scripts/generators/exit-bus-oracle.ts +++ b/programs/omnibus-scripts/generators/exit-bus-oracle.ts @@ -1,11 +1,39 @@ import { consensusForExitBusContract, exitBusOracleContract } from '@contracts'; import { encodeUnpauseIfPaused } from './pause-until'; -import { promptScriptsOracleInitialEpochIfNotSet, promptScriptsOracleMembersIfEmpty } from './oracles'; +import { + encodeScriptsOracleInitialEpochIfPassed, + encodeScriptsOracleMembers, + promptScriptsOracleInitialEpochIfNotSet, + promptScriptsOracleMembersIfEmpty, +} from './oracles'; +import { DEFAULT_DEVNET_CONFIG } from './devnet'; + +export const encodeScriptsVEBO = async (oracleMembers: string[], oracleQuorum: number, initialEpoch?: number) => { + const veboResumeScripts = await encodeScriptsVEBOResumeIfPaused(); + const veboMembersScripts = await encodeScriptsVEBOMembers(oracleMembers, oracleQuorum); + const veboInitialEpochScripts = await encodeScriptsVEBOInitialEpoch(initialEpoch); + return [...veboResumeScripts, ...veboMembersScripts, ...veboInitialEpochScripts]; +}; + +export const promptScriptsVEBO = async () => { + const veboResumeScripts = await encodeScriptsVEBOResumeIfPaused(); + const veboMembersScripts = await promptScriptsVEBOMembers(DEFAULT_DEVNET_CONFIG.ORACLE_MEMBERS); + const veboInitialEpochScripts = await promptScriptsVEBOInitialEpoch(); + return [...veboResumeScripts, ...veboMembersScripts, ...veboInitialEpochScripts]; +}; export const encodeScriptsVEBOResumeIfPaused = async () => { return encodeUnpauseIfPaused('VEBO', exitBusOracleContract); }; +export const encodeScriptsVEBOMembers = async (members: string[], quorum: number) => { + return await encodeScriptsOracleMembers('VEBO', consensusForExitBusContract, members, quorum); +}; + +export const encodeScriptsVEBOInitialEpoch = async (initialEpoch?: number) => { + return await encodeScriptsOracleInitialEpochIfPassed('VEBO', consensusForExitBusContract, initialEpoch); +}; + export const promptScriptsVEBOMembers = async (initialMembers: number) => { return await promptScriptsOracleMembersIfEmpty('VEBO', consensusForExitBusContract, initialMembers); }; diff --git a/programs/omnibus-scripts/generators/index.ts b/programs/omnibus-scripts/generators/index.ts index f2dd007..4639440 100644 --- a/programs/omnibus-scripts/generators/index.ts +++ b/programs/omnibus-scripts/generators/index.ts @@ -2,6 +2,7 @@ export * from './access-control'; export * from './accounting-oracle'; export * from './aragon'; export * from './desc'; +export * from './devnet'; export * from './dsm'; export * from './exit-bus-oracle'; export * from './lido'; diff --git a/programs/omnibus-scripts/generators/lido.ts b/programs/omnibus-scripts/generators/lido.ts index 5ef9e3d..c5a6d31 100644 --- a/programs/omnibus-scripts/generators/lido.ts +++ b/programs/omnibus-scripts/generators/lido.ts @@ -24,7 +24,23 @@ export const promptScriptsLidoResume = async (initialStakingLimit: number) => { message: 'Enter daily Lido staking limit', }); - const parsedLimit = parseEther(String(limit)); + return await encodeScriptsLidoResume(limit); +}; + +export const encodeScriptsLidoResumeIfStopped = async (stakingLimit: number) => { + const isLidoStopped = await lidoContract.isStopped(); + + if (isLidoStopped) { + logger.log('Contract is stopped. Preparing scripts to resume and set staking limit'); + return await encodeScriptsLidoResume(stakingLimit); + } + + logger.warn('Contract is already running. Skipping resume and staking limit setting'); + return []; +}; + +export const encodeScriptsLidoResume = async (stakingLimit: number | string) => { + const parsedLimit = parseEther(String(stakingLimit)); const [, resumeProtocolCall, resumeStakingCall, setStakingLimitCall] = resumeLidoAndSetStakingLimit(parsedLimit); return [resumeProtocolCall, resumeStakingCall, setStakingLimitCall]; }; diff --git a/programs/omnibus-scripts/generators/oracles.ts b/programs/omnibus-scripts/generators/oracles.ts index 7ddd284..151821b 100644 --- a/programs/omnibus-scripts/generators/oracles.ts +++ b/programs/omnibus-scripts/generators/oracles.ts @@ -58,6 +58,49 @@ export const promptScriptsOracleMembers = async ( return calls; }; +export const encodeScriptsOracleMembers = async ( + oracleName: string, + hashConsensusContract: Contract, + members: string[], + quorum: number, +) => { + const calls: CallScriptActionWithDescription[] = []; + const total = members.length; + + if (total === 0) { + logger.warn('No members to add. Skipping adding members'); + return []; + } + + logger.log('Preparing scripts to grant required role and add members'); + + if (!validateOracleQuorum(quorum, total)) { + const minQuorum = getOracleMinQuorum(total); + throw new Error(`Quorum ${quorum} is not in range [${minQuorum}...${total}]`); + } + + const [, grantManageMembersRoleCall] = await encodeFromAgentGrantRole( + `${oracleName} consensus`, + hashConsensusContract, + 'MANAGE_MEMBERS_AND_QUORUM_ROLE', + aragonAgentAddress, + ); + calls.push(grantManageMembersRoleCall); + + for (let i = 0; i < total; i++) { + const quorumForIteration = Math.min(i + 1, quorum); + const [, addMemberCall] = await encodeFromAgentAddOracle( + `${oracleName} consensus`, + hashConsensusContract, + members[i], + quorumForIteration, + ); + calls.push(addMemberCall); + } + + return calls; +}; + export const promptMembersNumber = async (initialMembers: number) => { const { total } = await prompts({ type: 'number', @@ -70,7 +113,7 @@ export const promptMembersNumber = async (initialMembers: number) => { }; export const promptMembersQuorum = async (total: number) => { - const minQuorum = Math.min(Math.floor(total / 2) + 1, total); + const minQuorum = getOracleMinQuorum(total); const { quorum } = await prompts({ type: 'number', @@ -78,7 +121,7 @@ export const promptMembersQuorum = async (total: number) => { initial: Math.floor(total / 2) + 1, validate: (value) => { const isDefaultValue = value === ''; - const inRange = Number(value) >= minQuorum && Number(value) <= total; + const inRange = validateOracleQuorum(Number(value), total); return isDefaultValue || inRange; }, message: `Enter quorum (must be in range [${minQuorum}...${total}])`, @@ -87,6 +130,15 @@ export const promptMembersQuorum = async (total: number) => { return quorum; }; +export const getOracleMinQuorum = (total: number) => { + return Math.min(Math.floor(total / 2) + 1, total); +}; + +export const validateOracleQuorum = (quorum: number, total: number) => { + const minQuorum = getOracleMinQuorum(total); + return quorum >= minQuorum && quorum <= total; +}; + export const promptOracleAddress = async (index: number) => { const { address } = await prompts({ type: 'text', @@ -141,6 +193,28 @@ export const getFarFutureEpoch = async (hashConsensusContract: Contract) => { export const promptScriptsOracleInitialEpoch = async (oracleName: string, hashConsensusContract: Contract) => { const initialEpoch = await promptOracleInitialEpoch(); + return await encodeScriptsOracleInitialEpoch(oracleName, hashConsensusContract, initialEpoch); +}; + +export const encodeScriptsOracleInitialEpochIfPassed = async ( + oracleName: string, + hashConsensusContract: Contract, + initialEpoch?: number, +) => { + if (initialEpoch != null) { + return await encodeScriptsOracleInitialEpoch(oracleName, hashConsensusContract, initialEpoch); + } + + return []; +}; + +export const encodeScriptsOracleInitialEpoch = async ( + oracleName: string, + hashConsensusContract: Contract, + initialEpoch: number, +) => { + logger.log('Preparing scripts to update initial epoch'); + const [, updateInitialEpochCall] = await encodeFromAgentUpdateInitialEpoch( oracleName, hashConsensusContract, diff --git a/programs/omnibus-scripts/generators/roles-auxiliary.ts b/programs/omnibus-scripts/generators/roles-auxiliary.ts index 27a55ac..d66029c 100644 --- a/programs/omnibus-scripts/generators/roles-auxiliary.ts +++ b/programs/omnibus-scripts/generators/roles-auxiliary.ts @@ -2,9 +2,115 @@ import prompts from 'prompts'; import { isAddress } from 'ethers'; import { logger } from '@utils'; import chalk from 'chalk'; +import { + encodeFromAgentGrantRolesAccessControl, + encodeFromAgentGrantRolesAccessControlWithConfirm, +} from './access-control'; +import { encodeFromVotingGrantRolesAragon, encodeFromVotingGrantRolesAragonWithConfirm } from './aragon'; +import { + consensusForAccountingContract, + consensusForExitBusContract, + norContract, + oracleConfigContract, + sanityCheckerContract, + stakingRouterContract, +} from '@contracts'; +import { DEFAULT_DEVNET_CONFIG } from './devnet'; + +const NOR_ROLES = [ + 'STAKING_ROUTER_ROLE', + 'MANAGE_SIGNING_KEYS', + 'SET_NODE_OPERATOR_LIMIT_ROLE', + 'MANAGE_NODE_OPERATOR_ROLE', +]; + +const HASH_CONSENSUS_ROLES = [ + 'MANAGE_MEMBERS_AND_QUORUM_ROLE', + 'MANAGE_FRAME_CONFIG_ROLE', + 'MANAGE_FAST_LANE_CONFIG_ROLE', +]; + +const ORACLE_CONFIG_ROLES = ['CONFIG_MANAGER_ROLE']; +const STAKING_ROUTER_ROLES = ['STAKING_MODULE_MANAGE_ROLE']; +const SANITY_CHECKER_ROLES = ['ALL_LIMITS_MANAGER_ROLE']; const bold = chalk.white.bold; +export const promptScriptsRoles = async () => { + const beneficiary = await promptRolesBeneficiary(DEFAULT_DEVNET_CONFIG.ROLES_BENEFICIARY); + return promptScriptsRolesWithConfirm(beneficiary); +}; + +export const promptScriptsRolesWithConfirm = async (beneficiary: string) => { + const srScripts = await encodeFromAgentGrantRolesAccessControlWithConfirm( + 'SR', + STAKING_ROUTER_ROLES, + stakingRouterContract, + beneficiary, + ); + const norScripts = await encodeFromVotingGrantRolesAragonWithConfirm('NOR', NOR_ROLES, norContract, beneficiary); + const aoScripts = await encodeFromAgentGrantRolesAccessControlWithConfirm( + 'AO consensus', + HASH_CONSENSUS_ROLES, + consensusForAccountingContract, + beneficiary, + ); + const veboScripts = await encodeFromAgentGrantRolesAccessControlWithConfirm( + 'VEBO consensus', + HASH_CONSENSUS_ROLES, + consensusForExitBusContract, + beneficiary, + ); + const oracleConfigScripts = await encodeFromAgentGrantRolesAccessControlWithConfirm( + 'Oracle daemon config', + ORACLE_CONFIG_ROLES, + oracleConfigContract, + beneficiary, + ); + const sanityCheckerScripts = await encodeFromAgentGrantRolesAccessControlWithConfirm( + 'Sanity checker', + SANITY_CHECKER_ROLES, + sanityCheckerContract, + beneficiary, + ); + return [...srScripts, ...norScripts, ...aoScripts, ...veboScripts, ...oracleConfigScripts, ...sanityCheckerScripts]; +}; + +export const encodeScriptsRoles = async (beneficiary: string) => { + const srScripts = await encodeFromAgentGrantRolesAccessControl( + 'SR', + STAKING_ROUTER_ROLES, + stakingRouterContract, + beneficiary, + ); + const norScripts = await encodeFromVotingGrantRolesAragon('NOR', NOR_ROLES, norContract, beneficiary); + const aoScripts = await encodeFromAgentGrantRolesAccessControl( + 'AO consensus', + HASH_CONSENSUS_ROLES, + consensusForAccountingContract, + beneficiary, + ); + const veboScripts = await encodeFromAgentGrantRolesAccessControl( + 'VEBO consensus', + HASH_CONSENSUS_ROLES, + consensusForExitBusContract, + beneficiary, + ); + const oracleConfigScripts = await encodeFromAgentGrantRolesAccessControl( + 'Oracle daemon config', + ORACLE_CONFIG_ROLES, + oracleConfigContract, + beneficiary, + ); + const sanityCheckerScripts = await encodeFromAgentGrantRolesAccessControl( + 'Sanity checker', + SANITY_CHECKER_ROLES, + sanityCheckerContract, + beneficiary, + ); + return [...srScripts, ...norScripts, ...aoScripts, ...veboScripts, ...oracleConfigScripts, ...sanityCheckerScripts]; +}; + export const promptRolesBeneficiary = async (initialAddress: string) => { const { address } = await prompts({ type: 'text', diff --git a/programs/omnibus-scripts/index.ts b/programs/omnibus-scripts/index.ts index d6f88d8..3a59659 100644 --- a/programs/omnibus-scripts/index.ts +++ b/programs/omnibus-scripts/index.ts @@ -1,6 +1,7 @@ export * from './clone-nor-module'; export * from './csm-update'; export * from './devnet-start'; +export * from './devnet-csm-start'; export * from './sanity-checker-update'; export * from './staking-router-2'; export * from './staking-router-fix'; diff --git a/programs/omnibus.ts b/programs/omnibus.ts index 70cb458..1788930 100644 --- a/programs/omnibus.ts +++ b/programs/omnibus.ts @@ -32,7 +32,7 @@ omnibus omnibus .command('script') - .argument('