diff --git a/scripts/permissions-migration/aragon-permissions.json b/scripts/permissions-migration/aragon-permissions.json
new file mode 100644
index 00000000..db90fae2
--- /dev/null
+++ b/scripts/permissions-migration/aragon-permissions.json
@@ -0,0 +1,8 @@
+[
+ {
+ "where": "0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034",
+ "what": "0xa42eee1333c0758ba72be38e728b6dadb32ea767de5b4ddbaea1dae85b1b051f",
+ "who": "0xE92329EC7ddB11D25e25b3c21eeBf11f15eB325d",
+ "granted": true
+ }
+]
diff --git a/scripts/permissions-migration/config/agent-transfer-permissions-config.ts b/scripts/permissions-migration/config/agent-transfer-permissions-config.ts
new file mode 100644
index 00000000..cc9ca5d1
--- /dev/null
+++ b/scripts/permissions-migration/config/agent-transfer-permissions-config.ts
@@ -0,0 +1,500 @@
+import { AragonContractPermissionConfigs } from "../src/aragon-permissions";
+import { ManagedContractsConfig } from "../src/managed-contracts";
+import { OZContractRolesConfig } from "../src/oz-roles";
+import { LIDO_CONTRACTS } from "./lido-contracts";
+
+// TODO: Check the completeness of the configs
+
+export const ARAGON_CONTRACT_ROLES_CONFIG: AragonContractPermissionConfigs = {
+ // Core protocol
+ Lido: {
+ address: LIDO_CONTRACTS.Lido,
+ permissions: {
+ STAKING_CONTROL_ROLE: { manager: "None", grantedTo: ["Agent"] },
+ // RESUME_ROLE: { manager: "None" },
+ // PAUSE_ROLE: { manager: "None" },
+ // UNSAFE_CHANGE_DEPOSITED_VALIDATORS_ROLE: { manager: "None" },
+ // STAKING_PAUSE_ROLE: { manager: "None" },
+ },
+ },
+
+ // Oracle Contracts
+ // LegacyOracle: {
+ // address: LIDO_CONTRACTS.LegacyOracle,
+ // permissions: {},
+ // },
+
+ // // DAO Contracts
+ // DAOKernel: {
+ // address: LIDO_CONTRACTS.DAOKernel,
+ // permissions: {
+ // APP_MANAGER_ROLE: { manager: "Agent" },
+ // },
+ // },
+ // Voting: {
+ // address: LIDO_CONTRACTS.Voting,
+ // permissions: {
+ // UNSAFELY_MODIFY_VOTE_TIME_ROLE: { manager: "Voting" },
+ // MODIFY_QUORUM_ROLE: { manager: "Voting", grantedTo: ["Voting"] },
+ // MODIFY_SUPPORT_ROLE: { manager: "Voting", grantedTo: ["Voting"] },
+ // CREATE_VOTES_ROLE: { manager: "Voting", grantedTo: ["TokenManager"] },
+ // },
+ // },
+ // TokenManager: {
+ // address: LIDO_CONTRACTS.TokenManager,
+ // permissions: {
+ // ISSUE_ROLE: { manager: "Voting" },
+ // ASSIGN_ROLE: { manager: "Voting", grantedTo: ["Voting"] },
+ // BURN_ROLE: { manager: "Voting" },
+ // MINT_ROLE: { manager: "Voting" },
+ // REVOKE_VESTINGS_ROLE: { manager: "Voting" },
+ // },
+ // },
+ // Finance: {
+ // address: LIDO_CONTRACTS.Finance,
+ // permissions: {
+ // CREATE_PAYMENTS_ROLE: {
+ // manager: "Voting",
+ // grantedTo: ["Voting", "EasyTrackEvmScriptExecutor"],
+ // },
+ // CHANGE_PERIOD_ROLE: { manager: "Voting" },
+ // CHANGE_BUDGETS_ROLE: { manager: "Voting" },
+ // EXECUTE_PAYMENTS_ROLE: { manager: "Voting", grantedTo: ["Voting"] },
+ // MANAGE_PAYMENTS_ROLE: { manager: "Voting", grantedTo: ["Voting"] },
+ // },
+ // },
+ // Agent: {
+ // address: LIDO_CONTRACTS.Agent,
+ // permissions: {
+ // ADD_PROTECTED_TOKEN_ROLE: { manager: "Voting" },
+ // REMOVE_PROTECTED_TOKEN_ROLE: { manager: "Voting" },
+ // TRANSFER_ROLE: { manager: "Voting", grantedTo: ["Finance"] },
+ // RUN_SCRIPT_ROLE: {
+ // manager: "DualGovernance",
+ // grantedTo: ["DualGovernance"],
+ // },
+ // SAFE_EXECUTE_ROLE: { manager: "None" },
+ // DESIGNATE_SIGNER_ROLE: { manager: "Voting" },
+ // EXECUTE_ROLE: {
+ // manager: "DualGovernance",
+ // grantedTo: ["DualGovernance"],
+ // },
+ // ADD_PRESIGNED_HASH_ROLE: { manager: "Voting" },
+ // },
+ // },
+ // ACL: {
+ // address: LIDO_CONTRACTS.ACL,
+ // permissions: { CREATE_PERMISSIONS_ROLE: { manager: "Agent" } },
+ // },
+ // AragonPM: {
+ // address: LIDO_CONTRACTS.AragonPM,
+ // permissions: {
+ // CREATE_REPO_ROLE: { manager: "None" },
+ // },
+ // },
+ // EVMScriptRegistry: {
+ // address: LIDO_CONTRACTS.EVMScriptRegistry,
+ // permissions: {
+ // REGISTRY_ADD_EXECUTOR_ROLE: { manager: "None" },
+ // REGISTRY_MANAGER_ROLE: { manager: "None" },
+ // },
+ // },
+ // VotingRepo: {
+ // address: LIDO_CONTRACTS.VotingRepo,
+ // permissions: {
+ // CREATE_VERSION_ROLE: { manager: "None" },
+ // },
+ // },
+ // LidoRepo: {
+ // address: LIDO_CONTRACTS.LidoRepo,
+ // permissions: {
+ // CREATE_VERSION_ROLE: { manager: "None" },
+ // },
+ // },
+ // LegacyOracleRepo: {
+ // address: LIDO_CONTRACTS.LegacyOracleRepo,
+ // permissions: {
+ // CREATE_VERSION_ROLE: { manager: "None" },
+ // },
+ // },
+ // CuratedModuleRepo: {
+ // address: LIDO_CONTRACTS.CuratedModuleRepo,
+ // permissions: {
+ // CREATE_VERSION_ROLE: { manager: "None" },
+ // },
+ // },
+ // SimpleDVTRepo: {
+ // address: LIDO_CONTRACTS.SimpleDVTRepo,
+ // permissions: {
+ // CREATE_VERSION_ROLE: { manager: "None" },
+ // },
+ // },
+ // // Staking Modules
+ // CuratedModule: {
+ // address: LIDO_CONTRACTS.CuratedModule,
+ // permissions: {
+ // STAKING_ROUTER_ROLE: { manager: "Agent", grantedTo: ["StakingRouter"] },
+ // MANAGE_NODE_OPERATOR_ROLE: { manager: "Agent", grantedTo: ["Agent"] },
+ // SET_NODE_OPERATOR_LIMIT_ROLE: {
+ // manager: "Agent",
+ // grantedTo: ["EasyTrackEvmScriptExecutor"],
+ // },
+ // MANAGE_SIGNING_KEYS: {
+ // manager: "Agent",
+ // },
+ // },
+ // },
+ // SimpleDVT: {
+ // address: LIDO_CONTRACTS.SimpleDVT,
+ // permissions: {
+ // STAKING_ROUTER_ROLE: {
+ // manager: "Agent",
+ // grantedTo: ["StakingRouter", "EasyTrackEvmScriptExecutor"],
+ // },
+ // MANAGE_NODE_OPERATOR_ROLE: {
+ // manager: "Agent",
+ // grantedTo: ["EasyTrackEvmScriptExecutor"],
+ // },
+ // SET_NODE_OPERATOR_LIMIT_ROLE: {
+ // manager: "Agent",
+ // grantedTo: ["EasyTrackEvmScriptExecutor"],
+ // },
+ // MANAGE_SIGNING_KEYS: {
+ // manager: "EasyTrackEvmScriptExecutor",
+ // grantedTo: ["EasyTrackEvmScriptExecutor"],
+ // },
+ // },
+ // },
+};
+
+export const OZ_CONTRACT_ROLES_CONFIG: OZContractRolesConfig = {
+ // Core Protocol
+ StakingRouter: {
+ address: LIDO_CONTRACTS.StakingRouter,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Agent"],
+ MANAGE_WITHDRAWAL_CREDENTIALS_ROLE: [],
+ REPORT_EXITED_VALIDATORS_ROLE: ["AccountingOracle"],
+ REPORT_REWARDS_MINTED_ROLE: ["Lido"],
+ STAKING_MODULE_MANAGE_ROLE: ["Agent"],
+ STAKING_MODULE_UNVETTING_ROLE: ["DepositSecurityModule"],
+ UNSAFE_SET_EXITED_VALIDATORS_ROLE: [],
+ },
+ },
+ WithdrawalQueueERC721: {
+ address: LIDO_CONTRACTS.WithdrawalQueueERC721,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Agent"],
+ FINALIZE_ROLE: ["Lido"],
+ MANAGE_TOKEN_URI_ROLE: [],
+ ORACLE_ROLE: ["AccountingOracle"],
+ PAUSE_ROLE: ["OraclesGateSeal"],
+ RESUME_ROLE: [],
+ },
+ },
+ Burner: {
+ address: LIDO_CONTRACTS.Burner,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Agent"],
+ REQUEST_BURN_MY_STETH_ROLE: ["Agent"],
+ REQUEST_BURN_SHARES_ROLE: [
+ "Lido",
+ "CuratedModule",
+ "SimpleDVT",
+ "CSAccounting",
+ ],
+ },
+ },
+
+ // Oracle Contracts
+ AccountingOracle: {
+ address: LIDO_CONTRACTS.AccountingOracle,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Agent"],
+ MANAGE_CONSENSUS_CONTRACT_ROLE: [],
+ MANAGE_CONSENSUS_VERSION_ROLE: [],
+ SUBMIT_DATA_ROLE: [],
+ },
+ },
+ AccountingOracleHashConsensus: {
+ address: LIDO_CONTRACTS.AccountingOracleHashConsensus,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Agent"],
+ DISABLE_CONSENSUS_ROLE: [],
+ MANAGE_FAST_LANE_CONFIG_ROLE: [],
+ MANAGE_FRAME_CONFIG_ROLE: [],
+ MANAGE_MEMBERS_AND_QUORUM_ROLE: ["Agent"],
+ MANAGE_REPORT_PROCESSOR_ROLE: [],
+ },
+ },
+ ValidatorExitBusOracle: {
+ address: LIDO_CONTRACTS.ValidatorExitBusOracle,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Agent"],
+ MANAGE_CONSENSUS_CONTRACT_ROLE: [],
+ MANAGE_CONSENSUS_VERSION_ROLE: [],
+ PAUSE_ROLE: ["OraclesGateSeal"],
+ RESUME_ROLE: [],
+ SUBMIT_DATA_ROLE: [],
+ },
+ },
+ ValidatorExitBusHashConsensus: {
+ address: LIDO_CONTRACTS.ValidatorExitBusHashConsensus,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Agent"],
+ DISABLE_CONSENSUS_ROLE: [],
+ MANAGE_FAST_LANE_CONFIG_ROLE: [],
+ MANAGE_FRAME_CONFIG_ROLE: [],
+ MANAGE_MEMBERS_AND_QUORUM_ROLE: ["Agent"],
+ MANAGE_REPORT_PROCESSOR_ROLE: [],
+ },
+ },
+ OracleReportSanityChecker: {
+ address: LIDO_CONTRACTS.OracleReportSanityChecker,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Agent"],
+ ALL_LIMITS_MANAGER_ROLE: [],
+ ANNUAL_BALANCE_INCREASE_LIMIT_MANAGER_ROLE: [],
+ APPEARED_VALIDATORS_PER_DAY_LIMIT_MANAGER_ROLE: [],
+ EXITED_VALIDATORS_PER_DAY_LIMIT_MANAGER_ROLE: [],
+ INITIAL_SLASHING_AND_PENALTIES_MANAGER_ROLE: [],
+ MAX_ITEMS_PER_EXTRA_DATA_TRANSACTION_ROLE: [],
+ MAX_NODE_OPERATORS_PER_EXTRA_DATA_ITEM_ROLE: [],
+ MAX_POSITIVE_TOKEN_REBASE_MANAGER_ROLE: [],
+ MAX_VALIDATOR_EXIT_REQUESTS_PER_REPORT_ROLE: [],
+ REQUEST_TIMESTAMP_MARGIN_MANAGER_ROLE: [],
+ SECOND_OPINION_MANAGER_ROLE: [],
+ SHARE_RATE_DEVIATION_LIMIT_MANAGER_ROLE: [],
+ },
+ },
+ OracleDaemonConfig: {
+ address: LIDO_CONTRACTS.OracleDaemonConfig,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Agent"],
+ CONFIG_MANAGER_ROLE: [],
+ },
+ },
+ // Staking Modules
+ CSModule: {
+ address: LIDO_CONTRACTS.CSModule,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Agent"],
+ MODULE_MANAGER_ROLE: [],
+ PAUSE_ROLE: ["CSGateSeal"],
+ RECOVERER_ROLE: [],
+ REPORT_EL_REWARDS_STEALING_PENALTY_ROLE: ["CSCommitteeMultisig"],
+ RESUME_ROLE: [],
+ SETTLE_EL_REWARDS_STEALING_PENALTY_ROLE: ["EasyTrackEvmScriptExecutor"],
+ STAKING_ROUTER_ROLE: ["StakingRouter"],
+ VERIFIER_ROLE: ["CSVerifier"],
+ },
+ },
+ CSAccounting: {
+ address: LIDO_CONTRACTS.CSAccounting,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Agent"],
+ ACCOUNTING_MANAGER_ROLE: [],
+ MANAGE_BOND_CURVES_ROLE: [],
+ PAUSE_ROLE: ["CSGateSeal"],
+ RECOVERER_ROLE: [],
+ RESET_BOND_CURVE_ROLE: ["CSModule", "CSCommitteeMultisig"],
+ RESUME_ROLE: [],
+ SET_BOND_CURVE_ROLE: ["CSModule", "CSCommitteeMultisig"],
+ },
+ },
+ CSFeeDistributor: {
+ address: LIDO_CONTRACTS.CSFeeDistributor,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Agent"],
+ RECOVERER_ROLE: [],
+ },
+ },
+ CSFeeOracle: {
+ address: LIDO_CONTRACTS.CSFeeOracle,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Agent"],
+ CONTRACT_MANAGER_ROLE: [],
+ MANAGE_CONSENSUS_CONTRACT_ROLE: [],
+ MANAGE_CONSENSUS_VERSION_ROLE: [],
+ PAUSE_ROLE: ["CSGateSeal"],
+ RECOVERER_ROLE: [],
+ RESUME_ROLE: [],
+ SUBMIT_DATA_ROLE: [],
+ },
+ },
+ CSHashConsensus: {
+ address: LIDO_CONTRACTS.CSHashConsensus,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Agent"],
+ DISABLE_CONSENSUS_ROLE: [],
+ MANAGE_FAST_LANE_CONFIG_ROLE: [],
+ MANAGE_FRAME_CONFIG_ROLE: [],
+ MANAGE_MEMBERS_AND_QUORUM_ROLE: ["Agent"],
+ MANAGE_REPORT_PROCESSOR_ROLE: [],
+ },
+ },
+ // Easy Track
+ EasyTrack: {
+ address: LIDO_CONTRACTS.EasyTrack,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Voting"],
+ CANCEL_ROLE: ["Voting"],
+ PAUSE_ROLE: ["Voting", "EmergencyBrakesMultisig"],
+ UNPAUSE_ROLE: ["Voting"],
+ },
+ },
+ AllowedTokensRegistry: {
+ address: LIDO_CONTRACTS.AllowedTokensRegistry,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Voting"],
+ ADD_TOKEN_TO_ALLOWED_LIST_ROLE: ["Voting"],
+ REMOVE_TOKEN_FROM_ALLOWED_LIST_ROLE: ["Voting"],
+ },
+ },
+ // Arbitrum
+ L1ERC20TokenGateway_Arbitrum: {
+ address: LIDO_CONTRACTS.L1ERC20TokenGateway_Arbitrum,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Agent"],
+ DEPOSITS_DISABLER_ROLE: ["Agent", "EmergencyBrakesMultisig"],
+ DEPOSITS_ENABLER_ROLE: ["Agent"],
+ WITHDRAWALS_DISABLER_ROLE: ["Agent", "EmergencyBrakesMultisig"],
+ WITHDRAWALS_ENABLER_ROLE: ["Agent"],
+ },
+ },
+ // Optimism
+ L1TokensBridge_Optimism: {
+ address: LIDO_CONTRACTS.L1TokensBridge_Optimism,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Agent"],
+ DEPOSITS_DISABLER_ROLE: ["Agent", "EmergencyBrakesMultisig"],
+ DEPOSITS_ENABLER_ROLE: ["Agent"],
+ WITHDRAWALS_DISABLER_ROLE: ["Agent", "EmergencyBrakesMultisig"],
+ WITHDRAWALS_ENABLER_ROLE: ["Agent"],
+ },
+ },
+ // Polygon
+ ERC20Predicate_Polygon: {
+ address: LIDO_CONTRACTS.ERC20Predicate_Polygon,
+ roles: {
+ DEFAULT_ADMIN_ROLE: [],
+ MANAGER_ROLE: [],
+ },
+ },
+ // Base
+ L1ERC20TokenBridge_Base: {
+ address: LIDO_CONTRACTS.L1ERC20TokenBridge_Base,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Agent"],
+ DEPOSITS_DISABLER_ROLE: ["Agent", "EmergencyBrakesMultisig"],
+ DEPOSITS_ENABLER_ROLE: ["Agent"],
+ WITHDRAWALS_DISABLER_ROLE: ["Agent", "EmergencyBrakesMultisig"],
+ WITHDRAWALS_ENABLER_ROLE: ["Agent"],
+ },
+ },
+ L1ERC20Bridge_zkSync: {
+ address: LIDO_CONTRACTS.L1ERC20Bridge_zkSync,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Agent"],
+ DEPOSITS_DISABLER_ROLE: ["Agent", "EmergencyBrakesMultisig"],
+ DEPOSITS_ENABLER_ROLE: ["Agent"],
+ WITHDRAWALS_DISABLER_ROLE: ["Agent", "EmergencyBrakesMultisig"],
+ WITHDRAWALS_ENABLER_ROLE: ["Agent"],
+ },
+ },
+ // Mantle
+ L1ERC20TokenBridge_Mantle: {
+ address: LIDO_CONTRACTS.L1ERC20TokenBridge_Mantle,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Agent"],
+ DEPOSITS_DISABLER_ROLE: ["Agent", "EmergencyBrakesMultisig"],
+ DEPOSITS_ENABLER_ROLE: ["Agent"],
+ WITHDRAWALS_DISABLER_ROLE: ["Agent", "EmergencyBrakesMultisig"],
+ WITHDRAWALS_ENABLER_ROLE: ["Agent"],
+ },
+ },
+
+ L1LidoGateway_Scroll: {
+ address: LIDO_CONTRACTS.L1LidoGateway_Scroll,
+ roles: {
+ DEPOSITS_DISABLER_ROLE: ["Agent", "EmergencyBrakesMultisig"],
+ DEPOSITS_ENABLER_ROLE: ["Agent"],
+ WITHDRAWALS_DISABLER_ROLE: ["Agent", "EmergencyBrakesMultisig"],
+ WITHDRAWALS_ENABLER_ROLE: ["Agent"],
+ },
+ },
+
+ // Mode
+ L1ERC20TokenBridge_Mode: {
+ address: LIDO_CONTRACTS.L1ERC20TokenBridge_Mode,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Agent"],
+ DEPOSITS_DISABLER_ROLE: ["Agent", "EmergencyBrakesMultisig"],
+ DEPOSITS_ENABLER_ROLE: ["Agent"],
+ WITHDRAWALS_DISABLER_ROLE: ["Agent", "EmergencyBrakesMultisig"],
+ WITHDRAWALS_ENABLER_ROLE: ["Agent"],
+ },
+ },
+
+ //Zircuit
+ L1ERC20TokenBridge_Zircuit: {
+ address: LIDO_CONTRACTS.L1ERC20TokenBridge_Zircuit,
+ roles: {
+ DEFAULT_ADMIN_ROLE: ["Agent"],
+ DEPOSITS_DISABLER_ROLE: ["Agent", "EmergencyBrakesMultisig"],
+ DEPOSITS_ENABLER_ROLE: ["Agent"],
+ WITHDRAWALS_DISABLER_ROLE: ["Agent", "EmergencyBrakesMultisig"],
+ WITHDRAWALS_ENABLER_ROLE: ["Agent"],
+ },
+ },
+} as const;
+
+export const MANAGED_CONTRACTS: ManagedContractsConfig = {
+ DSM: {
+ address: "0xfFA96D84dEF2EA035c7AB153D8B991128e3d72fD",
+ properties: {
+ owner: { property: "getOwner", managedBy: "Agent" },
+ },
+ },
+ "LidoLocator :: Proxy": {
+ address: "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb",
+ properties: { admin: { property: "proxy__getAdmin", managedBy: "Agent" } },
+ },
+ "StakingRouter :: Proxy": {
+ address: "0xFdDf38947aFB03C621C71b06C9C70bce73f12999",
+ properties: { admin: { property: "proxy__getAdmin", managedBy: "Agent" } },
+ },
+ "WithdrawalQueue :: Proxy": {
+ address: "0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1",
+ properties: { admin: { property: "proxy__getAdmin", managedBy: "Agent" } },
+ },
+ "WithdrawalVault :: Proxy": {
+ address: "0xB9D7934878B5FB9610B3fE8A5e441e8fad7E293f",
+ properties: { admin: { property: "proxy_getAdmin", managedBy: "Agent" } },
+ },
+ "AccountingOracle :: Proxy": {
+ address: "0x852deD011285fe67063a08005c71a85690503Cee",
+ properties: { admin: { property: "proxy__getAdmin", managedBy: "Agent" } },
+ },
+ "ValidatorsExitBusOracle :: Proxy": {
+ address: "0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e",
+ properties: { admin: { property: "proxy__getAdmin", managedBy: "Agent" } },
+ },
+ ScrollL1LidoGateway: {
+ address: "0x6625c6332c9f91f2d27c304e729b86db87a3f504",
+ properties: { owner: { property: "owner", managedBy: "Agent" } },
+ },
+ ScrollProxyAdmin: {
+ address: "0xCC2C53556Bc75217cf698721b29071d6f12628A9",
+ properties: { owner: { property: "owner", managedBy: "Agent" } },
+ },
+ InsuranceFund: {
+ address: "0x8B3f33234ABD88493c0Cd28De33D583B70beDe35",
+ properties: { owner: { property: "owner", managedBy: "Voting" } }, // ??
+ },
+ ZKSync_L1Executor: {
+ address: LIDO_CONTRACTS.L1Executor_zkSync,
+ properties: { owner: { property: "owner", managedBy: "Agent" } },
+ },
+ // TODO: Add missing contracts
+};
diff --git a/scripts/permissions-migration/config/lido-contracts.ts b/scripts/permissions-migration/config/lido-contracts.ts
new file mode 100644
index 00000000..4b23ae18
--- /dev/null
+++ b/scripts/permissions-migration/config/lido-contracts.ts
@@ -0,0 +1,115 @@
+import bytes, { Address } from "../src/bytes";
+
+export const LIDO_GENESIS_BLOCK = 11473216;
+
+export type LidoContractName =
+ | `Unknown(${Address})`
+ | "None"
+ | "DualGovernance"
+ | keyof typeof LIDO_CONTRACTS;
+
+export const LIDO_CONTRACTS = {
+ // Core Protocol
+ LidoLocator: "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb",
+ Lido: "0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034",
+ StakingRouter: "0xFdDf38947aFB03C621C71b06C9C70bce73f12999",
+ DepositSecurityModule: "0xffa96d84def2ea035c7ab153d8b991128e3d72fd",
+ ExecutionLayerRewardsVault: "0x388C818CA8B9251b393131C08a736A67ccB19297",
+ WithdrawalQueueERC721: "0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1",
+ WithdrawalVault: "0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f",
+ Burner: "0xD15a672319Cf0352560eE76d9e89eAB0889046D3",
+ MEVBoostRelayAllowedList: "0xF95f069F9AD107938F6ba802a3da87892298610E",
+
+ // Oracle Contracts
+ AccountingOracle: "0x852deD011285fe67063a08005c71a85690503Cee",
+ AccountingOracleHashConsensus: "0xD624B08C83bAECF0807Dd2c6880C3154a5F0B288",
+ ValidatorExitBusOracle: "0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e",
+ ValidatorExitBusHashConsensus: "0x7FaDB6358950c5fAA66Cb5EB8eE5147De3df355a",
+ OracleReportSanityChecker: "0x6232397ebac4f5772e53285B26c47914E9461E75",
+ OracleDaemonConfig: "0xbf05A929c3D7885a6aeAd833a992dA6E5ac23b09",
+ LegacyOracle: "0x442af784A788A5bd6F42A01Ebe9F287a871243fb",
+
+ // DAO Contracts
+ DAOKernel: "0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc",
+ Voting: "0xdA7d2573Df555002503F29aA4003e398d28cc00f ",
+ Agent: "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c",
+ TokenManager: "0xf73a1260d222f447210581DDf212D915c09a3249",
+ Finance: "0xB9E5CBB9CA5b0d659238807E84D0176930753d86",
+ ACL: "0xfd1E42595CeC3E83239bf8dFc535250e7F48E0bC",
+ AragonPM: "0x0cb113890b04b49455dfe06554e2d784598a29c9",
+ VotingRepo: "0x4ee3118e3858e8d7164a634825bfe0f73d99c792",
+ LidoRepo: "0xF5Dc67E54FC96F993CD06073f71ca732C1E654B1",
+ LegacyOracleRepo: "0xF9339DE629973c60c4d2b76749c81E6F40960E3A",
+ CuratedModuleRepo: "0x0D97E876ad14DB2b183CFeEB8aa1A5C788eB1831",
+ SimpleDVTRepo: "0x2325b0a607808dE42D918DB07F925FFcCfBb2968",
+ OraclesGateSeal: "0x79243345edbe01a7e42edff5900156700d22611c",
+ EVMScriptRegistry: "0x853cc0D5917f49B57B8e9F89e491F5E18919093A",
+
+ // Staking Modules
+ CuratedModule: "0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5",
+ SimpleDVT: "0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433",
+ CSModule: "0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F",
+ CSAccounting: "0x4d72BFF1BeaC69925F8Bd12526a39BAAb069e5Da",
+ CSFeeDistributor: "0xD99CC66fEC647E68294C6477B40fC7E0F6F618D0",
+ CSGateSeal: "0x5cFCa30450B1e5548F140C24A47E36c10CE306F0",
+ CSFeeOracle: "0x4D4074628678Bd302921c20573EEa1ed38DdF7FB",
+ CSHashConsensus: "0x71093efF8D8599b5fA340D665Ad60fA7C80688e4",
+ CSCommitteeMultisig: "0xc52fc3081123073078698f1eac2f1dc7bd71880f",
+ CSVerifier: "0x3dfc50f22aca652a0a6f28a0f892ab62074b5583",
+
+ // Anchor Integration
+ AnchorVault: "0xA2F987A546D4CD1c607Ee8141276876C26b72Bdf",
+ bETH: "0x707f9118e33a9b8998bea41dd0d46f38bb963fc8",
+
+ // EasyTrack
+ EasyTrack: "0xF0211b7660680B49De1A7E9f25C65660F0a13Fea",
+ EasyTrackEvmScriptExecutor: "0xFE5986E06210aC1eCC1aDCafc0cc7f8D63B3F977",
+
+ // Easy Track Factories for token transfers
+ AllowedTokensRegistry: "0x4AC40c34f8992bb1e5E856A448792158022551ca",
+
+ // Arbitrum
+ L1ERC20TokenGateway_Arbitrum: "0x0F25c1DC2a9922304f2eac71DCa9B07E310e8E5a",
+
+ // Optimism
+ TokenRateNotifier_Optimism: "0xe6793B9e4FbA7DE0ee833F9D02bba7DB5EB27823",
+ L1TokensBridge_Optimism: "0x76943C0D61395d8F2edF9060e1533529cAe05dE6",
+
+ // Polygon
+ ERC20Predicate_Polygon: "0x40ec5B33f54e0E8A33A975908C5BA1c14e5BbbDf",
+
+ // Base
+ L1ERC20TokenBridge_Base: "0x9de443AdC5A411E83F1878Ef24C3F52C61571e72",
+
+ // zkSync
+ L1Executor_zkSync: "0xFf7F4d05e3247374e86A3f7231A2Ed1CA63647F2",
+ L1ERC20Bridge_zkSync: "0x41527B2d03844dB6b0945f25702cB958b6d55989",
+
+ // Mantle
+ L1ERC20TokenBridge_Mantle: "0x2D001d79E5aF5F65a939781FE228B267a8Ed468B",
+
+ // Linea
+ L1TokenBridge_Linea: "0x051f1d88f0af5763fb888ec4378b4d8b29ea3319",
+
+ // Scroll
+ L1LidoGateway_Scroll: "0x6625c6332c9f91f2d27c304e729b86db87a3f504",
+
+ // Mode
+ L1ERC20TokenBridge_Mode: "0xD0DeA0a3bd8E4D55170943129c025d3fe0493F2A",
+
+ //Zircuit
+ L1ERC20TokenBridge_Zircuit: "0x912C7271a6A3622dfb8B218eb46a6122aB046C79",
+
+ // Emergency Brakes
+ EmergencyBrakesMultisig: "0x73b047fe6337183A454c5217241D780a932777bD",
+} as const;
+
+export const LIDO_CONTRACTS_NAMES: Record
= {};
+
+for (const [name, address] of Object.entries(LIDO_CONTRACTS)) {
+ LIDO_CONTRACTS_NAMES[bytes.normalize(address)] = name as LidoContractName;
+}
+
+export const CONTRACT_LABELS: Partial> = {
+ EasyTrackEvmScriptExecutor: "ET :: EVMScriptExecutor",
+};
diff --git a/scripts/permissions-migration/contracts/AragonRolesVerifier.sol b/scripts/permissions-migration/contracts/AragonRolesVerifier.sol
new file mode 100644
index 00000000..ea6eaded
--- /dev/null
+++ b/scripts/permissions-migration/contracts/AragonRolesVerifier.sol
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.26;
+
+interface ACL {
+ function hasPermission(address _who, address _where, bytes32 _what) external view returns (bool);
+}
+
+contract AragonRolesVerifier {
+ struct RoleToVerify {
+ address who;
+ bytes32 what;
+ address where;
+ bool granted;
+ }
+
+ address public constant ACL_ADDRESS = 0xfd1E42595CeC3E83239bf8dFc535250e7F48E0bC;
+
+ RoleToVerify[] public rolesToVerify;
+
+ constructor(RoleToVerify[] memory _rolesToVerify) {
+ for (uint256 i = 0; i < _rolesToVerify.length; i++) {
+ rolesToVerify.push(_rolesToVerify[i]);
+ }
+ }
+
+ function verify() public view {
+ ACL acl = ACL(ACL_ADDRESS);
+ for (uint256 i = 0; i < rolesToVerify.length; i++) {
+ bool isPermissionGranted =
+ acl.hasPermission(rolesToVerify[i].who, rolesToVerify[i].where, rolesToVerify[i].what);
+ assert(isPermissionGranted == rolesToVerify[i].granted);
+ }
+ }
+}
diff --git a/scripts/permissions-migration/deploy/deploy-aragon-verifier.ts b/scripts/permissions-migration/deploy/deploy-aragon-verifier.ts
new file mode 100644
index 00000000..54e54c05
--- /dev/null
+++ b/scripts/permissions-migration/deploy/deploy-aragon-verifier.ts
@@ -0,0 +1,63 @@
+import fs from "fs";
+
+import { getAddress, hexlify } from "ethers";
+import { JsonRpcProvider, Wallet, ContractFactory } from "ethers";
+
+export async function deployAragonVerifier(
+ provider: JsonRpcProvider,
+ privateKey?: string,
+ etherscanApiKey?: string,
+) {
+ if (!privateKey) throw new Error("PRIVATE_KEY env variable is missing.");
+ if (!etherscanApiKey)
+ throw new Error("ETHERSCAN_API env variable is missing.");
+
+ const wallet = new Wallet(privateKey, provider);
+ const contractJson = JSON.parse(
+ fs.readFileSync(
+ "./out/AragonRolesVerifier.sol/AragonRolesVerifier.json",
+ "utf8",
+ ),
+ );
+ const contractABI = contractJson.abi;
+ const contractBytecode = contractJson.bytecode;
+
+ const rawData = JSON.parse(
+ fs.readFileSync(
+ "./scripts/permissions-migration/aragon-permissions.json",
+ "utf8",
+ ),
+ );
+
+ console.log(rawData[0]);
+
+ const roles = rawData.map((role: any) => ({
+ who: getAddress(role.who),
+ what: hexlify(role.what),
+ where: getAddress(role.where),
+ granted: role.granted,
+ }));
+
+ (async () => {
+ try {
+ console.log("🚀 Деплоим AragonRolesVerifier...");
+
+ // 📌 Создать контрактный factory
+ const factory = new ContractFactory(
+ contractABI,
+ contractBytecode,
+ wallet,
+ );
+
+ // 📌 Деплоить контракт с массивом структур
+ const contract = await factory.deploy(roles);
+
+ console.log("📡 Ожидание деплоя...");
+ await contract.waitForDeployment();
+
+ console.log(`✅ Контракт развернут по адресу: ${contract.target}`);
+ } catch (error) {
+ console.error("❌ Ошибка при деплое:", error);
+ }
+ })();
+}
diff --git a/scripts/permissions-migration/index.ts b/scripts/permissions-migration/index.ts
new file mode 100644
index 00000000..af7b38ae
--- /dev/null
+++ b/scripts/permissions-migration/index.ts
@@ -0,0 +1,55 @@
+import "dotenv/config";
+import { JsonRpcProvider } from "ethers";
+
+import oz from "./src/oz-roles";
+import aragon from "./src/aragon-permissions";
+import managed from "./src/managed-contracts";
+import { retrieveDeployConfiguration } from "./src/aragon-deploy";
+
+import { aragonGenerateJson } from "./src/aragon-generate-json";
+import { deployAragonVerifier } from "./deploy/deploy-aragon-verifier";
+
+import {
+ ARAGON_CONTRACT_ROLES_CONFIG,
+ MANAGED_CONTRACTS,
+ OZ_CONTRACT_ROLES_CONFIG,
+} from "./config/agent-transfer-permissions-config";
+
+const RPC_URL = process.env.MAINNET_RPC_URL;
+const PRIVATE_KEY = process.env.PRIVATE_KEY;
+const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY;
+
+if (!RPC_URL) {
+ throw new Error("RPC_URL env variable not set");
+}
+
+async function main() {
+ const provider = new JsonRpcProvider(RPC_URL);
+ console.log(`## Lido Permissions Transitions`);
+ const aragonPermissions = await aragon.collectPermissionsData(
+ provider,
+ ARAGON_CONTRACT_ROLES_CONFIG,
+ );
+
+ aragonGenerateJson(aragonPermissions);
+ // deployAragonVerifier(provider, PRIVATE_KEY, ETHERSCAN_API_KEY);
+
+ // const ozPermissions = await oz.collectRolesInfo(
+ // provider,
+ // OZ_CONTRACT_ROLES_CONFIG,
+ // );
+ // const managedContractsInfo = await managed.collectManagedContractsInfo(
+ // provider,
+ // MANAGED_CONTRACTS,
+ // );
+
+ // console.log(aragon.formatContractPermissionsSection(aragonPermissions));
+ // console.log(oz.formatContractRolesSection(ozPermissions));
+ // console.log(managed.formatControlledContractsSection(managedContractsInfo));
+ // console.log(await retrieveDeployConfiguration(provider));
+}
+
+main().catch((error) => {
+ console.error(error);
+ process.exitCode = 1;
+});
diff --git a/scripts/permissions-migration/src/aragon-deploy.ts b/scripts/permissions-migration/src/aragon-deploy.ts
new file mode 100644
index 00000000..7ea232c8
--- /dev/null
+++ b/scripts/permissions-migration/src/aragon-deploy.ts
@@ -0,0 +1,47 @@
+import { ethers, id, JsonRpcProvider, getAddress } from "ethers";
+import { LIDO_CONTRACTS } from "../config/lido-contracts";
+
+import md from "./markdown";
+
+const txHash =
+ "0x3feabd79e8549ad68d1827c074fa7123815c80206498946293d5373a160fd866";
+
+export async function retrieveDeployConfiguration(provider: JsonRpcProvider) {
+ const txReceipt = await provider.getTransactionReceipt(txHash);
+ const iface = new ethers.Interface([
+ "event SetApp(bytes32 indexed namespace, bytes32 indexed appId, address app)",
+ ]);
+
+ const deployedApps: Set = new Set();
+
+ if (!txReceipt) return;
+
+ for (const log of txReceipt.logs) {
+ const mutableLog = { ...log, topics: [...log.topics] };
+ if (mutableLog.topics[0] === id("SetApp(bytes32,bytes32,address)")) {
+ const decoded = iface.parseLog(mutableLog);
+
+ if (decoded) {
+ deployedApps.add(decoded.args[2]);
+ }
+ }
+ }
+
+ for (const lidoContract of Object.entries(LIDO_CONTRACTS)) {
+ const [name, address] = lidoContract;
+ const normalizedAddress = getAddress(address);
+
+ if (deployedApps.has(normalizedAddress)) {
+ deployedApps.delete(normalizedAddress);
+ deployedApps.add(name);
+ }
+ }
+
+ const result = [md.header(["Deployed apps"])];
+
+ for (const app of deployedApps) {
+ result.push(md.row([app]));
+ }
+
+ return result.join("\n");
+}
diff --git a/scripts/permissions-migration/src/aragon-generate-json.ts b/scripts/permissions-migration/src/aragon-generate-json.ts
new file mode 100644
index 00000000..4f054453
--- /dev/null
+++ b/scripts/permissions-migration/src/aragon-generate-json.ts
@@ -0,0 +1,45 @@
+import { writeFileSync } from "fs";
+import { keccak256, toUtf8Bytes } from "ethers";
+import { AragonPermissionsInfo } from "./aragon-permissions";
+import { LIDO_CONTRACTS } from "../config/lido-contracts";
+
+export async function aragonGenerateJson(
+ aragonPermissions: AragonPermissionsInfo,
+) {
+ const result: any = [];
+
+ for (const contract of Object.keys(aragonPermissions)) {
+ const contractAddress = contract as keyof typeof LIDO_CONTRACTS;
+ const modifiedPermissions = aragonPermissions[contract].filter(
+ (permission) => permission.isModified,
+ );
+ if (!modifiedPermissions.length) continue;
+
+ for (const permission of modifiedPermissions) {
+ for (const grantee of permission.holdersToGrantRole) {
+ if (grantee == "DualGovernance") continue;
+ result.push({
+ where: LIDO_CONTRACTS[contractAddress],
+ what: keccak256(toUtf8Bytes(permission.name)),
+ who: LIDO_CONTRACTS[grantee as keyof typeof LIDO_CONTRACTS],
+ granted: true,
+ });
+ }
+
+ for (const grantee of permission.holdersToRevokeRole) {
+ if (grantee == "DualGovernance") continue;
+ result.push({
+ where: LIDO_CONTRACTS[contractAddress],
+ what: keccak256(toUtf8Bytes(permission.name)),
+ who: LIDO_CONTRACTS[grantee as keyof typeof LIDO_CONTRACTS],
+ granted: false,
+ });
+ }
+ }
+ }
+
+ writeFileSync(
+ "./scripts/permissions-migration/aragon-permissions.json",
+ JSON.stringify(result),
+ );
+}
diff --git a/scripts/permissions-migration/src/aragon-permissions.ts b/scripts/permissions-migration/src/aragon-permissions.ts
new file mode 100644
index 00000000..396be772
--- /dev/null
+++ b/scripts/permissions-migration/src/aragon-permissions.ts
@@ -0,0 +1,354 @@
+import { id, JsonRpcProvider } from "ethers";
+
+import md from "./markdown";
+import bytes, { Address, HexStrPrefixed } from "./bytes";
+import {
+ decodeAddress,
+ decodeBool,
+ decodeBytes32,
+ makeContractCall,
+ sortLogs,
+} from "./utils";
+
+import {
+ CONTRACT_LABELS,
+ LIDO_CONTRACTS,
+ LIDO_CONTRACTS_NAMES,
+ LIDO_GENESIS_BLOCK,
+ LidoContractName,
+} from "../config/lido-contracts";
+
+export interface AragonPermissionConfig {
+ manager: LidoContractName;
+ grantedTo?: LidoContractName[];
+}
+
+export interface AragonContractPermissionConfig {
+ address: Address;
+ permissions: Record;
+}
+
+export type AragonContractPermissionConfigs = Record<
+ string,
+ AragonContractPermissionConfig
+>;
+
+export interface AragonPermissionInfo {
+ name: string;
+ isModified: boolean;
+ oldManager: LidoContractName;
+ newManager: LidoContractName;
+ holdersToGrantRole: LidoContractName[];
+ holdersToRevokeRole: LidoContractName[];
+ holderAlreadyGrantedWithRole: LidoContractName[];
+}
+
+export interface AragonPermissionsInfo {
+ [contractName: string]: AragonPermissionInfo[];
+}
+
+interface AragonPermissionHolders {
+ managers: {
+ [app: Address]: {
+ [role: HexStrPrefixed]: Address;
+ };
+ };
+ permissions: {
+ [app: Address]: {
+ [role: HexStrPrefixed]: Address[];
+ };
+ };
+}
+
+function formatContractPermissionsSection(
+ aragonContractsInfo: AragonPermissionsInfo,
+) {
+ const resSectionLines: string[] = ["### Aragon Roles Transition \n"];
+
+ let totalModifiedRoles = 0;
+
+ for (const [contractName, roles] of Object.entries(aragonContractsInfo)) {
+ if (roles.length === 0) continue;
+ resSectionLines.push(`#### ${contractName}\n`);
+ const [modifiedRolesCount, rowsText] = formatPermissionsInfoTable(roles);
+ resSectionLines.push(rowsText);
+ resSectionLines.push("\n");
+ totalModifiedRoles += modifiedRolesCount;
+ }
+
+ resSectionLines.push(`\n **Total Roles Modified: ${totalModifiedRoles}** \n`);
+
+ return resSectionLines.join("\n");
+}
+
+function formatPermissionsInfoTable(aragonRolesInfo: AragonPermissionInfo[]) {
+ const columnHeaders = ["Role", "Manager", "Revoked", "Granted"];
+ const rows: string[][] = [];
+
+ let modifiedRolesCount = 0;
+
+ for (const role of aragonRolesInfo.sort(
+ (a, b) => Number(!a.isModified) - Number(!b.isModified),
+ )) {
+ if (role.isModified) {
+ modifiedRolesCount += 1;
+ }
+
+ const oldManagerLabel = md.label(
+ role.oldManager === "None" ? md.empty() : role.oldManager,
+ );
+ const newManagerLabel = md.label(
+ role.newManager === "None" ? md.empty() : role.newManager,
+ );
+
+ const managerTransition =
+ role.oldManager != role.newManager
+ ? md.bold(`${oldManagerLabel} -> ${newManagerLabel}`)
+ : md.label(oldManagerLabel);
+
+ const unknownRoleHolders = role.holdersToRevokeRole.filter((holderName) =>
+ holderName.startsWith("Unknown"),
+ );
+ const knownRoleHolders = role.holdersToRevokeRole.filter(
+ (holderName) => !holderName.startsWith("Unknown"),
+ );
+
+ const revokedFromItems =
+ role.holdersToRevokeRole.length === 0
+ ? [md.empty()]
+ : knownRoleHolders.map((roleHolder) =>
+ md.bold(md.label(CONTRACT_LABELS[roleHolder] ?? roleHolder)),
+ );
+
+ if (unknownRoleHolders.length > 0) {
+ revokedFromItems.push(
+ md.label(`+${unknownRoleHolders.length} **UNKNOWN** holders`),
+ );
+ }
+
+ const grantedToItems: string[] = [
+ ...role.holderAlreadyGrantedWithRole.map((roleHolder) =>
+ md.label(CONTRACT_LABELS[roleHolder] ?? roleHolder),
+ ),
+ ...role.holdersToGrantRole.map((roleHolder) =>
+ md.bold(CONTRACT_LABELS[roleHolder] ?? md.label(roleHolder)),
+ ),
+ ];
+
+ if (grantedToItems.length === 0) {
+ grantedToItems.push(md.empty());
+ }
+
+ const roleNameText = role.isModified
+ ? md.modified(md.roleName(role.name))
+ : md.unchanged(md.roleName(role.name));
+
+ rows.push([
+ roleNameText,
+ managerTransition,
+ revokedFromItems.join(", "),
+ grantedToItems.join(", "),
+ ]);
+ }
+ return [
+ modifiedRolesCount,
+ [md.header(columnHeaders), ...rows.map((row) => md.row(row))].join("\n"),
+ ] as const;
+}
+
+async function collectPermissionsData(
+ provider: JsonRpcProvider,
+ config: AragonContractPermissionConfigs,
+) {
+ const { managers, permissions } = await fetchACLPermissionsInfo(provider);
+ const aragonContractsInfo: AragonPermissionsInfo = {};
+
+ for (const [contractName, contractInfo] of Object.entries(config)) {
+ const address = bytes.normalize(contractInfo.address);
+ aragonContractsInfo[contractName] = [];
+
+ for (const [permissionName, { manager, grantedTo }] of Object.entries(
+ contractInfo.permissions,
+ )) {
+ const permissionHash = await getPermissionHash(
+ provider,
+ address,
+ permissionName,
+ );
+
+ const newManager = manager;
+ const oldManagerAddress = managers[address]?.[permissionHash];
+ const oldManager = oldManagerAddress
+ ? LIDO_CONTRACTS_NAMES[oldManagerAddress]
+ : "None";
+
+ const currentlyGrantedTo = (
+ permissions[address]?.[permissionHash] ?? []
+ ).map((roleHolderAddress) => {
+ return LIDO_CONTRACTS_NAMES[roleHolderAddress] === undefined
+ ? (`Unknown(${roleHolderAddress})` as LidoContractName)
+ : LIDO_CONTRACTS_NAMES[roleHolderAddress];
+ });
+
+ const holdersToGrantRole = (grantedTo ?? []).filter(
+ (roleHolder) => !currentlyGrantedTo.includes(roleHolder),
+ );
+ const holdersToRevokeRole = currentlyGrantedTo.filter(
+ (roleHolderName) => !(grantedTo ?? []).includes(roleHolderName),
+ );
+ const holderAlreadyGrantedWithRole = (grantedTo ?? []).filter(
+ (roleHolder) => currentlyGrantedTo.includes(roleHolder),
+ );
+
+ aragonContractsInfo[contractName].push({
+ name: permissionName,
+ oldManager,
+ newManager,
+ isModified:
+ newManager !== oldManager ||
+ holdersToGrantRole.length > 0 ||
+ holdersToRevokeRole.length > 0,
+ holdersToGrantRole,
+ holdersToRevokeRole,
+ holderAlreadyGrantedWithRole,
+ });
+ }
+ }
+ return aragonContractsInfo;
+}
+
+async function fetchACLPermissionsInfo(provider: JsonRpcProvider) {
+ const aclPermissionsEvents = await getACLPermissionEvents(provider, {
+ fromBlock: LIDO_GENESIS_BLOCK,
+ });
+
+ const result: AragonPermissionHolders = {
+ managers: {},
+ permissions: {},
+ };
+
+ for (const event of aclPermissionsEvents) {
+ if (event.name === "ChangePermissionManager") {
+ const { app, role, manager } = event.args;
+ if (!result.managers[app]) {
+ result.managers[app] = {};
+ }
+ result.managers[app][role] = manager!;
+ } else if (event.name === "SetPermission") {
+ const { entity, app, role, allowed } = event.args;
+
+ if (!result.permissions[app]) {
+ result.permissions[app] = {};
+ }
+
+ if (!result.permissions[app][role]) {
+ result.permissions[app][role] = [];
+ }
+
+ if (allowed && !result.permissions[app][role].includes(entity!)) {
+ if (result.permissions[app][role].includes(entity!)) {
+ }
+ result.permissions[app][role].push(entity!);
+ } else {
+ if (!result.permissions[app][role].includes(entity!)) {
+ }
+ result.permissions[app][role] = result.permissions[app][role].filter(
+ (e) => e !== entity,
+ );
+ }
+ }
+ // TODO: Check that SetPermissionParams events doesn't impact the granted permissions
+ }
+
+ return result;
+}
+
+async function getACLPermissionEvents(
+ provider: JsonRpcProvider,
+ filterRange?: { fromBlock: number; toBlock?: number },
+) {
+ const setPermissionTopic = id("SetPermission(address,address,bytes32,bool)");
+ const setPermissionParamsTopic = id(
+ "SetPermissionParams(address,address,bytes32,bytes32)",
+ );
+ const changePermissionManagerTopic = id(
+ "ChangePermissionManager(address,bytes32,address)",
+ );
+
+ const filterParams = {
+ address: LIDO_CONTRACTS.ACL,
+ fromBlock: filterRange?.fromBlock,
+ toBlock: filterRange?.toBlock,
+ };
+ const [
+ setPermissionLogs,
+ setPermissionParamsLogs,
+ changePermissionManagerLogs,
+ ] = await Promise.all([
+ provider.getLogs({ ...filterParams, topics: [setPermissionTopic] }),
+ provider.getLogs({ ...filterParams, topics: [setPermissionParamsTopic] }),
+ provider.getLogs({
+ ...filterParams,
+ topics: [changePermissionManagerTopic],
+ }),
+ ]);
+ return sortLogs([
+ ...setPermissionLogs,
+ ...setPermissionParamsLogs,
+ ...changePermissionManagerLogs,
+ ]).map((log) => {
+ const commonEventInfo = {
+ blockNumber: log.blockNumber,
+ index: log.index,
+ transactionIndex: log.transactionIndex,
+ };
+ if (log.topics[0] === setPermissionTopic) {
+ return {
+ ...commonEventInfo,
+ name: "SetPermission",
+ args: {
+ entity: decodeAddress(log.topics[1]),
+ app: decodeAddress(log.topics[2]),
+ role: decodeBytes32(log.topics[3]),
+ allowed: decodeBool(log.data),
+ },
+ } as const;
+ } else if (log.topics[0] === setPermissionParamsTopic) {
+ return {
+ ...commonEventInfo,
+ name: "SetPermissionParams",
+ args: {
+ entity: decodeAddress(log.topics[1]),
+ app: decodeAddress(log.topics[2]),
+ role: decodeBytes32(log.topics[3]),
+ paramsHash: decodeBytes32(log.data),
+ } as const,
+ };
+ } else if (log.topics[0] === changePermissionManagerTopic) {
+ return {
+ ...commonEventInfo,
+ name: "ChangePermissionManager",
+ args: {
+ app: decodeAddress(log.topics[1]),
+ role: decodeBytes32(log.topics[2]),
+ manager: decodeAddress(log.topics[3]),
+ } as const,
+ };
+ } else {
+ throw new Error(`Unexpected event topic ${log.topics[0]}`);
+ }
+ });
+}
+
+async function getPermissionHash(
+ provider: JsonRpcProvider,
+ address: Address,
+ permissionName: string,
+) {
+ return makeContractCall(provider, address, permissionName);
+}
+
+export default {
+ formatContractPermissionsSection,
+ collectPermissionsData,
+};
diff --git a/scripts/permissions-migration/src/bytes.ts b/scripts/permissions-migration/src/bytes.ts
new file mode 100644
index 00000000..cb29f937
--- /dev/null
+++ b/scripts/permissions-migration/src/bytes.ts
@@ -0,0 +1,29 @@
+export type Address = `0x${string}`;
+export type HexStrNonPrefixed = string;
+export type HexStrPrefixed = `0x${HexStrNonPrefixed}`;
+export type HexStr = HexStrPrefixed | HexStrNonPrefixed;
+
+function normalize(bytes: T): HexStrPrefixed {
+ return prefix0x(bytes.toLowerCase() as T);
+}
+
+function prefix0x(bytes: T): HexStrPrefixed {
+ return is0xPrefixed(bytes) ? bytes : (("0x" + bytes) as HexStrPrefixed);
+}
+
+function strip0x(bytes: HexStr): HexStrNonPrefixed {
+ return bytes.startsWith("0x") ? bytes.slice(2) : bytes;
+}
+
+function join(...bytes: HexStr[]): HexStrPrefixed {
+ return prefix0x(bytes.reduce((res, b) => res + strip0x(b), ""));
+}
+
+function is0xPrefixed(bytes: HexStr): bytes is HexStrPrefixed {
+ return bytes.startsWith("0x");
+}
+
+export default {
+ join,
+ normalize,
+};
diff --git a/scripts/permissions-migration/src/comparison.ts b/scripts/permissions-migration/src/comparison.ts
new file mode 100644
index 00000000..bc19b2cc
--- /dev/null
+++ b/scripts/permissions-migration/src/comparison.ts
@@ -0,0 +1 @@
+export async function getContractsToCompareWithOmnubis() {}
diff --git a/scripts/permissions-migration/src/managed-contracts.ts b/scripts/permissions-migration/src/managed-contracts.ts
new file mode 100644
index 00000000..975c9a12
--- /dev/null
+++ b/scripts/permissions-migration/src/managed-contracts.ts
@@ -0,0 +1,123 @@
+import { JsonRpcProvider } from "ethers";
+
+import md from "./markdown";
+import { Address } from "./bytes";
+import { decodeAddress, makeContractCall } from "./utils";
+
+import {
+ CONTRACT_LABELS,
+ LIDO_CONTRACTS_NAMES,
+ LidoContractName,
+} from "../config/lido-contracts";
+
+export type ManagedContractsConfig = Record;
+
+interface ManagedContractConfig {
+ address: Address;
+ properties: Record;
+}
+
+interface ManagedContractProperty {
+ property: string;
+ managedBy: LidoContractName;
+}
+
+interface ManagedContractPropertyInfo {
+ address: Address;
+ isModified: boolean;
+ contractName: string;
+ propertyName: string;
+ propertyGetter: string;
+ oldManagedBy: LidoContractName;
+ newManagedBy: LidoContractName;
+}
+
+function formatControlledContractsSection(
+ managedContractsPropertiesInfo: ManagedContractPropertyInfo[]
+) {
+ const resSectionLines: string[] = ["### Managed Contracts Updates \n"];
+ const [totalModifiedRoles, tableText] = formatControlledContractsTable(
+ managedContractsPropertiesInfo
+ );
+
+ resSectionLines.push(tableText);
+ resSectionLines.push(`\n **Total Roles Modified: ${totalModifiedRoles}** \n`);
+
+ return resSectionLines.join("\n");
+}
+
+function formatControlledContractsTable(
+ managedContractsPropertiesInfo: ManagedContractPropertyInfo[]
+) {
+ const columnHeaders = ["Contract", "Property", "Old Manager", "New Manager"];
+ const rows: string[][] = [];
+
+ let modifiedPropertiesCount = 0;
+ for (const info of managedContractsPropertiesInfo) {
+ if (info.isModified) {
+ modifiedPropertiesCount += 1;
+ }
+ const contractNameText = info.isModified
+ ? md.bold(md.modified(info.contractName))
+ : md.unchanged(info.contractName);
+ const newManagedBy = info.isModified
+ ? md.bold(
+ md.label(CONTRACT_LABELS[info.newManagedBy] ?? info.newManagedBy)
+ )
+ : md.label(CONTRACT_LABELS[info.newManagedBy] ?? info.newManagedBy);
+ rows.push([
+ contractNameText,
+ md.label(info.propertyGetter + "()"),
+ md.label(CONTRACT_LABELS[info.oldManagedBy] ?? info.oldManagedBy),
+ CONTRACT_LABELS[info.oldManagedBy] ?? newManagedBy,
+ ]);
+ }
+
+ return [
+ modifiedPropertiesCount,
+ [md.header(columnHeaders), ...rows.map((row) => md.row(row))].join("\n"),
+ ] as const;
+}
+
+async function collectManagedContractsInfo(
+ provider: JsonRpcProvider,
+ config: ManagedContractsConfig
+) {
+ const controlledContractsInfo: ManagedContractPropertyInfo[] = [];
+
+ for (const [contractName, { address, properties }] of Object.entries(
+ config
+ )) {
+ for (const [propertyName, { property, managedBy }] of Object.entries(
+ properties
+ )) {
+ const currentlyManagedByAddress = decodeAddress(
+ await makeContractCall(provider, address, property)
+ );
+ if (!LIDO_CONTRACTS_NAMES[currentlyManagedByAddress]) {
+ throw new Error(
+ `Unknown lido contract address ${currentlyManagedByAddress}`
+ );
+ }
+ const currentlyManagedBy =
+ LIDO_CONTRACTS_NAMES[currentlyManagedByAddress];
+
+ controlledContractsInfo.push({
+ address,
+ isModified: managedBy !== currentlyManagedBy,
+ contractName,
+ propertyName,
+ propertyGetter: property,
+ newManagedBy: managedBy,
+ oldManagedBy: currentlyManagedBy,
+ });
+ }
+ }
+
+ return controlledContractsInfo;
+}
+
+export default {
+ formatControlledContractsSection,
+ collectManagedContractsInfo,
+};
diff --git a/scripts/permissions-migration/src/markdown.ts b/scripts/permissions-migration/src/markdown.ts
new file mode 100644
index 00000000..8d05f8ca
--- /dev/null
+++ b/scripts/permissions-migration/src/markdown.ts
@@ -0,0 +1,30 @@
+export default {
+ row(items: string[]) {
+ return `| ${items.join(" | ")} |`;
+ },
+ header(columns: string[]) {
+ return [
+ this.row(columns),
+ this.row(Array(columns.length).fill("---")),
+ ].join("\n");
+ },
+ bold(text: string) {
+ return `**${text}**`;
+ },
+ label(text: string) {
+ return `\`${text}\``;
+ },
+ empty() {
+ return "∅";
+ },
+ roleName(rawRoleName: string) {
+ const roleNameParts = rawRoleName.split("_");
+ return roleNameParts.join(" ");
+ },
+ modified(text: string) {
+ return `⚠️ **${text}**`;
+ },
+ unchanged(text: string) {
+ return `${text}`;
+ },
+};
diff --git a/scripts/permissions-migration/src/oz-roles.ts b/scripts/permissions-migration/src/oz-roles.ts
new file mode 100644
index 00000000..1c02f4a6
--- /dev/null
+++ b/scripts/permissions-migration/src/oz-roles.ts
@@ -0,0 +1,295 @@
+import { AbiCoder, ethers, id, JsonRpcProvider } from "ethers";
+
+import md from "./markdown";
+import { makeContractCall, sortLogs } from "./utils";
+import bytes, { Address, HexStrPrefixed } from "./bytes";
+
+import {
+ LidoContractName,
+ LIDO_CONTRACTS_NAMES,
+ LIDO_GENESIS_BLOCK,
+ CONTRACT_LABELS,
+} from "../config/lido-contracts";
+
+export type OZContractRolesConfig = Record;
+
+interface OZContractConfig {
+ address: Address;
+ roles: Record;
+}
+
+interface OZRoleInfo {
+ roleName: string;
+ isModified: boolean;
+ holdersToGrantRole: LidoContractName[];
+ holdersToRevokeRole: LidoContractName[];
+ holderAlreadyGrantedWithRole: LidoContractName[];
+}
+
+interface OZRolesInfo {
+ [contractName: string]: OZRoleInfo[];
+}
+
+interface OZRoleHolders {
+ [contract: Address]: {
+ [role: HexStrPrefixed]: Address[];
+ };
+}
+
+const DEFAULT_ADMIN_ROLE_HASH = ethers.ZeroHash;
+
+function formatContractRolesSection(ozContractsInfo: OZRolesInfo) {
+ const resSectionLines: string[] = ["### OpenZeppelin Roles Transition \n"];
+
+ let totalModifiedRoles = 0;
+
+ for (const [contractName, roles] of Object.entries(ozContractsInfo)) {
+ if (roles.length === 0) continue;
+ resSectionLines.push(`#### ${contractName}\n`);
+ const [modifiedRolesCount, rowsText] = formatRolesInfoTable(roles);
+ resSectionLines.push(rowsText);
+ resSectionLines.push("\n");
+ totalModifiedRoles += modifiedRolesCount;
+ }
+
+ resSectionLines.push(`\n **Total Roles Modified: ${totalModifiedRoles}** \n`);
+
+ return resSectionLines.join("\n");
+}
+
+async function collectRolesInfo(
+ provider: JsonRpcProvider,
+ config: Record
+) {
+ const ozRolesInfo: OZRolesInfo = {};
+
+ const ozRolesHolders = await fetchRoleHolders(provider, config);
+
+ for (const [contractName, { address, roles }] of Object.entries(config)) {
+ ozRolesInfo[contractName] = [];
+
+ for (const [roleName, desiredRoleGrantees] of Object.entries(roles)) {
+ const roleHash = await getRoleHash(provider, address, roleName);
+
+ const currentlyGrantedTo = (ozRolesHolders[address][roleHash] || []).map(
+ (roleHolderAddress) => {
+ if (LIDO_CONTRACTS_NAMES[roleHolderAddress] === undefined) {
+ throw new Error(
+ `Unknown contract with address ${roleHolderAddress}`
+ );
+ }
+ return LIDO_CONTRACTS_NAMES[roleHolderAddress];
+ }
+ );
+
+ const holdersToGrantRole = desiredRoleGrantees.filter(
+ (roleHolder) => !currentlyGrantedTo.includes(roleHolder)
+ );
+ const holdersToRevokeRole = currentlyGrantedTo.filter(
+ (roleHolderName) => !desiredRoleGrantees.includes(roleHolderName)
+ );
+ const holderAlreadyGrantedWithRole = desiredRoleGrantees.filter(
+ (roleHolder) => currentlyGrantedTo.includes(roleHolder)
+ );
+
+ ozRolesInfo[contractName].push({
+ roleName,
+ holdersToGrantRole,
+ holdersToRevokeRole,
+ holderAlreadyGrantedWithRole,
+ isModified:
+ holdersToGrantRole.length > 0 || holdersToRevokeRole.length > 0,
+ });
+ }
+ }
+ return ozRolesInfo;
+}
+
+function formatRolesInfoTable(ozRolesInfo: OZRoleInfo[]) {
+ const columnHeaders = ["Role", "Revoked", "Granted"];
+ const rows: string[][] = [];
+
+ let modifiedRolesCount = 0;
+
+ for (const role of ozRolesInfo.sort(
+ (a, b) => Number(!a.isModified) - Number(!b.isModified)
+ )) {
+ if (role.isModified) {
+ modifiedRolesCount += 1;
+ }
+
+ const revokedFromItems =
+ role.holdersToRevokeRole.length === 0
+ ? [md.empty()]
+ : role.holdersToRevokeRole.map((roleHolder) =>
+ md.bold(md.label(CONTRACT_LABELS[roleHolder] ?? roleHolder))
+ );
+
+ const grantedToItems: string[] = [
+ ...role.holderAlreadyGrantedWithRole.map((roleHolder) =>
+ md.label(CONTRACT_LABELS[roleHolder] ?? roleHolder)
+ ),
+ ...role.holdersToGrantRole.map((roleHolder) =>
+ md.bold(md.label(CONTRACT_LABELS[roleHolder] ?? roleHolder))
+ ),
+ ];
+
+ if (grantedToItems.length === 0) {
+ grantedToItems.push(md.empty());
+ }
+
+ const roleNameText = role.isModified
+ ? md.modified(md.roleName(role.roleName))
+ : md.unchanged(md.roleName(role.roleName));
+
+ rows.push([
+ roleNameText,
+ revokedFromItems.join(", "),
+ grantedToItems.join(", "),
+ ]);
+ }
+ return [
+ modifiedRolesCount,
+ [md.header(columnHeaders), ...rows.map((row) => md.row(row))].join("\n"),
+ ] as const;
+}
+
+async function fetchRoleHolders(
+ provider: JsonRpcProvider,
+ config: Record
+) {
+ const ozRolesHolders: OZRoleHolders = {};
+
+ for (const { address: ozContractAddress } of Object.values(config)) {
+ ozRolesHolders[ozContractAddress] = {};
+
+ const roleGrantedRevokedEvents = await getRoleGrantedRevokedEvents(
+ provider,
+ ozContractAddress,
+ {
+ fromBlock: LIDO_GENESIS_BLOCK,
+ }
+ );
+
+ for (const event of roleGrantedRevokedEvents) {
+ const role = bytes.normalize(event.args.role);
+ const account = bytes.normalize(event.args.account);
+ if (!ozRolesHolders[ozContractAddress][role]) {
+ ozRolesHolders[ozContractAddress][role] = [];
+ }
+
+ if (event.eventName === "RoleGranted") {
+ ozRolesHolders[ozContractAddress][role].push(account);
+ } else if (event.eventName === "RoleRevoked") {
+ ozRolesHolders[ozContractAddress][role] = ozRolesHolders[
+ ozContractAddress
+ ][role].filter((roleHolder) => roleHolder !== account);
+ } else {
+ throw Error(`Unknown event name ${event.eventName}`);
+ }
+ }
+ }
+
+ return ozRolesHolders;
+}
+
+async function checkRoleAdmins(
+ provider: JsonRpcProvider,
+ config: Record
+) {
+ for (const [contractName, { address, roles }] of Object.entries(config)) {
+ const roleNames = Object.keys(roles);
+
+ const roleHashesAndAdmins = await Promise.all(
+ roleNames.map(async (roleName) => {
+ const roleHash = await getRoleHash(provider, address, roleName);
+ return [
+ roleHash,
+ await getRoleAdminHash(provider, address, roleHash),
+ ] as [roleHash: string, roleAdmin: string];
+ })
+ );
+
+ console.log(`Checking role admins for "${contractName}":`);
+ for (let i = 0; i < roleNames.length; ++i) {
+ const [roleHash, roleAdminHash] = roleHashesAndAdmins[i];
+ console.log(` - ${roleNames[i]}(${roleHash}): ${roleAdminHash}`);
+ if (bytes.normalize(roleAdminHash) !== DEFAULT_ADMIN_ROLE_HASH) {
+ throw new Error(`🚨 Unexpected Role Admin`);
+ }
+ }
+ console.log();
+
+ // Protection from the API requests per second limit
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ }
+}
+
+async function getRoleAdminHash(
+ provider: JsonRpcProvider,
+ address: Address,
+ roleHash: string
+) {
+ return makeContractCall(
+ provider,
+ address,
+ "getRoleAdmin",
+ ["bytes32"],
+ [roleHash]
+ );
+}
+
+async function getRoleHash(
+ provider: JsonRpcProvider,
+ address: Address,
+ roleName: string
+) {
+ return makeContractCall(provider, address, roleName);
+}
+
+async function getRoleGrantedRevokedEvents(
+ provider: JsonRpcProvider,
+ contract: Address,
+ filterRange?: { fromBlock: number; toBlock?: number }
+) {
+ const roleGrantedTopic = id("RoleGranted(bytes32,address,address)");
+ const roleRevokedTopic = id("RoleRevoked(bytes32,address,address)");
+ const filterParams = {
+ address: contract,
+ fromBlock: filterRange?.fromBlock,
+ toBlock: filterRange?.toBlock,
+ };
+ const [grantRoleLogs, revokeRoleLogs] = await Promise.all([
+ provider.getLogs({ ...filterParams, topics: [roleGrantedTopic] }),
+ provider.getLogs({ ...filterParams, topics: [roleRevokedTopic] }),
+ ]);
+ return sortLogs([...grantRoleLogs, ...revokeRoleLogs]).map((log) => {
+ if (log.topics.length !== 4) {
+ throw new Error("Unexpected topics length");
+ }
+ return {
+ eventName:
+ log.topics[0] === roleGrantedTopic ? "RoleGranted" : "RoleRevoked",
+ blockNumber: log.blockNumber,
+ index: log.index,
+ transactionIndex: log.transactionIndex,
+ args: {
+ role: log.topics[1],
+ account: AbiCoder.defaultAbiCoder().decode(
+ ["address"],
+ log.topics[2]
+ )[0] as HexStrPrefixed,
+ sender: AbiCoder.defaultAbiCoder().decode(
+ ["address"],
+ log.topics[3]
+ )[0] as HexStrPrefixed,
+ },
+ };
+ });
+}
+
+export default {
+ collectRolesInfo,
+ formatContractRolesSection,
+ checkRoleAdmins,
+};
diff --git a/scripts/permissions-migration/src/utils.ts b/scripts/permissions-migration/src/utils.ts
new file mode 100644
index 00000000..0bd23929
--- /dev/null
+++ b/scripts/permissions-migration/src/utils.ts
@@ -0,0 +1,62 @@
+import { AbiCoder, id, JsonRpcProvider, Log } from "ethers";
+import bytes, { Address, HexStrPrefixed } from "./bytes";
+
+export function startCase(text: string) {
+ return (
+ text
+ // Replace any non-alphanumeric sequences with a space
+ .replace(/([A-Z])/g, " $1") // Insert space before uppercase letters
+ .replace(/[_-]+/g, " ") // Replace underscores and hyphens with spaces
+ .trim() // Remove extra spaces at the start and end
+ .split(/\s+/) // Split by one or more spaces
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) // Capitalize each word
+ .join(" ")
+ ); // Join words with a single space
+}
+
+export async function makeContractCall(
+ provider: JsonRpcProvider,
+ address: Address,
+ methodName: string,
+ argTypes: string[] = [],
+ argValues: any[] = []
+) {
+ const methodId = id(methodName + `(${argTypes.join(",")})`).slice(0, 10);
+ return bytes.normalize(
+ await provider.call({
+ to: address,
+ data:
+ argTypes.length > 0
+ ? bytes.join(
+ methodId,
+ AbiCoder.defaultAbiCoder().encode(argTypes, argValues)
+ )
+ : methodId,
+ })
+ );
+}
+
+export function sortLogs(logs: Log[]) {
+ return logs.sort((a, b) => {
+ if (a.blockNumber !== b.blockNumber) {
+ return a.blockNumber - b.blockNumber; // Sort by block number
+ }
+ return a.index - b.index; // Sort by log index
+ });
+}
+
+export function decodeAddress(encodedAddress: string): Address {
+ return bytes.normalize(
+ AbiCoder.defaultAbiCoder().decode(["address"], encodedAddress)[0]
+ );
+}
+
+export function decodeBool(encodedBool: string): boolean {
+ return AbiCoder.defaultAbiCoder().decode(["bool"], encodedBool)[0] as boolean;
+}
+
+export function decodeBytes32(encodedBytes32: string): HexStrPrefixed {
+ return bytes.normalize(
+ AbiCoder.defaultAbiCoder().decode(["bytes32"], encodedBytes32)[0]
+ );
+}