Skip to content

Commit

Permalink
Adding an access contract for personal spaces
Browse files Browse the repository at this point in the history
  • Loading branch information
brickpop committed Aug 7, 2024
1 parent da3352c commit 62e1520
Show file tree
Hide file tree
Showing 3 changed files with 331 additions and 8 deletions.
11 changes: 6 additions & 5 deletions packages/contracts/src/governance/MemberAccessPlugin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,15 @@ contract MemberAccessPlugin is IMultisig, PluginUUPSUpgradeable, ProposalUpgrade
/// @param parameters The proposal-specific approve settings at the time of the proposal creation.
/// @param approvers The approves casted by the approvers.
/// @param actions The actions to be executed when the proposal passes.
/// @param _failsafeActionMap A bitmap allowing the proposal to succeed, even if certain actions might revert. 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.
/// @param destinationPlugin The address of the plugin where addMember should be called.
/// @param failsafeActionMap A bitmap allowing the proposal to succeed, even if certain actions might revert. 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 {
bool executed;
uint16 approvals;
ProposalParameters parameters;
mapping(address => bool) approvers;
IDAO.Action[] actions;
MainVotingPlugin mainVotingPlugin;
MainVotingPlugin destinationPlugin;
uint256 failsafeActionMap;
}

Expand Down Expand Up @@ -152,7 +153,7 @@ contract MemberAccessPlugin is IMultisig, PluginUUPSUpgradeable, ProposalUpgrade

/// @notice Creates a proposal to add a new member.
/// @param _metadata The metadata of the proposal.
/// @param _proposedMember The address of the member who may eveutnally be added.
/// @param _proposedMember The address of the member who may eventually be added.
/// @param _proposer The address to use as the proposal creator.
/// @return proposalId The ID of the proposal.
function proposeAddMember(
Expand Down Expand Up @@ -212,7 +213,7 @@ contract MemberAccessPlugin is IMultisig, PluginUUPSUpgradeable, ProposalUpgrade
proposal_.parameters.startDate = _startDate;
proposal_.parameters.endDate = _endDate;

proposal_.mainVotingPlugin = MainVotingPlugin(msg.sender);
proposal_.destinationPlugin = MainVotingPlugin(msg.sender);
for (uint256 i; i < _actions.length; ) {
proposal_.actions.push(_actions[i]);
unchecked {
Expand Down Expand Up @@ -360,7 +361,7 @@ contract MemberAccessPlugin is IMultisig, PluginUUPSUpgradeable, ProposalUpgrade
if (!_isProposalOpen(proposal_)) {
// The proposal was executed already
return false;
} else if (!proposal_.mainVotingPlugin.isEditor(_account)) {
} else if (!proposal_.destinationPlugin.isEditor(_account)) {
// The approver has no voting power.
return false;
} else if (proposal_.approvers[_account]) {
Expand Down
319 changes: 319 additions & 0 deletions packages/contracts/src/personal/PersonalAccessHelper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

pragma solidity ^0.8.8;

import {SafeCastUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol";

import {IDAO} from "@aragon/osx/core/dao/IDAO.sol";
import {PermissionManager} from "@aragon/osx/core/permission/PermissionManager.sol";
import {ProposalUpgradeable} from "@aragon/osx/core/plugin/proposal/ProposalUpgradeable.sol";
import {PluginCloneable} from "@aragon/osx/core/plugin/PluginCloneable.sol";
import {IEditors} from "../base/IEditors.sol";
import {PersonalSpaceAdminPlugin} from "./PersonalSpaceAdminPlugin.sol";

bytes4 constant PERSONAL_ACCESS_INTERFACE_ID = PersonalAccessPlugin.initialize.selector ^
PersonalAccessPlugin.updatePluginSettings.selector ^
PersonalAccessPlugin.proposeAddMember.selector ^
PersonalAccessPlugin.getProposal.selector;

/// @title Personal access plugin (SingleApproval) - Release 1, Build 1
/// @author Aragon - 2024
/// @notice The on-chain governance plugin in which a proposal passes when approved by the first editor
contract PersonalAccessPlugin is PluginCloneable, ProposalUpgradeable {
using SafeCastUpgradeable for uint256;

/// @notice The ID of the permission required to call the `addAddresses` functions.
bytes32 public constant UPDATE_PLUGIN_SETTINGS_PERMISSION_ID =
keccak256("UPDATE_PLUGIN_SETTINGS_PERMISSION");

/// @notice The ID of the permission required to create new membership proposals.
bytes32 public constant PROPOSER_PERMISSION_ID = keccak256("PROPOSER_PERMISSION");

/// @notice A container for proposal-related information.
/// @param executed Whether the proposal is executed or not.
/// @param approvals The number of approvals casted.
/// @param parameters The proposal-specific approve settings at the time of the proposal creation.
/// @param approvers The approves casted by the approvers.
/// @param actions The actions to be executed when the proposal passes.
/// @param destinationPlugin The address of the plugin where addMember should be called.
/// @param failsafeActionMap A bitmap allowing the proposal to succeed, even if certain actions might revert. 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 {
bool executed;
ProposalParameters parameters;
IDAO.Action[] actions;
PersonalSpaceAdminPlugin destinationPlugin;
uint256 failsafeActionMap;
}

/// @notice A container for the proposal parameters.
/// @param startDate The timestamp when the proposal starts.
/// @param endDate The timestamp when the proposal expires.
struct ProposalParameters {
uint64 startDate;
uint64 endDate;
}

/// @notice A container for the plugin settings.
/// @param proposalDuration The amount of time before a non-approved proposal expires.
struct PluginSettings {
uint64 proposalDuration;
}

/// @notice A mapping between proposal IDs and proposal information.
mapping(uint256 => Proposal) internal proposals;

/// @notice The current plugin settings.
PluginSettings public pluginSettings;

/// @notice Keeps track at which block number the plugin settings have been changed the last time.
/// @dev This variable prevents a proposal from being created in the same block in which the plugin settings change.
uint64 public lastPluginSettingsChange;

/// @notice Thrown when creating a proposal at the same block that the settings were changed.
error ProposalCreationForbiddenOnSameBlock();

/// @notice Thrown if an approver is not allowed to cast an approve. This can be because the proposal
/// - is not open,
/// - was executed, or
/// - the approver is not on the address list
/// @param proposalId The ID of the proposal.
/// @param sender The address of the sender.
error ApprovalCastForbidden(uint256 proposalId, address sender);

/// @notice Thrown if the proposal execution is forbidden.
/// @param proposalId The ID of the proposal.
error ProposalExecutionForbidden(uint256 proposalId);

/// @notice Thrown when called from an incompatible contract.
error InvalidInterface();

/// @notice Emitted when a proposal is approved by an editor.
/// @param proposalId The ID of the proposal.
/// @param editor The editor casting the approve.
event Approved(uint256 indexed proposalId, address indexed editor);

/// @notice Emitted when a proposal is rejected by an editor.
/// @param proposalId The ID of the proposal.
/// @param editor The editor casting the rejection.
event Rejected(uint256 indexed proposalId, address indexed editor);

/// @notice Emitted when the plugin settings are set.
/// @param proposalDuration The amount of time before a non-approved proposal expires.
event PluginSettingsUpdated(uint64 proposalDuration);

/// @notice Initializes Release 1, Build 1.
/// @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 _pluginSettings The multisig settings.
function initialize(
IDAO _dao,
PluginSettings calldata _pluginSettings
) external virtual initializer {
__PluginCloneable_init(_dao);

_updatePluginSettings(_pluginSettings);
}

/// @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(PluginCloneable, ProposalUpgradeable) returns (bool) {
return
_interfaceId == PERSONAL_ACCESS_INTERFACE_ID || super.supportsInterface(_interfaceId);
}

/// @notice Updates the plugin settings.
/// @param _pluginSettings The new settings.
function updatePluginSettings(
PluginSettings calldata _pluginSettings
) external auth(UPDATE_PLUGIN_SETTINGS_PERMISSION_ID) {
_updatePluginSettings(_pluginSettings);
}

/// @notice Creates a proposal to add a new member.
/// @param _metadata The metadata of the proposal.
/// @param _proposedMember The address of the member who may eventually be added.
/// @param _proposer The address to use as the proposal creator.
/// @return proposalId The ID of the proposal.
function proposeAddMember(
bytes calldata _metadata,
address _proposedMember,
address _proposer
) public auth(PROPOSER_PERMISSION_ID) returns (uint256 proposalId) {
// Check that the caller supports the `addMember` function
if (!PersonalSpaceAdminPlugin(msg.sender).supportsInterface(type(IEditors).interfaceId)) {
revert InvalidInterface();
}

// Build the list of actions
IDAO.Action[] memory _actions = new IDAO.Action[](1);

_actions[0] = IDAO.Action({
to: address(msg.sender), // We are called by the PersonalSpaceAdminPlugin
value: 0,
data: abi.encodeCall(PersonalSpaceAdminPlugin.submitNewMember, (_proposedMember))
});

// Create proposal
uint64 snapshotBlock;
unchecked {
snapshotBlock = block.number.toUint64() - 1; // The snapshot block must be mined already to protect the transaction against backrunning transactions causing census changes.
}

// Revert if the settings have been changed in the same block as this proposal should be created in.
// This prevents a malicious party from voting with previous addresses and the new settings.
if (lastPluginSettingsChange > snapshotBlock) {
revert ProposalCreationForbiddenOnSameBlock();
}

uint64 _startDate = block.timestamp.toUint64();
uint64 _endDate = _startDate + pluginSettings.proposalDuration;

proposalId = _createProposalId();

emit ProposalCreated({
proposalId: proposalId,
creator: _proposer,
metadata: _metadata,
startDate: _startDate,
endDate: _endDate,
actions: _actions,
allowFailureMap: uint8(0)
});

// Create the proposal
Proposal storage proposal_ = proposals[proposalId];

proposal_.parameters.startDate = _startDate;
proposal_.parameters.endDate = _endDate;

proposal_.destinationPlugin = PersonalSpaceAdminPlugin(msg.sender);
for (uint256 i; i < _actions.length; ) {
proposal_.actions.push(_actions[i]);
unchecked {
++i;
}
}

// An editor needs to approve. If the proposer is an editor, approve right away.
if (PersonalSpaceAdminPlugin(msg.sender).isEditor(_proposer)) {
_approve(proposalId, _proposer);
}
}

/// @param _proposalId The Id of the proposal to approve.
function approve(uint256 _proposalId) public {
_approve(_proposalId, msg.sender);
}

/// @notice Internal implementation, allowing proposeAddMember() to specify the approver.
function _approve(uint256 _proposalId, address _approver) internal {
if (!_canApprove(_proposalId, _approver)) {
revert ApprovalCastForbidden(_proposalId, _approver);
}

emit Approved({proposalId: _proposalId, editor: _approver});

_execute(_proposalId);
}

/// @notice Rejects the given proposal immediately.
/// @param _proposalId The Id of the proposal to reject.
function reject(uint256 _proposalId) public {
if (!_canApprove(_proposalId, msg.sender)) {
revert ApprovalCastForbidden(_proposalId, msg.sender);
}

Proposal storage proposal_ = proposals[_proposalId];

// Prevent any further approvals, expire it
proposal_.parameters.endDate = block.timestamp.toUint64();

emit Rejected({proposalId: _proposalId, editor: msg.sender});
}

function canApprove(uint256 _proposalId, address _account) external view returns (bool) {
return _canApprove(_proposalId, _account);
}

/// @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.
/// @return parameters The parameters of the proposal vote.
/// @return actions The actions to be executed in the associated DAO after the proposal has passed.
/// @param failsafeActionMap A bitmap allowing the proposal to succeed, even if individual actions might revert. 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.
function getProposal(
uint256 _proposalId
)
public
view
returns (
bool executed,
ProposalParameters memory parameters,
IDAO.Action[] memory actions,
uint256 failsafeActionMap
)
{
Proposal storage proposal_ = proposals[_proposalId];

executed = proposal_.executed;
parameters = proposal_.parameters;
actions = proposal_.actions;
failsafeActionMap = proposal_.failsafeActionMap;
}

/// @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 {
Proposal storage proposal_ = proposals[_proposalId];

proposal_.executed = true;

_executeProposal(
dao(),
_proposalId,
proposals[_proposalId].actions,
proposals[_proposalId].failsafeActionMap
);
}

/// @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.
/// @return Returns `true` if the given account can approve on a certain proposal and `false` otherwise.
function _canApprove(uint256 _proposalId, address _account) internal view returns (bool) {
Proposal storage proposal_ = proposals[_proposalId];

if (!_isProposalOpen(proposal_)) {
// The proposal was executed already
return false;
} else if (!proposal_.destinationPlugin.isEditor(_account)) {
// The approver has no voting power.
return false;
}

return true;
}

/// @notice Internal function to check if a proposal vote is still open.
/// @param proposal_ The proposal struct.
/// @return True if the proposal vote is open, false otherwise.
function _isProposalOpen(Proposal storage proposal_) internal view returns (bool) {
uint64 currentTimestamp64 = block.timestamp.toUint64();
return
!proposal_.executed &&
proposal_.parameters.startDate <= currentTimestamp64 &&
proposal_.parameters.endDate >= currentTimestamp64;
}

/// @notice Internal function to update the plugin settings.
/// @param _pluginSettings The new settings.
function _updatePluginSettings(PluginSettings calldata _pluginSettings) internal {
pluginSettings = _pluginSettings;
lastPluginSettingsChange = block.number.toUint64();

emit PluginSettingsUpdated({proposalDuration: _pluginSettings.proposalDuration});
}
}
9 changes: 6 additions & 3 deletions packages/contracts/src/personal/PersonalSpaceAdminPlugin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ contract PersonalSpaceAdminPlugin is PluginCloneable, ProposalUpgradeable, IEdit
this.submitEdits.selector ^
this.submitAcceptSubspace.selector ^
this.submitRemoveSubspace.selector ^
this.submitNewMember.selector ^
this.addMember.selector ^
this.submitRemoveMember.selector ^
this.submitNewEditor.selector ^
this.submitRemoveEditor.selector ^
this.leaveSpace.selector;

/// @notice The ID of the permission required to call the `addMember` function.
bytes32 public constant ADD_MEMBER_PERMISSION_ID = keccak256("ADD_MEMBER_PERMISSION");

/// @notice Raised when a wallet who is not an editor or a member attempts to do something
error NotAMember(address caller);

Expand Down Expand Up @@ -139,9 +142,9 @@ contract PersonalSpaceAdminPlugin is PluginCloneable, ProposalUpgradeable, IEdit
// The event will be emitted by the space plugin
}

/// @notice Creates and executes a proposal that makes the DAO grant membership permission to the given address
/// @notice Creates and executes a proposal that makes the DAO grant membership permission to the given address.
/// @param _newMember The address to grant member permission to
function submitNewMember(address _newMember) public auth(EDITOR_PERMISSION_ID) {
function addMember(address _newMember) public auth(ADD_MEMBER_PERMISSION_ID) {
IDAO.Action[] memory _actions = new IDAO.Action[](1);
_actions[0].to = address(dao());
_actions[0].data = abi.encodeCall(
Expand Down

0 comments on commit 62e1520

Please sign in to comment.