Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(governance): use listed signers when creating a multisig #13490

Merged
merged 7 commits into from Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
julien51 marked this conversation as resolved.
Show resolved Hide resolved
'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