From ccb7dee8f477e71eea4a0b5a57aa5b6f54568a22 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Tue, 20 Aug 2024 17:51:24 +0400 Subject: [PATCH 01/59] fix multisig/multisigsetup tests for new implementation --- packages/contracts/hardhat.config.ts | 1 + .../contracts/src/ListedCheckCondition.sol | 32 + packages/contracts/src/Multisig.sol | 100 ++- packages/contracts/src/MultisigSetup.sol | 122 ++- packages/contracts/src/build-metadata.json | 45 + .../test/10_unit-testing/11_plugin.ts | 840 +++++++----------- .../test/10_unit-testing/12_plugin-setup.ts | 163 +++- .../31_upgradeability.ts | 8 +- packages/contracts/test/multisig-constants.ts | 16 +- .../test/test-utils/uups-upgradeable.ts | 14 +- 10 files changed, 779 insertions(+), 562 deletions(-) create mode 100644 packages/contracts/src/ListedCheckCondition.sol diff --git a/packages/contracts/hardhat.config.ts b/packages/contracts/hardhat.config.ts index 88a4fbff..ace6baa1 100644 --- a/packages/contracts/hardhat.config.ts +++ b/packages/contracts/hardhat.config.ts @@ -158,6 +158,7 @@ const config: HardhatUserConfig = { solidity: { version: '0.8.17', settings: { + viaIR: true, metadata: { // Not including the metadata hash // https://github.com/paulrberg/hardhat-template/issues/31 diff --git a/packages/contracts/src/ListedCheckCondition.sol b/packages/contracts/src/ListedCheckCondition.sol new file mode 100644 index 00000000..ef926d86 --- /dev/null +++ b/packages/contracts/src/ListedCheckCondition.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.8; + +import {Multisig} from "./Multisig.sol"; + +import {PermissionCondition} from "@aragon/osx-commons-contracts/src/permission/condition/PermissionCondition.sol"; + +contract ListedCheckCondition is PermissionCondition { + Multisig private immutable multisig; + + constructor(address _multisig) { + multisig = Multisig(_multisig); + } + + function isGranted( + address _where, + address _who, + bytes32 _permissionId, + bytes calldata _data + ) public view override returns (bool) { + (_where, _data, _permissionId); + + (bool onlyListed, ) = multisig.multisigSettings(); + + if (onlyListed && !multisig.isListed(_who)) { + return false; + } + + return true; + } +} diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index 54468b5c..d9f3255e 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -9,6 +9,7 @@ import {IMembership} from "@aragon/osx-commons-contracts/src/plugin/extensions/m import {Addresslist} from "@aragon/osx-commons-contracts/src/plugin/extensions/governance/Addresslist.sol"; import {ProposalUpgradeable} from "@aragon/osx-commons-contracts/src/plugin/extensions/proposal/ProposalUpgradeable.sol"; import {PluginUUPSUpgradeable} from "@aragon/osx-commons-contracts/src/plugin/PluginUUPSUpgradeable.sol"; +import {IProposal} from "@aragon/osx-commons-contracts/src/plugin/extensions/proposal/IProposal.sol"; import {IDAO} from "@aragon/osx-commons-contracts/src/dao/IDAO.sol"; import {IMultisig} from "./IMultisig.sol"; @@ -45,6 +46,8 @@ contract Multisig is mapping(address => bool) approvers; IDAO.Action[] actions; uint256 allowFailureMap; + address target; // added in v1.4.0 + Operation operation; // added in v1.4.0 } /// @notice A container for the proposal parameters. @@ -71,13 +74,20 @@ contract Multisig is bytes4 internal constant MULTISIG_INTERFACE_ID = this.initialize.selector ^ this.updateMultisigSettings.selector ^ - this.createProposal.selector ^ + bytes4( + keccak256( + "createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)" + ) + ) ^ this.getProposal.selector; /// @notice The ID of the permission required to call the `addAddresses` and `removeAddresses` functions. bytes32 public constant UPDATE_MULTISIG_SETTINGS_PERMISSION_ID = keccak256("UPDATE_MULTISIG_SETTINGS_PERMISSION"); + /// @notice The ID of the permission required to call the `addAddresses` and `removeAddresses` functions. + bytes32 public constant CREATE_PROPOSAL_PERMISSION_ID = keccak256("CREATE_PROPOSAL_PERMISSION"); + /// @notice A mapping between proposal IDs and proposal information. // solhint-disable-next-line named-parameters-mapping mapping(uint256 => Proposal) internal proposals; @@ -117,6 +127,10 @@ contract Multisig is /// @param actual The actual value. error AddresslistLengthOutOfBounds(uint16 limit, uint256 actual); + /// @notice Thrown if the proposal with same actions and metadata already exists. + /// @param proposalId The id of the proposal. + error ProposalAlreadyExists(uint256 proposalId); + /// @notice Thrown if a date is out of bounds. /// @param limit The limit value. /// @param actual The actual value. @@ -140,7 +154,8 @@ contract Multisig is function initialize( IDAO _dao, address[] calldata _members, - MultisigSettings calldata _multisigSettings + MultisigSettings calldata _multisigSettings, + TargetConfig calldata _targetConfig ) external initializer { __PluginUUPSUpgradeable_init(_dao); @@ -152,6 +167,14 @@ contract Multisig is emit MembersAdded({members: _members}); _updateMultisigSettings(_multisigSettings); + + _setTargetConfig(_targetConfig); + } + + // TODO: Would we need version checking and only calling _setTargetConfig in specific cases ? + // TODO: What should the number in reinitializer(x) must be ? + function initializeFrom(TargetConfig calldata _targetConfig) external reinitializer(2) { + _setTargetConfig(_targetConfig); } /// @notice Checks if this or the parent contract supports an interface by its ID. @@ -236,11 +259,7 @@ contract Multisig is bool _tryExecution, uint64 _startDate, uint64 _endDate - ) external returns (uint256 proposalId) { - if (multisigSettings.onlyListed && !isListed(_msgSender())) { - revert ProposalCreationForbidden(_msgSender()); - } - + ) public auth(CREATE_PROPOSAL_PERMISSION_ID) returns (uint256 proposalId) { uint64 snapshotBlock; unchecked { // The snapshot block must be mined already to protect the transaction against backrunning transactions @@ -264,23 +283,32 @@ contract Multisig is revert DateOutOfBounds({limit: _startDate, actual: _endDate}); } - proposalId = _createProposal({ - _creator: _msgSender(), - _metadata: _metadata, - _startDate: _startDate, - _endDate: _endDate, - _actions: _actions, - _allowFailureMap: _allowFailureMap - }); + proposalId = hashProposal(_actions, _metadata); // Create the proposal Proposal storage proposal_ = proposals[proposalId]; + // Multisig doesn't allow `minApprovals` settings to be less than 0. + // If it is, that means proposal hasn't been created yet. + if (proposal_.parameters.minApprovals != 0) { + revert ProposalAlreadyExists(proposalId); + } + proposal_.parameters.snapshotBlock = snapshotBlock; proposal_.parameters.startDate = _startDate; proposal_.parameters.endDate = _endDate; proposal_.parameters.minApprovals = multisigSettings.minApprovals; + TargetConfig memory currentTarget = getTargetConfig(); + + if (currentTarget.target == address(0)) { + proposal_.target = address(dao()); + proposal_.operation = Operation.Call; + } else { + proposal_.target = currentTarget.target; + proposal_.operation = currentTarget.operation; + } + // Reduce costs if (_allowFailureMap != 0) { proposal_.allowFailureMap = _allowFailureMap; @@ -296,6 +324,26 @@ contract Multisig is if (_approveProposal) { approve(proposalId, _tryExecution); } + + emit ProposalCreated( + proposalId, + _msgSender(), + _startDate, + _endDate, + _metadata, + _actions, + _allowFailureMap + ); + } + + function createProposal( + bytes calldata _metadata, + IDAO.Action[] calldata _actions, + uint64 _startDate, + uint64 _endDate + ) external override returns (uint256 proposalId) { + // Calls public function for permission check. + proposalId = createProposal(_metadata, _actions, 0, false, false, _startDate, _endDate); } /// @inheritdoc IMultisig @@ -328,7 +376,9 @@ contract Multisig is } /// @inheritdoc IMultisig - function canExecute(uint256 _proposalId) external view returns (bool) { + function canExecute( + uint256 _proposalId + ) external view override(IMultisig, IProposal) returns (bool) { return _canExecute(_proposalId); } @@ -382,6 +432,13 @@ contract Multisig is return isListed(_account); } + function hashProposal( + IDAO.Action[] calldata _actions, + bytes memory _metadata + ) public pure returns (uint256 proposalId) { + proposalId = uint256(keccak256(abi.encode(_actions, _metadata))); + } + /// @notice Internal function to execute a vote. It assumes the queried proposal exists. /// @param _proposalId The ID of the proposal. function _execute(uint256 _proposalId) internal { @@ -389,12 +446,15 @@ contract Multisig is proposal_.executed = true; - _executeProposal( - dao(), - _proposalId, + _execute( + proposal_.target, + bytes32(_proposalId), proposals[_proposalId].actions, - proposals[_proposalId].allowFailureMap + proposals[_proposalId].allowFailureMap, + proposal_.operation ); + + emit ProposalExecuted(_proposalId); } /// @notice Internal function to check if an account can approve. It assumes the queried proposal exists. diff --git a/packages/contracts/src/MultisigSetup.sol b/packages/contracts/src/MultisigSetup.sol index 8e9fd4ee..b0e48eaa 100644 --- a/packages/contracts/src/MultisigSetup.sol +++ b/packages/contracts/src/MultisigSetup.sol @@ -6,9 +6,13 @@ pragma solidity ^0.8.8; import {PermissionLib} from "@aragon/osx-commons-contracts/src/permission/PermissionLib.sol"; import {IPluginSetup} from "@aragon/osx-commons-contracts/src/plugin/setup/IPluginSetup.sol"; import {PluginUpgradeableSetup} from "@aragon/osx-commons-contracts/src/plugin/setup/PluginUpgradeableSetup.sol"; +import {PluginUUPSUpgradeable} from "@aragon/osx-commons-contracts/src/plugin/PluginUUPSUpgradeable.sol"; + import {ProxyLib} from "@aragon/osx-commons-contracts/src/utils/deployment/ProxyLib.sol"; import {IDAO} from "@aragon/osx-commons-contracts/src/dao/IDAO.sol"; +import {ListedCheckCondition} from "./ListedCheckCondition.sol"; + import {Multisig} from "./Multisig.sol"; /* solhint-enable max-line-length */ @@ -21,10 +25,20 @@ import {Multisig} from "./Multisig.sol"; contract MultisigSetup is PluginUpgradeableSetup { using ProxyLib for address; - // TODO This permission identifier will be moved into a library in task OS-954. /// @notice The ID of the permission required to call the `execute` function. bytes32 internal constant EXECUTE_PERMISSION_ID = keccak256("EXECUTE_PERMISSION"); + /// @notice The ID of the permission required to call the `upgradeToAndCall` function. + bytes32 internal constant UPGRADE_PLUGIN_PERMISSION_ID = keccak256("UPGRADE_PLUGIN_PERMISSION"); + + /// @notice The ID of the permission required to call the `setTargetConfig` function. + bytes32 public constant SET_TARGET_CONFIG_PERMISSION_ID = + keccak256("SET_TARGET_CONFIG_PERMISSION"); + + /// @notice The ID of the permission required to call the `updateMultisigSettings` function. + bytes32 public constant UPDATE_MULTISIG_SETTINGS_PERMISSION_ID = + keccak256("UPDATE_MULTISIG_SETTINGS_PERMISSION"); + /// @notice The contract constructor, that deploys the `Multisig` plugin logic contract. constructor() PluginUpgradeableSetup(address(new Multisig())) {} @@ -34,19 +48,28 @@ contract MultisigSetup is PluginUpgradeableSetup { bytes calldata _data ) external returns (address plugin, PreparedSetupData memory preparedSetupData) { // Decode `_data` to extract the params needed for deploying and initializing `Multisig` plugin. - (address[] memory members, Multisig.MultisigSettings memory multisigSettings) = abi.decode( - _data, - (address[], Multisig.MultisigSettings) - ); + ( + address[] memory members, + Multisig.MultisigSettings memory multisigSettings, + PluginUUPSUpgradeable.TargetConfig memory targetConfig + ) = abi.decode( + _data, + (address[], Multisig.MultisigSettings, PluginUUPSUpgradeable.TargetConfig) + ); // Deploy and initialize the plugin UUPS proxy. plugin = IMPLEMENTATION.deployUUPSProxy( - abi.encodeCall(Multisig.initialize, (IDAO(_dao), members, multisigSettings)) + abi.encodeCall( + Multisig.initialize, + (IDAO(_dao), members, multisigSettings, targetConfig) + ) ); + address listedCheckCondition = address(new ListedCheckCondition(plugin)); + // Prepare permissions PermissionLib.MultiTargetPermission[] - memory permissions = new PermissionLib.MultiTargetPermission[](2); + memory permissions = new PermissionLib.MultiTargetPermission[](4); // Set permissions to be granted. // Grant the list of permissions of the plugin to the DAO. @@ -55,10 +78,9 @@ contract MultisigSetup is PluginUpgradeableSetup { where: plugin, who: _dao, condition: PermissionLib.NO_CONDITION, - permissionId: Multisig(IMPLEMENTATION).UPDATE_MULTISIG_SETTINGS_PERMISSION_ID() + permissionId: UPDATE_MULTISIG_SETTINGS_PERMISSION_ID }); - // Grant `EXECUTE_PERMISSION` of the DAO to the plugin. permissions[1] = PermissionLib.MultiTargetPermission({ operation: PermissionLib.Operation.Grant, where: _dao, @@ -67,7 +89,26 @@ contract MultisigSetup is PluginUpgradeableSetup { permissionId: EXECUTE_PERMISSION_ID }); + permissions[2] = PermissionLib.MultiTargetPermission( + PermissionLib.Operation.GrantWithCondition, + plugin, + address(type(uint160).max), // ANY_ADDR + listedCheckCondition, + Multisig(IMPLEMENTATION).CREATE_PROPOSAL_PERMISSION_ID() + ); + + permissions[3] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Grant, + where: plugin, + who: _dao, + condition: PermissionLib.NO_CONDITION, + permissionId: SET_TARGET_CONFIG_PERMISSION_ID + }); + preparedSetupData.permissions = permissions; + + preparedSetupData.helpers = new address[](1); + preparedSetupData.helpers[0] = listedCheckCondition; } /// @inheritdoc IPluginSetup @@ -78,24 +119,51 @@ contract MultisigSetup is PluginUpgradeableSetup { SetupPayload calldata _payload ) external - view override returns (bytes memory initData, PreparedSetupData memory preparedSetupData) { (initData); + + // todo: multisig never been upgraded right ? if (_fromBuild < 3) { + address listedCheckCondition = address(new ListedCheckCondition(_payload.plugin)); + PermissionLib.MultiTargetPermission[] - memory permissions = new PermissionLib.MultiTargetPermission[](1); + memory permissions = new PermissionLib.MultiTargetPermission[](3); permissions[0] = PermissionLib.MultiTargetPermission({ operation: PermissionLib.Operation.Revoke, where: _payload.plugin, who: _dao, condition: PermissionLib.NO_CONDITION, - permissionId: Multisig(IMPLEMENTATION).UPGRADE_PLUGIN_PERMISSION_ID() + permissionId: UPGRADE_PLUGIN_PERMISSION_ID + }); + + permissions[1] = PermissionLib.MultiTargetPermission( + PermissionLib.Operation.GrantWithCondition, + _payload.plugin, + address(type(uint160).max), // ANY_ADDR + listedCheckCondition, + Multisig(IMPLEMENTATION).CREATE_PROPOSAL_PERMISSION_ID() + ); + + permissions[2] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Grant, + where: _payload.plugin, + who: _dao, + condition: PermissionLib.NO_CONDITION, + permissionId: SET_TARGET_CONFIG_PERMISSION_ID }); preparedSetupData.permissions = permissions; + + preparedSetupData.helpers = new address[](1); + preparedSetupData.helpers[0] = listedCheckCondition; + + initData = abi.encodeCall( + Multisig.initializeFrom, + (abi.decode(_payload.data, (PluginUUPSUpgradeable.TargetConfig))) + ); } } @@ -105,7 +173,7 @@ contract MultisigSetup is PluginUpgradeableSetup { SetupPayload calldata _payload ) external view returns (PermissionLib.MultiTargetPermission[] memory permissions) { // Prepare permissions - permissions = new PermissionLib.MultiTargetPermission[](2); + permissions = new PermissionLib.MultiTargetPermission[](5); // Set permissions to be Revoked. permissions[0] = PermissionLib.MultiTargetPermission({ @@ -113,15 +181,39 @@ contract MultisigSetup is PluginUpgradeableSetup { where: _payload.plugin, who: _dao, condition: PermissionLib.NO_CONDITION, - permissionId: Multisig(IMPLEMENTATION).UPDATE_MULTISIG_SETTINGS_PERMISSION_ID() + permissionId: UPDATE_MULTISIG_SETTINGS_PERMISSION_ID }); - permissions[1] = PermissionLib.MultiTargetPermission({ + permissions[1] = PermissionLib.MultiTargetPermission( + PermissionLib.Operation.Revoke, + _payload.plugin, + _dao, + PermissionLib.NO_CONDITION, + UPGRADE_PLUGIN_PERMISSION_ID + ); + + permissions[2] = PermissionLib.MultiTargetPermission({ operation: PermissionLib.Operation.Revoke, where: _dao, who: _payload.plugin, condition: PermissionLib.NO_CONDITION, permissionId: EXECUTE_PERMISSION_ID }); + + permissions[3] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Revoke, + where: _payload.plugin, + who: _dao, + condition: PermissionLib.NO_CONDITION, + permissionId: SET_TARGET_CONFIG_PERMISSION_ID + }); + + permissions[4] = PermissionLib.MultiTargetPermission( + PermissionLib.Operation.Revoke, + _payload.plugin, + address(type(uint160).max), // ANY_ADDR + PermissionLib.NO_CONDITION, + Multisig(IMPLEMENTATION).CREATE_PROPOSAL_PERMISSION_ID() + ); } } diff --git a/packages/contracts/src/build-metadata.json b/packages/contracts/src/build-metadata.json index 6f46d68b..644c41b8 100644 --- a/packages/contracts/src/build-metadata.json +++ b/packages/contracts/src/build-metadata.json @@ -30,6 +30,26 @@ "name": "multisigSettings", "type": "tuple", "description": "The initial multisig settings." + }, + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address", + "description": "The target contract to which actions will be forwarded to for execution." + }, + { + "internalType": "uint8", + "name": "operation", + "type": "uint8", + "description": "The operation type(either `call` or `delegatecall`) that will be used for execution forwarding." + } + ], + "internalType": "struct Multisig.TargetConfig", + "name": "TargetConfig", + "type": "tuple", + "description": "The initial target config" } ] }, @@ -41,6 +61,31 @@ "2": { "description": "No input is required for the update.", "inputs": [] + }, + "3": { + "description": "No input is required for the update.", + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address", + "description": "The target contract to which actions will be forwarded to for execution." + }, + { + "internalType": "uint8", + "name": "operation", + "type": "uint8", + "description": "The operation type(either `call` or `delegatecall`) that will be used for execution forwarding." + } + ], + "internalType": "struct Multisig.TargetConfig", + "name": "TargetConfig", + "type": "tuple", + "description": "The initial target config" + } + ] } }, "prepareUninstallation": { diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index f0a4e595..cb8f013d 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -7,16 +7,20 @@ import { IPlugin__factory, IProposal__factory, IProtocolVersion__factory, + ListedCheckCondition__factory, ProxyFactory__factory, } from '../../typechain'; import {ExecutedEvent} from '../../typechain/@aragon/osx-commons-contracts/src/dao/IDAO'; import {ProxyCreatedEvent} from '../../typechain/@aragon/osx-commons-contracts/src/utils/deployment/ProxyFactory'; +import {PluginUUPSUpgradeable} from '../../typechain/@aragon/osx-v1.0.0/core/plugin'; import { ApprovedEvent, ProposalCreatedEvent, ProposalExecutedEvent, } from '../../typechain/src/Multisig'; import { + ANY_ADDR, + CREATE_PROPOSAL_PERMISSION_ID, MULTISIG_EVENTS, MULTISIG_INTERFACE, UPDATE_MULTISIG_SETTINGS_PERMISSION_ID, @@ -37,6 +41,11 @@ import {expect} from 'chai'; import {BigNumber} from 'ethers'; import {ethers} from 'hardhat'; +type TargetConfig = { + target: string; + operation: number; +}; + type FixtureResult = { deployer: SignerWithAddress; alice: SignerWithAddress; @@ -49,6 +58,7 @@ type FixtureResult = { defaultInitData: { members: string[]; settings: Multisig.MultisigSettingsStruct; + targetConfig: TargetConfig; }; dao: DAO; dummyActions: DAOStructs.ActionStruct[]; @@ -75,10 +85,19 @@ async function fixture(): Promise { onlyListed: true, minApprovals: 2, }, + targetConfig: { + target: dao.address, + operation: 0, + }, }; const pluginInitdata = pluginImplementation.interface.encodeFunctionData( 'initialize', - [dao.address, defaultInitData.members, defaultInitData.settings] + [ + dao.address, + defaultInitData.members, + defaultInitData.settings, + defaultInitData.targetConfig, + ] ); const deploymentTx1 = await proxyFactory.deployUUPSProxy(pluginInitdata); const proxyCreatedEvent1 = findEvent( @@ -126,6 +145,31 @@ async function fixture(): Promise { }; } +async function loadFixtureAndGrantCreatePermission(): Promise { + let data = await loadFixture(fixture); + const {deployer, dao, initializedPlugin, uninitializedPlugin} = data; + + const condition = await new ListedCheckCondition__factory(deployer).deploy( + initializedPlugin.address + ); + + await dao.grantWithCondition( + initializedPlugin.address, + ANY_ADDR, + CREATE_PROPOSAL_PERMISSION_ID, + condition.address + ); + + await dao.grantWithCondition( + uninitializedPlugin.address, + ANY_ADDR, + CREATE_PROPOSAL_PERMISSION_ID, + condition.address + ); + return data; +} + +// TODO: maybe add test for `initializeFrom` and whether it successfuly works - i.e whether reinitializer(x) is correct and so on. describe('Multisig', function () { describe('initialize', async () => { it('reverts if trying to re-initialize', async () => { @@ -138,7 +182,8 @@ describe('Multisig', function () { initializedPlugin.initialize( dao.address, defaultInitData.members, - defaultInitData.settings + defaultInitData.settings, + defaultInitData.targetConfig ) ).to.be.revertedWith('Initializable: contract is already initialized'); }); @@ -157,7 +202,8 @@ describe('Multisig', function () { await plugin.initialize( dao.address, defaultInitData.members, - defaultInitData.settings + defaultInitData.settings, + defaultInitData.targetConfig ); // Check that all members from the init data have been listed as members. @@ -194,7 +240,8 @@ describe('Multisig', function () { uninitializedPlugin.initialize( dao.address, defaultInitData.members, - defaultInitData.settings + defaultInitData.settings, + defaultInitData.targetConfig ) ) .to.emit(uninitializedPlugin, MULTISIG_EVENTS.MultisigSettingsUpdated) @@ -220,6 +267,7 @@ describe('Multisig', function () { dao.address, overflowingMemberList, defaultInitData.settings, + defaultInitData.targetConfig, { gasLimit: BigNumber.from(10).pow(8).toNumber(), } @@ -283,6 +331,13 @@ describe('Multisig', function () { it('supports the `Multisig` interface', async () => { const {initializedPlugin: plugin} = await loadFixture(fixture); + // TODO: Figure out if in multisig contract, whether the below is correct and if so, + // fix this test, if not first fix multisig. + // bytes4( + // keccak256( + // "createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)" + // ) + // ) const interfaceId = getInterfaceId(MULTISIG_INTERFACE); expect(await plugin.supportsInterface(interfaceId)).to.be.true; }); @@ -643,129 +698,70 @@ describe('Multisig', function () { }); describe('createProposal', async () => { - it('increments the proposal count', async () => { - const { - alice, - initializedPlugin: plugin, - dummyMetadata, - dummyActions, - } = await loadFixture(fixture); - - // Check that the proposal count is 0. - expect(await plugin.proposalCount()).to.equal(0); - - // Create a proposal as Alice. - const endDate = (await time.latest()) + TIME.HOUR; - - await plugin - .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - // Check that the proposal count is 1. - expect(await plugin.proposalCount()).to.equal(1); + let data: FixtureResult; + beforeEach(async () => { + data = await loadFixtureAndGrantCreatePermission(); + }); + it('reverts if permission is not given', async () => { + const {deployer, dao, dummyMetadata, initializedPlugin: plugin} = data; + await dao.revoke(plugin.address, ANY_ADDR, CREATE_PROPOSAL_PERMISSION_ID); - // Create another proposal as Alice. - await plugin - .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate + await expect( + plugin[ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ]('0x', [], 0, false, false, 0, await time.latest()) + ) + .to.be.revertedWithCustomError(plugin, 'DaoUnauthorized') + .withArgs( + dao.address, + plugin.address, + deployer.address, + CREATE_PROPOSAL_PERMISSION_ID ); - - // Check that the proposal count is 2. - expect(await plugin.proposalCount()).to.equal(2); }); - it('creates unique proposal IDs for each proposal', async () => { + it('generates the proposal id by hashing the actions + metadata', async () => { const { alice, initializedPlugin: plugin, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + dao, + defaultInitData, + } = data; - // Make a static call to `createProposal` to get the first proposal ID ahead of time. - const endDate = (await time.latest()) + TIME.HOUR; - const proposalId0 = await plugin - .connect(alice) - .callStatic.createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); + const proposalId = plugin.hashProposal(dummyActions, dummyMetadata); - // Create the new proposal as Alice. - await expect( - plugin - .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ) - ).not.to.be.reverted; + // Create a proposal as Alice. + const endDate = (await time.latest()) + TIME.HOUR; - // Make a static call to `createProposal` to get the next proposal ID ahead of time. - const proposalId1 = await plugin + await plugin .connect(alice) - .callStatic.createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - // Check that the proposal IDs are as expected. - expect(proposalId0).to.equal(0); - expect(proposalId1).to.equal(1); + // Check that proposal exists + const proposal = await plugin.getProposal(proposalId); + expect(proposal.actions.length).to.equal(dummyActions.length); + expect(proposal.parameters.minApprovals).to.equal( + defaultInitData.settings.minApprovals + ); }); it('emits the `ProposalCreated` event', async () => { - const { - alice, - initializedPlugin: plugin, - dummyMetadata, - } = await loadFixture(fixture); + const {alice, initializedPlugin: plugin, dummyMetadata} = data; // Create a proposal as Alice and check the event arguments. const startDate = (await time.latest()) + TIME.MINUTE; const endDate = startDate + TIME.HOUR; - const expectedProposalId = 0; + const expectedProposalId = await plugin.hashProposal([], dummyMetadata); await expect( plugin .connect(alice) - .createProposal( - dummyMetadata, - [], - 0, - false, - false, - startDate, - endDate - ) + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, [], 0, false, false, startDate, endDate) ) .to.emit(plugin, 'ProposalCreated') .withArgs( @@ -780,11 +776,7 @@ describe('Multisig', function () { }); it('reverts if the multisig settings have been changed in the same block', async () => { - const { - alice, - initializedPlugin: plugin, - dao, - } = await loadFixture(fixture); + const {alice, initializedPlugin: plugin, dao} = data; // Grant Alice the permission to update the settings. await dao.grant( @@ -815,11 +807,17 @@ describe('Multisig', function () { uninitializedPlugin: plugin, dummyMetadata, dao, - } = await loadFixture(fixture); - await plugin.initialize(dao.address, [alice.address], { - onlyListed: true, - minApprovals: 1, - }); + defaultInitData, + } = data; + await plugin.initialize( + dao.address, + [alice.address], + { + onlyListed: true, + minApprovals: 1, + }, + defaultInitData.targetConfig + ); // Grant permissions between the DAO and the plugin. await dao.grant( @@ -852,27 +850,35 @@ describe('Multisig', function () { await ethers.provider.send('evm_setAutomine', [false]); // Create and execute proposal #1 calling `updateMultisigSettings`. - await plugin.connect(alice).createProposal( - dummyMetadata, - [updateMultisigSettingsAction], - 0, - true, // approve - true, // execute - 0, - endDate - ); - - // Try to call update the settings a second time. - await expect( - plugin.connect(alice).createProposal( + await plugin + .connect(alice) + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ]( dummyMetadata, [updateMultisigSettingsAction], 0, - false, // approve - false, // execute + true, // approve + true, // execute 0, endDate - ) + ); + + // Try to call update the settings a second time. + await expect( + plugin + .connect(alice) + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ]( + dummyMetadata, + [updateMultisigSettingsAction], + 0, + false, // approve + false, // execute + 0, + endDate + ) ) .to.revertedWithCustomError(plugin, 'ProposalCreationForbidden') .withArgs(alice.address); @@ -889,7 +895,7 @@ describe('Multisig', function () { initializedPlugin: plugin, dao, dummyMetadata, - } = await loadFixture(fixture); + } = data; // Grant Alice the permission to update settings. await dao.grant( @@ -908,20 +914,14 @@ describe('Multisig', function () { const startDate = (await time.latest()) + TIME.MINUTE; const endDate = startDate + TIME.HOUR; - const expectedProposalId = 0; + const expectedProposalId = await plugin.hashProposal([], dummyMetadata); await expect( plugin .connect(dave) // Dave is not listed. - .createProposal( - dummyMetadata, - [], - 0, - false, - false, - startDate, - endDate - ) + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, [], 0, false, false, startDate, endDate) ) .to.emit(plugin, 'ProposalCreated') .withArgs( @@ -939,29 +939,29 @@ describe('Multisig', function () { describe('`onlyListed` is set to `true`', async () => { it('reverts if the caller is not listed and only listed accounts can create proposals', async () => { const { + dao, dave, initializedPlugin: plugin, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; // Try to create a proposal as Dave (who is not listed), which should revert. const endDate = (await time.latest()) + TIME.HOUR; await expect( plugin .connect(dave) // Dave is not listed. - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ) + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate) ) - .to.be.revertedWithCustomError(plugin, 'ProposalCreationForbidden') - .withArgs(dave.address); + .to.be.revertedWithCustomError(plugin, 'DaoUnauthorized') + .withArgs( + dao.address, + plugin.address, + dave.address, + CREATE_PROPOSAL_PERMISSION_ID + ); }); it('reverts if caller is not listed in the current block although she was listed in the last block', async () => { @@ -973,7 +973,7 @@ describe('Multisig', function () { dao, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; // Grant Alice the permission to update settings. await dao.grant( @@ -1001,32 +1001,25 @@ describe('Multisig', function () { await expect( plugin .connect(carol) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ) + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate) ) - .to.be.revertedWithCustomError(plugin, 'ProposalCreationForbidden') - .withArgs(carol.address); + .to.be.revertedWithCustomError(plugin, 'DaoUnauthorized') + .withArgs( + dao.address, + plugin.address, + carol.address, + CREATE_PROPOSAL_PERMISSION_ID + ); // Transaction 4: Create the proposal as Dave const tx4 = await plugin .connect(dave) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); // Check the listed members before the block is mined. expect(await plugin.isListed(carol.address)).to.equal(true); @@ -1073,27 +1066,31 @@ describe('Multisig', function () { initializedPlugin: plugin, defaultInitData, dummyMetadata, - } = await loadFixture(fixture); + } = data; // Create a proposal (ID 0) as Alice but don't approve on creation. const startDate = (await time.latest()) + TIME.MINUTE; const endDate = startDate + TIME.HOUR; const allowFailureMap = 0; await time.setNextBlockTimestamp(startDate); - const id = 0; + const id = await plugin.hashProposal([], dummyMetadata); const approveProposal = false; await expect( - plugin.connect(alice).createProposal( - dummyMetadata, - [], - allowFailureMap, - approveProposal, // false - false, - startDate, - endDate - ) + plugin + .connect(alice) + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ]( + dummyMetadata, + [], + allowFailureMap, + approveProposal, // false + false, + startDate, + endDate + ) ) .to.emit(plugin, 'ProposalCreated') .withArgs(id, alice.address, startDate, endDate, dummyMetadata, [], 0); @@ -1128,7 +1125,7 @@ describe('Multisig', function () { initializedPlugin: plugin, defaultInitData, dummyMetadata, - } = await loadFixture(fixture); + } = data; // Create a proposal as Alice and approve on creation. const startDate = (await time.latest()) + TIME.MINUTE; @@ -1138,17 +1135,21 @@ describe('Multisig', function () { await time.setNextBlockTimestamp(startDate); - const id = 0; + const id = await plugin.hashProposal([], dummyMetadata); await expect( - plugin.connect(alice).createProposal( - dummyMetadata, - [], - allowFailureMap, - approveProposal, // true - false, - startDate, - endDate - ) + plugin + .connect(alice) + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ]( + dummyMetadata, + [], + allowFailureMap, + approveProposal, // true + false, + startDate, + endDate + ) ) .to.emit(plugin, 'ProposalCreated') .withArgs( @@ -1192,7 +1193,7 @@ describe('Multisig', function () { initializedPlugin: plugin, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; // Set the clock to the start date. const startDate = (await time.latest()) + TIME.MINUTE; @@ -1204,7 +1205,9 @@ describe('Multisig', function () { await expect( plugin .connect(alice) - .createProposal( + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ]( dummyMetadata, dummyActions, 0, @@ -1224,7 +1227,7 @@ describe('Multisig', function () { initializedPlugin: plugin, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; // Try to create a proposal as Alice where the end date is before the start date const startDate = (await time.latest()) + TIME.MINUTE; @@ -1232,15 +1235,9 @@ describe('Multisig', function () { await expect( plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - true, - false, - startDate, - endDate - ) + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, true, false, startDate, endDate) ) .to.be.revertedWithCustomError(plugin, 'DateOutOfBounds') .withArgs(startDate, endDate); @@ -1248,6 +1245,11 @@ describe('Multisig', function () { }); context('Approving and executing proposals', async () => { + let data: FixtureResult; + beforeEach(async () => { + data = await loadFixtureAndGrantCreatePermission(); + }); + describe('canApprove', async () => { it('returns `false` if the proposal is already executed', async () => { const { @@ -1258,23 +1260,17 @@ describe('Multisig', function () { dao, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; const endDate = (await time.latest()) + TIME.HOUR; - // Create a proposal (with ID 0) await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + + const id = await plugin.hashProposal(dummyActions, dummyMetadata); await dao.grant( dao.address, @@ -1304,21 +1300,15 @@ describe('Multisig', function () { initializedPlugin: plugin, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; // Create a proposal as Alice. const endDate = (await time.latest()) + TIME.HOUR; await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); const id = 0; // Check that Dave who is not listed cannot approve. @@ -1332,23 +1322,17 @@ describe('Multisig', function () { initializedPlugin: plugin, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; const endDate = (await time.latest()) + TIME.HOUR; // Create a proposal (with ID 0) await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); await plugin.connect(alice).approve(id, false); expect(await plugin.canApprove(id, alice.address)).to.be.false; @@ -1360,23 +1344,17 @@ describe('Multisig', function () { initializedPlugin: plugin, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; const endDate = (await time.latest()) + TIME.HOUR; // Create a proposal (with ID 0) await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); expect(await plugin.canApprove(id, alice.address)).to.be.true; }); @@ -1387,7 +1365,7 @@ describe('Multisig', function () { initializedPlugin: plugin, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; const startDate = (await time.latest()) + TIME.MINUTE; const endDate = startDate + TIME.HOUR; @@ -1395,16 +1373,10 @@ describe('Multisig', function () { // Create a proposal (with ID 0) await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - startDate, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, startDate, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); expect(await plugin.canApprove(id, alice.address)).to.be.false; @@ -1419,23 +1391,17 @@ describe('Multisig', function () { initializedPlugin: plugin, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; const endDate = (await time.latest()) + TIME.HOUR; // Create a proposal (with ID 0) await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); expect(await plugin.canApprove(id, alice.address)).to.be.true; @@ -1452,23 +1418,17 @@ describe('Multisig', function () { initializedPlugin: plugin, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; const endDate = (await time.latest()) + TIME.HOUR; // Create a proposal (with ID 0) await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); expect(await plugin.hasApproved(id, alice.address)).to.be.false; }); @@ -1479,22 +1439,16 @@ describe('Multisig', function () { initializedPlugin: plugin, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; const endDate = (await time.latest()) + TIME.HOUR; await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); await plugin.connect(alice).approve(id, false); expect(await plugin.hasApproved(id, alice.address)).to.be.true; @@ -1508,23 +1462,17 @@ describe('Multisig', function () { initializedPlugin: plugin, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; const endDate = (await time.latest()) + TIME.HOUR; // Create a proposal (with ID 0) await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); await plugin.connect(alice).approve(id, true); @@ -1541,23 +1489,17 @@ describe('Multisig', function () { dummyMetadata, dummyActions, dao, - } = await loadFixture(fixture); + } = data; const endDate = (await time.latest()) + TIME.HOUR; // Create a proposal (with ID 0) await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. await dao.grant( @@ -1580,23 +1522,17 @@ describe('Multisig', function () { initializedPlugin: plugin, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; const endDate = (await time.latest()) + TIME.HOUR; // Create a proposal (with ID 0). await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); // Check that there are 0 approvals yet. expect((await plugin.getProposal(id)).approvals).to.equal(0); @@ -1619,28 +1555,22 @@ describe('Multisig', function () { initializedPlugin: plugin, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; // Create a proposal as Alice that didn't started yet. const startDate = (await time.latest()) + TIME.MINUTE; const endDate = startDate + TIME.HOUR; await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - startDate, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, startDate, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); // Try to approve the proposal as Alice although being before the start date. await expect(plugin.connect(alice).approve(id, false)) .to.be.revertedWithCustomError(plugin, 'ApprovalCastForbidden') - .withArgs(0, alice.address); + .withArgs(id, alice.address); // Advance to the start date. await time.increaseTo(startDate); @@ -1656,7 +1586,7 @@ describe('Multisig', function () { initializedPlugin: plugin, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; // Create a proposal as Alice that starts now. const startDate = (await time.latest()) + TIME.MINUTE; @@ -1664,16 +1594,10 @@ describe('Multisig', function () { await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); // Advance time after the end date. await time.increaseTo(endDate + 1); @@ -1691,22 +1615,16 @@ describe('Multisig', function () { initializedPlugin: plugin, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; // Create a proposal as Alice. const endDate = (await time.latest()) + TIME.HOUR; await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); // Check that `minApprovals` isn't met yet. const proposal = await plugin.getProposal(id); @@ -1724,22 +1642,16 @@ describe('Multisig', function () { dao, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; // Create a proposal as Alice. const endDate = (await time.latest()) + TIME.HOUR; await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. await dao.grant( @@ -1767,22 +1679,16 @@ describe('Multisig', function () { initializedPlugin: plugin, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; const endDate = (await time.latest()) + TIME.HOUR; await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); await plugin.connect(alice).approve(id, false); await plugin.connect(bob).approve(id, false); @@ -1799,23 +1705,17 @@ describe('Multisig', function () { initializedPlugin: plugin, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; const startDate = (await time.latest()) + TIME.MINUTE; const endDate = startDate + TIME.HOUR; await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - startDate, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, startDate, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); expect(await plugin.canExecute(id)).to.be.false; @@ -1835,22 +1735,16 @@ describe('Multisig', function () { initializedPlugin: plugin, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; const endDate = (await time.latest()) + TIME.HOUR; await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); await plugin.connect(alice).approve(id, false); await plugin.connect(bob).approve(id, false); @@ -1872,22 +1766,16 @@ describe('Multisig', function () { dao, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; // Create a proposal as Alice. const endDate = (await time.latest()) + TIME.HOUR; await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. await dao.grant( @@ -1911,22 +1799,16 @@ describe('Multisig', function () { dao, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; // Create a proposal as Alice. const endDate = (await time.latest()) + TIME.HOUR; await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. await dao.grant( @@ -1965,22 +1847,16 @@ describe('Multisig', function () { dao, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; const endDate = (await time.latest()) + TIME.HOUR; await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); await dao.grant( dao.address, @@ -2014,22 +1890,16 @@ describe('Multisig', function () { dao, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; // Create a proposal as Alice. const endDate = (await time.latest()) + TIME.HOUR; await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. await dao.grant( @@ -2078,7 +1948,9 @@ describe('Multisig', function () { ); expect(event.args.actor).to.equal(plugin.address); - expect(event.args.callId).to.equal(proposalIdToBytes32(id)); + expect(event.args.callId).to.equal( + ethers.utils.hexZeroPad(id.toHexString(), 32) + ); expect(event.args.actions.length).to.equal(1); expect(event.args.actions[0].to).to.equal(dummyActions[0].to); expect(event.args.actions[0].value).to.equal(dummyActions[0].value); @@ -2112,22 +1984,16 @@ describe('Multisig', function () { dao, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; // Create a proposal as Alice. const endDate = (await time.latest()) + TIME.HOUR; await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` on the DAO. await dao.grant( @@ -2156,22 +2022,16 @@ describe('Multisig', function () { dao, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; // Create a proposal as Alice. const endDate = (await time.latest()) + TIME.HOUR; await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. await dao.grant( @@ -2199,23 +2059,17 @@ describe('Multisig', function () { dao, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; // Create a proposal as Alice. const startDate = (await time.latest()) + TIME.MINUTE; const endDate = startDate + TIME.HOUR; await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - startDate, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, startDate, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. await dao.grant( @@ -2247,23 +2101,17 @@ describe('Multisig', function () { initializedPlugin: plugin, dummyMetadata, dummyActions, - } = await loadFixture(fixture); + } = data; // Create a proposal as Alice. const startDate = (await time.latest()) + TIME.MINUTE; const endDate = startDate + TIME.HOUR; await plugin .connect(alice) - .createProposal( - dummyMetadata, - dummyActions, - 0, - false, - false, - 0, - endDate - ); - const id = 0; + [ + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' + ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + const id = await plugin.hashProposal(dummyActions, dummyMetadata); // Approve the proposal but do not execute yet. await plugin.connect(alice).approve(id, false); diff --git a/packages/contracts/test/10_unit-testing/12_plugin-setup.ts b/packages/contracts/test/10_unit-testing/12_plugin-setup.ts index e8b6f39c..423dfa88 100644 --- a/packages/contracts/test/10_unit-testing/12_plugin-setup.ts +++ b/packages/contracts/test/10_unit-testing/12_plugin-setup.ts @@ -2,8 +2,12 @@ import {createDaoProxy} from '../20_integration-testing/test-helpers'; import {METADATA} from '../../plugin-settings'; import {MultisigSetup, MultisigSetup__factory} from '../../typechain'; import { + ANY_ADDR, + CREATE_PROPOSAL_PERMISSION_ID, MULTISIG_INTERFACE, + SET_TARGET_CONFIG_PERMISSION_ID, UPDATE_MULTISIG_SETTINGS_PERMISSION_ID, + UPGRADE_PLUGIN_PERMISSION_ID, } from '../multisig-constants'; import {Multisig__factory, Multisig} from '../test-utils/typechain-versions'; import { @@ -22,12 +26,18 @@ import {ethers} from 'hardhat'; const abiCoder = ethers.utils.defaultAbiCoder; const AddressZero = ethers.constants.AddressZero; +type TargetConfig = { + target: string; + operation: number; +}; + type FixtureResult = { deployer: SignerWithAddress; alice: SignerWithAddress; bob: SignerWithAddress; carol: SignerWithAddress; pluginSetup: MultisigSetup; + defaultTargetConfig: TargetConfig; defaultMembers: string[]; defaultMultisigSettings: Multisig.MultisigSettingsStruct; prepareInstallationInputs: string; @@ -52,12 +62,18 @@ async function fixture(): Promise { minApprovals: 1, }; + const defaultTargetConfig = {target: dao.address, operation: 0}; + // Provide installation inputs const prepareInstallationInputs = ethers.utils.defaultAbiCoder.encode( getNamedTypesFromMetadata( METADATA.build.pluginSetup.prepareInstallation.inputs ), - [defaultMembers, Object.values(defaultMultisigSettings)] + [ + defaultMembers, + Object.values(defaultMultisigSettings), + defaultTargetConfig, + ] ); // Provide uninstallation inputs @@ -75,6 +91,7 @@ async function fixture(): Promise { carol, pluginSetup, defaultMembers, + defaultTargetConfig, defaultMultisigSettings, prepareInstallationInputs, prepareUninstallationInputs, @@ -82,7 +99,7 @@ async function fixture(): Promise { }; } -describe('MultisigSetup', function () { +describe.only('MultisigSetup', function () { it('does not support the empty interface', async () => { const {pluginSetup} = await loadFixture(fixture); expect(await pluginSetup.supportsInterface('0xffffffff')).to.be.false; @@ -96,6 +113,7 @@ describe('MultisigSetup', function () { await pluginSetup.implementation() ); + // TODO: fix this with the same idea as the test in `plugin.ts` that also needs fixing. expect( await multisigImplementation.supportsInterface( getInterfaceId(MULTISIG_INTERFACE) @@ -128,8 +146,13 @@ describe('MultisigSetup', function () { }); it('reverts if zero members are provided in the initialization data', async () => { - const {deployer, pluginSetup, dao, defaultMultisigSettings} = - await loadFixture(fixture); + const { + deployer, + pluginSetup, + dao, + defaultMultisigSettings, + defaultTargetConfig, + } = await loadFixture(fixture); // Create input data containing an empty list of initial members. const noMembers: string[] = []; @@ -137,7 +160,7 @@ describe('MultisigSetup', function () { getNamedTypesFromMetadata( METADATA.build.pluginSetup.prepareInstallation.inputs ), - [noMembers, defaultMultisigSettings] + [noMembers, defaultMultisigSettings, defaultTargetConfig] ); // Anticipate the plugin proxy address that will be deployed. @@ -166,7 +189,8 @@ describe('MultisigSetup', function () { }); it('reverts if the `minApprovals` value in `_data` is zero', async () => { - const {deployer, pluginSetup, dao} = await loadFixture(fixture); + const {deployer, pluginSetup, dao, defaultTargetConfig} = + await loadFixture(fixture); // Create input data containing a `minApprovals` threshold of 0. const multisigSettings: Multisig.MultisigSettingsStruct = { @@ -178,7 +202,7 @@ describe('MultisigSetup', function () { getNamedTypesFromMetadata( METADATA.build.pluginSetup.prepareInstallation.inputs ), - [members, multisigSettings] + [members, multisigSettings, defaultTargetConfig] ); // Anticipate the plugin proxy address that will be deployed. @@ -207,7 +231,8 @@ describe('MultisigSetup', function () { }); it('reverts if the `minApprovals` value in `_data` is greater than the number of members', async () => { - const {deployer, pluginSetup, dao} = await loadFixture(fixture); + const {deployer, pluginSetup, dao, defaultTargetConfig} = + await loadFixture(fixture); // Create input data containing an initial member list with a length lower that the specified `minApprovals` // threshold. @@ -220,7 +245,7 @@ describe('MultisigSetup', function () { getNamedTypesFromMetadata( METADATA.build.pluginSetup.prepareInstallation.inputs ), - [members, multisigSettings] + [members, multisigSettings, defaultTargetConfig] ); // Anticipate the plugin proxy address that will be deployed. @@ -273,8 +298,11 @@ describe('MultisigSetup', function () { // Check the return data. expect(plugin).to.be.equal(anticipatedPluginAddress); - expect(helpers.length).to.be.equal(0); - expect(permissions.length).to.be.equal(2); + expect(helpers.length).to.be.equal(1); + expect(permissions.length).to.be.equal(4); + + const condition = helpers[0]; + expect(permissions).to.deep.equal([ [ Operation.Grant, @@ -290,6 +318,20 @@ describe('MultisigSetup', function () { AddressZero, DAO_PERMISSIONS.EXECUTE_PERMISSION_ID, ], + [ + Operation.GrantWithCondition, + plugin, + ANY_ADDR, + condition, + CREATE_PROPOSAL_PERMISSION_ID, + ], + [ + Operation.Grant, + plugin, + dao.address, + AddressZero, + SET_TARGET_CONFIG_PERMISSION_ID, + ], ]); }); @@ -334,9 +376,18 @@ describe('MultisigSetup', function () { describe('prepareUpdate', async () => { it('returns the permissions expected for the update from build 1', async () => { - const {pluginSetup, dao} = await loadFixture(fixture); + const {pluginSetup, dao, defaultTargetConfig} = await loadFixture( + fixture + ); const plugin = ethers.Wallet.createRandom().address; + const prepareUpdateInputs = ethers.utils.defaultAbiCoder.encode( + getNamedTypesFromMetadata( + METADATA.build.pluginSetup.prepareUpdate[3].inputs + ), + [defaultTargetConfig] + ); + // Make a static call to check that the plugin update data being returned is correct. const { initData: initData, @@ -346,14 +397,19 @@ describe('MultisigSetup', function () { ethers.Wallet.createRandom().address, ethers.Wallet.createRandom().address, ], - data: [], + data: prepareUpdateInputs, plugin, }); // Check the return data. - expect(initData).to.be.eq('0x'); - expect(permissions.length).to.be.eql(1); - expect(helpers).to.be.eql([]); + expect(initData).to.be.eq( + Multisig__factory.createInterface().encodeFunctionData( + 'initializeFrom', + [defaultTargetConfig] + ) + ); + expect(permissions.length).to.be.equal(3); + expect(helpers.length).to.be.equal(1); // check correct permission is revoked expect(permissions).to.deep.equal([ [ @@ -363,13 +419,36 @@ describe('MultisigSetup', function () { AddressZero, PLUGIN_UUPS_UPGRADEABLE_PERMISSIONS.UPGRADE_PLUGIN_PERMISSION_ID, ], + [ + Operation.GrantWithCondition, + plugin, + ANY_ADDR, + helpers[0], + CREATE_PROPOSAL_PERMISSION_ID, + ], + [ + Operation.Grant, + plugin, + dao.address, + AddressZero, + SET_TARGET_CONFIG_PERMISSION_ID, + ], ]); }); it('returns the permissions expected for the update from build 2', async () => { - const {pluginSetup, dao} = await loadFixture(fixture); + const {pluginSetup, dao, defaultTargetConfig} = await loadFixture( + fixture + ); const plugin = ethers.Wallet.createRandom().address; + const prepareUpdateInputs = ethers.utils.defaultAbiCoder.encode( + getNamedTypesFromMetadata( + METADATA.build.pluginSetup.prepareUpdate[3].inputs + ), + [defaultTargetConfig] + ); + // Make a static call to check that the plugin update data being returned is correct. const { initData: initData, @@ -379,14 +458,19 @@ describe('MultisigSetup', function () { ethers.Wallet.createRandom().address, ethers.Wallet.createRandom().address, ], - data: [], + data: prepareUpdateInputs, plugin, }); // Check the return data. - expect(initData).to.be.eq('0x'); - expect(permissions.length).to.be.eql(1); - expect(helpers).to.be.eql([]); + expect(initData).to.be.eq( + Multisig__factory.createInterface().encodeFunctionData( + 'initializeFrom', + [defaultTargetConfig] + ) + ); + expect(permissions.length).to.be.equal(3); + expect(helpers.length).to.be.equal(1); // check correct permission is revoked expect(permissions).to.deep.equal([ [ @@ -396,6 +480,20 @@ describe('MultisigSetup', function () { AddressZero, PLUGIN_UUPS_UPGRADEABLE_PERMISSIONS.UPGRADE_PLUGIN_PERMISSION_ID, ], + [ + Operation.GrantWithCondition, + plugin, + ANY_ADDR, + helpers[0], + CREATE_PROPOSAL_PERMISSION_ID, + ], + [ + Operation.Grant, + plugin, + dao.address, + AddressZero, + SET_TARGET_CONFIG_PERMISSION_ID, + ], ]); }); }); @@ -421,7 +519,7 @@ describe('MultisigSetup', function () { ); // Check the return data. - expect(permissions.length).to.be.equal(2); + expect(permissions.length).to.be.equal(5); expect(permissions).to.deep.equal([ [ Operation.Revoke, @@ -430,6 +528,13 @@ describe('MultisigSetup', function () { AddressZero, UPDATE_MULTISIG_SETTINGS_PERMISSION_ID, ], + [ + Operation.Revoke, + plugin, + dao.address, + AddressZero, + UPGRADE_PLUGIN_PERMISSION_ID, + ], [ Operation.Revoke, dao.address, @@ -437,6 +542,20 @@ describe('MultisigSetup', function () { AddressZero, DAO_PERMISSIONS.EXECUTE_PERMISSION_ID, ], + [ + Operation.Revoke, + plugin, + dao.address, + AddressZero, + SET_TARGET_CONFIG_PERMISSION_ID, + ], + [ + Operation.Revoke, + plugin, + ANY_ADDR, + AddressZero, + CREATE_PROPOSAL_PERMISSION_ID, + ], ]); }); }); diff --git a/packages/contracts/test/30_regression-testing/31_upgradeability.ts b/packages/contracts/test/30_regression-testing/31_upgradeability.ts index 6f65cf69..447bfcfe 100644 --- a/packages/contracts/test/30_regression-testing/31_upgradeability.ts +++ b/packages/contracts/test/30_regression-testing/31_upgradeability.ts @@ -17,6 +17,7 @@ import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; import {expect} from 'chai'; import {ethers} from 'hardhat'; +// TODO: pass `initializeFrom` initData on upgrade to test it as well. describe('Upgrades', () => { it('upgrades to a new implementation', async () => { const {deployer, alice, dao, defaultInitData} = await loadFixture(fixture); @@ -25,7 +26,12 @@ describe('Upgrades', () => { await deployAndUpgradeSelfCheck( deployer, alice, - [dao.address, defaultInitData.members, defaultInitData.settings], + [ + dao.address, + defaultInitData.members, + defaultInitData.settings, + {target: dao.address, operation: 0}, + ], 'initialize', currentContractFactory, PLUGIN_UUPS_UPGRADEABLE_PERMISSIONS.UPGRADE_PLUGIN_PERMISSION_ID, diff --git a/packages/contracts/test/multisig-constants.ts b/packages/contracts/test/multisig-constants.ts index d9e55641..77cf5948 100644 --- a/packages/contracts/test/multisig-constants.ts +++ b/packages/contracts/test/multisig-constants.ts @@ -8,10 +8,24 @@ export const MULTISIG_EVENTS = { export const MULTISIG_INTERFACE = new ethers.utils.Interface([ 'function initialize(address,address[],tuple(bool,uint16))', 'function updateMultisigSettings(tuple(bool,uint16))', - 'function createProposal(bytes,tuple(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64) ', + 'function createProposal(bytes,tuple(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)', 'function getProposal(uint256)', ]); export const UPDATE_MULTISIG_SETTINGS_PERMISSION_ID = ethers.utils.id( 'UPDATE_MULTISIG_SETTINGS_PERMISSION' ); + +export const UPGRADE_PLUGIN_PERMISSION_ID = ethers.utils.id( + 'UPGRADE_PLUGIN_PERMISSION' +); + +export const CREATE_PROPOSAL_PERMISSION_ID = ethers.utils.id( + 'CREATE_PROPOSAL_PERMISSION' +); + +export const SET_TARGET_CONFIG_PERMISSION_ID = ethers.utils.id( + 'SET_TARGET_CONFIG_PERMISSION' +); + +export const ANY_ADDR = '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF'; diff --git a/packages/contracts/test/test-utils/uups-upgradeable.ts b/packages/contracts/test/test-utils/uups-upgradeable.ts index 80183815..8574d4b4 100644 --- a/packages/contracts/test/test-utils/uups-upgradeable.ts +++ b/packages/contracts/test/test-utils/uups-upgradeable.ts @@ -33,7 +33,7 @@ export async function deployAndUpgradeSelfCheck( { kind: 'uups', initializer: initializerName, - unsafeAllow: ['constructor'], + unsafeAllow: ['constructor', 'delegatecall'], constructorArgs: [], } ); @@ -49,7 +49,7 @@ export async function deployAndUpgradeSelfCheck( if (managingContract === undefined) { await expect( upgrades.upgradeProxy(proxy.address, factory.connect(upgrader), { - unsafeAllow: ['constructor'], + unsafeAllow: ['constructor', 'delegatecall'], constructorArgs: [], }) ) @@ -62,7 +62,7 @@ export async function deployAndUpgradeSelfCheck( else { await expect( upgrades.upgradeProxy(proxy.address, factory.connect(upgrader), { - unsafeAllow: ['constructor'], + unsafeAllow: ['constructor', 'delegatecall'], constructorArgs: [], }) ) @@ -115,7 +115,7 @@ export async function deployAndUpgradeFromToCheck( { kind: 'uups', initializer: initializerName, - unsafeAllow: ['constructor'], + unsafeAllow: ['constructor', 'delegatecall'], constructorArgs: [], } ); @@ -134,7 +134,7 @@ export async function deployAndUpgradeFromToCheck( if (managingDao === undefined) { await expect( upgrades.upgradeProxy(proxy.address, to.connect(upgrader), { - unsafeAllow: ['constructor'], + unsafeAllow: ['constructor', 'delegatecall'], constructorArgs: [], }) ) @@ -145,7 +145,7 @@ export async function deployAndUpgradeFromToCheck( } else { await expect( upgrades.upgradeProxy(proxy.address, to.connect(upgrader), { - unsafeAllow: ['constructor'], + unsafeAllow: ['constructor', 'delegatecall'], constructorArgs: [], }) ) @@ -157,7 +157,7 @@ export async function deployAndUpgradeFromToCheck( // Upgrade the proxy to a new implementation from a different factory await upgrades.upgradeProxy(proxy.address, to.connect(upgrader), { - unsafeAllow: ['constructor'], + unsafeAllow: ['constructor', 'delegatecall'], constructorArgs: [], }); From 2ca811392f58f43264bdde013aef1c3f7148776f Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Wed, 21 Aug 2024 11:41:40 +0400 Subject: [PATCH 02/59] fix more tests --- packages/contracts/src/Multisig.sol | 7 +++-- .../test/10_unit-testing/11_plugin.ts | 16 ++--------- .../test/10_unit-testing/12_plugin-setup.ts | 12 +++----- .../20_integration-testing/21_deployment.ts | 2 ++ .../22_setup-processing.ts | 28 ++++++++++++++----- .../20_integration-testing/test-helpers.ts | 12 ++++++-- .../31_upgradeability.ts | 8 +++++- packages/contracts/test/multisig-constants.ts | 12 +++++++- 8 files changed, 61 insertions(+), 36 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index d9f3255e..6d894725 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -46,8 +46,8 @@ contract Multisig is mapping(address => bool) approvers; IDAO.Action[] actions; uint256 allowFailureMap; - address target; // added in v1.4.0 - Operation operation; // added in v1.4.0 + address target; // added in v1.3.0 + Operation operation; // added in v1.3.0 } /// @notice A container for the proposal parameters. @@ -70,6 +70,8 @@ contract Multisig is uint16 minApprovals; } + // todo: since `initialize` was changed, this means it no longer supports the old interfaceId. this could be a breaking change. + // maybe UI was already using `supportsinterface`. /// @notice The [ERC-165](https://eips.ethereum.org/EIPS/eip-165) interface ID of the contract. bytes4 internal constant MULTISIG_INTERFACE_ID = this.initialize.selector ^ @@ -171,7 +173,6 @@ contract Multisig is _setTargetConfig(_targetConfig); } - // TODO: Would we need version checking and only calling _setTargetConfig in specific cases ? // TODO: What should the number in reinitializer(x) must be ? function initializeFrom(TargetConfig calldata _targetConfig) external reinitializer(2) { _setTargetConfig(_targetConfig); diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index cb8f013d..f79c8c22 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -23,6 +23,8 @@ import { CREATE_PROPOSAL_PERMISSION_ID, MULTISIG_EVENTS, MULTISIG_INTERFACE, + Operation, + TargetConfig, UPDATE_MULTISIG_SETTINGS_PERMISSION_ID, } from '../multisig-constants'; import {Multisig__factory, Multisig} from '../test-utils/typechain-versions'; @@ -41,11 +43,6 @@ import {expect} from 'chai'; import {BigNumber} from 'ethers'; import {ethers} from 'hardhat'; -type TargetConfig = { - target: string; - operation: number; -}; - type FixtureResult = { deployer: SignerWithAddress; alice: SignerWithAddress; @@ -87,7 +84,7 @@ async function fixture(): Promise { }, targetConfig: { target: dao.address, - operation: 0, + operation: Operation.call, }, }; const pluginInitdata = pluginImplementation.interface.encodeFunctionData( @@ -331,13 +328,6 @@ describe('Multisig', function () { it('supports the `Multisig` interface', async () => { const {initializedPlugin: plugin} = await loadFixture(fixture); - // TODO: Figure out if in multisig contract, whether the below is correct and if so, - // fix this test, if not first fix multisig. - // bytes4( - // keccak256( - // "createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)" - // ) - // ) const interfaceId = getInterfaceId(MULTISIG_INTERFACE); expect(await plugin.supportsInterface(interfaceId)).to.be.true; }); diff --git a/packages/contracts/test/10_unit-testing/12_plugin-setup.ts b/packages/contracts/test/10_unit-testing/12_plugin-setup.ts index 423dfa88..818abb46 100644 --- a/packages/contracts/test/10_unit-testing/12_plugin-setup.ts +++ b/packages/contracts/test/10_unit-testing/12_plugin-setup.ts @@ -6,9 +6,11 @@ import { CREATE_PROPOSAL_PERMISSION_ID, MULTISIG_INTERFACE, SET_TARGET_CONFIG_PERMISSION_ID, + TargetConfig, UPDATE_MULTISIG_SETTINGS_PERMISSION_ID, UPGRADE_PLUGIN_PERMISSION_ID, } from '../multisig-constants'; +import {Operation as op} from '../multisig-constants'; import {Multisig__factory, Multisig} from '../test-utils/typechain-versions'; import { getInterfaceId, @@ -26,11 +28,6 @@ import {ethers} from 'hardhat'; const abiCoder = ethers.utils.defaultAbiCoder; const AddressZero = ethers.constants.AddressZero; -type TargetConfig = { - target: string; - operation: number; -}; - type FixtureResult = { deployer: SignerWithAddress; alice: SignerWithAddress; @@ -62,7 +59,7 @@ async function fixture(): Promise { minApprovals: 1, }; - const defaultTargetConfig = {target: dao.address, operation: 0}; + const defaultTargetConfig = {target: dao.address, operation: op.call}; // Provide installation inputs const prepareInstallationInputs = ethers.utils.defaultAbiCoder.encode( @@ -99,7 +96,7 @@ async function fixture(): Promise { }; } -describe.only('MultisigSetup', function () { +describe('MultisigSetup', function () { it('does not support the empty interface', async () => { const {pluginSetup} = await loadFixture(fixture); expect(await pluginSetup.supportsInterface('0xffffffff')).to.be.false; @@ -113,7 +110,6 @@ describe.only('MultisigSetup', function () { await pluginSetup.implementation() ); - // TODO: fix this with the same idea as the test in `plugin.ts` that also needs fixing. expect( await multisigImplementation.supportsInterface( getInterfaceId(MULTISIG_INTERFACE) diff --git a/packages/contracts/test/20_integration-testing/21_deployment.ts b/packages/contracts/test/20_integration-testing/21_deployment.ts index 1e3ba978..1449af41 100644 --- a/packages/contracts/test/20_integration-testing/21_deployment.ts +++ b/packages/contracts/test/20_integration-testing/21_deployment.ts @@ -79,6 +79,8 @@ describe(`Deployment on network '${productionNetworkName}'`, function () { expect(results.buildMetadata).to.equal( ethers.utils.hexlify(ethers.utils.toUtf8Bytes(buildMetadataURI)) ); + + expect(results.tag.build).to.equal(VERSION.build); }); }); }); diff --git a/packages/contracts/test/20_integration-testing/22_setup-processing.ts b/packages/contracts/test/20_integration-testing/22_setup-processing.ts index cff58961..e3a7907c 100644 --- a/packages/contracts/test/20_integration-testing/22_setup-processing.ts +++ b/packages/contracts/test/20_integration-testing/22_setup-processing.ts @@ -1,6 +1,7 @@ import {METADATA, VERSION} from '../../plugin-settings'; import {MultisigSetup, Multisig__factory} from '../../typechain'; import {getProductionNetworkName, findPluginRepo} from '../../utils/helpers'; +import {Operation, TargetConfig} from '../multisig-constants'; import {Multisig} from '../test-utils/typechain-versions'; import { createDaoProxy, @@ -41,6 +42,7 @@ type FixtureResult = { defaultInitData: { members: string[]; settings: Multisig.MultisigSettingsStruct; + targetConfig: TargetConfig; }; psp: PluginSetupProcessor; pluginRepo: PluginRepo; @@ -90,6 +92,10 @@ async function fixture(): Promise { onlyListed: true, minApprovals: 1, }, + targetConfig: { + target: dao.address, + operation: Operation.call, + }, }; const pluginSetupRefLatestBuild = { @@ -113,10 +119,18 @@ async function fixture(): Promise { }; } +// TODO: when upgrade happens, test that `initializeFrom` was called successfully. describe(`PluginSetup processing on network '${productionNetworkName}'`, function () { it('installs & uninstalls the current build', async () => { - const {alice, bob, deployer, psp, dao, pluginSetupRefLatestBuild} = - await loadFixture(fixture); + const { + alice, + bob, + deployer, + psp, + dao, + pluginSetupRefLatestBuild, + defaultInitData, + } = await loadFixture(fixture); // Grant deployer all required permissions await dao @@ -153,7 +167,7 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio getNamedTypesFromMetadata( METADATA.build.pluginSetup.prepareInstallation.inputs ), - [initialMembers, multisigSettings] + [initialMembers, multisigSettings, defaultInitData.targetConfig] ) ); @@ -162,7 +176,7 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio deployer ); - // Check that the setup worked + // // Check that the setup worked expect(await plugin.isMember(alice.address)).to.be.true; // Uninstall the current build. @@ -178,7 +192,7 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio ), [] ), - [] + results.preparedEvent.args.preparedSetupData.helpers ); }); @@ -200,7 +214,7 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio pluginSetupRefLatestBuild, 1, [defaultInitData.members, Object.values(defaultInitData.settings)], - [] + [defaultInitData.targetConfig] ); }); @@ -222,7 +236,7 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio pluginSetupRefLatestBuild, 2, [defaultInitData.members, Object.values(defaultInitData.settings)], - [] + [defaultInitData.targetConfig] ); }); }); diff --git a/packages/contracts/test/20_integration-testing/test-helpers.ts b/packages/contracts/test/20_integration-testing/test-helpers.ts index d1a47612..ff2818c0 100644 --- a/packages/contracts/test/20_integration-testing/test-helpers.ts +++ b/packages/contracts/test/20_integration-testing/test-helpers.ts @@ -26,6 +26,7 @@ import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; import {expect} from 'chai'; import {ContractTransaction} from 'ethers'; import {ethers} from 'hardhat'; +import {create} from 'ipfs-http-client'; export async function installPLugin( signer: SignerWithAddress, @@ -272,6 +273,7 @@ export async function updateFromBuildTest( }, pluginSetupRepo: pluginRepo.address, }; + const installationResults = await installPLugin( deployer, psp, @@ -279,7 +281,12 @@ export async function updateFromBuildTest( pluginSetupRefPreviousBuild, ethers.utils.defaultAbiCoder.encode( getNamedTypesFromMetadata( - METADATA.build.pluginSetup.prepareInstallation.inputs + // NOTE that this approach is not efficient and in reality, we should be + // fetching `build`'s ipfs cid from pluginRepo and getting the abi from there. + [ + METADATA.build.pluginSetup.prepareInstallation.inputs[0], + METADATA.build.pluginSetup.prepareInstallation.inputs[1], + ] ), installationInputs ) @@ -324,7 +331,7 @@ export async function updateFromBuildTest( pluginSetupRefLatestBuild, ethers.utils.defaultAbiCoder.encode( getNamedTypesFromMetadata( - METADATA.build.pluginSetup.prepareUpdate[1].inputs + METADATA.build.pluginSetup.prepareUpdate[3].inputs ), updateInputs ) @@ -344,7 +351,6 @@ export async function updateFromBuildTest( expect(await plugin.implementation()).to.equal(implementationLatestBuild); } -// TODO Move into OSX commons as part of Task OS-928. export async function createDaoProxy( deployer: SignerWithAddress, dummyMetadata: string diff --git a/packages/contracts/test/30_regression-testing/31_upgradeability.ts b/packages/contracts/test/30_regression-testing/31_upgradeability.ts index 447bfcfe..be28aae3 100644 --- a/packages/contracts/test/30_regression-testing/31_upgradeability.ts +++ b/packages/contracts/test/30_regression-testing/31_upgradeability.ts @@ -1,4 +1,5 @@ import {createDaoProxy} from '../20_integration-testing/test-helpers'; +import {Operation, TargetConfig} from '../multisig-constants'; import { Multisig_V1_1__factory, Multisig_V1_2__factory, @@ -30,7 +31,7 @@ describe('Upgrades', () => { dao.address, defaultInitData.members, defaultInitData.settings, - {target: dao.address, operation: 0}, + defaultInitData.targetConfig, ], 'initialize', currentContractFactory, @@ -108,6 +109,7 @@ type FixtureResult = { defaultInitData: { members: string[]; settings: Multisig.MultisigSettingsStruct; + targetConfig: TargetConfig; }; dao: DAO; }; @@ -126,6 +128,10 @@ async function fixture(): Promise { onlyListed: true, minApprovals: 2, }, + targetConfig: { + target: dao.address, + operation: Operation.call, + }, }; return { diff --git a/packages/contracts/test/multisig-constants.ts b/packages/contracts/test/multisig-constants.ts index 77cf5948..791fbc45 100644 --- a/packages/contracts/test/multisig-constants.ts +++ b/packages/contracts/test/multisig-constants.ts @@ -6,7 +6,7 @@ export const MULTISIG_EVENTS = { }; export const MULTISIG_INTERFACE = new ethers.utils.Interface([ - 'function initialize(address,address[],tuple(bool,uint16))', + 'function initialize(address,address[],tuple(bool,uint16),tuple(address,uint8))', 'function updateMultisigSettings(tuple(bool,uint16))', 'function createProposal(bytes,tuple(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)', 'function getProposal(uint256)', @@ -29,3 +29,13 @@ export const SET_TARGET_CONFIG_PERMISSION_ID = ethers.utils.id( ); export const ANY_ADDR = '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF'; + +export enum Operation { + call, + delegatecall, +} + +export type TargetConfig = { + target: string; + operation: number; +}; From 516a53214b16266ebe67c66cc90df0f7dfda39b4 Mon Sep 17 00:00:00 2001 From: Claudia Date: Wed, 21 Aug 2024 14:01:44 +0200 Subject: [PATCH 03/59] feat: add unit test for initialize from function --- .../test/10_unit-testing/11_plugin.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index f79c8c22..7eff7f2f 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -278,6 +278,59 @@ describe('Multisig', function () { }); }); + describe('reinitialize', async () => { + it('reverts if trying to re-reinitialize', async () => { + const {uninitializedPlugin, deployer} = await loadFixture(fixture); + + const dummyTarget = { + target: deployer.address, + operation: Operation.delegatecall, + }; + + // reinitialize the plugin. + uninitializedPlugin.initializeFrom(dummyTarget); + + // Try to reinitialize the plugin. + await expect( + uninitializedPlugin.initializeFrom(dummyTarget) + ).to.be.revertedWith('Initializable: contract is already initialized'); + }); + + it('sets the `_targetConfig`', async () => { + const {uninitializedPlugin, deployer} = await loadFixture(fixture); + + // reinitialize the plugin. + await uninitializedPlugin.initializeFrom({ + target: deployer.address, + operation: Operation.delegatecall, + }); + + expect((await uninitializedPlugin.getTargetConfig()).target).to.be.eq( + deployer.address + ); + expect((await uninitializedPlugin.getTargetConfig()).operation).to.be.eq( + Operation.delegatecall + ); + }); + + it('sets the `_targetConfig` when reinitializing an initialized plugin', async () => { + const {initializedPlugin, deployer} = await loadFixture(fixture); + + // reinitialize the plugin. + await initializedPlugin.initializeFrom({ + target: deployer.address, + operation: Operation.delegatecall, + }); + + expect((await initializedPlugin.getTargetConfig()).target).to.be.eq( + deployer.address + ); + expect((await initializedPlugin.getTargetConfig()).operation).to.be.eq( + Operation.delegatecall + ); + }); + }); + describe('ERC-165', async () => { it('does not support the empty interface', async () => { const {initializedPlugin: plugin} = await loadFixture(fixture); From c710d76a9a5b5e2a481f3a972bdbc7b75ecfd731 Mon Sep 17 00:00:00 2001 From: Claudia Date: Wed, 21 Aug 2024 14:02:24 +0200 Subject: [PATCH 04/59] ci: remove comment --- packages/contracts/test/10_unit-testing/11_plugin.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index 7eff7f2f..79cb4f37 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -166,7 +166,6 @@ async function loadFixtureAndGrantCreatePermission(): Promise { return data; } -// TODO: maybe add test for `initializeFrom` and whether it successfuly works - i.e whether reinitializer(x) is correct and so on. describe('Multisig', function () { describe('initialize', async () => { it('reverts if trying to re-initialize', async () => { From b2996b6c844577319d1a7a10d5a435b3832b3501 Mon Sep 17 00:00:00 2001 From: Claudia Date: Wed, 21 Aug 2024 14:18:22 +0200 Subject: [PATCH 05/59] feat: add check to ensure the plugin is being reinitialized when upgrading --- .../22_setup-processing.ts | 6 ++++-- .../test/20_integration-testing/test-helpers.ts | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/contracts/test/20_integration-testing/22_setup-processing.ts b/packages/contracts/test/20_integration-testing/22_setup-processing.ts index e3a7907c..557cc06b 100644 --- a/packages/contracts/test/20_integration-testing/22_setup-processing.ts +++ b/packages/contracts/test/20_integration-testing/22_setup-processing.ts @@ -214,7 +214,8 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio pluginSetupRefLatestBuild, 1, [defaultInitData.members, Object.values(defaultInitData.settings)], - [defaultInitData.targetConfig] + [defaultInitData.targetConfig], + 2 ); }); @@ -236,7 +237,8 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio pluginSetupRefLatestBuild, 2, [defaultInitData.members, Object.values(defaultInitData.settings)], - [defaultInitData.targetConfig] + [defaultInitData.targetConfig], + 2 ); }); }); diff --git a/packages/contracts/test/20_integration-testing/test-helpers.ts b/packages/contracts/test/20_integration-testing/test-helpers.ts index ff2818c0..fc2c5f41 100644 --- a/packages/contracts/test/20_integration-testing/test-helpers.ts +++ b/packages/contracts/test/20_integration-testing/test-helpers.ts @@ -26,7 +26,8 @@ import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; import {expect} from 'chai'; import {ContractTransaction} from 'ethers'; import {ethers} from 'hardhat'; -import {create} from 'ipfs-http-client'; + +const OZ_INITIALIZED_SLOT_POSITION = 0; export async function installPLugin( signer: SignerWithAddress, @@ -243,7 +244,8 @@ export async function updateFromBuildTest( pluginSetupRefLatestBuild: PluginSetupProcessorStructs.PluginSetupRefStruct, build: number, installationInputs: any[], - updateInputs: any[] + updateInputs: any[], + reinitializedVersion: number ) { // Grant deployer all required permissions await dao @@ -349,6 +351,16 @@ export async function updateFromBuildTest( deployer ).implementation(); expect(await plugin.implementation()).to.equal(implementationLatestBuild); + + // check the plugin was reinitialized, OZs `_initialized` at storage slot [0] is correct + expect( + ethers.BigNumber.from( + await ethers.provider.getStorageAt( + plugin.address, + OZ_INITIALIZED_SLOT_POSITION + ) + ).toNumber() + ).to.equal(reinitializedVersion); } export async function createDaoProxy( From 8bfe5d02350e64a27884fa4e96290dbc545f7a94 Mon Sep 17 00:00:00 2001 From: Claudia Date: Wed, 21 Aug 2024 14:18:29 +0200 Subject: [PATCH 06/59] fix test --- packages/contracts/test/10_unit-testing/11_plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index 79cb4f37..f6096760 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -287,7 +287,7 @@ describe('Multisig', function () { }; // reinitialize the plugin. - uninitializedPlugin.initializeFrom(dummyTarget); + await uninitializedPlugin.initializeFrom(dummyTarget); // Try to reinitialize the plugin. await expect( From b80d6262f24069fba542ae79bb16afc6e7d7b686 Mon Sep 17 00:00:00 2001 From: Claudia Date: Wed, 21 Aug 2024 14:19:34 +0200 Subject: [PATCH 07/59] ci: remove comment --- .../contracts/test/20_integration-testing/22_setup-processing.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/contracts/test/20_integration-testing/22_setup-processing.ts b/packages/contracts/test/20_integration-testing/22_setup-processing.ts index 557cc06b..d9197378 100644 --- a/packages/contracts/test/20_integration-testing/22_setup-processing.ts +++ b/packages/contracts/test/20_integration-testing/22_setup-processing.ts @@ -119,7 +119,6 @@ async function fixture(): Promise { }; } -// TODO: when upgrade happens, test that `initializeFrom` was called successfully. describe(`PluginSetup processing on network '${productionNetworkName}'`, function () { it('installs & uninstalls the current build', async () => { const { From 1b24da14c77a77834fca8a4bf05303937ab1ddef Mon Sep 17 00:00:00 2001 From: Claudia Date: Wed, 21 Aug 2024 15:00:12 +0200 Subject: [PATCH 08/59] feat: add reinitialization when upgrading plugin and check it on the regresion tests --- .../31_upgradeability.ts | 84 +++++++++++++++++++ .../test/test-utils/uups-upgradeable.ts | 13 ++- 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/packages/contracts/test/30_regression-testing/31_upgradeability.ts b/packages/contracts/test/30_regression-testing/31_upgradeability.ts index be28aae3..87f4d8bb 100644 --- a/packages/contracts/test/30_regression-testing/31_upgradeability.ts +++ b/packages/contracts/test/30_regression-testing/31_upgradeability.ts @@ -99,6 +99,90 @@ describe('Upgrades', () => { expect(fromProtocolVersion).to.deep.equal([1, 0, 0]); expect(toProtocolVersion).to.deep.equal([1, 4, 0]); }); + + it('upgrades from v1.1 with initializeFrom', async () => { + const {deployer, alice, dao, defaultInitData} = await loadFixture(fixture); + const currentContractFactory = new Multisig__factory(deployer); + const legacyContractFactory = new Multisig_V1_1__factory(deployer); + + const {proxy, fromImplementation, toImplementation} = + await deployAndUpgradeFromToCheck( + deployer, + alice, + [dao.address, defaultInitData.members, defaultInitData.settings], + 'initialize', + legacyContractFactory, + currentContractFactory, + PLUGIN_UUPS_UPGRADEABLE_PERMISSIONS.UPGRADE_PLUGIN_PERMISSION_ID, + dao, + 'initializeFrom', + [{target: deployer.address, operation: Operation.delegatecall}] + ); + expect(toImplementation).to.not.equal(fromImplementation); // The build did change + + const fromProtocolVersion = await getProtocolVersion( + legacyContractFactory.attach(fromImplementation) + ); + const toProtocolVersion = await getProtocolVersion( + currentContractFactory.attach(toImplementation) + ); + + expect(fromProtocolVersion).to.not.deep.equal(toProtocolVersion); + expect(fromProtocolVersion).to.deep.equal([1, 0, 0]); + expect(toProtocolVersion).to.deep.equal([1, 4, 0]); + + // expects the plugin was reinitialized + const newMultisig = Multisig__factory.connect(proxy.address, deployer); + + expect((await newMultisig.getTargetConfig()).target).to.deep.equal( + deployer.address + ); + expect((await newMultisig.getTargetConfig()).operation).to.deep.equal( + Operation.delegatecall + ); + }); + + it('from v1.2 with initializeFrom', async () => { + const {deployer, alice, dao, defaultInitData} = await loadFixture(fixture); + const currentContractFactory = new Multisig__factory(deployer); + const legacyContractFactory = new Multisig_V1_2__factory(deployer); + + const {proxy, fromImplementation, toImplementation} = + await deployAndUpgradeFromToCheck( + deployer, + alice, + [dao.address, defaultInitData.members, defaultInitData.settings], + 'initialize', + legacyContractFactory, + currentContractFactory, + PLUGIN_UUPS_UPGRADEABLE_PERMISSIONS.UPGRADE_PLUGIN_PERMISSION_ID, + dao, + 'initializeFrom', + [{target: deployer.address, operation: Operation.delegatecall}] + ); + expect(toImplementation).to.not.equal(fromImplementation); + + const fromProtocolVersion = await getProtocolVersion( + legacyContractFactory.attach(fromImplementation) + ); + const toProtocolVersion = await getProtocolVersion( + currentContractFactory.attach(toImplementation) + ); + + expect(fromProtocolVersion).to.not.deep.equal(toProtocolVersion); + expect(fromProtocolVersion).to.deep.equal([1, 0, 0]); + expect(toProtocolVersion).to.deep.equal([1, 4, 0]); + + // expects the plugin was reinitialized + const newMultisig = Multisig__factory.connect(proxy.address, deployer); + + expect((await newMultisig.getTargetConfig()).target).to.deep.equal( + deployer.address + ); + expect((await newMultisig.getTargetConfig()).operation).to.deep.equal( + Operation.delegatecall + ); + }); }); type FixtureResult = { diff --git a/packages/contracts/test/test-utils/uups-upgradeable.ts b/packages/contracts/test/test-utils/uups-upgradeable.ts index 8574d4b4..458d5978 100644 --- a/packages/contracts/test/test-utils/uups-upgradeable.ts +++ b/packages/contracts/test/test-utils/uups-upgradeable.ts @@ -102,7 +102,9 @@ export async function deployAndUpgradeFromToCheck( from: ContractFactory, to: ContractFactory, upgradePermissionId: string, - managingDao?: DAO | PluginRepo + managingDao?: DAO | PluginRepo, + reinitializerName?: string, + reinitArgs?: any ): Promise<{ proxy: Contract; fromImplementation: string; @@ -155,10 +157,19 @@ export async function deployAndUpgradeFromToCheck( await managingDao.connect(deployer).grant(...grantArgs); } + let call; + if (reinitializerName && reinitArgs) { + call = { + fn: reinitializerName, + args: reinitArgs, + }; + } + // Upgrade the proxy to a new implementation from a different factory await upgrades.upgradeProxy(proxy.address, to.connect(upgrader), { unsafeAllow: ['constructor', 'delegatecall'], constructorArgs: [], + call: call, }); const toImplementation = await ethers.provider From 2b679eda3fae8d20bdc6c1da37578dceb3140fce Mon Sep 17 00:00:00 2001 From: Claudia Date: Wed, 21 Aug 2024 15:00:38 +0200 Subject: [PATCH 09/59] ci: remove comment --- .../contracts/test/30_regression-testing/31_upgradeability.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/contracts/test/30_regression-testing/31_upgradeability.ts b/packages/contracts/test/30_regression-testing/31_upgradeability.ts index 87f4d8bb..a92abe65 100644 --- a/packages/contracts/test/30_regression-testing/31_upgradeability.ts +++ b/packages/contracts/test/30_regression-testing/31_upgradeability.ts @@ -18,7 +18,6 @@ import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; import {expect} from 'chai'; import {ethers} from 'hardhat'; -// TODO: pass `initializeFrom` initData on upgrade to test it as well. describe('Upgrades', () => { it('upgrades to a new implementation', async () => { const {deployer, alice, dao, defaultInitData} = await loadFixture(fixture); From a50a5c8c5ec7c05c8f5acbdf7ca2a85a2c108f4d Mon Sep 17 00:00:00 2001 From: Claudia Date: Wed, 21 Aug 2024 15:03:33 +0200 Subject: [PATCH 10/59] ci: add comment --- .../test/20_integration-testing/22_setup-processing.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/test/20_integration-testing/22_setup-processing.ts b/packages/contracts/test/20_integration-testing/22_setup-processing.ts index d9197378..41964d40 100644 --- a/packages/contracts/test/20_integration-testing/22_setup-processing.ts +++ b/packages/contracts/test/20_integration-testing/22_setup-processing.ts @@ -214,7 +214,7 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio 1, [defaultInitData.members, Object.values(defaultInitData.settings)], [defaultInitData.targetConfig], - 2 + 2 // current reinitializer version ); }); @@ -237,7 +237,7 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio 2, [defaultInitData.members, Object.values(defaultInitData.settings)], [defaultInitData.targetConfig], - 2 + 2 // current reinitializer version ); }); }); From d4e80c09575a7eac5c5f544f93984159ce5a2410 Mon Sep 17 00:00:00 2001 From: Claudia Date: Wed, 21 Aug 2024 15:05:46 +0200 Subject: [PATCH 11/59] ci: update comment --- packages/contracts/src/Multisig.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index 6d894725..af362951 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -70,8 +70,9 @@ contract Multisig is uint16 minApprovals; } - // todo: since `initialize` was changed, this means it no longer supports the old interfaceId. this could be a breaking change. - // maybe UI was already using `supportsinterface`. + // todo: since `initialize` was changed, this means it no longer supports the old interfaceId. + // todo this could be a breaking change. + // todo maybe UI was already using `supportsinterface`. /// @notice The [ERC-165](https://eips.ethereum.org/EIPS/eip-165) interface ID of the contract. bytes4 internal constant MULTISIG_INTERFACE_ID = this.initialize.selector ^ From 3300d840654f451159c091fc0136ae4eceeda33b Mon Sep 17 00:00:00 2001 From: Claudia Date: Wed, 21 Aug 2024 15:08:21 +0200 Subject: [PATCH 12/59] fix lint --- packages/contracts/src/ListedCheckCondition.sol | 8 ++++---- packages/contracts/test/10_unit-testing/11_plugin.ts | 7 ++----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/contracts/src/ListedCheckCondition.sol b/packages/contracts/src/ListedCheckCondition.sol index ef926d86..b2616676 100644 --- a/packages/contracts/src/ListedCheckCondition.sol +++ b/packages/contracts/src/ListedCheckCondition.sol @@ -7,10 +7,10 @@ import {Multisig} from "./Multisig.sol"; import {PermissionCondition} from "@aragon/osx-commons-contracts/src/permission/condition/PermissionCondition.sol"; contract ListedCheckCondition is PermissionCondition { - Multisig private immutable multisig; + Multisig private immutable MULTISIG; constructor(address _multisig) { - multisig = Multisig(_multisig); + MULTISIG = Multisig(_multisig); } function isGranted( @@ -21,9 +21,9 @@ contract ListedCheckCondition is PermissionCondition { ) public view override returns (bool) { (_where, _data, _permissionId); - (bool onlyListed, ) = multisig.multisigSettings(); + (bool onlyListed, ) = MULTISIG.multisigSettings(); - if (onlyListed && !multisig.isListed(_who)) { + if (onlyListed && !MULTISIG.isListed(_who)) { return false; } diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index f6096760..194e4200 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -12,7 +12,6 @@ import { } from '../../typechain'; import {ExecutedEvent} from '../../typechain/@aragon/osx-commons-contracts/src/dao/IDAO'; import {ProxyCreatedEvent} from '../../typechain/@aragon/osx-commons-contracts/src/utils/deployment/ProxyFactory'; -import {PluginUUPSUpgradeable} from '../../typechain/@aragon/osx-v1.0.0/core/plugin'; import { ApprovedEvent, ProposalCreatedEvent, @@ -30,7 +29,6 @@ import { import {Multisig__factory, Multisig} from '../test-utils/typechain-versions'; import { getInterfaceId, - proposalIdToBytes32, findEvent, findEventTopicLog, TIME, @@ -143,7 +141,7 @@ async function fixture(): Promise { } async function loadFixtureAndGrantCreatePermission(): Promise { - let data = await loadFixture(fixture); + const data = await loadFixture(fixture); const {deployer, dao, initializedPlugin, uninitializedPlugin} = data; const condition = await new ListedCheckCondition__factory(deployer).deploy( @@ -745,7 +743,7 @@ describe('Multisig', function () { data = await loadFixtureAndGrantCreatePermission(); }); it('reverts if permission is not given', async () => { - const {deployer, dao, dummyMetadata, initializedPlugin: plugin} = data; + const {deployer, dao, initializedPlugin: plugin} = data; await dao.revoke(plugin.address, ANY_ADDR, CREATE_PROPOSAL_PERMISSION_ID); await expect( @@ -768,7 +766,6 @@ describe('Multisig', function () { initializedPlugin: plugin, dummyMetadata, dummyActions, - dao, defaultInitData, } = data; From 4ef810fb77a4177a4892dfc97516f93a388f38d8 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Tue, 27 Aug 2024 11:02:06 +0400 Subject: [PATCH 13/59] proposal hash return value --- packages/contracts/src/Multisig.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index 6d894725..a92acf4d 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -284,7 +284,7 @@ contract Multisig is revert DateOutOfBounds({limit: _startDate, actual: _endDate}); } - proposalId = hashProposal(_actions, _metadata); + proposalId = getProposalHash(_actions, _metadata); // Create the proposal Proposal storage proposal_ = proposals[proposalId]; @@ -433,11 +433,11 @@ contract Multisig is return isListed(_account); } - function hashProposal( + function getProposalHash( IDAO.Action[] calldata _actions, bytes memory _metadata - ) public pure returns (uint256 proposalId) { - proposalId = uint256(keccak256(abi.encode(_actions, _metadata))); + ) public pure returns (uint256) { + return uint256(keccak256(abi.encode(_actions, _metadata))); } /// @notice Internal function to execute a vote. It assumes the queried proposal exists. From 43ef91e42dfb38a74bbf5be0dd436d2f2b613c84 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Tue, 27 Aug 2024 15:35:25 +0400 Subject: [PATCH 14/59] add create proposal id --- packages/contracts/src/Multisig.sol | 18 ++++- .../test/10_unit-testing/11_plugin.ts | 70 ++++++++++--------- 2 files changed, 53 insertions(+), 35 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index a92acf4d..c1238bd0 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -284,7 +284,7 @@ contract Multisig is revert DateOutOfBounds({limit: _startDate, actual: _endDate}); } - proposalId = getProposalHash(_actions, _metadata); + proposalId = createProposalId(_actions, _metadata); // Create the proposal Proposal storage proposal_ = proposals[proposalId]; @@ -433,10 +433,22 @@ contract Multisig is return isListed(_account); } - function getProposalHash( + /// @notice Hashing function used to (re)build the proposal id from the proposal details.. + /// @dev The proposal id is produced by hashing the ABI encoded `targets` array, the `values` array, the `calldatas` array + /// and the descriptionHash (bytes32 which itself is the keccak256 hash of the description string). This proposal id + /// can be produced from the proposal data which is part of the {ProposalCreated} event. It can even be computed in + /// advance, before the proposal is submitted. + /// The chainId and the governor address are not part of the proposal id computation. Consequently, the + /// same proposal (with same operation and same description) will have the same id if submitted on multiple governors + /// across multiple networks. This also means that in order to execute the same operation twice (on the same + /// governor) the proposer will have to change the description in order to avoid proposal id conflicts. + /// @param _actions The actions that will be executed after the proposal passes. + /// @param _metadata The metadata of the proposal. + /// @return proposalId The ID of the proposal. + function createProposalId( IDAO.Action[] calldata _actions, bytes memory _metadata - ) public pure returns (uint256) { + ) public pure override returns (uint256) { return uint256(keccak256(abi.encode(_actions, _metadata))); } diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index f79c8c22..7419b94f 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -167,7 +167,7 @@ async function loadFixtureAndGrantCreatePermission(): Promise { } // TODO: maybe add test for `initializeFrom` and whether it successfuly works - i.e whether reinitializer(x) is correct and so on. -describe('Multisig', function () { +describe.only('Multisig', function () { describe('initialize', async () => { it('reverts if trying to re-initialize', async () => { const {dao, initializedPlugin, defaultInitData} = await loadFixture( @@ -720,7 +720,7 @@ describe('Multisig', function () { defaultInitData, } = data; - const proposalId = plugin.hashProposal(dummyActions, dummyMetadata); + const proposalId = plugin.createProposalId(dummyActions, dummyMetadata); // Create a proposal as Alice. const endDate = (await time.latest()) + TIME.HOUR; @@ -745,7 +745,10 @@ describe('Multisig', function () { // Create a proposal as Alice and check the event arguments. const startDate = (await time.latest()) + TIME.MINUTE; const endDate = startDate + TIME.HOUR; - const expectedProposalId = await plugin.hashProposal([], dummyMetadata); + const expectedProposalId = await plugin.createProposalId( + [], + dummyMetadata + ); await expect( plugin .connect(alice) @@ -904,7 +907,10 @@ describe('Multisig', function () { const startDate = (await time.latest()) + TIME.MINUTE; const endDate = startDate + TIME.HOUR; - const expectedProposalId = await plugin.hashProposal([], dummyMetadata); + const expectedProposalId = await plugin.createProposalId( + [], + dummyMetadata + ); await expect( plugin @@ -1009,7 +1015,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Check the listed members before the block is mined. expect(await plugin.isListed(carol.address)).to.equal(true); @@ -1063,7 +1069,7 @@ describe('Multisig', function () { const endDate = startDate + TIME.HOUR; const allowFailureMap = 0; await time.setNextBlockTimestamp(startDate); - const id = await plugin.hashProposal([], dummyMetadata); + const id = await plugin.createProposalId([], dummyMetadata); const approveProposal = false; @@ -1125,7 +1131,7 @@ describe('Multisig', function () { await time.setNextBlockTimestamp(startDate); - const id = await plugin.hashProposal([], dummyMetadata); + const id = await plugin.createProposalId([], dummyMetadata); await expect( plugin .connect(alice) @@ -1260,7 +1266,7 @@ describe('Multisig', function () { 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); await dao.grant( dao.address, @@ -1322,7 +1328,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); await plugin.connect(alice).approve(id, false); expect(await plugin.canApprove(id, alice.address)).to.be.false; @@ -1344,7 +1350,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); expect(await plugin.canApprove(id, alice.address)).to.be.true; }); @@ -1366,7 +1372,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, startDate, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); expect(await plugin.canApprove(id, alice.address)).to.be.false; @@ -1391,7 +1397,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); expect(await plugin.canApprove(id, alice.address)).to.be.true; @@ -1418,7 +1424,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); expect(await plugin.hasApproved(id, alice.address)).to.be.false; }); @@ -1438,7 +1444,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); await plugin.connect(alice).approve(id, false); expect(await plugin.hasApproved(id, alice.address)).to.be.true; @@ -1462,7 +1468,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); await plugin.connect(alice).approve(id, true); @@ -1489,7 +1495,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. await dao.grant( @@ -1522,7 +1528,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Check that there are 0 approvals yet. expect((await plugin.getProposal(id)).approvals).to.equal(0); @@ -1555,7 +1561,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, startDate, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Try to approve the proposal as Alice although being before the start date. await expect(plugin.connect(alice).approve(id, false)) @@ -1587,7 +1593,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Advance time after the end date. await time.increaseTo(endDate + 1); @@ -1614,7 +1620,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Check that `minApprovals` isn't met yet. const proposal = await plugin.getProposal(id); @@ -1641,7 +1647,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. await dao.grant( @@ -1678,7 +1684,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); await plugin.connect(alice).approve(id, false); await plugin.connect(bob).approve(id, false); @@ -1705,7 +1711,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, startDate, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); expect(await plugin.canExecute(id)).to.be.false; @@ -1734,7 +1740,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); await plugin.connect(alice).approve(id, false); await plugin.connect(bob).approve(id, false); @@ -1765,7 +1771,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. await dao.grant( @@ -1798,7 +1804,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. await dao.grant( @@ -1846,7 +1852,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); await dao.grant( dao.address, @@ -1889,7 +1895,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. await dao.grant( @@ -1983,7 +1989,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` on the DAO. await dao.grant( @@ -2021,7 +2027,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. await dao.grant( @@ -2059,7 +2065,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, startDate, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. await dao.grant( @@ -2101,7 +2107,7 @@ describe('Multisig', function () { [ 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); - const id = await plugin.hashProposal(dummyActions, dummyMetadata); + const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Approve the proposal but do not execute yet. await plugin.connect(alice).approve(id, false); From a8c73763bd1f12b6b69cd00cb0d42304da592141 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Tue, 27 Aug 2024 15:45:01 +0400 Subject: [PATCH 15/59] remove .only --- packages/contracts/test/10_unit-testing/11_plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index 7419b94f..76e6b079 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -167,7 +167,7 @@ async function loadFixtureAndGrantCreatePermission(): Promise { } // TODO: maybe add test for `initializeFrom` and whether it successfuly works - i.e whether reinitializer(x) is correct and so on. -describe.only('Multisig', function () { +describe('Multisig', function () { describe('initialize', async () => { it('reverts if trying to re-initialize', async () => { const {dao, initializedPlugin, defaultInitData} = await loadFixture( From 6a2c080836fa3340d8dc0b37c78f9225a1c2ee7b Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Tue, 10 Sep 2024 21:31:09 +0400 Subject: [PATCH 16/59] initializeFrom and erc165 changes for upgrades --- packages/contracts/src/Multisig.sol | 18 ++++---- packages/contracts/src/MultisigSetup.sol | 5 +-- .../test/10_unit-testing/12_plugin-setup.ts | 43 ++++++++++--------- packages/contracts/test/multisig-constants.ts | 1 - 4 files changed, 34 insertions(+), 33 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index c1238bd0..7e2b767e 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -70,12 +70,9 @@ contract Multisig is uint16 minApprovals; } - // todo: since `initialize` was changed, this means it no longer supports the old interfaceId. this could be a breaking change. - // maybe UI was already using `supportsinterface`. /// @notice The [ERC-165](https://eips.ethereum.org/EIPS/eip-165) interface ID of the contract. bytes4 internal constant MULTISIG_INTERFACE_ID = - this.initialize.selector ^ - this.updateMultisigSettings.selector ^ + this.updateMultisigSettings.selector ^ bytes4( keccak256( "createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)" @@ -158,7 +155,7 @@ contract Multisig is address[] calldata _members, MultisigSettings calldata _multisigSettings, TargetConfig calldata _targetConfig - ) external initializer { + ) external reinitializer(2) { __PluginUUPSUpgradeable_init(_dao); if (_members.length > type(uint16).max) { @@ -173,9 +170,14 @@ contract Multisig is _setTargetConfig(_targetConfig); } - // TODO: What should the number in reinitializer(x) must be ? - function initializeFrom(TargetConfig calldata _targetConfig) external reinitializer(2) { - _setTargetConfig(_targetConfig); + /// @notice Reinitializes the TokenVoting after an upgrade from a previous protocol version. + /// @param _fromBuild The build version number of the previous implementation contract this upgrade is transitioning from. + /// @param _initData The initialization data to be passed to via `upgradeToAndCall` (see [ERC-1967](https://docs.openzeppelin.com/contracts/4.x/api/proxy#ERC1967Upgrade)). + function initializeFrom(uint16 _fromBuild, bytes calldata _initData) external reinitializer(2) { + if (_fromBuild < 3) { + TargetConfig memory targetConfig = abi.decode(_initData, (TargetConfig)); + _setTargetConfig(targetConfig); + } } /// @notice Checks if this or the parent contract supports an interface by its ID. diff --git a/packages/contracts/src/MultisigSetup.sol b/packages/contracts/src/MultisigSetup.sol index b0e48eaa..aacd8555 100644 --- a/packages/contracts/src/MultisigSetup.sol +++ b/packages/contracts/src/MultisigSetup.sol @@ -160,10 +160,7 @@ contract MultisigSetup is PluginUpgradeableSetup { preparedSetupData.helpers = new address[](1); preparedSetupData.helpers[0] = listedCheckCondition; - initData = abi.encodeCall( - Multisig.initializeFrom, - (abi.decode(_payload.data, (PluginUUPSUpgradeable.TargetConfig))) - ); + initData = abi.encodeCall(Multisig.initializeFrom, (_fromBuild, _payload.data)); } } diff --git a/packages/contracts/test/10_unit-testing/12_plugin-setup.ts b/packages/contracts/test/10_unit-testing/12_plugin-setup.ts index 818abb46..3f3e79a6 100644 --- a/packages/contracts/test/10_unit-testing/12_plugin-setup.ts +++ b/packages/contracts/test/10_unit-testing/12_plugin-setup.ts @@ -35,9 +35,11 @@ type FixtureResult = { carol: SignerWithAddress; pluginSetup: MultisigSetup; defaultTargetConfig: TargetConfig; + updateTargetConfig: TargetConfig; defaultMembers: string[]; defaultMultisigSettings: Multisig.MultisigSettingsStruct; prepareInstallationInputs: string; + prepareUpdateBuild3Inputs: string; prepareUninstallationInputs: string; dao: DAO; }; @@ -81,6 +83,19 @@ async function fixture(): Promise { [] ); + const updateTargetConfig = { + target: pluginSetup.address, + operation: op.delegatecall, + }; + + // Provide update inputs + const prepareUpdateBuild3Inputs = ethers.utils.defaultAbiCoder.encode( + getNamedTypesFromMetadata( + METADATA.build.pluginSetup.prepareUpdate[3].inputs + ), + [updateTargetConfig] + ); + return { deployer, alice, @@ -89,8 +104,10 @@ async function fixture(): Promise { pluginSetup, defaultMembers, defaultTargetConfig, + updateTargetConfig, defaultMultisigSettings, prepareInstallationInputs, + prepareUpdateBuild3Inputs, prepareUninstallationInputs, dao, }; @@ -372,18 +389,11 @@ describe('MultisigSetup', function () { describe('prepareUpdate', async () => { it('returns the permissions expected for the update from build 1', async () => { - const {pluginSetup, dao, defaultTargetConfig} = await loadFixture( + const {pluginSetup, dao, prepareUpdateBuild3Inputs} = await loadFixture( fixture ); const plugin = ethers.Wallet.createRandom().address; - const prepareUpdateInputs = ethers.utils.defaultAbiCoder.encode( - getNamedTypesFromMetadata( - METADATA.build.pluginSetup.prepareUpdate[3].inputs - ), - [defaultTargetConfig] - ); - // Make a static call to check that the plugin update data being returned is correct. const { initData: initData, @@ -393,7 +403,7 @@ describe('MultisigSetup', function () { ethers.Wallet.createRandom().address, ethers.Wallet.createRandom().address, ], - data: prepareUpdateInputs, + data: prepareUpdateBuild3Inputs, plugin, }); @@ -401,7 +411,7 @@ describe('MultisigSetup', function () { expect(initData).to.be.eq( Multisig__factory.createInterface().encodeFunctionData( 'initializeFrom', - [defaultTargetConfig] + [1, prepareUpdateBuild3Inputs] ) ); expect(permissions.length).to.be.equal(3); @@ -433,18 +443,11 @@ describe('MultisigSetup', function () { }); it('returns the permissions expected for the update from build 2', async () => { - const {pluginSetup, dao, defaultTargetConfig} = await loadFixture( + const {pluginSetup, dao, prepareUpdateBuild3Inputs} = await loadFixture( fixture ); const plugin = ethers.Wallet.createRandom().address; - const prepareUpdateInputs = ethers.utils.defaultAbiCoder.encode( - getNamedTypesFromMetadata( - METADATA.build.pluginSetup.prepareUpdate[3].inputs - ), - [defaultTargetConfig] - ); - // Make a static call to check that the plugin update data being returned is correct. const { initData: initData, @@ -454,7 +457,7 @@ describe('MultisigSetup', function () { ethers.Wallet.createRandom().address, ethers.Wallet.createRandom().address, ], - data: prepareUpdateInputs, + data: prepareUpdateBuild3Inputs, plugin, }); @@ -462,7 +465,7 @@ describe('MultisigSetup', function () { expect(initData).to.be.eq( Multisig__factory.createInterface().encodeFunctionData( 'initializeFrom', - [defaultTargetConfig] + [2, prepareUpdateBuild3Inputs] ) ); expect(permissions.length).to.be.equal(3); diff --git a/packages/contracts/test/multisig-constants.ts b/packages/contracts/test/multisig-constants.ts index 791fbc45..f736a108 100644 --- a/packages/contracts/test/multisig-constants.ts +++ b/packages/contracts/test/multisig-constants.ts @@ -6,7 +6,6 @@ export const MULTISIG_EVENTS = { }; export const MULTISIG_INTERFACE = new ethers.utils.Interface([ - 'function initialize(address,address[],tuple(bool,uint16),tuple(address,uint8))', 'function updateMultisigSettings(tuple(bool,uint16))', 'function createProposal(bytes,tuple(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)', 'function getProposal(uint256)', From 31ebac103be0d8a52e874c5c87d46c0ee8104589 Mon Sep 17 00:00:00 2001 From: Claudia Date: Fri, 13 Sep 2024 15:30:47 +0200 Subject: [PATCH 17/59] ci: define latest version as constant --- .../test/20_integration-testing/22_setup-processing.ts | 10 +++++++--- packages/contracts/test/multisig-constants.ts | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/contracts/test/20_integration-testing/22_setup-processing.ts b/packages/contracts/test/20_integration-testing/22_setup-processing.ts index 41964d40..49335c5e 100644 --- a/packages/contracts/test/20_integration-testing/22_setup-processing.ts +++ b/packages/contracts/test/20_integration-testing/22_setup-processing.ts @@ -1,7 +1,11 @@ import {METADATA, VERSION} from '../../plugin-settings'; import {MultisigSetup, Multisig__factory} from '../../typechain'; import {getProductionNetworkName, findPluginRepo} from '../../utils/helpers'; -import {Operation, TargetConfig} from '../multisig-constants'; +import { + Operation, + TargetConfig, + latestInitializerVersion, +} from '../multisig-constants'; import {Multisig} from '../test-utils/typechain-versions'; import { createDaoProxy, @@ -214,7 +218,7 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio 1, [defaultInitData.members, Object.values(defaultInitData.settings)], [defaultInitData.targetConfig], - 2 // current reinitializer version + latestInitializerVersion ); }); @@ -237,7 +241,7 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio 2, [defaultInitData.members, Object.values(defaultInitData.settings)], [defaultInitData.targetConfig], - 2 // current reinitializer version + latestInitializerVersion ); }); }); diff --git a/packages/contracts/test/multisig-constants.ts b/packages/contracts/test/multisig-constants.ts index f736a108..73ed4b35 100644 --- a/packages/contracts/test/multisig-constants.ts +++ b/packages/contracts/test/multisig-constants.ts @@ -38,3 +38,5 @@ export type TargetConfig = { target: string; operation: number; }; + +export const latestInitializerVersion = 2; From f7ec5fc44e67da57a28c80d4509d826f0f7e087b Mon Sep 17 00:00:00 2001 From: Claudia Date: Fri, 13 Sep 2024 15:31:14 +0200 Subject: [PATCH 18/59] feat: fix test for initialize from function --- .../test/10_unit-testing/11_plugin.ts | 71 +++++++++++-------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index 94159ea0..8a2b5593 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -25,6 +25,7 @@ import { Operation, TargetConfig, UPDATE_MULTISIG_SETTINGS_PERMISSION_ID, + latestInitializerVersion, } from '../multisig-constants'; import {Multisig__factory, Multisig} from '../test-utils/typechain-versions'; import { @@ -276,53 +277,67 @@ describe('Multisig', function () { }); describe('reinitialize', async () => { - it('reverts if trying to re-reinitialize', async () => { + it('reverts if trying to re-reinitializeFrom', async () => { const {uninitializedPlugin, deployer} = await loadFixture(fixture); - const dummyTarget = { - target: deployer.address, - operation: Operation.delegatecall, - }; + const encodedDummyTarget = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint8'], + [deployer.address, Operation.delegatecall] + ); // reinitialize the plugin. - await uninitializedPlugin.initializeFrom(dummyTarget); + await uninitializedPlugin.initializeFrom( + latestInitializerVersion, + encodedDummyTarget + ); // Try to reinitialize the plugin. await expect( - uninitializedPlugin.initializeFrom(dummyTarget) + uninitializedPlugin.initializeFrom( + latestInitializerVersion, + encodedDummyTarget + ) ).to.be.revertedWith('Initializable: contract is already initialized'); }); - it('sets the `_targetConfig`', async () => { - const {uninitializedPlugin, deployer} = await loadFixture(fixture); - - // reinitialize the plugin. - await uninitializedPlugin.initializeFrom({ - target: deployer.address, - operation: Operation.delegatecall, - }); + it('reverts if trying to initializeFrom an initialized plugin', async () => { + const {initializedPlugin, deployer} = await loadFixture(fixture); - expect((await uninitializedPlugin.getTargetConfig()).target).to.be.eq( - deployer.address - ); - expect((await uninitializedPlugin.getTargetConfig()).operation).to.be.eq( - Operation.delegatecall + const encodedDummyTarget = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint8'], + [deployer.address, Operation.delegatecall] ); + + // Try to reinitialize the plugin. + await expect( + initializedPlugin.initializeFrom( + latestInitializerVersion, + encodedDummyTarget + ) + ).to.be.revertedWith('Initializable: contract is already initialized'); }); - it('sets the `_targetConfig` when reinitializing an initialized plugin', async () => { - const {initializedPlugin, deployer} = await loadFixture(fixture); + // todo add test for checking that plugins already initialized on a previous version can not be initialized again + it('reverts if trying to initialize lower version plugin'); + + it('sets the `_targetConfig` when initializing an uninitialized plugin', async () => { + const {uninitializedPlugin, deployer} = await loadFixture(fixture); + + const encodedDummyTarget = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint8'], + [deployer.address, Operation.delegatecall] + ); // reinitialize the plugin. - await initializedPlugin.initializeFrom({ - target: deployer.address, - operation: Operation.delegatecall, - }); + await uninitializedPlugin.initializeFrom( + latestInitializerVersion, + encodedDummyTarget + ); - expect((await initializedPlugin.getTargetConfig()).target).to.be.eq( + expect((await uninitializedPlugin.getTargetConfig()).target).to.be.eq( deployer.address ); - expect((await initializedPlugin.getTargetConfig()).operation).to.be.eq( + expect((await uninitializedPlugin.getTargetConfig()).operation).to.be.eq( Operation.delegatecall ); }); From 6f84d889c16dd32b9c65e8b30bfec3f99a9f9bfc Mon Sep 17 00:00:00 2001 From: Claudia Date: Fri, 13 Sep 2024 15:34:16 +0200 Subject: [PATCH 19/59] fix: upgradeability tests --- .../30_regression-testing/31_upgradeability.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/contracts/test/30_regression-testing/31_upgradeability.ts b/packages/contracts/test/30_regression-testing/31_upgradeability.ts index a92abe65..99825396 100644 --- a/packages/contracts/test/30_regression-testing/31_upgradeability.ts +++ b/packages/contracts/test/30_regression-testing/31_upgradeability.ts @@ -11,6 +11,7 @@ import { deployAndUpgradeSelfCheck, getProtocolVersion, } from '../test-utils/uups-upgradeable'; +import {latestInitializerVersion} from './../multisig-constants'; import {PLUGIN_UUPS_UPGRADEABLE_PERMISSIONS} from '@aragon/osx-commons-sdk'; import {DAO} from '@aragon/osx-ethers'; import {loadFixture} from '@nomicfoundation/hardhat-network-helpers'; @@ -104,6 +105,11 @@ describe('Upgrades', () => { const currentContractFactory = new Multisig__factory(deployer); const legacyContractFactory = new Multisig_V1_1__factory(deployer); + const encodedDummyTarget = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint8'], + [deployer.address, Operation.delegatecall] + ); + const {proxy, fromImplementation, toImplementation} = await deployAndUpgradeFromToCheck( deployer, @@ -115,7 +121,7 @@ describe('Upgrades', () => { PLUGIN_UUPS_UPGRADEABLE_PERMISSIONS.UPGRADE_PLUGIN_PERMISSION_ID, dao, 'initializeFrom', - [{target: deployer.address, operation: Operation.delegatecall}] + [latestInitializerVersion, encodedDummyTarget] ); expect(toImplementation).to.not.equal(fromImplementation); // The build did change @@ -146,6 +152,11 @@ describe('Upgrades', () => { const currentContractFactory = new Multisig__factory(deployer); const legacyContractFactory = new Multisig_V1_2__factory(deployer); + const encodedDummyTarget = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint8'], + [deployer.address, Operation.delegatecall] + ); + const {proxy, fromImplementation, toImplementation} = await deployAndUpgradeFromToCheck( deployer, @@ -157,7 +168,7 @@ describe('Upgrades', () => { PLUGIN_UUPS_UPGRADEABLE_PERMISSIONS.UPGRADE_PLUGIN_PERMISSION_ID, dao, 'initializeFrom', - [{target: deployer.address, operation: Operation.delegatecall}] + [latestInitializerVersion, encodedDummyTarget] ); expect(toImplementation).to.not.equal(fromImplementation); From 065802a664f12fc306b65af59fad0abef695522f Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Thu, 19 Sep 2024 13:06:10 +0400 Subject: [PATCH 20/59] modifier added for safety and target config improved --- packages/contracts/src/Multisig.sol | 40 ++++++++++--------- .../test/10_unit-testing/11_plugin.ts | 2 +- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index 7e2b767e..16ce7cdc 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -19,7 +19,7 @@ import {IMultisig} from "./IMultisig.sol"; /// @title Multisig /// @author Aragon X - 2022-2023 /// @notice The on-chain multisig governance plugin in which a proposal passes if X out of Y approvals are met. -/// @dev v1.3 (Release 1, Build 3) +/// @dev v1.3 (Release 1, Build 3). For each upgrade, if the reinitialization step is required, increment the version numbers in the modifier for both the initialize and initializeFrom functions. /// @custom:security-contact sirt@aragon.org contract Multisig is IMultisig, @@ -46,8 +46,7 @@ contract Multisig is mapping(address => bool) approvers; IDAO.Action[] actions; uint256 allowFailureMap; - address target; // added in v1.3.0 - Operation operation; // added in v1.3.0 + TargetConfig targetConfig; } /// @notice A container for the proposal parameters. @@ -103,6 +102,9 @@ contract Multisig is /// @param sender The sender address. error ProposalCreationForbidden(address sender); + /// @notice Thrown when initialize is called after it has already been executed. + error AlreadyInitialized(); + /// @notice Thrown if an approver is not allowed to cast an approve. This can be because the proposal /// - is not open, /// - was executed, or @@ -145,6 +147,15 @@ contract Multisig is /// @param minApprovals The minimum amount of approvals needed to pass a proposal. event MultisigSettingsUpdated(bool onlyListed, uint16 indexed minApprovals); + /// @notice This ensures that the initialize function cannot be called during the upgrade process. + modifier onlyCallAtInitialization() { + if (_getInitializedVersion() != 0) { + revert AlreadyInitialized(); + } + + _; + } + /// @notice Initializes Release 1, Build 2. /// @dev This method is required to support [ERC-1822](https://eips.ethereum.org/EIPS/eip-1822). /// @param _dao The IDAO interface of the associated DAO. @@ -155,7 +166,7 @@ contract Multisig is address[] calldata _members, MultisigSettings calldata _multisigSettings, TargetConfig calldata _targetConfig - ) external reinitializer(2) { + ) external onlyCallAtInitialization reinitializer(2) { __PluginUUPSUpgradeable_init(_dao); if (_members.length > type(uint16).max) { @@ -170,7 +181,8 @@ contract Multisig is _setTargetConfig(_targetConfig); } - /// @notice Reinitializes the TokenVoting after an upgrade from a previous protocol version. + /// @notice Reinitializes the TokenVoting after an upgrade from a previous protocol version. For each reinitialization step, use the `_fromBuild` version to decide which internal functions to call for reinitialization. + /// @dev WARNING: The contract should only be upgradeable through PSP to ensure that _fromBuild is not incorrectly passed, and that the appropriate permissions for the upgrade are properly configured. /// @param _fromBuild The build version number of the previous implementation contract this upgrade is transitioning from. /// @param _initData The initialization data to be passed to via `upgradeToAndCall` (see [ERC-1967](https://docs.openzeppelin.com/contracts/4.x/api/proxy#ERC1967Upgrade)). function initializeFrom(uint16 _fromBuild, bytes calldata _initData) external reinitializer(2) { @@ -302,15 +314,7 @@ contract Multisig is proposal_.parameters.endDate = _endDate; proposal_.parameters.minApprovals = multisigSettings.minApprovals; - TargetConfig memory currentTarget = getTargetConfig(); - - if (currentTarget.target == address(0)) { - proposal_.target = address(dao()); - proposal_.operation = Operation.Call; - } else { - proposal_.target = currentTarget.target; - proposal_.operation = currentTarget.operation; - } + proposal_.targetConfig = getTargetConfig(); // Reduce costs if (_allowFailureMap != 0) { @@ -462,11 +466,11 @@ contract Multisig is proposal_.executed = true; _execute( - proposal_.target, + proposal_.targetConfig.target, bytes32(_proposalId), - proposals[_proposalId].actions, - proposals[_proposalId].allowFailureMap, - proposal_.operation + proposal_.actions, + proposal_.allowFailureMap, + proposal_.targetConfig.operation ); emit ProposalExecuted(_proposalId); diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index 76e6b079..220d497b 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -182,7 +182,7 @@ describe('Multisig', function () { defaultInitData.settings, defaultInitData.targetConfig ) - ).to.be.revertedWith('Initializable: contract is already initialized'); + ).to.be.revertedWithCustomError(initializedPlugin, 'AlreadyInitialized'); }); it('adds the initial addresses to the address list', async () => { From 6b6fa6f9b1b817a0f6eb453cff9b14cad5c0e77a Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Thu, 19 Sep 2024 13:10:21 +0400 Subject: [PATCH 21/59] add comment --- packages/contracts/src/Multisig.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index 16ce7cdc..c0533bd7 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -46,7 +46,7 @@ contract Multisig is mapping(address => bool) approvers; IDAO.Action[] actions; uint256 allowFailureMap; - TargetConfig targetConfig; + TargetConfig targetConfig; // added in build 3. } /// @notice A container for the proposal parameters. From c87ff68ee88656076fdc24c71692d0fa628eb4fa Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Thu, 19 Sep 2024 17:44:36 +0400 Subject: [PATCH 22/59] bytes param added --- packages/contracts/src/Multisig.sol | 30 +- .../test/10_unit-testing/11_plugin.ts | 592 +++++++++++++----- packages/contracts/test/multisig-constants.ts | 6 + 3 files changed, 470 insertions(+), 158 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index c0533bd7..8d91fe70 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -343,14 +343,38 @@ contract Multisig is ); } + /// @inheritdoc IProposal function createProposal( bytes calldata _metadata, IDAO.Action[] calldata _actions, uint64 _startDate, - uint64 _endDate + uint64 _endDate, + bytes memory _data ) external override returns (uint256 proposalId) { - // Calls public function for permission check. - proposalId = createProposal(_metadata, _actions, 0, false, false, _startDate, _endDate); + // Note that this calls public function for permission check. + if (_data.length == 0) { + // Proposal can still be created with default values. + proposalId = createProposal(_metadata, _actions, 0, false, false, _startDate, _endDate); + } else { + (uint256 allowFailureMap, bool approveProposal, bool tryExecution) = abi.decode( + _data, + (uint256, bool, bool) + ); + proposalId = createProposal( + _metadata, + _actions, + allowFailureMap, + approveProposal, + tryExecution, + _startDate, + _endDate + ); + } + } + + /// @inheritdoc IProposal + function createProposalParams() external pure override returns (string memory) { + return "[uint256 allowFailureMap, bool approveProposal, bool tryExecution]"; } /// @inheritdoc IMultisig diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index 220d497b..93c45a41 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -21,6 +21,8 @@ import { import { ANY_ADDR, CREATE_PROPOSAL_PERMISSION_ID, + CREATE_PROPOSAL_SIGNATURE, + CREATE_PROPOSAL_SIGNATURE_IProposal, MULTISIG_EVENTS, MULTISIG_INTERFACE, Operation, @@ -687,19 +689,119 @@ describe('Multisig', function () { }); }); + // These tests ensure that overriden `createProposal` function from `IProposal` + // successfully creates a proposal with default values(when `data` is not passed) + // and with custom values when it's passed. + describe('Proposal creation: IProposal Interface Function', async () => { + let data: FixtureResult; + beforeEach(async () => { + data = await loadFixtureAndGrantCreatePermission(); + const {deployer, dao, initializedPlugin: plugin} = data; + + await dao.grant( + plugin.address, + deployer.address, + UPDATE_MULTISIG_SETTINGS_PERMISSION_ID + ); + + await plugin.updateMultisigSettings({ + onlyListed: false, + minApprovals: 1, + }); + + await dao.grant( + dao.address, + plugin.address, + DAO_PERMISSIONS.EXECUTE_PERMISSION_ID + ); + }); + + it('creates proposal with default values if `data` param is encoded with custom values', async () => { + const { + alice, + dummyMetadata, + dummyActions, + initializedPlugin: plugin, + } = data; + + const encodedParam = ethers.utils.defaultAbiCoder.encode( + ['uint256', 'bool', 'bool'], + [1, true, true] + ); + + await plugin + .connect(alice) + [CREATE_PROPOSAL_SIGNATURE_IProposal]( + dummyMetadata, + dummyActions, + 0, + (await time.latest()) + TIME.HOUR, + encodedParam + ); + + const proposalId = await plugin.createProposalId( + dummyActions, + dummyMetadata + ); + const proposal = await plugin.getProposal(proposalId); + expect(proposal.allowFailureMap).to.equal(1); + + expect(await plugin.hasApproved(proposalId, alice.address)).to.be.true; + expect(proposal.executed).to.be.true; + }); + + it('creates proposal with default values if `data` param is passed as empty', async () => { + const { + alice, + dummyMetadata, + dummyActions, + initializedPlugin: plugin, + } = data; + + await plugin + .connect(alice) + [CREATE_PROPOSAL_SIGNATURE_IProposal]( + dummyMetadata, + dummyActions, + 0, + (await time.latest()) + TIME.HOUR, + '0x' + ); + + const proposalId = await plugin.createProposalId( + dummyActions, + dummyMetadata + ); + + const proposal = await plugin.getProposal(proposalId); + expect(proposal.actions.length).to.equal(dummyActions.length); + expect(proposal.allowFailureMap).to.equal(0); + + expect(await plugin.hasApproved(proposalId, alice.address)).to.be.false; + expect(proposal.executed).to.be.false; + }); + }); + describe('createProposal', async () => { let data: FixtureResult; beforeEach(async () => { data = await loadFixtureAndGrantCreatePermission(); }); + it('reverts if permission is not given', async () => { const {deployer, dao, dummyMetadata, initializedPlugin: plugin} = data; await dao.revoke(plugin.address, ANY_ADDR, CREATE_PROPOSAL_PERMISSION_ID); await expect( - plugin[ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ]('0x', [], 0, false, false, 0, await time.latest()) + plugin[CREATE_PROPOSAL_SIGNATURE]( + '0x', + [], + 0, + false, + false, + 0, + await time.latest() + ) ) .to.be.revertedWithCustomError(plugin, 'DaoUnauthorized') .withArgs( @@ -727,9 +829,15 @@ describe('Multisig', function () { await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); // Check that proposal exists const proposal = await plugin.getProposal(proposalId); @@ -752,9 +860,15 @@ describe('Multisig', function () { await expect( plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, [], 0, false, false, startDate, endDate) + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + [], + 0, + false, + false, + startDate, + endDate + ) ) .to.emit(plugin, 'ProposalCreated') .withArgs( @@ -843,35 +957,27 @@ describe('Multisig', function () { await ethers.provider.send('evm_setAutomine', [false]); // Create and execute proposal #1 calling `updateMultisigSettings`. - await plugin - .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ]( + await plugin.connect(alice)[CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + [updateMultisigSettingsAction], + 0, + true, // approve + true, // execute + 0, + endDate + ); + + // Try to call update the settings a second time. + await expect( + plugin.connect(alice)[CREATE_PROPOSAL_SIGNATURE]( dummyMetadata, [updateMultisigSettingsAction], 0, - true, // approve - true, // execute + false, // approve + false, // execute 0, endDate - ); - - // Try to call update the settings a second time. - await expect( - plugin - .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ]( - dummyMetadata, - [updateMultisigSettingsAction], - 0, - false, // approve - false, // execute - 0, - endDate - ) + ) ) .to.revertedWithCustomError(plugin, 'ProposalCreationForbidden') .withArgs(alice.address); @@ -915,9 +1021,15 @@ describe('Multisig', function () { await expect( plugin .connect(dave) // Dave is not listed. - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, [], 0, false, false, startDate, endDate) + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + [], + 0, + false, + false, + startDate, + endDate + ) ) .to.emit(plugin, 'ProposalCreated') .withArgs( @@ -947,9 +1059,15 @@ describe('Multisig', function () { await expect( plugin .connect(dave) // Dave is not listed. - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate) + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ) ) .to.be.revertedWithCustomError(plugin, 'DaoUnauthorized') .withArgs( @@ -997,9 +1115,15 @@ describe('Multisig', function () { await expect( plugin .connect(carol) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate) + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ) ) .to.be.revertedWithCustomError(plugin, 'DaoUnauthorized') .withArgs( @@ -1012,9 +1136,15 @@ describe('Multisig', function () { // Transaction 4: Create the proposal as Dave const tx4 = await plugin .connect(dave) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Check the listed members before the block is mined. @@ -1074,19 +1204,15 @@ describe('Multisig', function () { const approveProposal = false; await expect( - plugin - .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ]( - dummyMetadata, - [], - allowFailureMap, - approveProposal, // false - false, - startDate, - endDate - ) + plugin.connect(alice)[CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + [], + allowFailureMap, + approveProposal, // false + false, + startDate, + endDate + ) ) .to.emit(plugin, 'ProposalCreated') .withArgs(id, alice.address, startDate, endDate, dummyMetadata, [], 0); @@ -1133,19 +1259,15 @@ describe('Multisig', function () { const id = await plugin.createProposalId([], dummyMetadata); await expect( - plugin - .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ]( - dummyMetadata, - [], - allowFailureMap, - approveProposal, // true - false, - startDate, - endDate - ) + plugin.connect(alice)[CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + [], + allowFailureMap, + approveProposal, // true + false, + startDate, + endDate + ) ) .to.emit(plugin, 'ProposalCreated') .withArgs( @@ -1201,9 +1323,7 @@ describe('Multisig', function () { await expect( plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ]( + [CREATE_PROPOSAL_SIGNATURE]( dummyMetadata, dummyActions, 0, @@ -1231,9 +1351,15 @@ describe('Multisig', function () { await expect( plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, true, false, startDate, endDate) + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + true, + false, + startDate, + endDate + ) ) .to.be.revertedWithCustomError(plugin, 'DateOutOfBounds') .withArgs(startDate, endDate); @@ -1262,9 +1388,15 @@ describe('Multisig', function () { await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); @@ -1302,9 +1434,15 @@ describe('Multisig', function () { const endDate = (await time.latest()) + TIME.HOUR; await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = 0; // Check that Dave who is not listed cannot approve. @@ -1325,9 +1463,15 @@ describe('Multisig', function () { // Create a proposal (with ID 0) await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); await plugin.connect(alice).approve(id, false); @@ -1347,9 +1491,15 @@ describe('Multisig', function () { // Create a proposal (with ID 0) await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); expect(await plugin.canApprove(id, alice.address)).to.be.true; @@ -1369,9 +1519,15 @@ describe('Multisig', function () { // Create a proposal (with ID 0) await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, startDate, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + startDate, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); expect(await plugin.canApprove(id, alice.address)).to.be.false; @@ -1394,9 +1550,15 @@ describe('Multisig', function () { // Create a proposal (with ID 0) await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); expect(await plugin.canApprove(id, alice.address)).to.be.true; @@ -1421,9 +1583,15 @@ describe('Multisig', function () { // Create a proposal (with ID 0) await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); expect(await plugin.hasApproved(id, alice.address)).to.be.false; @@ -1441,9 +1609,15 @@ describe('Multisig', function () { await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); await plugin.connect(alice).approve(id, false); @@ -1465,9 +1639,15 @@ describe('Multisig', function () { // Create a proposal (with ID 0) await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); await plugin.connect(alice).approve(id, true); @@ -1492,9 +1672,15 @@ describe('Multisig', function () { // Create a proposal (with ID 0) await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. @@ -1525,9 +1711,15 @@ describe('Multisig', function () { // Create a proposal (with ID 0). await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Check that there are 0 approvals yet. @@ -1558,9 +1750,15 @@ describe('Multisig', function () { const endDate = startDate + TIME.HOUR; await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, startDate, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + startDate, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Try to approve the proposal as Alice although being before the start date. @@ -1590,9 +1788,15 @@ describe('Multisig', function () { await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Advance time after the end date. @@ -1617,9 +1821,15 @@ describe('Multisig', function () { const endDate = (await time.latest()) + TIME.HOUR; await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Check that `minApprovals` isn't met yet. @@ -1644,9 +1854,15 @@ describe('Multisig', function () { const endDate = (await time.latest()) + TIME.HOUR; await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. @@ -1681,9 +1897,15 @@ describe('Multisig', function () { await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); await plugin.connect(alice).approve(id, false); @@ -1708,9 +1930,15 @@ describe('Multisig', function () { await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, startDate, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + startDate, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); expect(await plugin.canExecute(id)).to.be.false; @@ -1737,9 +1965,15 @@ describe('Multisig', function () { await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); await plugin.connect(alice).approve(id, false); @@ -1768,9 +2002,15 @@ describe('Multisig', function () { const endDate = (await time.latest()) + TIME.HOUR; await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. @@ -1801,9 +2041,15 @@ describe('Multisig', function () { const endDate = (await time.latest()) + TIME.HOUR; await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. @@ -1849,9 +2095,15 @@ describe('Multisig', function () { await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); await dao.grant( @@ -1892,9 +2144,15 @@ describe('Multisig', function () { const endDate = (await time.latest()) + TIME.HOUR; await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. @@ -1986,9 +2244,15 @@ describe('Multisig', function () { const endDate = (await time.latest()) + TIME.HOUR; await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` on the DAO. @@ -2024,9 +2288,15 @@ describe('Multisig', function () { const endDate = (await time.latest()) + TIME.HOUR; await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. @@ -2062,9 +2332,15 @@ describe('Multisig', function () { const endDate = startDate + TIME.HOUR; await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, startDate, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + startDate, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. @@ -2104,9 +2380,15 @@ describe('Multisig', function () { const endDate = startDate + TIME.HOUR; await plugin .connect(alice) - [ - 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)' - ](dummyMetadata, dummyActions, 0, false, false, 0, endDate); + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Approve the proposal but do not execute yet. diff --git a/packages/contracts/test/multisig-constants.ts b/packages/contracts/test/multisig-constants.ts index f736a108..2c5401c5 100644 --- a/packages/contracts/test/multisig-constants.ts +++ b/packages/contracts/test/multisig-constants.ts @@ -27,6 +27,12 @@ export const SET_TARGET_CONFIG_PERMISSION_ID = ethers.utils.id( 'SET_TARGET_CONFIG_PERMISSION' ); +export const CREATE_PROPOSAL_SIGNATURE = + 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)'; + +export const CREATE_PROPOSAL_SIGNATURE_IProposal = + 'createProposal(bytes,(address,uint256,bytes)[],uint64,uint64,bytes)'; + export const ANY_ADDR = '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF'; export enum Operation { From 11364d5b020dba6857375525eaf1dfff7a560ae8 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Thu, 19 Sep 2024 18:04:46 +0400 Subject: [PATCH 23/59] rename param function: --- packages/contracts/src/Multisig.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index 8d91fe70..ce630c86 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -373,7 +373,7 @@ contract Multisig is } /// @inheritdoc IProposal - function createProposalParams() external pure override returns (string memory) { + function createProposalParamsABI() external pure override returns (string memory) { return "[uint256 allowFailureMap, bool approveProposal, bool tryExecution]"; } From 9164a7c3e382f5613464719dee84c70f78be4a59 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Mon, 23 Sep 2024 12:34:07 +0400 Subject: [PATCH 24/59] Update packages/contracts/src/Multisig.sol Co-authored-by: Claudia Barcelo --- packages/contracts/src/Multisig.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index c0533bd7..89d15b3b 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -83,7 +83,7 @@ contract Multisig is bytes32 public constant UPDATE_MULTISIG_SETTINGS_PERMISSION_ID = keccak256("UPDATE_MULTISIG_SETTINGS_PERMISSION"); - /// @notice The ID of the permission required to call the `addAddresses` and `removeAddresses` functions. + /// @notice The ID of the permission required to call the `createProposal` function. bytes32 public constant CREATE_PROPOSAL_PERMISSION_ID = keccak256("CREATE_PROPOSAL_PERMISSION"); /// @notice A mapping between proposal IDs and proposal information. From 28d65f7074052436d2a1af47e9f7bfd28907cb3e Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Wed, 25 Sep 2024 10:58:53 +0400 Subject: [PATCH 25/59] Update packages/contracts/src/Multisig.sol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jør∂¡ <4456749+brickpop@users.noreply.github.com> --- packages/contracts/src/Multisig.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index ce630c86..4bc5e586 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -374,7 +374,7 @@ contract Multisig is /// @inheritdoc IProposal function createProposalParamsABI() external pure override returns (string memory) { - return "[uint256 allowFailureMap, bool approveProposal, bool tryExecution]"; + return "(uint256 allowFailureMap, bool approveProposal, bool tryExecution)"; } /// @inheritdoc IMultisig From c262bad235ea6c9df813f3ad2323b2d25d691733 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Wed, 25 Sep 2024 10:59:37 +0400 Subject: [PATCH 26/59] Update packages/contracts/src/Multisig.sol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jør∂¡ <4456749+brickpop@users.noreply.github.com> --- packages/contracts/src/Multisig.sol | 30 +++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index 4bc5e586..a8b43f69 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -352,24 +352,26 @@ contract Multisig is bytes memory _data ) external override returns (uint256 proposalId) { // Note that this calls public function for permission check. - if (_data.length == 0) { - // Proposal can still be created with default values. - proposalId = createProposal(_metadata, _actions, 0, false, false, _startDate, _endDate); - } else { - (uint256 allowFailureMap, bool approveProposal, bool tryExecution) = abi.decode( + // Custom parameters + uint256 _allowFailureMap; + bool _approveProposal; + bool _tryExecution; + + if (_data.length != 0) { + (_allowFailureMap, _approveProposal, _tryExecution) = abi.decode( _data, (uint256, bool, bool) ); - proposalId = createProposal( - _metadata, - _actions, - allowFailureMap, - approveProposal, - tryExecution, - _startDate, - _endDate - ); } + proposalId = createProposal( + _metadata, + _actions, + _allowFailureMap, + _approveProposal, + _tryExecution, + _startDate, + _endDate + ); } /// @inheritdoc IProposal From 27c09ee451de9e8908e68fc661685d2984621ce7 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Thu, 26 Sep 2024 14:07:04 +0400 Subject: [PATCH 27/59] add test for delegatecall --- .../src/mocks/CustomExecutorMock.sol | 24 ++++++++ .../test/10_unit-testing/11_plugin.ts | 56 ++++++++++++++++++- 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 packages/contracts/src/mocks/CustomExecutorMock.sol diff --git a/packages/contracts/src/mocks/CustomExecutorMock.sol b/packages/contracts/src/mocks/CustomExecutorMock.sol new file mode 100644 index 00000000..31ce3025 --- /dev/null +++ b/packages/contracts/src/mocks/CustomExecutorMock.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.8; + +import {IDAO} from "@aragon/osx-commons-contracts/src/dao/IDAO.sol"; + +/// @dev DO NOT USE IN PRODUCTION! +contract CustomExecutorMock { + error FailedCustom(); + + event ExecutedCustom(); + + function execute( + bytes32 callId, + IDAO.Action[] memory _actions, + uint256 allowFailureMap + ) external returns (bytes[] memory execResults, uint256 failureMap) { + if (callId == bytes32(0)) { + revert FailedCustom(); + } else { + emit ExecutedCustom(); + } + } +} diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index 93c45a41..71df216a 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -1,6 +1,8 @@ import {createDaoProxy} from '../20_integration-testing/test-helpers'; import { Addresslist__factory, + CustomExecutorMock__factory, + ERC1967Proxy__factory, IERC165Upgradeable__factory, IMembership__factory, IMultisig__factory, @@ -26,6 +28,7 @@ import { MULTISIG_EVENTS, MULTISIG_INTERFACE, Operation, + SET_TARGET_CONFIG_PERMISSION_ID, TargetConfig, UPDATE_MULTISIG_SETTINGS_PERMISSION_ID, } from '../multisig-constants'; @@ -38,7 +41,12 @@ import { TIME, DAO_PERMISSIONS, } from '@aragon/osx-commons-sdk'; -import {DAO, DAOStructs, DAO__factory} from '@aragon/osx-ethers'; +import { + DAO, + DAOStructs, + DAO__factory, + PluginUUPSUpgradeableV1Mock__factory, +} from '@aragon/osx-ethers'; import {loadFixture, time} from '@nomicfoundation/hardhat-network-helpers'; import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; import {expect} from 'chai'; @@ -2403,6 +2411,52 @@ describe('Multisig', function () { plugin.connect(bob).execute(id) ).to.be.revertedWithCustomError(plugin, 'ProposalExecutionForbidden'); }); + + it('executes target with delegate call', async () => { + const {alice, bob, dummyMetadata, dummyActions, deployer, dao} = data; + + let {initializedPlugin: plugin} = data; + + const executorFactory = new CustomExecutorMock__factory(deployer); + const executor = await executorFactory.deploy(); + + const abiA = CustomExecutorMock__factory.abi; + const abiB = Multisig__factory.abi; + // @ts-ignore + const mergedABI = abiA.concat(abiB); + + await dao.grant( + plugin.address, + deployer.address, + SET_TARGET_CONFIG_PERMISSION_ID + ); + + await plugin.connect(deployer).setTargetConfig({ + target: executor.address, + operation: Operation.delegatecall, + }); + + plugin = (await ethers.getContractAt( + mergedABI, + plugin.address + )) as Multisig; + + const endDate = (await time.latest()) + TIME.HOUR; + await plugin.connect(alice)[CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 1, + true, // approve right away. + false, + 0, + endDate + ); + + const id = await plugin.createProposalId(dummyActions, dummyMetadata); + await expect(plugin.connect(bob).approve(id, true)) + .to.emit(plugin, 'ExecutedCustom') + .to.emit(plugin, 'ProposalExecuted'); + }); }); }); }); From 0eaa19dbcba5227f4f3e450f86f642a20986bee1 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Thu, 26 Sep 2024 14:40:15 +0400 Subject: [PATCH 28/59] silence warnings --- packages/contracts/src/mocks/CustomExecutorMock.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/mocks/CustomExecutorMock.sol b/packages/contracts/src/mocks/CustomExecutorMock.sol index 31ce3025..1c53739f 100644 --- a/packages/contracts/src/mocks/CustomExecutorMock.sol +++ b/packages/contracts/src/mocks/CustomExecutorMock.sol @@ -12,9 +12,11 @@ contract CustomExecutorMock { function execute( bytes32 callId, - IDAO.Action[] memory _actions, - uint256 allowFailureMap + IDAO.Action[] memory, + uint256 ) external returns (bytes[] memory execResults, uint256 failureMap) { + (execResults, failureMap); + if (callId == bytes32(0)) { revert FailedCustom(); } else { From 451c556238b354bb69daf6545a15c7938ae72453 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Thu, 26 Sep 2024 16:14:39 +0400 Subject: [PATCH 29/59] executor interface --- packages/contracts/src/Multisig.sol | 11 ++++++----- packages/contracts/src/mocks/CustomExecutorMock.sol | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index a8b43f69..c051bcce 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -11,6 +11,7 @@ import {ProposalUpgradeable} from "@aragon/osx-commons-contracts/src/plugin/exte import {PluginUUPSUpgradeable} from "@aragon/osx-commons-contracts/src/plugin/PluginUUPSUpgradeable.sol"; import {IProposal} from "@aragon/osx-commons-contracts/src/plugin/extensions/proposal/IProposal.sol"; import {IDAO} from "@aragon/osx-commons-contracts/src/dao/IDAO.sol"; +import {IExecutor} from "@aragon/osx-commons-contracts/src/executors/IExecutor.sol"; import {IMultisig} from "./IMultisig.sol"; @@ -44,7 +45,7 @@ contract Multisig is uint16 approvals; ProposalParameters parameters; mapping(address => bool) approvers; - IDAO.Action[] actions; + IExecutor.Action[] actions; uint256 allowFailureMap; TargetConfig targetConfig; // added in build 3. } @@ -268,7 +269,7 @@ contract Multisig is // solhint-disable-next-line code-complexity function createProposal( bytes calldata _metadata, - IDAO.Action[] calldata _actions, + IExecutor.Action[] calldata _actions, uint256 _allowFailureMap, bool _approveProposal, bool _tryExecution, @@ -346,7 +347,7 @@ contract Multisig is /// @inheritdoc IProposal function createProposal( bytes calldata _metadata, - IDAO.Action[] calldata _actions, + IExecutor.Action[] calldata _actions, uint64 _startDate, uint64 _endDate, bytes memory _data @@ -433,7 +434,7 @@ contract Multisig is bool executed, uint16 approvals, ProposalParameters memory parameters, - IDAO.Action[] memory actions, + IExecutor.Action[] memory actions, uint256 allowFailureMap ) { @@ -478,7 +479,7 @@ contract Multisig is /// @param _metadata The metadata of the proposal. /// @return proposalId The ID of the proposal. function createProposalId( - IDAO.Action[] calldata _actions, + IExecutor.Action[] calldata _actions, bytes memory _metadata ) public pure override returns (uint256) { return uint256(keccak256(abi.encode(_actions, _metadata))); diff --git a/packages/contracts/src/mocks/CustomExecutorMock.sol b/packages/contracts/src/mocks/CustomExecutorMock.sol index 1c53739f..1cb6ba04 100644 --- a/packages/contracts/src/mocks/CustomExecutorMock.sol +++ b/packages/contracts/src/mocks/CustomExecutorMock.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.8; -import {IDAO} from "@aragon/osx-commons-contracts/src/dao/IDAO.sol"; +import {IExecutor} from "@aragon/osx-commons-contracts/src/executors/IExecutor.sol"; /// @dev DO NOT USE IN PRODUCTION! contract CustomExecutorMock { @@ -12,7 +12,7 @@ contract CustomExecutorMock { function execute( bytes32 callId, - IDAO.Action[] memory, + IExecutor.Action[] memory, uint256 ) external returns (bytes[] memory execResults, uint256 failureMap) { (execResults, failureMap); From 7e860904d71af5ad307a7ac46520eaa7379ddad1 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Fri, 27 Sep 2024 08:30:39 +0400 Subject: [PATCH 30/59] action as free level --- packages/contracts/src/Multisig.sol | 12 ++++++------ packages/contracts/src/mocks/CustomExecutorMock.sol | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index c051bcce..ad40c9ac 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -11,7 +11,7 @@ import {ProposalUpgradeable} from "@aragon/osx-commons-contracts/src/plugin/exte import {PluginUUPSUpgradeable} from "@aragon/osx-commons-contracts/src/plugin/PluginUUPSUpgradeable.sol"; import {IProposal} from "@aragon/osx-commons-contracts/src/plugin/extensions/proposal/IProposal.sol"; import {IDAO} from "@aragon/osx-commons-contracts/src/dao/IDAO.sol"; -import {IExecutor} from "@aragon/osx-commons-contracts/src/executors/IExecutor.sol"; +import {IExecutor, Action} from "@aragon/osx-commons-contracts/src/executors/IExecutor.sol"; import {IMultisig} from "./IMultisig.sol"; @@ -45,7 +45,7 @@ contract Multisig is uint16 approvals; ProposalParameters parameters; mapping(address => bool) approvers; - IExecutor.Action[] actions; + Action[] actions; uint256 allowFailureMap; TargetConfig targetConfig; // added in build 3. } @@ -269,7 +269,7 @@ contract Multisig is // solhint-disable-next-line code-complexity function createProposal( bytes calldata _metadata, - IExecutor.Action[] calldata _actions, + Action[] calldata _actions, uint256 _allowFailureMap, bool _approveProposal, bool _tryExecution, @@ -347,7 +347,7 @@ contract Multisig is /// @inheritdoc IProposal function createProposal( bytes calldata _metadata, - IExecutor.Action[] calldata _actions, + Action[] calldata _actions, uint64 _startDate, uint64 _endDate, bytes memory _data @@ -434,7 +434,7 @@ contract Multisig is bool executed, uint16 approvals, ProposalParameters memory parameters, - IExecutor.Action[] memory actions, + Action[] memory actions, uint256 allowFailureMap ) { @@ -479,7 +479,7 @@ contract Multisig is /// @param _metadata The metadata of the proposal. /// @return proposalId The ID of the proposal. function createProposalId( - IExecutor.Action[] calldata _actions, + Action[] calldata _actions, bytes memory _metadata ) public pure override returns (uint256) { return uint256(keccak256(abi.encode(_actions, _metadata))); diff --git a/packages/contracts/src/mocks/CustomExecutorMock.sol b/packages/contracts/src/mocks/CustomExecutorMock.sol index 1cb6ba04..e59fa539 100644 --- a/packages/contracts/src/mocks/CustomExecutorMock.sol +++ b/packages/contracts/src/mocks/CustomExecutorMock.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.8; -import {IExecutor} from "@aragon/osx-commons-contracts/src/executors/IExecutor.sol"; +import {IExecutor, Action} from "@aragon/osx-commons-contracts/src/executors/IExecutor.sol"; /// @dev DO NOT USE IN PRODUCTION! contract CustomExecutorMock { @@ -12,7 +12,7 @@ contract CustomExecutorMock { function execute( bytes32 callId, - IExecutor.Action[] memory, + Action[] memory, uint256 ) external returns (bytes[] memory execResults, uint256 failureMap) { (execResults, failureMap); From dfe1336eda8363239d308f1ff5cddec814be1a4a Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Fri, 27 Sep 2024 12:25:09 +0400 Subject: [PATCH 31/59] remove unnecessary revoke --- packages/contracts/src/MultisigSetup.sol | 16 ++++------------ .../test/10_unit-testing/12_plugin-setup.ts | 9 +-------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/packages/contracts/src/MultisigSetup.sol b/packages/contracts/src/MultisigSetup.sol index aacd8555..5ff61e24 100644 --- a/packages/contracts/src/MultisigSetup.sol +++ b/packages/contracts/src/MultisigSetup.sol @@ -170,7 +170,7 @@ contract MultisigSetup is PluginUpgradeableSetup { SetupPayload calldata _payload ) external view returns (PermissionLib.MultiTargetPermission[] memory permissions) { // Prepare permissions - permissions = new PermissionLib.MultiTargetPermission[](5); + permissions = new PermissionLib.MultiTargetPermission[](4); // Set permissions to be Revoked. permissions[0] = PermissionLib.MultiTargetPermission({ @@ -181,15 +181,7 @@ contract MultisigSetup is PluginUpgradeableSetup { permissionId: UPDATE_MULTISIG_SETTINGS_PERMISSION_ID }); - permissions[1] = PermissionLib.MultiTargetPermission( - PermissionLib.Operation.Revoke, - _payload.plugin, - _dao, - PermissionLib.NO_CONDITION, - UPGRADE_PLUGIN_PERMISSION_ID - ); - - permissions[2] = PermissionLib.MultiTargetPermission({ + permissions[1] = PermissionLib.MultiTargetPermission({ operation: PermissionLib.Operation.Revoke, where: _dao, who: _payload.plugin, @@ -197,7 +189,7 @@ contract MultisigSetup is PluginUpgradeableSetup { permissionId: EXECUTE_PERMISSION_ID }); - permissions[3] = PermissionLib.MultiTargetPermission({ + permissions[2] = PermissionLib.MultiTargetPermission({ operation: PermissionLib.Operation.Revoke, where: _payload.plugin, who: _dao, @@ -205,7 +197,7 @@ contract MultisigSetup is PluginUpgradeableSetup { permissionId: SET_TARGET_CONFIG_PERMISSION_ID }); - permissions[4] = PermissionLib.MultiTargetPermission( + permissions[3] = PermissionLib.MultiTargetPermission( PermissionLib.Operation.Revoke, _payload.plugin, address(type(uint160).max), // ANY_ADDR diff --git a/packages/contracts/test/10_unit-testing/12_plugin-setup.ts b/packages/contracts/test/10_unit-testing/12_plugin-setup.ts index 3f3e79a6..86109d43 100644 --- a/packages/contracts/test/10_unit-testing/12_plugin-setup.ts +++ b/packages/contracts/test/10_unit-testing/12_plugin-setup.ts @@ -518,7 +518,7 @@ describe('MultisigSetup', function () { ); // Check the return data. - expect(permissions.length).to.be.equal(5); + expect(permissions.length).to.be.equal(4); expect(permissions).to.deep.equal([ [ Operation.Revoke, @@ -527,13 +527,6 @@ describe('MultisigSetup', function () { AddressZero, UPDATE_MULTISIG_SETTINGS_PERMISSION_ID, ], - [ - Operation.Revoke, - plugin, - dao.address, - AddressZero, - UPGRADE_PLUGIN_PERMISSION_ID, - ], [ Operation.Revoke, dao.address, From b1fdf94e343f80c08369e098083db0f4b16a0adb Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Sat, 28 Sep 2024 12:10:39 +0400 Subject: [PATCH 32/59] better upgrade tests --- .../31_upgradeability.ts | 201 ++++++++++-------- .../test/test-utils/typechain-versions.ts | 4 +- .../test/test-utils/uups-upgradeable.ts | 4 +- 3 files changed, 112 insertions(+), 97 deletions(-) diff --git a/packages/contracts/test/30_regression-testing/31_upgradeability.ts b/packages/contracts/test/30_regression-testing/31_upgradeability.ts index 99825396..7f50e671 100644 --- a/packages/contracts/test/30_regression-testing/31_upgradeability.ts +++ b/packages/contracts/test/30_regression-testing/31_upgradeability.ts @@ -1,8 +1,8 @@ import {createDaoProxy} from '../20_integration-testing/test-helpers'; import {Operation, TargetConfig} from '../multisig-constants'; import { - Multisig_V1_1__factory, - Multisig_V1_2__factory, + Multisig_V1_0_0__factory, + Multisig_V1_3_0__factory, Multisig__factory, Multisig, } from '../test-utils/typechain-versions'; @@ -19,6 +19,9 @@ import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; import {expect} from 'chai'; import {ethers} from 'hardhat'; +const AlreadyInitializedSignature = + Multisig__factory.createInterface().encodeErrorResult('AlreadyInitialized'); + describe('Upgrades', () => { it('upgrades to a new implementation', async () => { const {deployer, alice, dao, defaultInitData} = await loadFixture(fixture); @@ -40,89 +43,51 @@ describe('Upgrades', () => { ); }); - it('upgrades from v1.1', async () => { - const {deployer, alice, dao, defaultInitData} = await loadFixture(fixture); + it('upgrades from v1.0.0 with initializeFrom', async () => { + const {deployer, alice, dao, defaultInitData, encodedParamsForUpgrade} = + await loadFixture(fixture); const currentContractFactory = new Multisig__factory(deployer); - const legacyContractFactory = new Multisig_V1_1__factory(deployer); - - const {fromImplementation, toImplementation} = - await deployAndUpgradeFromToCheck( - deployer, - alice, - [dao.address, defaultInitData.members, defaultInitData.settings], - 'initialize', - legacyContractFactory, - currentContractFactory, - PLUGIN_UUPS_UPGRADEABLE_PERMISSIONS.UPGRADE_PLUGIN_PERMISSION_ID, - dao - ); - expect(toImplementation).to.not.equal(fromImplementation); // The build did change - - const fromProtocolVersion = await getProtocolVersion( - legacyContractFactory.attach(fromImplementation) - ); - const toProtocolVersion = await getProtocolVersion( - currentContractFactory.attach(toImplementation) - ); - - expect(fromProtocolVersion).to.not.deep.equal(toProtocolVersion); - expect(fromProtocolVersion).to.deep.equal([1, 0, 0]); - expect(toProtocolVersion).to.deep.equal([1, 4, 0]); - }); + const legacyContractFactory = new Multisig_V1_0_0__factory(deployer); - it('from v1.2', async () => { - const {deployer, alice, dao, defaultInitData} = await loadFixture(fixture); - const currentContractFactory = new Multisig__factory(deployer); - const legacyContractFactory = new Multisig_V1_2__factory(deployer); + const data = [ + deployer, + alice, + [dao.address, defaultInitData.members, defaultInitData.settings], + 'initialize', + legacyContractFactory, + currentContractFactory, + PLUGIN_UUPS_UPGRADEABLE_PERMISSIONS.UPGRADE_PLUGIN_PERMISSION_ID, + dao, + 'initialize', + [ + dao.address, + defaultInitData.members, + defaultInitData.settings, + defaultInitData.targetConfig, + ], + ]; - const {fromImplementation, toImplementation} = + // Ensure that on the `upgrade`, `initialize` can not be called. + try { await deployAndUpgradeFromToCheck( - deployer, - alice, - [dao.address, defaultInitData.members, defaultInitData.settings], - 'initialize', - legacyContractFactory, - currentContractFactory, - PLUGIN_UUPS_UPGRADEABLE_PERMISSIONS.UPGRADE_PLUGIN_PERMISSION_ID, - dao + // @ts-ignore + ...data ); - expect(toImplementation).to.not.equal(fromImplementation); + throw new Error(''); + } catch (err: any) { + expect(err.data).to.equal(AlreadyInitializedSignature); + } - const fromProtocolVersion = await getProtocolVersion( - legacyContractFactory.attach(fromImplementation) - ); - const toProtocolVersion = await getProtocolVersion( - currentContractFactory.attach(toImplementation) - ); - - expect(fromProtocolVersion).to.not.deep.equal(toProtocolVersion); - expect(fromProtocolVersion).to.deep.equal([1, 0, 0]); - expect(toProtocolVersion).to.deep.equal([1, 4, 0]); - }); - - it('upgrades from v1.1 with initializeFrom', async () => { - const {deployer, alice, dao, defaultInitData} = await loadFixture(fixture); - const currentContractFactory = new Multisig__factory(deployer); - const legacyContractFactory = new Multisig_V1_1__factory(deployer); - - const encodedDummyTarget = ethers.utils.defaultAbiCoder.encode( - ['address', 'uint8'], - [deployer.address, Operation.delegatecall] - ); + data[8] = 'initializeFrom'; + // @ts-ignore + data[9] = [latestInitializerVersion, encodedParamsForUpgrade]; const {proxy, fromImplementation, toImplementation} = await deployAndUpgradeFromToCheck( - deployer, - alice, - [dao.address, defaultInitData.members, defaultInitData.settings], - 'initialize', - legacyContractFactory, - currentContractFactory, - PLUGIN_UUPS_UPGRADEABLE_PERMISSIONS.UPGRADE_PLUGIN_PERMISSION_ID, - dao, - 'initializeFrom', - [latestInitializerVersion, encodedDummyTarget] + // @ts-ignore + ...data ); + expect(toImplementation).to.not.equal(fromImplementation); // The build did change const fromProtocolVersion = await getProtocolVersion( @@ -145,32 +110,65 @@ describe('Upgrades', () => { expect((await newMultisig.getTargetConfig()).operation).to.deep.equal( Operation.delegatecall ); + + // `initializeFrom` was called on the upgrade, make sure + // `initialize` can not be called. + await expect( + proxy.initialize( + dao.address, + defaultInitData.members, + defaultInitData.settings, + defaultInitData.targetConfig + ) + ).to.be.revertedWithCustomError(proxy, 'AlreadyInitialized'); }); - it('from v1.2 with initializeFrom', async () => { - const {deployer, alice, dao, defaultInitData} = await loadFixture(fixture); + it('from v1.3.0 with initializeFrom', async () => { + const {deployer, alice, dao, defaultInitData, encodedParamsForUpgrade} = + await loadFixture(fixture); const currentContractFactory = new Multisig__factory(deployer); - const legacyContractFactory = new Multisig_V1_2__factory(deployer); + const legacyContractFactory = new Multisig_V1_3_0__factory(deployer); - const encodedDummyTarget = ethers.utils.defaultAbiCoder.encode( - ['address', 'uint8'], - [deployer.address, Operation.delegatecall] - ); + const data = [ + deployer, + alice, + [dao.address, defaultInitData.members, defaultInitData.settings], + 'initialize', + legacyContractFactory, + currentContractFactory, + PLUGIN_UUPS_UPGRADEABLE_PERMISSIONS.UPGRADE_PLUGIN_PERMISSION_ID, + dao, + 'initialize', + [ + dao.address, + defaultInitData.members, + defaultInitData.settings, + defaultInitData.targetConfig, + ], + ]; + + // Ensure that on the `upgrade`, `initialize` can not be called. + try { + await deployAndUpgradeFromToCheck( + // @ts-ignore + ...data + ); + throw new Error(''); + } catch (err: any) { + expect(err.data).to.equal(AlreadyInitializedSignature); + } + + data[8] = 'initializeFrom'; + // @ts-ignore + data[9] = [latestInitializerVersion, encodedParamsForUpgrade]; const {proxy, fromImplementation, toImplementation} = await deployAndUpgradeFromToCheck( - deployer, - alice, - [dao.address, defaultInitData.members, defaultInitData.settings], - 'initialize', - legacyContractFactory, - currentContractFactory, - PLUGIN_UUPS_UPGRADEABLE_PERMISSIONS.UPGRADE_PLUGIN_PERMISSION_ID, - dao, - 'initializeFrom', - [latestInitializerVersion, encodedDummyTarget] + // @ts-ignore + ...data ); - expect(toImplementation).to.not.equal(fromImplementation); + + expect(toImplementation).to.not.equal(fromImplementation); // The build did change const fromProtocolVersion = await getProtocolVersion( legacyContractFactory.attach(fromImplementation) @@ -192,6 +190,16 @@ describe('Upgrades', () => { expect((await newMultisig.getTargetConfig()).operation).to.deep.equal( Operation.delegatecall ); + // `initializeFrom` was called on the upgrade, make sure + // `initialize` can not be called. + await expect( + proxy.initialize( + dao.address, + defaultInitData.members, + defaultInitData.settings, + defaultInitData.targetConfig + ) + ).to.be.revertedWithCustomError(proxy, 'AlreadyInitialized'); }); }); @@ -206,6 +214,7 @@ type FixtureResult = { targetConfig: TargetConfig; }; dao: DAO; + encodeDataForUpgrade: string; }; async function fixture(): Promise { @@ -228,6 +237,11 @@ async function fixture(): Promise { }, }; + const encodedParamsForUpgrade = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint8'], + [deployer.address, Operation.delegatecall] + ); + return { deployer, alice, @@ -235,5 +249,6 @@ async function fixture(): Promise { carol, dao, defaultInitData, + encodedParamsForUpgrade, }; } diff --git a/packages/contracts/test/test-utils/typechain-versions.ts b/packages/contracts/test/test-utils/typechain-versions.ts index 34988e62..e9f430bd 100644 --- a/packages/contracts/test/test-utils/typechain-versions.ts +++ b/packages/contracts/test/test-utils/typechain-versions.ts @@ -2,8 +2,8 @@ /// The version specified in src is the factory and contract without the version number. /// Import as needed in the test files, and use the correct version of the contract. -export {Multisig__factory as Multisig_V1_1__factory} from '../../typechain/factories/@aragon/osx-v1.0.0/plugins/governance/multisig/Multisig__factory'; -export {Multisig__factory as Multisig_V1_2__factory} from '../../typechain/factories/@aragon/osx-v1.3.0/plugins/governance/multisig/Multisig__factory'; +export {Multisig__factory as Multisig_V1_0_0__factory} from '../../typechain/factories/@aragon/osx-v1.0.0/plugins/governance/multisig/Multisig__factory'; +export {Multisig__factory as Multisig_V1_3_0__factory} from '../../typechain/factories/@aragon/osx-v1.3.0/plugins/governance/multisig/Multisig__factory'; export {Multisig__factory} from '../../typechain/factories/src/Multisig__factory'; export {Multisig} from '../../typechain/src/Multisig'; export {IMultisig} from '../../typechain/src/IMultisig'; diff --git a/packages/contracts/test/test-utils/uups-upgradeable.ts b/packages/contracts/test/test-utils/uups-upgradeable.ts index 458d5978..afc1dd5b 100644 --- a/packages/contracts/test/test-utils/uups-upgradeable.ts +++ b/packages/contracts/test/test-utils/uups-upgradeable.ts @@ -111,7 +111,7 @@ export async function deployAndUpgradeFromToCheck( toImplementation: string; }> { // Deploy proxy and implementation - const proxy = await upgrades.deployProxy( + let proxy = await upgrades.deployProxy( from.connect(deployer), Object.values(initArgs), { @@ -166,7 +166,7 @@ export async function deployAndUpgradeFromToCheck( } // Upgrade the proxy to a new implementation from a different factory - await upgrades.upgradeProxy(proxy.address, to.connect(upgrader), { + proxy = await upgrades.upgradeProxy(proxy.address, to.connect(upgrader), { unsafeAllow: ['constructor', 'delegatecall'], constructorArgs: [], call: call, From 4d68a56d368752071f7bc6ea2ddbec33c83258c2 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Sat, 28 Sep 2024 12:27:49 +0400 Subject: [PATCH 33/59] callinitialization from osx-commons --- packages/contracts/src/Multisig.sol | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index 1fa5cf3b..cabd5fb1 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -11,7 +11,7 @@ import {ProposalUpgradeable} from "@aragon/osx-commons-contracts/src/plugin/exte import {PluginUUPSUpgradeable} from "@aragon/osx-commons-contracts/src/plugin/PluginUUPSUpgradeable.sol"; import {IProposal} from "@aragon/osx-commons-contracts/src/plugin/extensions/proposal/IProposal.sol"; import {IDAO} from "@aragon/osx-commons-contracts/src/dao/IDAO.sol"; -import {IExecutor, Action} from "@aragon/osx-commons-contracts/src/executors/IExecutor.sol"; +import {Action} from "@aragon/osx-commons-contracts/src/executors/IExecutor.sol"; import {IMultisig} from "./IMultisig.sol"; @@ -103,9 +103,6 @@ contract Multisig is /// @param sender The sender address. error ProposalCreationForbidden(address sender); - /// @notice Thrown when initialize is called after it has already been executed. - error AlreadyInitialized(); - /// @notice Thrown if an approver is not allowed to cast an approve. This can be because the proposal /// - is not open, /// - was executed, or @@ -148,15 +145,6 @@ contract Multisig is /// @param minApprovals The minimum amount of approvals needed to pass a proposal. event MultisigSettingsUpdated(bool onlyListed, uint16 indexed minApprovals); - /// @notice This ensures that the initialize function cannot be called during the upgrade process. - modifier onlyCallAtInitialization() { - if (_getInitializedVersion() != 0) { - revert AlreadyInitialized(); - } - - _; - } - /// @notice Initializes Release 1, Build 2. /// @dev This method is required to support [ERC-1822](https://eips.ethereum.org/EIPS/eip-1822). /// @param _dao The IDAO interface of the associated DAO. From 056dbfa7ba369d9d091458e4cda7524d8e085cbe Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Sun, 29 Sep 2024 18:07:56 +0400 Subject: [PATCH 34/59] fix tohex --- packages/contracts/deploy/20_new_version/23_publish.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/contracts/deploy/20_new_version/23_publish.ts b/packages/contracts/deploy/20_new_version/23_publish.ts index 7c9510f9..a0807eac 100644 --- a/packages/contracts/deploy/20_new_version/23_publish.ts +++ b/packages/contracts/deploy/20_new_version/23_publish.ts @@ -64,6 +64,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { // Check build number const latestBuild = (await pluginRepo.buildCount(VERSION.release)).toNumber(); + if (VERSION.build < latestBuild) { throw Error( `Publishing with build number ${VERSION.build} is not possible. The latest build is ${latestBuild}. Aborting publication...` @@ -142,8 +143,12 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { createVersion: { _release: VERSION.release, _pluginSetup: setup.address, - _buildMetadata: toHex(buildMetadataURI), - _releaseMetadata: toHex(releaseMetadataURI), + _buildMetadata: ethers.utils.hexlify( + ethers.utils.toUtf8Bytes(buildMetadataURI) + ), + _releaseMetadata: ethers.utils.hexlify( + ethers.utils.toUtf8Bytes(releaseMetadataURI) + ), }, }, ], From 71de210472e84ac375a6e5a2023794e8e152dd73 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Mon, 7 Oct 2024 20:07:30 +0400 Subject: [PATCH 35/59] add metadata support --- packages/contracts/hardhat.config.ts | 1 - packages/contracts/src/Multisig.sol | 52 ++++++++++++++++++---- packages/contracts/src/MultisigSetup.sol | 24 ++++++++-- packages/contracts/src/build-metadata.json | 14 ++++++ 4 files changed, 77 insertions(+), 14 deletions(-) diff --git a/packages/contracts/hardhat.config.ts b/packages/contracts/hardhat.config.ts index ace6baa1..88a4fbff 100644 --- a/packages/contracts/hardhat.config.ts +++ b/packages/contracts/hardhat.config.ts @@ -158,7 +158,6 @@ const config: HardhatUserConfig = { solidity: { version: '0.8.17', settings: { - viaIR: true, metadata: { // Not including the metadata hash // https://github.com/paulrberg/hardhat-template/issues/31 diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index cabd5fb1..4bd05499 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -12,6 +12,7 @@ import {PluginUUPSUpgradeable} from "@aragon/osx-commons-contracts/src/plugin/Pl import {IProposal} from "@aragon/osx-commons-contracts/src/plugin/extensions/proposal/IProposal.sol"; import {IDAO} from "@aragon/osx-commons-contracts/src/dao/IDAO.sol"; import {Action} from "@aragon/osx-commons-contracts/src/executors/IExecutor.sol"; +import {MetadataExtensionUpgradeable} from "@aragon/osx-commons-contracts/src/utils/metadata/MetadataExtensionUpgradeable.sol"; import {IMultisig} from "./IMultisig.sol"; @@ -25,6 +26,7 @@ import {IMultisig} from "./IMultisig.sol"; contract Multisig is IMultisig, IMembership, + MetadataExtensionUpgradeable, PluginUUPSUpgradeable, ProposalUpgradeable, Addresslist @@ -154,7 +156,8 @@ contract Multisig is IDAO _dao, address[] calldata _members, MultisigSettings calldata _multisigSettings, - TargetConfig calldata _targetConfig + TargetConfig calldata _targetConfig, + bytes calldata _metadata ) external onlyCallAtInitialization reinitializer(2) { __PluginUUPSUpgradeable_init(_dao); @@ -166,6 +169,7 @@ contract Multisig is emit MembersAdded({members: _members}); _updateMultisigSettings(_multisigSettings); + _updateMetadata(_metadata); _setTargetConfig(_targetConfig); } @@ -176,8 +180,13 @@ contract Multisig is /// @param _initData The initialization data to be passed to via `upgradeToAndCall` (see [ERC-1967](https://docs.openzeppelin.com/contracts/4.x/api/proxy#ERC1967Upgrade)). function initializeFrom(uint16 _fromBuild, bytes calldata _initData) external reinitializer(2) { if (_fromBuild < 3) { - TargetConfig memory targetConfig = abi.decode(_initData, (TargetConfig)); + (TargetConfig memory targetConfig, bytes memory metadata) = abi.decode( + _initData, + (TargetConfig, bytes) + ); + _setTargetConfig(targetConfig); + _updateMetadata(metadata); } } @@ -186,7 +195,13 @@ contract Multisig is /// @return Returns `true` if the interface is supported. function supportsInterface( bytes4 _interfaceId - ) public view virtual override(PluginUUPSUpgradeable, ProposalUpgradeable) returns (bool) { + ) + public + view + virtual + override(MetadataExtensionUpgradeable, PluginUUPSUpgradeable, ProposalUpgradeable) + returns (bool) + { return _interfaceId == MULTISIG_INTERFACE_ID || _interfaceId == type(IMultisig).interfaceId || @@ -321,14 +336,13 @@ contract Multisig is approve(proposalId, _tryExecution); } - emit ProposalCreated( - proposalId, - _msgSender(), + _emitProposalCreatedEvent( + _actions, + _metadata, + _allowFailureMap, _startDate, _endDate, - _metadata, - _actions, - _allowFailureMap + proposalId ); } @@ -566,6 +580,26 @@ contract Multisig is }); } + /// @dev Helper function to avoid stack too deep. + function _emitProposalCreatedEvent( + Action[] memory _actions, + bytes memory _metadata, + uint256 _allowFailureMap, + uint64 _startDate, + uint64 _endDate, + uint256 _proposalId + ) private { + emit ProposalCreated( + _proposalId, + _msgSender(), + _startDate, + _endDate, + _metadata, + _actions, + _allowFailureMap + ); + } + /// @dev This empty reserved space is put in place to allow future versions to add new /// variables without shifting down storage in the inheritance chain. /// https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps diff --git a/packages/contracts/src/MultisigSetup.sol b/packages/contracts/src/MultisigSetup.sol index 5ff61e24..1d2735e7 100644 --- a/packages/contracts/src/MultisigSetup.sol +++ b/packages/contracts/src/MultisigSetup.sol @@ -39,6 +39,9 @@ contract MultisigSetup is PluginUpgradeableSetup { bytes32 public constant UPDATE_MULTISIG_SETTINGS_PERMISSION_ID = keccak256("UPDATE_MULTISIG_SETTINGS_PERMISSION"); + /// @notice Thrown when metadata's length is 0. + error EmptyMetadata(); + /// @notice The contract constructor, that deploys the `Multisig` plugin logic contract. constructor() PluginUpgradeableSetup(address(new Multisig())) {} @@ -51,17 +54,22 @@ contract MultisigSetup is PluginUpgradeableSetup { ( address[] memory members, Multisig.MultisigSettings memory multisigSettings, - PluginUUPSUpgradeable.TargetConfig memory targetConfig + PluginUUPSUpgradeable.TargetConfig memory targetConfig, + bytes memory metadata ) = abi.decode( _data, - (address[], Multisig.MultisigSettings, PluginUUPSUpgradeable.TargetConfig) + (address[], Multisig.MultisigSettings, PluginUUPSUpgradeable.TargetConfig, bytes) ); + if (metadata.length == 0) { + revert EmptyMetadata(); + } + // Deploy and initialize the plugin UUPS proxy. plugin = IMPLEMENTATION.deployUUPSProxy( abi.encodeCall( Multisig.initialize, - (IDAO(_dao), members, multisigSettings, targetConfig) + (IDAO(_dao), members, multisigSettings, targetConfig, metadata) ) ); @@ -124,8 +132,16 @@ contract MultisigSetup is PluginUpgradeableSetup { { (initData); - // todo: multisig never been upgraded right ? if (_fromBuild < 3) { + (, bytes memory metadata) = abi.decode( + _payload.data, + (PluginUUPSUpgradeable.TargetConfig, bytes) + ); + + if (metadata.length == 0) { + revert EmptyMetadata(); + } + address listedCheckCondition = address(new ListedCheckCondition(_payload.plugin)); PermissionLib.MultiTargetPermission[] diff --git a/packages/contracts/src/build-metadata.json b/packages/contracts/src/build-metadata.json index 644c41b8..755861a4 100644 --- a/packages/contracts/src/build-metadata.json +++ b/packages/contracts/src/build-metadata.json @@ -1,5 +1,7 @@ { "ui": {}, + "name": "multisig", + "description": "description", "change": "v1.3\n - Removed an unneccessary permission that allowed the Dao to upgrade the plugin, because this is supposed to happens as part of the update itself. The unnecessary permission, which was granted on installation of previous versions, will be automatically removed with the update to this version.\n", "pluginSetup": { "prepareInstallation": { @@ -50,6 +52,12 @@ "name": "TargetConfig", "type": "tuple", "description": "The initial target config" + }, + { + "internalType": "bytes", + "name": "metadata", + "type": "bytes", + "description": "The metadata that contains the information about the multisig." } ] }, @@ -84,6 +92,12 @@ "name": "TargetConfig", "type": "tuple", "description": "The initial target config" + }, + { + "internalType": "bytes", + "name": "metadata", + "type": "bytes", + "description": "The metadata that contains the information about the multisig." } ] } From 4a00b144acaf961aeee777584d07f4c14ceaab12 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Tue, 8 Oct 2024 10:29:31 +0400 Subject: [PATCH 36/59] metadata extension --- packages/contracts/src/MultisigSetup.sol | 13 ------ .../test/10_unit-testing/11_plugin.ts | 41 +++++++++++++------ .../test/10_unit-testing/12_plugin-setup.ts | 10 +++-- .../22_setup-processing.ts | 13 ++++-- .../31_upgradeability.ts | 27 +++++++----- 5 files changed, 61 insertions(+), 43 deletions(-) diff --git a/packages/contracts/src/MultisigSetup.sol b/packages/contracts/src/MultisigSetup.sol index 1d2735e7..ca2c59ea 100644 --- a/packages/contracts/src/MultisigSetup.sol +++ b/packages/contracts/src/MultisigSetup.sol @@ -61,10 +61,6 @@ contract MultisigSetup is PluginUpgradeableSetup { (address[], Multisig.MultisigSettings, PluginUUPSUpgradeable.TargetConfig, bytes) ); - if (metadata.length == 0) { - revert EmptyMetadata(); - } - // Deploy and initialize the plugin UUPS proxy. plugin = IMPLEMENTATION.deployUUPSProxy( abi.encodeCall( @@ -133,15 +129,6 @@ contract MultisigSetup is PluginUpgradeableSetup { (initData); if (_fromBuild < 3) { - (, bytes memory metadata) = abi.decode( - _payload.data, - (PluginUUPSUpgradeable.TargetConfig, bytes) - ); - - if (metadata.length == 0) { - revert EmptyMetadata(); - } - address listedCheckCondition = address(new ListedCheckCondition(_payload.plugin)); PermissionLib.MultiTargetPermission[] diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index 59c9f309..8e5ff9cf 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -65,6 +65,7 @@ type FixtureResult = { members: string[]; settings: Multisig.MultisigSettingsStruct; targetConfig: TargetConfig; + metadata: string; }; dao: DAO; dummyActions: DAOStructs.ActionStruct[]; @@ -95,6 +96,7 @@ async function fixture(): Promise { target: dao.address, operation: Operation.call, }, + metadata: '0x11', }; const pluginInitdata = pluginImplementation.interface.encodeFunctionData( 'initialize', @@ -103,6 +105,7 @@ async function fixture(): Promise { defaultInitData.members, defaultInitData.settings, defaultInitData.targetConfig, + defaultInitData.metadata, ] ); const deploymentTx1 = await proxyFactory.deployUUPSProxy(pluginInitdata); @@ -188,7 +191,8 @@ describe('Multisig', function () { dao.address, defaultInitData.members, defaultInitData.settings, - defaultInitData.targetConfig + defaultInitData.targetConfig, + defaultInitData.metadata ) ).to.be.revertedWithCustomError(initializedPlugin, 'AlreadyInitialized'); }); @@ -208,7 +212,8 @@ describe('Multisig', function () { dao.address, defaultInitData.members, defaultInitData.settings, - defaultInitData.targetConfig + defaultInitData.targetConfig, + defaultInitData.metadata ); // Check that all members from the init data have been listed as members. @@ -230,6 +235,13 @@ describe('Multisig', function () { ).to.be.eq(defaultInitData.settings.minApprovals); }); + it('sets the `metadata`', async () => { + const {initializedPlugin, defaultInitData} = await loadFixture(fixture); + expect(await initializedPlugin.getMetadata()).to.be.eq( + defaultInitData.metadata + ); + }); + it('sets `onlyListed`', async () => { const {initializedPlugin, defaultInitData} = await loadFixture(fixture); expect((await initializedPlugin.multisigSettings()).onlyListed).to.be.eq( @@ -246,7 +258,8 @@ describe('Multisig', function () { dao.address, defaultInitData.members, defaultInitData.settings, - defaultInitData.targetConfig + defaultInitData.targetConfig, + defaultInitData.metadata ) ) .to.emit(uninitializedPlugin, MULTISIG_EVENTS.MultisigSettingsUpdated) @@ -273,6 +286,7 @@ describe('Multisig', function () { overflowingMemberList, defaultInitData.settings, defaultInitData.targetConfig, + defaultInitData.metadata, { gasLimit: BigNumber.from(10).pow(8).toNumber(), } @@ -290,22 +304,22 @@ describe('Multisig', function () { it('reverts if trying to re-reinitializeFrom', async () => { const {uninitializedPlugin, deployer} = await loadFixture(fixture); - const encodedDummyTarget = ethers.utils.defaultAbiCoder.encode( - ['address', 'uint8'], - [deployer.address, Operation.delegatecall] + const encodedData = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint8', 'bytes'], + [deployer.address, Operation.delegatecall, '0x'] ); // reinitialize the plugin. await uninitializedPlugin.initializeFrom( latestInitializerVersion, - encodedDummyTarget + encodedData ); // Try to reinitialize the plugin. await expect( uninitializedPlugin.initializeFrom( latestInitializerVersion, - encodedDummyTarget + encodedData ) ).to.be.revertedWith('Initializable: contract is already initialized'); }); @@ -333,15 +347,15 @@ describe('Multisig', function () { it('sets the `_targetConfig` when initializing an uninitialized plugin', async () => { const {uninitializedPlugin, deployer} = await loadFixture(fixture); - const encodedDummyTarget = ethers.utils.defaultAbiCoder.encode( - ['address', 'uint8'], - [deployer.address, Operation.delegatecall] + const encodedData = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint8', 'bytes'], + [deployer.address, Operation.delegatecall, '0x'] ); // reinitialize the plugin. await uninitializedPlugin.initializeFrom( latestInitializerVersion, - encodedDummyTarget + encodedData ); expect((await uninitializedPlugin.getTargetConfig()).target).to.be.eq( @@ -995,7 +1009,8 @@ describe('Multisig', function () { onlyListed: true, minApprovals: 1, }, - defaultInitData.targetConfig + defaultInitData.targetConfig, + defaultInitData.metadata ); // Grant permissions between the DAO and the plugin. diff --git a/packages/contracts/test/10_unit-testing/12_plugin-setup.ts b/packages/contracts/test/10_unit-testing/12_plugin-setup.ts index 86109d43..8879fa11 100644 --- a/packages/contracts/test/10_unit-testing/12_plugin-setup.ts +++ b/packages/contracts/test/10_unit-testing/12_plugin-setup.ts @@ -62,6 +62,7 @@ async function fixture(): Promise { }; const defaultTargetConfig = {target: dao.address, operation: op.call}; + const defaultMetadata = '0x'; // Provide installation inputs const prepareInstallationInputs = ethers.utils.defaultAbiCoder.encode( @@ -72,6 +73,7 @@ async function fixture(): Promise { defaultMembers, Object.values(defaultMultisigSettings), defaultTargetConfig, + defaultMetadata, ] ); @@ -93,7 +95,7 @@ async function fixture(): Promise { getNamedTypesFromMetadata( METADATA.build.pluginSetup.prepareUpdate[3].inputs ), - [updateTargetConfig] + [updateTargetConfig, defaultMetadata] ); return { @@ -173,7 +175,7 @@ describe('MultisigSetup', function () { getNamedTypesFromMetadata( METADATA.build.pluginSetup.prepareInstallation.inputs ), - [noMembers, defaultMultisigSettings, defaultTargetConfig] + [noMembers, defaultMultisigSettings, defaultTargetConfig, '0x'] ); // Anticipate the plugin proxy address that will be deployed. @@ -215,7 +217,7 @@ describe('MultisigSetup', function () { getNamedTypesFromMetadata( METADATA.build.pluginSetup.prepareInstallation.inputs ), - [members, multisigSettings, defaultTargetConfig] + [members, multisigSettings, defaultTargetConfig, '0x'] ); // Anticipate the plugin proxy address that will be deployed. @@ -258,7 +260,7 @@ describe('MultisigSetup', function () { getNamedTypesFromMetadata( METADATA.build.pluginSetup.prepareInstallation.inputs ), - [members, multisigSettings, defaultTargetConfig] + [members, multisigSettings, defaultTargetConfig, '0x'] ); // Anticipate the plugin proxy address that will be deployed. diff --git a/packages/contracts/test/20_integration-testing/22_setup-processing.ts b/packages/contracts/test/20_integration-testing/22_setup-processing.ts index 49335c5e..51e3d67a 100644 --- a/packages/contracts/test/20_integration-testing/22_setup-processing.ts +++ b/packages/contracts/test/20_integration-testing/22_setup-processing.ts @@ -47,6 +47,7 @@ type FixtureResult = { members: string[]; settings: Multisig.MultisigSettingsStruct; targetConfig: TargetConfig; + metadata: string; }; psp: PluginSetupProcessor; pluginRepo: PluginRepo; @@ -100,6 +101,7 @@ async function fixture(): Promise { target: dao.address, operation: Operation.call, }, + metadata: '0x', }; const pluginSetupRefLatestBuild = { @@ -170,7 +172,12 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio getNamedTypesFromMetadata( METADATA.build.pluginSetup.prepareInstallation.inputs ), - [initialMembers, multisigSettings, defaultInitData.targetConfig] + [ + initialMembers, + multisigSettings, + defaultInitData.targetConfig, + defaultInitData.metadata, + ] ) ); @@ -217,7 +224,7 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio pluginSetupRefLatestBuild, 1, [defaultInitData.members, Object.values(defaultInitData.settings)], - [defaultInitData.targetConfig], + [defaultInitData.targetConfig, defaultInitData.metadata], latestInitializerVersion ); }); @@ -240,7 +247,7 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio pluginSetupRefLatestBuild, 2, [defaultInitData.members, Object.values(defaultInitData.settings)], - [defaultInitData.targetConfig], + [defaultInitData.targetConfig, defaultInitData.metadata], latestInitializerVersion ); }); diff --git a/packages/contracts/test/30_regression-testing/31_upgradeability.ts b/packages/contracts/test/30_regression-testing/31_upgradeability.ts index 7f50e671..debb2dd5 100644 --- a/packages/contracts/test/30_regression-testing/31_upgradeability.ts +++ b/packages/contracts/test/30_regression-testing/31_upgradeability.ts @@ -35,6 +35,7 @@ describe('Upgrades', () => { defaultInitData.members, defaultInitData.settings, defaultInitData.targetConfig, + defaultInitData.metadata, ], 'initialize', currentContractFactory, @@ -44,7 +45,7 @@ describe('Upgrades', () => { }); it('upgrades from v1.0.0 with initializeFrom', async () => { - const {deployer, alice, dao, defaultInitData, encodedParamsForUpgrade} = + const {deployer, alice, dao, defaultInitData, encodeDataForUpgrade} = await loadFixture(fixture); const currentContractFactory = new Multisig__factory(deployer); const legacyContractFactory = new Multisig_V1_0_0__factory(deployer); @@ -64,6 +65,7 @@ describe('Upgrades', () => { defaultInitData.members, defaultInitData.settings, defaultInitData.targetConfig, + defaultInitData.metadata, ], ]; @@ -80,7 +82,7 @@ describe('Upgrades', () => { data[8] = 'initializeFrom'; // @ts-ignore - data[9] = [latestInitializerVersion, encodedParamsForUpgrade]; + data[9] = [latestInitializerVersion, encodeDataForUpgrade]; const {proxy, fromImplementation, toImplementation} = await deployAndUpgradeFromToCheck( @@ -118,13 +120,14 @@ describe('Upgrades', () => { dao.address, defaultInitData.members, defaultInitData.settings, - defaultInitData.targetConfig + defaultInitData.targetConfig, + defaultInitData.metadata ) ).to.be.revertedWithCustomError(proxy, 'AlreadyInitialized'); }); it('from v1.3.0 with initializeFrom', async () => { - const {deployer, alice, dao, defaultInitData, encodedParamsForUpgrade} = + const {deployer, alice, dao, defaultInitData, encodeDataForUpgrade} = await loadFixture(fixture); const currentContractFactory = new Multisig__factory(deployer); const legacyContractFactory = new Multisig_V1_3_0__factory(deployer); @@ -144,6 +147,7 @@ describe('Upgrades', () => { defaultInitData.members, defaultInitData.settings, defaultInitData.targetConfig, + defaultInitData.metadata, ], ]; @@ -160,7 +164,7 @@ describe('Upgrades', () => { data[8] = 'initializeFrom'; // @ts-ignore - data[9] = [latestInitializerVersion, encodedParamsForUpgrade]; + data[9] = [latestInitializerVersion, encodeDataForUpgrade]; const {proxy, fromImplementation, toImplementation} = await deployAndUpgradeFromToCheck( @@ -197,7 +201,8 @@ describe('Upgrades', () => { dao.address, defaultInitData.members, defaultInitData.settings, - defaultInitData.targetConfig + defaultInitData.targetConfig, + defaultInitData.metadata ) ).to.be.revertedWithCustomError(proxy, 'AlreadyInitialized'); }); @@ -212,6 +217,7 @@ type FixtureResult = { members: string[]; settings: Multisig.MultisigSettingsStruct; targetConfig: TargetConfig; + metadata: string; }; dao: DAO; encodeDataForUpgrade: string; @@ -235,11 +241,12 @@ async function fixture(): Promise { target: dao.address, operation: Operation.call, }, + metadata: '0x', }; - const encodedParamsForUpgrade = ethers.utils.defaultAbiCoder.encode( - ['address', 'uint8'], - [deployer.address, Operation.delegatecall] + const encodeDataForUpgrade = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint8', 'bytes'], + [deployer.address, Operation.delegatecall, '0x'] ); return { @@ -249,6 +256,6 @@ async function fixture(): Promise { carol, dao, defaultInitData, - encodedParamsForUpgrade, + encodeDataForUpgrade, }; } From 832eafd75acd7bbd382c1add165bc415d39788f1 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Tue, 8 Oct 2024 13:23:06 +0400 Subject: [PATCH 37/59] Update packages/contracts/src/MultisigSetup.sol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jør∂¡ <4456749+brickpop@users.noreply.github.com> --- packages/contracts/src/MultisigSetup.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/contracts/src/MultisigSetup.sol b/packages/contracts/src/MultisigSetup.sol index ca2c59ea..fec25ae2 100644 --- a/packages/contracts/src/MultisigSetup.sol +++ b/packages/contracts/src/MultisigSetup.sol @@ -39,9 +39,6 @@ contract MultisigSetup is PluginUpgradeableSetup { bytes32 public constant UPDATE_MULTISIG_SETTINGS_PERMISSION_ID = keccak256("UPDATE_MULTISIG_SETTINGS_PERMISSION"); - /// @notice Thrown when metadata's length is 0. - error EmptyMetadata(); - /// @notice The contract constructor, that deploys the `Multisig` plugin logic contract. constructor() PluginUpgradeableSetup(address(new Multisig())) {} From d095f553ccae7ffdaacf1359312d634fa6fa3ed8 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Tue, 8 Oct 2024 13:24:06 +0400 Subject: [PATCH 38/59] Update packages/contracts/src/Multisig.sol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jør∂¡ <4456749+brickpop@users.noreply.github.com> --- packages/contracts/src/Multisig.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index 4bd05499..f9144e96 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -180,13 +180,13 @@ contract Multisig is /// @param _initData The initialization data to be passed to via `upgradeToAndCall` (see [ERC-1967](https://docs.openzeppelin.com/contracts/4.x/api/proxy#ERC1967Upgrade)). function initializeFrom(uint16 _fromBuild, bytes calldata _initData) external reinitializer(2) { if (_fromBuild < 3) { - (TargetConfig memory targetConfig, bytes memory metadata) = abi.decode( + (TargetConfig memory targetConfig, bytes memory pluginMetadata) = abi.decode( _initData, (TargetConfig, bytes) ); _setTargetConfig(targetConfig); - _updateMetadata(metadata); + _updateMetadata(pluginMetadata); } } From 032ea3c8a3129b0621c004c6cf182bbec8dc64c5 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Tue, 8 Oct 2024 13:24:13 +0400 Subject: [PATCH 39/59] Update packages/contracts/src/Multisig.sol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jør∂¡ <4456749+brickpop@users.noreply.github.com> --- packages/contracts/src/Multisig.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index f9144e96..e67fba43 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -169,7 +169,7 @@ contract Multisig is emit MembersAdded({members: _members}); _updateMultisigSettings(_multisigSettings); - _updateMetadata(_metadata); + _updateMetadata(_pluginMetadata); _setTargetConfig(_targetConfig); } From 8e66bfa5537e7ad16410b3389dc277b820c5dd70 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Tue, 8 Oct 2024 13:24:19 +0400 Subject: [PATCH 40/59] Update packages/contracts/src/Multisig.sol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jør∂¡ <4456749+brickpop@users.noreply.github.com> --- packages/contracts/src/Multisig.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index e67fba43..8b73b616 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -157,7 +157,7 @@ contract Multisig is address[] calldata _members, MultisigSettings calldata _multisigSettings, TargetConfig calldata _targetConfig, - bytes calldata _metadata + bytes calldata _pluginMetadata ) external onlyCallAtInitialization reinitializer(2) { __PluginUUPSUpgradeable_init(_dao); From fc1381dfc584a661598190a724b1b226a063d3e5 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Tue, 8 Oct 2024 13:53:28 +0400 Subject: [PATCH 41/59] permission added --- packages/contracts/src/MultisigSetup.sol | 35 ++++++++++++++++-- .../test/10_unit-testing/12_plugin-setup.ts | 37 +++++++++++++++++-- packages/contracts/test/multisig-constants.ts | 4 ++ 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/packages/contracts/src/MultisigSetup.sol b/packages/contracts/src/MultisigSetup.sol index fec25ae2..87b4f135 100644 --- a/packages/contracts/src/MultisigSetup.sol +++ b/packages/contracts/src/MultisigSetup.sol @@ -35,6 +35,9 @@ contract MultisigSetup is PluginUpgradeableSetup { bytes32 public constant SET_TARGET_CONFIG_PERMISSION_ID = keccak256("SET_TARGET_CONFIG_PERMISSION"); + /// @notice The ID of the permission required to call the `updateMetadata` function. + bytes32 public constant UPDATE_METADATA_PERMISSION_ID = keccak256("UPDATE_METADATA_PERMISSION"); + /// @notice The ID of the permission required to call the `updateMultisigSettings` function. bytes32 public constant UPDATE_MULTISIG_SETTINGS_PERMISSION_ID = keccak256("UPDATE_MULTISIG_SETTINGS_PERMISSION"); @@ -70,7 +73,7 @@ contract MultisigSetup is PluginUpgradeableSetup { // Prepare permissions PermissionLib.MultiTargetPermission[] - memory permissions = new PermissionLib.MultiTargetPermission[](4); + memory permissions = new PermissionLib.MultiTargetPermission[](5); // Set permissions to be granted. // Grant the list of permissions of the plugin to the DAO. @@ -106,6 +109,14 @@ contract MultisigSetup is PluginUpgradeableSetup { permissionId: SET_TARGET_CONFIG_PERMISSION_ID }); + permissions[4] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Grant, + where: plugin, + who: _dao, + condition: PermissionLib.NO_CONDITION, + permissionId: UPDATE_METADATA_PERMISSION_ID + }); + preparedSetupData.permissions = permissions; preparedSetupData.helpers = new address[](1); @@ -129,7 +140,7 @@ contract MultisigSetup is PluginUpgradeableSetup { address listedCheckCondition = address(new ListedCheckCondition(_payload.plugin)); PermissionLib.MultiTargetPermission[] - memory permissions = new PermissionLib.MultiTargetPermission[](3); + memory permissions = new PermissionLib.MultiTargetPermission[](4); permissions[0] = PermissionLib.MultiTargetPermission({ operation: PermissionLib.Operation.Revoke, @@ -155,6 +166,14 @@ contract MultisigSetup is PluginUpgradeableSetup { permissionId: SET_TARGET_CONFIG_PERMISSION_ID }); + permissions[3] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Grant, + where: _payload.plugin, + who: _dao, + condition: PermissionLib.NO_CONDITION, + permissionId: UPDATE_METADATA_PERMISSION_ID + }); + preparedSetupData.permissions = permissions; preparedSetupData.helpers = new address[](1); @@ -170,7 +189,7 @@ contract MultisigSetup is PluginUpgradeableSetup { SetupPayload calldata _payload ) external view returns (PermissionLib.MultiTargetPermission[] memory permissions) { // Prepare permissions - permissions = new PermissionLib.MultiTargetPermission[](4); + permissions = new PermissionLib.MultiTargetPermission[](5); // Set permissions to be Revoked. permissions[0] = PermissionLib.MultiTargetPermission({ @@ -197,7 +216,15 @@ contract MultisigSetup is PluginUpgradeableSetup { permissionId: SET_TARGET_CONFIG_PERMISSION_ID }); - permissions[3] = PermissionLib.MultiTargetPermission( + permissions[3] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Revoke, + where: _payload.plugin, + who: _dao, + condition: PermissionLib.NO_CONDITION, + permissionId: UPDATE_METADATA_PERMISSION_ID + }); + + permissions[4] = PermissionLib.MultiTargetPermission( PermissionLib.Operation.Revoke, _payload.plugin, address(type(uint160).max), // ANY_ADDR diff --git a/packages/contracts/test/10_unit-testing/12_plugin-setup.ts b/packages/contracts/test/10_unit-testing/12_plugin-setup.ts index 8879fa11..d97b78d2 100644 --- a/packages/contracts/test/10_unit-testing/12_plugin-setup.ts +++ b/packages/contracts/test/10_unit-testing/12_plugin-setup.ts @@ -7,6 +7,7 @@ import { MULTISIG_INTERFACE, SET_TARGET_CONFIG_PERMISSION_ID, TargetConfig, + UPDATE_METADATA_PERMISSION_ID, UPDATE_MULTISIG_SETTINGS_PERMISSION_ID, UPGRADE_PLUGIN_PERMISSION_ID, } from '../multisig-constants'; @@ -314,7 +315,7 @@ describe('MultisigSetup', function () { // Check the return data. expect(plugin).to.be.equal(anticipatedPluginAddress); expect(helpers.length).to.be.equal(1); - expect(permissions.length).to.be.equal(4); + expect(permissions.length).to.be.equal(5); const condition = helpers[0]; @@ -347,6 +348,13 @@ describe('MultisigSetup', function () { AddressZero, SET_TARGET_CONFIG_PERMISSION_ID, ], + [ + Operation.Grant, + plugin, + dao.address, + AddressZero, + UPDATE_METADATA_PERMISSION_ID, + ], ]); }); @@ -416,7 +424,7 @@ describe('MultisigSetup', function () { [1, prepareUpdateBuild3Inputs] ) ); - expect(permissions.length).to.be.equal(3); + expect(permissions.length).to.be.equal(4); expect(helpers.length).to.be.equal(1); // check correct permission is revoked expect(permissions).to.deep.equal([ @@ -441,6 +449,13 @@ describe('MultisigSetup', function () { AddressZero, SET_TARGET_CONFIG_PERMISSION_ID, ], + [ + Operation.Grant, + plugin, + dao.address, + AddressZero, + UPDATE_METADATA_PERMISSION_ID, + ], ]); }); @@ -470,7 +485,7 @@ describe('MultisigSetup', function () { [2, prepareUpdateBuild3Inputs] ) ); - expect(permissions.length).to.be.equal(3); + expect(permissions.length).to.be.equal(4); expect(helpers.length).to.be.equal(1); // check correct permission is revoked expect(permissions).to.deep.equal([ @@ -495,6 +510,13 @@ describe('MultisigSetup', function () { AddressZero, SET_TARGET_CONFIG_PERMISSION_ID, ], + [ + Operation.Grant, + plugin, + dao.address, + AddressZero, + UPDATE_METADATA_PERMISSION_ID, + ], ]); }); }); @@ -520,7 +542,7 @@ describe('MultisigSetup', function () { ); // Check the return data. - expect(permissions.length).to.be.equal(4); + expect(permissions.length).to.be.equal(5); expect(permissions).to.deep.equal([ [ Operation.Revoke, @@ -543,6 +565,13 @@ describe('MultisigSetup', function () { AddressZero, SET_TARGET_CONFIG_PERMISSION_ID, ], + [ + Operation.Revoke, + plugin, + dao.address, + AddressZero, + UPDATE_METADATA_PERMISSION_ID, + ], [ Operation.Revoke, plugin, diff --git a/packages/contracts/test/multisig-constants.ts b/packages/contracts/test/multisig-constants.ts index f2f3f4e0..0b245d8a 100644 --- a/packages/contracts/test/multisig-constants.ts +++ b/packages/contracts/test/multisig-constants.ts @@ -27,6 +27,10 @@ export const SET_TARGET_CONFIG_PERMISSION_ID = ethers.utils.id( 'SET_TARGET_CONFIG_PERMISSION' ); +export const UPDATE_METADATA_PERMISSION_ID = ethers.utils.id( + 'UPDATE_METADATA_PERMISSION' +); + export const CREATE_PROPOSAL_SIGNATURE = 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)'; From 732bf4244c0e15a057be5549392a98f5d3dfc8af Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Tue, 8 Oct 2024 13:54:54 +0400 Subject: [PATCH 42/59] rename --- packages/contracts/src/MultisigSetup.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/MultisigSetup.sol b/packages/contracts/src/MultisigSetup.sol index 87b4f135..0b5b2089 100644 --- a/packages/contracts/src/MultisigSetup.sol +++ b/packages/contracts/src/MultisigSetup.sol @@ -55,7 +55,7 @@ contract MultisigSetup is PluginUpgradeableSetup { address[] memory members, Multisig.MultisigSettings memory multisigSettings, PluginUUPSUpgradeable.TargetConfig memory targetConfig, - bytes memory metadata + bytes memory pluginMetadata ) = abi.decode( _data, (address[], Multisig.MultisigSettings, PluginUUPSUpgradeable.TargetConfig, bytes) @@ -65,7 +65,7 @@ contract MultisigSetup is PluginUpgradeableSetup { plugin = IMPLEMENTATION.deployUUPSProxy( abi.encodeCall( Multisig.initialize, - (IDAO(_dao), members, multisigSettings, targetConfig, metadata) + (IDAO(_dao), members, multisigSettings, targetConfig, pluginMetadata) ) ); From 3cea0c52e8dd5458e522848bfb9146731f4cfa73 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Wed, 9 Oct 2024 17:23:12 +0400 Subject: [PATCH 43/59] metadata rename --- packages/contracts/src/Multisig.sol | 4 ++-- packages/contracts/src/MultisigSetup.sol | 19 ++++++++----------- .../test/10_unit-testing/12_plugin-setup.ts | 10 +++++----- packages/contracts/test/multisig-constants.ts | 4 ++-- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index 8b73b616..2b7155df 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -169,7 +169,7 @@ contract Multisig is emit MembersAdded({members: _members}); _updateMultisigSettings(_multisigSettings); - _updateMetadata(_pluginMetadata); + _setMetadata(_pluginMetadata); _setTargetConfig(_targetConfig); } @@ -186,7 +186,7 @@ contract Multisig is ); _setTargetConfig(targetConfig); - _updateMetadata(pluginMetadata); + _setMetadata(pluginMetadata); } } diff --git a/packages/contracts/src/MultisigSetup.sol b/packages/contracts/src/MultisigSetup.sol index 0b5b2089..80e997c5 100644 --- a/packages/contracts/src/MultisigSetup.sol +++ b/packages/contracts/src/MultisigSetup.sol @@ -6,7 +6,7 @@ pragma solidity ^0.8.8; import {PermissionLib} from "@aragon/osx-commons-contracts/src/permission/PermissionLib.sol"; import {IPluginSetup} from "@aragon/osx-commons-contracts/src/plugin/setup/IPluginSetup.sol"; import {PluginUpgradeableSetup} from "@aragon/osx-commons-contracts/src/plugin/setup/PluginUpgradeableSetup.sol"; -import {PluginUUPSUpgradeable} from "@aragon/osx-commons-contracts/src/plugin/PluginUUPSUpgradeable.sol"; +import {IPlugin} from "@aragon/osx-commons-contracts/src/plugin/IPlugin.sol"; import {ProxyLib} from "@aragon/osx-commons-contracts/src/utils/deployment/ProxyLib.sol"; import {IDAO} from "@aragon/osx-commons-contracts/src/dao/IDAO.sol"; @@ -35,8 +35,8 @@ contract MultisigSetup is PluginUpgradeableSetup { bytes32 public constant SET_TARGET_CONFIG_PERMISSION_ID = keccak256("SET_TARGET_CONFIG_PERMISSION"); - /// @notice The ID of the permission required to call the `updateMetadata` function. - bytes32 public constant UPDATE_METADATA_PERMISSION_ID = keccak256("UPDATE_METADATA_PERMISSION"); + /// @notice The ID of the permission required to call the `setMetadata` function. + bytes32 public constant SET_METADATA_PERMISSION_ID = keccak256("SET_METADATA_PERMISSION"); /// @notice The ID of the permission required to call the `updateMultisigSettings` function. bytes32 public constant UPDATE_MULTISIG_SETTINGS_PERMISSION_ID = @@ -54,12 +54,9 @@ contract MultisigSetup is PluginUpgradeableSetup { ( address[] memory members, Multisig.MultisigSettings memory multisigSettings, - PluginUUPSUpgradeable.TargetConfig memory targetConfig, + IPlugin.TargetConfig memory targetConfig, bytes memory pluginMetadata - ) = abi.decode( - _data, - (address[], Multisig.MultisigSettings, PluginUUPSUpgradeable.TargetConfig, bytes) - ); + ) = abi.decode(_data, (address[], Multisig.MultisigSettings, IPlugin.TargetConfig, bytes)); // Deploy and initialize the plugin UUPS proxy. plugin = IMPLEMENTATION.deployUUPSProxy( @@ -114,7 +111,7 @@ contract MultisigSetup is PluginUpgradeableSetup { where: plugin, who: _dao, condition: PermissionLib.NO_CONDITION, - permissionId: UPDATE_METADATA_PERMISSION_ID + permissionId: SET_METADATA_PERMISSION_ID }); preparedSetupData.permissions = permissions; @@ -171,7 +168,7 @@ contract MultisigSetup is PluginUpgradeableSetup { where: _payload.plugin, who: _dao, condition: PermissionLib.NO_CONDITION, - permissionId: UPDATE_METADATA_PERMISSION_ID + permissionId: SET_METADATA_PERMISSION_ID }); preparedSetupData.permissions = permissions; @@ -221,7 +218,7 @@ contract MultisigSetup is PluginUpgradeableSetup { where: _payload.plugin, who: _dao, condition: PermissionLib.NO_CONDITION, - permissionId: UPDATE_METADATA_PERMISSION_ID + permissionId: SET_METADATA_PERMISSION_ID }); permissions[4] = PermissionLib.MultiTargetPermission( diff --git a/packages/contracts/test/10_unit-testing/12_plugin-setup.ts b/packages/contracts/test/10_unit-testing/12_plugin-setup.ts index d97b78d2..2ceafd9a 100644 --- a/packages/contracts/test/10_unit-testing/12_plugin-setup.ts +++ b/packages/contracts/test/10_unit-testing/12_plugin-setup.ts @@ -7,7 +7,7 @@ import { MULTISIG_INTERFACE, SET_TARGET_CONFIG_PERMISSION_ID, TargetConfig, - UPDATE_METADATA_PERMISSION_ID, + SET_METADATA_PERMISSION_ID, UPDATE_MULTISIG_SETTINGS_PERMISSION_ID, UPGRADE_PLUGIN_PERMISSION_ID, } from '../multisig-constants'; @@ -353,7 +353,7 @@ describe('MultisigSetup', function () { plugin, dao.address, AddressZero, - UPDATE_METADATA_PERMISSION_ID, + SET_METADATA_PERMISSION_ID, ], ]); }); @@ -454,7 +454,7 @@ describe('MultisigSetup', function () { plugin, dao.address, AddressZero, - UPDATE_METADATA_PERMISSION_ID, + SET_METADATA_PERMISSION_ID, ], ]); }); @@ -515,7 +515,7 @@ describe('MultisigSetup', function () { plugin, dao.address, AddressZero, - UPDATE_METADATA_PERMISSION_ID, + SET_METADATA_PERMISSION_ID, ], ]); }); @@ -570,7 +570,7 @@ describe('MultisigSetup', function () { plugin, dao.address, AddressZero, - UPDATE_METADATA_PERMISSION_ID, + SET_METADATA_PERMISSION_ID, ], [ Operation.Revoke, diff --git a/packages/contracts/test/multisig-constants.ts b/packages/contracts/test/multisig-constants.ts index 0b245d8a..6a958910 100644 --- a/packages/contracts/test/multisig-constants.ts +++ b/packages/contracts/test/multisig-constants.ts @@ -27,8 +27,8 @@ export const SET_TARGET_CONFIG_PERMISSION_ID = ethers.utils.id( 'SET_TARGET_CONFIG_PERMISSION' ); -export const UPDATE_METADATA_PERMISSION_ID = ethers.utils.id( - 'UPDATE_METADATA_PERMISSION' +export const SET_METADATA_PERMISSION_ID = ethers.utils.id( + 'SET_METADATA_PERMISSION' ); export const CREATE_PROPOSAL_SIGNATURE = From 2141a952ca84696f27a81d516679b162eed3e74b Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Mon, 14 Oct 2024 12:46:11 +0400 Subject: [PATCH 44/59] fix contracts and test (#25) * fix contracts and test * change * createproposal id include actions * rename --- packages/contracts/src/Multisig.sol | 23 +- .../src/mocks/CustomExecutorMock.sol | 2 +- .../test/10_unit-testing/11_plugin.ts | 258 ++++++++++++++---- 3 files changed, 208 insertions(+), 75 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index 2b7155df..6821fe68 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -302,7 +302,7 @@ contract Multisig is revert DateOutOfBounds({limit: _startDate, actual: _endDate}); } - proposalId = createProposalId(_actions, _metadata); + proposalId = _createProposalId(keccak256(abi.encode(_actions, _metadata))); // Create the proposal Proposal storage proposal_ = proposals[proposalId]; @@ -378,7 +378,7 @@ contract Multisig is } /// @inheritdoc IProposal - function createProposalParamsABI() external pure override returns (string memory) { + function customProposalParamsABI() external pure override returns (string memory) { return "(uint256 allowFailureMap, bool approveProposal, bool tryExecution)"; } @@ -468,25 +468,6 @@ contract Multisig is return isListed(_account); } - /// @notice Hashing function used to (re)build the proposal id from the proposal details.. - /// @dev The proposal id is produced by hashing the ABI encoded `targets` array, the `values` array, the `calldatas` array - /// and the descriptionHash (bytes32 which itself is the keccak256 hash of the description string). This proposal id - /// can be produced from the proposal data which is part of the {ProposalCreated} event. It can even be computed in - /// advance, before the proposal is submitted. - /// The chainId and the governor address are not part of the proposal id computation. Consequently, the - /// same proposal (with same operation and same description) will have the same id if submitted on multiple governors - /// across multiple networks. This also means that in order to execute the same operation twice (on the same - /// governor) the proposer will have to change the description in order to avoid proposal id conflicts. - /// @param _actions The actions that will be executed after the proposal passes. - /// @param _metadata The metadata of the proposal. - /// @return proposalId The ID of the proposal. - function createProposalId( - Action[] calldata _actions, - bytes memory _metadata - ) public pure override returns (uint256) { - return uint256(keccak256(abi.encode(_actions, _metadata))); - } - /// @notice Internal function to execute a vote. It assumes the queried proposal exists. /// @param _proposalId The ID of the proposal. function _execute(uint256 _proposalId) internal { diff --git a/packages/contracts/src/mocks/CustomExecutorMock.sol b/packages/contracts/src/mocks/CustomExecutorMock.sol index e59fa539..31986842 100644 --- a/packages/contracts/src/mocks/CustomExecutorMock.sol +++ b/packages/contracts/src/mocks/CustomExecutorMock.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.8; -import {IExecutor, Action} from "@aragon/osx-commons-contracts/src/executors/IExecutor.sol"; +import {Action} from "@aragon/osx-commons-contracts/src/executors/IExecutor.sol"; /// @dev DO NOT USE IN PRODUCTION! contract CustomExecutorMock { diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index 8e5ff9cf..ab323fde 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -46,10 +46,12 @@ import { DAO__factory, PluginUUPSUpgradeableV1Mock__factory, } from '@aragon/osx-ethers'; +import {defaultAbiCoder} from '@ethersproject/abi'; import {loadFixture, time} from '@nomicfoundation/hardhat-network-helpers'; import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; import {expect} from 'chai'; import {BigNumber} from 'ethers'; +import {keccak256} from 'ethers/lib/utils'; import {ethers} from 'hardhat'; type FixtureResult = { @@ -72,6 +74,30 @@ type FixtureResult = { dummyMetadata: string; }; +let chainId: number; + +async function createProposalId( + pluginAddress: string, + actions: DAOStructs.ActionStruct[], + metadata: string +): Promise { + const blockNumber = (await ethers.provider.getBlock('latest')).number; + const salt = keccak256( + defaultAbiCoder.encode( + ['tuple(address to,uint256 value,bytes data)[]', 'bytes'], + [actions, metadata] + ) + ); + return BigNumber.from( + keccak256( + defaultAbiCoder.encode( + ['uint256', 'uint256', 'address', 'bytes32'], + [chainId, blockNumber + 1, pluginAddress, salt] + ) + ) + ); +} + async function fixture(): Promise { const [deployer, alice, bob, carol, dave, eve] = await ethers.getSigners(); @@ -179,6 +205,9 @@ async function loadFixtureAndGrantCreatePermission(): Promise { } describe('Multisig', function () { + before(async () => { + chainId = (await ethers.provider.getNetwork()).chainId; + }); describe('initialize', async () => { it('reverts if trying to re-initialize', async () => { const {dao, initializedPlugin, defaultInitData} = await loadFixture( @@ -816,6 +845,12 @@ describe('Multisig', function () { [1, true, true] ); + const proposalId = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin .connect(alice) [CREATE_PROPOSAL_SIGNATURE_IProposal]( @@ -826,10 +861,6 @@ describe('Multisig', function () { encodedParam ); - const proposalId = await plugin.createProposalId( - dummyActions, - dummyMetadata - ); const proposal = await plugin.getProposal(proposalId); expect(proposal.allowFailureMap).to.equal(1); @@ -845,6 +876,12 @@ describe('Multisig', function () { initializedPlugin: plugin, } = data; + const proposalId = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin .connect(alice) [CREATE_PROPOSAL_SIGNATURE_IProposal]( @@ -855,11 +892,6 @@ describe('Multisig', function () { '0x' ); - const proposalId = await plugin.createProposalId( - dummyActions, - dummyMetadata - ); - const proposal = await plugin.getProposal(proposalId); expect(proposal.actions.length).to.equal(dummyActions.length); expect(proposal.allowFailureMap).to.equal(0); @@ -908,7 +940,11 @@ describe('Multisig', function () { defaultInitData, } = data; - const proposalId = plugin.createProposalId(dummyActions, dummyMetadata); + const proposalId = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); // Create a proposal as Alice. const endDate = (await time.latest()) + TIME.HOUR; @@ -939,7 +975,8 @@ describe('Multisig', function () { // Create a proposal as Alice and check the event arguments. const startDate = (await time.latest()) + TIME.MINUTE; const endDate = startDate + TIME.HOUR; - const expectedProposalId = await plugin.createProposalId( + const expectedProposalId = await createProposalId( + plugin.address, [], dummyMetadata ); @@ -1100,7 +1137,8 @@ describe('Multisig', function () { const startDate = (await time.latest()) + TIME.MINUTE; const endDate = startDate + TIME.HOUR; - const expectedProposalId = await plugin.createProposalId( + const expectedProposalId = await createProposalId( + plugin.address, [], dummyMetadata ); @@ -1232,7 +1270,11 @@ describe('Multisig', function () { 0, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); // Check the listed members before the block is mined. expect(await plugin.isListed(carol.address)).to.equal(true); @@ -1286,7 +1328,7 @@ describe('Multisig', function () { const endDate = startDate + TIME.HOUR; const allowFailureMap = 0; await time.setNextBlockTimestamp(startDate); - const id = await plugin.createProposalId([], dummyMetadata); + const id = await createProposalId(plugin.address, [], dummyMetadata); const approveProposal = false; @@ -1344,7 +1386,7 @@ describe('Multisig', function () { await time.setNextBlockTimestamp(startDate); - const id = await plugin.createProposalId([], dummyMetadata); + const id = await createProposalId(plugin.address, [], dummyMetadata); await expect( plugin.connect(alice)[CREATE_PROPOSAL_SIGNATURE]( dummyMetadata, @@ -1472,6 +1514,11 @@ describe('Multisig', function () { } = data; const endDate = (await time.latest()) + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); await plugin .connect(alice) @@ -1485,8 +1532,6 @@ describe('Multisig', function () { endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); - await dao.grant( dao.address, plugin.address, @@ -1547,7 +1592,12 @@ describe('Multisig', function () { const endDate = (await time.latest()) + TIME.HOUR; - // Create a proposal (with ID 0) + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin .connect(alice) [CREATE_PROPOSAL_SIGNATURE]( @@ -1559,7 +1609,6 @@ describe('Multisig', function () { 0, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); await plugin.connect(alice).approve(id, false); expect(await plugin.canApprove(id, alice.address)).to.be.false; @@ -1575,7 +1624,12 @@ describe('Multisig', function () { const endDate = (await time.latest()) + TIME.HOUR; - // Create a proposal (with ID 0) + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin .connect(alice) [CREATE_PROPOSAL_SIGNATURE]( @@ -1587,7 +1641,6 @@ describe('Multisig', function () { 0, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); expect(await plugin.canApprove(id, alice.address)).to.be.true; }); @@ -1603,7 +1656,12 @@ describe('Multisig', function () { const startDate = (await time.latest()) + TIME.MINUTE; const endDate = startDate + TIME.HOUR; - // Create a proposal (with ID 0) + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin .connect(alice) [CREATE_PROPOSAL_SIGNATURE]( @@ -1615,7 +1673,6 @@ describe('Multisig', function () { startDate, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); expect(await plugin.canApprove(id, alice.address)).to.be.false; @@ -1634,7 +1691,12 @@ describe('Multisig', function () { const endDate = (await time.latest()) + TIME.HOUR; - // Create a proposal (with ID 0) + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin .connect(alice) [CREATE_PROPOSAL_SIGNATURE]( @@ -1646,7 +1708,6 @@ describe('Multisig', function () { 0, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); expect(await plugin.canApprove(id, alice.address)).to.be.true; @@ -1667,7 +1728,12 @@ describe('Multisig', function () { const endDate = (await time.latest()) + TIME.HOUR; - // Create a proposal (with ID 0) + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin .connect(alice) [CREATE_PROPOSAL_SIGNATURE]( @@ -1679,7 +1745,6 @@ describe('Multisig', function () { 0, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); expect(await plugin.hasApproved(id, alice.address)).to.be.false; }); @@ -1693,6 +1758,11 @@ describe('Multisig', function () { } = data; const endDate = (await time.latest()) + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); await plugin .connect(alice) @@ -1705,7 +1775,6 @@ describe('Multisig', function () { 0, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); await plugin.connect(alice).approve(id, false); expect(await plugin.hasApproved(id, alice.address)).to.be.true; @@ -1723,7 +1792,11 @@ describe('Multisig', function () { const endDate = (await time.latest()) + TIME.HOUR; - // Create a proposal (with ID 0) + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); await plugin .connect(alice) [CREATE_PROPOSAL_SIGNATURE]( @@ -1735,7 +1808,6 @@ describe('Multisig', function () { 0, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); await plugin.connect(alice).approve(id, true); @@ -1756,7 +1828,12 @@ describe('Multisig', function () { const endDate = (await time.latest()) + TIME.HOUR; - // Create a proposal (with ID 0) + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin .connect(alice) [CREATE_PROPOSAL_SIGNATURE]( @@ -1768,7 +1845,6 @@ describe('Multisig', function () { 0, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. await dao.grant( @@ -1795,7 +1871,12 @@ describe('Multisig', function () { const endDate = (await time.latest()) + TIME.HOUR; - // Create a proposal (with ID 0). + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin .connect(alice) [CREATE_PROPOSAL_SIGNATURE]( @@ -1807,7 +1888,6 @@ describe('Multisig', function () { 0, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Check that there are 0 approvals yet. expect((await plugin.getProposal(id)).approvals).to.equal(0); @@ -1835,6 +1915,12 @@ describe('Multisig', function () { // Create a proposal as Alice that didn't started yet. const startDate = (await time.latest()) + TIME.MINUTE; const endDate = startDate + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin .connect(alice) [CREATE_PROPOSAL_SIGNATURE]( @@ -1846,7 +1932,6 @@ describe('Multisig', function () { startDate, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Try to approve the proposal as Alice although being before the start date. await expect(plugin.connect(alice).approve(id, false)) @@ -1872,6 +1957,11 @@ describe('Multisig', function () { // Create a proposal as Alice that starts now. const startDate = (await time.latest()) + TIME.MINUTE; const endDate = startDate + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); await plugin .connect(alice) @@ -1884,7 +1974,6 @@ describe('Multisig', function () { 0, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Advance time after the end date. await time.increaseTo(endDate + 1); @@ -1906,6 +1995,12 @@ describe('Multisig', function () { // Create a proposal as Alice. const endDate = (await time.latest()) + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin .connect(alice) [CREATE_PROPOSAL_SIGNATURE]( @@ -1917,7 +2012,6 @@ describe('Multisig', function () { 0, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Check that `minApprovals` isn't met yet. const proposal = await plugin.getProposal(id); @@ -1939,6 +2033,11 @@ describe('Multisig', function () { // Create a proposal as Alice. const endDate = (await time.latest()) + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); await plugin .connect(alice) [CREATE_PROPOSAL_SIGNATURE]( @@ -1950,7 +2049,6 @@ describe('Multisig', function () { 0, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. await dao.grant( @@ -1981,6 +2079,11 @@ describe('Multisig', function () { } = data; const endDate = (await time.latest()) + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); await plugin .connect(alice) @@ -1993,7 +2096,6 @@ describe('Multisig', function () { 0, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); await plugin.connect(alice).approve(id, false); await plugin.connect(bob).approve(id, false); @@ -2014,6 +2116,11 @@ describe('Multisig', function () { const startDate = (await time.latest()) + TIME.MINUTE; const endDate = startDate + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); await plugin .connect(alice) @@ -2026,7 +2133,6 @@ describe('Multisig', function () { startDate, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); expect(await plugin.canExecute(id)).to.be.false; @@ -2049,6 +2155,11 @@ describe('Multisig', function () { } = data; const endDate = (await time.latest()) + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); await plugin .connect(alice) @@ -2061,7 +2172,6 @@ describe('Multisig', function () { 0, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); await plugin.connect(alice).approve(id, false); await plugin.connect(bob).approve(id, false); @@ -2087,6 +2197,12 @@ describe('Multisig', function () { // Create a proposal as Alice. const endDate = (await time.latest()) + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin .connect(alice) [CREATE_PROPOSAL_SIGNATURE]( @@ -2098,7 +2214,6 @@ describe('Multisig', function () { 0, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. await dao.grant( @@ -2126,6 +2241,12 @@ describe('Multisig', function () { // Create a proposal as Alice. const endDate = (await time.latest()) + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin .connect(alice) [CREATE_PROPOSAL_SIGNATURE]( @@ -2137,7 +2258,6 @@ describe('Multisig', function () { 0, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. await dao.grant( @@ -2179,6 +2299,11 @@ describe('Multisig', function () { } = data; const endDate = (await time.latest()) + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); await plugin .connect(alice) @@ -2191,7 +2316,6 @@ describe('Multisig', function () { 0, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); await dao.grant( dao.address, @@ -2229,6 +2353,12 @@ describe('Multisig', function () { // Create a proposal as Alice. const endDate = (await time.latest()) + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin .connect(alice) [CREATE_PROPOSAL_SIGNATURE]( @@ -2240,7 +2370,6 @@ describe('Multisig', function () { 0, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. await dao.grant( @@ -2289,9 +2418,7 @@ describe('Multisig', function () { ); expect(event.args.actor).to.equal(plugin.address); - expect(event.args.callId).to.equal( - ethers.utils.hexZeroPad(id.toHexString(), 32) - ); + expect(event.args.callId).to.equal(ethers.utils.hexZeroPad(id, 32)); expect(event.args.actions.length).to.equal(1); expect(event.args.actions[0].to).to.equal(dummyActions[0].to); expect(event.args.actions[0].value).to.equal(dummyActions[0].value); @@ -2329,6 +2456,12 @@ describe('Multisig', function () { // Create a proposal as Alice. const endDate = (await time.latest()) + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin .connect(alice) [CREATE_PROPOSAL_SIGNATURE]( @@ -2340,7 +2473,6 @@ describe('Multisig', function () { 0, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` on the DAO. await dao.grant( @@ -2373,6 +2505,12 @@ describe('Multisig', function () { // Create a proposal as Alice. const endDate = (await time.latest()) + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin .connect(alice) [CREATE_PROPOSAL_SIGNATURE]( @@ -2384,7 +2522,6 @@ describe('Multisig', function () { 0, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. await dao.grant( @@ -2417,6 +2554,12 @@ describe('Multisig', function () { // Create a proposal as Alice. const startDate = (await time.latest()) + TIME.MINUTE; const endDate = startDate + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin .connect(alice) [CREATE_PROPOSAL_SIGNATURE]( @@ -2428,7 +2571,6 @@ describe('Multisig', function () { startDate, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. await dao.grant( @@ -2465,6 +2607,12 @@ describe('Multisig', function () { // Create a proposal as Alice. const startDate = (await time.latest()) + TIME.MINUTE; const endDate = startDate + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin .connect(alice) [CREATE_PROPOSAL_SIGNATURE]( @@ -2476,7 +2624,6 @@ describe('Multisig', function () { 0, endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); // Approve the proposal but do not execute yet. await plugin.connect(alice).approve(id, false); @@ -2521,6 +2668,12 @@ describe('Multisig', function () { )) as Multisig; const endDate = (await time.latest()) + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin.connect(alice)[CREATE_PROPOSAL_SIGNATURE]( dummyMetadata, dummyActions, @@ -2531,7 +2684,6 @@ describe('Multisig', function () { endDate ); - const id = await plugin.createProposalId(dummyActions, dummyMetadata); await expect(plugin.connect(bob).approve(id, true)) .to.emit(plugin, 'ExecutedCustom') .to.emit(plugin, 'ProposalExecuted'); From da239654a7e4832260b0021a8811f01fb639ca02 Mon Sep 17 00:00:00 2001 From: Rekard0 <5880388+Rekard0@users.noreply.github.com> Date: Thu, 31 Oct 2024 21:46:09 +0100 Subject: [PATCH 45/59] feat: add auth to execute() and update Setup & testing --- packages/contracts/src/Multisig.sol | 15 +++- packages/contracts/src/MultisigSetup.sol | 75 +++++++++++----- .../test/10_unit-testing/11_plugin.ts | 88 ++++++++++++++++++- .../test/10_unit-testing/12_plugin-setup.ts | 37 +++++++- packages/contracts/test/multisig-constants.ts | 4 + 5 files changed, 188 insertions(+), 31 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index 6821fe68..63207b88 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -89,6 +89,10 @@ contract Multisig is /// @notice The ID of the permission required to call the `createProposal` function. bytes32 public constant CREATE_PROPOSAL_PERMISSION_ID = keccak256("CREATE_PROPOSAL_PERMISSION"); + /// @notice The ID of the permission required to call the `execute` functions. + bytes32 public constant EXECUTE_PROPOSAL_PERMISSION_ID = + keccak256("EXECUTE_PROPOSAL_PERMISSION"); + /// @notice A mapping between proposal IDs and proposal information. // solhint-disable-next-line named-parameters-mapping mapping(uint256 => Proposal) internal proposals; @@ -401,7 +405,14 @@ contract Multisig is emit Approved({proposalId: _proposalId, approver: approver}); - if (_tryExecution && _canExecute(_proposalId)) { + if (!_tryExecution) { + return; + } + + if ( + _canExecute(_proposalId) && + dao().hasPermission(address(this), approver, EXECUTE_PROPOSAL_PERMISSION_ID, msg.data) + ) { _execute(_proposalId); } } @@ -455,7 +466,7 @@ contract Multisig is } /// @inheritdoc IMultisig - function execute(uint256 _proposalId) public { + function execute(uint256 _proposalId) public auth(EXECUTE_PROPOSAL_PERMISSION_ID) { if (!_canExecute(_proposalId)) { revert ProposalExecutionForbidden(_proposalId); } diff --git a/packages/contracts/src/MultisigSetup.sol b/packages/contracts/src/MultisigSetup.sol index 80e997c5..23198009 100644 --- a/packages/contracts/src/MultisigSetup.sol +++ b/packages/contracts/src/MultisigSetup.sol @@ -42,6 +42,9 @@ contract MultisigSetup is PluginUpgradeableSetup { bytes32 public constant UPDATE_MULTISIG_SETTINGS_PERMISSION_ID = keccak256("UPDATE_MULTISIG_SETTINGS_PERMISSION"); + /// @notice A special address encoding permissions that are valid for any address `who` or `where`. + address internal constant ANY_ADDR = address(type(uint160).max); + /// @notice The contract constructor, that deploys the `Multisig` plugin logic contract. constructor() PluginUpgradeableSetup(address(new Multisig())) {} @@ -70,7 +73,7 @@ contract MultisigSetup is PluginUpgradeableSetup { // Prepare permissions PermissionLib.MultiTargetPermission[] - memory permissions = new PermissionLib.MultiTargetPermission[](5); + memory permissions = new PermissionLib.MultiTargetPermission[](6); // Set permissions to be granted. // Grant the list of permissions of the plugin to the DAO. @@ -90,13 +93,13 @@ contract MultisigSetup is PluginUpgradeableSetup { permissionId: EXECUTE_PERMISSION_ID }); - permissions[2] = PermissionLib.MultiTargetPermission( - PermissionLib.Operation.GrantWithCondition, - plugin, - address(type(uint160).max), // ANY_ADDR - listedCheckCondition, - Multisig(IMPLEMENTATION).CREATE_PROPOSAL_PERMISSION_ID() - ); + permissions[2] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.GrantWithCondition, + where: plugin, + who: ANY_ADDR, + condition: listedCheckCondition, + permissionId: Multisig(IMPLEMENTATION).CREATE_PROPOSAL_PERMISSION_ID() + }); permissions[3] = PermissionLib.MultiTargetPermission({ operation: PermissionLib.Operation.Grant, @@ -114,6 +117,14 @@ contract MultisigSetup is PluginUpgradeableSetup { permissionId: SET_METADATA_PERMISSION_ID }); + permissions[5] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Grant, + where: plugin, + who: ANY_ADDR, + condition: PermissionLib.NO_CONDITION, + permissionId: Multisig(IMPLEMENTATION).EXECUTE_PROPOSAL_PERMISSION_ID() + }); + preparedSetupData.permissions = permissions; preparedSetupData.helpers = new address[](1); @@ -137,7 +148,7 @@ contract MultisigSetup is PluginUpgradeableSetup { address listedCheckCondition = address(new ListedCheckCondition(_payload.plugin)); PermissionLib.MultiTargetPermission[] - memory permissions = new PermissionLib.MultiTargetPermission[](4); + memory permissions = new PermissionLib.MultiTargetPermission[](5); permissions[0] = PermissionLib.MultiTargetPermission({ operation: PermissionLib.Operation.Revoke, @@ -147,13 +158,13 @@ contract MultisigSetup is PluginUpgradeableSetup { permissionId: UPGRADE_PLUGIN_PERMISSION_ID }); - permissions[1] = PermissionLib.MultiTargetPermission( - PermissionLib.Operation.GrantWithCondition, - _payload.plugin, - address(type(uint160).max), // ANY_ADDR - listedCheckCondition, - Multisig(IMPLEMENTATION).CREATE_PROPOSAL_PERMISSION_ID() - ); + permissions[1] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.GrantWithCondition, + where: _payload.plugin, + who: ANY_ADDR, + condition: listedCheckCondition, + permissionId: Multisig(IMPLEMENTATION).CREATE_PROPOSAL_PERMISSION_ID() + }); permissions[2] = PermissionLib.MultiTargetPermission({ operation: PermissionLib.Operation.Grant, @@ -171,6 +182,14 @@ contract MultisigSetup is PluginUpgradeableSetup { permissionId: SET_METADATA_PERMISSION_ID }); + permissions[4] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Grant, + where: _payload.plugin, + who: ANY_ADDR, + condition: PermissionLib.NO_CONDITION, + permissionId: Multisig(IMPLEMENTATION).EXECUTE_PROPOSAL_PERMISSION_ID() + }); + preparedSetupData.permissions = permissions; preparedSetupData.helpers = new address[](1); @@ -186,7 +205,7 @@ contract MultisigSetup is PluginUpgradeableSetup { SetupPayload calldata _payload ) external view returns (PermissionLib.MultiTargetPermission[] memory permissions) { // Prepare permissions - permissions = new PermissionLib.MultiTargetPermission[](5); + permissions = new PermissionLib.MultiTargetPermission[](6); // Set permissions to be Revoked. permissions[0] = PermissionLib.MultiTargetPermission({ @@ -221,12 +240,20 @@ contract MultisigSetup is PluginUpgradeableSetup { permissionId: SET_METADATA_PERMISSION_ID }); - permissions[4] = PermissionLib.MultiTargetPermission( - PermissionLib.Operation.Revoke, - _payload.plugin, - address(type(uint160).max), // ANY_ADDR - PermissionLib.NO_CONDITION, - Multisig(IMPLEMENTATION).CREATE_PROPOSAL_PERMISSION_ID() - ); + permissions[4] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Revoke, + where: _payload.plugin, + who: ANY_ADDR, + condition: PermissionLib.NO_CONDITION, + permissionId: Multisig(IMPLEMENTATION).CREATE_PROPOSAL_PERMISSION_ID() + }); + + permissions[5] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Revoke, + where: _payload.plugin, + who: ANY_ADDR, + condition: PermissionLib.NO_CONDITION, + permissionId: Multisig(IMPLEMENTATION).EXECUTE_PROPOSAL_PERMISSION_ID() + }); } } diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index ab323fde..46412950 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -24,6 +24,7 @@ import { CREATE_PROPOSAL_PERMISSION_ID, CREATE_PROPOSAL_SIGNATURE, CREATE_PROPOSAL_SIGNATURE_IProposal, + EXECUTE_PROPOSAL_PERMISSION_ID, MULTISIG_EVENTS, MULTISIG_INTERFACE, Operation, @@ -164,6 +165,18 @@ async function fixture(): Promise { }, ]; + await dao.grant( + initializedPlugin.address, + ANY_ADDR, + EXECUTE_PROPOSAL_PERMISSION_ID + ); + + await dao.grant( + uninitializedPlugin.address, + ANY_ADDR, + EXECUTE_PROPOSAL_PERMISSION_ID + ); + return { deployer, alice, @@ -182,7 +195,7 @@ async function fixture(): Promise { async function loadFixtureAndGrantCreatePermission(): Promise { const data = await loadFixture(fixture); - const {deployer, dao, initializedPlugin, uninitializedPlugin} = data; + const {deployer, alice, dao, initializedPlugin, uninitializedPlugin} = data; const condition = await new ListedCheckCondition__factory(deployer).deploy( initializedPlugin.address @@ -201,6 +214,7 @@ async function loadFixtureAndGrantCreatePermission(): Promise { CREATE_PROPOSAL_PERMISSION_ID, condition.address ); + return data; } @@ -2688,6 +2702,78 @@ describe('Multisig', function () { .to.emit(plugin, 'ExecutedCustom') .to.emit(plugin, 'ProposalExecuted'); }); + + it('can not execute if execute permission is not granted', async () => { + const { + alice, + bob, + initializedPlugin: plugin, + defaultInitData, + dao, + dummyMetadata, + dummyActions, + } = data; + + // Create a proposal as Alice. + const endDate = (await time.latest()) + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + + await plugin + .connect(alice) + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); + + // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. + await dao.grant( + dao.address, + plugin.address, + DAO_PERMISSIONS.EXECUTE_PERMISSION_ID + ); + + // Approve with Alice and Bob. + await plugin.connect(alice).approve(id, false); + await plugin.connect(bob).approve(id, false); + + // Check that the `minApprovals` threshold is met. + const proposal = await plugin.getProposal(id); + expect(proposal.parameters.minApprovals).to.equal( + defaultInitData.settings.minApprovals + ); + expect(proposal.approvals).to.be.eq( + defaultInitData.settings.minApprovals + ); + + // Check that the proposal can be executed. + expect(await plugin.canExecute(id)).to.be.true; + + // Revoke execute permission from ANY_ADDR + await dao.revoke( + plugin.address, + ANY_ADDR, + EXECUTE_PROPOSAL_PERMISSION_ID + ); + + // Check that it executes. + await expect(plugin.connect(alice).execute(id)) + .to.be.revertedWithCustomError(plugin, 'DaoUnauthorized') + .withArgs( + dao.address, + plugin.address, + alice.address, + EXECUTE_PROPOSAL_PERMISSION_ID + ); + }); }); }); }); diff --git a/packages/contracts/test/10_unit-testing/12_plugin-setup.ts b/packages/contracts/test/10_unit-testing/12_plugin-setup.ts index 2ceafd9a..75309b75 100644 --- a/packages/contracts/test/10_unit-testing/12_plugin-setup.ts +++ b/packages/contracts/test/10_unit-testing/12_plugin-setup.ts @@ -10,6 +10,7 @@ import { SET_METADATA_PERMISSION_ID, UPDATE_MULTISIG_SETTINGS_PERMISSION_ID, UPGRADE_PLUGIN_PERMISSION_ID, + EXECUTE_PROPOSAL_PERMISSION_ID, } from '../multisig-constants'; import {Operation as op} from '../multisig-constants'; import {Multisig__factory, Multisig} from '../test-utils/typechain-versions'; @@ -315,7 +316,7 @@ describe('MultisigSetup', function () { // Check the return data. expect(plugin).to.be.equal(anticipatedPluginAddress); expect(helpers.length).to.be.equal(1); - expect(permissions.length).to.be.equal(5); + expect(permissions.length).to.be.equal(6); const condition = helpers[0]; @@ -355,6 +356,13 @@ describe('MultisigSetup', function () { AddressZero, SET_METADATA_PERMISSION_ID, ], + [ + Operation.Grant, + plugin, + ANY_ADDR, + AddressZero, + EXECUTE_PROPOSAL_PERMISSION_ID, + ], ]); }); @@ -424,7 +432,7 @@ describe('MultisigSetup', function () { [1, prepareUpdateBuild3Inputs] ) ); - expect(permissions.length).to.be.equal(4); + expect(permissions.length).to.be.equal(5); expect(helpers.length).to.be.equal(1); // check correct permission is revoked expect(permissions).to.deep.equal([ @@ -456,6 +464,13 @@ describe('MultisigSetup', function () { AddressZero, SET_METADATA_PERMISSION_ID, ], + [ + Operation.Grant, + plugin, + ANY_ADDR, + AddressZero, + EXECUTE_PROPOSAL_PERMISSION_ID, + ], ]); }); @@ -485,7 +500,7 @@ describe('MultisigSetup', function () { [2, prepareUpdateBuild3Inputs] ) ); - expect(permissions.length).to.be.equal(4); + expect(permissions.length).to.be.equal(5); expect(helpers.length).to.be.equal(1); // check correct permission is revoked expect(permissions).to.deep.equal([ @@ -517,6 +532,13 @@ describe('MultisigSetup', function () { AddressZero, SET_METADATA_PERMISSION_ID, ], + [ + Operation.Grant, + plugin, + ANY_ADDR, + AddressZero, + EXECUTE_PROPOSAL_PERMISSION_ID, + ], ]); }); }); @@ -542,7 +564,7 @@ describe('MultisigSetup', function () { ); // Check the return data. - expect(permissions.length).to.be.equal(5); + expect(permissions.length).to.be.equal(6); expect(permissions).to.deep.equal([ [ Operation.Revoke, @@ -579,6 +601,13 @@ describe('MultisigSetup', function () { AddressZero, CREATE_PROPOSAL_PERMISSION_ID, ], + [ + Operation.Revoke, + plugin, + ANY_ADDR, + AddressZero, + EXECUTE_PROPOSAL_PERMISSION_ID, + ], ]); }); }); diff --git a/packages/contracts/test/multisig-constants.ts b/packages/contracts/test/multisig-constants.ts index 6a958910..400a47b0 100644 --- a/packages/contracts/test/multisig-constants.ts +++ b/packages/contracts/test/multisig-constants.ts @@ -31,6 +31,10 @@ export const SET_METADATA_PERMISSION_ID = ethers.utils.id( 'SET_METADATA_PERMISSION' ); +export const EXECUTE_PROPOSAL_PERMISSION_ID = ethers.utils.id( + 'EXECUTE_PROPOSAL_PERMISSION' +); + export const CREATE_PROPOSAL_SIGNATURE = 'createProposal(bytes,(address,uint256,bytes)[],uint256,bool,bool,uint64,uint64)'; From 343b35256c1e6c073c9caa4637b49f9d9fe5c44e Mon Sep 17 00:00:00 2001 From: Rekard0 <5880388+Rekard0@users.noreply.github.com> Date: Thu, 31 Oct 2024 23:50:40 +0100 Subject: [PATCH 46/59] add test for recording vote with tryExecution when caller don't have execute permission --- .../test/10_unit-testing/11_plugin.ts | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index 46412950..efd917ac 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -2774,6 +2774,80 @@ describe('Multisig', function () { EXECUTE_PROPOSAL_PERMISSION_ID ); }); + + it('records approve correctly without execting when tryExecution is selected & execute permission is not granted', async () => { + const { + alice, + bob, + initializedPlugin: plugin, + defaultInitData, + dao, + dummyMetadata, + dummyActions, + } = data; + + // Create a proposal as Alice. + const endDate = (await time.latest()) + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + + await plugin + .connect(alice) + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); + + // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. + await dao.grant( + dao.address, + plugin.address, + DAO_PERMISSIONS.EXECUTE_PERMISSION_ID + ); + + // Check that the no one submited an approve yet. + let proposal = await plugin.getProposal(id); + expect(proposal.approvals).to.be.equal(0); + + // Approve with Alice, but without try execution.. + await plugin.connect(alice).approve(id, false); + + // Check that the `minApprovals` threshold is not met yet. + expect(proposal.parameters.minApprovals).to.equal( + defaultInitData.settings.minApprovals + ); + expect(proposal.approvals).to.be.lt( + defaultInitData.settings.minApprovals + ); + proposal = await plugin.getProposal(id); + expect(proposal.approvals).to.be.equal(1); + + // Revoke execute permission from ANY_ADDR + await dao.revoke( + plugin.address, + ANY_ADDR, + EXECUTE_PROPOSAL_PERMISSION_ID + ); + + // Approve with Bob and try execution. + await plugin.connect(bob).approve(id, true); + + // Check that the `minApprovals` threshold is met. + proposal = await plugin.getProposal(id); + expect(proposal.approvals).to.be.equal( + defaultInitData.settings.minApprovals + ); + expect(proposal.approvals).to.be.equal(2); + expect(proposal.executed).to.be.equal(false); + }); }); }); }); From 7b948f2e47d7d8606f55af15a7cfafcc31ad2261 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Fri, 1 Nov 2024 11:09:59 +0400 Subject: [PATCH 47/59] comment --- packages/contracts/src/Multisig.sol | 2 +- packages/contracts/test/10_unit-testing/11_plugin.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index 63207b88..40fb44e2 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -89,7 +89,7 @@ contract Multisig is /// @notice The ID of the permission required to call the `createProposal` function. bytes32 public constant CREATE_PROPOSAL_PERMISSION_ID = keccak256("CREATE_PROPOSAL_PERMISSION"); - /// @notice The ID of the permission required to call the `execute` functions. + /// @notice The ID of the permission required to call the `execute` function. bytes32 public constant EXECUTE_PROPOSAL_PERMISSION_ID = keccak256("EXECUTE_PROPOSAL_PERMISSION"); diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index efd917ac..c57f6a15 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -2775,7 +2775,7 @@ describe('Multisig', function () { ); }); - it('records approve correctly without execting when tryExecution is selected & execute permission is not granted', async () => { + it.only('records approve correctly without execting when tryExecution is selected & execute permission is not granted', async () => { const { alice, bob, @@ -2845,6 +2845,7 @@ describe('Multisig', function () { expect(proposal.approvals).to.be.equal( defaultInitData.settings.minApprovals ); + expect(await plugin.canExecute(id)).to.be.true; expect(proposal.approvals).to.be.equal(2); expect(proposal.executed).to.be.equal(false); }); From 005aacb0f082836e2a2f1d54db5d9705f9fa242f Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Fri, 1 Nov 2024 11:10:16 +0400 Subject: [PATCH 48/59] remove .only --- packages/contracts/test/10_unit-testing/11_plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index c57f6a15..a8074555 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -2775,7 +2775,7 @@ describe('Multisig', function () { ); }); - it.only('records approve correctly without execting when tryExecution is selected & execute permission is not granted', async () => { + it('records approve correctly without execting when tryExecution is selected & execute permission is not granted', async () => { const { alice, bob, From c6744845f29f291fba7009c28ef187c3a88e6402 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Mon, 4 Nov 2024 16:14:15 +0400 Subject: [PATCH 49/59] add hassucceeded --- packages/contracts/src/Multisig.sol | 11 +- .../test/10_unit-testing/11_plugin.ts | 126 ++++++++++++++++++ 2 files changed, 134 insertions(+), 3 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index 40fb44e2..508732d5 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -423,12 +423,17 @@ contract Multisig is } /// @inheritdoc IMultisig - function canExecute( - uint256 _proposalId - ) external view override(IMultisig, IProposal) returns (bool) { + function canExecute(uint256 _proposalId) external view virtual override returns (bool) { return _canExecute(_proposalId); } + /// @inheritdoc IProposal + function hasSucceeded(uint256 _proposalId) external view virtual override returns (bool) { + Proposal storage proposal_ = proposals[_proposalId]; + + return proposal_.approvals >= proposal_.parameters.minApprovals; + } + /// @notice Returns all information for a proposal vote by its ID. /// @param _proposalId The ID of the proposal. /// @return executed Whether the proposal is executed or not. diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index a8074555..7c57bd68 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -1998,6 +1998,132 @@ describe('Multisig', function () { }); }); + describe('hasSucceeded', async () => { + it('returns `false` if the proposal has not reached the minimum approval yet', async () => { + const { + alice, + initializedPlugin: plugin, + dummyMetadata, + dummyActions, + } = data; + + // Create a proposal as Alice. + const endDate = (await time.latest()) + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + + await plugin + .connect(alice) + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); + + // Check that `minApprovals` isn't met yet. + const proposal = await plugin.getProposal(id); + expect(proposal.approvals).to.be.lt(proposal.parameters.minApprovals); + + // Check that the proposal can not be executed. + expect(await plugin.hasSucceeded(id)).to.be.false; + }); + + it('returns `true` if threshold is met even if the proposal is already executed', async () => { + const { + alice, + bob, + initializedPlugin: plugin, + dao, + dummyMetadata, + dummyActions, + } = data; + + // Create a proposal as Alice. + const endDate = (await time.latest()) + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin + .connect(alice) + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); + + // Grant the plugin `EXECUTE_PERMISSION_ID` permission on the DAO. + await dao.grant( + dao.address, + plugin.address, + DAO_PERMISSIONS.EXECUTE_PERMISSION_ID + ); + + // Approve as Alice. + await plugin.connect(alice).approve(id, false); + // Approve and execute as Bob. + await plugin.connect(bob).approve(id, true); + + // Check that the proposal got executed. + expect((await plugin.getProposal(id)).executed).to.be.true; + + // Check that it cannot be executed again. + expect(await plugin.hasSucceeded(id)).to.be.true; + }); + + it('returns `true` if threshold is met and time window expired', async () => { + const { + alice, + bob, + initializedPlugin: plugin, + dao, + dummyMetadata, + dummyActions, + } = data; + + // Create a proposal as Alice. + const endDate = (await time.latest()) + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin + .connect(alice) + [CREATE_PROPOSAL_SIGNATURE]( + dummyMetadata, + dummyActions, + 0, + false, + false, + 0, + endDate + ); + + // Approve as Alice. + await plugin.connect(alice).approve(id, false); + // Approve and execute as Bob. + await plugin.connect(bob).approve(id, false); + + await time.increaseTo(endDate + 1); + + // Check that it cannot be executed again. + expect(await plugin.hasSucceeded(id)).to.be.true; + }); + }); + describe('canExecute', async () => { it('returns `false` if the proposal has not reached the minimum approval yet', async () => { const { From c8a4f3ca8baa195cd717a769962584cbc4b957c9 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Mon, 4 Nov 2024 16:18:16 +0400 Subject: [PATCH 50/59] comments in tests --- .../contracts/test/10_unit-testing/11_plugin.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index 7c57bd68..fd2a745a 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -2031,11 +2031,11 @@ describe('Multisig', function () { const proposal = await plugin.getProposal(id); expect(proposal.approvals).to.be.lt(proposal.parameters.minApprovals); - // Check that the proposal can not be executed. + // Check that the proposal has not yet succeeded. expect(await plugin.hasSucceeded(id)).to.be.false; }); - it('returns `true` if threshold is met even if the proposal is already executed', async () => { + it('returns `true` if threshold is met even if the proposal is executed or not', async () => { const { alice, bob, @@ -2074,12 +2074,17 @@ describe('Multisig', function () { // Approve as Alice. await plugin.connect(alice).approve(id, false); // Approve and execute as Bob. - await plugin.connect(bob).approve(id, true); + await plugin.connect(bob).approve(id, false); + + // It must still return true even if proposal is not executed yet. + expect(await plugin.hasSucceeded(id)).to.be.true; + + await plugin.execute(id); // Check that the proposal got executed. expect((await plugin.getProposal(id)).executed).to.be.true; - // Check that it cannot be executed again. + // It must still return true even if proposal has been executed. expect(await plugin.hasSucceeded(id)).to.be.true; }); @@ -2119,7 +2124,8 @@ describe('Multisig', function () { await time.increaseTo(endDate + 1); - // Check that it cannot be executed again. + // Check that it still returns true even after time windows are expired. + // Ensures that this function doesn't depend on time checks. expect(await plugin.hasSucceeded(id)).to.be.true; }); }); From 682b07401e3e2cf3845f5176bbed394e59a446fb Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Mon, 4 Nov 2024 18:21:14 +0400 Subject: [PATCH 51/59] reverts --- packages/contracts/src/Multisig.sol | 25 ++++++++++++- .../test/10_unit-testing/11_plugin.ts | 36 +++++++++++++++++-- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index 508732d5..2efe9953 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -109,6 +109,10 @@ contract Multisig is /// @param sender The sender address. error ProposalCreationForbidden(address sender); + /// @notice Thrown when a proposal doesn't exist. + /// @param proposalId The ID of the proposal which doesn't exist. + error NonexistentProposal(uint256 proposalId); + /// @notice Thrown if an approver is not allowed to cast an approve. This can be because the proposal /// - is not open, /// - was executed, or @@ -132,7 +136,7 @@ contract Multisig is /// @param actual The actual value. error AddresslistLengthOutOfBounds(uint16 limit, uint256 actual); - /// @notice Thrown if the proposal with same actions and metadata already exists. + /// @notice Thrown if the proposal with the same id already exists. /// @param proposalId The id of the proposal. error ProposalAlreadyExists(uint256 proposalId); @@ -419,16 +423,28 @@ contract Multisig is /// @inheritdoc IMultisig function canApprove(uint256 _proposalId, address _account) external view returns (bool) { + if (!_proposalExists(_proposalId)) { + revert NonexistentProposal(_proposalId); + } + return _canApprove(_proposalId, _account); } /// @inheritdoc IMultisig function canExecute(uint256 _proposalId) external view virtual override returns (bool) { + if (!_proposalExists(_proposalId)) { + revert NonexistentProposal(_proposalId); + } + return _canExecute(_proposalId); } /// @inheritdoc IProposal function hasSucceeded(uint256 _proposalId) external view virtual override returns (bool) { + if (!_proposalExists(_proposalId)) { + revert NonexistentProposal(_proposalId); + } + Proposal storage proposal_ = proposals[_proposalId]; return proposal_.approvals >= proposal_.parameters.minApprovals; @@ -502,6 +518,13 @@ contract Multisig is emit ProposalExecuted(_proposalId); } + /// @notice Checks if proposal exists or not. + /// @param _proposalId The ID of the proposal. + /// @return Returns `true` if proposal exists, otherwise false. + function _proposalExists(uint256 _proposalId) private view returns (bool) { + return proposals[_proposalId].parameters.snapshotBlock != 0; + } + /// @notice Internal function to check if an account can approve. It assumes the queried proposal exists. /// @param _proposalId The ID of the proposal. /// @param _account The account to check. diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index fd2a745a..7bf4df2a 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -218,7 +218,7 @@ async function loadFixtureAndGrantCreatePermission(): Promise { return data; } -describe('Multisig', function () { +describe.only('Multisig', function () { before(async () => { chainId = (await ethers.provider.getNetwork()).chainId; }); @@ -1516,6 +1516,15 @@ describe('Multisig', function () { }); describe('canApprove', async () => { + it('reverts if proposal does not exist', async () => { + const {initializedPlugin: plugin} = data; + const id = 10; + + await expect(plugin.canApprove(id, plugin.address)) + .to.be.revertedWithCustomError(plugin, 'NonexistentProposal') + .withArgs(id); + }); + it('returns `false` if the proposal is already executed', async () => { const { alice, @@ -1578,6 +1587,12 @@ describe('Multisig', function () { // Create a proposal as Alice. const endDate = (await time.latest()) + TIME.HOUR; + const id = await createProposalId( + plugin.address, + dummyActions, + dummyMetadata + ); + await plugin .connect(alice) [CREATE_PROPOSAL_SIGNATURE]( @@ -1589,7 +1604,6 @@ describe('Multisig', function () { 0, endDate ); - const id = 0; // Check that Dave who is not listed cannot approve. expect(await plugin.isListed(dave.address)).to.be.false; @@ -1999,6 +2013,15 @@ describe('Multisig', function () { }); describe('hasSucceeded', async () => { + it('reverts if proposal does not exist', async () => { + const {initializedPlugin: plugin} = data; + const id = 10; + + await expect(plugin.hasSucceeded(id)) + .to.be.revertedWithCustomError(plugin, 'NonexistentProposal') + .withArgs(id); + }); + it('returns `false` if the proposal has not reached the minimum approval yet', async () => { const { alice, @@ -2131,6 +2154,15 @@ describe('Multisig', function () { }); describe('canExecute', async () => { + it('reverts if proposal does not exist', async () => { + const {initializedPlugin: plugin} = data; + const id = 10; + + await expect(plugin.canExecute(id)) + .to.be.revertedWithCustomError(plugin, 'NonexistentProposal') + .withArgs(id); + }); + it('returns `false` if the proposal has not reached the minimum approval yet', async () => { const { alice, From 76ee96470b08d038d72bfaef346e6710dd1d1528 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Mon, 4 Nov 2024 18:23:04 +0400 Subject: [PATCH 52/59] remove .only --- packages/contracts/test/10_unit-testing/11_plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index 7bf4df2a..d99857fe 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -218,7 +218,7 @@ async function loadFixtureAndGrantCreatePermission(): Promise { return data; } -describe.only('Multisig', function () { +describe('Multisig', function () { before(async () => { chainId = (await ethers.provider.getNetwork()).chainId; }); From a51622dda2b4ca965c3bf07dbb1fef1fadab56b7 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Tue, 5 Nov 2024 11:32:19 +0400 Subject: [PATCH 53/59] docgen --- .gitignore | 2 ++ packages/contracts/hardhat.config.ts | 9 ++++++++- packages/contracts/package.json | 1 + packages/contracts/src/Multisig.sol | 3 ++- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 8180f988..4be16d13 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ packages/subgraph/imported packages/subgraph/generated packages/subgraph/tests/.bin +docs + # files *.env *.log diff --git a/packages/contracts/hardhat.config.ts b/packages/contracts/hardhat.config.ts index 88a4fbff..524f651c 100644 --- a/packages/contracts/hardhat.config.ts +++ b/packages/contracts/hardhat.config.ts @@ -20,6 +20,7 @@ import { import type {NetworkUserConfig} from 'hardhat/types'; import {resolve} from 'path'; import 'solidity-coverage'; +import 'solidity-docgen'; const dotenvConfigPath: string = process.env.DOTENV_CONFIG_PATH || '../../.env'; dotenvConfig({path: resolve(__dirname, dotenvConfigPath), override: true}); @@ -154,7 +155,6 @@ const config: HardhatUserConfig = { tests: './test', deploy: './deploy', }, - solidity: { version: '0.8.17', settings: { @@ -175,6 +175,13 @@ const config: HardhatUserConfig = { outDir: 'typechain', target: 'ethers-v5', }, + docgen: { + outputDir: 'docs', + theme: 'markdown', + pages: 'files', + collapseNewlines: true, + exclude: ['test', 'mocks'], + }, }; export default config; diff --git a/packages/contracts/package.json b/packages/contracts/package.json index a70f99c5..ea09130b 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -8,6 +8,7 @@ "lint:sol": "cd ../../ && yarn run lint:contracts:sol", "lint:ts": "cd ../../ && yarn run lint:contracts:ts", "test": "hardhat test", + "docgen": "hardhat docgen", "typechain": "cross-env TS_NODE_TRANSPILE_ONLY=true hardhat typechain", "clean": "rimraf ./artifacts ./cache ./coverage ./typechain ./types ./coverage.json && yarn typechain" }, diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index 2efe9953..6cdd4dcf 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -21,7 +21,8 @@ import {IMultisig} from "./IMultisig.sol"; /// @title Multisig /// @author Aragon X - 2022-2023 /// @notice The on-chain multisig governance plugin in which a proposal passes if X out of Y approvals are met. -/// @dev v1.3 (Release 1, Build 3). For each upgrade, if the reinitialization step is required, increment the version numbers in the modifier for both the initialize and initializeFrom functions. +/// @dev v1.3 (Release 1, Build 4). For each upgrade, if the reinitialization step is required, +/// increment the version numbers in the modifier for both the initialize and initializeFrom functions. /// @custom:security-contact sirt@aragon.org contract Multisig is IMultisig, From 5b5fe646869bea4e28a115f43c0d0fe3dc0f91b2 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Tue, 5 Nov 2024 11:34:39 +0400 Subject: [PATCH 54/59] new line comment --- packages/contracts/src/Multisig.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index 6cdd4dcf..5d0271b9 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -21,8 +21,8 @@ import {IMultisig} from "./IMultisig.sol"; /// @title Multisig /// @author Aragon X - 2022-2023 /// @notice The on-chain multisig governance plugin in which a proposal passes if X out of Y approvals are met. -/// @dev v1.3 (Release 1, Build 4). For each upgrade, if the reinitialization step is required, -/// increment the version numbers in the modifier for both the initialize and initializeFrom functions. +/// @dev v1.3 (Release 1, Build 4). For each upgrade, if the reinitialization step is required, increment +/// the version numbers in the modifier for both the initialize and initializeFrom functions. /// @custom:security-contact sirt@aragon.org contract Multisig is IMultisig, From 752fff1adeb46d5e926fc21dca1b0b1486a0adde Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Tue, 5 Nov 2024 11:41:56 +0400 Subject: [PATCH 55/59] original comment --- packages/contracts/src/Multisig.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index 5d0271b9..c7d255aa 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -21,8 +21,7 @@ import {IMultisig} from "./IMultisig.sol"; /// @title Multisig /// @author Aragon X - 2022-2023 /// @notice The on-chain multisig governance plugin in which a proposal passes if X out of Y approvals are met. -/// @dev v1.3 (Release 1, Build 4). For each upgrade, if the reinitialization step is required, increment -/// the version numbers in the modifier for both the initialize and initializeFrom functions. +/// @dev v1.3 (Release 1, Build 4). For each upgrade, if the reinitialization step is required, increment the version numbers in the modifier for both the initialize and initializeFrom functions. /// @custom:security-contact sirt@aragon.org contract Multisig is IMultisig, From ceee8fd5c615b5ee98a4b39610837934a480af13 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Tue, 5 Nov 2024 13:12:13 +0400 Subject: [PATCH 56/59] multi line comment --- packages/contracts/src/Multisig.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index c7d255aa..6e7e13e5 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -21,7 +21,8 @@ import {IMultisig} from "./IMultisig.sol"; /// @title Multisig /// @author Aragon X - 2022-2023 /// @notice The on-chain multisig governance plugin in which a proposal passes if X out of Y approvals are met. -/// @dev v1.3 (Release 1, Build 4). For each upgrade, if the reinitialization step is required, increment the version numbers in the modifier for both the initialize and initializeFrom functions. +/// @dev v1.3 (Release 1, Build 3). For each upgrade, if the reinitialization step is required, +/// increment the version numbers in the modifier for both the initialize and initializeFrom functions. /// @custom:security-contact sirt@aragon.org contract Multisig is IMultisig, From 42948ff4ca45811eef38a8925af116f5196d1bd8 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Tue, 5 Nov 2024 13:18:43 +0400 Subject: [PATCH 57/59] comments --- packages/contracts/src/Multisig.sol | 8 +++++--- packages/contracts/src/MultisigSetup.sol | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index 6e7e13e5..152c0134 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -19,7 +19,7 @@ import {IMultisig} from "./IMultisig.sol"; /* solhint-enable max-line-length */ /// @title Multisig -/// @author Aragon X - 2022-2023 +/// @author Aragon X - 2022-2024 /// @notice The on-chain multisig governance plugin in which a proposal passes if X out of Y approvals are met. /// @dev v1.3 (Release 1, Build 3). For each upgrade, if the reinitialization step is required, /// increment the version numbers in the modifier for both the initialize and initializeFrom functions. @@ -183,8 +183,10 @@ contract Multisig is _setTargetConfig(_targetConfig); } - /// @notice Reinitializes the TokenVoting after an upgrade from a previous protocol version. For each reinitialization step, use the `_fromBuild` version to decide which internal functions to call for reinitialization. - /// @dev WARNING: The contract should only be upgradeable through PSP to ensure that _fromBuild is not incorrectly passed, and that the appropriate permissions for the upgrade are properly configured. + /// @notice Reinitializes the TokenVoting after an upgrade from a previous protocol version. For each reinitialization step, + /// use the `_fromBuild` version to decide which internal functions to call for reinitialization. + /// @dev WARNING: The contract should only be upgradeable through PSP to ensure that _fromBuild is not incorrectly passed, + /// and that the appropriate permissions for the upgrade are properly configured. /// @param _fromBuild The build version number of the previous implementation contract this upgrade is transitioning from. /// @param _initData The initialization data to be passed to via `upgradeToAndCall` (see [ERC-1967](https://docs.openzeppelin.com/contracts/4.x/api/proxy#ERC1967Upgrade)). function initializeFrom(uint16 _fromBuild, bytes calldata _initData) external reinitializer(2) { diff --git a/packages/contracts/src/MultisigSetup.sol b/packages/contracts/src/MultisigSetup.sol index 23198009..db91c0a7 100644 --- a/packages/contracts/src/MultisigSetup.sol +++ b/packages/contracts/src/MultisigSetup.sol @@ -18,7 +18,7 @@ import {Multisig} from "./Multisig.sol"; /* solhint-enable max-line-length */ /// @title MultisigSetup -/// @author Aragon X - 2022-2023 +/// @author Aragon X - 2022-2024 /// @notice The setup contract of the `Multisig` plugin. /// @dev v1.3 (Release 1, Build 3) /// @custom:security-contact sirt@aragon.org From 9115d427f2db0d6f9035cfae8d6680681592fa25 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Tue, 5 Nov 2024 13:20:31 +0400 Subject: [PATCH 58/59] add docgen --- packages/contracts/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/contracts/package.json b/packages/contracts/package.json index ea09130b..ee32f9a2 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -36,6 +36,7 @@ "@openzeppelin/hardhat-upgrades": "^1.28.0", "@typechain/ethers-v5": "^10.1.1", "@typechain/hardhat": "^6.1.4", + "solidity-docgen": "^0.6.0-beta.35", "@types/chai": "^4.3.4", "@types/mocha": "^10.0.0", "@types/node": "^18.11.9", From 2818638f99cc652d63a9959b3761564b802b76d7 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Tue, 5 Nov 2024 13:25:47 +0400 Subject: [PATCH 59/59] reorder comment --- packages/contracts/src/Multisig.sol | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/contracts/src/Multisig.sol b/packages/contracts/src/Multisig.sol index 152c0134..9709eac4 100644 --- a/packages/contracts/src/Multisig.sol +++ b/packages/contracts/src/Multisig.sol @@ -183,10 +183,11 @@ contract Multisig is _setTargetConfig(_targetConfig); } - /// @notice Reinitializes the TokenVoting after an upgrade from a previous protocol version. For each reinitialization step, - /// use the `_fromBuild` version to decide which internal functions to call for reinitialization. - /// @dev WARNING: The contract should only be upgradeable through PSP to ensure that _fromBuild is not incorrectly passed, - /// and that the appropriate permissions for the upgrade are properly configured. + /// @notice Reinitializes the TokenVoting after an upgrade from a previous protocol version. For each + /// reinitialization step, use the `_fromBuild` version to decide which internal functions to call + /// for reinitialization. + /// @dev WARNING: The contract should only be upgradeable through PSP to ensure that _fromBuild is not + /// incorrectly passed, and that the appropriate permissions for the upgrade are properly configured. /// @param _fromBuild The build version number of the previous implementation contract this upgrade is transitioning from. /// @param _initData The initialization data to be passed to via `upgradeToAndCall` (see [ERC-1967](https://docs.openzeppelin.com/contracts/4.x/api/proxy#ERC1967Upgrade)). function initializeFrom(uint16 _fromBuild, bytes calldata _initData) external reinitializer(2) {