diff --git a/governance/helpers/multisig.js b/governance/helpers/multisig.js new file mode 100644 index 00000000000..439b91c7901 --- /dev/null +++ b/governance/helpers/multisig.js @@ -0,0 +1,205 @@ +const { ethers } = require('hardhat') +const { networks } = require('@unlock-protocol/networks') +const { getNetwork } = require('@unlock-protocol/hardhat-helpers') +const multisigABI = require('@unlock-protocol/hardhat-helpers/dist/ABIs/multisig2.json') +const multisigOldABI = require('@unlock-protocol/hardhat-helpers/dist/ABIs/multisig.json') +const SafeApiKit = require('@safe-global/api-kit').default + +// custom services URL for network not supported by Safe +const safeServiceURLs = { + 324: 'https://safe-transaction-zksync.safe.global/api', + 1101: 'https://safe-transaction-zkevm.safe.global/api', + 534352: 'https://transaction.safe.scroll.xyz/api', + 59144: 'https://transaction.safe.linea.build/api', +} + +const prodSigners = [ + '0x9d3ea9e9adde71141f4534dB3b9B80dF3D03Ee5f', // cc + '0x4Ce2DD8373ECe0d7baAA16E559A5817CC875b16a', // jg + '0x4011d09a86D0acA8377a4A8baD691F1ACeeCd672', // nf + '0xcFd35259E3A468E7bDF84a95bCddAc0B614A9212', // aa + '0xccb5D94FbfBFDc4953Ca8a114f88773C2fF98e80', // sm + '0x246A13358Fb27523642D86367a51C2aEB137Ac6C', // cr + '0x2785f2a3DDaCfDE5947F1A9D6c878CCD7F885400', // cn + '0x7A23608a8eBe71868013BDA0d900351A83bb4Dc2', // nm + '0x8de33D8204929ceb2F7AA6299d0643a7f6664c9b', // bw +].sort() + +const devSigners = [ + '0x4Ce2DD8373ECe0d7baAA16E559A5817CC875b16a', // jg + '0x246A13358Fb27523642D86367a51C2aEB137Ac6C', // cr + '0x9d3ea9e9adde71141f4534dB3b9B80dF3D03Ee5f', // cc +].sort() + +const getExpectedSigners = async (chainId) => { + const { isTestNetwork } = await getNetwork(chainId) + const expectedSigners = isTestNetwork ? devSigners : prodSigners + return expectedSigners +} + +const logError = (name, chainId, multisig, msg) => + console.log(`[${name} (${chainId})]: ${multisig} ${msg}`) + +const getSafeService = async (chainId) => { + const txServiceUrl = safeServiceURLs[chainId] || null + console.log(`Using Safe Global service at ${txServiceUrl} - chain ${chainId}`) + + const safeService = new SafeApiKit({ + chainId, + txServiceUrl, + }) + + return safeService +} + +const getMultiSigInfo = async (chainId, multisig) => { + const errors = [] + const { isTestNetwork } = networks[chainId] + const expectedSigners = isTestNetwork ? devSigners : prodSigners + const provider = await getProvider(chainId) + const safeService = await getSafeService(chainId) + + const { count } = await safeService.getPendingTransactions(multisig) + if (count) { + errors.push(`${count} pending txs are waiting to be signed`) + } + // the flags to get only un-executed transactions does not work + // filed here https://github.com/safe-global/safe-core-sdk/issues/690 + // const allTxs = await safeService.getAllTransactions(multisig, { + // executed: false, + // trusted: false, + // queued: false, + // }) + + if (!multisig) { + errors.push('Missing multisig') + } else { + const safe = new ethers.Contract(multisig, multisigABI, provider) + const owners = await safe.getOwners() + const policy = await safe.getThreshold() + + if (isTestNetwork && policy < 2) { + errors.push('❌ Policy below 2!') + } + if (!isTestNetwork && policy < 4) { + errors.push( + `❌ Unexpected policy: ${policy}/${owners.length} for 4/${expectedSigners.length} expected` + ) + } + + let extraSigners = owners.filter((x) => !expectedSigners.includes(x)) + if (extraSigners.length > 0) { + errors.push(`❌ Extra signers: ${[...extraSigners].sort()}`) + } + + let missingSigners = expectedSigners.filter((x) => !owners.includes(x)) + if (missingSigners.length > 0) { + errors.push(`❌ Missing signers: ${missingSigners}`) + } + } + return errors +} + +// get the correct provider if chainId is specified +const getProvider = async (chainId) => { + let provider + if (chainId) { + const { publicProvider } = networks[chainId] + provider = new ethers.JsonRpcProvider(publicProvider) + } else { + ;({ provider } = ethers) + ;({ chainId } = await getNetwork()) + } + return { provider, chainId } +} + +// get safeAddress directly from unlock if needed +const getSafeAddress = async (chainId) => { + const { multisig } = networks[chainId] + return multisig +} + +const getSafeVersion = async (safeAddress) => { + const abi = [ + { + inputs: [], + name: 'VERSION', + outputs: [ + { + internalType: 'string', + name: '', + type: 'string', + }, + ], + stateMutability: 'view', + type: 'function', + }, + ] + const safe = await ethers.getContractAt(abi, safeAddress) + try { + const version = await safe.VERSION() + return version + } catch (error) { + return 'old' + } +} + +// mainnet still uses older versions of the safe +const submitTxOldMultisig = async ({ safeAddress, tx, signer }) => { + // encode contract call + const { + contractNameOrAbi, + contractAddress, + functionName, + functionArgs, + calldata, + value, // in ETH + } = tx + + let encodedFunctionCall + if (!calldata) { + const { interface } = await ethers.getContractFactory(contractNameOrAbi) + encodedFunctionCall = interface.encodeFunctionData( + functionName, + functionArgs + ) + } else { + encodedFunctionCall = calldata + } + + console.log( + `Submitting ${functionName} to multisig ${safeAddress} (v: old)...` + ) + + const safe = new ethers.Contract(safeAddress, multisigOldABI, signer) + const txSubmitted = await safe.submitTransaction( + contractAddress, + value || 0, // ETH value + encodedFunctionCall + ) + + // submit to multisig + const receipt = await txSubmitted.wait() + const { transactionHash, events } = receipt + const nonce = events + .find(({ event }) => event === 'Submission') + .args.transactionId.toNumber() + console.log( + `Tx submitted to multisig with id '${nonce}' (txid: ${transactionHash})` + ) + return nonce +} + +module.exports = { + getProvider, + getSafeAddress, + getSafeVersion, + submitTxOldMultisig, + safeServiceURLs, + prodSigners, + devSigners, + getMultiSigInfo, + getExpectedSigners, + logError, + getSafeService, +} diff --git a/governance/package.json b/governance/package.json index 9e0196fde01..ac88e1f30f6 100644 --- a/governance/package.json +++ b/governance/package.json @@ -32,7 +32,7 @@ "test": "hardhat test", "ci": "yarn lint && yarn test", "check": "node ./all_networks unlock:info --quiet", - "check:multisig": "yarn hardhat run scripts/multisig/info.js", + "check:multisig": "yarn hardhat run scripts/multisig/check-all.js", "lint:contracts": "solhint 'contracts/**/*.sol'", "lint:code": "eslint --resolve-plugins-relative-to ../packages/eslint-config .", "lint": "yarn lint:contracts && yarn lint:code" diff --git a/governance/scripts/getters/unlock-info.js b/governance/scripts/getters/unlock-info.js index 160736cf7ff..0f9d7078a8c 100644 --- a/governance/scripts/getters/unlock-info.js +++ b/governance/scripts/getters/unlock-info.js @@ -46,7 +46,7 @@ async function main({ unlockAddress, quiet = false }) { try { nbOwners = (await getOwners({ safeAddress: unlockOwner })).length } catch (error) { - errorLog(`Unlock owner is not the team multisig !`) + errorLog(`Unlock owner is not the team multisig (${safeAddress})!`) } if (nbOwners && !isMultisig) { diff --git a/governance/scripts/multisig/_helpers.js b/governance/scripts/multisig/_helpers.js deleted file mode 100644 index 59ab713d128..00000000000 --- a/governance/scripts/multisig/_helpers.js +++ /dev/null @@ -1,110 +0,0 @@ -const { ethers } = require('hardhat') -const { networks } = require('@unlock-protocol/networks') -const { getNetwork } = require('@unlock-protocol/hardhat-helpers') -const multisigOldABI = require('@unlock-protocol/hardhat-helpers/dist/ABIs/multisig.json') - -// get the correct provider if chainId is specified -const getProvider = async (chainId) => { - let provider - if (chainId) { - const { publicProvider } = networks[chainId] - provider = new ethers.JsonRpcProvider(publicProvider) - } else { - ;({ provider } = ethers) - ;({ chainId } = await getNetwork()) - } - return { provider, chainId } -} - -// custom services URL for network not supported by Safe -const safeServiceURLs = { - 324: 'https://safe-transaction-zksync.safe.global/api', - 1101: 'https://safe-transaction-zkevm.safe.global/api', - 534352: 'https://transaction.safe.scroll.xyz/api', - 59144: 'https://transaction.safe.linea.build/api', -} - -// get safeAddress directly from unlock if needed -const getSafeAddress = async (chainId) => { - const { multisig } = networks[chainId] - return multisig -} - -const getSafeVersion = async (safeAddress) => { - const abi = [ - { - inputs: [], - name: 'VERSION', - outputs: [ - { - internalType: 'string', - name: '', - type: 'string', - }, - ], - stateMutability: 'view', - type: 'function', - }, - ] - const safe = await ethers.getContractAt(abi, safeAddress) - try { - const version = await safe.VERSION() - return version - } catch (error) { - return 'old' - } -} - -// mainnet still uses older versions of the safe -const submitTxOldMultisig = async ({ safeAddress, tx, signer }) => { - // encode contract call - const { - contractNameOrAbi, - contractAddress, - functionName, - functionArgs, - calldata, - value, // in ETH - } = tx - - let encodedFunctionCall - if (!calldata) { - const { interface } = await ethers.getContractFactory(contractNameOrAbi) - encodedFunctionCall = interface.encodeFunctionData( - functionName, - functionArgs - ) - } else { - encodedFunctionCall = calldata - } - - console.log( - `Submitting ${functionName} to multisig ${safeAddress} (v: old)...` - ) - - const safe = new ethers.Contract(safeAddress, multisigOldABI, signer) - const txSubmitted = await safe.submitTransaction( - contractAddress, - value || 0, // ETH value - encodedFunctionCall - ) - - // submit to multisig - const receipt = await txSubmitted.wait() - const { transactionHash, events } = receipt - const nonce = events - .find(({ event }) => event === 'Submission') - .args.transactionId.toNumber() - console.log( - `Tx submitted to multisig with id '${nonce}' (txid: ${transactionHash})` - ) - return nonce -} - -module.exports = { - getProvider, - getSafeAddress, - getSafeVersion, - submitTxOldMultisig, - safeServiceURLs, -} diff --git a/governance/scripts/multisig/check-all.js b/governance/scripts/multisig/check-all.js new file mode 100644 index 00000000000..714f463ff3c --- /dev/null +++ b/governance/scripts/multisig/check-all.js @@ -0,0 +1,21 @@ +const { networks } = require('@unlock-protocol/networks') +const checkMultisig = require('./check') + +async function main() { + for (let chainId in networks) { + if (chainId === 31337) return + await checkMultisig({ chainId }) + } +} + +// execute as standalone +if (require.main === module) { + main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) +} + +module.exports = main diff --git a/governance/scripts/multisig/check.js b/governance/scripts/multisig/check.js new file mode 100644 index 00000000000..9ed6090eb7a --- /dev/null +++ b/governance/scripts/multisig/check.js @@ -0,0 +1,32 @@ +const { getMultiSigInfo, logError } = require('../../helpers/multisig') +const { getNetwork } = require('@unlock-protocol/hardhat-helpers') + +async function main({ chainId, safeAddress }) { + let errors + const { name, multisig, id } = await getNetwork(chainId) + if (!chainId) { + chainId = id + } + if (!safeAddress) { + safeAddress = multisig + } + + try { + errors = await getMultiSigInfo(chainId, safeAddress) + } catch (error) { + errors = [`Couldn't fetch multisig info: ${error.message}`] + } + errors.forEach((error) => logError(name, chainId, multisig, error)) +} + +// execute as standalone +if (require.main === module) { + main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) +} + +module.exports = main diff --git a/governance/scripts/multisig/create.js b/governance/scripts/multisig/create.js index 60f5700dc60..8b368ff82d6 100644 --- a/governance/scripts/multisig/create.js +++ b/governance/scripts/multisig/create.js @@ -1,6 +1,6 @@ const { ethers } = require('hardhat') const { SafeFactory, EthersAdapter } = require('@safe-global/protocol-kit') -const getOwners = require('./owners') +const { getExpectedSigners } = require('../../helpers/multisig') async function main({ owners, threshold = 4 }) { if (owners && owners.length % 2 == 0) { @@ -10,10 +10,9 @@ async function main({ owners, threshold = 4 }) { throw new Error('SAFE SETUP > Threshold is greater than number of owners.') } - // get mainnet owners if needed + // get signers if needed if (!owners) { - const mainnetOwners = await getOwners({ chainId: 1 }) - owners = mainnetOwners + owners = await getExpectedSigners() } const [deployer] = await ethers.getSigners() diff --git a/governance/scripts/multisig/existing.js b/governance/scripts/multisig/existing.js index 0af67f24a3f..77d1b065508 100644 --- a/governance/scripts/multisig/existing.js +++ b/governance/scripts/multisig/existing.js @@ -1,4 +1,4 @@ -const { getSafeAddress } = require('./_helpers') +const { getSafeAddress } = require('../../helpers/multisig') async function main({ chainId }) { const safeAddress = await getSafeAddress(chainId) diff --git a/governance/scripts/multisig/index.js b/governance/scripts/multisig/index.js index a67a250ff60..81266ab6262 100644 --- a/governance/scripts/multisig/index.js +++ b/governance/scripts/multisig/index.js @@ -1,10 +1,8 @@ -const helpers = require('../multisig/_helpers') const getSafeAddress = require('../multisig/existing') const submitTx = require('../multisig/submitTx') const getOwners = require('../multisig/owners') module.exports = { - ...helpers, submitTx, getOwners, getSafeAddress, diff --git a/governance/scripts/multisig/info.js b/governance/scripts/multisig/info.js deleted file mode 100644 index 3964753f535..00000000000 --- a/governance/scripts/multisig/info.js +++ /dev/null @@ -1,106 +0,0 @@ -const { ethers } = require('hardhat') -const { getProvider, safeServiceURLs } = require('./_helpers') -const multisigABI = require('@unlock-protocol/hardhat-helpers/dist/ABIs/multisig2.json') -const { networks } = require('@unlock-protocol/networks') - -const SafeApiKit = require('@safe-global/api-kit').default - -const prodSigners = [ - '0x9d3ea9e9adde71141f4534dB3b9B80dF3D03Ee5f', // cc - '0x4Ce2DD8373ECe0d7baAA16E559A5817CC875b16a', // jg - '0x4011d09a86D0acA8377a4A8baD691F1ACeeCd672', // nf - '0xcFd35259E3A468E7bDF84a95bCddAc0B614A9212', // aa - '0xccb5D94FbfBFDc4953Ca8a114f88773C2fF98e80', // sm - '0x246A13358Fb27523642D86367a51C2aEB137Ac6C', // cr - '0x2785f2a3DDaCfDE5947F1A9D6c878CCD7F885400', // cn - '0x7A23608a8eBe71868013BDA0d900351A83bb4Dc2', // nm - '0x8de33D8204929ceb2F7AA6299d0643a7f6664c9b', // bw -].sort() - -const devSigners = [ - '0x4Ce2DD8373ECe0d7baAA16E559A5817CC875b16a', // jg - '0x246A13358Fb27523642D86367a51C2aEB137Ac6C', // cr - '0x9d3ea9e9adde71141f4534dB3b9B80dF3D03Ee5f', // cc -].sort() - -const getMultiSigInfo = async (chainId, multisig) => { - const errors = [] - const { isTestNetwork } = networks[chainId] - const expectedSigners = isTestNetwork ? devSigners : prodSigners - const provider = await getProvider(chainId) - // get Safe service - const safeService = new SafeApiKit({ - chainId, - txServiceUrl: safeServiceURLs[chainId] || null, - }) - - const { count } = await safeService.getPendingTransactions(multisig) - if (count) { - errors.push(`${count} pending txs are waiting to be signed`) - } - // the flags to get only un-executed transactions does not work - // filed here https://github.com/safe-global/safe-core-sdk/issues/690 - // const allTxs = await safeService.getAllTransactions(multisig, { - // executed: false, - // trusted: false, - // queued: false, - // }) - - if (!multisig) { - errors.push('Missing multisig') - } else { - const safe = new ethers.Contract(multisig, multisigABI, provider) - const owners = await safe.getOwners() - const policy = await safe.getThreshold() - - if (isTestNetwork && policy < 2) { - errors.push('❌ Policy below 2!') - } - if (!isTestNetwork && policy < 4) { - errors.push( - `❌ Unexpected policy: ${policy}/${owners.length} for 4/${expectedSigners.length} expected` - ) - } - - let extraSigners = owners.filter((x) => !expectedSigners.includes(x)) - if (extraSigners.length > 0) { - errors.push(`❌ Extra signers: ${[...extraSigners].sort()}`) - } - - let missingSigners = expectedSigners.filter((x) => !owners.includes(x)) - if (missingSigners.length > 0) { - errors.push(`❌ Missing signers: ${missingSigners}`) - } - } - return errors -} - -const log = (name, chainId, multisig, msg) => - console.log(`[${name} (${chainId})]: ${multisig} ${msg}`) - -async function main() { - for (let chainId in networks) { - let errors - if (chainId === 31337) return - const { multisig, name } = networks[chainId] - - try { - errors = await getMultiSigInfo(chainId, multisig) - } catch (error) { - errors = [`Couldn't fetch multisig info: ${error.message}`] - } - errors.forEach((error) => log(name, chainId, multisig, error)) - } -} - -// execute as standalone -if (require.main === module) { - main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error) - process.exit(1) - }) -} - -module.exports = main diff --git a/governance/scripts/multisig/owners.js b/governance/scripts/multisig/owners.js index b007b0d8932..a4fa31f9717 100644 --- a/governance/scripts/multisig/owners.js +++ b/governance/scripts/multisig/owners.js @@ -1,5 +1,5 @@ const { ethers } = require('hardhat') -const { getProvider, getSafeAddress } = require('./_helpers') +const { getProvider, getSafeAddress } = require('../../helpers/multisig') const multisigABI = require('@unlock-protocol/hardhat-helpers/dist/ABIs/multisig.json') const { getNetwork } = require('@unlock-protocol/hardhat-helpers') diff --git a/governance/scripts/multisig/submitTx.js b/governance/scripts/multisig/submitTx.js index 37ea2b0dedd..9f805eed19d 100644 --- a/governance/scripts/multisig/submitTx.js +++ b/governance/scripts/multisig/submitTx.js @@ -4,16 +4,15 @@ const { getSafeVersion, submitTxOldMultisig, confirmMultisigTx, - safeServiceURLs, -} = require('./_helpers') + getSafeService, +} = require('../../helpers/multisig') const { ADDRESS_ZERO, getNetwork } = require('@unlock-protocol/hardhat-helpers') const { EthersAdapter } = require('@safe-global/protocol-kit') const Safe = require('@safe-global/protocol-kit').default -const SafeApiKit = require('@safe-global/api-kit').default async function main({ safeAddress, tx, signer }) { - const { chainId, id } = await getNetwork() + const { id: chainId } = await getNetwork() if (!safeAddress) { safeAddress = getSafeAddress(chainId) } @@ -41,13 +40,7 @@ async function main({ safeAddress, tx, signer }) { }) // get Safe service URL if not default - const txServiceUrl = safeServiceURLs[id] - console.log(`Using Safe Global service at ${txServiceUrl} - chain ${id}`) - - const safeService = new SafeApiKit({ - chainId: id, - txServiceUrl: txServiceUrl || null, - }) + const safeService = await getSafeService(chainId) // create tx const safeSdk = await Safe.create({ ethAdapter, safeAddress }) diff --git a/governance/tasks/safe.js b/governance/tasks/safe.js index 6bc9e22fa21..a5c9c1e436a 100644 --- a/governance/tasks/safe.js +++ b/governance/tasks/safe.js @@ -99,3 +99,12 @@ task('safe:add-owner', 'Submit a new owner to a multisig') const addOwner = require('../scripts/multisig/addOwner') await addOwner({ safeAddress, newOwner }) }) + +task('safe:check', 'Check the state of a multisig') + .addOptionalParam('chainId', 'The id of the chain where multisig is deployed') + .addOptionalParam('safeAddress', 'the address of the multisig contract') + .setAction(async ({ chainId, safeAddress }) => { + // eslint-disable-next-line global-require + const check = require('../scripts/multisig/check') + await check({ chainId, safeAddress }) + })