From fdc577169bbc25d5b734943518b9f9c5aa7c4133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8r=E2=88=82=C2=A1?= Date: Fri, 5 Jan 2024 14:10:34 +0100 Subject: [PATCH] Adding an E2E test for the member access condition --- packages/contracts/src/MemberAccessPlugin.sol | 2 +- .../src/test/TestGovernancePluginsSetup.sol | 287 ++++++++ .../src/test/TestMemberAccessPlugin.sol | 26 + .../governance-plugins-setup.ts | 319 +-------- .../member-access-condition.ts | 407 +++++++++++ .../integration-testing/plugin-upgrader.ts | 647 ++++++++++++++++++ .../test/integration-testing/space-setup.ts | 311 +-------- .../contracts/test/unit-testing/common.ts | 2 + 8 files changed, 1376 insertions(+), 625 deletions(-) create mode 100644 packages/contracts/src/test/TestGovernancePluginsSetup.sol create mode 100644 packages/contracts/src/test/TestMemberAccessPlugin.sol create mode 100644 packages/contracts/test/integration-testing/member-access-condition.ts create mode 100644 packages/contracts/test/integration-testing/plugin-upgrader.ts diff --git a/packages/contracts/src/MemberAccessPlugin.sol b/packages/contracts/src/MemberAccessPlugin.sol index bd22e29..939e41b 100644 --- a/packages/contracts/src/MemberAccessPlugin.sol +++ b/packages/contracts/src/MemberAccessPlugin.sol @@ -133,7 +133,7 @@ contract MemberAccessPlugin is IMultisig, PluginUUPSUpgradeable, ProposalUpgrade function initialize( IDAO _dao, MultisigSettings calldata _multisigSettings - ) external initializer { + ) external virtual initializer { __PluginUUPSUpgradeable_init(_dao); _updateMultisigSettings(_multisigSettings); diff --git a/packages/contracts/src/test/TestGovernancePluginsSetup.sol b/packages/contracts/src/test/TestGovernancePluginsSetup.sol new file mode 100644 index 0000000..adf9aea --- /dev/null +++ b/packages/contracts/src/test/TestGovernancePluginsSetup.sol @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.8; + +import {PermissionLib} from "@aragon/osx/core/permission/PermissionLib.sol"; +import {DAO} from "@aragon/osx/core/dao/DAO.sol"; +import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; +import {PluginSetup, IPluginSetup} from "@aragon/osx/framework/plugin/setup/PluginSetup.sol"; +import {PluginSetupProcessor} from "@aragon/osx/framework/plugin/setup/PluginSetupProcessor.sol"; +import {TestMemberAccessPlugin} from "./TestMemberAccessPlugin.sol"; +import {MemberAccessExecuteCondition} from "../MemberAccessExecuteCondition.sol"; +import {OnlyPluginUpgraderCondition} from "../OnlyPluginUpgraderCondition.sol"; +import {MainVotingPlugin} from "../MainVotingPlugin.sol"; +import {MajorityVotingBase} from "@aragon/osx/plugins/governance/majority-voting/MajorityVotingBase.sol"; + +// Not ideal, but to test this E2E, the contract needs to be cloned +contract TestGovernancePluginsSetup is PluginSetup { + address private immutable mainVotingPluginImplementationAddr; + address public immutable memberAccessPluginImplementationAddr; + address private immutable pluginSetupProcessor; + + /// @notice Thrown when the array of helpers does not have the correct size + error InvalidHelpers(uint256 actualLength); + + /// @notice Initializes the setup contract + /// @param pluginSetupProcessorAddress The address of the PluginSetupProcessor contract deployed by Aragon on that chain + constructor(PluginSetupProcessor pluginSetupProcessorAddress) { + pluginSetupProcessor = address(pluginSetupProcessorAddress); + mainVotingPluginImplementationAddr = address(new MainVotingPlugin()); + memberAccessPluginImplementationAddr = address(new TestMemberAccessPlugin()); + } + + /// @inheritdoc IPluginSetup + /// @notice Prepares the installation of the two governance plugins in one go + function prepareInstallation( + address _dao, + bytes memory _data + ) external returns (address mainVotingPlugin, PreparedSetupData memory preparedSetupData) { + // Decode the custom installation parameters + ( + MajorityVotingBase.VotingSettings memory _votingSettings, + address[] memory _initialEditors, + uint64 _memberAccessProposalDuration, + address _pluginUpgrader + ) = decodeInstallationParams(_data); + + // Deploy the main voting plugin + mainVotingPlugin = createERC1967Proxy( + mainVotingPluginImplementationAddr, + abi.encodeCall( + MainVotingPlugin.initialize, + (IDAO(_dao), _votingSettings, _initialEditors) + ) + ); + + // Deploy the member access plugin + TestMemberAccessPlugin.MultisigSettings memory _multisigSettings; + _multisigSettings.proposalDuration = _memberAccessProposalDuration; + _multisigSettings.mainVotingPlugin = MainVotingPlugin(mainVotingPlugin); + + address _memberAccessPlugin = createERC1967Proxy( + memberAccessPluginImplementation(), + abi.encodeCall(TestMemberAccessPlugin.initialize, (IDAO(_dao), _multisigSettings)) + ); + + // Condition contract (member access plugin execute) + address _memberAccessExecuteCondition = address( + new MemberAccessExecuteCondition(mainVotingPlugin) + ); + + // List the requested permissions + PermissionLib.MultiTargetPermission[] + memory permissions = new PermissionLib.MultiTargetPermission[]( + _pluginUpgrader == address(0x0) ? 5 : 6 + ); + + // The main voting plugin can execute on the DAO + permissions[0] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Grant, + where: _dao, + who: mainVotingPlugin, + condition: PermissionLib.NO_CONDITION, + permissionId: DAO(payable(_dao)).EXECUTE_PERMISSION_ID() + }); + // The DAO can update the main voting plugin settings + permissions[1] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Grant, + where: mainVotingPlugin, + who: _dao, + condition: PermissionLib.NO_CONDITION, + permissionId: MainVotingPlugin(mainVotingPluginImplementationAddr) + .UPDATE_VOTING_SETTINGS_PERMISSION_ID() + }); + // The DAO can manage the list of addresses + permissions[2] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Grant, + where: mainVotingPlugin, + who: _dao, + condition: PermissionLib.NO_CONDITION, + permissionId: MainVotingPlugin(mainVotingPluginImplementationAddr) + .UPDATE_ADDRESSES_PERMISSION_ID() + }); + + // The member access plugin needs to execute on the DAO + permissions[3] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.GrantWithCondition, + where: _dao, + who: _memberAccessPlugin, + condition: _memberAccessExecuteCondition, + permissionId: DAO(payable(_dao)).EXECUTE_PERMISSION_ID() + }); + // The DAO needs to be able to update the member access plugin settings + permissions[4] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Grant, + where: _memberAccessPlugin, + who: _dao, + condition: PermissionLib.NO_CONDITION, + permissionId: TestMemberAccessPlugin(_memberAccessPlugin) + .UPDATE_MULTISIG_SETTINGS_PERMISSION_ID() + }); + + // The DAO doesn't need APPLY_UPDATE_PERMISSION_ID on the PSP + + // pluginUpgrader permissions + if (_pluginUpgrader != address(0x0)) { + // pluginUpgrader can make the DAO execute applyUpdate + // pluginUpgrader can make the DAO execute grant/revoke + address[] memory _targetPluginAddresses = new address[](2); + _targetPluginAddresses[0] = mainVotingPlugin; + _targetPluginAddresses[1] = _memberAccessPlugin; + OnlyPluginUpgraderCondition _onlyPluginUpgraderCondition = new OnlyPluginUpgraderCondition( + DAO(payable(_dao)), + PluginSetupProcessor(pluginSetupProcessor), + _targetPluginAddresses + ); + permissions[5] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.GrantWithCondition, + where: _dao, + who: _pluginUpgrader, + condition: address(_onlyPluginUpgraderCondition), + permissionId: DAO(payable(_dao)).EXECUTE_PERMISSION_ID() + }); + } + + preparedSetupData.permissions = permissions; + preparedSetupData.helpers = new address[](1); + preparedSetupData.helpers[0] = _memberAccessPlugin; + } + + /// @inheritdoc IPluginSetup + function prepareUninstallation( + address _dao, + SetupPayload calldata _payload + ) external view returns (PermissionLib.MultiTargetPermission[] memory permissionChanges) { + if (_payload.currentHelpers.length != 1) { + revert InvalidHelpers(_payload.currentHelpers.length); + } + + // Decode incoming params + address _pluginUpgrader = decodeUninstallationParams(_payload.data); + address _memberAccessPlugin = _payload.currentHelpers[0]; + + permissionChanges = new PermissionLib.MultiTargetPermission[]( + _pluginUpgrader == address(0x0) ? 5 : 6 + ); + + // Main voting plugin permissions + + // The plugin can no longer execute on the DAO + permissionChanges[0] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Revoke, + where: _dao, + who: _payload.plugin, + condition: address(0), + permissionId: DAO(payable(_dao)).EXECUTE_PERMISSION_ID() + }); + // The DAO can no longer update the plugin settings + permissionChanges[1] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Revoke, + where: _payload.plugin, + who: _dao, + condition: address(0), + permissionId: MainVotingPlugin(mainVotingPluginImplementationAddr) + .UPDATE_VOTING_SETTINGS_PERMISSION_ID() + }); + // The DAO can no longer manage the list of addresses + permissionChanges[2] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Revoke, + where: _payload.plugin, + who: _dao, + condition: address(0), + permissionId: MainVotingPlugin(mainVotingPluginImplementationAddr) + .UPDATE_ADDRESSES_PERMISSION_ID() + }); + + // Member access plugin permissions + + // The plugin can no longer execute on the DAO + permissionChanges[3] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Revoke, + where: _dao, + who: _memberAccessPlugin, + condition: address(0), + permissionId: DAO(payable(_dao)).EXECUTE_PERMISSION_ID() + }); + // The DAO can no longer update the plugin settings + permissionChanges[4] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Revoke, + where: _memberAccessPlugin, + who: _dao, + condition: address(0), + permissionId: TestMemberAccessPlugin(memberAccessPluginImplementation()) + .UPDATE_MULTISIG_SETTINGS_PERMISSION_ID() + }); + + if (_pluginUpgrader != address(0x0)) { + // pluginUpgrader can no longer make the DAO execute applyUpdate + // pluginUpgrader can no longer make the DAO execute grant/revoke + permissionChanges[5] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Revoke, + where: _dao, + who: _pluginUpgrader, + condition: address(0), + permissionId: DAO(payable(_dao)).EXECUTE_PERMISSION_ID() + }); + } + } + + /// @inheritdoc IPluginSetup + function implementation() external view virtual returns (address) { + return mainVotingPluginImplementationAddr; + } + + /// @notice Returns the address of the MemberAccessPlugin implementation + function memberAccessPluginImplementation() public view returns (address) { + return memberAccessPluginImplementationAddr; + } + + /// @notice Encodes the given installation parameters into a byte array + function encodeInstallationParams( + MajorityVotingBase.VotingSettings calldata _votingSettings, + address[] calldata _initialEditors, + uint64 _memberAccessProposalDuration, + address _pluginUpgrader + ) public pure returns (bytes memory) { + return + abi.encode( + _votingSettings, + _initialEditors, + _memberAccessProposalDuration, + _pluginUpgrader + ); + } + + /// @notice Decodes the given byte array into the original installation parameters + function decodeInstallationParams( + bytes memory _data + ) + public + pure + returns ( + MajorityVotingBase.VotingSettings memory votingSettings, + address[] memory initialEditors, + uint64 memberAccessProposalDuration, + address pluginUpgrader + ) + { + (votingSettings, initialEditors, memberAccessProposalDuration, pluginUpgrader) = abi.decode( + _data, + (MajorityVotingBase.VotingSettings, address[], uint64, address) + ); + } + + /// @notice Encodes the given uninstallation parameters into a byte array + function encodeUninstallationParams( + address _pluginUpgrader + ) public pure returns (bytes memory) { + return abi.encode(_pluginUpgrader); + } + + /// @notice Decodes the given byte array into the original uninstallation parameters + function decodeUninstallationParams( + bytes memory _data + ) public pure returns (address pluginUpgrader) { + (pluginUpgrader) = abi.decode(_data, (address)); + } +} diff --git a/packages/contracts/src/test/TestMemberAccessPlugin.sol b/packages/contracts/src/test/TestMemberAccessPlugin.sol new file mode 100644 index 0000000..8eb3da4 --- /dev/null +++ b/packages/contracts/src/test/TestMemberAccessPlugin.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.8; + +import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; +import {MemberAccessPlugin} from "../MemberAccessPlugin.sol"; + +/// @notice A clone of the MemberAccessPlugin contract, just to test +contract TestMemberAccessPlugin is MemberAccessPlugin { + function createArbitraryProposal( + bytes calldata _metadata, + IDAO.Action[] memory _actions + ) public returns (uint256 proposalId) { + // Exposing createProposal for E2E testing + return createProposal(_metadata, _actions); + } + + function initialize( + IDAO _dao, + MultisigSettings calldata _multisigSettings + ) public override initializer { + __PluginUUPSUpgradeable_init(_dao); + + _updateMultisigSettings(_multisigSettings); + } +} diff --git a/packages/contracts/test/integration-testing/governance-plugins-setup.ts b/packages/contracts/test/integration-testing/governance-plugins-setup.ts index 6ed77c4..f140977 100644 --- a/packages/contracts/test/integration-testing/governance-plugins-setup.ts +++ b/packages/contracts/test/integration-testing/governance-plugins-setup.ts @@ -9,41 +9,22 @@ import { MemberAccessPlugin__factory, PluginRepo, } from '../../typechain'; -import { - ExecutedEvent, - UpgradedEvent, -} from '../../typechain/@aragon/osx/core/dao/DAO'; import {PluginSetupRefStruct} from '../../typechain/@aragon/osx/framework/dao/DAOFactory'; -import {UpdatePreparedEvent} from '../../typechain/@aragon/osx/framework/plugin/setup/PluginSetupProcessor'; -import { - findEvent, - findEventTopicLog, - getPluginRepoFactoryAddress, - hashHelpers, - osxContracts, -} from '../../utils/helpers'; -import {toHex} from '../../utils/ipfs'; +import {osxContracts} from '../../utils/helpers'; import {getPluginRepoInfo} from '../../utils/plugin-repo-info'; import {installPlugin, uninstallPlugin} from '../helpers/setup'; import {deployTestDao} from '../helpers/test-dao'; -import { - ADDRESS_ZERO, - UPGRADE_PLUGIN_PERMISSION_ID, - ZERO_BYTES32, -} from '../unit-testing/common'; -// import { getNamedTypesFromMetadata } from "../helpers/types"; +import {ADDRESS_ZERO} from '../unit-testing/common'; import { DAO, PluginRepo__factory, PluginSetupProcessor, PluginSetupProcessor__factory, - PluginRepoFactory__factory, - PluginRepoRegistry__factory, DAO__factory, } from '@aragon/osx-ethers'; import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; import {expect} from 'chai'; -import {ethers, network} from 'hardhat'; +import {ethers} from 'hardhat'; const release = 1; const hardhatForkNetwork = process.env.NETWORK_NAME ?? 'mainnet'; @@ -170,297 +151,3 @@ describe('GovernancePluginsSetup processing', function () { }); }); }); - -describe('GovernancePluginsSetup with pluginUpgrader', () => { - let deployer: SignerWithAddress; - let pluginUpgrader: SignerWithAddress; - let pSetupBuild1: GovernancePluginsSetup; - - let psp: PluginSetupProcessor; - let dao: DAO; - let pluginRepo: PluginRepo; - let gpsFactory: GovernancePluginsSetup__factory; - - before(async () => { - [deployer, pluginUpgrader] = await ethers.getSigners(); - - // PSP - psp = PluginSetupProcessor__factory.connect( - osxContracts[hardhatForkNetwork]['PluginSetupProcessor'], - deployer - ); - - // Deploy DAO. - dao = await deployTestDao(deployer); - - // The DAO is root on itself - await dao.grant( - dao.address, - dao.address, - ethers.utils.id('ROOT_PERMISSION') - ); - - // Get the PluginRepoFactory address - const pluginRepoFactoryAddr: string = getPluginRepoFactoryAddress( - network.name - ); - - const pluginRepoFactory = PluginRepoFactory__factory.connect( - pluginRepoFactoryAddr, - deployer - ); - - // Create a new PluginRepo - let tx = await pluginRepoFactory.createPluginRepo( - 'testing-governance-plugin', - deployer.address - ); - const eventLog = await findEventTopicLog( - tx, - PluginRepoRegistry__factory.createInterface(), - 'PluginRepoRegistered' - ); - if (!eventLog) { - throw new Error('Failed to get PluginRepoRegistered event log'); - } - - pluginRepo = PluginRepo__factory.connect( - eventLog.args.pluginRepo, - deployer - ); - - // Deploy PluginSetup build 1 - gpsFactory = new GovernancePluginsSetup__factory().connect(deployer); - pSetupBuild1 = await gpsFactory.deploy(psp.address); - - // Publish build 1 - tx = await pluginRepo.createVersion( - 1, - pSetupBuild1.address, - toHex('build'), - toHex('release') - ); - }); - - it('Allows pluginUpgrader to execute psp.applyUpdate()', async () => { - const pluginSetupRef1: PluginSetupRefStruct = { - versionTag: { - release, - build: 1, - }, - pluginSetupRepo: pluginRepo.address, - }; - - // Temporary permissions for installing - await dao.grant( - dao.address, - psp.address, - ethers.utils.id('ROOT_PERMISSION') - ); - await dao.grant( - psp.address, - deployer.address, - ethers.utils.id('APPLY_INSTALLATION_PERMISSION') - ); - - // Install build 1 - const data1 = await pSetupBuild1.encodeInstallationParams( - pluginSettings, - [deployer.address], - minMemberAccessProposalDuration, - pluginUpgrader.address - ); - const installation1 = await installPlugin(psp, dao, pluginSetupRef1, data1); - - // Drop temp permissions - await dao.revoke( - dao.address, - psp.address, - ethers.utils.id('ROOT_PERMISSION') - ); - await dao.revoke( - psp.address, - deployer.address, - ethers.utils.id('APPLY_INSTALLATION_PERMISSION') - ); - - // Deployed plugin and helper - const mainVotingPlugin = MainVotingPlugin__factory.connect( - installation1.preparedEvent.args.plugin, - deployer - ); - const memberAccessPlugin = MemberAccessPlugin__factory.connect( - installation1.preparedEvent.args.preparedSetupData.helpers[0], - deployer - ); - - // Check implementations build 1 - expect(await mainVotingPlugin.implementation()).to.be.eq( - await pSetupBuild1.implementation() - ); - expect(await memberAccessPlugin.implementation()).to.be.eq( - await pSetupBuild1.memberAccessPluginImplementation() - ); - - // Deploy PluginSetup build 2 (new instance, disregarding the lack of changes) - const pSetupBuild2 = await gpsFactory.deploy(psp.address); - - // Check - expect(await pSetupBuild1.implementation()).to.not.be.eq( - await pSetupBuild2.implementation(), - 'Builds 1-2 implementation should differ' - ); - - // Publish build 2 - let tx = await pluginRepo.createVersion( - 1, - pSetupBuild2.address, - toHex('build'), - toHex('release') - ); - await tx.wait(); - - // Upgrade to build 2 - tx = await psp.prepareUpdate(dao.address, { - currentVersionTag: { - release: release, - build: 1, - }, - newVersionTag: { - release: release, - build: 2, - }, - pluginSetupRepo: pluginRepo.address, - setupPayload: { - currentHelpers: [memberAccessPlugin.address], - data: '0x', - plugin: mainVotingPlugin.address, - }, - }); - const preparedEvent = await findEvent( - tx, - 'UpdatePrepared' - ); - if (!preparedEvent) { - throw new Error('Failed to get UpdatePrepared event'); - } - - // Should not allow to execute other than the expected 3 actions - { - await expect( - dao.execute(toHex('01234123412341234123412341234123'), [], 0) - ).to.be.reverted; - await expect( - dao - .connect(pluginUpgrader) - .execute(toHex('01234123412341234123412341234123'), [], 0) - ).to.be.reverted; - await expect( - dao - .connect(pluginUpgrader) - .execute( - toHex('01234123412341234123412341234123'), - [{to: dao.address, value: 0, data: '0x'}], - 0 - ) - ).to.be.reverted; - await expect( - dao.connect(pluginUpgrader).execute( - toHex('01234123412341234123412341234123'), - [ - { - to: mainVotingPlugin.address, - value: 0, - data: MainVotingPlugin__factory.createInterface().encodeFunctionData( - 'addAddresses', - [[pluginUpgrader.address]] - ), - }, - ], - 0 - ) - ).to.be.reverted; - } - - // Params - const applyUpdateParams: PluginSetupProcessor.ApplyUpdateParamsStruct = { - plugin: mainVotingPlugin.address, - pluginSetupRef: { - pluginSetupRepo: pluginRepo.address, - versionTag: { - release, - build: 2, - }, - }, - initData: preparedEvent.args.initData, - permissions: preparedEvent.args.preparedSetupData.permissions, - helpersHash: hashHelpers(preparedEvent.args.preparedSetupData.helpers), - }; - - // Execute grant + applyUpdate + revoke - tx = await dao.connect(pluginUpgrader).execute( - ZERO_BYTES32, - [ - // Grant permission to the PSP - { - to: dao.address, - value: 0, - data: daoInterface.encodeFunctionData('grant', [ - mainVotingPlugin.address, - psp.address, - UPGRADE_PLUGIN_PERMISSION_ID, - ]), - }, - // Execute psp.applyUpdate() from the DAO to the plugin - { - to: psp.address, - value: 0, - data: pspInterface.encodeFunctionData('applyUpdate', [ - dao.address, - applyUpdateParams, - ]), - }, - // Revoke permission to the PSP - { - to: dao.address, - value: 0, - data: daoInterface.encodeFunctionData('revoke', [ - mainVotingPlugin.address, - psp.address, - UPGRADE_PLUGIN_PERMISSION_ID, - ]), - }, - ], - 0 - ); - - const receipt = await tx.wait(); - const executedEvent: ExecutedEvent | undefined = ( - receipt.events || [] - ).find(event => event.event === 'Executed') as any; - if (!executedEvent) { - throw new Error('Failed to get Executed event'); - } - - const upgradedEvent = await findEvent(tx, 'Upgraded'); - if (!upgradedEvent) { - throw new Error('Failed to get Upgraded event'); - } - - // Check implementations build 2 - expect(await mainVotingPlugin.implementation()).to.not.be.eq( - await pSetupBuild1.implementation(), - "Implementation shouldn't be build 1" - ); - - expect(await mainVotingPlugin.implementation()).to.be.eq( - await pSetupBuild2.implementation(), - 'Implementation should be build 2' - ); - - expect(await memberAccessPlugin.implementation()).to.be.eq( - await pSetupBuild1.memberAccessPluginImplementation(), - 'Implementation reamain as build 1' - ); - }); -}); diff --git a/packages/contracts/test/integration-testing/member-access-condition.ts b/packages/contracts/test/integration-testing/member-access-condition.ts new file mode 100644 index 0000000..240c470 --- /dev/null +++ b/packages/contracts/test/integration-testing/member-access-condition.ts @@ -0,0 +1,407 @@ +import { + TestGovernancePluginsSetup, + TestGovernancePluginsSetup__factory, + MainVotingPlugin, + MainVotingPlugin__factory, + MajorityVotingBase, + TestMemberAccessPlugin, + TestMemberAccessPlugin__factory, + PluginRepo, +} from '../../typechain'; +import {PluginSetupRefStruct} from '../../typechain/@aragon/osx/framework/dao/DAOFactory'; +import { + findEventTopicLog, + getPluginRepoFactoryAddress, + osxContracts, +} from '../../utils/helpers'; +import {toHex} from '../../utils/ipfs'; +import {installPlugin} from '../helpers/setup'; +import {deployTestDao} from '../helpers/test-dao'; +import { + EXECUTE_PERMISSION_ID, + MEMBER_PERMISSION_ID, + ROOT_PERMISSION_ID, + UPGRADE_PLUGIN_PERMISSION_ID, + ONE_BYTES32, +} from '../unit-testing/common'; +import { + DAO, + PluginRepo__factory, + PluginSetupProcessor, + PluginSetupProcessor__factory, + PluginRepoFactory__factory, + PluginRepoRegistry__factory, + DAO__factory, + IDAO, +} from '@aragon/osx-ethers'; +import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; +import {expect} from 'chai'; +import {ethers, network} from 'hardhat'; + +const release = 1; +const hardhatForkNetwork = process.env.NETWORK_NAME ?? 'mainnet'; +const pluginSettings: MajorityVotingBase.VotingSettingsStruct = { + minDuration: 60 * 60 * 24, + minParticipation: 1, + supportThreshold: 1, + minProposerVotingPower: 0, + votingMode: 0, +}; +const minMemberAccessProposalDuration = 60 * 60 * 24; +const daoInterface = DAO__factory.createInterface(); +const pspInterface = PluginSetupProcessor__factory.createInterface(); + +describe('Member Access Condition E2E', () => { + let deployer: SignerWithAddress; + let pluginUpgrader: SignerWithAddress; + let alice: SignerWithAddress; + let bob: SignerWithAddress; + let carol: SignerWithAddress; + + let psp: PluginSetupProcessor; + let dao: DAO; + let pluginRepo: PluginRepo; + + let pluginSetupRef: PluginSetupRefStruct; + let pluginSetup: TestGovernancePluginsSetup; + let gpsFactory: TestGovernancePluginsSetup__factory; + let mainVotingPlugin: MainVotingPlugin; + let memberAccessPlugin: TestMemberAccessPlugin; + + before(async () => { + [deployer, pluginUpgrader, alice, bob, carol] = await ethers.getSigners(); + + // Get the PluginRepoFactory address + const pluginRepoFactoryAddr: string = getPluginRepoFactoryAddress( + network.name + ); + const pluginRepoFactory = PluginRepoFactory__factory.connect( + pluginRepoFactoryAddr, + deployer + ); + + // PSP + psp = PluginSetupProcessor__factory.connect( + osxContracts[hardhatForkNetwork]['PluginSetupProcessor'], + deployer + ); + + // Create a new PluginRepo + let tx = await pluginRepoFactory.createPluginRepo( + 'testing-governance-plugin-condition', + deployer.address + ); + const eventLog = await findEventTopicLog( + tx, + PluginRepoRegistry__factory.createInterface(), + 'PluginRepoRegistered' + ); + if (!eventLog) { + throw new Error('Failed to get PluginRepoRegistered event log'); + } + + pluginRepo = PluginRepo__factory.connect( + eventLog.args.pluginRepo, + deployer + ); + + // Deploy PluginSetup build 1 + gpsFactory = new TestGovernancePluginsSetup__factory().connect(deployer); + pluginSetup = await gpsFactory.deploy(psp.address); + + // Publish build 1 + tx = await pluginRepo.createVersion( + 1, + pluginSetup.address, + toHex('build'), + toHex('release') + ); + + // Deploy setups + pluginSetupRef = { + versionTag: { + release, + build: 1, + }, + pluginSetupRepo: pluginRepo.address, + }; + }); + + beforeEach(async () => { + // Deploy DAO + dao = await deployTestDao(deployer); + + // The DAO is root on itself + await dao.grant( + dao.address, + dao.address, + ethers.utils.id('ROOT_PERMISSION') + ); + await dao.grant( + dao.address, + psp.address, + ethers.utils.id('ROOT_PERMISSION') + ); + await dao.grant( + psp.address, + deployer.address, + ethers.utils.id('APPLY_INSTALLATION_PERMISSION') + ); + + // Install plugin + const data = await pluginSetup.encodeInstallationParams( + pluginSettings, + [deployer.address], + minMemberAccessProposalDuration, + pluginUpgrader.address + ); + const installation = await installPlugin(psp, dao, pluginSetupRef, data); + + mainVotingPlugin = MainVotingPlugin__factory.connect( + installation.preparedEvent.args.plugin, + deployer + ); + memberAccessPlugin = TestMemberAccessPlugin__factory.connect( + installation.preparedEvent.args.preparedSetupData.helpers[0], + deployer + ); + }); + + it('Executing a proposal to add membership works', async () => { + expect( + await dao.isGranted( + mainVotingPlugin.address, // where + alice.address, // who + MEMBER_PERMISSION_ID, // permission + '0x' + ) + ).to.eq(false); + + await expect(memberAccessPlugin.proposeNewMember('0x', alice.address)).to + .not.be.reverted; + + expect( + await dao.isGranted( + mainVotingPlugin.address, // where + alice.address, // who + MEMBER_PERMISSION_ID, // permission + '0x' + ) + ).to.eq(true); + + // Valid grant + const actions: IDAO.ActionStruct[] = [ + { + to: dao.address, + value: 0, + data: daoInterface.encodeFunctionData('grant', [ + mainVotingPlugin.address, + bob.address, + MEMBER_PERMISSION_ID, + ]), + }, + ]; + + // Via direct create proposal + expect( + await dao.isGranted( + mainVotingPlugin.address, // where + bob.address, // who + MEMBER_PERMISSION_ID, // permission + '0x' + ) + ).to.eq(false); + + await expect(memberAccessPlugin.createArbitraryProposal('0x', actions)).to + .not.be.reverted; + + expect( + await dao.isGranted( + mainVotingPlugin.address, // where + bob.address, // who + MEMBER_PERMISSION_ID, // permission + '0x' + ) + ).to.eq(true); + }); + + it('Executing a proposal to remove membership works', async () => { + await expect(memberAccessPlugin.proposeNewMember('0x', alice.address)).to + .not.be.reverted; + await expect(memberAccessPlugin.proposeRemoveMember('0x', alice.address)).to + .not.be.reverted; + expect( + await dao.isGranted( + mainVotingPlugin.address, // where + alice.address, // who + MEMBER_PERMISSION_ID, // permission + '0x' + ) + ).to.eq(false); + + // Valid revoke + const grantAction = { + to: dao.address, + value: 0, + data: daoInterface.encodeFunctionData('grant', [ + mainVotingPlugin.address, + bob.address, + MEMBER_PERMISSION_ID, + ]), + }; + const revokeAction = { + to: dao.address, + value: 0, + data: daoInterface.encodeFunctionData('revoke', [ + mainVotingPlugin.address, + bob.address, + MEMBER_PERMISSION_ID, + ]), + }; + + // Via direct create proposal + await expect( + memberAccessPlugin.createArbitraryProposal('0x', [grantAction]) + ).to.not.be.reverted; + await expect( + memberAccessPlugin.createArbitraryProposal('0x', [revokeAction]) + ).to.not.be.reverted; + + expect( + await dao.isGranted( + mainVotingPlugin.address, // where + bob.address, // who + MEMBER_PERMISSION_ID, // permission + '0x' + ) + ).to.eq(false); + }); + + it('Executing a proposal to do something else reverts', async () => { + const validActions = [ + { + to: dao.address, + value: 0, + data: daoInterface.encodeFunctionData('grant', [ + mainVotingPlugin.address, + bob.address, + MEMBER_PERMISSION_ID, + ]), + }, + { + to: dao.address, + value: 0, + data: daoInterface.encodeFunctionData('revoke', [ + mainVotingPlugin.address, + bob.address, + MEMBER_PERMISSION_ID, + ]), + }, + ]; + const invalidActions = [ + { + to: dao.address, + value: 0, + data: daoInterface.encodeFunctionData('grant', [ + mainVotingPlugin.address, + bob.address, + EXECUTE_PERMISSION_ID, + ]), + }, + { + to: dao.address, + value: 0, + data: daoInterface.encodeFunctionData('grant', [ + dao.address, + alice.address, + EXECUTE_PERMISSION_ID, + ]), + }, + { + to: dao.address, + value: 0, + data: daoInterface.encodeFunctionData('grant', [ + dao.address, + alice.address, + ROOT_PERMISSION_ID, + ]), + }, + { + to: dao.address, + value: 0, + data: daoInterface.encodeFunctionData('grant', [ + psp.address, + alice.address, + ethers.utils.id('APPLY_INSTALLATION_PERMISSION'), + ]), + }, + { + to: dao.address, + value: 0, + data: daoInterface.encodeFunctionData('grant', [ + psp.address, + alice.address, + ethers.utils.id('APPLY_UPDATE_PERMISSION'), + ]), + }, + { + to: dao.address, + value: 0, + data: daoInterface.encodeFunctionData('grant', [ + psp.address, + alice.address, + ethers.utils.id('APPLY_UNINSTALLATION_PERMISSION'), + ]), + }, + { + to: dao.address, + value: 0, + data: daoInterface.encodeFunctionData('grant', [ + mainVotingPlugin.address, + alice.address, + UPGRADE_PLUGIN_PERMISSION_ID, + ]), + }, + { + to: dao.address, + value: 0, + data: daoInterface.encodeFunctionData('revoke', [ + mainVotingPlugin.address, + bob.address, + ROOT_PERMISSION_ID, + ]), + }, + { + to: dao.address, + value: 0, + data: daoInterface.encodeFunctionData('execute', [ + ONE_BYTES32, + validActions, + 0, + ]), + }, + { + to: dao.address, + value: 0, + data: daoInterface.encodeFunctionData('setMetadata', ['0x']), + }, + { + to: dao.address, + value: 0, + data: daoInterface.encodeFunctionData('setDaoURI', ['0x']), + }, + ]; + + // Should work + for (const action of validActions) { + await expect(memberAccessPlugin.createArbitraryProposal('0x', [action])) + .to.not.be.reverted; + } + + // Should fail + for (const action of invalidActions) { + await expect(memberAccessPlugin.createArbitraryProposal('0x', [action])) + .to.be.reverted; + } + }); +}); diff --git a/packages/contracts/test/integration-testing/plugin-upgrader.ts b/packages/contracts/test/integration-testing/plugin-upgrader.ts new file mode 100644 index 0000000..ee9cf65 --- /dev/null +++ b/packages/contracts/test/integration-testing/plugin-upgrader.ts @@ -0,0 +1,647 @@ +import { + GovernancePluginsSetup, + GovernancePluginsSetup__factory, + MainVotingPlugin__factory, + MajorityVotingBase, + MemberAccessPlugin__factory, + PluginRepo, + SpacePluginSetup, + SpacePluginSetup__factory, + SpacePlugin__factory, +} from '../../typechain'; +import { + ExecutedEvent, + UpgradedEvent, +} from '../../typechain/@aragon/osx/core/dao/DAO'; +import {PluginSetupRefStruct} from '../../typechain/@aragon/osx/framework/dao/DAOFactory'; +import {UpdatePreparedEvent} from '../../typechain/@aragon/osx/framework/plugin/setup/PluginSetupProcessor'; +import { + findEvent, + findEventTopicLog, + getPluginRepoFactoryAddress, + hashHelpers, + osxContracts, +} from '../../utils/helpers'; +import {toHex} from '../../utils/ipfs'; +import {installPlugin} from '../helpers/setup'; +import {deployTestDao} from '../helpers/test-dao'; +import { + ADDRESS_ONE, + ADDRESS_ZERO, + UPGRADE_PLUGIN_PERMISSION_ID, + ZERO_BYTES32, +} from '../unit-testing/common'; +// import { getNamedTypesFromMetadata } from "../helpers/types"; +import { + DAO, + PluginRepo__factory, + PluginSetupProcessor, + PluginSetupProcessor__factory, + PluginRepoFactory__factory, + PluginRepoRegistry__factory, + DAO__factory, +} from '@aragon/osx-ethers'; +import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; +import {expect} from 'chai'; +import {ethers, network} from 'hardhat'; + +const release = 1; +const hardhatForkNetwork = process.env.NETWORK_NAME ?? 'mainnet'; +const pluginSettings: MajorityVotingBase.VotingSettingsStruct = { + minDuration: 60 * 60 * 24, + minParticipation: 1, + supportThreshold: 1, + minProposerVotingPower: 0, + votingMode: 0, +}; +const minMemberAccessProposalDuration = 60 * 60 * 24; +const daoInterface = DAO__factory.createInterface(); +const pspInterface = PluginSetupProcessor__factory.createInterface(); + +describe('Plugin upgrader', () => { + let deployer: SignerWithAddress; + let pluginUpgrader: SignerWithAddress; + + let psp: PluginSetupProcessor; + let dao: DAO; + let pluginRepo: PluginRepo; + + describe('GovernancePluginsSetup', () => { + let pSetupBuild1: GovernancePluginsSetup; + let gpsFactory: GovernancePluginsSetup__factory; + + before(async () => { + [deployer, pluginUpgrader] = await ethers.getSigners(); + + // PSP + psp = PluginSetupProcessor__factory.connect( + osxContracts[hardhatForkNetwork]['PluginSetupProcessor'], + deployer + ); + + // Deploy DAO. + dao = await deployTestDao(deployer); + + // The DAO is root on itself + await dao.grant( + dao.address, + dao.address, + ethers.utils.id('ROOT_PERMISSION') + ); + + // Get the PluginRepoFactory address + const pluginRepoFactoryAddr: string = getPluginRepoFactoryAddress( + network.name + ); + + const pluginRepoFactory = PluginRepoFactory__factory.connect( + pluginRepoFactoryAddr, + deployer + ); + + // Create a new PluginRepo + let tx = await pluginRepoFactory.createPluginRepo( + 'testing-governance-plugin', + deployer.address + ); + const eventLog = await findEventTopicLog( + tx, + PluginRepoRegistry__factory.createInterface(), + 'PluginRepoRegistered' + ); + if (!eventLog) { + throw new Error('Failed to get PluginRepoRegistered event log'); + } + + pluginRepo = PluginRepo__factory.connect( + eventLog.args.pluginRepo, + deployer + ); + + // Deploy PluginSetup build 1 + gpsFactory = new GovernancePluginsSetup__factory().connect(deployer); + pSetupBuild1 = await gpsFactory.deploy(psp.address); + + // Publish build 1 + tx = await pluginRepo.createVersion( + 1, + pSetupBuild1.address, + toHex('build'), + toHex('release') + ); + }); + + it('Allows pluginUpgrader to execute psp.applyUpdate()', async () => { + const pluginSetupRef1: PluginSetupRefStruct = { + versionTag: { + release, + build: 1, + }, + pluginSetupRepo: pluginRepo.address, + }; + + // Temporary permissions for installing + await dao.grant( + dao.address, + psp.address, + ethers.utils.id('ROOT_PERMISSION') + ); + await dao.grant( + psp.address, + deployer.address, + ethers.utils.id('APPLY_INSTALLATION_PERMISSION') + ); + + // Install build 1 + const data1 = await pSetupBuild1.encodeInstallationParams( + pluginSettings, + [deployer.address], + minMemberAccessProposalDuration, + pluginUpgrader.address + ); + const installation1 = await installPlugin( + psp, + dao, + pluginSetupRef1, + data1 + ); + + // Drop temp permissions + await dao.revoke( + dao.address, + psp.address, + ethers.utils.id('ROOT_PERMISSION') + ); + await dao.revoke( + psp.address, + deployer.address, + ethers.utils.id('APPLY_INSTALLATION_PERMISSION') + ); + + // Deployed plugin and helper + const mainVotingPlugin = MainVotingPlugin__factory.connect( + installation1.preparedEvent.args.plugin, + deployer + ); + const memberAccessPlugin = MemberAccessPlugin__factory.connect( + installation1.preparedEvent.args.preparedSetupData.helpers[0], + deployer + ); + + // Check implementations build 1 + expect(await mainVotingPlugin.implementation()).to.be.eq( + await pSetupBuild1.implementation() + ); + expect(await memberAccessPlugin.implementation()).to.be.eq( + await pSetupBuild1.memberAccessPluginImplementation() + ); + + // Deploy PluginSetup build 2 (new instance, disregarding the lack of changes) + const pSetupBuild2 = await gpsFactory.deploy(psp.address); + + // Check + expect(await pSetupBuild1.implementation()).to.not.be.eq( + await pSetupBuild2.implementation(), + 'Builds 1-2 implementation should differ' + ); + + // Publish build 2 + let tx = await pluginRepo.createVersion( + 1, + pSetupBuild2.address, + toHex('build'), + toHex('release') + ); + await tx.wait(); + + // Upgrade to build 2 + tx = await psp.prepareUpdate(dao.address, { + currentVersionTag: { + release: release, + build: 1, + }, + newVersionTag: { + release: release, + build: 2, + }, + pluginSetupRepo: pluginRepo.address, + setupPayload: { + currentHelpers: [memberAccessPlugin.address], + data: '0x', + plugin: mainVotingPlugin.address, + }, + }); + const preparedEvent = await findEvent( + tx, + 'UpdatePrepared' + ); + if (!preparedEvent) { + throw new Error('Failed to get UpdatePrepared event'); + } + + // Should not allow to execute other than the expected 3 actions + { + await expect( + dao.execute(toHex('01234123412341234123412341234123'), [], 0) + ).to.be.reverted; + await expect( + dao + .connect(pluginUpgrader) + .execute(toHex('01234123412341234123412341234123'), [], 0) + ).to.be.reverted; + await expect( + dao + .connect(pluginUpgrader) + .execute( + toHex('01234123412341234123412341234123'), + [{to: dao.address, value: 0, data: '0x'}], + 0 + ) + ).to.be.reverted; + await expect( + dao.connect(pluginUpgrader).execute( + toHex('01234123412341234123412341234123'), + [ + { + to: mainVotingPlugin.address, + value: 0, + data: MainVotingPlugin__factory.createInterface().encodeFunctionData( + 'addAddresses', + [[pluginUpgrader.address]] + ), + }, + ], + 0 + ) + ).to.be.reverted; + } + + // Params + const applyUpdateParams: PluginSetupProcessor.ApplyUpdateParamsStruct = { + plugin: mainVotingPlugin.address, + pluginSetupRef: { + pluginSetupRepo: pluginRepo.address, + versionTag: { + release, + build: 2, + }, + }, + initData: preparedEvent.args.initData, + permissions: preparedEvent.args.preparedSetupData.permissions, + helpersHash: hashHelpers(preparedEvent.args.preparedSetupData.helpers), + }; + + // Execute grant + applyUpdate + revoke + tx = await dao.connect(pluginUpgrader).execute( + ZERO_BYTES32, + [ + // Grant permission to the PSP + { + to: dao.address, + value: 0, + data: daoInterface.encodeFunctionData('grant', [ + mainVotingPlugin.address, + psp.address, + UPGRADE_PLUGIN_PERMISSION_ID, + ]), + }, + // Execute psp.applyUpdate() from the DAO to the plugin + { + to: psp.address, + value: 0, + data: pspInterface.encodeFunctionData('applyUpdate', [ + dao.address, + applyUpdateParams, + ]), + }, + // Revoke permission to the PSP + { + to: dao.address, + value: 0, + data: daoInterface.encodeFunctionData('revoke', [ + mainVotingPlugin.address, + psp.address, + UPGRADE_PLUGIN_PERMISSION_ID, + ]), + }, + ], + 0 + ); + + const receipt = await tx.wait(); + const executedEvent: ExecutedEvent | undefined = ( + receipt.events || [] + ).find(event => event.event === 'Executed') as any; + if (!executedEvent) { + throw new Error('Failed to get Executed event'); + } + + const upgradedEvent = await findEvent(tx, 'Upgraded'); + if (!upgradedEvent) { + throw new Error('Failed to get Upgraded event'); + } + + // Check implementations build 2 + expect(await mainVotingPlugin.implementation()).to.not.be.eq( + await pSetupBuild1.implementation(), + "Implementation shouldn't be build 1" + ); + + expect(await mainVotingPlugin.implementation()).to.be.eq( + await pSetupBuild2.implementation(), + 'Implementation should be build 2' + ); + + expect(await memberAccessPlugin.implementation()).to.be.eq( + await pSetupBuild1.memberAccessPluginImplementation(), + 'Implementation reamain as build 1' + ); + }); + }); + + describe('SpacePluginSetup', () => { + let deployer: SignerWithAddress; + let pluginUpgrader: SignerWithAddress; + let pSetupBuild1: SpacePluginSetup; + + let psp: PluginSetupProcessor; + let dao: DAO; + let pluginRepo: PluginRepo; + let spFactory: SpacePluginSetup__factory; + + before(async () => { + [deployer, pluginUpgrader] = await ethers.getSigners(); + + // PSP + psp = PluginSetupProcessor__factory.connect( + osxContracts[hardhatForkNetwork]['PluginSetupProcessor'], + deployer + ); + + // Deploy DAO. + dao = await deployTestDao(deployer); + + // The DAO is root on itself + await dao.grant( + dao.address, + dao.address, + ethers.utils.id('ROOT_PERMISSION') + ); + + // Get the PluginRepoFactory address + const pluginRepoFactoryAddr: string = getPluginRepoFactoryAddress( + network.name + ); + + const pluginRepoFactory = PluginRepoFactory__factory.connect( + pluginRepoFactoryAddr, + deployer + ); + + // Create a new PluginRepo + let tx = await pluginRepoFactory.createPluginRepo( + 'testing-space-plugin', + deployer.address + ); + const eventLog = await findEventTopicLog( + tx, + PluginRepoRegistry__factory.createInterface(), + 'PluginRepoRegistered' + ); + if (!eventLog) { + throw new Error('Failed to get PluginRepoRegistered event log'); + } + + pluginRepo = PluginRepo__factory.connect( + eventLog.args.pluginRepo, + deployer + ); + + // Deploy PluginSetup build 1 + spFactory = new SpacePluginSetup__factory().connect(deployer); + pSetupBuild1 = await spFactory.deploy(psp.address); + + // Publish build 1 + tx = await pluginRepo.createVersion( + 1, + pSetupBuild1.address, + toHex('build'), + toHex('release') + ); + }); + + it('Allows pluginUpgrader to execute psp.applyUpdate()', async () => { + const pluginSetupRef1: PluginSetupRefStruct = { + versionTag: { + release, + build: 1, + }, + pluginSetupRepo: pluginRepo.address, + }; + + // Temporary permissions for installing + await dao.grant( + dao.address, + psp.address, + ethers.utils.id('ROOT_PERMISSION') + ); + await dao.grant( + psp.address, + deployer.address, + ethers.utils.id('APPLY_INSTALLATION_PERMISSION') + ); + + // Install build 1 + const data1 = await pSetupBuild1.encodeInstallationParams( + toHex('ipfs://1234'), + ADDRESS_ZERO, + pluginUpgrader.address + ); + const installation1 = await installPlugin( + psp, + dao, + pluginSetupRef1, + data1 + ); + + // Drop temp permissions + await dao.revoke( + dao.address, + psp.address, + ethers.utils.id('ROOT_PERMISSION') + ); + await dao.revoke( + psp.address, + deployer.address, + ethers.utils.id('APPLY_INSTALLATION_PERMISSION') + ); + + // Deployed plugin + const spacePlugin = SpacePlugin__factory.connect( + installation1.preparedEvent.args.plugin, + deployer + ); + + // Check implementations build 1 + expect(await spacePlugin.implementation()).to.be.eq( + await pSetupBuild1.implementation() + ); + + // Deploy PluginSetup build 2 (new instance, disregarding the lack of changes) + const pSetupBuild2 = await spFactory.deploy(psp.address); + + // Check + expect(await pSetupBuild1.implementation()).to.not.be.eq( + await pSetupBuild2.implementation(), + 'Builds 1-2 implementation should differ' + ); + + // Publish build 2 + let tx = await pluginRepo.createVersion( + 1, + pSetupBuild2.address, + toHex('build'), + toHex('release') + ); + await tx.wait(); + + // Upgrade to build 2 + tx = await psp.prepareUpdate(dao.address, { + currentVersionTag: { + release: release, + build: 1, + }, + newVersionTag: { + release: release, + build: 2, + }, + pluginSetupRepo: pluginRepo.address, + setupPayload: { + currentHelpers: [], + data: '0x', + plugin: spacePlugin.address, + }, + }); + const preparedEvent = await findEvent( + tx, + 'UpdatePrepared' + ); + if (!preparedEvent) { + throw new Error('Failed to get UpdatePrepared event'); + } + + // Should not allow to execute other than the expected 3 actions + { + await expect( + dao.execute(toHex('23412341234123412341234123412341'), [], 0) + ).to.be.reverted; + await expect( + dao + .connect(pluginUpgrader) + .execute(toHex('23412341234123412341234123412341'), [], 0) + ).to.be.reverted; + await expect( + dao + .connect(pluginUpgrader) + .execute( + toHex('23412341234123412341234123412341'), + [{to: dao.address, value: 0, data: '0x'}], + 0 + ) + ).to.be.reverted; + await expect( + dao.connect(pluginUpgrader).execute( + toHex('23412341234123412341234123412341'), + [ + { + to: spacePlugin.address, + value: 0, + data: SpacePlugin__factory.createInterface().encodeFunctionData( + 'removeSubspace', + [ADDRESS_ONE] + ), + }, + ], + 0 + ) + ).to.be.reverted; + } + + // Params + const applyUpdateParams: PluginSetupProcessor.ApplyUpdateParamsStruct = { + plugin: spacePlugin.address, + pluginSetupRef: { + pluginSetupRepo: pluginRepo.address, + versionTag: { + release, + build: 2, + }, + }, + initData: preparedEvent.args.initData, + permissions: preparedEvent.args.preparedSetupData.permissions, + helpersHash: hashHelpers(preparedEvent.args.preparedSetupData.helpers), + }; + + // Execute grant + applyUpdate + revoke + tx = await dao.connect(pluginUpgrader).execute( + ZERO_BYTES32, + [ + // Grant permission to the PSP + { + to: dao.address, + value: 0, + data: daoInterface.encodeFunctionData('grant', [ + spacePlugin.address, + psp.address, + UPGRADE_PLUGIN_PERMISSION_ID, + ]), + }, + // Execute psp.applyUpdate() from the DAO to the plugin + { + to: psp.address, + value: 0, + data: pspInterface.encodeFunctionData('applyUpdate', [ + dao.address, + applyUpdateParams, + ]), + }, + // Revoke permission to the PSP + { + to: dao.address, + value: 0, + data: daoInterface.encodeFunctionData('revoke', [ + spacePlugin.address, + psp.address, + UPGRADE_PLUGIN_PERMISSION_ID, + ]), + }, + ], + 0 + ); + + const receipt = await tx.wait(); + const executedEvent: ExecutedEvent | undefined = ( + receipt.events || [] + ).find(event => event.event === 'Executed') as any; + if (!executedEvent) { + throw new Error('Failed to get Executed event'); + } + + const upgradedEvent = await findEvent(tx, 'Upgraded'); + if (!upgradedEvent) { + throw new Error('Failed to get Upgraded event'); + } + + // Check implementations build 2 + expect(await spacePlugin.implementation()).to.not.be.eq( + await pSetupBuild1.implementation(), + "Implementation shouldn't be build 1" + ); + + expect(await spacePlugin.implementation()).to.be.eq( + await pSetupBuild2.implementation(), + 'Implementation should be build 2' + ); + }); + }); +}); diff --git a/packages/contracts/test/integration-testing/space-setup.ts b/packages/contracts/test/integration-testing/space-setup.ts index 87ee68f..75e3c9e 100644 --- a/packages/contracts/test/integration-testing/space-setup.ts +++ b/packages/contracts/test/integration-testing/space-setup.ts @@ -6,48 +6,24 @@ import { SpacePluginSetup, SpacePluginSetup__factory, } from '../../typechain'; -import { - ExecutedEvent, - UpgradedEvent, -} from '../../typechain/@aragon/osx/core/dao/DAO'; import {PluginSetupRefStruct} from '../../typechain/@aragon/osx/framework/dao/DAOFactory'; -import {UpdatePreparedEvent} from '../../typechain/@aragon/osx/framework/plugin/setup/PluginSetupProcessor'; -import { - findEvent, - findEventTopicLog, - getPluginRepoFactoryAddress, - hashHelpers, - osxContracts, -} from '../../utils/helpers'; +import {osxContracts} from '../../utils/helpers'; import {toHex} from '../../utils/ipfs'; import {getPluginRepoInfo} from '../../utils/plugin-repo-info'; import {installPlugin, uninstallPlugin} from '../helpers/setup'; import {deployTestDao} from '../helpers/test-dao'; -import { - ADDRESS_ONE, - ADDRESS_ZERO, - UPGRADE_PLUGIN_PERMISSION_ID, - ZERO_BYTES32, -} from '../unit-testing/common'; +import {ADDRESS_ZERO} from '../unit-testing/common'; // import { getNamedTypesFromMetadata } from "../helpers/types"; import { DAO, - DAO__factory, PluginRepo__factory, - PluginRepoFactory__factory, - PluginRepoRegistry__factory, PluginSetupProcessor, PluginSetupProcessor__factory, } from '@aragon/osx-ethers'; import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; import {expect} from 'chai'; import {BigNumber} from 'ethers'; -import {ethers, network} from 'hardhat'; - -const release = 1; -const hardhatForkNetwork = process.env.NETWORK_NAME ?? 'mainnet'; -const daoInterface = DAO__factory.createInterface(); -const pspInterface = PluginSetupProcessor__factory.createInterface(); +import {ethers} from 'hardhat'; describe('SpacePluginSetup processing', function () { let deployer: SignerWithAddress; @@ -153,284 +129,3 @@ describe('SpacePluginSetup processing', function () { }); }); }); - -describe('SpacePluginSetup with pluginUpgrader', () => { - let deployer: SignerWithAddress; - let pluginUpgrader: SignerWithAddress; - let pSetupBuild1: SpacePluginSetup; - - let psp: PluginSetupProcessor; - let dao: DAO; - let pluginRepo: PluginRepo; - let spFactory: SpacePluginSetup__factory; - - before(async () => { - [deployer, pluginUpgrader] = await ethers.getSigners(); - - // PSP - psp = PluginSetupProcessor__factory.connect( - osxContracts[hardhatForkNetwork]['PluginSetupProcessor'], - deployer - ); - - // Deploy DAO. - dao = await deployTestDao(deployer); - - // The DAO is root on itself - await dao.grant( - dao.address, - dao.address, - ethers.utils.id('ROOT_PERMISSION') - ); - - // Get the PluginRepoFactory address - const pluginRepoFactoryAddr: string = getPluginRepoFactoryAddress( - network.name - ); - - const pluginRepoFactory = PluginRepoFactory__factory.connect( - pluginRepoFactoryAddr, - deployer - ); - - // Create a new PluginRepo - let tx = await pluginRepoFactory.createPluginRepo( - 'testing-space-plugin', - deployer.address - ); - const eventLog = await findEventTopicLog( - tx, - PluginRepoRegistry__factory.createInterface(), - 'PluginRepoRegistered' - ); - if (!eventLog) { - throw new Error('Failed to get PluginRepoRegistered event log'); - } - - pluginRepo = PluginRepo__factory.connect( - eventLog.args.pluginRepo, - deployer - ); - - // Deploy PluginSetup build 1 - spFactory = new SpacePluginSetup__factory().connect(deployer); - pSetupBuild1 = await spFactory.deploy(psp.address); - - // Publish build 1 - tx = await pluginRepo.createVersion( - 1, - pSetupBuild1.address, - toHex('build'), - toHex('release') - ); - }); - - it('Allows pluginUpgrader to execute psp.applyUpdate()', async () => { - const pluginSetupRef1: PluginSetupRefStruct = { - versionTag: { - release, - build: 1, - }, - pluginSetupRepo: pluginRepo.address, - }; - - // Temporary permissions for installing - await dao.grant( - dao.address, - psp.address, - ethers.utils.id('ROOT_PERMISSION') - ); - await dao.grant( - psp.address, - deployer.address, - ethers.utils.id('APPLY_INSTALLATION_PERMISSION') - ); - - // Install build 1 - const data1 = await pSetupBuild1.encodeInstallationParams( - toHex('ipfs://1234'), - ADDRESS_ZERO, - pluginUpgrader.address - ); - const installation1 = await installPlugin(psp, dao, pluginSetupRef1, data1); - - // Drop temp permissions - await dao.revoke( - dao.address, - psp.address, - ethers.utils.id('ROOT_PERMISSION') - ); - await dao.revoke( - psp.address, - deployer.address, - ethers.utils.id('APPLY_INSTALLATION_PERMISSION') - ); - - // Deployed plugin - const spacePlugin = SpacePlugin__factory.connect( - installation1.preparedEvent.args.plugin, - deployer - ); - - // Check implementations build 1 - expect(await spacePlugin.implementation()).to.be.eq( - await pSetupBuild1.implementation() - ); - - // Deploy PluginSetup build 2 (new instance, disregarding the lack of changes) - const pSetupBuild2 = await spFactory.deploy(psp.address); - - // Check - expect(await pSetupBuild1.implementation()).to.not.be.eq( - await pSetupBuild2.implementation(), - 'Builds 1-2 implementation should differ' - ); - - // Publish build 2 - let tx = await pluginRepo.createVersion( - 1, - pSetupBuild2.address, - toHex('build'), - toHex('release') - ); - await tx.wait(); - - // Upgrade to build 2 - tx = await psp.prepareUpdate(dao.address, { - currentVersionTag: { - release: release, - build: 1, - }, - newVersionTag: { - release: release, - build: 2, - }, - pluginSetupRepo: pluginRepo.address, - setupPayload: { - currentHelpers: [], - data: '0x', - plugin: spacePlugin.address, - }, - }); - const preparedEvent = await findEvent( - tx, - 'UpdatePrepared' - ); - if (!preparedEvent) { - throw new Error('Failed to get UpdatePrepared event'); - } - - // Should not allow to execute other than the expected 3 actions - { - await expect( - dao.execute(toHex('23412341234123412341234123412341'), [], 0) - ).to.be.reverted; - await expect( - dao - .connect(pluginUpgrader) - .execute(toHex('23412341234123412341234123412341'), [], 0) - ).to.be.reverted; - await expect( - dao - .connect(pluginUpgrader) - .execute( - toHex('23412341234123412341234123412341'), - [{to: dao.address, value: 0, data: '0x'}], - 0 - ) - ).to.be.reverted; - await expect( - dao.connect(pluginUpgrader).execute( - toHex('23412341234123412341234123412341'), - [ - { - to: spacePlugin.address, - value: 0, - data: SpacePlugin__factory.createInterface().encodeFunctionData( - 'removeSubspace', - [ADDRESS_ONE] - ), - }, - ], - 0 - ) - ).to.be.reverted; - } - - // Params - const applyUpdateParams: PluginSetupProcessor.ApplyUpdateParamsStruct = { - plugin: spacePlugin.address, - pluginSetupRef: { - pluginSetupRepo: pluginRepo.address, - versionTag: { - release, - build: 2, - }, - }, - initData: preparedEvent.args.initData, - permissions: preparedEvent.args.preparedSetupData.permissions, - helpersHash: hashHelpers(preparedEvent.args.preparedSetupData.helpers), - }; - - // Execute grant + applyUpdate + revoke - tx = await dao.connect(pluginUpgrader).execute( - ZERO_BYTES32, - [ - // Grant permission to the PSP - { - to: dao.address, - value: 0, - data: daoInterface.encodeFunctionData('grant', [ - spacePlugin.address, - psp.address, - UPGRADE_PLUGIN_PERMISSION_ID, - ]), - }, - // Execute psp.applyUpdate() from the DAO to the plugin - { - to: psp.address, - value: 0, - data: pspInterface.encodeFunctionData('applyUpdate', [ - dao.address, - applyUpdateParams, - ]), - }, - // Revoke permission to the PSP - { - to: dao.address, - value: 0, - data: daoInterface.encodeFunctionData('revoke', [ - spacePlugin.address, - psp.address, - UPGRADE_PLUGIN_PERMISSION_ID, - ]), - }, - ], - 0 - ); - - const receipt = await tx.wait(); - const executedEvent: ExecutedEvent | undefined = ( - receipt.events || [] - ).find(event => event.event === 'Executed') as any; - if (!executedEvent) { - throw new Error('Failed to get Executed event'); - } - - const upgradedEvent = await findEvent(tx, 'Upgraded'); - if (!upgradedEvent) { - throw new Error('Failed to get Upgraded event'); - } - - // Check implementations build 2 - expect(await spacePlugin.implementation()).to.not.be.eq( - await pSetupBuild1.implementation(), - "Implementation shouldn't be build 1" - ); - - expect(await spacePlugin.implementation()).to.be.eq( - await pSetupBuild2.implementation(), - 'Implementation should be build 2' - ); - }); -}); diff --git a/packages/contracts/test/unit-testing/common.ts b/packages/contracts/test/unit-testing/common.ts index e292e7c..ff91be2 100644 --- a/packages/contracts/test/unit-testing/common.ts +++ b/packages/contracts/test/unit-testing/common.ts @@ -6,6 +6,8 @@ export const abiCoder = ethers.utils.defaultAbiCoder; export const EMPTY_DATA = '0x'; export const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000'; +export const ONE_BYTES32 = + '0x0000000000000000000000000000000000000000000000000000000000000001'; export const DEPLOYER_PERMISSION_ID = ethers.utils.id('DEPLOYER_PERMISSION'); export const EDITOR_PERMISSION_ID = ethers.utils.id('EDITOR_PERMISSION');