diff --git a/scripts/governance/get-createProposalWithSolution-txdata.js b/scripts/governance/get-createProposalWithSolution-txdata.js new file mode 100644 index 0000000000..ce0e463d8d --- /dev/null +++ b/scripts/governance/get-createProposalWithSolution-txdata.js @@ -0,0 +1,114 @@ +require('dotenv').config(); +const path = require('node:path'); + +const { ethers } = require('hardhat'); +const ipfsClient = require('ipfs-http-client'); + +const { simulateTransaction, constants } = require('./helpers'); +const fs = require('fs'); +const { GOVERNANCE_ADDRESS, IPFS_API_URL, CATEGORY_PARAM_TYPES } = constants; + +const ipfs = ipfsClient({ url: IPFS_API_URL }); + +const verifyDecodedTxInputs = (inputs, decodedTxInputs) => { + if (decodedTxInputs[0] !== inputs[0]) { + throw new Error(`Title mismatch: ${decodedTxInputs[0]} !== ${inputs[0]}`); + } + + if (decodedTxInputs[1] !== inputs[1]) { + throw new Error(`Short description mismatch: ${decodedTxInputs[1]} !== ${inputs[1]}`); + } + + if (decodedTxInputs[2] !== inputs[2]) { + throw new Error(`Ipfs hash mismatch: ${decodedTxInputs[2]} !== ${inputs[2]}`); + } + + if (decodedTxInputs[3] !== inputs[3]) { + throw new Error(`Category mismatch: ${decodedTxInputs[3]} !== ${inputs[3]}`); + } + + if (decodedTxInputs[4] !== inputs[4]) { + throw new Error(`Solution Hash mismatch: ${decodedTxInputs[4]} !== ${inputs[4]}`); + } + + if (decodedTxInputs[5] !== inputs[5]) { + throw new Error(`Action mismatch: ${decodedTxInputs[5]} !== ${inputs[5]}`); + } +}; + +/** + * + * Generate the tx data for the Governance.createProposalWithSolution transaction using the provided proposal data + * + * @param proposalFilePath path for file of proposal data containing title, shortDescription, and description + * @param categoryId category id for the proposal + * @param actionParamsRaw action params for the proposal as stringified JSON + * @param solutionHash hash of the solution for the proposal + * @returns {Promise<{createProposalWithSolution: *}>} + */ +const main = async (proposalFilePath, categoryId, actionParamsRaw, solutionHash = '') => { + const governance = await ethers.getContractAt('Governance', GOVERNANCE_ADDRESS); + const [proposal] = require(path.resolve(proposalFilePath)); + const actionParams = JSON.parse(actionParamsRaw); + + // check for any missing required data before processing and uploading files to IPFS + if (Object.keys(proposal).length > 3) { + throw new Error('Proposal data should only contain title, shortDescription, and description'); + } + + if (!proposal.title) { + throw new Error('Proposal title is required'); + } + + if (!proposal.shortDescription) { + throw new Error('Proposal short description is required'); + } + + if (!proposal.description) { + throw new Error('Proposal description is required'); + } + + if (!categoryId) { + throw new Error('Category ID is required'); + } + + if (!actionParams) { + throw new Error('Action is required'); + } + + if (CATEGORY_PARAM_TYPES[categoryId].length !== actionParams.length) { + throw new Error( + `Action Params length mismatch: ${CATEGORY_PARAM_TYPES[categoryId].length} !== ${actionParams.length}`, + ); + } + + const encodedActionParams = ethers.utils.defaultAbiCoder.encode(CATEGORY_PARAM_TYPES[categoryId], actionParams); + + // upload proposal file to IPFS + const file = await ipfs.add(fs.readFileSync(proposalFilePath)); + await ipfs.pin.add(file.path); + + // group the inputs for the createProposalWithSolution transaction + const inputs = [proposal.title, proposal.shortDescription, file.path, categoryId, solutionHash, encodedActionParams]; + + // create the transaction data for createProposalwithSolution + const createProposalTransaction = await governance.populateTransaction.createProposalwithSolution(...inputs); + console.log(`Tx data:\n${createProposalTransaction.data}`); + + // simulate the transaction + const decodedTxInputs = await simulateTransaction(createProposalTransaction.data); + + // verify the decoded inputs match the inputs + verifyDecodedTxInputs(inputs, decodedTxInputs); + + return createProposalTransaction; +}; + +if (require.main === module) { + main(process.argv[2], process.argv[3], process.argv[4], process.argv[5]).catch(e => { + console.log('Unhandled error encountered: ', e.stack); + process.exit(1); + }); +} + +module.exports = main; diff --git a/scripts/governance/helpers.js b/scripts/governance/helpers.js new file mode 100644 index 0000000000..46e2ccdfda --- /dev/null +++ b/scripts/governance/helpers.js @@ -0,0 +1,58 @@ +const axios = require('axios'); +const { inspect } = require('node:util'); +const nexusSdk = require('@nexusmutual/deployments'); + +const AB_MEMBER = '0x87B2a7559d85f4653f13E6546A14189cd5455d45'; +const GOVERNANCE_ADDRESS = nexusSdk.addresses.Governance; + +const IPFS_API_URL = 'https://api.nexusmutual.io/ipfs-api/api/v0'; + +const CATEGORY_PARAM_TYPES = { + 29: ['bytes2[]', 'address[]'], + 43: ['bytes2[]', 'address[]', 'uint256[]'], +}; + +/** + * NOTE: requires TENDERLY_ACCESS_KEY env + * @param {HexString} input - the tx.data + */ +const simulateTransaction = async input => { + const payload = { + save: true, // save result to dashboard + save_if_fails: true, // show reverted txs in dashboard + simulation_type: 'full', + network_id: '1', + from: AB_MEMBER, + to: GOVERNANCE_ADDRESS, + gas: 8000000, + gas_price: 0, + value: 0, + input, + }; + + const response = await axios.post( + `https://api.tenderly.co/api/v1/account/NexusMutual/project/nexusmutual/simulate`, + payload, + { headers: { 'X-Access-Key': process.env.TENDERLY_ACCESS_KEY } }, + ); + + const { transaction, simulation } = response.data; + const decodedTxInputs = transaction.transaction_info.call_trace.decoded_input.map(input => input.value); + console.info('governance.createProposal input:\n', inspect(decodedTxInputs, { depth: null })); + console.info( + '\nTenderly Simulated transaction:\n', + `https://dashboard.tenderly.co/NexusMutual/nexusmutual/simulator/${simulation.id}`, + ); + + return decodedTxInputs; +}; + +module.exports = { + simulateTransaction, + constants: { + GOVERNANCE_ADDRESS, + AB_MEMBER, + IPFS_API_URL, + CATEGORY_PARAM_TYPES, + }, +}; diff --git a/scripts/governance/proposal-sample.json b/scripts/governance/proposal-sample.json new file mode 100644 index 0000000000..1df9d6f860 --- /dev/null +++ b/scripts/governance/proposal-sample.json @@ -0,0 +1,5 @@ +[{ + "title": "", + "shortDescription": "", + "description": "" +}]