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 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/scripts/bridge/_lib.js b/governance/helpers/bridge/delayMod.js similarity index 63% rename from governance/scripts/bridge/_lib.js rename to governance/helpers/bridge/delayMod.js index f6653e05d42..62b0c9442ca 100644 --- a/governance/scripts/bridge/_lib.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 */ @@ -699,13 +440,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 )} @@ -716,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..de193379a05 --- /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`, //polygon + 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 439b91c7901..19db7bf047a 100644 --- a/governance/helpers/multisig.js +++ b/governance/helpers/multisig.js @@ -1,9 +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 { + 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 = { @@ -190,6 +195,43 @@ const submitTxOldMultisig = async ({ safeAddress, tx, signer }) => { return nonce } +// pack multiple calls in a single multicall +const parseSafeMulticall = async ({ calls, chainId, options }) => { + const transactions = calls.map( + ({ contractAddress, calldata = '0x', value = 0, operation = null }) => ({ + to: contractAddress, + value, + data: calldata, + operation, + }) + ) + + // 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, + }) + + // get multicall data from lib + 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 +} + module.exports = { getProvider, getSafeAddress, @@ -202,4 +244,5 @@ module.exports = { getExpectedSigners, logError, getSafeService, + parseSafeMulticall, } 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 7a4265bf41b..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, @@ -15,58 +16,7 @@ const { abi: proxyAdminABI, } = require('@unlock-protocol/hardhat-helpers/dist/ABIs/ProxyAdmin.json') -// 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', - }, -] +const { parseSafeMulticall } = require('../helpers/multisig') // addresses const deployedContracts = { @@ -110,8 +60,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, @@ -167,17 +115,8 @@ 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() - console.log( - `from ${chainId} to chains ${targetChains.map(({ id }) => id).join(' - ')}` - ) - const { governanceBridge: { connext: bridgeAddress }, } = networks[chainId] @@ -217,43 +156,28 @@ 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 parseSafeMulticall(destCalls) + + // add to the list of calls to be passed to the bridge + bridgeCalls.push({ + contractAddress: bridgeAddress, + contractNameOrAbi: IConnext, + functionName: 'xcall', + functionArgs: [ + destDomainId, + destAddress, // destMultisigAddress, + ADDRESS_ZERO, // asset + ADDRESS_ZERO, // delegate + 0, // amount + 30, // slippage + moduleData, // calldata + ], + }) }) ) 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 +220,9 @@ Onwards ! The Unlock Protocol Team ` - console.log(proposalName) return { proposalName, calls, + explainers, } } diff --git a/governance/proposals/010-test-multicall.js b/governance/proposals/010-test-multicall.js new file mode 100644 index 00000000000..4b465f1448e --- /dev/null +++ b/governance/proposals/010-test-multicall.js @@ -0,0 +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: 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 packedCalls = await parseSafeMulticall({ chainId, calls }) + return { + proposalName: 'Test a multicall', + calls: [packedCalls], + } +} diff --git a/governance/scripts/bridge/execTx.js b/governance/scripts/bridge/execTx.js index a0123721287..e8b5dce5fc8 100644 --- a/governance/scripts/bridge/execTx.js +++ b/governance/scripts/bridge/execTx.js @@ -7,51 +7,47 @@ * */ -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') + 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 +58,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) { diff --git a/governance/scripts/bridge/bump.js b/governance/scripts/bridge/payFee.js similarity index 96% rename from governance/scripts/bridge/bump.js rename to governance/scripts/bridge/payFee.js index b21218c1bf8..0da71ef4e10 100644 --- a/governance/scripts/bridge/bump.js +++ b/governance/scripts/bridge/payFee.js @@ -5,13 +5,13 @@ * 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') 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 e39bccfb389..9208e423821 100644 --- a/governance/scripts/bridge/status.js +++ b/governance/scripts/bridge/status.js @@ -1,10 +1,19 @@ +/** + * 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, fetchDestinationXCall, getSupportedChainsByDomainId, logStatus, -} = require('./_lib') +} = require('../../helpers/bridge') const fs = require('fs-extra') @@ -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 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 diff --git a/governance/scripts/multisig/submitTx.js b/governance/scripts/multisig/submitTx.js index 9f805eed19d..2f4728faba5 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( @@ -108,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, }) 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) } )