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] + ); +}