diff --git a/.gitignore b/.gitignore index fa68b87eb..d936a1228 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ yarn-error.log* deployments deployed_contracts.json managementDAOTX.json +packages/contracts/scripts/management-dao-proposal/files-to-merge/ # generated generated diff --git a/README.md b/README.md index 9983779a2..350039b05 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,8 @@ You can find more details about [our deployment checklist here](https://github.c Follow [our update checklist here](https://github.com/aragon/osx/blob/develop/UPDATE_CHECKLIST.md). +Also check the [README](./packages/contracts/scripts/management-dao-proposal/README.md) of the Management DAO proposal generation flow. + ## Pull request commands Certain actions can be triggered via a command to a pull request. To issue a command just comment on a pull request with one of these commands. diff --git a/packages/contracts/deploy/new/40_finalize-management-dao/20_register-management-dao-on-dao-registry.ts b/packages/contracts/deploy/new/40_finalize-management-dao/20_register-management-dao-on-dao-registry.ts index 9dc23c84f..7444680f6 100644 --- a/packages/contracts/deploy/new/40_finalize-management-dao/20_register-management-dao-on-dao-registry.ts +++ b/packages/contracts/deploy/new/40_finalize-management-dao/20_register-management-dao-on-dao-registry.ts @@ -82,10 +82,15 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { let metadataCIDPath = '0x'; if (!isLocal(hre.network)) { + if (!process.env.PUB_PINATA_JWT) { + throw new Error('PUB_PINATA_JWT is not set'); + } + // Upload the metadata to IPFS metadataCIDPath = await uploadToPinata( - JSON.stringify(MANAGEMENT_DAO_METADATA, null, 2), - `management-dao-metadata` + MANAGEMENT_DAO_METADATA, + `management-dao-metadata`, + process.env.PUB_PINATA_JWT ); } diff --git a/packages/contracts/deploy/update/to_v1.4.0/01_Info.ts b/packages/contracts/deploy/update/to_v1.4.0/01_Info.ts new file mode 100644 index 000000000..643998f87 --- /dev/null +++ b/packages/contracts/deploy/update/to_v1.4.0/01_Info.ts @@ -0,0 +1,24 @@ +import {DeployFunction} from 'hardhat-deploy/types'; +import {HardhatRuntimeEnvironment} from 'hardhat/types'; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + console.log('\nInfo: Updating to version 1.4.0'); + + hre.proposalInfo = { + proposalTitle: 'Upgrade OSx Protocol to version 1.4.0', + proposalSummary: + 'Upgrade OSx Protocol to version 1.4.0, and release Admin Plugin v1.2, Multisig v1.3 and TokenVoting v1.3', + proposalResources: [ + { + name: 'audit report', + url: 'https://github.com/aragon/osx/tree/main/audits', + }, + ], + // Adjust this values + proposalStartDate: 0, + proposalEndDate: Math.ceil(Date.now() / 1000), + }; +}; +export default func; +func.tags = ['Info', 'v1.4.0']; +func.dependencies = ['Env']; diff --git a/packages/contracts/deploy/update/to_v1.4.0/10_DAOFactory.ts b/packages/contracts/deploy/update/to_v1.4.0/10_DAOFactory.ts index c3fc286e7..331e186e0 100644 --- a/packages/contracts/deploy/update/to_v1.4.0/10_DAOFactory.ts +++ b/packages/contracts/deploy/update/to_v1.4.0/10_DAOFactory.ts @@ -52,7 +52,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { to: managementDAOAddress, value: 0, data: calldata, - description: `Grant the REGISTER_DAO_PERMISSION_ID permission on the DAORegistry (${daoRegistryAddress}) to the new DAOFactory (${deployResult.address}).`, + description: `\n- Grant the **REGISTER_DAO_PERMISSION_ID** permission on the **DAORegistry** (\`${daoRegistryAddress}\`) to the new **DAOFactory** (\`${deployResult.address}\`).`, }); }; export default func; diff --git a/packages/contracts/deploy/update/to_v1.4.0/20_PluginRepoFactory.ts b/packages/contracts/deploy/update/to_v1.4.0/20_PluginRepoFactory.ts index 7eb42616c..8968e93f1 100644 --- a/packages/contracts/deploy/update/to_v1.4.0/20_PluginRepoFactory.ts +++ b/packages/contracts/deploy/update/to_v1.4.0/20_PluginRepoFactory.ts @@ -63,7 +63,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { to: managementDAOAddress, value: 0, data: calldata, - description: `Moves the REGISTER_PLUGIN_REPO_PERMISSION permission on the PluginRepoRegistry (${pluginRepoRegistryAddress}) from the old PluginRepoFactory (${previousPluginRepoFactoryAddress}) to the new PluginRepoFactory (${deployResult.address}).`, + description: `\n- Moves the **REGISTER_PLUGIN_REPO_PERMISSION_ID** permission on the **PluginRepoRegistry** (\`${pluginRepoRegistryAddress}\`) from the old **PluginRepoFactory** (\`${previousPluginRepoFactoryAddress}\`) to the new **PluginRepoFactory** (\`${deployResult.address}\`).`, }); }; export default func; diff --git a/packages/contracts/deploy/update/to_v1.4.0/31_DAORegistry.ts b/packages/contracts/deploy/update/to_v1.4.0/31_DAORegistry.ts index c11ac384e..a28b5beb0 100644 --- a/packages/contracts/deploy/update/to_v1.4.0/31_DAORegistry.ts +++ b/packages/contracts/deploy/update/to_v1.4.0/31_DAORegistry.ts @@ -47,7 +47,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { to: upgradeTX.to, data: upgradeTX.data, value: 0, - description: `Upgrade the DaoRegistry (${daoRegistryAddress}) to the new implementation (${result.address}).`, + description: `\n- Upgrade the **DaoRegistry** (\`${daoRegistryAddress}\`) to the new **implementation** (\`${result.address}\`).`, }); }; export default func; diff --git a/packages/contracts/deploy/update/to_v1.4.0/41_PluginRepoRegistry.ts b/packages/contracts/deploy/update/to_v1.4.0/41_PluginRepoRegistry.ts index f86404700..c68abec7c 100644 --- a/packages/contracts/deploy/update/to_v1.4.0/41_PluginRepoRegistry.ts +++ b/packages/contracts/deploy/update/to_v1.4.0/41_PluginRepoRegistry.ts @@ -51,7 +51,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { to: upgradeTX.to, data: upgradeTX.data, value: 0, - description: `Upgrade the PluginRepoRegistry (${pluginRepoRegistryAddress}) to the new implementation (${result.address}).`, + description: `\n- Upgrade the **PluginRepoRegistry** (\`${pluginRepoRegistryAddress}\`) to the new **implementation** (\`${result.address}\`).`, }); }; export default func; diff --git a/packages/contracts/deploy/update/to_v1.4.0/90_ManagingDAO.ts b/packages/contracts/deploy/update/to_v1.4.0/90_ManagingDAO.ts index e09beaf51..db069d86c 100644 --- a/packages/contracts/deploy/update/to_v1.4.0/90_ManagingDAO.ts +++ b/packages/contracts/deploy/update/to_v1.4.0/90_ManagingDAO.ts @@ -37,7 +37,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { to: upgradeTX.to, data: upgradeTX.data, value: 0, - description: `Upgrade the management DAO (${managementDAOAddress}) to the new implementation (${newDaoImplementation}).`, + description: `\n- Upgrade the **management DAO** (\`${managementDAOAddress}\`) to the new **implementation** (\`${newDaoImplementation}\`).`, }); }; export default func; diff --git a/packages/contracts/deploy/verification/99_conclude/00_save-contract-addresses.ts b/packages/contracts/deploy/verification/99_conclude/00_save-contract-addresses.ts index 20782b6aa..370aeb003 100644 --- a/packages/contracts/deploy/verification/99_conclude/00_save-contract-addresses.ts +++ b/packages/contracts/deploy/verification/99_conclude/00_save-contract-addresses.ts @@ -43,11 +43,16 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { } const storeInfo = { + proposalInfo: hre.proposalInfo, deployedContractAddresses, managementDAOActions: hre.managementDAOActions, }; - await fs.writeFile('deployed_contracts.json', JSON.stringify(storeInfo)); + await fs.writeFile( + 'deployed_contracts.json', + JSON.stringify(storeInfo, null, 2), + 'utf-8' + ); }; export default func; func.tags = ['New', 'Conclude', 'ConcludeEnd']; diff --git a/packages/contracts/hardhat.config.ts b/packages/contracts/hardhat.config.ts index 0f8f74039..f8b0a59f5 100644 --- a/packages/contracts/hardhat.config.ts +++ b/packages/contracts/hardhat.config.ts @@ -104,7 +104,12 @@ const config: HardhatUserConfig = { gasPrice: 80000000000, deploy: ENABLE_DEPLOY_TEST ? ['./deploy'] - : ['./deploy/env', './deploy/new', './deploy/verification'], + : [ + './deploy/env', + './deploy/new', + './deploy/verification', + './deploy/update', + ], }, localhost: { deploy: ENABLE_DEPLOY_TEST diff --git a/packages/contracts/package.json b/packages/contracts/package.json index e8f544165..69874020f 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -32,7 +32,8 @@ "prepublishOnly": "yarn build && yarn build:npm", "docgen": "hardhat docgen", "docs": "DOCS=true scripts/prepare-docs.sh", - "clean": "rm -rf artifacts cache deployments typechain" + "clean": "rm -rf artifacts cache deployments typechain", + "generate-proposal": "ts-node scripts/management-dao-proposal/generate-managing-dao-proposal-info.ts" }, "repository": { "type": "git", @@ -52,7 +53,7 @@ }, "devDependencies": { "@aragon/osx-commons-configs": "^0.8.0", - "@aragon/osx-commons-sdk": "^0.1.0", + "@aragon/osx-commons-sdk": "^0.2.0", "@aragon/osx-ethers-v1.2.0": "npm:@aragon/osx-ethers@1.2.0", "@aragon/osx-v1.0.1": "npm:@aragon/osx@1.0.1", "@aragon/osx-v1.3.0": "npm:@aragon/osx@1.3.0", diff --git a/packages/contracts/scripts/management-dao-proposal/README.md b/packages/contracts/scripts/management-dao-proposal/README.md new file mode 100644 index 000000000..c19ca3fdd --- /dev/null +++ b/packages/contracts/scripts/management-dao-proposal/README.md @@ -0,0 +1,66 @@ +## Description + +This script merges the plugin proposal actions defined in the files inside the `files-to-merge` folder and the `deployed_contracts.json` file into the `merged-proposals.json` file. + +## How it works + +Before running this script, you need to deploy the plugins and the framework. + +To deploy the framework, run the deploy script in the `packages/contracts` folder: + +``` +yarn deploy --network --tags +``` + +For deploying version `1.4.0` on Sepolia, you can run: + +``` +yarn deploy --network sepolia --tags v1.4.0,VerifyEnd,ConcludeEnd +``` + +This will generate a `deployed_contracts.json` file in the `packages/contracts` folder that will be used by this script to merge proposal actions. The proposal information will be included (for more details, check the first deployment step in the `01_Info.ts` file). You can modify this information as needed (elements like the proposal end date might need modification). + +After deploying the framework, go to each plugin repository and run the deploy script: + +``` +yarn deploy --network --tags +``` + +For deploying and publishing a new version on Sepolia, you can run: + +``` +yarn deploy --network sepolia --tags NewVersion,Verification +``` + +This deployment will generate a `createVersionProposalData-network.json` file that you need to copy to the `./files-to-merge` folder. + +Once you have both the `deployed_contracts.json` file and the plugin deployment json files, you can run this script to merge the proposal actions: + +``` +yarn generate-proposal +``` + +This will generate two files in the `./generated` folder: + +- A `merged-proposals.json` file with all the proposal and deployment details +- A `calldata.json` file with the proposal information and the raw calldata + +## Testing the calldata + +Once all the files are in place: + +- Add `RUN_UPGRADE_1_4_0_TESTS_AT_FORK_BLOCK` to the .env and set a fork block, it is recommended to use a recent block. +- Run `yarn test`. + This will test the call data and simulate the proposal creation and execution. + +## Steps + +1. Deploy the framework +2. Deploy the plugins +3. Copy the deployment json files to the `./files-to-merge` folder +4. Run the script to merge the proposal actions +5. The `./generated` folder will be created with the `merged-proposals.json` file and the `calldata.json` file + +## Note + +To test the deployment and the generated calldata, you can check the tests in the `test/deploy/deployment-1.4.0.ts` file. It will attempt to create, approve, and execute the proposal with the calldata generated by this script. This file is specific for checking the deployment of version `1.4.0`; you might need to modify it for other versions. Also, note that the test is forking the network, so you might need to adjust the network name and the fork block number. diff --git a/packages/contracts/scripts/management-dao-proposal/generate-managing-dao-proposal-info.ts b/packages/contracts/scripts/management-dao-proposal/generate-managing-dao-proposal-info.ts new file mode 100644 index 000000000..071a1128e --- /dev/null +++ b/packages/contracts/scripts/management-dao-proposal/generate-managing-dao-proposal-info.ts @@ -0,0 +1,223 @@ +// note using version 1.3.0 due to is the one currently installed on the managing dao change it once it's upgraded +import {Multisig__factory as Multisig_v1_3_0__factory} from '../../typechain/@aragon/osx-v1.3.0/plugins/governance/multisig/Multisig.sol'; +import {uploadToPinata} from '@aragon/osx-commons-sdk'; +import dotenv from 'dotenv'; +import {ethers} from 'ethers'; +import * as fs from 'fs'; +import * as path from 'path'; + +dotenv.config({path: path.resolve(__dirname, '../../.env')}); + +interface Action { + to: string; + value: number; + data: string; + description: string; +} + +interface ProposalAction { + to: string; + value: number; + data: string; +} + +const deployedContractsPath = path.join( + __dirname, + '../../deployed_contracts.json' +); +const proposalActionsPath = path.join(__dirname, './files-to-merge'); + +const mergedProposalActionsPath = path.join( + __dirname, + './generated/merged-proposals.json' +); + +const calldataPath = path.join(__dirname, './generated/calldata.json'); + +function generateProposalJson() { + // check if the file exists + if (!fs.existsSync(deployedContractsPath)) { + throw new Error('deployed_contracts.json file not found'); + } + + const deployedContractsRaw = fs.readFileSync(deployedContractsPath, 'utf-8'); + // load the osx deployed contracts + const deployedContracts = JSON.parse(deployedContractsRaw); + + // creates the folder if it doesn't exist + if (!fs.existsSync(proposalActionsPath)) { + fs.mkdirSync(proposalActionsPath, {recursive: true}); + + throw new Error('No plugin proposals found in files-to-merge'); + } + + // read the plugin proposals data + const proposalFiles = fs + .readdirSync(proposalActionsPath) + .filter(file => file.endsWith('.json')); + + if (proposalFiles.length === 0) { + throw new Error('No plugin proposals found in files-to-merge'); + } + + // store each plugin action + let tmpAction: Action; + for (const [index, file] of proposalFiles.entries()) { + const filePath = path.join(proposalActionsPath, file); + const dataRaw = fs.readFileSync(filePath, 'utf-8'); + const data = JSON.parse(dataRaw); + + // Validate that the JSON has both 'actions' and 'proposalDescription' + if ( + !data.actions || + !Array.isArray(data.actions) || + data.actions.length === 0 || + !data.proposalDescription || + !data.actions[0].to || + data.actions[0].value === undefined || + !data.actions[0].data + ) { + console.error( + `File ${file} is missing required fields ('actions' and/or 'proposalDescription'). Skipping.` + ); + continue; + } + + let proposalDescription = data.proposalDescription; + if (index === 0) { + proposalDescription = + '\n\n# Publish New Plguin Versions' + data.proposalDescription; + } + + tmpAction = { + to: data.actions[0]?.to, + value: data.actions[0]?.value, + data: data.actions[0]?.data, + description: proposalDescription, + }; + + deployedContracts.managementDAOActions.push(tmpAction); + } + + // Create directory if it doesn't exist + const mergedProposalDir = path.dirname(mergedProposalActionsPath); + if (!fs.existsSync(mergedProposalDir)) { + fs.mkdirSync(mergedProposalDir, {recursive: true}); + } + + // write the updated JSON back to a new file + fs.writeFileSync( + mergedProposalActionsPath, + JSON.stringify(deployedContracts, null, 2), + 'utf-8' + ); + + console.log( + `Successfully created merged-proposals.json with all proposal actions in ${mergedProposalActionsPath}!` + ); +} + +function generateHexCalldataInJson(functionArgs: any[]) { + const abi = Multisig_v1_3_0__factory.abi; + const iface = new ethers.utils.Interface(abi); + + const calldata = iface.encodeFunctionData('createProposal', functionArgs); + + const jsonOutput = { + functionName: 'createProposal', + functionArgs: functionArgs, + calldata: calldata, + }; + + // write the call information in the json file + fs.writeFileSync(calldataPath, JSON.stringify(jsonOutput, null, 2), 'utf-8'); + console.log( + `Successfully created calldata.json with the function call information in ${calldataPath}!` + ); +} + +async function main() { + generateProposalJson(); + + // Check if merged proposals file exists + if (!fs.existsSync(mergedProposalActionsPath)) { + throw new Error(`File not found: ${mergedProposalActionsPath}`); + } + + // get the actions to send it to the createHexCalldata function + const jsonFile = JSON.parse( + fs.readFileSync(mergedProposalActionsPath, 'utf-8') + ); + + let args = []; + + if (!jsonFile.managementDAOActions) { + throw new Error('No actions found in merged-proposals.json'); + } + // remove the description from the actions + let proposalActions: ProposalAction[] = []; + let proposalMetadataFullDescription: string = ''; + proposalMetadataFullDescription = '# Protocol Upgrade'; + jsonFile.managementDAOActions.forEach((action: Action) => { + proposalActions.push({ + to: action.to, + value: action.value, + data: action.data, + }); + proposalMetadataFullDescription += action.description + ' '; + }); + + // this should be adjusted based on the actual proposal + if (!jsonFile.proposalInfo) { + throw new Error('No proposal info found in merged-proposals.json'); + } + + const metadata = { + title: jsonFile.proposalInfo.proposalTitle, + summary: jsonFile.proposalInfo.proposalSummary, + description: proposalMetadataFullDescription, + resources: jsonFile.proposalInfo.proposalResources, + }; + + if (!process.env.PUB_PINATA_JWT) { + throw new Error('PUB_PINATA_JWT is not set'); + } + + const metadataCIDPath = await uploadToPinata( + metadata, + `management-dao-proposal-update-v1.4.0-metadata`, + process.env.PUB_PINATA_JWT + ); + + console.log('Uploaded proposal metadata:', metadataCIDPath); + + // push the metadata + args.push(ethers.utils.hexlify(ethers.utils.toUtf8Bytes(metadataCIDPath))); + // push the actions + args.push(proposalActions); + + // push allow failure map => to zero + args.push(0); + + // push approve proposal => to false + args.push(false); + + // push try execution => to false + args.push(false); + + // push the start and end dates + args.push(jsonFile.proposalInfo.proposalStartDate); + args.push(jsonFile.proposalInfo.proposalEndDate); + + // generate the calldata in a json file + generateHexCalldataInJson(args); +} + +main() + .then(() => { + console.log('done!'); + }) + .catch(error => { + console.error('Error in main:', error); + process.exit(1); + }); diff --git a/packages/contracts/src/test/Migration.sol b/packages/contracts/src/test/Migration.sol index 0eb78ee6c..49db5d76b 100644 --- a/packages/contracts/src/test/Migration.sol +++ b/packages/contracts/src/test/Migration.sol @@ -40,6 +40,9 @@ import {PluginRepoRegistry as PluginRepoRegistry_v1_3_0} from "@aragon/osx-v1.3. import {ENSSubdomainRegistrar as ENSSubdomainRegistrar_v1_0_0} from "@aragon/osx-v1.0.1/framework/utils/ens/ENSSubdomainRegistrar.sol"; import {ENSSubdomainRegistrar as ENSSubdomainRegistrar_v1_3_0} from "@aragon/osx-v1.3.0/framework/utils/ens/ENSSubdomainRegistrar.sol"; +// needed in the script to generate the managing dao proposal when upgrading +import {Multisig as Multisig_v1_3_0} from "@aragon/osx-v1.3.0/plugins/governance/multisig/Multisig.sol"; + // Integration Testing import {ProxyFactory} from "@aragon/osx-commons-contracts/src/utils/deployment/ProxyFactory.sol"; diff --git a/packages/contracts/test/deploy/deployment-1.4.0.ts b/packages/contracts/test/deploy/deployment-1.4.0.ts new file mode 100644 index 000000000..027d61545 --- /dev/null +++ b/packages/contracts/test/deploy/deployment-1.4.0.ts @@ -0,0 +1,469 @@ +import {getLatestContractAddress} from '../../deploy/helpers'; +import {DAO__factory, PluginRepo__factory} from '../../typechain'; +import {Multisig__factory as Multisig_v1_3_0__factory} from '../../typechain/@aragon/osx-v1.3.0/plugins/governance/multisig/Multisig.sol'; +import {closeFork, initForkForOsxVersion} from '../test-utils/fixture'; +import { + DAO_REGISTRY_PERMISSIONS, + PLUGIN_REGISTRY_PERMISSIONS, +} from '@aragon/osx-commons-sdk'; +import {expect} from 'chai'; +import {defaultAbiCoder} from 'ethers/lib/utils'; +import * as fs from 'fs'; +import hre, {ethers} from 'hardhat'; +import * as path from 'path'; + +const FORK_BLOCK_NUMBER = process.env.RUN_UPGRADE_1_4_0_TESTS_AT_FORK_BLOCK + ? parseInt(process.env.RUN_UPGRADE_1_4_0_TESTS_AT_FORK_BLOCK, 10) + : 0; // Default value if not set + +// change to test on a different network +const NETWORK = 'sepolia'; + +const mergedProposalActionsPath = path.join( + __dirname, + '../../scripts/management-dao-proposal/generated/merged-proposals.json' +); + +const calldataPath = path.join( + __dirname, + '../../scripts/management-dao-proposal/generated/calldata.json' +); + +const daoAddress = '0xca834b3f404c97273f34e108029eed776144d324'; +const daoMultisigAddr = '0xfcead61339e3e73090b587968fce8b090e0600ef'; +const daoMultisigMembers = [ + '0x25cd4b8a02a8f9e920eb02fac38c2954694a3fa5', + '0x3ffe3f16d47a54b1c6a3f47c9e6ff5c2c1b32859', + '0x42342037e0fc34c130cdb079139f8ae56d38453f', + '0xaf2c536f9af22548829b20e9afc567259c820c62', + '0xdf62645a2c714febbf6060d1fb607e7eccef0659', +]; + +const IMPLEMENTATION_ADDRESS_SLOT = + '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc'; + +type OldAddresses = { + daoRegistry: string; + pluginRepoRegistry: string; + oldDaoFactory: string; + oldPluginRepoFactory: string; + oldDaoRegistryImplementation: string; + oldPluginRepoRegistryImplementation: string; +}; +type Addresses = { + daoFactory: string; + pluginRepoFactory: string; + daoRegistryImplementation: string; + pluginRepoRegistryImplementation: string; + adminRepo: string; + tokenVotingRepo: string; + multisigRepo: string; + adminPluginSetup: string; + tokenVotingPluginSetup: string; + multisigPluginSetup: string; +}; + +async function forkNetwork(network: string) { + hre.network.deploy = ['./deploy/update/to_v1.4.0']; + + await initForkForOsxVersion(network, { + version: '1.3.0', + forkBlockNumber: FORK_BLOCK_NUMBER, + activeContracts: [], + }); +} + +function getCalldataJson() { + // read calldata json + const calldataJson = JSON.parse(fs.readFileSync(calldataPath, 'utf8')); + + return calldataJson; +} + +function getAddressFromDescription(description: string): string { + const address = description.split("at **'")[1].split("'**")[0]; + return address; +} + +function getAddress(name: string) { + return getLatestContractAddress(name, hre); +} + +function getOldAddresses(): OldAddresses { + return { + daoRegistry: getAddress('DAORegistryProxy'), + pluginRepoRegistry: getAddress('PluginRepoRegistryProxy'), + oldDaoFactory: getAddress('DAOFactory'), + oldPluginRepoFactory: getAddress('PluginRepoFactory'), + oldDaoRegistryImplementation: getAddress('DAORegistryImplementation'), + oldPluginRepoRegistryImplementation: getAddress( + 'PluginRepoRegistryImplementation' + ), + }; +} + +function getAddresses(): Addresses { + const addresses = JSON.parse( + fs.readFileSync(mergedProposalActionsPath, 'utf8') + ); + + // Find Admin plugin setup address from managementDAOActions + let adminIdx = -1; + let multisigIdx = -1; + let tokenVotingIdx = -1; + for (let i = 0; i < addresses.managementDAOActions.length; i++) { + const action = addresses.managementDAOActions[i]; + if (action.description.includes('AdminSetup')) { + adminIdx = i; + } else if (action.description.includes('TokenVotingSetup')) { + tokenVotingIdx = i; + } else if (action.description.includes('MultisigSetup')) { + multisigIdx = i; + } + } + if (adminIdx === -1 || multisigIdx === -1 || tokenVotingIdx === -1) { + throw new Error('Admin, Multisig, or TokenVotingSetup not found'); + } + + return { + daoFactory: addresses.deployedContractAddresses.DAOFactory, + pluginRepoFactory: addresses.deployedContractAddresses.PluginRepoFactory, + daoRegistryImplementation: + addresses.deployedContractAddresses.DAORegistryImplementation, + pluginRepoRegistryImplementation: + addresses.deployedContractAddresses.PluginRepoRegistryImplementation, + adminRepo: addresses.managementDAOActions[adminIdx].to, + tokenVotingRepo: addresses.managementDAOActions[tokenVotingIdx].to, + multisigRepo: addresses.managementDAOActions[multisigIdx].to, + adminPluginSetup: getAddressFromDescription( + addresses.managementDAOActions[adminIdx].description + ), + tokenVotingPluginSetup: getAddressFromDescription( + addresses.managementDAOActions[tokenVotingIdx].description + ), + multisigPluginSetup: getAddressFromDescription( + addresses.managementDAOActions[multisigIdx].description + ), + }; +} + +async function impersonateAccount(addr: string) { + await hre.network.provider.send('hardhat_setBalance', [ + addr, + ethers.utils.parseUnits('3000', 'ether').toHexString(), + ]); + + await hre.network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [addr], + }); + + return ethers.getSigner(addr); +} + +function getMultisigEvents( + receipt: any, + eventName: string, + multisig: any +): any { + let event: any; + for (const log of receipt.logs) { + const parsedLog = multisig.interface.parseLog(log); + if (parsedLog.name === eventName) { + event = parsedLog; + } + } + return event; +} + +// this function is deployment 1.4.0 specific adjust it for future deployments +async function checkStatusAfterProposal() { + // Actions + // 1- grant REGISTER_DAO_PERMISSION_ID to the new DAOFactory + // 2- grant REGISTER_PLUGIN_REPO_PERMISSION to the new PluginRepoFactory and revoke it on the old PluginRepoFactory + // 3- upgrade the DAORegistry implementation + // 4- upgrade the PluginRepoRegistry implementation + // 5- upgrade the managing DAO implementation + // 6- deploy new admin version + // 7- deploy new token voting version + // 8- deploy new multisig version + + const member0 = await impersonateAccount(daoMultisigMembers[0]); + const dao = DAO__factory.connect(daoAddress, member0); + + const addresses = getAddresses(); + const oldAddresses = getOldAddresses(); + + // new dao factory has REGISTER_DAO_PERMISSION_ID on the DAORegistry + expect( + await dao.hasPermission( + oldAddresses.daoRegistry, // where + addresses.daoFactory, // who + DAO_REGISTRY_PERMISSIONS.REGISTER_DAO_PERMISSION_ID, // permission id + '0x' // data + ), + 'new dao factory permission' + ).to.be.true; + + // old dao factory has REGISTER_DAO_PERMISSION_ID on the DAORegistry + expect( + await dao.hasPermission( + oldAddresses.daoRegistry, // where + oldAddresses.oldDaoFactory, // who + DAO_REGISTRY_PERMISSIONS.REGISTER_DAO_PERMISSION_ID, // permission id + '0x' // data + ), + 'old dao factory permission' + ).to.be.true; + + // new repo factory has REGISTER_PLUGIN_REPO_PERMISSION on the PluginRepoRegistry + expect( + await dao.hasPermission( + oldAddresses.pluginRepoRegistry, // where + addresses.pluginRepoFactory, // who + PLUGIN_REGISTRY_PERMISSIONS.REGISTER_PLUGIN_REPO_PERMISSION_ID, // permission id + '0x' // data + ), + 'new repo factory permission' + ).to.be.true; + + // old repo factory has not REGISTER_PLUGIN_REPO_PERMISSION on the PluginRepoRegistry + expect( + await dao.hasPermission( + oldAddresses.pluginRepoRegistry, // where + oldAddresses.oldPluginRepoFactory, // who + PLUGIN_REGISTRY_PERMISSIONS.REGISTER_PLUGIN_REPO_PERMISSION_ID, // permission id + '0x' // data + ), + 'old repo factory permission' + ).to.be.false; + + // check the dao registry implementation has changed + const newDaoRegistryImplementation = defaultAbiCoder + .decode( + ['address'], + await ethers.provider.getStorageAt( + oldAddresses.daoRegistry, + IMPLEMENTATION_ADDRESS_SLOT + ) + )[0] + .toLowerCase(); + + expect( + newDaoRegistryImplementation, + 'dao registry implementation' + ).not.to.equal(oldAddresses.oldDaoRegistryImplementation); + expect( + newDaoRegistryImplementation.toLowerCase(), + 'new dao registry implementation' + ).to.equal(addresses.daoRegistryImplementation.toLowerCase()); + + // check the plugin repo registry implementation has changed + const newPluginRepoRegistryImplementation = defaultAbiCoder + .decode( + ['address'], + await ethers.provider.getStorageAt( + oldAddresses.pluginRepoRegistry, + IMPLEMENTATION_ADDRESS_SLOT + ) + )[0] + .toLowerCase(); + + expect( + newPluginRepoRegistryImplementation, + 'plugin repo registry implementation' + ).not.to.equal(oldAddresses.oldPluginRepoRegistryImplementation); + expect( + newPluginRepoRegistryImplementation.toLowerCase(), + 'new plugin repo registry implementation' + ).to.equal(addresses.pluginRepoRegistryImplementation.toLowerCase()); + + // management dao implementation (version) has changed + expect(await dao.protocolVersion(), 'managing dao version').to.deep.equal([ + 1, 4, 0, + ]); + + // check new admin version is deployed with correct setup + const adminRepo = PluginRepo__factory.connect(addresses.adminRepo, member0); + const adminLatestVersion = await adminRepo['getLatestVersion(uint8)'](1); + + expect(adminLatestVersion.tag.release, 'admin release').to.equal(1); + expect(adminLatestVersion.tag.build, 'admin build').to.equal(2); + expect(adminLatestVersion.pluginSetup, 'admin setup').to.deep.equal( + addresses.adminPluginSetup + ); + + // check new token voting version is deployed with correct setup + const tokenVotingRepo = PluginRepo__factory.connect( + addresses.tokenVotingRepo, + member0 + ); + const tokenVotingLatestVersion = await tokenVotingRepo[ + 'getLatestVersion(uint8)' + ](1); + + expect(tokenVotingLatestVersion.tag.release, 'tokenVoting release').to.equal( + 1 + ); + expect(tokenVotingLatestVersion.tag.build, 'tokenVoting build').to.equal(3); + expect(tokenVotingLatestVersion.pluginSetup, 'tokenVoting setup').to.equal( + addresses.tokenVotingPluginSetup + ); + + // check new multisig version is deployed with correct setup + const multisigRepo = PluginRepo__factory.connect( + addresses.multisigRepo, + member0 + ); + const multisigLatestVersion = await multisigRepo['getLatestVersion(uint8)']( + 1 + ); + + expect(multisigLatestVersion.tag.release, 'multisig release').to.equal(1); + expect(multisigLatestVersion.tag.build, 'multisig build').to.equal(3); + expect(multisigLatestVersion.pluginSetup, 'multisig setup').to.equal( + addresses.multisigPluginSetup + ); +} + +if (process.env.RUN_UPGRADE_1_4_0_TESTS_AT_FORK_BLOCK) { + describe('1.4.0 Upgrade Deployment', function () { + let calldataJson: any; + + beforeEach(async () => { + await forkNetwork(NETWORK); + console.log('forked network: ', NETWORK); + + calldataJson = getCalldataJson(); + }); + + // Close fork so that other tests(not related to this file) are + // not run in forked network. + afterEach(async () => { + closeFork(); + }); + + it('test the proposal can be created', async () => { + const member0 = await impersonateAccount(daoMultisigMembers[0]); + const multisig = Multisig_v1_3_0__factory.connect( + daoMultisigAddr, + member0 + ); + + // get proposal count before + const proposalCountBefore = await multisig.proposalCount(); + + // execute the generated calldata + let tx = await member0.sendTransaction({ + to: multisig.address, + data: calldataJson.calldata, + gasLimit: 3000000, + }); + + let receipt = await tx.wait(); + + const proposalCountAfter = await multisig.proposalCount(); + + // check proposal count is increased + expect(proposalCountAfter).to.be.greaterThan( + ethers.BigNumber.from(proposalCountBefore) + ); + + // check proposal created event + let proposalCreatedEvent = getMultisigEvents( + receipt, + 'ProposalCreated', + multisig + ); + + const proposalId = proposalCreatedEvent.args.proposalId; + expect(proposalCreatedEvent).to.not.be.undefined; + expect(proposalCreatedEvent.args.creator).to.equal(member0.address); + expect(proposalCreatedEvent.args.endDate).to.equal( + calldataJson.functionArgs[calldataJson.functionArgs.length - 1] + ); + expect(proposalCreatedEvent.args.actions.length).to.equal( + calldataJson.functionArgs[1].length + ); + expect(proposalCreatedEvent.args.actions.length).to.equal( + calldataJson.functionArgs[1].length + ); + + // get proposal and check the info + let proposal = await multisig.getProposal(proposalId); + + expect(proposal.executed).to.be.false; + expect(proposal.approvals).to.equal(0); + expect(proposal.actions.length).to.equal( + calldataJson.functionArgs[1].length + ); + + // impersonate member1 member2 and member3 to approve proposal + const member1 = await impersonateAccount(daoMultisigMembers[1]); + const member2 = await impersonateAccount(daoMultisigMembers[2]); + const member3 = await impersonateAccount(daoMultisigMembers[3]); + + const multisigAsMember1 = Multisig_v1_3_0__factory.connect( + daoMultisigAddr, + member1 + ); + const multisigAsMember2 = Multisig_v1_3_0__factory.connect( + daoMultisigAddr, + member2 + ); + const multisigAsMember3 = Multisig_v1_3_0__factory.connect( + daoMultisigAddr, + member3 + ); + + // vote for the proposal + await multisigAsMember1.approve(proposalId, false); + await multisigAsMember2.approve(proposalId, false); + await multisigAsMember3.approve(proposalId, false); + + // check the members approved the proposal + expect(await multisig.hasApproved(proposalId, member0.address)).to.be + .false; + expect(await multisig.hasApproved(proposalId, member1.address)).to.be + .true; + expect(await multisig.hasApproved(proposalId, member2.address)).to.be + .true; + expect(await multisig.hasApproved(proposalId, member3.address)).to.be + .true; + + // check the proposal can execute + expect(await multisig.canExecute(proposalId)).to.be.true; + + // execute the proposal + tx = await multisig.execute(proposalId); + receipt = await tx.wait(); + + // check the proposal is executed + proposal = await multisig.getProposal(proposalId); + expect(proposal.executed).to.be.true; + + await checkStatusAfterProposal(); + }); + + it('execute all the proposal actions one by one', async () => { + const daoSigner = await impersonateAccount(daoAddress); + + // iterate over the actions and execute them one by one + const actions = calldataJson.functionArgs[1]; + for (const action of actions) { + let tx = await daoSigner.sendTransaction({ + to: action.to, + data: action.data, + }); + } + + await checkStatusAfterProposal(); + }); + }); +} else { + describe.skip('1.4.0 Upgrade Deployment', function () { + it('Skipped because RUN_UPGRADE_1_4_0_TESTS_AT_FORK_BLOCK is not set in .env', function () { + this.skip(); + }); + }); +} diff --git a/packages/contracts/test/deploy/update-1.4.0.ts b/packages/contracts/test/deploy/update-1.4.0.ts index 72a37b616..c52db8368 100644 --- a/packages/contracts/test/deploy/update-1.4.0.ts +++ b/packages/contracts/test/deploy/update-1.4.0.ts @@ -24,6 +24,8 @@ import {expect} from 'chai'; import {defaultAbiCoder} from 'ethers/lib/utils'; import hre, {ethers, deployments} from 'hardhat'; +const FORK_BLOCK_NUMBER = 7805006; + const IMPLEMENTATION_ADDRESS_SLOT = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc'; @@ -45,7 +47,7 @@ async function forkSepolia() { // console.log(hre); await initForkForOsxVersion('sepolia', { version: '1.3.0', - forkBlockNumber: 7296100, + forkBlockNumber: FORK_BLOCK_NUMBER, activeContracts: [], }); } diff --git a/packages/contracts/types/hardhat.d.ts b/packages/contracts/types/hardhat.d.ts index e0dff83ee..3514bd2ff 100644 --- a/packages/contracts/types/hardhat.d.ts +++ b/packages/contracts/types/hardhat.d.ts @@ -27,6 +27,16 @@ declare module 'hardhat/types/runtime' { aragonToVerifyContracts: AragonVerifyEntry[]; managementDAOMultisigPluginAddress: string; placeholderBuildCIDPath: string; + proposalInfo: { + proposalTitle: string; + proposalSummary: string; + proposalResources: { + name: string; + url: string; + }[]; + proposalStartDate: number; + proposalEndDate: number; + }; managementDAOActions: { to: string; value: BigNumberish; diff --git a/yarn.lock b/yarn.lock index 1e9e141cf..fc607f7bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,13 +10,6 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@aragon/osx-commons-configs@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@aragon/osx-commons-configs/-/osx-commons-configs-0.4.0.tgz#5b6ae025de1ccf7f9a135bfbcb0aa822c774acf9" - integrity sha512-/2wIQCbv/spMRdOjRXK0RrXG1TK5aMcbD73RvMgMwQwSrKcA1dCntUuSxmTm2W8eEtOzs8E1VPjqZk0cXL4SSQ== - dependencies: - tslib "^2.6.2" - "@aragon/osx-commons-configs@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@aragon/osx-commons-configs/-/osx-commons-configs-0.8.0.tgz#71e27c7063c3ca7a26a2c5ae12594063800bd9db" @@ -32,20 +25,18 @@ "@openzeppelin/contracts" "4.9.6" "@openzeppelin/contracts-upgradeable" "4.9.6" -"@aragon/osx-commons-sdk@^0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@aragon/osx-commons-sdk/-/osx-commons-sdk-0.1.0.tgz#ec027744e2d9275d1c5535e356fc3607339c44c9" - integrity sha512-T7svAQsfMIpADvAKLnicS7bNV54EE9q8vFUOZy1hh5oF9N/07qD2eeXqXZ9W0uWEKfgpbIyZKV6k+X5kt2kklQ== +"@aragon/osx-commons-sdk@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@aragon/osx-commons-sdk/-/osx-commons-sdk-0.2.0.tgz#e5ca5db172866775a02337c22af3a05ac16541fa" + integrity sha512-bxzvIbxdQtObqLPrJlJcPuy2cMzrE9TI7zlNMcbKnyr4swp44ZHZi5NKcwX1eFAf+oZc1aFlehb1gPwPMvaT8Q== dependencies: - "@aragon/osx-commons-configs" "^0.4.0" + "@aragon/osx-commons-configs" "^0.8.0" "@ethersproject/address" "5.7.0" "@ethersproject/bignumber" "5.7.0" "@ethersproject/contracts" "5.7.0" "@ethersproject/hash" "5.7.0" "@ethersproject/logger" "5.7.0" "@ethersproject/providers" "5.7.2" - dotenv "^16.4.5" - undici "^6.21.0" "@aragon/osx-commons-subgraph@^0.0.4": version "0.0.4" @@ -5441,7 +5432,7 @@ dotenv@^10.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== -dotenv@^16.0.0, dotenv@^16.4.5: +dotenv@^16.0.0: version "16.4.7" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26" integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ== @@ -12405,11 +12396,6 @@ undici@^6.18.2, undici@^6.19.5: resolved "https://registry.yarnpkg.com/undici/-/undici-6.21.0.tgz#4b3d3afaef984e07b48e7620c34ed8a285ed4cd4" integrity sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw== -undici@^6.21.0: - version "6.21.1" - resolved "https://registry.yarnpkg.com/undici/-/undici-6.21.1.tgz#336025a14162e6837e44ad7b819b35b6c6af0e05" - integrity sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ== - unfetch@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be"