The following project contains the plugin smart contracts providing the foundation of the Geo Browser project. See packages/contracts
and packages/contracts-ethers
.
cp .env.template .env
Add your Infura API key and then, run:
yarn
cd packages/contracts
yarn build
yarn test
See the deployment checklist
A Space is composed by a DAO and several plugins installed on it. The DAO contract holds all the assets and rights to act on external components, while plugins are custom, composable, opt-in pieces of logic that can perform certain actions governed by the DAO's permission database.
The DAO contract can be deployed by using Aragon's DAOFactory contract. This will deploy a new DAO with the desired plugins and their respective settings.
The current repository provides the plugins necessary to cover two use cases:
- A standard space where members propose changes and editors vote on them
- Space plugin
- Member Access plugin
- Main Voting plugin
- A personal space, where editors apply changes immediately
- Space plugin
- Personal Space Admin plugin
In standard spaces, members can create proposals while editors can vote on them. Approved proposals can be executed by anyone and this will make the DAO call the predefined proposal actions.
The most typical case will be telling the Space Plugin to emit the event of a proposal being processed. It can include emitting a new hash for the contents or accepting a subspace.
The Main Voting Plugin can also pass proposals that change its own settings.
To manage who can create proposals, the Member Access Plugin allows anyone to request becoming a member. Editors can approve or reject incoming proposals.
Personal spaces are a simplified version, where anyone defined as editor can immediatelly execute proposals. Typically to edit the contents of a space.
Editors may also execute proposals who define new editors.
There's an optional feature, where a predefined address can execute the actions to upgrade a plugin to the latest published version.
- When the
MainVotingPlugin
is installed, an initial editor is defined
- Someone calls
proposeAddMember()
on theMainVotingPlugin
- If the caller is the only editor, the proposal succeeds immediately
- The proposal is created on the
MemberAccessPlugin
because the governance rules differ from the rest of proposals
- One of the editors calls
approve()
orreject()
- Calling
approve()
makes the proposal succeed - Calling
reject()
cancels the proposal
- Calling
- A succeeded proposal is executed automatically
- This makes the DAO call
addMember()
on the main voting plugin
- This makes the DAO call
- An editor or a member creates a proposal
- Editors can vote on it for a predefined amount of time
- If the proposal exceeds the required quorum and support, the proposal succeeds
- Succeeded proposals can be executed by anyone
- When a proposal regarding the space is passed, the
MainVotingPlugin
will callexecute()
on the DAO - The actions from the proposal will target the
publishEdits()
,acceptSubspace()
orremoveSubspace()
functions on theSpacePlugin
. - The
SpacePlugin
will be called by the DAO and emit the corresponding events - An external indexer will fetch all these events and update the current state of this specific space
The implementation of the four plugins is built on top of existing and thoroughly autited plugins from Aragon OSx. In order to fully accomodate to Geo's design, some functions and interfaces needed a few tweaks, which made it necessary to fork and adapt these contracts in-place.
They can be found on packages/contracts/src/governance/base
Each Space created is an Aragon DAO. It holds any assets and most importantly, it manages the permission database that governs all plugin interactions.
A permission looks like:
- An address
who
holdsMY_PERMISSION_ID
on a target contractwhere
New DAO's deployed manyally will grant ROOT_PERMISSION
to its creator. However, most DAO's are typically deployed via Aragon's DAO factory, which will deploy a new contract with safe defaults, install all the requested plugins and drop the ROOT permission when the set up is done.
Managing permissions is made via two functions that are called on the DAO:
function grant(address _where, address _who, bytes32 _permissionId);
function revoke(address _where, address _who, bytes32 _permissionId);
For the cases where an unrestricted permission is not derisable, a Permission Condition can be used.
Conditional permissions look like this:
- An address
who
holdsMY_PERMISSION_ID
on a target contractwhere
, onlywhen
the condition contract approves it
Conditional permissions are granted like this:
function grantWithCondition(
address _where,
address _who,
bytes32 _permissionId,
IPermissionCondition _condition
);
See the MemberAccessExecuteCondition
contract. It restricts what the MemberAccessPlugin can execute on the DAO.
Learn more about OSx permissions
For standard governance spaces:
Below are all the permissions that a PluginSetup contract may want to request:
Proposal:
EDITOR_PERMISSION
is required to execute proposals on the PersonalSpaceAdminPluginEXECUTE_PERMISSION
is required to make the DAOexecute
a set of actions- Only plugins should have this permission
- Some plugins should restrict it with a condition
Spaces:
CONTENT_PERMISSION_ID
is required to call the function that emits new content events on the SpacePlugin- Typically called by the DAO via proposal
SUBSPACE_PERMISSION_ID
is required to call the functions that emit new subspace accept/reject events on the SpacePlugin- Typically called by the DAO via proposal
Governance settings:
UPDATE_VOTING_SETTINGS_PERMISSION_ID
is required to change the settings of the MainVotingPluginUPDATE_ADDRESSES_PERMISSION_ID
is required to add or remove members or editors on the MainVotingPlugin- Typically called by the DAO via proposal
UPDATE_MULTISIG_SETTINGS_PERMISSION_ID
is required to change the settings of the MemberAccessPlugin- Typically called by the DAO via proposal
Permission management:
ROOT_PERMISSION
is required to make the DAOgrant
orrevoke
permissions- If the DAO is executing a set of actions, it needs to be ROOT on itself (it is by default)
- The PluginSetupProcessor may be granted ROOT permission temporarily
- Nobody else should be ROOT on a DAO
Plugin versioning:
UPGRADE_PLUGIN_PERMISSION
is required to be able to callupgradeTo()
orupgradeToAndCall()
on a plugin- Called by the PSP via proposal on the DAO
APPLY_UPDATE_PERMISSION
is needed to callapplyUpdate()
on the PluginSetupProcessor- Optionally granted to an additional address, for convenience
- Correspondingly,
APPLY_INSTALLATION_PERMISSION
andAPPLY_UNINSTALLATION_PERMISSION
allow to callapplyInstallation()
orapplyUninstallation()
Other DAO permissions:
EXECUTE_PERMISSION
UPGRADE_DAO_PERMISSION
SET_METADATA_PERMISSION
SET_TRUSTED_FORWARDER_PERMISSION
SET_SIGNATURE_VALIDATOR_PERMISSION
REGISTER_STANDARD_CALLBACK_PERMISSION
Run yarn build && yarn typechain
on the packages/contracts
folder.
See packages/contracts/typechain
for all the generated JS/TS wrappers to interact with the contracts.
Making calls to the DAO is straightforward, however executing arbitrary actions requires them to be encoded, stored on chain and be approved before they can be executed.
To this end, the DAO has a struct called Action { to, value, data }
, which will make the DAO call the to
address, with value
ether and call the given calldata (if any).
It uses the generated typechain artifacts, which contain the interfaces for the available contract methods and allow to easily encode function calls into hex strings.
On Spaces with the standard governance, a MemberAccessPlugin and a MainVotingPlugin will be installed.
- Send a transaction to call
proposeNewMember()
- Have an editor (different to the proposer) calling
approve()
for this proposal - This will add the requested address to the members list on the main voting contract
The same applies to remove members with proposeRemoveMember()
- A member or editor creates a proposal
- The proposal should have an action to make the DAO
execute()
a call toaddEditor()
on the plugin - A majority of editors call
vote()
and approve it - Someone calls
plugin.execute()
so that the DAO executes the requested action on the plugin - The new editor will be able to vote on proposals created from then on
The same procedure applies to removing editors with removeEditor()
- Execute a proposal with an action to call
grant(address(mainVotingPlugin), targetAddress, EDITOR_PERMISSION_ID)
- With the permission granted,
targetAddress
can immediately start executing proposals
The same applies for removing a member.
Acts as the source of truth regarding the Space associated to the DAO. It is in charge of emitting the events that notify new content being approved and it also emits events accepting a certain DAO as a Subpspace.
The same plugin is used for both governance cases. The difference lies on the governance model.
This plugin is upgradeable.
function initialize(IDAO _dao, string _firstContentUri, address predecessorSpace);
function publishEdits(string _contentUri);
function acceptSubspace(address _subspaceDao);
function removeSubspace(address _subspaceDao);
Inherited:
function upgradeTo(address newImplementation);
function upgradeToAndCall(address newImplementation, bytes data);
Inherited:
function implementation() returns (address);
event EditsPublished(address dao, string contentUri);
event SuccessorSpaceCreated(address dao, address predecessorSpaceDao);
event SubspaceAccepted(address dao, address subspaceDao);
event SubspaceRemoved(address dao, address subspaceDao);
- The DAO can call
publishEdits()
on the plugin - The DAO can accept/remove a subspace on the plugin
- The DAO can upgrade the plugin
- See Plugin upgrader (optional)
Provides a simple way for any address to request membership on a space. It is a adapted version of Aragon's Multisig plugin. It creates a proposal to addMember()
on the main voting plugin and Editors can approve or reject it. Once approved, the member create proposals on the main voting plugin.
function initialize(IDAO _dao, MultisigSettings _multisigSettings);
function updateMultisigSettings(MultisigSettings _multisigSettings);
function proposeNewMember(bytes _metadata, address _proposedMember);
function proposeRemoveMember(bytes _metadata, address _proposedMember);
function approve(uint256 _proposalId);
function reject(uint256 _proposalId);
function execute(uint256 _proposalId);
Inherited:
function upgradeTo(address newImplementation);
function upgradeToAndCall(address newImplementation, bytes data);
function supportsInterface(bytes4 _interfaceId) returns (bool);
function canApprove(uint256 _proposalId, address _account) returns (bool);
function canExecute(uint256 _proposalId) returns (bool);
function getProposal(
uint256 _proposalId
)
returns (
bool executed,
uint16 approvals,
ProposalParameters parameters,
IDAO.Action[] actions,
uint256 failsafeActionMap
);
function hasApproved(uint256 _proposalId, address _account) returns (bool);
function isMember(address _account) returns (bool);
function isEditor(address _account) returns (bool);
Inherited:
function proposalCount() external view returns (uint256);
function implementation() returns (address);
event Approved(uint256 indexed proposalId, address indexed editor);
event Rejected(uint256 indexed proposalId, address indexed editor);
event MultisigSettingsUpdated(uint64 proposalDuration, address mainVotingPlugin);
Inherited:
event ProposalCreated(uint256 indexed proposalId, address indexed creator, uint64 startDate, uint64 endDate, bytes metadata, IDAO.Action[] actions, uint256 allowFailureMap);
event ProposalExecuted(uint256 indexed proposalId);
- Anyone can create membership proposals
- Editors can approve and reject proposals
- The plugin can execute on the DAO (with a condition)
- The DAO can update the plugin settings
- The DAO can upgrade the plugin
- See Plugin upgrader (optional)
It's the main governance plugin for standard spaces, where all proposals are voted by editors. It is a adapted version of Aragon's AddresslistVoting plugin. Only members (or editors) can create proposals and they can only be executed after a qualified majority has voted for it.
It acts as the source of truth about who is an editor, on Spaces with standard governance.
The governance settings need to be defined when the plugin is deployed but the DAO can change them at any time. Proposal creators can cancel their own proposals before they end.
function initialize(
IDAO _dao,
VotingSettings calldata _votingSettings,
address[] calldata _initialEditors
);
function addEditor(address);
function removeEditor(address);
function addMember(address);
function removeMember(address);
function createProposal(
bytes calldata metadata,
IDAO.Action[] calldata actions,
uint256 allowFailureMap,
VoteOption voteOption,
bool tryEarlyExecution
);
function cancelProposal(uint256 _proposalId);
Inherited:
function vote(uint256 _proposalId, VoteOption _voteOption, bool _tryEarlyExecution);
function execute(uint256 _proposalId);
function updateVotingSettings(VotingSettings calldata _votingSettings);
function upgradeTo(address newImplementation);
function upgradeToAndCall(address newImplementation, bytes data);
function isMember(address _account) returns (bool);
function isEditor(address _account) returns (bool);
function supportsInterface(bytes4 _interfaceId) returns (bool);
Inherited:
function canVote(uint256 _proposalId, address _voter, VoteOption _voteOption);
function getProposal(
uint256 _proposalId
)
returns (
bool open,
bool executed,
ProposalParameters parameters,
Tally tally,
IDAO.Action[] actions,
uint256 allowFailureMap
);
function getVoteOption(uint256 _proposalId, address _voter);
function isSupportThresholdReached(uint256 _proposalId) returns (bool);
function isSupportThresholdReachedEarly(uint256 _proposalId);
function isMinParticipationReached(uint256 _proposalId) returns (bool);
function canExecute(uint256 _proposalId) returns (bool);
function supportThreshold() returns (uint32);
function minParticipation() returns (uint32);
function duration() returns (uint64);
function votingMode() returns (VotingMode);
function totalVotingPower(uint256 _blockNumber) returns (uint256);
function implementation() returns (address);
event ProposalCanceled(uint256 proposalId);
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 duration);
- Members (and editors) can create proposals
- Editors can vote on proposals
- The plugin can execute on the DAO
- The DAO can update the plugin settings
- The DAO can manage the list of addresses
- The DAO can upgrade the plugin
- See Plugin upgrader (optional)
Governance plugin providing the default implementation for personal spaces, where addresses with editor permissioin can apply proposals right away. It is a adapted version of Aragon's Admin plugin.
Since this plugin has the power to unilaterally perform actions, it is not upgradeable. Adding many editors is possible via proposals with a grant/revoke action.
function initialize(IDAO _dao);
function executeProposal(
bytes calldata _metadata,
IDAO.Action[] calldata _actions,
uint256 _allowFailureMap
);
function isEditor(address _account) returns (bool);
function supportsInterface(bytes4 _interfaceId) returns (bool);
Inherited:
function proposalCount() external view returns (uint256);
function implementation() returns (address);
Inherited:
event ProposalCreated(uint256 indexed proposalId, address indexed creator, uint64 startDate, uint64 endDate, bytes metadata, IDAO.Action[] actions, uint256 allowFailureMap);
event ProposalExecuted(uint256 indexed proposalId);
- Editors can execute proposals right away
- The plugin can execute actions on the DAO
So far, we have been talking about the plugin contracts. However, they need to be prepared and installed on a DAO and for this, a DAO needs to approve for it. To this end, PluginSetup contracts act as an install script in charge of preparing installations, updates and uninstallations. They always have two steps:
- An unprivileged step to prepare the plugin and request any privileged changes
- An approval step after which, editors execute an action that applies the requested installation, upgrade or uninstallation
This is taken care by the DAOFactory
. The DAO creator calls daoFactory.createDao()
:
- The call contains:
- The DAO settings
- An array with the details and the settings of the desired plugins
- The method will deploy a new DAO and grant
ROOT_PERMISSION
to theDaoFactory
, temporarily- Given the settings of the desired plugins, it will call the
PluginSetupProcessor
- Given the settings of the desired plugins, it will call the
- The PSP will then call
prepareInstallation()
on the given plugin set up contract - Immedially after,
applyInstallation()
will be called by theDaoFactory
- The DaoFactory drops
ROOT_PERMISSION
on itself
See a JS example of installing plugins during a DAO's deployment
Plugin changes need a proposal to be passed when the DAO already exists.
- Calling
pluginSetupProcessor.prepareInstallation()
which callsprepareInstallation()
on the plugin's setup contract- A new plugin instance is deployed with the desired settings
- The call returns a set of requested permissions to be applied by the DAO
- Editors pass a proposal to make the DAO call
applyInstallation()
on the PluginSetupProcessor- This applies the requested permissions and the plugin becomes installed
Learn more about plugin setup's and preparing installations.
In both of the cases described above, a call to prepareInstallation()
will be made by the PluginSetupProcessor
from OSx.
function prepareInstallation(
address _dao,
bytes memory _data
) external returns (address plugin, PreparedSetupData memory preparedSetupData)
- The first parameter (dao address) is provided by the PSP.
- The second parameter contains the ABI-encoded custom settings that the plugin setup needs to operate.
You can use these convenience functions from the plugin setup contracts:
// governance/SpacePluginSetup.sol
function encodeInstallationParams(
string memory _firstBlockContentUri,
address _predecessorAddress,
address _pluginUpgrader
) public pure returns (bytes memory);
function encodeUninstallationParams(
address _pluginUpgrader
) public pure returns (bytes memory)
The same also applies to prepareUpdate
(if present) and to prepareUninstallation
.
This contract handles the install/update/uninstall scripts for MainVotingPlugin
and MemberAccessPlugin
The second plugin needs to know the address of the first one, therefore the setup deploys them together.
Note:
When preparing the installation, an InstallationPrepared
event is emitted. Using Typechain with Ethers:
event.args.preparedSetupData.plugin
contains the address of the Main Voting pluginevent.args.preparedSetupData.helpers
contains an array with the address of the Member Access plugin
This contract implements the deployment script for the SpacePlugin
contract.
This contract implements the deployment script for the PersonalSpaceAdminPlugin
contract.
The recommended way to create a DAO is by using @aragon/sdk-client
. It uses the DAOFactory
under the hood and it reduces the amount of low level interactions with the protocol.
In the example, the code is making use of the existing JS client for Aragon's Token Voting plugin. They encapsulate all the Typechain and Subgraph calls and provide a high level library.
- The HardHat deployment scripts are located on the
packages/contracts/deploy
folder. - The settings about the naming, ID's and versions can be found on
packages/contracts/plugin-setup-params.ts
. - The deployments made will populate data to the
packages/contracts/plugin-repo-info.json
andpackages/contracts/plugin-repo-info-dev.json
. - You need to copy
.env.template
into.env
and provide your Infura API key
- Never grant
ROOT_PERMISSION
unless you are just trying things out - Never uninstall all plugins, as this would brick your DAO
- Ensure that there is at least always one plugin with
EXECUTE_PERMISSION
on the DAO - Ensure that the DAO is ROOT on itself
- If you create it manually, you should eventually grant ROOT to the DAO and revoke it to yourself as the creator.
- The DAO factory will automatically do this for you
- Use the
_gap[]
variable for upgradeable plugins, as a way to reserve storage slots for future plugin implementations- Decrement the
_gap
number for every new variable you add in the future
- Decrement the
The first step to upgrade a plugin is calling ThePluginSetup.prepareUpdate()
, which will register an update request on the PluginSetupProcessor
. See Plugin Setup install parameters above.
By default, only the DAO can upgrade plugins to newer versions. This requires passing a proposal with these 3 actions:
Action 1:
- Grant
UPGRADE_PLUGIN_PERMISSION_ID
to the PSP on the target plugin
Action 2:
- Make the DAO call
PSP.applyUpdate()
with the ID generated duringprepareUpdate()
Action 3:
- Revoke
UPGRADE_PLUGIN_PERMISSION_ID
to the PSP on the target plugin
The address of the PluginSetupProcessor
depends on the chain. The existing deployments can be checked here.
For the 3 upgradeable plugins, their plugin setup allows to pass an optional parameter to define an address that can perform upgrades without the need for a proposal.
- When a zero address is passed, only a proposal can make the DAO call
PSP.applyUpdate()
. - When a non-zero address is passed, the given address will be able to execute the 3 actions abover to upgrade to any new version the developer has published.
Every new version needs to be published to the plugin's repository.
Learn more about plugin upgrades.
The plugins from this repo are built on top of many contract primitives from Aragon. In some cases, certain parameters are not required or data types need to differ. For this reason, the packages/contracts/src/governance/base
folder contains 5 forks of existing Aragon primitives.
Addresslist.sol
- Functions
addMembers
andremoveMembers
accepted anaddress[] calldata
parameter - However, the plugin needed to pass an
address[] memory
- Functions
IEditors.sol
andIMembers.sol
- Originally from
IMembership.sol
- Geo defines a concept of members with conflicted with how the address list interprets its "members" (which are editors)
- Using separate, explicit interfaces to clarify the difference between members and editors
- Originally from
IMultisig.sol
- The
accept()
function required 2 parameters, of which the second always has to betrue
. - Changing the signature of
accept()
to only use relevant parameters
- The
MajorityVotingBase.sol
createProposal()
originally had two parameters that didn't apply to the current specs.- The forked version Omits these 2 parameters (start date and end date) and instead:
- Starts immediately
- Ends after the predefined duration
minDuration()
was confusing given that the setting is used as the final duration, soduration()
is used insteadminProposerVotingPower
wasn't used
The rest of dependencies are imported directly from Aragon or from OpenZeppelin.