diff --git a/packages/financial-templates-lib/src/helpers/multicall.ts b/packages/financial-templates-lib/src/helpers/multicall.ts index cafe851acc..ba32709de6 100644 --- a/packages/financial-templates-lib/src/helpers/multicall.ts +++ b/packages/financial-templates-lib/src/helpers/multicall.ts @@ -4,6 +4,7 @@ import type { MulticallWeb3 } from "@uma/contracts-node"; import { TransactionDataDecoder } from "./AbiUtils"; import assert from "assert"; import type Web3 from "web3"; +import type { TransactionReceipt } from "web3-core"; // Decode `returnData` into Javascript type using known contract ABI informtaion // from the `callData` originally used to produce `returnData`. @@ -42,6 +43,22 @@ export const aggregateTransactionsAndCall = async ( return returnData.map((data, i) => _decodeOutput(transactions[i].callData, data, web3)); }; +export const aggregateTransactionsAndSend = async ( + multicallAddress: string, + web3: Web3, + transactions: Transaction[], + txnConfigObj: any +): Promise => { + const multicallContract = (new web3.eth.Contract(getAbi("Multicall"), multicallAddress) as unknown) as MulticallWeb3; + for (let i = 0; i < transactions.length; i++) { + assert( + transactions[i].target && transactions[i].callData, + "transaction expected in form {target: address, callData: bytes}" + ); + } + return await multicallContract.methods.aggregate((transactions as unknown) as [string, string][]).send(txnConfigObj); +}; + export const multicallAddressMap: { [network: string]: { multicall: string } } = { mainnet: { multicall: "0xeefba1e63905ef1d7acba5a8513c70307c1ce441" }, kovan: { multicall: "0x2cc8688c5f75e365aaeeb4ea8d6a480405a48d2a" }, diff --git a/packages/scripts/src/admin-proposals/README.md b/packages/scripts/src/admin-proposals/README.md index f739c79245..4ae6d68497 100644 --- a/packages/scripts/src/admin-proposals/README.md +++ b/packages/scripts/src/admin-proposals/README.md @@ -27,7 +27,8 @@ HARDHAT_CHAIN_ID=1 yarn hardhat node --fork https://mainnet.infura.io/v3/YOUR-IN Be sure to set `NODE_URL_1=http://localhost:9545` in your environment if you want to continue running scripts against the forked mainnet network. -2. Request to impersonate accounts we'll need to propose and vote on admin proposals: +2. Request to impersonate accounts we'll need to propose and vote on admin proposals. This script will also mint the + wallet enough UMA required to submit a new governance proposal. ```sh ./packages/scripts/setupFork.sh @@ -62,6 +63,14 @@ node ./packages/scripts/src/admin-proposals/collateral.js \ --network mainnet-fork ``` +6. Retrieve bond staked for submitting proposal: + +``` +node ./packages/scripts/src/admin-proposals/resolveProposal.js \ + --sender 0x2bAaA41d155ad8a4126184950B31F50A1513cE25 \ + --network mainnet-fork +``` + ## Running on a public network in production mode For production, simply run the script with a production network passed to the `--network` flag (along with other params like --keys) like so: `node ... --network mainnet_gckms --keys deployer`. diff --git a/packages/scripts/src/admin-proposals/resolveProposal.js b/packages/scripts/src/admin-proposals/resolveProposal.js new file mode 100644 index 0000000000..1f4f331242 --- /dev/null +++ b/packages/scripts/src/admin-proposals/resolveProposal.js @@ -0,0 +1,103 @@ +// Description: +// - Resolve all possible proposals for a specific sender. + +// Usage: +// NODE_URL_1=http://localhost:9545 \ +// node ./packages/scripts/src/admin-proposals/resolveProposal.js \ +// --network mainnet_mnemonic \ +// --sender
+ +require("dotenv").config(); +const { getWeb3ByChainId } = require("@uma/common"); +const { setupGasEstimator } = require("./utils"); +const { + aggregateTransactionsAndSend, + multicallAddressMap, + aggregateTransactionsAndCall, +} = require("@uma/financial-templates-lib"); +const { _getContractAddressByName } = require("../utils"); + +const hre = require("hardhat"); +const { getContract } = hre; +const Proposer = getContract("Proposer"); +const Governor = getContract("Governor"); +const argv = require("minimist")(process.argv.slice(), { + string: [ + // proposer address to resolve proposals for + "sender", + ], +}); + +async function run() { + const web3 = getWeb3ByChainId(1); + const accounts = await web3.eth.getAccounts(); + const sender = argv.sender; + + // Initialize Eth contracts by grabbing deployed addresses from networks/1.json file. + const gasEstimator = await setupGasEstimator(); + + const proposer = new web3.eth.Contract(Proposer.abi, await _getContractAddressByName("Proposer", 1)); + const governor = new web3.eth.Contract(Governor.abi, await _getContractAddressByName("Governor", 1)); + + // Query the state of all possible bonded proposals and determine which ones can be resolved. If the `lockedBond` + // property of the `bondedProposal` is 0 then the proposal cannot be resolved. + const totalProposals = Number(await governor.methods.numProposals().call()); + const bondedProposalTransactions = [...Array(totalProposals).keys()].map((i) => { + return { target: proposer.options.address, callData: proposer.methods.bondedProposals(i).encodeABI() }; + }); + + console.group(`\n📢 There are a total of ${totalProposals.length} possible resolvable proposals`); + + // Read bonded proposal state in a single batched web3 call: + console.log("- Reading bondedProposals() state for all ids..."); + const bondedProposals = await aggregateTransactionsAndCall( + multicallAddressMap.mainnet.multicall, + web3, + bondedProposalTransactions + ); + + const proposalToResolve = []; + for (let i = 0; i < bondedProposals.length; i++) { + if (web3.utils.toBN(bondedProposals[i].lockedBond.toString()).gt(web3.utils.toBN("0"))) { + console.log( + `- Proposal #${i} has a locked bond of ${bondedProposals[i].lockedBond.toString()} UMA, proposer was @ ${ + bondedProposals[i].sender + }` + ); + if (bondedProposals[i].sender === sender) { + proposalToResolve.push(i); + } else { + console.log(`Skipping proposal #${i}, only resolving bonded proposals where sender is ${sender}`); + } + } + } + + if (proposalToResolve.length === 0) { + console.groupEnd("No proposals to resolve."); + } else { + console.log(`- Resolving ${proposalToResolve.length} proposals: ${proposalToResolve}`); + const resolveProposalTransactions = proposalToResolve.map((i) => { + return { target: proposer.options.address, callData: proposer.methods.resolveProposal(i).encodeABI() }; + }); + const txn = await aggregateTransactionsAndSend( + multicallAddressMap.mainnet.multicall, + web3, + resolveProposalTransactions, + { from: accounts[0], ...gasEstimator.getCurrentFastPrice() } + ); + console.groupEnd("Transaction: ", txn?.transactionHash); + } +} + +function main() { + const startTime = Date.now(); + run() + .catch((err) => { + console.error(err); + }) + .finally(() => { + const timeElapsed = Date.now() - startTime; + console.log(`Done in ${(timeElapsed / 1000).toFixed(2)}s`); + }); +} +main(); diff --git a/packages/scripts/src/admin-proposals/utils.js b/packages/scripts/src/admin-proposals/utils.js index 1abab32af7..6232a8a68c 100644 --- a/packages/scripts/src/admin-proposals/utils.js +++ b/packages/scripts/src/admin-proposals/utils.js @@ -2,7 +2,7 @@ const hre = require("hardhat"); const { getContract } = hre; const { getWeb3ByChainId, interfaceName, MAX_UINT_VAL } = require("@uma/common"); const { _getContractAddressByName } = require("../utils"); -const { GasEstimator, aggregateTransactionsAndCall, multicallAddressMap } = require("@uma/financial-templates-lib"); +const { GasEstimator } = require("@uma/financial-templates-lib"); const winston = require("winston"); const Web3 = require("Web3"); const { fromWei, toBN } = Web3.utils; @@ -254,8 +254,6 @@ const verifyGovernanceRootTunnelMessage = async (targetAddress, message, governo }; const proposeAdminTransactions = async (web3, adminProposalTransactions, caller, gasPriceObj) => { - await resolveProposals(web3, caller); - const proposer = new web3.eth.Contract(Proposer.abi, await _getContractAddressByName("Proposer", 1)); console.group(`\n📨 Sending to proposer @ ${proposer.options.address}`); console.log(`- Admin proposal contains ${adminProposalTransactions.length} transactions`); @@ -305,41 +303,6 @@ const proposeAdminTransactions = async (web3, adminProposalTransactions, caller, console.groupEnd(); }; -const resolveProposals = async (web3, caller, proposalsToLookback = 10) => { - const proposer = new web3.eth.Contract(Proposer.abi, await _getContractAddressByName("Proposer", 1)); - const governor = new web3.eth.Contract(Governor.abi, await _getContractAddressByName("Governor", 1)); - - console.group(`\nResolving the latest ${proposalsToLookback} proposals`); - const totalProposals = Number(await governor.methods.numProposals().call()); - const bondedProposalTransactions = [...Array(totalProposals).keys()].map((i) => { - return { target: proposer.options.address, callData: proposer.methods.bondedProposals(i).encodeABI() }; - }); - const bondedProposals = await aggregateTransactionsAndCall( - multicallAddressMap.mainnet.multicall, - web3, - bondedProposalTransactions - ); - for (let i = bondedProposals.length - 1; i >= bondedProposals.length - 1 - proposalsToLookback; i--) { - const bondedProposal = bondedProposals[i]; - if (bondedProposal.sender === caller && toBN(bondedProposal.lockedBond.toString()).gt(toBN(0))) { - console.log( - `- Proposal #${i} has a locked bond of ${bondedProposal.lockedBond.toString()} UMA, proposer was @ ${ - bondedProposal.sender - }` - ); - try { - const txn = await proposer.methods.resolveProposal(i).send({ from: caller }); - console.log("- Transaction: ", txn?.transactionHash); - } catch (err) { - console.log("- Resolution failed, has price been resolved for admin proposal?", err); - continue; - } - } - } - console.log("- No more proposals to resolve"); - console.groupEnd(); -}; - module.exports = { L2_ADMIN_NETWORK_NAMES, L2_ADMIN_NETWORKS,