@idle-finance/hardhat-proposals-plugin
Version:
Hardhat plugin for governance proposals
355 lines • 18.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AlphaProposalBuilder = exports.AlphaProposal = exports.AlphaProposalState = void 0;
const ethers_1 = require("ethers");
const utils_1 = require("ethers/lib/utils");
const utils_2 = require("ethers/lib/utils");
const plugins_1 = require("hardhat/plugins");
const index_1 = require("../ethers-contracts/index");
const Timelock__factory_1 = require("../ethers-contracts/factories/Timelock__factory");
const constants_1 = require("../constants");
const proposal_1 = require("./proposal");
const util_1 = require("../util");
// ---------- Helper Functions ----------
function loadGovernor(contract, provider) {
// If `contract` is already a type contract
if (contract instanceof ethers_1.Contract) {
return contract;
}
return index_1.GovernorAlpha__factory.connect(contract, provider);
}
function loadVotingToken(contract, provider) {
// If `contract` is already a type contract
if (contract instanceof ethers_1.Contract) {
return contract;
}
return index_1.VotingToken__factory.connect(contract, provider);
}
// -------------------- Define proposal states --------------------
var AlphaProposalState;
(function (AlphaProposalState) {
AlphaProposalState[AlphaProposalState["PENDING"] = 0] = "PENDING";
AlphaProposalState[AlphaProposalState["ACTIVE"] = 1] = "ACTIVE";
AlphaProposalState[AlphaProposalState["CANCELED"] = 2] = "CANCELED";
AlphaProposalState[AlphaProposalState["DEFEATED"] = 3] = "DEFEATED";
AlphaProposalState[AlphaProposalState["SUCCEDED"] = 4] = "SUCCEDED";
AlphaProposalState[AlphaProposalState["QUEUED"] = 5] = "QUEUED";
AlphaProposalState[AlphaProposalState["EXPIRED"] = 6] = "EXPIRED";
AlphaProposalState[AlphaProposalState["EXECUTED"] = 7] = "EXECUTED";
})(AlphaProposalState = exports.AlphaProposalState || (exports.AlphaProposalState = {}));
// -------------------- Define proposal --------------------
class AlphaProposal extends proposal_1.Proposal {
constructor(hre, governor, votingToken) {
super(hre); // call constructor on Proposal
this.state = AlphaProposalState.PENDING;
this.id = ethers_1.BigNumber.from("0");
this.description = "";
this.contracts = [];
this.args = [];
this.governor = governor ? loadGovernor(governor, this.getEthersProvider()) : undefined;
this.votingToken = votingToken ? loadVotingToken(votingToken, this.getEthersProvider()) : undefined;
}
addAction(action) {
super.addAction(action);
this.contracts.push(action.contract);
this.args.push(action.args);
}
setProposer(proposer) { this.proposer = proposer; }
setGovernor(governor) { this.governor = loadGovernor(governor, this.getEthersProvider()); }
setVotingToken(votingToken) {
this.votingToken = loadVotingToken(votingToken, this.getEthersProvider());
}
async propose(proposer) {
if (!this.governor)
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.NO_GOVERNOR);
proposer = proposer ? proposer : this.proposer;
if (!proposer)
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.NO_PROPOSER);
const governorAsProposer = this.governor.connect(proposer);
const proposalId = await governorAsProposer
.callStatic
.propose(this.targets, this.values, this.signatures, this.calldatas, this.description);
await governorAsProposer.propose(this.targets, this.values, this.signatures, this.calldatas, this.description);
this.id = proposalId;
this.markAsSubmitted();
}
async loadProposal(data) {
if (!this.governor)
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.NO_GOVERNOR);
if (!this.votingToken)
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.NO_VOTING_TOKEN);
let id = data;
let proposal = new AlphaProposal(this.hre, this.governor, this.votingToken);
proposal.markAsSubmitted();
proposal.id = ethers_1.BigNumber.from(id);
let proposalInfo = await this.governor.proposals(id);
let actionsInfo = await this.governor.getActions(id);
proposal.proposer = new this.hre.ethers.VoidSigner(proposalInfo.proposer);
proposal.targets = actionsInfo.targets;
proposal.values = actionsInfo[1]; // `values` gets overwrittn by array.values
proposal.signatures = actionsInfo.signatures;
proposal.calldatas = actionsInfo.calldatas;
proposal.description = "<DESCRIPTION NOT LOADED>";
let args = [];
for (let i = 0; i < proposal.targets.length; i++) {
proposal.contracts.push(null); // push null to contracts.
const signature = proposal.signatures[i];
const calldata = proposal.calldatas[i];
const arg = utils_2.defaultAbiCoder.decode([signature], calldata);
args.push(arg);
}
proposal.args = args;
return proposal;
}
async getProposalState() {
if (!this.governor)
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.NO_GOVERNOR);
if (this.internalState != proposal_1.InternalProposalState.SUBMITTED) {
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.PROPOSAL_NOT_SUBMITTED);
}
const proposalState = await this.governor.state(this.id);
return proposalState;
}
async vote(signer, support = true) {
if (!this.governor)
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.NO_GOVERNOR);
if (this.internalState != proposal_1.InternalProposalState.SUBMITTED) {
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.PROPOSAL_NOT_SUBMITTED);
}
let currentState = await this.getProposalState();
if (currentState == AlphaProposalState.ACTIVE) {
await this.governor.connect(signer).castVote(this.id, support);
}
else {
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, `Proposal is not in an active state, received ${currentState}`);
}
}
async queue(signer) {
if (!this.governor)
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.NO_GOVERNOR);
if (this.internalState != proposal_1.InternalProposalState.SUBMITTED) {
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.PROPOSAL_NOT_SUBMITTED);
}
signer = signer ? signer : this.proposer;
if (!signer)
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.NO_SIGNER);
await this.governor.connect(signer).queue(this.id);
}
async execute(signer) {
if (!this.governor)
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.NO_GOVERNOR);
if (this.internalState != proposal_1.InternalProposalState.SUBMITTED) {
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.PROPOSAL_NOT_SUBMITTED);
}
signer = signer ? signer : this.proposer;
if (!signer)
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.NO_SIGNER);
await this.governor.connect(signer).execute(this.id);
}
/**
* This method will simulate the proposal using the full on-chain process.
* This may take significant time depending on the hardware you are using.
*
* @notice For this method to work the proposal must have a proposer with enough votes to reach quorem
*/
async _fullSimulate() {
if (!this.governor)
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.NO_GOVERNOR);
if (!this.votingToken)
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.NO_VOTING_TOKEN);
if (!this.proposer)
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.NO_PROPOSER);
let provider = this.getEthersProvider();
let proposerAddress = await this.proposer.getAddress();
let quoremVotes = await this.governor.quorumVotes();
let proposerVotes = await this.votingToken.getCurrentVotes(proposerAddress);
if (proposerVotes < quoremVotes)
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.NOT_ENOUGH_VOTES);
let timelock = Timelock__factory_1.Timelock__factory.connect(await this.governor.timelock(), provider);
await this.propose();
let votingDelay = await (await this.governor.votingDelay()).add(1);
await this.mineBlocks(votingDelay);
await this.vote(this.proposer, true);
let votingPeriod = await this.governor.votingPeriod();
await this.mineBlocks(votingPeriod);
await this.queue();
let delay = await timelock.delay();
let blockInfo = await provider.getBlock("latest");
let endTimeStamp = delay.add(blockInfo.timestamp).add("50").toNumber();
await this.mineBlock(endTimeStamp);
await this.execute();
}
// queues the action to the timelock by impersonating the governor
// advances time in order to execute proposal
// analyses errors
async _simulate() {
if (!this.governor)
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.NO_GOVERNOR);
let provider = this.getEthersProvider();
await provider.send("hardhat_impersonateAccount", [this.governor.address]);
await provider.send("hardhat_setBalance", [this.governor.address, "0xffffffffffffffff"]);
let governorSigner = await this.hre.ethers.getSigner(this.governor.address);
let timelock = Timelock__factory_1.Timelock__factory.connect(await this.governor.timelock(), governorSigner);
await provider.send("hardhat_impersonateAccount", [timelock.address]);
await provider.send("hardhat_setBalance", [timelock.address, "0xffffffffffffffff"]);
let timelockSigner = provider.getSigner(timelock.address);
let blockInfo = await provider.getBlock("latest");
let delay = await timelock.delay();
let eta = delay.add(blockInfo.timestamp).add("50");
await provider.send("evm_setAutomine", [false]);
for (let i = 0; i < this.targets.length; i++) {
await timelock.queueTransaction(this.targets[i], this.values[i], this.signatures[i], this.calldatas[i], eta);
}
await this.mineBlocks(1);
await this.mineBlock(eta.toNumber());
let receipts = new Array();
for (let i = 0; i < this.targets.length; i++) {
await timelock.executeTransaction(this.targets[i], this.values[i], this.signatures[i], this.calldatas[i], eta).then(receipt => { receipts.push(receipt); }, async (timelockError) => {
var _a;
// analyse error
let timelockErrorMessage = timelockError.error.message.match(/^[\w\s:]+'(.*)'$/m)[1];
let contractErrorMesage;
// call the method on the contract as if it was the timelock
// this will produce a more relavent message as to the failure of the action
let contract = await ((_a = this.contracts[i]) === null || _a === void 0 ? void 0 : _a.connect(timelockSigner));
if (contract) {
await contract.callStatic[this.signatures[i]](...this.args[i]).catch(contractError => {
contractErrorMesage = contractError.message.match(/^[\w\s:]+'(.*)'$/m)[1];
});
}
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, `Proposal action ${i} failed.
Target: ${this.targets[i]}
Signature: ${this.signatures[i]}
Args: ${this.args[i]}\n
Timelock revert message: ${timelockErrorMessage}
Contract revert message: ${contractErrorMesage}`);
});
}
await this.mineBlock();
for (let i = 0; i < this.targets.length; i++) {
let r = await receipts[i].wait().catch(r => { return r.receipt; });
if (r.status != 1) {
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, `Action ${i} failed`);
}
}
await provider.send("evm_setAutomine", [true]);
await provider.send("hardhat_stopImpersonatingAccount", [this.governor.address]);
await provider.send("hardhat_stopImpersonatingAccount", [timelock.address]);
}
async printProposalInfo() {
if (!this.governor)
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.NO_GOVERNOR);
if (!this.votingToken)
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.NO_VOTING_TOKEN);
console.log('--------------------------------------------------------');
if (this.internalState == proposal_1.InternalProposalState.SUBMITTED) {
const proposalInfo = await this.governor.proposals(this.id);
const state = await this.getProposalState();
let votingTokenName = await this.votingToken.name();
let votingTokenDecimals = ethers_1.BigNumber.from("10").pow(await this.votingToken.decimals());
console.log(`Id: ${this.id.toString()}`);
console.log(`Description: ${this.description}`);
console.log(`For Votes: ${proposalInfo.forVotes.div(votingTokenDecimals)} ${votingTokenName} Votes`);
console.log(`Agasint Votes: ${proposalInfo.againstVotes.div(votingTokenDecimals)} ${votingTokenName} Votes`);
console.log(`Vote End: ${proposalInfo.endBlock}`);
console.log(`State: ${state.toString()}`);
}
else {
console.log("Unsubmitted proposal");
console.log(`Description: ${this.description}`);
}
for (let i = 0; i < this.targets.length; i++) {
const contract = this.contracts[i];
const target = this.targets[i];
const signature = this.signatures[i];
const value = this.values[i];
const args = this.args[i];
let name = "";
if ((contract === null || contract === void 0 ? void 0 : contract.functions['name()']) != null) {
name = (await contract.functions['name()']()).toString();
}
console.log(`Action ${i}`);
if (name == "") {
console.log(` ├─ target ───── ${target}`);
}
else {
console.log(` ├─ target ───── ${target} (name: ${name})`);
}
if (!value.isZero()) {
console.log(` ├─ value ────── ${ethers_1.utils.formatEther(value.toString())} ETH`);
}
console.log(` ├─ signature ── ${signature}`);
for (let j = 0; j < args.length - 1; j++) {
const arg = args[j];
console.log(` ├─ args [ ${j} ] ─ ${arg}`);
}
console.log(` └─ args [ ${args.length - 1} ] ─ ${args[args.length - 1]}`);
}
}
}
exports.AlphaProposal = AlphaProposal;
// -------------------- Define proposal builder --------------------
class AlphaProposalBuilder extends proposal_1.ProposalBuilder {
constructor(hre, governor, votingToken, maxActions = 15) {
super(hre);
this.maxActions = maxActions;
this.proposal = new AlphaProposal(hre, governor, votingToken);
}
setGovernor(governor) {
this.proposal.setGovernor(governor);
return this;
}
setVotingToken(votingToken) {
this.proposal.setVotingToken(votingToken);
return this;
}
addAction(target, value, signature, calldata) {
if (this.proposal.targets.length >= this.maxActions) {
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.TOO_MANY_ACTIONS);
}
value = util_1.toBigNumber(value);
const contract = null;
const args = utils_2.defaultAbiCoder.decode([signature], calldata);
this.proposal.addAction({ target, value, signature, calldata, contract, args });
return this;
}
addContractAction(contract, method, functionArgs, value) {
if (this.proposal.targets.length >= this.maxActions) {
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, constants_1.errors.TOO_MANY_ACTIONS);
}
value = value ? util_1.toBigNumber(value) : util_1.toBigNumber("0");
const target = contract.address;
const functionFragment = contract.interface.getFunction(method);
const signature = functionFragment.format(utils_1.FormatTypes.sighash);
if (functionFragment.inputs.length != functionArgs.length) {
throw new plugins_1.HardhatPluginError(constants_1.PACKAGE_NAME, "arguments length do not match signature");
}
// encode function call data
const functionData = contract.interface.encodeFunctionData(functionFragment, functionArgs);
const args = contract.interface.decodeFunctionData(functionFragment, functionData);
const calldata = utils_1.hexDataSlice(functionData, 4); // Remove the sighash from the function data
this.proposal.addAction({ target, value, signature, calldata, contract, args });
return this;
}
setProposer(proposer) {
this.proposal.setProposer(proposer);
return this;
}
/**
* Set the description for the proposal
*
* Some UI interfaces for proposals require a newline `\n`
* be added in the description to partition the proposal
* title and the proposal description. It is at the users
* descresion whether to add this or not.
*
* @param description The description field to set for the proposal
*/
setDescription(description) {
this.proposal.description = description;
return this;
}
build() { return this.proposal; }
}
exports.AlphaProposalBuilder = AlphaProposalBuilder;
//# sourceMappingURL=compound-alpha.js.map