From 77625950dd9803963cdaaaf782a3825aa4d69321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Tue, 5 Mar 2024 14:54:12 +0100 Subject: [PATCH 01/13] add README --- governance/README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/governance/README.md b/governance/README.md index aa8f451c74c..86c99c4a4ff 100644 --- a/governance/README.md +++ b/governance/README.md @@ -234,3 +234,49 @@ Edit directly the amounts and prices in the script ``` yarn run scripts/uniswap/addLiquidity.js ``` + +## Cross-Chain DAO Proposals + +To maintain the integrity of the protocol accross various chains, we use a pattern of DAO proposals that allows execution on multiple chains. Messaging is sent accross the [Connext bridge](https://connext.network) to all supported chains. + +### Prepare a cross-chain proposal + +#### Write a cross-chain DAO proposal + +Read the explanations and follow the template in [`./proposals/006-cross-bridge-proposal.js`](./proposals/006-cross-bridge-proposal.js) to submit a cross-chain proposal to the DAO. + +#### Test a cross-chain DAO proposal + +To make sure all calls can be executed properly, you can use Tenderly forks to test execution of calls on each destination chains. + +### After proposal execution + +#### Pay bridge fees + +Each transaction contained in the proposal is sent separately to the bridge. For each tx, the bridge fee needs to be paid on origin chain (mainnet) for the txs to proceed. To pay all fees for the bridge, use the following script: + +``` +# update the txId accordingly +yarn hardhat run scripts/bridge/payFee.js --network mainnet +``` + +#### Check status of the calls + +You can check the status of all calls on various chains manually with the [Connext explorer](https://connextscan.io/) or directly parse calls from the execution tx using the following script: + +``` +# update the txId accordingly +yarn hardhat run scripts/bridge/status.js --network mainnet +``` + +NB: This will create a temporary JSON file named `xcalled.json.tmp` with the info and statuses of all tx. + +### Execute all tx on destination chains + +Once all calls have crossed the bridges they stay in cooldown in multisigs. Once cooldown ends, they can be executed. To execute the calls, use the following command _for each network_: + +``` +yarn hardhat run scripts/bridge/execTx.js --network optimism +``` + +NB: The tmp file with all txs statuses is required, so you need to first run the "Check status" step above From 3676e45ae600ed906d264b5dbb541adc14f3e42c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Tue, 5 Mar 2024 14:56:42 +0100 Subject: [PATCH 02/13] add script doc --- governance/scripts/bridge/{bump.js => payFee.js} | 2 +- governance/scripts/bridge/status.js | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) rename governance/scripts/bridge/{bump.js => payFee.js} (98%) diff --git a/governance/scripts/bridge/bump.js b/governance/scripts/bridge/payFee.js similarity index 98% rename from governance/scripts/bridge/bump.js rename to governance/scripts/bridge/payFee.js index b21218c1bf8..ffbaf4c2107 100644 --- a/governance/scripts/bridge/bump.js +++ b/governance/scripts/bridge/payFee.js @@ -5,7 +5,7 @@ * yarn hardhat run scripts/bridge/bump.js --network mainnet * * TODO: - * - make cli task to pass args + * - make cli task to pass txIx as args * */ const { ethers } = require('hardhat') diff --git a/governance/scripts/bridge/status.js b/governance/scripts/bridge/status.js index e39bccfb389..8cd7c59a524 100644 --- a/governance/scripts/bridge/status.js +++ b/governance/scripts/bridge/status.js @@ -1,3 +1,12 @@ +/** + * Script to check the status of all bridge calls contained in a specific + * tx + * + * Usage: + * 1. update the `txId` with the DAO proposal execution tx has + * 2. run the script with : + * `yarn hardhat run scripts/bridge/status.js --network mainnet` + */ const { getXCalledEvents, fetchOriginXCall, @@ -15,12 +24,8 @@ async function main({ // TODO: pass this hash via cli txId = '0x12d380bb7f995930872122033988524727a9f847687eede0b4e1fb2dcb8fce68', } = {}) { - console.log(`hello world ${txId}`) - const xCalls = await getXCalledEvents(txId) - console.log() - // sort by domain id const sorted = xCalls.reduce((prev, curr) => { const { destinationDomain } = curr.params From 5c730f57433ad4d76958b0c4ab8e511341607b3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Tue, 5 Mar 2024 17:28:31 +0100 Subject: [PATCH 03/13] remove extra logs from proposal --- governance/proposals/009-protocol-upgrade.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/governance/proposals/009-protocol-upgrade.js b/governance/proposals/009-protocol-upgrade.js index 7a4265bf41b..63686b78af0 100644 --- a/governance/proposals/009-protocol-upgrade.js +++ b/governance/proposals/009-protocol-upgrade.js @@ -110,8 +110,6 @@ const parseCalls = async ({ unlockAddress, name, id }) => { throw Error(`missing contract on chain ${name}(${id})`) } - console.log(`Parsing calls for ${name}(${id}) - Unlock: ${unlockAddress}`) - // submit template to Unlock const { interface: unlockInterface } = await ethers.getContractAt( UnlockV13.abi, @@ -174,10 +172,6 @@ module.exports = async () => { // src info const { id: chainId } = await getNetwork() - console.log( - `from ${chainId} to chains ${targetChains.map(({ id }) => id).join(' - ')}` - ) - const { governanceBridge: { connext: bridgeAddress }, } = networks[chainId] @@ -253,7 +247,6 @@ module.exports = async () => { ) const calls = [...mainnetCalls, ...bridgeCalls] - console.log(calls) // set proposal name and text const proposalName = `Upgrade protocol: switch to Unlock v13 and PublicLock v14 @@ -296,9 +289,9 @@ Onwards ! The Unlock Protocol Team ` - console.log(proposalName) return { proposalName, calls, + explainers, } } From 32e63514d65d8737fdfb7f00c30c5be3f89d5138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Tue, 5 Mar 2024 17:29:16 +0100 Subject: [PATCH 04/13] make sure calls are executed in correct order --- governance/scripts/bridge/_lib.js | 10 +- governance/scripts/bridge/execTx.js | 160 +++++++++++++++++++--------- 2 files changed, 112 insertions(+), 58 deletions(-) diff --git a/governance/scripts/bridge/_lib.js b/governance/scripts/bridge/_lib.js index f6653e05d42..9835c410322 100644 --- a/governance/scripts/bridge/_lib.js +++ b/governance/scripts/bridge/_lib.js @@ -699,13 +699,11 @@ const getDelayModule = async (delayModuleAddress) => { } const logStatus = (transferId, status) => { - const { origin, dest } = status + const { dest } = status const { explorer, name, id } = networks[dest.chainId] - console.log(`To ${name} (${id}) - ${transferId} - - origin (${origin.status}) - tx : ${explorer.urls.transaction( - origin.transactionHash - )} - - dest (${dest.status}) + console.log(`To ${name} (${id}) https://connextscan.io/tx/${transferId} (${ + dest.status + }) - executedTransactionHash: ${explorer.urls.transaction( dest.executedTransactionHash )} diff --git a/governance/scripts/bridge/execTx.js b/governance/scripts/bridge/execTx.js index a0123721287..5466e29b457 100644 --- a/governance/scripts/bridge/execTx.js +++ b/governance/scripts/bridge/execTx.js @@ -10,48 +10,41 @@ const { getDelayModule, logStatus, delayABI } = require('./_lib') const { fetchDataFromTx } = require('../../helpers/tx') const fs = require('fs-extra') +const { resolve } = require('path') +const { ethers } = require('hardhat') + const { getNetwork } = require('@unlock-protocol/hardhat-helpers') +const { loadProposal } = require('../../helpers/gov') + // use cache file to gather tx hashes from calls const filepath = './xcalled.json.tmp' const bigIntToDate = (num) => new Date(parseInt((num * 1000n).toString())) -const getTxStatus = async ({ delayMod, txHash, nextNonce } = {}) => { - const currentNonce = nextNonce - 1n - const { to, value, data, operation } = await fetchDataFromTx({ - txHash, - abi: delayABI, - }) - - // make sure tx is scheduled correctly - const txHashExec = await delayMod.getTransactionHash( - to, - value, - data, - operation - ) - - // verify tx hash from nonce - const txHashFromNonce = await delayMod.getTxHash(currentNonce) - if (txHashFromNonce !== txHashExec) { - console.error(`tx mismatch`) - console.error({ txHashFromNonce, txHashExec }) - } - - const createdAt = await delayMod.txCreatedAt(currentNonce) +const getTxStatus = async (delayMod, nonce) => { + const hash = await delayMod.getTxHash(nonce) + const createdAt = await delayMod.txCreatedAt(nonce) const cooldown = await delayMod.txCooldown() const expiration = await delayMod.txExpiration() - return { - execArgs: [to, value, data, operation], + nonce, + hash, createdAt, cooldown: createdAt + cooldown, expiration: createdAt + expiration, - nonce: currentNonce, } } +const explain = (explainers, args) => { + const exp = explainers.find( + ({ contractAddress, calldata }) => + contractAddress === args[0] && calldata === args[2] + ) + return `to: \`${exp.contractAddress}\` +func: \`${exp.explainer}\`` +} + async function main({ delayModuleAddress, bridgeTxHash, @@ -62,45 +55,108 @@ async function main({ bridgeTxHash = [bridgeTxHash] } + console.log(`\n-------\n`) + if (await fs.exists(filepath)) { // parse statuses from cache file const statuses = await fs.readJSON(filepath) - const { id } = await getNetwork() + const { + id, + name, + governanceBridge: { domainId }, + } = await getNetwork() + + // get original proposal to organize calls in correct order + const proposalPath = resolve('./proposals/009-protocol-upgrade') + const { calls, explainers: allExplainers } = await loadProposal( + proposalPath + ) + const explainers = allExplainers[id] - // prune calls only for the current chain + // unpack calls from proposal + const proposalCalls = calls.filter( + ({ calldata, functionArgs }) => !calldata && functionArgs[0] === domainId + ) + + // compute expected tx hashes from proposal data + const abiCoder = ethers.AbiCoder.defaultAbiCoder() + const computedFromProposal = await Promise.all( + proposalCalls + .map((call) => + // decode args from proposal + abiCoder.decode( + ['address', 'uint256', 'bytes', 'bool'], + call.functionArgs[6] + ) + ) + .map(async (a) => { + // compute expected hash from proposal args + let args = a.toArray() + // parse bool as uint + args[3] = args[3] ? 1n : 0n + const hash = await delayMod.getTransactionHash(...args) + return { hash, args } + }) + ) + + console.log( + `${ + proposalCalls.length + } bridge calls sent to ${name} in the original proposal +${computedFromProposal + .map( + ({ hash, args }, i) => + `[${i}]: +expected hash: \`${hash}\` +${explain(explainers, args)}\n` + ) + .join('\n')}` + ) + console.log(`\n-------\n`) + + // pick bridged calls statuses only for the current chain const transfers = Object.keys(statuses) + .reverse() .map((transferId) => ({ transferId, ...statuses[transferId], })) .filter(({ dest }) => dest.chainId == id) - console.log(`${transfers.length} calls`) - - // get info on calls from multisig - let nonce = currentNonce - const txs = await Promise.all( - transfers.map(async ({ dest: { executedTransactionHash } }) => { - nonce++ // increment nonce - return await getTxStatus({ - delayMod, - txHash: executedTransactionHash, - nextNonce: nonce, - }) - }) - ) - transfers.forEach((transfer, i) => { - console.log('----------------------\n') - logStatus(transfer.transferId, transfer) - const { createdAt, cooldown, expiration } = txs[i] - console.log( - `Status on the multisig delay mod - - createdAt: ${bigIntToDate(createdAt)} - - cooldown: ${bigIntToDate(cooldown)} - - expiration: ${bigIntToDate(expiration)} - ` + const dataFromChain = await Promise.all( + transfers.map(({ dest: { executedTransactionHash } }) => + fetchDataFromTx({ txHash: executedTransactionHash }) ) + ) + console.log(`${transfers.length} txs bridged to ${name} (${id}) \n`) + transfers.map(({ transferId, ...status }, i) => { + console.log(`#### [Connext bridged call ${i}]`) + + logStatus(transferId, status) + const [nonce, hash, ...args] = dataFromChain[i] + console.log(`Containing a \`TransactionAdded\` call to multisig (nonce: ${nonce}) +hash: \`${hash}\` +${explain(explainers, args)}\n`) }) + console.log(`\n-------\n`) + + // get tx hash from nonce + const txHashesFromNonce = await Promise.all( + transfers.map((_, i) => getTxStatus(delayMod, currentNonce + BigInt(i))) + ) + + console.log( + `Txs present in the Delay module at ${await delayMod.getAddress()}:\n`, + txHashesFromNonce + .map( + ({ hash, createdAt, cooldown, expiration, nonce }) => ` + ${hash} (nonce: ${nonce}) + - createdAt: ${bigIntToDate(createdAt)} (${createdAt}) + - cooldown: ${bigIntToDate(cooldown)} + - expiration: ${bigIntToDate(expiration)}` + ) + .join(`\n`) + ) // execute all if (execute) { From 70d0addf6fc943006da0f5da4fec433cc45246f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Wed, 20 Mar 2024 16:22:08 +0100 Subject: [PATCH 05/13] move helpers file to `./helpers` --- governance/{scripts/bridge/_lib.js => helpers/bridge.js} | 0 governance/scripts/bridge/execTx.js | 7 +++++-- governance/scripts/bridge/payFee.js | 2 +- governance/scripts/bridge/status.js | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) rename governance/{scripts/bridge/_lib.js => helpers/bridge.js} (100%) diff --git a/governance/scripts/bridge/_lib.js b/governance/helpers/bridge.js similarity index 100% rename from governance/scripts/bridge/_lib.js rename to governance/helpers/bridge.js diff --git a/governance/scripts/bridge/execTx.js b/governance/scripts/bridge/execTx.js index 5466e29b457..e8b5dce5fc8 100644 --- a/governance/scripts/bridge/execTx.js +++ b/governance/scripts/bridge/execTx.js @@ -7,8 +7,11 @@ * */ -const { getDelayModule, logStatus, delayABI } = require('./_lib') -const { fetchDataFromTx } = require('../../helpers/tx') +const { + getDelayModule, + fetchDataFromTx, + logStatus, +} = require('../../helpers/bridge') const fs = require('fs-extra') const { resolve } = require('path') const { ethers } = require('hardhat') diff --git a/governance/scripts/bridge/payFee.js b/governance/scripts/bridge/payFee.js index ffbaf4c2107..0da71ef4e10 100644 --- a/governance/scripts/bridge/payFee.js +++ b/governance/scripts/bridge/payFee.js @@ -11,7 +11,7 @@ const { ethers } = require('hardhat') const { getNetwork } = require('@unlock-protocol/hardhat-helpers') const submitTx = require('../multisig/submitTx') -const { getXCalledEvents } = require('./_lib') +const { getXCalledEvents } = require('../../helpers/bridge') const fetchRelayerFee = async ({ originDomain, destinationDomain }) => { const res = await fetch( diff --git a/governance/scripts/bridge/status.js b/governance/scripts/bridge/status.js index 8cd7c59a524..9208e423821 100644 --- a/governance/scripts/bridge/status.js +++ b/governance/scripts/bridge/status.js @@ -13,7 +13,7 @@ const { fetchDestinationXCall, getSupportedChainsByDomainId, logStatus, -} = require('./_lib') +} = require('../../helpers/bridge') const fs = require('fs-extra') From 1ad85916f7d5aed4bba250039eb8fa14b16cff2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Wed, 20 Mar 2024 17:28:46 +0100 Subject: [PATCH 06/13] small task to log explainers --- governance/tasks/gov.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/governance/tasks/gov.js b/governance/tasks/gov.js index 7f37242e123..fe34939e62f 100644 --- a/governance/tasks/gov.js +++ b/governance/tasks/gov.js @@ -265,6 +265,7 @@ task('gov:show', 'Show content of proposal') console.log('load from tx') proposal = await parseProposal({ txId, govAddress }) } - console.log(proposal) + const { explainers } = proposal + console.log(explainers) } ) From b0ef6d2d0242757968241514e449c604c3e424a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Wed, 20 Mar 2024 17:30:20 +0100 Subject: [PATCH 07/13] support multicall in safe --- governance/proposals/009-protocol-upgrade.js | 67 ++++++++++---------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/governance/proposals/009-protocol-upgrade.js b/governance/proposals/009-protocol-upgrade.js index 63686b78af0..0c5dee78895 100644 --- a/governance/proposals/009-protocol-upgrade.js +++ b/governance/proposals/009-protocol-upgrade.js @@ -5,6 +5,7 @@ const { ethers } = require('hardhat') const { UnlockV13 } = require('@unlock-protocol/contracts') const { networks } = require('@unlock-protocol/networks') +const { encodeMultiSendData } = require('@safe-global/protocol-kit') const { getProxyAdminAddress, @@ -164,6 +165,22 @@ const parseCalls = async ({ unlockAddress, name, id }) => { return calls } +// +const parseForSafe = async (calls) => { + console.log(calls) + const metaTxs = calls.map(({ contractAddress, calldata }) => ({ + to: contractAddress, + value: 0, + data: calldata, + // TODO? need to fetch if proxy or not ? + operation: 0, // operation: 0 for CALL, 1 for DELEGATECALL + })) + + // pack calls in a single multicall + const multicall = await encodeMultiSendData(metaTxs) + return multicall +} + module.exports = async () => { const targetChains = Object.keys(networks) .filter((id) => Object.keys(deployedContracts).includes(id.toString())) @@ -211,38 +228,24 @@ module.exports = async () => { // store explainers explainers[destChainId] = destCalls - const abiCoder = ethers.AbiCoder.defaultAbiCoder() - await Promise.all( - destCalls.map(async ({ contractAddress, calldata, explainer }) => { - // encode instructions to be executed by the SAFE - const moduleData = await abiCoder.encode( - ['address', 'uint256', 'bytes', 'bool'], - [ - contractAddress, // to - 0, // value - calldata, // data - 0, // operation: 0 for CALL, 1 for DELEGATECALL - // 0, - ] - ) - - // add to the list of calls to be passed to the bridge - bridgeCalls.push({ - contractAddress: bridgeAddress, - contractNameOrAbi: abiIConnext, - functionName: 'xcall', - functionArgs: [ - destDomainId, - destAddress, // destMultisigAddress, - ADDRESS_ZERO, // asset - ADDRESS_ZERO, // delegate - 0, // amount - 30, // slippage - moduleData, // calldata - ], - }) - }) - ) + // parse calls for Safe + const moduleData = await parseForSafe(destCalls) + + // add to the list of calls to be passed to the bridge + bridgeCalls.push({ + contractAddress: bridgeAddress, + contractNameOrAbi: abiIConnext, + functionName: 'xcall', + functionArgs: [ + destDomainId, + destAddress, // destMultisigAddress, + ADDRESS_ZERO, // asset + ADDRESS_ZERO, // delegate + 0, // amount + 30, // slippage + moduleData, // calldata + ], + }) }) ) From 60cb756219cc158c66e08d366ad42b398b45f63b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Tue, 26 Mar 2024 11:29:41 +0100 Subject: [PATCH 08/13] WIP: parse multicall --- governance/helpers/multisig.js | 22 +++++++++++++++++++ governance/proposals/009-protocol-upgrade.js | 21 +++--------------- governance/proposals/010-test-multicall.js | 23 ++++++++++++++++++++ governance/scripts/multisig/submitTx.js | 21 +++++++++++------- 4 files changed, 61 insertions(+), 26 deletions(-) create mode 100644 governance/proposals/010-test-multicall.js diff --git a/governance/helpers/multisig.js b/governance/helpers/multisig.js index 439b91c7901..05117cad665 100644 --- a/governance/helpers/multisig.js +++ b/governance/helpers/multisig.js @@ -4,6 +4,7 @@ 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 +const { encodeMultiSendData } = require('@safe-global/protocol-kit') // custom services URL for network not supported by Safe const safeServiceURLs = { @@ -190,6 +191,26 @@ const submitTxOldMultisig = async ({ safeAddress, tx, signer }) => { return nonce } +// pack multiple calls in a single multicall +const parseSafeMulticall = async (calls) => { + console.log(calls) + const metaTxs = calls.map( + ({ contractAddress, calldata = '0x', value = 0, operation = 0 }) => ({ + to: contractAddress, + value, + data: calldata, + // TODO? need to fetch if proxy or not ? + operation, // operation: 0 for CALL, 1 for DELEGATECALL + }) + ) + + const multicall = await encodeMultiSendData(metaTxs) + + // TODO: how to get multicall address (without a provider) + const multicallAddress = '' + return multicall +} + module.exports = { getProvider, getSafeAddress, @@ -202,4 +223,5 @@ module.exports = { getExpectedSigners, logError, getSafeService, + parseSafeMulticall, } diff --git a/governance/proposals/009-protocol-upgrade.js b/governance/proposals/009-protocol-upgrade.js index 0c5dee78895..3d22425532f 100644 --- a/governance/proposals/009-protocol-upgrade.js +++ b/governance/proposals/009-protocol-upgrade.js @@ -5,7 +5,6 @@ const { ethers } = require('hardhat') const { UnlockV13 } = require('@unlock-protocol/contracts') const { networks } = require('@unlock-protocol/networks') -const { encodeMultiSendData } = require('@safe-global/protocol-kit') const { getProxyAdminAddress, @@ -16,6 +15,8 @@ const { abi: proxyAdminABI, } = require('@unlock-protocol/hardhat-helpers/dist/ABIs/ProxyAdmin.json') +const { parseSafeMulticall } = require('../helpers/multisig') + // TODO: move to hardhat-helpers const abiIConnext = [ { @@ -165,22 +166,6 @@ const parseCalls = async ({ unlockAddress, name, id }) => { return calls } -// -const parseForSafe = async (calls) => { - console.log(calls) - const metaTxs = calls.map(({ contractAddress, calldata }) => ({ - to: contractAddress, - value: 0, - data: calldata, - // TODO? need to fetch if proxy or not ? - operation: 0, // operation: 0 for CALL, 1 for DELEGATECALL - })) - - // pack calls in a single multicall - const multicall = await encodeMultiSendData(metaTxs) - return multicall -} - module.exports = async () => { const targetChains = Object.keys(networks) .filter((id) => Object.keys(deployedContracts).includes(id.toString())) @@ -229,7 +214,7 @@ module.exports = async () => { explainers[destChainId] = destCalls // parse calls for Safe - const moduleData = await parseForSafe(destCalls) + const moduleData = await parseSafeMulticall(destCalls) // add to the list of calls to be passed to the bridge bridgeCalls.push({ diff --git a/governance/proposals/010-test-multicall.js b/governance/proposals/010-test-multicall.js new file mode 100644 index 00000000000..bcb796ba606 --- /dev/null +++ b/governance/proposals/010-test-multicall.js @@ -0,0 +1,23 @@ +const { ethers, ZeroAddress } = require('ethers') +const { parseSafeMulticall } = require('../helpers/multisig') + +module.exports = async () => { + const calls = [ + { + contractAddress: ZeroAddress, + value: ethers.parseEther('0.0001'), + }, + { + contractAddress: ZeroAddress, + value: ethers.parseEther('0.0001'), + }, + ] + + // parse calls for Safe + const packedData = await parseSafeMulticall(calls) + console.log(packedData) + return { + proposalName: 'Test a multicall', + calls: { calldata: packedData }, + } +} diff --git a/governance/scripts/multisig/submitTx.js b/governance/scripts/multisig/submitTx.js index 9f805eed19d..7bc8accdade 100644 --- a/governance/scripts/multisig/submitTx.js +++ b/governance/scripts/multisig/submitTx.js @@ -46,14 +46,19 @@ async function main({ safeAddress, tx, signer }) { const safeSdk = await Safe.create({ ethAdapter, safeAddress }) const txs = !Array.isArray(tx) ? [tx] : tx - const explainer = txs - .map(({ functionName, functionArgs, explainer }) => - explainer - ? explainer - : `'${functionName}(${Object.values(functionArgs).toString()})'` - ) - .join(', ') - console.log(`Submitting txs: ${explainer}`) + let explainer = '' + try { + explainer = txs + .map(({ functionName, functionArgs, explainer }) => + explainer + ? explainer + : `'${functionName}(${Object.values(functionArgs).toString()})'` + ) + .join(', ') + console.log(`Submitting txs: ${explainer}`) + } catch (error) { + console.log(`Missing explainers...`) + } // parse transactions const transactions = await Promise.all( From 2dd5547efdf5fa7479315d8fe0945b7f1f719f35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Wed, 3 Apr 2024 14:04:11 +0200 Subject: [PATCH 09/13] refactor bridge helpers --- governance/helpers/bridge/abis/IConnext.js | 51 ++++ governance/helpers/bridge/abis/IXCalled.js | 124 ++++++++ .../helpers/{bridge.js => bridge/delayMod.js} | 265 +----------------- governance/helpers/bridge/index.js | 11 + governance/helpers/bridge/xCall.js | 153 ++++++++++ governance/helpers/multisig.js | 37 ++- .../proposals/006-cross-bridge-proposal.js | 64 +---- governance/proposals/009-protocol-upgrade.js | 61 +--- 8 files changed, 371 insertions(+), 395 deletions(-) create mode 100644 governance/helpers/bridge/abis/IConnext.js create mode 100644 governance/helpers/bridge/abis/IXCalled.js rename governance/helpers/{bridge.js => bridge/delayMod.js} (64%) create mode 100644 governance/helpers/bridge/index.js create mode 100644 governance/helpers/bridge/xCall.js diff --git a/governance/helpers/bridge/abis/IConnext.js b/governance/helpers/bridge/abis/IConnext.js new file mode 100644 index 00000000000..0d8d00bde42 --- /dev/null +++ b/governance/helpers/bridge/abis/IConnext.js @@ -0,0 +1,51 @@ +module.exports = [ + { + inputs: [ + { + internalType: 'uint32', + name: '_destination', + type: 'uint32', + }, + { + internalType: 'address', + name: '_to', + type: 'address', + }, + { + internalType: 'address', + name: '_asset', + type: 'address', + }, + { + internalType: 'address', + name: '_delegate', + type: 'address', + }, + { + internalType: 'uint256', + name: '_amount', + type: 'uint256', + }, + { + internalType: 'uint256', + name: '_slippage', + type: 'uint256', + }, + { + internalType: 'bytes', + name: '_callData', + type: 'bytes', + }, + ], + name: 'xcall', + outputs: [ + { + internalType: 'bytes32', + name: '', + type: 'bytes32', + }, + ], + stateMutability: 'payable', + type: 'function', + }, +] diff --git a/governance/helpers/bridge/abis/IXCalled.js b/governance/helpers/bridge/abis/IXCalled.js new file mode 100644 index 00000000000..5b6810f93a7 --- /dev/null +++ b/governance/helpers/bridge/abis/IXCalled.js @@ -0,0 +1,124 @@ +module.exports = [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'transferId', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'uint256', + name: 'nonce', + type: 'uint256', + }, + { + indexed: true, + internalType: 'bytes32', + name: 'messageHash', + type: 'bytes32', + }, + { + components: [ + { + internalType: 'uint32', + name: 'originDomain', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'destinationDomain', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'canonicalDomain', + type: 'uint32', + }, + { + internalType: 'address', + name: 'to', + type: 'address', + }, + { + internalType: 'address', + name: 'delegate', + type: 'address', + }, + { + internalType: 'bool', + name: 'receiveLocal', + type: 'bool', + }, + { + internalType: 'bytes', + name: 'callData', + type: 'bytes', + }, + { + internalType: 'uint256', + name: 'slippage', + type: 'uint256', + }, + { + internalType: 'address', + name: 'originSender', + type: 'address', + }, + { + internalType: 'uint256', + name: 'bridgedAmt', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'normalizedIn', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'nonce', + type: 'uint256', + }, + { + internalType: 'bytes32', + name: 'canonicalId', + type: 'bytes32', + }, + ], + indexed: false, + internalType: 'struct TransferInfo', + name: 'params', + type: 'tuple', + }, + { + indexed: false, + internalType: 'address', + name: 'asset', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + indexed: false, + internalType: 'address', + name: 'local', + type: 'address', + }, + { + indexed: false, + internalType: 'bytes', + name: 'messageBody', + type: 'bytes', + }, + ], + name: 'XCalled', + type: 'event', + }, +] diff --git a/governance/helpers/bridge.js b/governance/helpers/bridge/delayMod.js similarity index 64% rename from governance/helpers/bridge.js rename to governance/helpers/bridge/delayMod.js index 9835c410322..62b0c9442ca 100644 --- a/governance/helpers/bridge.js +++ b/governance/helpers/bridge/delayMod.js @@ -1,266 +1,7 @@ const { ethers } = require('hardhat') -const { ADDRESS_ZERO, getNetwork } = require('@unlock-protocol/hardhat-helpers') +const { getNetwork } = require('@unlock-protocol/hardhat-helpers') const { networks } = require('@unlock-protocol/networks') -/*** - * CONNEXT logic - */ -const xCalledABI = [ - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'bytes32', - name: 'transferId', - type: 'bytes32', - }, - { - indexed: true, - internalType: 'uint256', - name: 'nonce', - type: 'uint256', - }, - { - indexed: true, - internalType: 'bytes32', - name: 'messageHash', - type: 'bytes32', - }, - { - components: [ - { - internalType: 'uint32', - name: 'originDomain', - type: 'uint32', - }, - { - internalType: 'uint32', - name: 'destinationDomain', - type: 'uint32', - }, - { - internalType: 'uint32', - name: 'canonicalDomain', - type: 'uint32', - }, - { - internalType: 'address', - name: 'to', - type: 'address', - }, - { - internalType: 'address', - name: 'delegate', - type: 'address', - }, - { - internalType: 'bool', - name: 'receiveLocal', - type: 'bool', - }, - { - internalType: 'bytes', - name: 'callData', - type: 'bytes', - }, - { - internalType: 'uint256', - name: 'slippage', - type: 'uint256', - }, - { - internalType: 'address', - name: 'originSender', - type: 'address', - }, - { - internalType: 'uint256', - name: 'bridgedAmt', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'normalizedIn', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'nonce', - type: 'uint256', - }, - { - internalType: 'bytes32', - name: 'canonicalId', - type: 'bytes32', - }, - ], - indexed: false, - internalType: 'struct TransferInfo', - name: 'params', - type: 'tuple', - }, - { - indexed: false, - internalType: 'address', - name: 'asset', - type: 'address', - }, - { - indexed: false, - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - { - indexed: false, - internalType: 'address', - name: 'local', - type: 'address', - }, - { - indexed: false, - internalType: 'bytes', - name: 'messageBody', - type: 'bytes', - }, - ], - name: 'XCalled', - type: 'event', - }, -] - -const getXCalledEvents = async (hash) => { - const { interface } = await ethers.getContractAt(xCalledABI, ADDRESS_ZERO) - const { logs } = await ethers.provider.getTransactionReceipt(hash) - const parsedLogs = logs.map((log) => { - try { - return interface.parseLog(log) - } catch (error) { - return {} - } - }) - - const xCalled = parsedLogs - .filter((e) => e !== null) - .filter(({ name }) => name === 'XCalled') - .map(({ args }) => args) - - return xCalled -} - -const fetchOriginXCall = async ({ transferIds = [], chainId = 1 }) => { - const query = ` - { - originTransfers(where:{ - transferId_in: ${JSON.stringify(transferIds)} - }) { - chainId - nonce - transferId - to - delegate - receiveLocal - callData - slippage - originSender - originDomain - destinationDomain - transactionHash - bridgedAmt - status - } - } - ` - const { originTransfers } = await fetchXCall({ query, chainId }) - return originTransfers -} - -const fetchDestinationXCall = async ({ transferIds, chainId }) => { - const query = ` - { - destinationTransfers(where:{ - transferId_in: ${JSON.stringify(transferIds)} - }) { - chainId - nonce - transferId - to - delegate - receiveLocal - callData - originDomain - destinationDomain - delegate - status - executedTransactionHash - reconciledTransactionHash - } - } - ` - const { destinationTransfers } = await fetchXCall({ query, chainId }) - return destinationTransfers -} - -// supported chains by domain id -const getSupportedChainsByDomainId = async () => { - return Object.keys(networks) - .map((id) => networks[id]) - .filter( - ({ governanceBridge, isTestNetwork, id }) => - !isTestNetwork && !!governanceBridge && id != 1 - ) - .reduce( - (prev, curr) => ({ - ...prev, - [curr.governanceBridge.domainId]: curr, - }), - {} - ) -} - -const connextSubgraphIds = { - 1: `FfTxiY98LJG6zoiAjCXdT34pAmCKDEP8vZRVuC8D5Gf`, - 137: `7mDXK2K6UfkVXiJMhXU8VEFuh7qi2TwdYxeyaRjkmexo`, //plygon - 10: `3115xfkzXPrYzbqDHTiWGtzRDYNXBxs8dyitva6J18jf`, //optimims - 42161: `F325dMRiLVCJpX8EUFHg3SX8LE3kXBUmrsLRASisPEQ3`, // arb - 100: `6oJrPk9YJEU9rWU4DAizjZdALSccxe5ZahBsTtFaGksU`, //gnosis -} - -const connextSubgraphURL = (chainId) => { - // bnb is hosted version - if (chainId == 56) { - return 'https://api.thegraph.com/subgraphs/name/connext/amarok-runtime-v0-bnb' - } - const { SUBGRAPH_QUERY_API_KEY } = process.env - if (!SUBGRAPH_QUERY_API_KEY) { - throw new Error(`Missing SUBGRAPH_QUERY_API_KEY env`) - } - const subgraphId = connextSubgraphIds[chainId] - if (!subgraphId) { - throw new Error(`Unknown chain id ${chainId}`) - } - return `https://gateway-arbitrum.network.thegraph.com/api/${SUBGRAPH_QUERY_API_KEY}/subgraphs/id/${subgraphId}` -} - -const fetchXCall = async ({ query, chainId }) => { - const endpoint = connextSubgraphURL(chainId) - const q = await fetch(endpoint, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - query, - }), - }) - - const { data, errors } = await q.json() - if (errors) { - console.log('LOCK > Error while fetching the graph', errors) - return [] - } - return data -} - /*** * Delay Mod Logic */ @@ -714,10 +455,6 @@ const logStatus = (transferId, status) => { module.exports = { delayABI, - getXCalledEvents, - fetchOriginXCall, - fetchDestinationXCall, - getSupportedChainsByDomainId, getDelayModule, logStatus, } diff --git a/governance/helpers/bridge/index.js b/governance/helpers/bridge/index.js new file mode 100644 index 00000000000..8b261c03aca --- /dev/null +++ b/governance/helpers/bridge/index.js @@ -0,0 +1,11 @@ +const delayMod = require('./delayMod') +const xCall = require('./xCall') +const IConnext = require('./abis/IConnext') +const IXCalled = require('./abis/IXCalled') + +module.exports = { + ...delayMod, + ...xCall, + IConnext, + IXCalled, +} diff --git a/governance/helpers/bridge/xCall.js b/governance/helpers/bridge/xCall.js new file mode 100644 index 00000000000..3044eef4fb0 --- /dev/null +++ b/governance/helpers/bridge/xCall.js @@ -0,0 +1,153 @@ +const { ethers } = require('hardhat') +const { ADDRESS_ZERO } = require('@unlock-protocol/hardhat-helpers') +const { networks } = require('@unlock-protocol/networks') +const IXCalled = require('./abis/IXCalled') + +const targetChains = Object.keys(networks) + .map((id) => networks[id]) + .filter( + ({ governanceBridge, isTestNetwork, id }) => + !isTestNetwork && !!governanceBridge && id != 1 + ) + +/*** + * CONNEXT logic + */ +const getXCalledEvents = async (hash) => { + const { interface } = await ethers.getContractAt(IXCalled, ADDRESS_ZERO) + const { logs } = await ethers.provider.getTransactionReceipt(hash) + const parsedLogs = logs.map((log) => { + try { + return interface.parseLog(log) + } catch (error) { + return {} + } + }) + + const xCalled = parsedLogs + .filter((e) => e !== null) + .filter(({ name }) => name === 'XCalled') + .map(({ args }) => args) + + return xCalled +} + +const fetchOriginXCall = async ({ transferIds = [], chainId = 1 }) => { + const query = ` + { + originTransfers(where:{ + transferId_in: ${JSON.stringify(transferIds)} + }) { + chainId + nonce + transferId + to + delegate + receiveLocal + callData + slippage + originSender + originDomain + destinationDomain + transactionHash + bridgedAmt + status + } + } + ` + const { originTransfers } = await fetchXCall({ query, chainId }) + return originTransfers +} + +const fetchDestinationXCall = async ({ transferIds, chainId }) => { + const query = ` + { + destinationTransfers(where:{ + transferId_in: ${JSON.stringify(transferIds)} + }) { + chainId + nonce + transferId + to + delegate + receiveLocal + callData + originDomain + destinationDomain + delegate + status + executedTransactionHash + reconciledTransactionHash + } + } + ` + const { destinationTransfers } = await fetchXCall({ query, chainId }) + return destinationTransfers +} + +// supported chains by domain id +const getSupportedChainsByDomainId = async () => { + return Object.keys(networks) + .map((id) => networks[id]) + .filter( + ({ governanceBridge, isTestNetwork, id }) => + !isTestNetwork && !!governanceBridge && id != 1 + ) + .reduce( + (prev, curr) => ({ + ...prev, + [curr.governanceBridge.domainId]: curr, + }), + {} + ) +} + +const connextSubgraphIds = { + 1: `FfTxiY98LJG6zoiAjCXdT34pAmCKDEP8vZRVuC8D5Gf`, + 137: `7mDXK2K6UfkVXiJMhXU8VEFuh7qi2TwdYxeyaRjkmexo`, //plygon + 10: `3115xfkzXPrYzbqDHTiWGtzRDYNXBxs8dyitva6J18jf`, //optimims + 42161: `F325dMRiLVCJpX8EUFHg3SX8LE3kXBUmrsLRASisPEQ3`, // arb + 100: `6oJrPk9YJEU9rWU4DAizjZdALSccxe5ZahBsTtFaGksU`, //gnosis +} + +const connextSubgraphURL = (chainId) => { + // bnb is hosted version + if (chainId == 56) { + return 'https://api.thegraph.com/subgraphs/name/connext/amarok-runtime-v0-bnb' + } + const { SUBGRAPH_QUERY_API_KEY } = process.env + if (!SUBGRAPH_QUERY_API_KEY) { + throw new Error(`Missing SUBGRAPH_QUERY_API_KEY env`) + } + const subgraphId = connextSubgraphIds[chainId] + if (!subgraphId) { + throw new Error(`Unknown chain id ${chainId}`) + } + return `https://gateway-arbitrum.network.thegraph.com/api/${SUBGRAPH_QUERY_API_KEY}/subgraphs/id/${subgraphId}` +} + +const fetchXCall = async ({ query, chainId }) => { + const endpoint = connextSubgraphURL(chainId) + const q = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query, + }), + }) + + const { data, errors } = await q.json() + if (errors) { + console.log('LOCK > Error while fetching the graph', errors) + return [] + } + return data +} + +module.exports = { + targetChains, + getXCalledEvents, + fetchOriginXCall, + fetchDestinationXCall, + getSupportedChainsByDomainId, +} diff --git a/governance/helpers/multisig.js b/governance/helpers/multisig.js index 05117cad665..738741c2810 100644 --- a/governance/helpers/multisig.js +++ b/governance/helpers/multisig.js @@ -1,10 +1,14 @@ const { ethers } = require('hardhat') const { networks } = require('@unlock-protocol/networks') -const { getNetwork } = require('@unlock-protocol/hardhat-helpers') +const { getNetwork, ADDRESS_ZERO } = 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 -const { encodeMultiSendData } = require('@safe-global/protocol-kit') +const { + EthersAdapter, + encodeMultiSendData, +} = require('@safe-global/protocol-kit') +const Safe = require('@safe-global/protocol-kit').default // custom services URL for network not supported by Safe const safeServiceURLs = { @@ -192,23 +196,34 @@ const submitTxOldMultisig = async ({ safeAddress, tx, signer }) => { } // pack multiple calls in a single multicall -const parseSafeMulticall = async (calls) => { +const parseSafeMulticall = async ({ calls, chainId, options }) => { console.log(calls) - const metaTxs = calls.map( - ({ contractAddress, calldata = '0x', value = 0, operation = 0 }) => ({ + const transactions = calls.map( + ({ contractAddress, calldata = '0x', value = 0, operation = null }) => ({ to: contractAddress, value, data: calldata, - // TODO? need to fetch if proxy or not ? - operation, // operation: 0 for CALL, 1 for DELEGATECALL + operation, }) ) - const multicall = await encodeMultiSendData(metaTxs) + // init safe lib with correct provider + const { provider } = await getProvider(chainId) + const { multisig } = await getNetwork(chainId) + const ethAdapter = new EthersAdapter({ ethers, signerOrProvider: provider }) + const safe = await Safe.create({ + ethAdapter, + safeAddress: multisig, + }) - // TODO: how to get multicall address (without a provider) - const multicallAddress = '' - return multicall + // get multicall data from lib + const totalValue = calls.reduce((total, { value }) => value + total, 0n) + const { data } = await safe.createTransaction({ + transactions, + options, + callsOnly: totalValue === 0, + }) + return data } module.exports = { diff --git a/governance/proposals/006-cross-bridge-proposal.js b/governance/proposals/006-cross-bridge-proposal.js index 20761a42cf8..e60971e6ee1 100644 --- a/governance/proposals/006-cross-bridge-proposal.js +++ b/governance/proposals/006-cross-bridge-proposal.js @@ -2,69 +2,11 @@ * Example of a bridged proposal that will be sent across Connext to multisigs * on the other side of the network. */ -const { ADDRESS_ZERO } = require('../test/helpers') +const { ADDRESS_ZERO } = require('@unlock-protocol/hardhat-helpers') +const { IConnext, targetChains } = require('../helpers/bridge') const { ethers } = require('hardhat') const { networks } = require('@unlock-protocol/networks') -const abiIConnext = [ - { - inputs: [ - { - internalType: 'uint32', - name: '_destination', - type: 'uint32', - }, - { - internalType: 'address', - name: '_to', - type: 'address', - }, - { - internalType: 'address', - name: '_asset', - type: 'address', - }, - { - internalType: 'address', - name: '_delegate', - type: 'address', - }, - { - internalType: 'uint256', - name: '_amount', - type: 'uint256', - }, - { - internalType: 'uint256', - name: '_slippage', - type: 'uint256', - }, - { - internalType: 'bytes', - name: '_callData', - type: 'bytes', - }, - ], - name: 'xcall', - outputs: [ - { - internalType: 'bytes32', - name: '', - type: 'bytes32', - }, - ], - stateMutability: 'payable', - type: 'function', - }, -] - -const targetChains = Object.keys(networks) - .map((id) => networks[id]) - .filter( - ({ governanceBridge, isTestNetwork, id }) => - !isTestNetwork && !!governanceBridge && id != 1 - ) - module.exports = async () => { // parse call data for function call const { interface: unlockInterface } = await ethers.getContractAt( @@ -138,7 +80,7 @@ module.exports = async () => { // proposed changes return { contractAddress: bridgeAddress, - contractNameOrAbi: abiIConnext, + contractNameOrAbi: IConnext, functionName: 'xcall', functionArgs: [ destDomainId, diff --git a/governance/proposals/009-protocol-upgrade.js b/governance/proposals/009-protocol-upgrade.js index 3d22425532f..f9e1df55f46 100644 --- a/governance/proposals/009-protocol-upgrade.js +++ b/governance/proposals/009-protocol-upgrade.js @@ -5,6 +5,7 @@ const { ethers } = require('hardhat') const { UnlockV13 } = require('@unlock-protocol/contracts') const { networks } = require('@unlock-protocol/networks') +const { IConnext, targetChains } = require('../helpers/bridge') const { getProxyAdminAddress, @@ -17,59 +18,6 @@ const { const { parseSafeMulticall } = require('../helpers/multisig') -// TODO: move to hardhat-helpers -const abiIConnext = [ - { - inputs: [ - { - internalType: 'uint32', - name: '_destination', - type: 'uint32', - }, - { - internalType: 'address', - name: '_to', - type: 'address', - }, - { - internalType: 'address', - name: '_asset', - type: 'address', - }, - { - internalType: 'address', - name: '_delegate', - type: 'address', - }, - { - internalType: 'uint256', - name: '_amount', - type: 'uint256', - }, - { - internalType: 'uint256', - name: '_slippage', - type: 'uint256', - }, - { - internalType: 'bytes', - name: '_callData', - type: 'bytes', - }, - ], - name: 'xcall', - outputs: [ - { - internalType: 'bytes32', - name: '', - type: 'bytes32', - }, - ], - stateMutability: 'payable', - type: 'function', - }, -] - // addresses const deployedContracts = { 1: { @@ -167,11 +115,6 @@ const parseCalls = async ({ unlockAddress, name, id }) => { } module.exports = async () => { - const targetChains = Object.keys(networks) - .filter((id) => Object.keys(deployedContracts).includes(id.toString())) - .filter((id) => id != 1) - .map((id) => networks[id]) - // src info const { id: chainId } = await getNetwork() const { @@ -219,7 +162,7 @@ module.exports = async () => { // add to the list of calls to be passed to the bridge bridgeCalls.push({ contractAddress: bridgeAddress, - contractNameOrAbi: abiIConnext, + contractNameOrAbi: IConnext, functionName: 'xcall', functionArgs: [ destDomainId, From 82fd6123d65f3c81fa83deb78894c1cc6ad9f194 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Wed, 3 Apr 2024 14:19:48 +0200 Subject: [PATCH 10/13] parse calls correctly --- governance/helpers/multisig.js | 10 ++++++-- governance/proposals/010-test-multicall.js | 27 ++++++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/governance/helpers/multisig.js b/governance/helpers/multisig.js index 738741c2810..19db7bf047a 100644 --- a/governance/helpers/multisig.js +++ b/governance/helpers/multisig.js @@ -197,7 +197,6 @@ const submitTxOldMultisig = async ({ safeAddress, tx, signer }) => { // pack multiple calls in a single multicall const parseSafeMulticall = async ({ calls, chainId, options }) => { - console.log(calls) const transactions = calls.map( ({ contractAddress, calldata = '0x', value = 0, operation = null }) => ({ to: contractAddress, @@ -217,12 +216,19 @@ const parseSafeMulticall = async ({ calls, chainId, options }) => { }) // get multicall data from lib - const totalValue = calls.reduce((total, { value }) => value + total, 0n) + const totalValue = calls.reduce( + (total, { value }) => (value || 0n) + total, + 0n + ) const { data } = await safe.createTransaction({ transactions, options, callsOnly: totalValue === 0, }) + + // parse calls correcly for our multisig/dao helpers + data.calldata = data.data + data.contractAddress = data.to return data } diff --git a/governance/proposals/010-test-multicall.js b/governance/proposals/010-test-multicall.js index bcb796ba606..4b465f1448e 100644 --- a/governance/proposals/010-test-multicall.js +++ b/governance/proposals/010-test-multicall.js @@ -1,23 +1,40 @@ const { ethers, ZeroAddress } = require('ethers') const { parseSafeMulticall } = require('../helpers/multisig') +const { UnlockV12 } = require('@unlock-protocol/contracts') +const { + getNetwork, + getERC20Contract, +} = require('@unlock-protocol/hardhat-helpers') module.exports = async () => { + const { unlockAddress, tokens, id: chainId } = await getNetwork() + const { address: USDC } = tokens.find(({ symbol }) => symbol === 'USDC') + const { interface } = await getERC20Contract(USDC) + const calls = [ { contractAddress: ZeroAddress, value: ethers.parseEther('0.0001'), }, { - contractAddress: ZeroAddress, - value: ethers.parseEther('0.0001'), + contractAddress: USDC, + calldata: interface.encodeFunctionData('transfer', [ + ZeroAddress, + ethers.parseUnits('0.1', 8), + ]), + }, + { + contractNameOrAbi: UnlockV12.abi, + contractAddress: unlockAddress, + functionName: 'setProtocolFee', + functionArgs: '0.0001', }, ] // parse calls for Safe - const packedData = await parseSafeMulticall(calls) - console.log(packedData) + const packedCalls = await parseSafeMulticall({ chainId, calls }) return { proposalName: 'Test a multicall', - calls: { calldata: packedData }, + calls: [packedCalls], } } From 8655d0cb386f2ce71dc35cc4a112f0c71713cb53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Wed, 3 Apr 2024 14:27:09 +0200 Subject: [PATCH 11/13] cleanup gov workflow --- governance/scripts/gov/execute.js | 2 +- governance/scripts/gov/index.js | 4 ++-- governance/scripts/gov/queue.js | 3 +-- governance/scripts/gov/submit.js | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/governance/scripts/gov/execute.js b/governance/scripts/gov/execute.js index 8cfa391a965..2bba93030b0 100644 --- a/governance/scripts/gov/execute.js +++ b/governance/scripts/gov/execute.js @@ -18,7 +18,7 @@ async function main({ proposal, proposalId, txId, govAddress }) { if (!proposal && !proposalId) { throw new Error('GOV EXEC > Missing proposal or proposalId.') } - if (proposalId && !txId) { + if (!proposal && proposalId && !txId) { throw new Error( 'GOV EXEC > The tx id of the proposal creation is required to execute the proposal.' ) diff --git a/governance/scripts/gov/index.js b/governance/scripts/gov/index.js index be0e6106d3b..8bbf8d95d6b 100644 --- a/governance/scripts/gov/index.js +++ b/governance/scripts/gov/index.js @@ -54,7 +54,7 @@ async function main({ proposal, proposalId, govAddress, txId }) { // Submit the proposal if necessary if (!proposalId) { - proposalId = await submit({ proposal, govAddress }) + ;({ proposalId } = await submit({ proposal, govAddress })) } // votes @@ -74,7 +74,7 @@ async function main({ proposal, proposalId, govAddress, txId }) { ) // Run the gov workflow - await queue({ proposalId, govAddress, txId }) + await queue({ proposalId, proposal, govAddress, txId }) const { logs } = await execute({ proposalId, txId, proposal, govAddress }) // log all events diff --git a/governance/scripts/gov/queue.js b/governance/scripts/gov/queue.js index 79f545ddceb..a32c06a0771 100644 --- a/governance/scripts/gov/queue.js +++ b/governance/scripts/gov/queue.js @@ -1,6 +1,5 @@ const { ethers } = require('hardhat') const { mineUpTo } = require('@nomicfoundation/hardhat-network-helpers') -const { getProposalArgsFromTx } = require('../../helpers/gov') const { queueProposal, @@ -17,7 +16,7 @@ async function main({ proposalId, txId, proposal, govAddress }) { if (!proposal && !proposalId) { throw new Error('GOV QUEUE > Missing proposal or proposalId.') } - if (proposalId && !txId) { + if (!proposal && proposalId && !txId) { throw new Error( 'GOV QUEUE > The tx id of the proposal creation is required to execute the proposal.' ) diff --git a/governance/scripts/gov/submit.js b/governance/scripts/gov/submit.js index 822b815d0f9..17ae2119d7d 100644 --- a/governance/scripts/gov/submit.js +++ b/governance/scripts/gov/submit.js @@ -34,7 +34,7 @@ async function main({ proposal, govAddress }) { `GOV SUBMIT > proposal submitted: ${await proposalId.toString()} (txid: ${hash}, block: ${currentBlock})` ) - return proposalId, hash + return { proposalId, hash } } // execute as standalone From 8ae9f7a0d91f6946c92cf203da8c9412564db207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Wed, 3 Apr 2024 14:49:50 +0200 Subject: [PATCH 12/13] update safe tx signing --- governance/scripts/multisig/submitTx.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/governance/scripts/multisig/submitTx.js b/governance/scripts/multisig/submitTx.js index 7bc8accdade..2f4728faba5 100644 --- a/governance/scripts/multisig/submitTx.js +++ b/governance/scripts/multisig/submitTx.js @@ -113,13 +113,13 @@ async function main({ safeAddress, tx, signer }) { // now send tx via Safe Global web service const safeTxHash = await safeSdk.getTransactionHash(safeTransaction) - const senderSignature = await safeSdk.signTransactionHash(safeTxHash) + const senderSignature = await safeSdk.signHash(safeTxHash) await safeService.proposeTransaction({ safeAddress, safeTransactionData: safeTransaction.data, safeTxHash, - senderAddress: signer.address, + senderAddress: await signer.getAddress(), senderSignature: senderSignature.data, }) From 86e0854126ff9ad5415745645a9bf6fee6b67e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Wed, 3 Apr 2024 14:50:13 +0200 Subject: [PATCH 13/13] Update governance/helpers/bridge/xCall.js Co-authored-by: Julien Genestoux --- governance/helpers/bridge/xCall.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/governance/helpers/bridge/xCall.js b/governance/helpers/bridge/xCall.js index 3044eef4fb0..de193379a05 100644 --- a/governance/helpers/bridge/xCall.js +++ b/governance/helpers/bridge/xCall.js @@ -104,7 +104,7 @@ const getSupportedChainsByDomainId = async () => { const connextSubgraphIds = { 1: `FfTxiY98LJG6zoiAjCXdT34pAmCKDEP8vZRVuC8D5Gf`, - 137: `7mDXK2K6UfkVXiJMhXU8VEFuh7qi2TwdYxeyaRjkmexo`, //plygon + 137: `7mDXK2K6UfkVXiJMhXU8VEFuh7qi2TwdYxeyaRjkmexo`, //polygon 10: `3115xfkzXPrYzbqDHTiWGtzRDYNXBxs8dyitva6J18jf`, //optimims 42161: `F325dMRiLVCJpX8EUFHg3SX8LE3kXBUmrsLRASisPEQ3`, // arb 100: `6oJrPk9YJEU9rWU4DAizjZdALSccxe5ZahBsTtFaGksU`, //gnosis