Skip to content

Commit

Permalink
fix: allow governance execution without dao spoke contract (#3114)
Browse files Browse the repository at this point in the history
* fix: allow governance execution without dao spoke contract

* feat: add governance hub-only test

* fix: remove hardhat console
  • Loading branch information
leric7 authored Feb 24, 2025
1 parent 0937065 commit 1b7d81f
Show file tree
Hide file tree
Showing 10 changed files with 1,112 additions and 40 deletions.
6 changes: 6 additions & 0 deletions packages/core/contracts/governance/MetaHumanGovernor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,12 @@ contract MetaHumanGovernor is

uint256 spokeContractsLength = spokeContractsSnapshots[proposalId]
.length;

// If there are no spoke contracts, finish the collection phase
if (spokeContractsLength == 0) {
_finishCollectionPhase(proposalId);
}

// Get a price of sending the message back to hub
uint256 sendMessageToHubCost = quoteCrossChainMessage(chainId, 0);

Expand Down
9 changes: 9 additions & 0 deletions packages/core/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,15 @@ const config: HardhatUserConfig = {
spacing: 2,
format: 'json',
},
{
path: './abis/governance',
runOnCompile: true,
clear: true,
flat: true,
only: ['contracts/governance/[a-zA-Z]*.sol'],
spacing: 2,
format: 'json',
},
],
etherscan: {
apiKey: {
Expand Down
35 changes: 9 additions & 26 deletions packages/core/scripts/create-proposal.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
import { ethers, network } from 'hardhat';
import { ethers } from 'hardhat';
import dotenv from 'dotenv';
import { getProposal } from './proposal';

dotenv.config();

async function main() {
const deployerPrivateKey = process.env.PRIVATE_KEY;
const governorAddress = process.env.GOVERNOR_ADDRESS || '';
const hmtTokenAddress = process.env.HMT_TOKEN_ADDRESS || '';
const description = process.env.DESCRIPTION || '';

if (
!deployerPrivateKey ||
!governorAddress ||
!hmtTokenAddress ||
!description
) {
if (!deployerPrivateKey || !governorAddress) {
throw new Error('One or more required environment variables are missing.');
}

Expand All @@ -25,25 +19,14 @@ async function main() {
deployerSigner
);

const hmToken = await ethers.getContractAt('IERC20', hmtTokenAddress);

const encodedCall = hmToken.interface.encodeFunctionData('transfer', [
await deployerSigner.getAddress(),
ethers.parseEther('1'),
]);

// Proposal data
const targets = [hmtTokenAddress];
const values = [0];
const calldatas = [encodedCall];

const proposal = await getProposal();
// Create proposal
const transactionResponse = await governanceContract.crossChainPropose(
targets,
values,
calldatas,
description,
{ value: ethers.parseEther('0.1') }
proposal.targets,
proposal.values,
proposal.calldatas,
proposal.description,
{ value: ethers.parseEther('0.01') }
);

await transactionResponse.wait();
Expand Down
30 changes: 17 additions & 13 deletions packages/core/scripts/deploy-hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,27 @@ async function main() {
const [deployer] = await ethers.getSigners();

// HMTDeployment
const HMToken = await ethers.getContractFactory(
'contracts/HMToken.sol:HMToken'
);
const HMTokenContract = await HMToken.deploy(
1000000000,
'HUMAN Token',
18,
'HMT'
);
await HMTokenContract.waitForDeployment();
console.log('HMToken Address: ', await HMTokenContract.getAddress());
// const HMToken = await ethers.getContractFactory(
// 'contracts/HMToken.sol:HMToken'
// );
// const HMTokenContract = await HMToken.deploy(
// 1000000000,
// 'HUMAN Token',
// 18,
// 'HMT'
// );
// await HMTokenContract.waitForDeployment();
// console.log('HMToken Address: ', await HMTokenContract.getAddress());
const hmtTokenAddress = process.env.HMT_TOKEN_ADDRESS || '';
if (!hmtTokenAddress) {
throw new Error('HMT Token Address is missing');
}

//vHMT Deployment
// vHMT Deployment
const VHMToken = await ethers.getContractFactory(
'contracts/governance/vhm-token/VHMToken.sol:VHMToken'
);
const VHMTokenContract = await VHMToken.deploy(HMTokenContract.getAddress());
const VHMTokenContract = await VHMToken.deploy(hmtTokenAddress);
await VHMTokenContract.waitForDeployment();
console.log('VHMToken deployed to:', await VHMTokenContract.getAddress());

Expand Down
37 changes: 37 additions & 0 deletions packages/core/scripts/deploy-vhmt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* eslint-disable no-console */
import { ethers } from 'hardhat';
import dotenv from 'dotenv';
dotenv.config();

async function main() {
// HMTDeployment
// const HMToken = await ethers.getContractFactory(
// 'contracts/HMToken.sol:HMToken'
// );
// const HMTokenContract = await HMToken.deploy(
// 1000000000,
// 'HUMAN Token',
// 18,
// 'HMT'
// );
// await HMTokenContract.waitForDeployment();
// console.log('HMToken Address: ', await HMTokenContract.getAddress());

const hmtTokenAddress = process.env.HMT_TOKEN_ADDRESS || '';
if (!hmtTokenAddress) {
throw new Error('HMT Token Address is missing');
}

//vHMT Deployment
const VHMToken = await ethers.getContractFactory(
'contracts/governance/vhm-token/VHMToken.sol:VHMToken'
);
const VHMTokenContract = await VHMToken.deploy(hmtTokenAddress);
await VHMTokenContract.waitForDeployment();
console.log('VHMToken deployed to:', await VHMTokenContract.getAddress());
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
57 changes: 57 additions & 0 deletions packages/core/scripts/proposal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ethers } from 'hardhat';
import dotenv from 'dotenv';

dotenv.config();

const abiCoder = ethers.AbiCoder.defaultAbiCoder();

export const getProposal = async () => {
const deployerPrivateKey = process.env.PRIVATE_KEY;
const governorAddress = process.env.GOVERNOR_ADDRESS || '';
const description = process.env.DESCRIPTION || '';

if (!deployerPrivateKey || !governorAddress || !description) {
throw new Error('One or more required environment variables are missing.');
}

const deployerSigner = new ethers.Wallet(deployerPrivateKey, ethers.provider);
const governanceContract = await ethers.getContractAt(
'MetaHumanGovernor',
governorAddress,
deployerSigner
);

const encodedCall = governanceContract.interface.encodeFunctionData(
'setVotingPeriod',
[86400]
);

// Proposal data
const targets = [governorAddress];
const values = [0];
const calldatas = [encodedCall];

// Example inputs (replace with actual values)
const descriptionHash = ethers.id(description);

// Encode the data similar to Solidity's `abi.encode`
const encodedData = abiCoder.encode(
['address[]', 'uint256[]', 'bytes[]', 'bytes32'],
[targets, values, calldatas, descriptionHash]
);

// Compute the keccak256 hash
const hash = ethers.keccak256(encodedData);

// Convert to uint256 (BigNumber)
const proposalId = ethers.toBigInt(hash);

return {
proposalId,
targets,
values,
calldatas,
description,
descriptionHash,
};
};
42 changes: 42 additions & 0 deletions packages/core/scripts/queue-proposal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ethers } from 'hardhat';
import dotenv from 'dotenv';
import { getProposal } from './proposal';

dotenv.config();

async function main() {
const deployerPrivateKey = process.env.PRIVATE_KEY;
const governorAddress = process.env.GOVERNOR_ADDRESS || '';

if (!deployerPrivateKey || !governorAddress) {
throw new Error('One or more required environment variables are missing.');
}

const deployerSigner = new ethers.Wallet(deployerPrivateKey, ethers.provider);
const governanceContract = await ethers.getContractAt(
'MetaHumanGovernor',
governorAddress,
deployerSigner
);

const proposal = await getProposal();

const transactionResponse = await governanceContract.cancel(
proposal.targets,
proposal.values,
proposal.calldatas,
proposal.descriptionHash
);

console.log(transactionResponse);

await transactionResponse.wait();
console.log('Proposal queued:');
}

main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
2 changes: 1 addition & 1 deletion packages/core/scripts/update-spokes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ async function main() {
);

const spokeContracts = spokeAddresses.map((address, index) => ({
contractAddress: ethers.zeroPadBytes(governorAddress, 32),
contractAddress: ethers.zeroPadBytes(address, 32),
chainId: spokeChainIds[index],
}));

Expand Down
109 changes: 109 additions & 0 deletions packages/core/test/GovernanceUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,3 +427,112 @@ export async function updateVotingDelay(
newDelay.toString()
);
}

export async function createBasicProposalOnHub(
voteToken: VHMToken,
governor: MetaHumanGovernor,
owner: Signer
): Promise<string> {
const encodedCall = voteToken.interface.encodeFunctionData('transfer', [
await owner.getAddress(),
ethers.parseEther('1'),
]);
const targets = [await voteToken.getAddress()];
const values = [0];
const calldatas = [encodedCall];

const txResponse = await governor.crossChainPropose(
targets,
values,
calldatas,
'',
{
value: 100,
}
);
const receipt = await txResponse.wait();
const eventSignature = ethers.id(
'ProposalCreated(uint256,address,address[],uint256[],string[],bytes[],uint256,uint256,string)'
);

const event = receipt?.logs?.find((log) => log.topics[0] === eventSignature);

if (!event) throw new Error('ProposalCreated event not found');

const decodedData = governor.interface.decodeEventLog(
'ProposalCreated',
event.data,
event.topics
);

const proposalId = decodedData[0];

return proposalId;
}

export async function updateVotingDelayOnHub(
voteToken: VHMToken,
governor: MetaHumanGovernor,
newDelay: number,
executer: Signer
): Promise<void> {
// mock account with voting power
await voteToken.transfer(await executer.getAddress(), ethers.parseEther('5'));
await voteToken.connect(executer).delegate(await executer.getAddress());

const encodedCall = governor.interface.encodeFunctionData('setVotingDelay', [
newDelay,
]);
const targets = [await governor.getAddress()];
const values = [0];
const calldatas = [encodedCall];

const txResponse = await governor.crossChainPropose(
targets,
values,
calldatas,
'setVotingDelay',
{
value: 100,
}
);
const receipt = await txResponse.wait();
const eventSignature = ethers.id(
'ProposalCreated(uint256,address,address[],uint256[],string[],bytes[],uint256,uint256,string)'
);

const event = receipt?.logs?.find((log) => log.topics[0] === eventSignature);

if (!event) throw new Error('ProposalCreated event not found');

const decodedData = governor.interface.decodeEventLog(
'ProposalCreated',
event.data,
event.topics
);

const proposalId = decodedData[0];

// wait for next block
await mineNBlocks(2);

//cast vote
await governor.connect(executer).castVote(proposalId, 1);

// wait for voting block to end
await mineNBlocks(50410);

await governor.requestCollections(proposalId, { value: 100 });

await governor.queue(targets, values, calldatas, ethers.id('setVotingDelay'));
await governor.execute(
targets,
values,
calldatas,
ethers.id('setVotingDelay')
);

expect((await governor.votingDelay()).toString()).to.equal(
newDelay.toString()
);
}
Loading

0 comments on commit 1b7d81f

Please sign in to comment.