Skip to content

Commit

Permalink
feat(governance): use listed signers when creating a multisig (#13490)
Browse files Browse the repository at this point in the history
* move multisig helpers to `helpers` folder

* adapt create safe to use epected signers

* add command to check a single safe

* remove helpres exports from index file

* fix linea service url

* show safe address in error message

* Update governance/helpers/multisig.js

---------

Co-authored-by: Julien Genestoux <julien.genestoux@gmail.com>
  • Loading branch information
clemsos and julien51 committed Mar 20, 2024
1 parent ede08ec commit a8e9799
Show file tree
Hide file tree
Showing 13 changed files with 278 additions and 237 deletions.
205 changes: 205 additions & 0 deletions 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,
}
2 changes: 1 addition & 1 deletion governance/package.json
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion governance/scripts/getters/unlock-info.js
Expand Up @@ -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) {
Expand Down
110 changes: 0 additions & 110 deletions governance/scripts/multisig/_helpers.js

This file was deleted.

21 changes: 21 additions & 0 deletions 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
32 changes: 32 additions & 0 deletions 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

0 comments on commit a8e9799

Please sign in to comment.