diff --git a/scripts/helpers/assert.js b/scripts/helpers/assert.js index 393245302..86c93d32b 100644 --- a/scripts/helpers/assert.js +++ b/scripts/helpers/assert.js @@ -13,6 +13,17 @@ assert.addressEqual = (actual, expected, msg) => { assert.equal(toChecksumAddress(actual), toChecksumAddress(expected), msg) } +assert.arrayOfAddressesEqual = (actual, expected, msg) => { + assert.equal(actual.length, expected.length, msg) + + const actualSorted = [...actual].sort() + const expectedSorted = [...expected].sort() + + for (let i = 0; i < actual.length; i++) { + assert.equal(toChecksumAddress(actualSorted[i]), toChecksumAddress(expectedSorted[i]), msg) + } +} + assert.hexEqual = (actual, expected, msg) => { assert.isTrue(isHexStrict(actual), `Actual string ${actual} is not a valid hex string`) assert.isTrue(isHexStrict(expected), `Expected string ${expected} is not a valid hex string`) diff --git a/scripts/scratch/20-check-dao.js b/scripts/scratch/20-check-dao.js index fb460f161..f650bee9a 100644 --- a/scripts/scratch/20-check-dao.js +++ b/scripts/scratch/20-check-dao.js @@ -1,4 +1,5 @@ const path = require('path') +const fs = require('fs') const chalk = require('chalk') const BN = require('bn.js') const { assertBn } = require('@aragon/contract-helpers-test/src/asserts') @@ -13,6 +14,7 @@ const { assertLastEvent, assertSingleEvent } = require('../helpers/events') const { assert } = require('../helpers/assert') const { percentToBP } = require('../helpers/index') const { resolveEnsAddress } = require('../components/ens') +const { isAddress } = require('web3-utils') const { APP_NAMES } = require('../constants') @@ -34,6 +36,8 @@ const STETH_TOKEN_DECIMALS = 18 const ZERO_WITHDRAWAL_CREDENTIALS = '0x0000000000000000000000000000000000000000000000000000000000000000' const PROTOCOL_PAUSED_AFTER_DEPLOY = true +const OSSIFIABLE_PROXY = 'OssifiableProxy' +const ACCESS_CONTROL_ENUMERABLE = 'AccessControlEnumerable' const DAO_LIVE = /^true|1$/i.test(process.env.DAO_LIVE) @@ -135,7 +139,7 @@ async function checkDAO({ web3, artifacts }) { log.splitter() - await assertDaoPermissions( + await assertAragonPermissions( { kernel: dao, lido, @@ -170,6 +174,14 @@ async function checkDAO({ web3, artifacts }) { log.splitter() + const permissionsConfig = JSON.parse(fs.readFileSync('./scripts/scratch/checks/scratch-deploy-permissions.json')) + await assertNonAragonPermissions(state, permissionsConfig) + + log.splitter() + + await assertHashConsensusMembers(state.hashConsensusForAccountingOracle.address, []) + await assertHashConsensusMembers(state.hashConsensusForValidatorsExitBusOracle.address, []) + console.log(`Total gas used during scratch deployment: ${state.initialDeployTotalGasUsed}`) } @@ -397,7 +409,7 @@ async function assertDAOConfig({ ) } -async function assertDaoPermissions({ kernel, lido, legacyOracle, nopsRegistry, agent, finance, tokenManager, voting, burner, stakingRouter }, fromBlock = 4532202) { +async function assertAragonPermissions({ kernel, lido, legacyOracle, nopsRegistry, agent, finance, tokenManager, voting, burner, stakingRouter }, fromBlock = 4532202) { const aclAddress = await kernel.acl() const acl = await artifacts.require('ACL').at(aclAddress) const allAclEvents = await acl.getPastEvents('allEvents', { fromBlock }) @@ -583,4 +595,90 @@ async function assertDaoPermissions({ kernel, lido, legacyOracle, nopsRegistry, }) } +function addressFromStateField(state, fieldOrAddress) { + if (isAddress(fieldOrAddress)) { + return fieldOrAddress + } + + if (state[fieldOrAddress] === undefined) { + throw new Error(`There is no field "${fieldOrAddress}" in state`) + } + + if (state[fieldOrAddress].address) { + return state[fieldOrAddress].address + } else if (state[fieldOrAddress].proxy.address) { + return state[fieldOrAddress].proxy.address + } else { + throw new Error(`Cannot get address for contract field "${fieldOrAddress}" from state file`) + } +} + +function getRoleBytes32ByName(roleName) { + if (roleName === 'DEFAULT_ADMIN_ROLE') { + return '0x0000000000000000000000000000000000000000000000000000000000000000' + } else { + return web3.utils.keccak256(roleName) + } +} + +async function assertHashConsensusMembers(hashConsensusAddress, expectedMembers) { + const hashConsensus = await artifacts.require('HashConsensus').at(hashConsensusAddress) + const actualMembers = (await hashConsensus.getMembers()).addresses + assert.log( + assert.arrayOfAddressesEqual, + actualMembers, + expectedMembers, + `HashConsensus ${hashConsensusAddress} members are expected: [${expectedMembers.toString()}]` + ) +} + +async function assertNonAragonPermissions(state, permissionsConfig) { + for (const [stateField, permissionTypes] of Object.entries(permissionsConfig)) { + for (const [contractType, permissionParams] of Object.entries(permissionTypes)) { + const contract = await artifacts.require(contractType).at(addressFromStateField(state, stateField)) + if (contractType == OSSIFIABLE_PROXY) { + const actualAdmin = await contract.proxy__getAdmin() + assert.log( + assert.addressEqual, + actualAdmin, + addressFromStateField(state, permissionParams.admin), + `${stateField} ${contractType} admin is ${actualAdmin}` + ) + } else if (contractType == ACCESS_CONTROL_ENUMERABLE) { + for (const [role, theHolders] of Object.entries(permissionParams.roles)) { + const roleHash = getRoleBytes32ByName(role) + const actualRoleMemberCount = await contract.getRoleMemberCount(roleHash) + assert.log( + assert.bnEqual, + theHolders.length, + actualRoleMemberCount, + `Contract ${stateField} ${contractType} has correct number of ${role} holders` + ) + for (const holder of theHolders) { + assert.log(assert.equal, true, + await contract.hasRole(roleHash, addressFromStateField(state, holder)), + `Contract ${stateField} ${contractType} has role ${role} holer ${holder}`) + } + } + } else if (permissionParams.specificViews !== undefined) { + for (const [methodName, expectedValue] of Object.entries(permissionParams.specificViews)) { + const actualValue = await contract[methodName].call() + if (isAddress(actualValue)) { + assert.log( + assert.addressEqual, + actualValue, + addressFromStateField(state, expectedValue), + `${stateField} ${contractType} ${methodName} is ${actualValue}` + ) + } else { + throw new Error(`Unsupported view ${methodName} result type of ${expectedValue} of contract ${stateField}`) + } + } + } else { + throw new Error(`Unsupported ACL contract type "${contractType}"`) + } + } + } +} + module.exports = runOrWrapScript(checkDAO, module) diff --git a/scripts/scratch/checks/scratch-deploy-permissions.json b/scripts/scratch/checks/scratch-deploy-permissions.json new file mode 100644 index 000000000..96c2f032c --- /dev/null +++ b/scripts/scratch/checks/scratch-deploy-permissions.json @@ -0,0 +1,132 @@ +{ + "lidoLocator": { + "OssifiableProxy": { + "admin": "app:aragon-agent" + } + }, + "burner": { + "AccessControlEnumerable": { + "roles": { + "DEFAULT_ADMIN_ROLE": ["app:aragon-agent"], + "REQUEST_BURN_MY_STETH_ROLE": [], + "REQUEST_BURN_SHARES_ROLE": ["app:lido", "app:node-operators-registry"] + } + } + }, + "stakingRouter": { + "AccessControlEnumerable": { + "roles": { + "DEFAULT_ADMIN_ROLE": ["app:aragon-agent"], + "MANAGE_WITHDRAWAL_CREDENTIALS_ROLE": [], + "STAKING_MODULE_PAUSE_ROLE": ["depositSecurityModule"], + "STAKING_MODULE_RESUME_ROLE": ["depositSecurityModule"], + "STAKING_MODULE_MANAGE_ROLE": [], + "REPORT_EXITED_VALIDATORS_ROLE": ["accountingOracle"], + "UNSAFE_SET_EXITED_VALIDATORS_ROLE": [], + "REPORT_REWARDS_MINTED_ROLE": ["app:lido"] + } + }, + "OssifiableProxy": { + "admin": "app:aragon-agent" + } + }, + "withdrawalQueueERC721": { + "AccessControlEnumerable": { + "roles": { + "DEFAULT_ADMIN_ROLE": ["app:aragon-agent"], + "PAUSE_ROLE": ["gateSeal"], + "RESUME_ROLE": [], + "FINALIZE_ROLE": ["app:lido"], + "ORACLE_ROLE": ["accountingOracle"], + "MANAGE_TOKEN_URI_ROLE": [] + } + }, + "OssifiableProxy": { + "admin": "app:aragon-agent" + } + }, + "accountingOracle": { + "AccessControlEnumerable": { + "roles": { + "DEFAULT_ADMIN_ROLE": ["app:aragon-agent"], + "SUBMIT_DATA_ROLE": [], + "MANAGE_CONSENSUS_CONTRACT_ROLE": [], + "MANAGE_CONSENSUS_VERSION_ROLE": [] + } + }, + "OssifiableProxy": { + "admin": "app:aragon-agent" + } + }, + "validatorsExitBusOracle": { + "AccessControlEnumerable": { + "roles": { + "DEFAULT_ADMIN_ROLE": ["app:aragon-agent"], + "SUBMIT_DATA_ROLE": [], + "PAUSE_ROLE": ["gateSeal"], + "RESUME_ROLE": [], + "MANAGE_CONSENSUS_CONTRACT_ROLE": [], + "MANAGE_CONSENSUS_VERSION_ROLE": [] + } + }, + "OssifiableProxy": { + "admin": "app:aragon-agent" + } + }, + "hashConsensusForAccountingOracle": { + "AccessControlEnumerable": { + "roles": { + "DEFAULT_ADMIN_ROLE": ["app:aragon-agent"], + "MANAGE_MEMBERS_AND_QUORUM_ROLE": [], + "DISABLE_CONSENSUS_ROLE": [], + "MANAGE_FRAME_CONFIG_ROLE": [], + "MANAGE_FAST_LANE_CONFIG_ROLE": [], + "MANAGE_REPORT_PROCESSOR_ROLE": [] + } + } + }, + "hashConsensusForValidatorsExitBusOracle": { + "AccessControlEnumerable": { + "roles": { + "DEFAULT_ADMIN_ROLE": ["app:aragon-agent"], + "MANAGE_MEMBERS_AND_QUORUM_ROLE": [], + "DISABLE_CONSENSUS_ROLE": [], + "MANAGE_FRAME_CONFIG_ROLE": [], + "MANAGE_FAST_LANE_CONFIG_ROLE": [], + "MANAGE_REPORT_PROCESSOR_ROLE": [] + } + } + }, + "oracleReportSanityChecker": { + "AccessControlEnumerable": { + "roles": { + "DEFAULT_ADMIN_ROLE": ["app:aragon-agent"], + "ALL_LIMITS_MANAGER_ROLE": [], + "CHURN_VALIDATORS_PER_DAY_LIMIT_MANAGER_ROLE": [], + "ONE_OFF_CL_BALANCE_DECREASE_LIMIT_MANAGER_ROLE": [], + "ANNUAL_BALANCE_INCREASE_LIMIT_MANAGER_ROLE": [], + "SHARE_RATE_DEVIATION_LIMIT_MANAGER_ROLE": [], + "MAX_VALIDATOR_EXIT_REQUESTS_PER_REPORT_ROLE": [], + "MAX_ACCOUNTING_EXTRA_DATA_LIST_ITEMS_COUNT_ROLE": [], + "MAX_NODE_OPERATORS_PER_EXTRA_DATA_ITEM_COUNT_ROLE": [], + "REQUEST_TIMESTAMP_MARGIN_MANAGER_ROLE": [], + "MAX_POSITIVE_TOKEN_REBASE_MANAGER_ROLE": [] + } + } + }, + "depositSecurityModule": { + "DepositSecurityModule": { + "specificViews": { + "getOwner": "app:aragon-agent" + } + } + }, + "oracleDaemonConfig": { + "AccessControlEnumerable": { + "roles": { + "DEFAULT_ADMIN_ROLE": ["app:aragon-agent"], + "CONFIG_MANAGER_ROLE": [] + } + } + } +} \ No newline at end of file