From da5de720969fa8a33b7137b3fae1d4cd8fdf2a97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Fri, 29 Mar 2024 10:31:14 +0100 Subject: [PATCH] feat(governance): allow test execution of proposal from tx id (#13509) * allow test execution of proposal from tx id * parse and pass correctly tx args * add unlock addresses in whales * allow proposal id only for votes * pass odwn tx receipt * herlpes for gov contract * log events after proposal execution * Update governance/scripts/gov/index.js --------- Co-authored-by: Julien Genestoux --- governance/helpers/gov.js | 228 ++++++++++-------- governance/helpers/tx.js | 26 ++ governance/proposals/000-example.js | 2 - .../proposals/001-example-w-calldata.js | 4 - governance/scripts/bridge/_lib.js | 18 +- governance/scripts/bridge/execTx.js | 8 +- governance/scripts/gov/execute.js | 20 +- governance/scripts/gov/index.js | 32 ++- governance/scripts/gov/queue.js | 18 +- governance/scripts/gov/submit.js | 18 +- governance/tasks/gov.js | 133 +++++++--- .../test/fixtures/proposal-000-example.js | 2 - governance/test/gov.test.js | 42 ++-- packages/hardhat-helpers/src/fork.js | 7 +- 14 files changed, 359 insertions(+), 199 deletions(-) create mode 100644 governance/helpers/tx.js diff --git a/governance/helpers/gov.js b/governance/helpers/gov.js index a007f26c916..44d5bb514c0 100644 --- a/governance/helpers/gov.js +++ b/governance/helpers/gov.js @@ -1,6 +1,7 @@ const { ethers } = require('hardhat') const { ADDRESS_ZERO } = require('@unlock-protocol/hardhat-helpers') const { GovernorUnlockProtocol } = require('@unlock-protocol/contracts') +const { fetchDataFromTx } = require('./tx') /** * Helper to parse a DAO proposal from an object @@ -15,10 +16,28 @@ const { GovernorUnlockProtocol } = require('@unlock-protocol/contracts') * @returns a formatted proposal in the form of an array of 3 arrays and a string * ex. [ [ to (address) ], [ value (in ETH) ], [ calldata (as string) ], "name of the proposal"] */ - const parseProposal = async ({ calls, // should be an array. If present will bypass functionName / functionArgs logic proposalName, + txId, + govAddress = ADDRESS_ZERO, +}) => { + let proposal + const gov = await getGovContract(govAddress) + if (calls && proposalName) { + proposal = await parseProposalFromFile({ + calls, + proposalName, + }) + } else { + proposal = await getProposalArgsFromTx({ txId, gov }) + } + return { ...proposal, gov } +} + +const parseProposalFromFile = async ({ + calls, // should be an array. If present will bypass functionName / functionArgs logic + proposalName, }) => { // parse an array of contract calls if (!calls || !calls.length) { @@ -37,6 +56,7 @@ const parseProposal = async ({ contractAddress, functionName, functionArgs, + value = 0, }) => { if (!calldata) { calldata = await encodeProposalArgs({ @@ -45,41 +65,77 @@ const parseProposal = async ({ functionArgs, }) } - return { calldata, contractAddress, value: 0 } + return { calldata, contractAddress, value } } ) ) - const parsed = encodedCalls.reduce( - (arr, { calldata, contractAddress, value }) => { - return !arr.length - ? [[contractAddress], [value], [calldata]] - : [ - [...arr[0], contractAddress], // contracts to send the proposal to - [...arr[1], value], // value in ETH, default to 0 - [...arr[2], calldata], // encoded func calls - ] - }, - [] + const { targets, values, calldatas } = encodedCalls.reduce( + ({ targets, values, calldatas }, { calldata, contractAddress, value }) => ({ + targets: [...targets, contractAddress], // contracts to send the proposal to + values: [...values, value], // value in ETH, default to 0 + calldatas: [...calldatas, calldata], // encoded func calls + }), + { + targets: [], + values: [], + calldatas: [], + } ) + const descriptionHash = ethers.keccak256(ethers.toUtf8Bytes(proposalName)) + return { + targets, + values, + calldatas, + descriptionHash, + description: proposalName, + } +} - return [...parsed, proposalName] +const getProposalArgsFromTx = async ({ gov, txId }) => { + const [proposalId, , _targets, _values, , _calldatas, , , description] = + await fetchDataFromTx({ + txHash: txId, + eventName: 'ProposalCreated', + abi: GovernorUnlockProtocol.abi, + }) + // make sure values are correct + const descriptionHash = ethers.keccak256(ethers.toUtf8Bytes(description)) + const targets = _targets.toArray() + const values = _values.toArray() + const calldatas = _calldatas.toArray() + const proposalIdFromFetchedValues = await gov.hashProposal( + targets, + values, + calldatas, + descriptionHash + ) + if (proposalIdFromFetchedValues !== proposalId) { + throw new Error('proposalId mismatch') + } + return { + targets, + values, + calldatas, + descriptionHash, + } } +/** + * HELPERS + */ const getProposalId = async (proposal) => { - const [targets, values, calldata, description] = await parseProposal({ + const { targets, values, calldatas, descriptionHash } = await parseProposal({ ...proposal, }) - const descriptionHash = ethers.keccak256(ethers.toUtf8Bytes(description)) - // solidityKeccak256 const encoder = ethers.AbiCoder.defaultAbiCoder() const proposalId = BigInt( ethers.keccak256( encoder.encode( ['address[]', 'uint256[]', 'bytes[]', 'bytes32'], - [targets, values, calldata, descriptionHash] + [targets, values, calldatas, descriptionHash] ) ) ) @@ -87,40 +143,14 @@ const getProposalId = async (proposal) => { return proposalId } -const getProposalIdFromContract = async (proposal, govAddress) => { - const { proposerAddress } = proposal - const [to, value, calldata, description] = await parseProposal({ - ...proposal, - }) - - const [defaultSigner] = await ethers.getSigners() - const proposerWallet = proposerAddress - ? defaultSigner - : await ethers.getSigner(proposerAddress) - - const gov = await ethers.getContractAt( - GovernorUnlockProtocol.abi, - govAddress, - proposerWallet - ) - - const descriptionHash = ethers.keccak256(ethers.toUtf8Bytes(description)) - - const proposalId = await gov.hashProposal( - to, - value, - calldata, - descriptionHash - ) - - return proposalId -} - -const validateProposalCall = (proposal) => { +const validateProposalCall = (call) => { // proposal contains a single contract call - if (!proposal.calldata && !proposal.functionArgs) { + if (!call.calldata && !call.functionArgs) { throw new Error('Missing calldata or function args.') } + if (!call.contractAddress) { + throw new Error('Missing target (to) in proposal call.') + } } const encodeProposalArgs = async ({ @@ -153,80 +183,87 @@ const decodeProposalArgs = async ({ return decoded } -const queueProposal = async ({ proposal, govAddress }) => { - const [targets, values, calldatas, description] = await parseProposal({ +const getProposalIdFromContract = async ({ proposal, govAddress, txId }) => { + const { targets, values, calldatas, descriptionHash } = await parseProposal({ ...proposal, + govAddress, + txId, }) - const descriptionHash = ethers.keccak256(ethers.toUtf8Bytes(description)) - const { proposerAddress } = proposal - let voterWallet - if (!proposerAddress) { - ;[voterWallet] = await ethers.getSigners() - } else { - voterWallet = await ethers.getSigner(proposerAddress) - } - console.log({ targets, values, calldatas, description }) - - const gov = await ethers.getContractAt(GovernorUnlockProtocol.abi, govAddress) + const gov = await getGovContract(govAddress) + const proposalId = await gov.hashProposal( + targets, + values, + calldatas, + descriptionHash + ) - return await gov - .connect(voterWallet) - .queue(targets, values, calldatas, descriptionHash) + return proposalId } -const executeProposal = async ({ proposal, govAddress }) => { - const { proposerAddress } = proposal - const [targets, values, calldatas, description] = await parseProposal({ - ...proposal, - }) - const descriptionHash = ethers.keccak256(ethers.toUtf8Bytes(description)) - let voterWallet - if (!proposerAddress) { - ;[voterWallet] = await ethers.getSigners() - } else { - voterWallet = await ethers.getSigner(proposerAddress) - } +const queueProposal = async ({ proposal, govAddress, proposalId, txId }) => { + const { targets, values, calldatas, descriptionHash, gov } = + await parseProposal({ + ...proposal, + govAddress, + txId, + }) + return await gov.queue(targets, values, calldatas, descriptionHash) +} - const gov = await ethers.getContractAt(GovernorUnlockProtocol.abi, govAddress) - return await gov - .connect(voterWallet) - .execute(targets, values, calldatas, descriptionHash) +const executeProposal = async ({ proposal, govAddress, proposalId, txId }) => { + const { gov, targets, values, calldatas, descriptionHash } = + await parseProposal({ + ...proposal, + proposalId, + govAddress, + txId, + }) + return await gov.execute(targets, values, calldatas, descriptionHash) } /** * Submits a proposal */ -const submitProposal = async ({ proposerAddress, proposal, govAddress }) => { +const submitProposal = async ({ proposal, govAddress, proposalId, txId }) => { + const gov = await getGovContract(govAddress) + const { targets, values, calldatas, description } = await parseProposal({ + ...proposal, + govAddress, + proposalId, + txId, + }) + return await gov.propose(targets, values, calldatas, description) +} + +const getGovContract = async (govAddress) => { const gov = await ethers.getContractAt(GovernorUnlockProtocol.abi, govAddress) - let proposer - if (!proposerAddress) { - ;[proposer] = await ethers.getSigners() - } else { - proposer = await ethers.getSigner(proposerAddress) - } - const parsed = await parseProposal(proposal) - return await gov.connect(proposer).propose(...parsed) + return gov } const getProposalVotes = async (proposalId, govAddress) => { - const gov = await ethers.getContractAt(GovernorUnlockProtocol.abi, govAddress) + const gov = await getGovContract(govAddress) const votes = await gov.proposalVotes(proposalId) return votes } const getQuorum = async (govAddress) => { - const gov = await ethers.getContractAt(GovernorUnlockProtocol.abi, govAddress) + const gov = await getGovContract(govAddress) const currentBlock = await ethers.provider.getBlockNumber() return await gov.quorum(currentBlock - 1) } const getGovTokenAddress = async (govAddress) => { - const gov = await ethers.getContractAt(GovernorUnlockProtocol.abi, govAddress) + const gov = await getGovContract(govAddress) return await gov.token() } +const getTimelockAddress = async (govAddress) => { + const gov = await getGovContract(govAddress) + return await gov.timelock() +} + const getProposalState = async (proposalId, govAddress) => { const states = [ 'Pending', @@ -239,7 +276,7 @@ const getProposalState = async (proposalId, govAddress) => { 'Executed', ] - const gov = await ethers.getContractAt(GovernorUnlockProtocol.abi, govAddress) + const gov = await getGovContract(govAddress) const state = await gov.state(proposalId) return states[state] } @@ -261,7 +298,9 @@ module.exports = { loadProposal, getProposalVotes, getQuorum, + getGovContract, getGovTokenAddress, + getTimelockAddress, getProposalState, getProposalId, getProposalIdFromContract, @@ -273,4 +312,5 @@ module.exports = { executeProposal, etaToDate, isAlreadyPast, + getProposalArgsFromTx, } diff --git a/governance/helpers/tx.js b/governance/helpers/tx.js new file mode 100644 index 00000000000..70c1560a35c --- /dev/null +++ b/governance/helpers/tx.js @@ -0,0 +1,26 @@ +const { ethers } = require('hardhat') +const { ADDRESS_ZERO } = require('@unlock-protocol/hardhat-helpers') + +const fetchDataFromTx = async ({ + txHash, + abi, + eventName = 'TransactionAdded', +}) => { + const { interface } = await ethers.getContractAt(abi, ADDRESS_ZERO) + + // fetch data from tx + const { logs } = await ethers.provider.getTransactionReceipt(txHash) + const parsedLogs = logs.map((log) => { + try { + return interface.parseLog(log) + } catch (error) { + return {} + } + }) + const { args } = parsedLogs.find(({ name }) => name === eventName) + return args +} + +module.exports = { + fetchDataFromTx, +} diff --git a/governance/proposals/000-example.js b/governance/proposals/000-example.js index 66d31b8c612..ff6040bb1e1 100644 --- a/governance/proposals/000-example.js +++ b/governance/proposals/000-example.js @@ -2,7 +2,6 @@ const ethers = require('ethers') const { UnlockDiscountTokenV2 } = require('@unlock-protocol/contracts') const tokenRecipientAddress = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' -const proposerAddress = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' module.exports = { proposalName: '#000 This is just an example!', @@ -14,6 +13,5 @@ module.exports = { functionArgs: [tokenRecipientAddress, ethers.parseEther('0.01')], }, ], - proposerAddress, // no payable value specified default to 0 } diff --git a/governance/proposals/001-example-w-calldata.js b/governance/proposals/001-example-w-calldata.js index f3a2605b02e..73ff7596715 100644 --- a/governance/proposals/001-example-w-calldata.js +++ b/governance/proposals/001-example-w-calldata.js @@ -1,15 +1,11 @@ const { UnlockDiscountTokenV2 } = require('@unlock-protocol/contracts') -// use hardhat default local address for testing -const proposerAddress = '0x9dED35Aef86F3c826Ff8fe9240f9e7a9Fb2094e5' - module.exports = { proposalName: 'Marketing unlock - david moderator', calls: [ { contractNameOrAbi: UnlockDiscountTokenV2.abi, functionName: 'transfer', - proposerAddress, calldata: '0xa9059cbb0000000000000000000000000235545f679b133543607c66988d60d772c10d4f000000000000000000000000000000000000000000000028a857425466f80000', }, diff --git a/governance/scripts/bridge/_lib.js b/governance/scripts/bridge/_lib.js index d5cb1ee5f8b..f6653e05d42 100644 --- a/governance/scripts/bridge/_lib.js +++ b/governance/scripts/bridge/_lib.js @@ -698,22 +698,6 @@ const getDelayModule = async (delayModuleAddress) => { return { delayMod, currentNonce, queueNonce } } -const fetchDataFromTx = async ({ txHash }) => { - const { interface } = await ethers.getContractAt(delayABI, ADDRESS_ZERO) - - // fetch data from tx - const { logs } = await ethers.provider.getTransactionReceipt(txHash) - const parsedLogs = logs.map((log) => { - try { - return interface.parseLog(log) - } catch (error) { - return {} - } - }) - const { args } = parsedLogs.find(({ name }) => name === 'TransactionAdded') - return args -} - const logStatus = (transferId, status) => { const { origin, dest } = status const { explorer, name, id } = networks[dest.chainId] @@ -731,11 +715,11 @@ const logStatus = (transferId, status) => { } module.exports = { + delayABI, getXCalledEvents, fetchOriginXCall, fetchDestinationXCall, getSupportedChainsByDomainId, getDelayModule, - fetchDataFromTx, logStatus, } diff --git a/governance/scripts/bridge/execTx.js b/governance/scripts/bridge/execTx.js index e2e6577ee4a..a0123721287 100644 --- a/governance/scripts/bridge/execTx.js +++ b/governance/scripts/bridge/execTx.js @@ -7,7 +7,8 @@ * */ -const { getDelayModule, fetchDataFromTx, logStatus } = require('./_lib') +const { getDelayModule, logStatus, delayABI } = require('./_lib') +const { fetchDataFromTx } = require('../../helpers/tx') const fs = require('fs-extra') const { getNetwork } = require('@unlock-protocol/hardhat-helpers') @@ -18,7 +19,10 @@ 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 }) + const { to, value, data, operation } = await fetchDataFromTx({ + txHash, + abi: delayABI, + }) // make sure tx is scheduled correctly const txHashExec = await delayMod.getTransactionHash( diff --git a/governance/scripts/gov/execute.js b/governance/scripts/gov/execute.js index fd0c9ba588e..8cfa391a965 100644 --- a/governance/scripts/gov/execute.js +++ b/governance/scripts/gov/execute.js @@ -10,16 +10,23 @@ const { isAlreadyPast, } = require('../../helpers/gov') -async function main({ proposal, govAddress }) { +async function main({ proposal, proposalId, txId, govAddress }) { // env settings const { chainId } = await ethers.provider.getNetwork() const isDev = chainId === 31337 || process.env.RUN_FORK - if (!proposal) { - throw new Error('GOV EXEC > Missing proposal.') + if (!proposal && !proposalId) { + throw new Error('GOV EXEC > Missing proposal or proposalId.') + } + if (proposalId && !txId) { + throw new Error( + 'GOV EXEC > The tx id of the proposal creation is required to execute the proposal.' + ) + } + + if (!proposalId) { + proposalId = proposal.proposalId || (await getProposalId(proposal)) } - console.log(proposal) - const proposalId = proposal.proposalId || (await getProposalId(proposal)) // contract instance etc let state = await getProposalState(proposalId, govAddress) @@ -52,13 +59,14 @@ async function main({ proposal, govAddress }) { } // execute the tx - const tx = await executeProposal({ proposal, govAddress }) + const tx = await executeProposal({ proposal, govAddress, txId }) const receipt = await tx.wait() const { event, hash } = await getEvent(receipt, 'ProposalExecuted') if (event) { // eslint-disable-next-line no-console console.log(`GOV EXEC > Proposal executed successfully (txid: ${hash})`) } + return receipt } else if (state === 'Executed') { console.log('GOV EXEC > Proposal has already been executed') } else { diff --git a/governance/scripts/gov/index.js b/governance/scripts/gov/index.js index 6b2f11d3911..be0e6106d3b 100644 --- a/governance/scripts/gov/index.js +++ b/governance/scripts/gov/index.js @@ -10,7 +10,7 @@ const vote = require('./vote') const queue = require('./queue') const execute = require('./execute') -async function main({ proposal, govAddress }) { +async function main({ proposal, proposalId, govAddress, txId }) { const [signer] = await ethers.getSigners() const { chainId } = await ethers.provider.getNetwork() @@ -52,11 +52,33 @@ async function main({ proposal, govAddress }) { await mine(10) } - // Run the gov workflow - const proposalId = await submit({ proposal, govAddress }) + // Submit the proposal if necessary + if (!proposalId) { + proposalId = await submit({ proposal, govAddress }) + } + + // votes await vote({ proposalId, govAddress, voterAddress: signer.address }) - await queue({ proposal, govAddress }) - await execute({ proposal, govAddress }) + + const udtWhales = [ + '0xa39b44c4AFfbb56b76a1BF1d19Eb93a5DfC2EBA9', // Unlock Labs + '0xF5C28ce24Acf47849988f147d5C75787c0103534', // unlock-protocol.eth + '0xc0948A2f0B48A2AA8474f3DF54FD7C364225AD7d', // @_Cryptosmonitor + '0xD2BC5cb641aE6f7A880c3dD5Aee0450b5210BE23', // stellaachenbach.eth + '0xCA7632327567796e51920F6b16373e92c7823854', // dannithomx.eth + ] + await Promise.all( + udtWhales.map((voterAddress) => + vote({ proposalId, govAddress, voterAddress }) + ) + ) + + // Run the gov workflow + await queue({ proposalId, govAddress, txId }) + const { logs } = await execute({ proposalId, txId, proposal, govAddress }) + + // log all events + console.log(logs) } // execute as standalone diff --git a/governance/scripts/gov/queue.js b/governance/scripts/gov/queue.js index c001e2809b7..79f545ddceb 100644 --- a/governance/scripts/gov/queue.js +++ b/governance/scripts/gov/queue.js @@ -1,5 +1,6 @@ const { ethers } = require('hardhat') const { mineUpTo } = require('@nomicfoundation/hardhat-network-helpers') +const { getProposalArgsFromTx } = require('../../helpers/gov') const { queueProposal, @@ -12,11 +13,22 @@ const { const { GovernorUnlockProtocol } = require('@unlock-protocol/contracts') const { getEvent } = require('@unlock-protocol/hardhat-helpers') -async function main({ proposal, govAddress }) { +async function main({ proposalId, txId, proposal, govAddress }) { + if (!proposal && !proposalId) { + throw new Error('GOV QUEUE > Missing proposal or proposalId.') + } + if (proposalId && !txId) { + throw new Error( + 'GOV QUEUE > The tx id of the proposal creation is required to execute the proposal.' + ) + } + // env settings const { chainId } = await ethers.provider.getNetwork() const isDev = chainId === 31337 || process.env.RUN_FORK - const proposalId = proposal.proposalId || (await getProposalId(proposal)) + if (!proposalId) { + proposalId = proposal.proposalId || (await getProposalId(proposal)) + } if (!proposalId) { throw new Error('GOV QUEUE > Missing proposal ID.') @@ -46,7 +58,7 @@ async function main({ proposal, govAddress }) { // queue proposal if (state === 'Succeeded') { - const tx = await queueProposal({ proposal, govAddress }) + const tx = await queueProposal({ proposal, govAddress, txId, proposalId }) const receipt = await tx.wait() const { event, hash } = await getEvent(receipt, 'ProposalQueued') const { eta } = event.args diff --git a/governance/scripts/gov/submit.js b/governance/scripts/gov/submit.js index 0241bd7271d..822b815d0f9 100644 --- a/governance/scripts/gov/submit.js +++ b/governance/scripts/gov/submit.js @@ -1,12 +1,8 @@ const { ethers } = require('hardhat') const { submitProposal } = require('../../helpers/gov') -const { impersonate, getEvent } = require('@unlock-protocol/hardhat-helpers') - -async function main({ proposal, proposerAddress, govAddress }) { - // env settings - const { chainId } = await ethers.provider.getNetwork() - const isDev = chainId === 31337 +const { getEvent } = require('@unlock-protocol/hardhat-helpers') +async function main({ proposal, govAddress }) { // log what is happening console.log( `GOV SUBMIT > Proposed (${proposal.calls.length} calls):\n${proposal.calls @@ -18,18 +14,10 @@ async function main({ proposal, proposerAddress, govAddress }) { ) // submit the proposal - if (isDev || process.env.RUN_MAINNET_FORK) { - // eslint-disable-next-line no-console - console.log('GOV SUBMIT (dev) > Impersonate proposer ') - await impersonate(proposerAddress) - } - const proposalTx = await submitProposal({ - proposerAddress, proposal, govAddress, }) - const receipt = await proposalTx.wait() const { event, hash } = await getEvent(receipt, 'ProposalCreated') @@ -46,7 +34,7 @@ async function main({ proposal, proposerAddress, govAddress }) { `GOV SUBMIT > proposal submitted: ${await proposalId.toString()} (txid: ${hash}, block: ${currentBlock})` ) - return proposalId + return proposalId, hash } // execute as standalone diff --git a/governance/tasks/gov.js b/governance/tasks/gov.js index 8dffa7815a2..7f37242e123 100644 --- a/governance/tasks/gov.js +++ b/governance/tasks/gov.js @@ -2,19 +2,32 @@ const { task } = require('hardhat/config') const { resolve } = require('path') -task('gov', 'Submit (and validate) a proposal to UDT Governor contract') - .addParam('proposal', 'The file containing the proposal') +task('gov', 'Test execution of the entire proposal lifecycle') + .addOptionalParam('proposal', 'The file containing the proposal') + .addOptionalParam('proposalId', 'The id of an existing proposal') + .addOptionalParam('txId', 'The id of the tx where the proposal was submitted') .addParam('govAddress', 'The address of the Governor contract') .addOptionalVariadicPositionalParam( 'params', 'List of params to pass to the proposal function' ) - .setAction(async ({ proposal: proposalPath, govAddress, params }) => { - const { loadProposal } = require('../helpers/gov') - const proposal = await loadProposal(resolve(proposalPath), params) - const processProposal = require('../scripts/gov') - return await processProposal({ proposal, govAddress }) - }) + .setAction( + async ({ + proposal: proposalPath, + txId, + proposalId, + govAddress, + params, + }) => { + let proposal + if (!proposalId) { + const { loadProposal } = require('../helpers/gov') + proposal = await loadProposal(resolve(proposalPath), params) + } + const processProposal = require('../scripts/gov') + return await processProposal({ proposal, govAddress, proposalId, txId }) + } + ) /** * Governor Workflow @@ -69,55 +82,93 @@ task('gov:vote', 'Vote for a proposal on UDT Governor contract') ) task('gov:queue', 'Queue proposal in timelock') - .addParam('proposal', 'The file containing the proposal') .addParam('govAddress', 'The address of the Governor contract') + .addOptionalParam('proposal', 'The file containing the proposal') + .addOptionalParam('proposalId', 'The id of an existing proposal') + .addOptionalParam('txId', 'The id of the tx where the proposal was submitted') .addOptionalVariadicPositionalParam( 'params', 'List of params to pass to the proposal function' ) - .setAction(async ({ proposal: proposalPath, govAddress, params }) => { - const queueProposal = require('../scripts/gov/queue') - const { loadProposal } = require('../helpers/gov') - const proposal = await loadProposal(resolve(proposalPath), params) - return await queueProposal({ proposal, govAddress }) - }) + .setAction( + async ({ + proposal: proposalPath, + govAddress, + params, + proposalId, + txId, + }) => { + const queueProposal = require('../scripts/gov/queue') + let proposal + if (!proposalId) { + const { loadProposal } = require('../helpers/gov') + proposal = await loadProposal(resolve(proposalPath), params) + } + return await queueProposal({ proposal, govAddress, proposalId, txId }) + } + ) task('gov:execute', 'Closing vote period and execute a proposal (local only)') - .addParam('proposal', 'The file containing the proposal') + .addOptionalParam('proposal', 'The file containing the proposal') + .addOptionalParam('proposalId', 'The id of an existing proposal') + .addOptionalParam('txId', 'The id of the tx where the proposal was submitted') .addParam('govAddress', 'The address of the Governor contract') .addOptionalVariadicPositionalParam( 'params', 'List of params to pass to the proposal function' ) - .setAction(async ({ proposal: proposalPath, govAddress, params }) => { - const executeProposal = require('../scripts/gov/execute') - const { loadProposal } = require('../helpers/gov') - const proposal = await loadProposal(resolve(proposalPath), params) - return await executeProposal({ proposal, govAddress, params }) - }) + .setAction( + async ({ + proposal: proposalPath, + govAddress, + params, + proposalId, + txId, + }) => { + const executeProposal = require('../scripts/gov/execute') + let proposal + if (!proposalId) { + const { loadProposal } = require('../helpers/gov') + proposal = await loadProposal(resolve(proposalPath), params) + } + return await executeProposal({ + proposal, + govAddress, + params, + txId, + proposalId, + }) + } + ) /** * Governor Utils */ task('gov:votes', 'Show votes for a specific proposal') - .addParam('proposal', 'The file containing the proposal') + .addOptionalParam('proposal', 'The file containing the proposal') + .addOptionalParam('proposalId', 'The id of the proposal') .addParam('govAddress', 'The address of the Governor contract') .addOptionalVariadicPositionalParam( 'params', 'List of params to pass to the proposal function' ) .setAction( - async ({ proposal: proposalPath, govAddress, params }, { ethers }) => { + async ( + { proposal: proposalPath, proposalId, govAddress, params }, + { ethers } + ) => { const { getProposalVotes, getProposalId, getQuorum, } = require('../helpers/gov') - const { loadProposal } = require('../helpers/gov') - const proposal = await loadProposal(resolve(proposalPath), params) - const proposalId = - proposal.proposalId || (await getProposalId(proposal, govAddress)) + if (!proposalId) { + const { loadProposal } = require('../helpers/gov') + const proposal = await loadProposal(resolve(proposalPath), params) + proposalId = + proposal.proposalId || (await getProposalId(proposal, govAddress)) + } const { againstVotes, forVotes, abstainVotes } = await getProposalVotes( proposalId, govAddress @@ -191,3 +242,29 @@ task('gov:delegate', 'Delagate voting power') govAddress, }) }) + +task('gov:show', 'Show content of proposal') + .addParam('govAddress', 'The address of the Governor contract') + .addOptionalParam('proposal', 'The file containing the proposal') + .addOptionalParam('proposalId', 'The id of an existing proposal') + .addOptionalParam('txId', 'The id of the tx where the proposal was submitted') + .setAction( + async ({ + proposal: proposalPath, + govAddress, + params, + proposalId, + txId, + }) => { + const { loadProposal, parseProposal } = require('../helpers/gov') + let proposal + if (!proposalId) { + const { loadProposal } = require('../helpers/gov') + proposal = await loadProposal(resolve(proposalPath), params) + } else { + console.log('load from tx') + proposal = await parseProposal({ txId, govAddress }) + } + console.log(proposal) + } + ) diff --git a/governance/test/fixtures/proposal-000-example.js b/governance/test/fixtures/proposal-000-example.js index c232cd8f7aa..df3cd95ed47 100644 --- a/governance/test/fixtures/proposal-000-example.js +++ b/governance/test/fixtures/proposal-000-example.js @@ -1,7 +1,6 @@ const ethers = require('ethers') const { UnlockDiscountTokenV2 } = require('@unlock-protocol/contracts') const tokenRecipientAddress = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' -const proposerAddress = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' module.exports = { proposalName: '#000 This is just an example!', @@ -13,6 +12,5 @@ module.exports = { functionArgs: [tokenRecipientAddress, ethers.parseEther('0.01')], }, ], - proposerAddress, // no payable value specified default to 0 } diff --git a/governance/test/gov.test.js b/governance/test/gov.test.js index 61aaf63e524..992a4580fd4 100644 --- a/governance/test/gov.test.js +++ b/governance/test/gov.test.js @@ -1,4 +1,7 @@ -const { GovernorUnlockProtocol } = require('@unlock-protocol/contracts') +const { + GovernorUnlockProtocol, + GovernorUnlockProtocolTimelock, +} = require('@unlock-protocol/contracts') const { ethers } = require('hardhat') const { assert } = require('chai') const { @@ -8,10 +11,12 @@ const { parseProposal, getProposalId, getProposalIdFromContract, + submitProposal, + getProposalArgsFromTx, } = require('../helpers/gov') const tokenRecipientAddress = '0x8d533d1A48b0D5ddDEF513A0B0a3677E991F3915' // ramdomly generated but deterministic for tests -const { ADRESS_ZERO } = require('@unlock-protocol/hardhat-helpers') +const { ADDRESS_ZERO } = require('@unlock-protocol/hardhat-helpers') const contractNameOrAbi = require('@unlock-protocol/hardhat-helpers/dist/ABIs/erc20.json') const functionName = 'transfer' @@ -47,7 +52,7 @@ describe('Proposal Helper', () => { describe('parseProposal', () => { it('parse gov args correctly', async () => { - const contractAddress = ADRESS_ZERO + const contractAddress = ADDRESS_ZERO const proposalName = 'Send some tokens to a grantee' const encoded = await encodeProposalArgs({ @@ -56,20 +61,25 @@ describe('Proposal Helper', () => { functionArgs, }) - const [to, value, calldata, proposalNameParsed] = await parseProposal({ - calls: [{ contractNameOrAbi, contractAddress, calldata: encoded }], - proposalName, - }) + const { targets, values, calldatas, descriptionHash } = + await parseProposal({ + calls: [{ contractNameOrAbi, contractAddress, calldata: encoded }], + proposalName, + }) + + const proposalNameHashed = ethers.keccak256( + ethers.toUtf8Bytes(proposalName) + ) - assert.equal(to[0], contractAddress) - assert.equal(value[0], 0) - assert.equal(calldata[0], [calldataEncoded]) - assert.equal(proposalNameParsed, proposalName) + assert.equal(targets[0], ADDRESS_ZERO) + assert.equal(values[0], 0) + assert.equal(calldatas[0], [calldataEncoded]) + assert.equal(descriptionHash, proposalNameHashed) }) }) describe('proposal ID', () => { - it('can be retrieved', async () => { + it('can be retrieved from chain', async () => { const proposalExample = await loadProposal( '../test/fixtures/proposal-000-example.js' ) @@ -77,10 +87,10 @@ describe('Proposal Helper', () => { const { abi, bytecode } = GovernorUnlockProtocol const Gov = await ethers.getContractFactory(abi, bytecode) const gov = await Gov.deploy() - const proposalIdFromContract = await getProposalIdFromContract( - proposalExample, - await gov.getAddress() - ) + const proposalIdFromContract = await getProposalIdFromContract({ + proposal: proposalExample, + govAddress: await gov.getAddress(), + }) assert.equal(proposalId.toString(), proposalIdFromContract.toString()) }) }) diff --git a/packages/hardhat-helpers/src/fork.js b/packages/hardhat-helpers/src/fork.js index c6d925491a8..f1cf16759fe 100644 --- a/packages/hardhat-helpers/src/fork.js +++ b/packages/hardhat-helpers/src/fork.js @@ -78,7 +78,7 @@ const parseForkUrl = (networks) => { } const resetNodeState = async () => { - const { ethers, network, config } = require('hardhat') + const { network, config } = require('hardhat') // reset fork const { forking } = config.networks.hardhat await network.provider.request({ @@ -94,10 +94,7 @@ const resetNodeState = async () => { }) } -const addSomeETH = async ( - address, - amount = ethers.utils.parseEther('1000') -) => { +const addSomeETH = async (address, amount = ethers.parseEther('1000')) => { const { network } = require('hardhat') const balance = `0x${BigInt(amount.toString()).toString(16)}` await network.provider.send('hardhat_setBalance', [address, balance])