Skip to content

Commit

Permalink
Simplifying the membership flow
Browse files Browse the repository at this point in the history
  • Loading branch information
brickpop committed Jan 29, 2024
1 parent cd25fae commit cf83166
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 72 deletions.
15 changes: 2 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,17 +99,6 @@ The alternative would be to fork these base contracts and include them as part o

[Learn more about Aragon OSx](https://devs.aragon.org/docs/osx/how-it-works/framework/)

### Quirks

- The `minProposerVotingPower` setting is ignored. The requirement is just being a member.
- Leave it to just `0`
- The second parameter of `approve()` is ignored on [MemberAccessPlugin](#member-access-plugin). It is assumed that an approval will trigger an early execution whenever possible.
- Leave it to just `false`
- The 4th and 5th parameters on `createProposal()` (startDate and endDate) are ignored
- Leave them to just `0`
- `minDuration` in `MainVotingSettings` defines the proposal duration, not the minimum duration.
- The methods `addAddresses()` and `removeAddresses()` on the [MemberAccessPlugin](#member-access-plugin) are disabled

## How permissions work

For each Space, an Aragon DAO is going to be created to act as the entry point. It will hold any assets and most importantly, manage the permission database which will govern all plugin interactions.
Expand Down Expand Up @@ -370,7 +359,7 @@ Inherited:
- `function canExecute(uint256 _proposalId) returns (bool)`
- `function supportThreshold() returns (uint32)`
- `function minParticipation() returns (uint32)`
- `function minDuration() returns (uint64)`
- `function duration() returns (uint64)`
- `function minProposerVotingPower() returns (uint256)`
- `function votingMode() returns (VotingMode)`
- `function totalVotingPower(uint256 _blockNumber) returns (uint256)`
Expand All @@ -385,7 +374,7 @@ Inherited:
- `event ProposalCreated(uint256 indexed proposalId, address indexed creator, uint64 startDate, uint64 endDate, bytes metadata, IDAO.Action[] actions, uint256 allowFailureMap)`
- `event VoteCast(uint256 indexed proposalId, address indexed voter, VoteOption voteOption, uint256 votingPower)`
- `event ProposalExecuted(uint256 indexed proposalId)`
- `event VotingSettingsUpdated(VotingMode votingMode, uint32 supportThreshold, uint32 minParticipation, uint64 minDuration, uint256 minProposerVotingPower)`
- `event VotingSettingsUpdated(VotingMode votingMode, uint32 supportThreshold, uint32 minParticipation, uint64 duration, uint256 minProposerVotingPower)`

#### Permissions

Expand Down
78 changes: 37 additions & 41 deletions packages/contracts/src/governance/MainVotingPlugin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,23 @@ pragma solidity ^0.8.8;
import {IDAO, PluginUUPSUpgradeable} from "@aragon/osx/core/plugin/PluginUUPSUpgradeable.sol";
import {SafeCastUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol";
import {PermissionManager} from "@aragon/osx/core/permission/PermissionManager.sol";
import {IMembership} from "@aragon/osx/core/plugin/membership/IMembership.sol";
import {Addresslist} from "@aragon/osx/plugins/utils/Addresslist.sol";
import {RATIO_BASE, _applyRatioCeiled} from "@aragon/osx/plugins/utils/Ratio.sol";
import {IMajorityVoting} from "@aragon/osx/plugins/governance/majority-voting/IMajorityVoting.sol";
import {MajorityVotingBase} from "./base/MajorityVotingBase.sol";
import {IMembers} from "./base/IMembers.sol";
import {IEditors} from "./base/IEditors.sol";
import {Addresslist} from "./base/Addresslist.sol";

// The [ERC-165](https://eips.ethereum.org/EIPS/eip-165) interface ID of the contract.
bytes4 constant MAIN_SPACE_VOTING_INTERFACE_ID = MainVotingPlugin.initialize.selector ^
MainVotingPlugin.addAddresses.selector ^
MainVotingPlugin.removeAddresses.selector ^
MainVotingPlugin.isMember.selector ^
MainVotingPlugin.isEditor.selector;
MainVotingPlugin.createProposal.selector ^
MainVotingPlugin.cancelProposal.selector;

/// @title MainVotingPlugin
/// @title MainVotingPlugin (Address list)
/// @author Aragon - 2023
/// @notice The majority voting implementation using a list of member addresses.
/// @dev This contract inherits from `MajorityVotingBase` and implements the `IMajorityVoting` interface.
contract MainVotingPlugin is IMembership, Addresslist, MajorityVotingBase {
contract MainVotingPlugin is Addresslist, MajorityVotingBase, IEditors, IMembers {
using SafeCastUpgradeable for uint256;

/// @notice The ID of the permission required to call the `addAddresses` and `removeAddresses` functions.
Expand All @@ -46,12 +45,6 @@ contract MainVotingPlugin is IMembership, Addresslist, MajorityVotingBase {
/// @notice Raised when a wallet who is not an editor or a member attempts to do something
error NotAMember(address caller);

/// @notice Raised when an address is defined as a space member
event NewSpaceMember(address account);

/// @notice Raised when an address is removed as a space member
event RemovedSpaceMember(address account);

/// @notice Raised when someone who didn't create a proposal attempts to cancel it
error OnlyCreatorCanCancel();

Expand All @@ -77,7 +70,7 @@ contract MainVotingPlugin is IMembership, Addresslist, MajorityVotingBase {
__MajorityVotingBase_init(_dao, _votingSettings);

_addAddresses(_initialEditors);
emit MembersAdded({members: _initialEditors});
emit EditorsAdded(_initialEditors);
}

/// @notice Checks if this or the parent contract supports an interface by its ID.
Expand All @@ -87,66 +80,69 @@ contract MainVotingPlugin is IMembership, Addresslist, MajorityVotingBase {
return
_interfaceId == MAIN_SPACE_VOTING_INTERFACE_ID ||
_interfaceId == type(Addresslist).interfaceId ||
_interfaceId == type(IMembership).interfaceId ||
_interfaceId == type(MajorityVotingBase).interfaceId ||
_interfaceId == type(IMembers).interfaceId ||
_interfaceId == type(IEditors).interfaceId ||
super.supportsInterface(_interfaceId);
}

/// @notice Adds new editors to the address list.
/// @param _editors The addresses of the editors to be added. NOTE: Only one member can be added at a time.
/// @param _account The address of the new editor.
/// @dev This function is used during the plugin initialization.
function addAddresses(
address[] calldata _editors
) external auth(UPDATE_ADDRESSES_PERMISSION_ID) {
if (_editors.length > 1) revert OnlyOneEditorPerCall(_editors.length);
function addEditor(address _account) external auth(UPDATE_ADDRESSES_PERMISSION_ID) {
if (isEditor(_account)) return;

address[] memory _editors = new address[](1);
_editors[0] = _account;

_addAddresses(_editors);
emit MembersAdded({members: _editors});
emit EditorAdded(_account);
}

/// @notice Removes existing editors from the address list.
/// @param _editors The addresses of the editors to be removed. NOTE: Only one member can be removed at a time.
function removeAddresses(
address[] calldata _editors
) external auth(UPDATE_ADDRESSES_PERMISSION_ID) {
if (_editors.length > 1) revert OnlyOneEditorPerCall(_editors.length);
/// @param _account The addresses of the editors to be removed. NOTE: Only one member can be removed at a time.
function removeEditor(address _account) external auth(UPDATE_ADDRESSES_PERMISSION_ID) {
if (!isEditor(_account)) return;
else if (addresslistLength() <= 1) revert NoEditorsLeft();

address[] memory _editors = new address[](1);
_editors[0] = _account;

_removeAddresses(_editors);
emit MembersRemoved({members: _editors});
emit EditorRemoved(_account);
}

/// @notice Defines the given address as a new space member that can create proposals.
/// @param _account The address of the space member to be added.
/// @dev This function is used during the plugin initialization.
function addSpaceMember(address _account) external auth(UPDATE_ADDRESSES_PERMISSION_ID) {
function addMember(address _account) external auth(UPDATE_ADDRESSES_PERMISSION_ID) {
if (members[_account]) return;

members[_account] = true;
emit NewSpaceMember({account: _account});
emit MemberAdded(_account);
}

/// @notice Removes the given address as a proposal creator.
/// @param _account The address of the space member to be removed.
function removeSpaceMember(address _account) external auth(UPDATE_ADDRESSES_PERMISSION_ID) {
function removeMember(address _account) external auth(UPDATE_ADDRESSES_PERMISSION_ID) {
if (!members[_account]) return;

members[_account] = false;
emit RemovedSpaceMember({account: _account});
emit MemberRemoved(_account);
}

/// @inheritdoc MajorityVotingBase
function totalVotingPower(uint256 _blockNumber) public view override returns (uint256) {
return addresslistLengthAtBlock(_blockNumber);
/// @notice Returns whether the given address is currently listed as an editor
function isEditor(address _account) public view returns (bool) {
return isListed(_account);
}

/// @notice Returns whether the given address holds membership/editor permission on the main voting plugin
function isMember(address _account) public view returns (bool) {
return members[_account] || isEditor(_account);
}

/// @notice Returns whether the given address is currently listed as an editor
function isEditor(address _account) public view returns (bool) {
return isListed(_account);
/// @inheritdoc MajorityVotingBase
function totalVotingPower(uint256 _blockNumber) public view override returns (uint256) {
return addresslistLengthAtBlock(_blockNumber);
}

/// @inheritdoc MajorityVotingBase
Expand All @@ -167,7 +163,7 @@ contract MainVotingPlugin is IMembership, Addresslist, MajorityVotingBase {
_creator: _msgSender(),
_metadata: _metadata,
_startDate: _startDate,
_endDate: _startDate + minDuration(),
_endDate: _startDate + duration(),
_actions: _actions,
_allowFailureMap: _allowFailureMap
});
Expand All @@ -176,7 +172,7 @@ contract MainVotingPlugin is IMembership, Addresslist, MajorityVotingBase {
Proposal storage proposal_ = proposals[proposalId];

proposal_.parameters.startDate = _startDate;
proposal_.parameters.endDate = _startDate + minDuration();
proposal_.parameters.endDate = _startDate + duration();
proposal_.parameters.snapshotBlock = snapshotBlock;
proposal_.parameters.votingMode = votingMode();
proposal_.parameters.supportThreshold = supportThreshold();
Expand Down
6 changes: 3 additions & 3 deletions packages/contracts/src/governance/MemberAccessPlugin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ bytes4 constant MEMBER_ACCESS_INTERFACE_ID = MemberAccessPlugin.initialize.selec
MemberAccessPlugin.proposeRemoveMember.selector ^
MemberAccessPlugin.getProposal.selector;

/// @title Multisig - Release 1, Build 1
/// @title Member access plugin (Multisig) - Release 1, Build 1
/// @author Aragon - 2023
/// @notice The on-chain multisig governance plugin in which a proposal passes if X out of Y approvals are met.
contract MemberAccessPlugin is IMultisig, PluginUUPSUpgradeable, ProposalUpgradeable {
Expand Down Expand Up @@ -238,7 +238,7 @@ contract MemberAccessPlugin is IMultisig, PluginUUPSUpgradeable, ProposalUpgrade
_actions[0] = IDAO.Action({
to: address(multisigSettings.mainVotingPlugin),
value: 0,
data: abi.encodeCall(MainVotingPlugin.addSpaceMember, (_proposedMember))
data: abi.encodeCall(MainVotingPlugin.addMember, (_proposedMember))
});

return createProposal(_metadata, _actions);
Expand All @@ -262,7 +262,7 @@ contract MemberAccessPlugin is IMultisig, PluginUUPSUpgradeable, ProposalUpgrade
_actions[0] = IDAO.Action({
to: address(multisigSettings.mainVotingPlugin),
value: 0,
data: abi.encodeCall(MainVotingPlugin.removeSpaceMember, (_proposedMember))
data: abi.encodeCall(MainVotingPlugin.removeMember, (_proposedMember))
});

return createProposal(_metadata, _actions);
Expand Down
96 changes: 96 additions & 0 deletions packages/contracts/src/governance/base/Addresslist.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

pragma solidity ^0.8.8;

import {CheckpointsUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/CheckpointsUpgradeable.sol";
import {_uncheckedAdd, _uncheckedSub} from "@aragon/osx/utils/UncheckedMath.sol";

/// @title Addresslist
/// @author Aragon Association - 2021-2024
/// @notice The majority voting implementation using a list of member addresses.
/// @dev This contract inherits from `MajorityVotingBase` and implements the `IMajorityVoting` interface.
abstract contract Addresslist {
using CheckpointsUpgradeable for CheckpointsUpgradeable.History;

/// @notice The mapping containing the checkpointed history of the address list.
mapping(address => CheckpointsUpgradeable.History) private _addresslistCheckpoints;

/// @notice The checkpointed history of the length of the address list.
CheckpointsUpgradeable.History private _addresslistLengthCheckpoints;

/// @notice Thrown when the address list update is invalid, which can be caused by the addition of an existing member or removal of a non-existing member.
/// @param member The array of member addresses to be added or removed.
error InvalidAddresslistUpdate(address member);

/// @notice Checks if an account is on the address list at a specific block number.
/// @param _account The account address being checked.
/// @param _blockNumber The block number.
/// @return Whether the account is listed at the specified block number.
function isListedAtBlock(
address _account,
uint256 _blockNumber
) public view virtual returns (bool) {
return _addresslistCheckpoints[_account].getAtBlock(_blockNumber) == 1;
}

/// @notice Checks if an account is currently on the address list.
/// @param _account The account address being checked.
/// @return Whether the account is currently listed.
function isListed(address _account) public view virtual returns (bool) {
return _addresslistCheckpoints[_account].latest() == 1;
}

/// @notice Returns the length of the address list at a specific block number.
/// @param _blockNumber The specific block to get the count from. If `0`, then the latest checkpoint value is returned.
/// @return The address list length at the specified block number.
function addresslistLengthAtBlock(uint256 _blockNumber) public view virtual returns (uint256) {
return _addresslistLengthCheckpoints.getAtBlock(_blockNumber);
}

/// @notice Returns the current length of the address list.
/// @return The current address list length.
function addresslistLength() public view virtual returns (uint256) {
return _addresslistLengthCheckpoints.latest();
}

/// @notice Internal function to add new addresses to the address list.
/// @param _newAddresses The new addresses to be added.
function _addAddresses(address[] memory _newAddresses) internal virtual {
for (uint256 i; i < _newAddresses.length; ) {
if (isListed(_newAddresses[i])) {
revert InvalidAddresslistUpdate(_newAddresses[i]);
}

// Mark the address as listed
_addresslistCheckpoints[_newAddresses[i]].push(1);

unchecked {
++i;
}
}
_addresslistLengthCheckpoints.push(_uncheckedAdd, _newAddresses.length);
}

/// @notice Internal function to remove existing addresses from the address list.
/// @param _exitingAddresses The existing addresses to be removed.
function _removeAddresses(address[] memory _exitingAddresses) internal virtual {
for (uint256 i; i < _exitingAddresses.length; ) {
if (!isListed(_exitingAddresses[i])) {
revert InvalidAddresslistUpdate(_exitingAddresses[i]);
}

// Mark the address as not listed
_addresslistCheckpoints[_exitingAddresses[i]].push(0);

unchecked {
++i;
}
}
_addresslistLengthCheckpoints.push(_uncheckedSub, _exitingAddresses.length);
}

/// @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
uint256[48] private __gap;
}
24 changes: 24 additions & 0 deletions packages/contracts/src/governance/base/IEditors.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

pragma solidity ^0.8.8;

/// @title IEditors
/// @author Aragon Association - 2024
interface IEditors {
/// @notice Emitted when an editors are added to the DAO plugin.
/// @param editors The addresses of the new editors.
event EditorsAdded(address[] editors);

/// @notice Emitted when an editor is added to the DAO plugin.
/// @param editor The address of the new editor.
event EditorAdded(address editor);

/// @notice Emitted when an editor is removed from the DAO plugin.
/// @param editor The address of the editor being removed.
event EditorRemoved(address editor);

/// @notice Checks if an account is an editor on the DAO.
/// @param _account The address of the account to be checked.
/// @return Whether the account is an editor or not.
function isEditor(address _account) external view returns (bool);
}
21 changes: 21 additions & 0 deletions packages/contracts/src/governance/base/IMembers.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

pragma solidity ^0.8.8;

/// @title IMembers
/// @author Aragon Association - 2024
/// @notice An interface to be implemented by DAO plugins that define membership.
interface IMembers {
/// @notice Emitted when a member is added to the DAO plugin.
/// @param member The address of the new member being added.
event MemberAdded(address member);

/// @notice Emitted when member is removed from the DAO plugin.
/// @param member The address of the existing member being removed.
event MemberRemoved(address member);

/// @notice Checks if an account is a member.
/// @param _account The address of the account to be checked.
/// @return Whether the account is a member or not.
function isMember(address _account) external view returns (bool);
}
Loading

0 comments on commit cf83166

Please sign in to comment.