diff --git a/packages/contracts/src/IMajorityVoting.sol b/packages/contracts/src/IMajorityVoting.sol index e2aa66d1..f3e73834 100644 --- a/packages/contracts/src/IMajorityVoting.sol +++ b/packages/contracts/src/IMajorityVoting.sol @@ -36,6 +36,10 @@ interface IMajorityVoting { /// @return The support threshold parameter. function supportThreshold() external view returns (uint32); + /// @notice Returns the min approval value stored configured. + /// @return The minimal approval value. + function minApproval() external view returns (uint256); + /// @notice Returns the minimum participation parameter stored in the voting settings. /// @return The minimum participation parameter. function minParticipation() external view returns (uint32); @@ -61,6 +65,13 @@ interface IMajorityVoting { /// @return Returns `true` if the participation is greater than the minimum participation and `false` otherwise. function isMinParticipationReached(uint256 _proposalId) external view returns (bool); + /// @notice Checks if the min approval value defined as: + ///$$\texttt{minApproval} = \frac{N_\text{yes}}{N_\text{total}}$$ + /// for a proposal vote is greater or equal than the minimum approval value. + /// @param _proposalId The ID of the proposal. + /// @return Returns `true` if the participation is greater than the minimum participation and `false` otherwise. + function isMinApprovalReached(uint256 _proposalId) external view returns (bool); + /// @notice Checks if an account can participate on a proposal vote. This can be because the vote /// - has not started, /// - has ended, diff --git a/packages/contracts/src/MajorityVotingBase.sol b/packages/contracts/src/MajorityVotingBase.sol index d59b25cb..ec6a3700 100644 --- a/packages/contracts/src/MajorityVotingBase.sol +++ b/packages/contracts/src/MajorityVotingBase.sol @@ -165,6 +165,7 @@ abstract contract MajorityVotingBase is /// @param voters The votes casted by the voters. /// @param actions The actions to be executed when the proposal passes. /// @param allowFailureMap A bitmap allowing the proposal to succeed, even if individual actions might revert. + /// @param minApprovalPower The minimum amount of yes votes power needed for the proposal advance. /// If the bit at index `i` is 1, the proposal succeeds even if the `i`th action reverts. /// A failure map value of 0 requires every action to not revert. struct Proposal { @@ -174,6 +175,7 @@ abstract contract MajorityVotingBase is mapping(address => IMajorityVoting.VoteOption) voters; IDAO.Action[] actions; uint256 allowFailureMap; + uint256 minApprovalPower; } /// @notice A container for the proposal parameters at the time of proposal creation. @@ -211,6 +213,7 @@ abstract contract MajorityVotingBase is this.totalVotingPower.selector ^ this.getProposal.selector ^ this.updateVotingSettings.selector ^ + this.updateMinApprovals.selector ^ this.createProposal.selector; /// @notice The ID of the permission required to call the `updateVotingSettings` function. @@ -224,6 +227,10 @@ abstract contract MajorityVotingBase is /// @notice The struct storing the voting settings. VotingSettings private votingSettings; + /// @notice The minimal ratio of yes votes needed for a proposal succeed. + /// @dev is not on the VotingSettings for compatibility reasons. + uint256 private minApprovals; // added in v1.3 + /// @notice Thrown if a date is out of bounds. /// @param limit The limit value. /// @param actual The actual value. @@ -266,6 +273,10 @@ abstract contract MajorityVotingBase is uint256 minProposerVotingPower ); + /// @notice Emitted when the min approval value is updated. + /// @param minApprovals The minimum amount of yes votes needed for a proposal succeed. + event VotingMinApprovalUpdated(uint256 minApprovals); + /// @notice Initializes the component to be used by inheriting contracts. /// @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. @@ -273,10 +284,12 @@ abstract contract MajorityVotingBase is // solhint-disable-next-line func-name-mixedcase function __MajorityVotingBase_init( IDAO _dao, - VotingSettings calldata _votingSettings + VotingSettings calldata _votingSettings, + uint256 _minApprovals ) internal onlyInitializing { __PluginUUPSUpgradeable_init(_dao); _updateVotingSettings(_votingSettings); + _updateMinApprovals(_minApprovals); } /// @notice Checks if this or the parent contract supports an interface by its ID. @@ -291,9 +304,17 @@ abstract contract MajorityVotingBase is override(ERC165Upgradeable, PluginUUPSUpgradeable, ProposalUpgradeable) returns (bool) { + // In addition to the current IMajorityVoting interface, also support previous version + // that did not include the isMinApprovalReached() and minApproval() functions, same + // happens with MAJORITY_VOTING_BASE_INTERFACE which did not included updateMinApprovals(). return _interfaceId == MAJORITY_VOTING_BASE_INTERFACE_ID || + _interfaceId == MAJORITY_VOTING_BASE_INTERFACE_ID ^ this.updateMinApprovals.selector || _interfaceId == type(IMajorityVoting).interfaceId || + _interfaceId == + type(IMajorityVoting).interfaceId ^ + this.isMinApprovalReached.selector ^ + this.minApproval.selector || super.supportsInterface(_interfaceId); } @@ -386,6 +407,16 @@ abstract contract MajorityVotingBase is proposal_.parameters.minVotingPower; } + /// @inheritdoc IMajorityVoting + function isMinApprovalReached(uint256 _proposalId) public view virtual returns (bool) { + return proposals[_proposalId].tally.yes >= proposals[_proposalId].minApprovalPower; + } + + /// @inheritdoc IMajorityVoting + function minApproval() public view virtual returns (uint256) { + return minApprovals; + } + /// @inheritdoc IMajorityVoting function supportThreshold() public view virtual returns (uint32) { return votingSettings.supportThreshold; @@ -460,6 +491,15 @@ abstract contract MajorityVotingBase is _updateVotingSettings(_votingSettings); } + // todo TBD define if permission should be the same one as update settings + /// @notice Updates the minimal approval value. + /// @param _minApprovals The new minimal approval value. + function updateMinApprovals( + uint256 _minApprovals + ) external virtual auth(UPDATE_VOTING_SETTINGS_PERMISSION_ID) { + _updateMinApprovals(_minApprovals); + } + /// @notice Creates a new majority voting proposal. /// @param _metadata The metadata of the proposal. /// @param _actions The actions that will be executed after the proposal passes. @@ -550,6 +590,9 @@ abstract contract MajorityVotingBase is if (!isMinParticipationReached(_proposalId)) { return false; } + if (!isMinApprovalReached(_proposalId)) { + return false; + } return true; } @@ -570,7 +613,7 @@ abstract contract MajorityVotingBase is /// @param _votingSettings The voting settings to be validated and updated. function _updateVotingSettings(VotingSettings calldata _votingSettings) internal virtual { // Require the support threshold value to be in the interval [0, 10^6-1], - // because `>` comparision is used in the support criterion and >100% could never be reached. + // because `>` comparison is used in the support criterion and >100% could never be reached. if (_votingSettings.supportThreshold > RATIO_BASE - 1) { revert RatioOutOfBounds({ limit: RATIO_BASE - 1, @@ -579,7 +622,7 @@ abstract contract MajorityVotingBase is } // Require the minimum participation value to be in the interval [0, 10^6], - // because `>=` comparision is used in the participation criterion. + // because `>=` comparison is used in the participation criterion. if (_votingSettings.minParticipation > RATIO_BASE) { revert RatioOutOfBounds({limit: RATIO_BASE, actual: _votingSettings.minParticipation}); } @@ -603,6 +646,19 @@ abstract contract MajorityVotingBase is }); } + /// @notice Internal function to update minimal approval value. + /// @param _minApprovals The new minimal approval value. + function _updateMinApprovals(uint256 _minApprovals) internal virtual { + // Require the minimum approval value to be in the interval [0, 10^6], + // because `>=` comparison is used in the participation criterion. + if (_minApprovals > RATIO_BASE) { + revert RatioOutOfBounds({limit: RATIO_BASE, actual: _minApprovals}); + } + + minApprovals = _minApprovals; + emit VotingMinApprovalUpdated(_minApprovals); + } + /// @notice Validates and returns the proposal vote dates. /// @param _start The start date of the proposal vote. /// If 0, the current timestamp is used and the vote starts immediately. @@ -644,5 +700,5 @@ abstract contract MajorityVotingBase is /// new variables without shifting down storage in the inheritance chain /// (see [OpenZeppelin's guide about storage gaps] /// (https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps)). - uint256[47] private __gap; + uint256[46] private __gap; } diff --git a/packages/contracts/src/TokenVoting.sol b/packages/contracts/src/TokenVoting.sol index 95a061a0..60c0bdbd 100644 --- a/packages/contracts/src/TokenVoting.sol +++ b/packages/contracts/src/TokenVoting.sol @@ -23,8 +23,11 @@ contract TokenVoting is IMembership, MajorityVotingBase { using SafeCastUpgradeable for uint256; /// @notice The [ERC-165](https://eips.ethereum.org/EIPS/eip-165) interface ID of the contract. - bytes4 internal constant TOKEN_VOTING_INTERFACE_ID = - this.initialize.selector ^ this.getVotingToken.selector; + // todo double check there is a strong reason for keeping the initialize function on the interface id + bytes4 internal constant TOKEN_VOTING_INTERFACE_ID = this.getVotingToken.selector; + bytes4 internal constant OLD_TOKEN_VOTING_INTERFACE_ID = + bytes4(keccak256("initialize(address,(uint8,uint32,uint32,uint64,uint256),address)")) ^ + this.getVotingToken.selector; /// @notice An [OpenZeppelin `Votes`](https://docs.openzeppelin.com/contracts/4.x/api/governance#Votes) /// compatible contract referencing the token being used for voting. @@ -33,29 +36,52 @@ contract TokenVoting is IMembership, MajorityVotingBase { /// @notice Thrown if the voting power is zero error NoVotingPower(); + error FunctionDeprecated(); + + /// @dev Deprecated function. + function initialize( + IDAO _dao, + VotingSettings calldata _votingSettings, + IVotesUpgradeable _token + ) external initializer { + (_dao, _votingSettings, _token); + + // todo TBD should we deprecate this function or only continue with old flow? + revert FunctionDeprecated(); + } + /// @notice Initializes the component. /// @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. /// @param _votingSettings The voting settings. /// @param _token The [ERC-20](https://eips.ethereum.org/EIPS/eip-20) token used for voting. + /// @param _minApprovals The minimal amount of approvals the proposal needs to succeed. function initialize( IDAO _dao, VotingSettings calldata _votingSettings, - IVotesUpgradeable _token + IVotesUpgradeable _token, + uint256 _minApprovals ) external initializer { - __MajorityVotingBase_init(_dao, _votingSettings); + __MajorityVotingBase_init(_dao, _votingSettings, _minApprovals); votingToken = _token; emit MembershipContractAnnounced({definingContract: address(_token)}); } + /// @notice Initializes the plugin after an upgrade from a previous version. + /// @param _minApprovals The minimal amount of approvals the proposal needs to succeed. + function initializeFrom(uint256 _minApprovals) external reinitializer(2) { + _updateMinApprovals(_minApprovals); + } + /// @notice Checks if this or the parent contract supports an interface by its ID. /// @param _interfaceId The ID of the interface. /// @return Returns `true` if the interface is supported. function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { return _interfaceId == TOKEN_VOTING_INTERFACE_ID || + _interfaceId == OLD_TOKEN_VOTING_INTERFACE_ID || _interfaceId == type(IMembership).interfaceId || super.supportsInterface(_interfaceId); } @@ -137,6 +163,8 @@ contract TokenVoting is IMembership, MajorityVotingBase { minParticipation() ); + proposal_.minApprovalPower = _applyRatioCeiled(totalVotingPower_, minApproval()); + // Reduce costs if (_allowFailureMap != 0) { proposal_.allowFailureMap = _allowFailureMap; diff --git a/packages/contracts/src/TokenVotingSetup.sol b/packages/contracts/src/TokenVotingSetup.sol index 0459fb90..05522063 100644 --- a/packages/contracts/src/TokenVotingSetup.sol +++ b/packages/contracts/src/TokenVotingSetup.sol @@ -96,10 +96,16 @@ contract TokenVotingSetup is PluginUpgradeableSetup { MajorityVotingBase.VotingSettings memory votingSettings, TokenSettings memory tokenSettings, // only used for GovernanceERC20(token is not passed) - GovernanceERC20.MintSettings memory mintSettings + GovernanceERC20.MintSettings memory mintSettings, + uint256 minApprovals ) = abi.decode( _data, - (MajorityVotingBase.VotingSettings, TokenSettings, GovernanceERC20.MintSettings) + ( + MajorityVotingBase.VotingSettings, + TokenSettings, + GovernanceERC20.MintSettings, + uint256 + ) ); address token = tokenSettings.addr; @@ -154,9 +160,12 @@ contract TokenVotingSetup is PluginUpgradeableSetup { // Prepare and deploy plugin proxy. plugin = address(tokenVotingBase).deployUUPSProxy( - abi.encodeCall( - TokenVoting.initialize, - (IDAO(_dao), votingSettings, IVotesUpgradeable(token)) + abi.encodeWithSignature( + "initialize(address,(uint8,uint32,uint32,uint64,uint256),address,uint256)", + IDAO(_dao), + votingSettings, + IVotesUpgradeable(token), + minApprovals ) ); @@ -213,7 +222,6 @@ contract TokenVotingSetup is PluginUpgradeableSetup { override returns (bytes memory initData, PreparedSetupData memory preparedSetupData) { - (initData); if (_fromBuild < 3) { PermissionLib.MultiTargetPermission[] memory permissions = new PermissionLib.MultiTargetPermission[](1); @@ -227,6 +235,12 @@ contract TokenVotingSetup is PluginUpgradeableSetup { }); preparedSetupData.permissions = permissions; + + // initialize the minAdvance value + initData = abi.encodeCall( + TokenVoting.initializeFrom, + (abi.decode(_payload.data, (uint256))) + ); } } diff --git a/packages/contracts/src/build-metadata.json b/packages/contracts/src/build-metadata.json index 989f7144..57bf51b5 100644 --- a/packages/contracts/src/build-metadata.json +++ b/packages/contracts/src/build-metadata.json @@ -88,6 +88,12 @@ "name": "mintSettings", "type": "tuple", "description": "The token mint settings struct containing the `receivers` and `amounts`." + }, + { + "internalType": "uint32", + "name": "minApproval", + "type": "uint32", + "description": "The minimum amount of yes votes needed for the proposal advance." } ] }, @@ -99,6 +105,17 @@ "2": { "description": "No input is required for the update.", "inputs": [] + }, + "3": { + "description": "The information required for the installation.", + "inputs": [ + { + "internalType": "uint32", + "name": "minApproval", + "type": "uint32", + "description": "The minimum amount of yes votes needed for the proposal advance." + } + ] } }, "prepareUninstallation": { diff --git a/packages/contracts/src/mocks/MajorityVotingMock.sol b/packages/contracts/src/mocks/MajorityVotingMock.sol index 55bc8c58..f82d23c8 100644 --- a/packages/contracts/src/mocks/MajorityVotingMock.sol +++ b/packages/contracts/src/mocks/MajorityVotingMock.sol @@ -5,8 +5,12 @@ pragma solidity ^0.8.8; import {MajorityVotingBase, IDAO} from "../MajorityVotingBase.sol"; contract MajorityVotingMock is MajorityVotingBase { - function initializeMock(IDAO _dao, VotingSettings calldata _votingSettings) public initializer { - __MajorityVotingBase_init(_dao, _votingSettings); + function initializeMock( + IDAO _dao, + VotingSettings calldata _votingSettings, + uint256 _minApprovals + ) public initializer { + __MajorityVotingBase_init(_dao, _votingSettings, _minApprovals); } function createProposal( diff --git a/packages/contracts/test/10_unit-testing/11_plugin.ts b/packages/contracts/test/10_unit-testing/11_plugin.ts index 401b487c..c8c26a62 100644 --- a/packages/contracts/test/10_unit-testing/11_plugin.ts +++ b/packages/contracts/test/10_unit-testing/11_plugin.ts @@ -19,15 +19,19 @@ import { import {ExecutedEvent} from '../../typechain/src/mocks/DAOMock'; import { MAJORITY_VOTING_BASE_INTERFACE, + MAJORITY_VOTING_BASE_OLD_INTERFACE, VOTING_EVENTS, } from '../test-utils/majority-voting-constants'; import { TOKEN_VOTING_INTERFACE, UPDATE_VOTING_SETTINGS_PERMISSION_ID, + INITIALIZE_SIGNATURE, + INITIALIZE_SIGNATURE_OLD, } from '../test-utils/token-voting-constants'; import { TokenVoting__factory, TokenVoting, + IMajorityVoting_V1_3_0__factory, } from '../test-utils/typechain-versions'; import { VoteOption, @@ -69,6 +73,7 @@ type GlobalFixtureResult = { initializedPlugin: TokenVoting; uninitializedPlugin: TokenVoting; defaultVotingSettings: MajorityVotingBase.VotingSettingsStruct; + defaultMinApproval: BigNumber; token: TestGovernanceERC20; dao: DAO; dummyActions: DAOStructs.ActionStruct[]; @@ -122,11 +127,13 @@ async function globalFixture(): Promise { minProposerVotingPower: 0, }; - const pluginInitdata = pluginImplementation.interface.encodeFunctionData( - 'initialize', - [dao.address, defaultVotingSettings, token.address] + const defaultMinApproval = pctToRatio(10); + + const pluginInitData = pluginImplementation.interface.encodeFunctionData( + INITIALIZE_SIGNATURE, + [dao.address, defaultVotingSettings, token.address, defaultMinApproval] ); - const deploymentTx1 = await proxyFactory.deployUUPSProxy(pluginInitdata); + const deploymentTx1 = await proxyFactory.deployUUPSProxy(pluginInitData); const proxyCreatedEvent1 = findEvent( await deploymentTx1.wait(), proxyFactory.interface.getEvent('ProxyCreated').name @@ -190,6 +197,7 @@ async function globalFixture(): Promise { initializedPlugin, uninitializedPlugin, defaultVotingSettings, + defaultMinApproval, token, dao, dummyActions, @@ -200,36 +208,65 @@ async function globalFixture(): Promise { describe('TokenVoting', function () { describe('initialize', async () => { it('reverts if trying to re-initialize', async () => { - const {dao, initializedPlugin, defaultVotingSettings, token} = - await loadFixture(globalFixture); + const { + dao, + initializedPlugin, + defaultVotingSettings, + defaultMinApproval, + token, + } = await loadFixture(globalFixture); // Try to reinitialize the initialized plugin. await expect( - initializedPlugin.initialize( + initializedPlugin[INITIALIZE_SIGNATURE]( dao.address, defaultVotingSettings, - token.address + token.address, + defaultMinApproval ) ).to.be.revertedWith('Initializable: contract is already initialized'); }); - it('emits the `MembershipContractAnnounced` event', async () => { + it('reverts if using DEPRECATED intialize function', async () => { const {dao, uninitializedPlugin, defaultVotingSettings, token} = await loadFixture(globalFixture); - // Initialize the uninitialized plugin. + // Try to call deprecated function (previous function with no minApproval param) await expect( - await uninitializedPlugin.initialize( + uninitializedPlugin[INITIALIZE_SIGNATURE_OLD]( dao.address, defaultVotingSettings, token.address ) + ).to.be.revertedWithCustomError( + uninitializedPlugin, + 'FunctionDeprecated' + ); + }); + + it('emits the `MembershipContractAnnounced` event', async () => { + const { + dao, + uninitializedPlugin, + defaultVotingSettings, + defaultMinApproval, + token, + } = await loadFixture(globalFixture); + + // Initialize the uninitialized plugin. + await expect( + await uninitializedPlugin[INITIALIZE_SIGNATURE]( + dao.address, + defaultVotingSettings, + token.address, + defaultMinApproval + ) ) .to.emit(uninitializedPlugin, 'MembershipContractAnnounced') .withArgs(token.address); }); - it('sets the voting settings and token', async () => { + it('sets the voting settings, token and minimal approval', async () => { const { dao, uninitializedPlugin: plugin, @@ -254,9 +291,15 @@ describe('TokenVoting', function () { minDuration: TIME.HOUR, minProposerVotingPower: 123, }; + const minApproval = pctToRatio(30); // Initialize the plugin. - await plugin.initialize(dao.address, votingSettings, token.address); + await plugin[INITIALIZE_SIGNATURE]( + dao.address, + votingSettings, + token.address, + minApproval + ); // Check that the voting settings have been set. expect(await plugin.minDuration()).to.equal(votingSettings.minDuration); @@ -273,6 +316,9 @@ describe('TokenVoting', function () { // Check that the token has been set. expect(await plugin.getVotingToken()).to.equal(token.address); + + // Check the minimal approval has been set. + expect(await plugin.minApproval()).to.equal(minApproval); }); }); @@ -318,6 +364,13 @@ describe('TokenVoting', function () { expect(await plugin.supportsInterface(getInterfaceId(iface))).to.be.true; }); + it('supports the `IMajorityVoting` OLD interface', async () => { + const {initializedPlugin: plugin} = await loadFixture(globalFixture); + const oldIface = IMajorityVoting_V1_3_0__factory.createInterface(); + expect(await plugin.supportsInterface(getInterfaceId(oldIface))).to.be + .true; + }); + it('supports the `MajorityVotingBase` interface', async () => { const {initializedPlugin: plugin} = await loadFixture(globalFixture); expect( @@ -327,6 +380,15 @@ describe('TokenVoting', function () { ).to.be.true; }); + it('supports the `MajorityVotingBase` OLD interface', async () => { + const {initializedPlugin: plugin} = await loadFixture(globalFixture); + expect( + await plugin.supportsInterface( + getInterfaceId(MAJORITY_VOTING_BASE_OLD_INTERFACE) + ) + ).to.be.true; + }); + it('supports the `TokenVoting` interface', async () => { const {initializedPlugin: plugin} = await loadFixture(globalFixture); const interfaceId = getInterfaceId(TOKEN_VOTING_INTERFACE); @@ -737,7 +799,7 @@ describe('TokenVoting', function () { ).not.to.be.reverted; }); - it('reverts if `_msgSender` doesn not own enough tokens herself/himself and has not tokens delegated to her/him in the current block', async () => { + it('reverts if `_msgSender` does not own enough tokens herself/himself and has not tokens delegated to her/him in the current block', async () => { const { deployer, alice, @@ -2106,7 +2168,7 @@ describe('TokenVoting', function () { // Vote `Yes` with Frank with `tryEarlyExecution` being turned off. The vote is decided now. await plugin.connect(frank).vote(id, VoteOption.Yes, false); - // Check that the proposal can be excuted but didn't execute yet. + // Check that the proposal can be executed but didn't execute yet. expect((await plugin.getProposal(id)).executed).to.equal(false); expect(await plugin.canExecute(id)).to.equal(true); @@ -2449,19 +2511,19 @@ describe('TokenVoting', function () { // Vote `Yes` with Eve with `tryEarlyExecution` being turned on. The vote is not decided yet. await plugin.connect(eve).vote(id, VoteOption.Yes, true); - // Check that the proposal cannot be excuted. + // Check that the proposal cannot be executed. expect((await plugin.getProposal(id)).executed).to.equal(false); expect(await plugin.canExecute(id)).to.equal(false); // Vote `Yes` with Frank with `tryEarlyExecution` being turned off. The vote is decided now. await plugin.connect(frank).vote(id, VoteOption.Yes, false); - // Check that the proposal cannot be excuted. + // Check that the proposal cannot be executed. expect((await plugin.getProposal(id)).executed).to.equal(false); expect(await plugin.canExecute(id)).to.equal(false); // Vote `Yes` with Eve with `tryEarlyExecution` being turned on. The vote is not decided yet. await plugin.connect(grace).vote(id, VoteOption.Yes, true); - // Check that the proposal cannot be excuted. + // Check that the proposal cannot be executed. expect((await plugin.getProposal(id)).executed).to.equal(false); expect(await plugin.canExecute(id)).to.equal(false); }); @@ -2495,7 +2557,7 @@ describe('TokenVoting', function () { }); describe('Different configurations:', async () => { - describe('A simple majority vote with >50% support and >=25% participation required', async () => { + describe('A simple majority vote with >50% support, >=25% participation required and minimal approval >= 21%', async () => { type LocalFixtureResult = { deployer: SignerWithAddress; alice: SignerWithAddress; @@ -2562,10 +2624,16 @@ describe('TokenVoting', function () { minProposerVotingPower: 0, }; + const newMinApproval = pctToRatio(21); + await initializedPlugin .connect(deployer) .updateVotingSettings(newVotingSettings); + await initializedPlugin + .connect(deployer) + .updateMinApprovals(newMinApproval); + return { deployer, alice, @@ -2623,6 +2691,50 @@ describe('TokenVoting', function () { expect(await plugin.canExecute(id)).to.equal(false); }); + it('does not execute if support and participation are high enough but minimal approval is too low', async () => { + const { + alice, + bob, + carol, + initializedPlugin: plugin, + dummyMetadata, + dummyActions, + } = await loadFixture(localFixture); + + const endDate = (await time.latest()) + TIME.DAY; + + await plugin.createProposal( + dummyMetadata, + dummyActions, + 0, + 0, + endDate, + VoteOption.None, + false + ); + const id = 0; + + await voteWithSigners(plugin, id, { + yes: [alice, carol], // 20 votes + no: [bob], // 10 votes + abstain: [], // 0 votes + }); + + expect(await plugin.isMinParticipationReached(id)).to.be.true; + expect(await plugin.isSupportThresholdReachedEarly(id)).to.be.false; + expect(await plugin.isMinApprovalReached(id)).to.be.false; + + expect(await plugin.canExecute(id)).to.be.false; + + await time.increaseTo(endDate); + + expect(await plugin.isMinParticipationReached(id)).to.be.true; + expect(await plugin.isSupportThresholdReached(id)).to.be.true; + expect(await plugin.isMinApprovalReached(id)).to.be.false; + + expect(await plugin.canExecute(id)).to.equal(false); + }); + it('does not execute if participation is high enough but support is too low', async () => { const { alice, @@ -2662,7 +2774,52 @@ describe('TokenVoting', function () { expect(await plugin.canExecute(id)).to.equal(false); }); - it('executes after the duration if participation and support are met', async () => { + it('does not execute if participation and minimal approval are high enough but support is too low', async () => { + const { + alice, + bob, + carol, + dave, + eve, + frank, + grace, + initializedPlugin: plugin, + dummyMetadata, + dummyActions, + } = await loadFixture(localFixture); + const endDate = (await time.latest()) + TIME.DAY; + + await plugin.createProposal( + dummyMetadata, + dummyActions, + 0, + 0, + endDate, + VoteOption.None, + false + ); + const id = 0; + + await voteWithSigners(plugin, id, { + yes: [alice, dave, eve], // 30 votes + no: [bob, carol, frank, grace], // 40 votes + abstain: [], // 0 votes + }); + + expect(await plugin.isMinParticipationReached(id)).to.be.true; + expect(await plugin.isMinApprovalReached(id)).to.be.true; + expect(await plugin.isSupportThresholdReachedEarly(id)).to.be.false; + expect(await plugin.canExecute(id)).to.equal(false); + + await time.increaseTo(endDate); + + expect(await plugin.isMinParticipationReached(id)).to.be.true; + expect(await plugin.isMinApprovalReached(id)).to.be.true; + expect(await plugin.isSupportThresholdReached(id)).to.be.false; + expect(await plugin.canExecute(id)).to.equal(false); + }); + + it('executes after the duration if participation, support and minimal approval are met', async () => { const { alice, bob, @@ -2691,6 +2848,8 @@ describe('TokenVoting', function () { }); expect(await plugin.isMinParticipationReached(id)).to.be.true; + expect(await plugin.isMinApprovalReached(id)).to.be.true; + expect(await plugin.isSupportThresholdReached(id)).to.be.true; expect(await plugin.isSupportThresholdReachedEarly(id)).to.be.false; expect(await plugin.canExecute(id)).to.equal(false); @@ -2698,10 +2857,11 @@ describe('TokenVoting', function () { expect(await plugin.isMinParticipationReached(id)).to.be.true; expect(await plugin.isSupportThresholdReached(id)).to.be.true; + expect(await plugin.isMinApprovalReached(id)).to.be.true; expect(await plugin.canExecute(id)).to.equal(true); }); - it('executes early if participation and support are met and the vote outcome cannot change anymore', async () => { + it('executes early if participation, support and minimal approval are met and the vote outcome cannot change anymore', async () => { const { alice, bob, @@ -2749,7 +2909,7 @@ describe('TokenVoting', function () { }); }); - describe('An edge case with `supportThreshold = 0%`, `minParticipation = 0%`, in early execution mode', async () => { + describe('An edge case with `supportThreshold = 0%`, `minParticipation = 0%`, `minApproval = 0%` in early execution mode', async () => { type LocalFixtureResult = { deployer: SignerWithAddress; alice: SignerWithAddress; @@ -2784,10 +2944,16 @@ describe('TokenVoting', function () { minProposerVotingPower: 0, }; + const minApproval = pctToRatio(0); + await initializedPlugin .connect(deployer) .updateVotingSettings(newVotingSettings); + await initializedPlugin + .connect(deployer) + .updateMinApprovals(minApproval); + return { deployer, alice, @@ -2832,7 +2998,7 @@ describe('TokenVoting', function () { expect(await plugin.canExecute(id)).to.equal(false); }); - it('executes if participation and support are met', async () => { + it('executes if participation, support and min approval are met', async () => { const { alice, initializedPlugin: plugin, @@ -2868,7 +3034,7 @@ describe('TokenVoting', function () { }); }); - describe('An edge case with `supportThreshold = 99.9999%` and `minParticipation = 100%` in early execution mode', async () => { + describe('An edge case with `supportThreshold = 99.9999%`, `minParticipation = 100%` and `minApproval = 100%` in early execution mode', async () => { describe('token balances are in the magnitude of 10^18', async () => { type LocalFixtureResult = { deployer: SignerWithAddress; @@ -2917,10 +3083,16 @@ describe('TokenVoting', function () { minProposerVotingPower: 0, }; + const minApproval = pctToRatio(100); // the largest possible value + await initializedPlugin .connect(deployer) .updateVotingSettings(newVotingSettings); + await initializedPlugin + .connect(deployer) + .updateMinApprovals(minApproval); + return { deployer, alice, @@ -3016,7 +3188,7 @@ describe('TokenVoting', function () { // Vote `yes` with Carol who has close to 0.0001% of the total supply (only 1 vote is missing that Bob has). await plugin.connect(carol).vote(id, VoteOption.Yes, false); - // Check that only 1 vote is missing to meet 100% particpiation. + // Check that only 1 vote is missing to meet 100% participation. const proposal = await plugin.getProposal(id); const tally = proposal.tally; const totalVotingPower = await plugin.totalVotingPower( @@ -3160,7 +3332,7 @@ describe('TokenVoting', function () { await plugin.connect(alice).vote(id, VoteOption.Yes, false); expect(await plugin.isMinParticipationReached(id)).to.be.false; - // 1 vote is still missing to meet particpiation = 100% + // 1 vote is still missing to meet participation = 100% const proposal = await plugin.getProposal(id); const tally = proposal.tally; const totalVotingPower = await plugin.totalVotingPower( 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 b73b5d94..e0430361 100644 --- a/packages/contracts/test/10_unit-testing/12_plugin-setup.ts +++ b/packages/contracts/test/10_unit-testing/12_plugin-setup.ts @@ -28,6 +28,7 @@ import {getNamedTypesFromMetadata} from '@aragon/osx-commons-sdk'; import {TIME} from '@aragon/osx-commons-sdk'; import {pctToRatio} from '@aragon/osx-commons-sdk'; import {DAO} from '@aragon/osx-ethers'; +import {BigNumber} from '@ethersproject/bignumber'; import {loadFixture} from '@nomicfoundation/hardhat-network-helpers'; import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; import {expect} from 'chai'; @@ -49,8 +50,10 @@ type FixtureResult = { symbol: string; }; defaultMintSettings: GovernanceERC20.MintSettingsStruct; + defaultMinApproval: BigNumber; prepareInstallationInputs: string; prepareUninstallationInputs: string; + prepareUpdateBuild3Inputs: string; dao: DAO; governanceERC20Base: GovernanceERC20; governanceWrappedERC20Base: GovernanceWrappedERC20; @@ -97,6 +100,8 @@ async function fixture(): Promise { minProposerVotingPower: 0, }; + const defaultMinApproval = pctToRatio(30); + // Provide installation inputs const prepareInstallationInputs = ethers.utils.defaultAbiCoder.encode( getNamedTypesFromMetadata( @@ -106,6 +111,7 @@ async function fixture(): Promise { Object.values(defaultVotingSettings), Object.values(defaultTokenSettings), Object.values(defaultMintSettings), + defaultMinApproval, ] ); @@ -117,6 +123,14 @@ async function fixture(): Promise { [] ); + // Provide update inputs + const prepareUpdateBuild3Inputs = ethers.utils.defaultAbiCoder.encode( + getNamedTypesFromMetadata( + METADATA.build.pluginSetup.prepareUpdate[3].inputs + ), + [defaultMinApproval] + ); + return { deployer, alice, @@ -126,8 +140,10 @@ async function fixture(): Promise { defaultVotingSettings, defaultTokenSettings, defaultMintSettings, + defaultMinApproval, prepareInstallationInputs, prepareUninstallationInputs, + prepareUpdateBuild3Inputs, dao, governanceERC20Base, governanceWrappedERC20Base, @@ -184,6 +200,7 @@ describe('TokenVotingSetup', function () { dao, defaultVotingSettings, defaultTokenSettings, + defaultMinApproval, } = await loadFixture(fixture); const receivers: string[] = [AddressZero]; @@ -196,6 +213,7 @@ describe('TokenVotingSetup', function () { Object.values(defaultVotingSettings), Object.values(defaultTokenSettings), {receivers, amounts}, + defaultMinApproval, ] ); @@ -226,6 +244,7 @@ describe('TokenVotingSetup', function () { dao, defaultVotingSettings, defaultMintSettings, + defaultMinApproval, } = await loadFixture(fixture); const data = abiCoder.encode( @@ -236,6 +255,7 @@ describe('TokenVotingSetup', function () { Object.values(defaultVotingSettings), [alice.address, '', ''], // Instead of a token address, we pass Alice's address here. Object.values(defaultMintSettings), + defaultMinApproval, ] ); @@ -245,8 +265,13 @@ describe('TokenVotingSetup', function () { }); it('fails if passed token address is not ERC20', async () => { - const {pluginSetup, dao, defaultVotingSettings, defaultMintSettings} = - await loadFixture(fixture); + const { + pluginSetup, + dao, + defaultVotingSettings, + defaultMintSettings, + defaultMinApproval, + } = await loadFixture(fixture); const data = abiCoder.encode( getNamedTypesFromMetadata( @@ -256,6 +281,7 @@ describe('TokenVotingSetup', function () { Object.values(defaultVotingSettings), [dao.address, '', ''], Object.values(defaultMintSettings), + defaultMinApproval, ] ); @@ -272,6 +298,7 @@ describe('TokenVotingSetup', function () { defaultTokenSettings, defaultMintSettings, erc20, + defaultMinApproval, } = await loadFixture(fixture); const nonce = await ethers.provider.getTransactionCount( @@ -299,6 +326,7 @@ describe('TokenVotingSetup', function () { defaultTokenSettings.symbol, ], Object.values(defaultMintSettings), + defaultMinApproval, ] ); @@ -337,6 +365,7 @@ describe('TokenVotingSetup', function () { defaultVotingSettings, defaultMintSettings, erc20, + defaultMinApproval, } = await loadFixture(fixture); const nonce = await ethers.provider.getTransactionCount( @@ -355,6 +384,7 @@ describe('TokenVotingSetup', function () { Object.values(defaultVotingSettings), [erc20.address, 'myName', 'mySymb'], Object.values(defaultMintSettings), + defaultMinApproval, ] ); @@ -382,6 +412,7 @@ describe('TokenVotingSetup', function () { dao, defaultVotingSettings, defaultMintSettings, + defaultMinApproval, } = await loadFixture(fixture); const governanceERC20 = await new GovernanceERC20__factory( @@ -405,6 +436,7 @@ describe('TokenVotingSetup', function () { Object.values(defaultVotingSettings), [governanceERC20.address, '', ''], Object.values(defaultMintSettings), + defaultMinApproval, ] ); @@ -436,13 +468,9 @@ describe('TokenVotingSetup', function () { }); it('correctly returns plugin, helpers and permissions, when a token address is not supplied', async () => { - const { - pluginSetup, - dao, - defaultVotingSettings, - defaultTokenSettings, - defaultMintSettings, - } = await loadFixture(fixture); + const {pluginSetup, dao, prepareInstallationInputs} = await loadFixture( + fixture + ); const nonce = await ethers.provider.getTransactionCount( pluginSetup.address @@ -457,21 +485,13 @@ describe('TokenVotingSetup', function () { nonce: nonce + 1, }); - const data = abiCoder.encode( - getNamedTypesFromMetadata( - METADATA.build.pluginSetup.prepareInstallation.inputs - ), - [ - Object.values(defaultVotingSettings), - Object.values(defaultTokenSettings), - Object.values(defaultMintSettings), - ] - ); - const { plugin, preparedSetupData: {helpers, permissions}, - } = await pluginSetup.callStatic.prepareInstallation(dao.address, data); + } = await pluginSetup.callStatic.prepareInstallation( + dao.address, + prepareInstallationInputs + ); expect(plugin).to.be.equal(anticipatedPluginAddress); expect(helpers.length).to.be.equal(1); @@ -510,6 +530,7 @@ describe('TokenVotingSetup', function () { defaultVotingSettings, defaultTokenSettings, defaultMintSettings, + defaultMinApproval, } = await loadFixture(fixture); const daoAddress = dao.address; @@ -522,6 +543,7 @@ describe('TokenVotingSetup', function () { Object.values(defaultVotingSettings), [AddressZero, defaultTokenSettings.name, defaultTokenSettings.symbol], Object.values(defaultMintSettings), + defaultMinApproval, ] ); @@ -575,7 +597,9 @@ describe('TokenVotingSetup', 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, prepareUpdateBuild3Inputs} = await loadFixture( + fixture + ); const plugin = ethers.Wallet.createRandom().address; // Make a static call to check that the plugin update data being returned is correct. @@ -587,12 +611,17 @@ describe('TokenVotingSetup', function () { ethers.Wallet.createRandom().address, ethers.Wallet.createRandom().address, ], - data: [], + data: prepareUpdateBuild3Inputs, plugin, }); // Check the return data. - expect(initData).to.be.eq('0x'); + expect(initData).to.be.eq( + TokenVoting__factory.createInterface().encodeFunctionData( + 'initializeFrom', + [prepareUpdateBuild3Inputs] + ) + ); expect(helpers).to.be.eql([]); expect(permissions.length).to.be.eql(1); expect(permissions).to.deep.equal([ @@ -607,7 +636,9 @@ describe('TokenVotingSetup', function () { }); it('returns the permissions expected for the update from build 2', async () => { - const {pluginSetup, dao} = await loadFixture(fixture); + const {pluginSetup, dao, prepareUpdateBuild3Inputs} = await loadFixture( + fixture + ); const plugin = ethers.Wallet.createRandom().address; // Make a static call to check that the plugin update data being returned is correct. @@ -619,12 +650,17 @@ describe('TokenVotingSetup', function () { ethers.Wallet.createRandom().address, ethers.Wallet.createRandom().address, ], - data: [], + data: prepareUpdateBuild3Inputs, plugin, }); // Check the return data. - expect(initData).to.be.eq('0x'); + expect(initData).to.be.eq( + TokenVoting__factory.createInterface().encodeFunctionData( + 'initializeFrom', + [prepareUpdateBuild3Inputs] + ) + ); expect(helpers).to.be.eql([]); expect(permissions.length).to.be.eql(1); expect(permissions).to.deep.equal([ diff --git a/packages/contracts/test/10_unit-testing/base/11_majority-voting.ts b/packages/contracts/test/10_unit-testing/base/11_majority-voting.ts index 1f8cb4fa..5eb50bac 100644 --- a/packages/contracts/test/10_unit-testing/base/11_majority-voting.ts +++ b/packages/contracts/test/10_unit-testing/base/11_majority-voting.ts @@ -11,7 +11,11 @@ import { } from '../../../typechain'; import {ProxyCreatedEvent} from '../../../typechain/@aragon/osx-commons-contracts/src/utils/deployment/ProxyFactory'; import {MajorityVotingBase} from '../../../typechain/src/MajorityVotingBase'; -import {MAJORITY_VOTING_BASE_INTERFACE} from '../../test-utils/majority-voting-constants'; +import { + MAJORITY_VOTING_BASE_INTERFACE, + MAJORITY_VOTING_BASE_OLD_INTERFACE, +} from '../../test-utils/majority-voting-constants'; +import {IMajorityVoting_V1_3_0__factory} from '../../test-utils/typechain-versions'; import {VotingMode} from '../../test-utils/voting-helpers'; import {TIME, findEvent} from '@aragon/osx-commons-sdk'; import {getInterfaceId} from '@aragon/osx-commons-sdk'; @@ -19,6 +23,7 @@ import {pctToRatio} from '@aragon/osx-commons-sdk'; import {DAO} from '@aragon/osx-ethers'; import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; import {expect} from 'chai'; +import {BigNumber} from 'ethers'; import {ethers} from 'hardhat'; describe('MajorityVotingMock', function () { @@ -28,6 +33,7 @@ describe('MajorityVotingMock', function () { let dao: DAO; let votingSettings: MajorityVotingBase.VotingSettingsStruct; + let minApproval: BigNumber; before(async () => { signers = await ethers.getSigners(); @@ -44,6 +50,7 @@ describe('MajorityVotingMock', function () { minDuration: TIME.HOUR, minProposerVotingPower: 0, }; + minApproval = pctToRatio(10); const pluginImplementation = await new MajorityVotingMock__factory( signers[0] @@ -70,10 +77,10 @@ describe('MajorityVotingMock', function () { describe('initialize', async () => { it('reverts if trying to re-initialize', async () => { - await votingBase.initializeMock(dao.address, votingSettings); + await votingBase.initializeMock(dao.address, votingSettings, minApproval); await expect( - votingBase.initializeMock(dao.address, votingSettings) + votingBase.initializeMock(dao.address, votingSettings, minApproval) ).to.be.revertedWith('Initializable: contract is already initialized'); }); }); @@ -113,6 +120,12 @@ describe('MajorityVotingMock', function () { .true; }); + it('supports the `IMajorityVoting` OLD interface', async () => { + const oldIface = IMajorityVoting_V1_3_0__factory.createInterface(); + expect(await votingBase.supportsInterface(getInterfaceId(oldIface))).to.be + .true; + }); + it('supports the `MajorityVotingBase` interface', async () => { expect( await votingBase.supportsInterface( @@ -120,11 +133,19 @@ describe('MajorityVotingMock', function () { ) ).to.be.true; }); + + it('supports the `MajorityVotingBase` OLD interface', async () => { + expect( + await votingBase.supportsInterface( + getInterfaceId(MAJORITY_VOTING_BASE_OLD_INTERFACE) + ) + ).to.be.true; + }); }); describe('updateVotingSettings', async () => { beforeEach(async () => { - await votingBase.initializeMock(dao.address, votingSettings); + await votingBase.initializeMock(dao.address, votingSettings, minApproval); }); it('reverts if the support threshold specified equals 100%', async () => { @@ -182,4 +203,24 @@ describe('MajorityVotingMock', function () { ); }); }); + + describe('updateMinApprovals', async () => { + beforeEach(async () => { + await votingBase.initializeMock(dao.address, votingSettings, minApproval); + }); + + it('reverts if the minimum approval specified exceeds 100%', async () => { + minApproval = pctToRatio(1000); + + await expect(votingBase.updateMinApprovals(minApproval)) + .to.be.revertedWithCustomError(votingBase, 'RatioOutOfBounds') + .withArgs(pctToRatio(100), minApproval); + }); + + it('should change the minimum approval successfully', async () => { + await expect(votingBase.updateMinApprovals(minApproval)) + .to.emit(votingBase, 'VotingMinApprovalUpdated') + .withArgs(minApproval); + }); + }); }); 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 3ad28a5a..87fe3f71 100644 --- a/packages/contracts/test/20_integration-testing/22_setup-processing.ts +++ b/packages/contracts/test/20_integration-testing/22_setup-processing.ts @@ -34,6 +34,7 @@ import { DAO, TokenVoting__factory, } from '@aragon/osx-ethers'; +import {BigNumber} from '@ethersproject/bignumber'; import {loadFixture} from '@nomicfoundation/hardhat-network-helpers'; import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; import {expect} from 'chai'; @@ -57,6 +58,10 @@ type FixtureResult = { symbol: string; }; defaultMintSettings: GovernanceERC20.MintSettingsStruct; + defaultMinApproval: BigNumber; + prepareInstallationInputs: string; + prepareInstallData: any; + prepareUpdateData: any; }; async function fixture(): Promise { @@ -123,6 +128,8 @@ async function fixture(): Promise { minProposerVotingPower: 0, }; + const defaultMinApproval = pctToRatio(30); + const defaultTokenSettings = { addr: token.address, name: '', // only relevant if `address(0)` is provided as the token address @@ -134,6 +141,29 @@ async function fixture(): Promise { amounts: [], }; + // Provide uninstallation inputs + const prepareInstallationInputs = ethers.utils.defaultAbiCoder.encode( + getNamedTypesFromMetadata( + METADATA.build.pluginSetup.prepareInstallation.inputs + ), + [ + Object.values(defaultVotingSettings), + Object.values(defaultTokenSettings), + Object.values(defaultMintSettings), + defaultMinApproval, + ] + ); + + const prepareInstallData = { + votingSettings: Object.values(defaultVotingSettings), + tokenSettings: Object.values(defaultTokenSettings), + mintSettings: Object.values(defaultMintSettings), + defaultMinApproval, + }; + + const prepareUpdateData = [defaultMinApproval]; + // Provide update inputs + // const prepareUpdateBuild3Data = [defaultMinApproval]; return { deployer, alice, @@ -146,6 +176,10 @@ async function fixture(): Promise { defaultVotingSettings, defaultTokenSettings, defaultMintSettings, + defaultMinApproval, + prepareInstallationInputs, + prepareInstallData, + prepareUpdateData, }; } @@ -157,9 +191,7 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio psp, dao, pluginSetupRefLatestBuild, - defaultVotingSettings, - defaultTokenSettings, - defaultMintSettings, + prepareInstallationInputs, } = await loadFixture(fixture); // Grant deployer all required permissions @@ -181,25 +213,12 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio .connect(deployer) .grant(dao.address, psp.address, DAO_PERMISSIONS.ROOT_PERMISSION_ID); - const prepareInstallData = { - votingSettings: Object.values(defaultVotingSettings), - tokenSettings: Object.values(defaultTokenSettings), - mintSettings: Object.values(defaultMintSettings), - }; - - const prepareInstallInputType = getNamedTypesFromMetadata( - METADATA.build.pluginSetup.prepareInstallation.inputs - ); - const results = await installPLugin( deployer, psp, dao, pluginSetupRefLatestBuild, - ethers.utils.defaultAbiCoder.encode( - prepareInstallInputType, - Object.values(prepareInstallData) - ) + prepareInstallationInputs ); const plugin = TokenVoting__factory.connect( @@ -239,6 +258,7 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio dao, defaultVotingSettings, pluginSetupRefLatestBuild, + defaultMinApproval, } = await loadFixture(fixture); // Grant deployer all required permissions @@ -264,6 +284,7 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio votingSettings: Object.values(defaultVotingSettings), tokenSettings: [ethers.constants.AddressZero, 'testToken', 'TEST'], mintSettings: [[alice.address], ['1000']], + defaultMinApproval, }; const prepareInstallInputType = getNamedTypesFromMetadata( @@ -315,19 +336,12 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio deployer, psp, dao, - defaultVotingSettings, - defaultTokenSettings, - defaultMintSettings, pluginRepo, pluginSetupRefLatestBuild, + prepareInstallData, + prepareUpdateData, } = await loadFixture(fixture); - const prepareInstallData = { - votingSettings: Object.values(defaultVotingSettings), - tokenSettings: Object.values(defaultTokenSettings), - mintSettings: Object.values(defaultMintSettings), - }; - await updateFromBuildTest( dao, deployer, @@ -336,7 +350,7 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio pluginSetupRefLatestBuild, 1, Object.values(prepareInstallData), - [] + prepareUpdateData ); }); @@ -347,16 +361,10 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio dao, pluginRepo, pluginSetupRefLatestBuild, - defaultVotingSettings, - defaultTokenSettings, - defaultMintSettings, - } = await loadFixture(fixture); - const prepareInstallData = { - votingSettings: Object.values(defaultVotingSettings), - tokenSettings: Object.values(defaultTokenSettings), - mintSettings: Object.values(defaultMintSettings), - }; + prepareInstallData, + prepareUpdateData, + } = await loadFixture(fixture); await updateFromBuildTest( dao, @@ -366,7 +374,7 @@ describe(`PluginSetup processing on network '${productionNetworkName}'`, functio pluginSetupRefLatestBuild, 2, Object.values(prepareInstallData), - [] + prepareUpdateData ); }); }); diff --git a/packages/contracts/test/20_integration-testing/test-helpers.ts b/packages/contracts/test/20_integration-testing/test-helpers.ts index 06aad9d8..75f56b3a 100644 --- a/packages/contracts/test/20_integration-testing/test-helpers.ts +++ b/packages/contracts/test/20_integration-testing/test-helpers.ts @@ -27,6 +27,8 @@ import {expect} from 'chai'; import {ContractTransaction} from 'ethers'; import {ethers} from 'hardhat'; +const latestBuild = 3; + export async function installPLugin( signer: SignerWithAddress, psp: PluginSetupProcessor, @@ -324,7 +326,7 @@ export async function updateFromBuildTest( pluginSetupRefLatestBuild, ethers.utils.defaultAbiCoder.encode( getNamedTypesFromMetadata( - METADATA.build.pluginSetup.prepareUpdate[1].inputs + METADATA.build.pluginSetup.prepareUpdate[latestBuild].inputs ), updateInputs ) diff --git a/packages/contracts/test/30_regression-testing/31_upgradeability.ts b/packages/contracts/test/30_regression-testing/31_upgradeability.ts index 2786e0d5..0651779e 100644 --- a/packages/contracts/test/30_regression-testing/31_upgradeability.ts +++ b/packages/contracts/test/30_regression-testing/31_upgradeability.ts @@ -1,6 +1,7 @@ import {createDaoProxy} from '../20_integration-testing/test-helpers'; import {TestGovernanceERC20} from '../../typechain'; import {MajorityVotingBase} from '../../typechain/src'; +import {INITIALIZE_SIGNATURE} from '../test-utils/token-voting-constants'; import { TokenVoting_V1_0_0__factory, TokenVoting_V1_3_0__factory, @@ -21,6 +22,7 @@ import {DAO, TestGovernanceERC20__factory} from '@aragon/osx-ethers'; import {loadFixture} from '@nomicfoundation/hardhat-network-helpers'; import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; import {expect} from 'chai'; +import {BigNumber} from 'ethers'; import {ethers} from 'hardhat'; describe('Upgrades', () => { @@ -35,8 +37,9 @@ describe('Upgrades', () => { dao.address, defaultInitData.votingSettings, defaultInitData.token.address, + defaultInitData.minApproval, ], - 'initialize', + INITIALIZE_SIGNATURE, currentContractFactory, PLUGIN_UUPS_UPGRADEABLE_PERMISSIONS.UPGRADE_PLUGIN_PERMISSION_ID, dao @@ -124,6 +127,7 @@ type FixtureResult = { defaultInitData: { votingSettings: MajorityVotingBase.VotingSettingsStruct; token: TestGovernanceERC20; + minApproval: BigNumber; }; }; @@ -156,6 +160,7 @@ async function fixture(): Promise { const defaultInitData = { votingSettings, token: token, + minApproval: pctToRatio(10), }; return { diff --git a/packages/contracts/test/test-utils/majority-voting-constants.ts b/packages/contracts/test/test-utils/majority-voting-constants.ts index f58704f0..edc447c3 100644 --- a/packages/contracts/test/test-utils/majority-voting-constants.ts +++ b/packages/contracts/test/test-utils/majority-voting-constants.ts @@ -1,6 +1,17 @@ import {ethers} from 'hardhat'; export const MAJORITY_VOTING_BASE_INTERFACE = new ethers.utils.Interface([ + 'function minDuration()', + 'function minProposerVotingPower()', + 'function votingMode()', + 'function totalVotingPower(uint256)', + 'function getProposal(uint256)', + 'function updateVotingSettings(tuple(uint8,uint32,uint32,uint64,uint256))', + 'function updateMinApprovals(uint256)', + 'function createProposal(bytes,tuple(address,uint256,bytes)[],uint256,uint64,uint64,uint8,bool)', +]); + +export const MAJORITY_VOTING_BASE_OLD_INTERFACE = new ethers.utils.Interface([ 'function minDuration()', 'function minProposerVotingPower()', 'function votingMode()', diff --git a/packages/contracts/test/test-utils/token-voting-constants.ts b/packages/contracts/test/test-utils/token-voting-constants.ts index 47ad4930..3cf0825b 100644 --- a/packages/contracts/test/test-utils/token-voting-constants.ts +++ b/packages/contracts/test/test-utils/token-voting-constants.ts @@ -25,3 +25,8 @@ export const VOTING_EVENTS = { VOTING_SETTINGS_UPDATED: 'VotingSettingsUpdated', VOTE_CAST: 'VoteCast', }; + +export const INITIALIZE_SIGNATURE_OLD = + 'initialize(address,(uint8,uint32,uint32,uint64,uint256),address)'; +export const INITIALIZE_SIGNATURE = + 'initialize(address,(uint8,uint32,uint32,uint64,uint256),address,uint256)'; diff --git a/packages/contracts/test/test-utils/typechain-versions.ts b/packages/contracts/test/test-utils/typechain-versions.ts index 2e52aee3..419fdf48 100644 --- a/packages/contracts/test/test-utils/typechain-versions.ts +++ b/packages/contracts/test/test-utils/typechain-versions.ts @@ -23,3 +23,7 @@ export {GovernanceWrappedERC20__factory as GovernanceWrappedERC20_V1_0_0__factor export {GovernanceWrappedERC20__factory as GovernanceWrappedERC20_V1_3_0__factory} from '../../typechain/factories/@aragon/osx-v1.3.0/token/ERC20/governance/GovernanceWrappedERC20__factory'; export {GovernanceWrappedERC20__factory} from '../../typechain/factories/src/ERC20/governance/GovernanceWrappedERC20__factory'; export {GovernanceWrappedERC20} from '../../typechain/src/ERC20/governance/GovernanceWrappedERC20'; + +/* Majority Voting Base */ +export {IMajorityVoting__factory as IMajorityVoting_V1_3_0__factory} from '../../typechain/factories/@aragon/osx-v1.0.0/plugins/governance/majority-voting/IMajorityVoting__factory'; +export {MajorityVotingBase__factory} from '../../typechain/factories/src/MajorityVotingBase__factory';