Aragon OSx is the DAO framework that underlies the Aragon App and SDK. The DAO framework is simple and modular, because the logic of your DAO is modified through plugins that can be installed, upgraded, and uninstalled.
You can think of plugins as apps that you install and uninstall on your computer or phone. They add extra functionality that extend what your DAO can do!
Plugins can perform lots of different actions onchain. Here are a few categories to get started:
In this guide, we’ll cover how to build a plugin on Aragon OSx, so that your DAO and all other DAOs built on Aragon OSx can use it!
A modularized approach makes a lot of sense to developers. It's also good for keeping the bytecode lean and cost-effective, and it can work because we already know many of the capabilities required for DAO administration.
- Andrei Taranu from dOrg, a DAO Expert specializing in custom DAOs and plugins on Aragon OSx
Plugins are smart contracts that extend the functionality of what DAOs can do. Every time a plugin is installed in a DAO, a new plugin instance is deployed containing the specific settings and attributes needed for that DAO’s plugin.
For example, if we want your DAO to make decisions through token voting, you would establish the token details, minimum participation threshold, minimum amount of tokens needed to create proposals, and other parameters as part of the installation instructions.
These types of configurations differ based on each DAO and plugin, which is why each instance of a plugin is installed and managed by one DAO.
The Aragon Plugin Registry contains the base template of the plugin, but a specific plugin instance is deployed for each DAO every time a DAO installs a plugin.
Plugins are installed in DAOs through the Plugin Setup Processor contract, whose role is to grant (or revoke in the case of uninstalling) permissions to the plugin contract so it can execute transactions on behalf of the DAO.
The DAO manages plugins with its permission manager, which plays an important role. The permission manager….
by checking if the caller hasPermission.
The role of the permission manager is illustrated here:
In this example, the permission manager determines whether the token voting plugin can execute actions on the DAO, a member can change its settings, or a DeFi-related plugin is allowed to invest in a specific external contract.
Whereas deployed plugin instances are managed by the DAO that installed them, the developer of the original plugin implementation owns the implementation and setup contracts of the plugin. The plugin developer is the maintainer of the Plugin’s Repository of versions, known as the plugin repo. Finally, the Aragon OSx protocol manages the registry in which the plugin repositories are listed: a list of all published plugins DAOs have access to.
Now that we understand what plugins are and what they do, let’s get started on building one!
In this guide, we use the SimpleAdmin plugin as an example, which we intend to work as follows:
The plugin allows one address, the admin address, to send actions to the DAO's executor. The admin address is set only once and can never be changed. An only slightly more advanced variant is provided by Aragon in the Aragon plugin registry.
So, let’s get started! Before we get started, know that a plugin is always composed of two key contracts:
First thing we do when building plugins is import the Aragon OSx contracts package into the project so we can use Aragon’s base functionality.
If you don’t have a project already created, here’s a tutorial for starting your own Hardhat or Foundry projects.
You can do this in your terminal with the following bash command:
yarn add @aragon/osx
Now you’re ready to get started!
There are two types of plugins you can build: upgradable and non-upgradable.
An upgradable plugin means it can be modified after deployment to do things such as fix bugs, add features, or change the rules enforced by it. To learn how to enable upgradable smart contracts, read this guide and get started in this tutorial. Be aware that upgradeable plugins can open up security risks, because they have changeable logic.
In this tutorial we will build a non-upgradeable plugin. Some things to know about them:
Once you have defined the type of plugin you want to build, we can start building the implementation of our contract.
The first thing you’ll do is create the permission the DAO needs in order to interact with this plugin. This is what will be granted during installation and is what will connect our plugin to the DAO that installed it. In this example, we call it the ADMIN_EXECUTE_PERMISSION.
Then, we will initialize the contract by passing it the IDAO _dao and the address _admin as parameters.
In this example, we’re using the minimal clones pattern (ERC-1167) for deploying our SimpleAdmin Plugin since it’s cheaper than using the new keyword. This is why you see that we create an initialize function and pass it __PluginCloneable_init(_dao) upon initialization. Alternatively, to deploy it using the new keyword, we could simply use a constructor.
Lastly, when we enable the address we’ve set as admin of the DAO to have execute access over the DAO’s assets.
contract SimpleAdmin is PluginCloneable {
/// @notice The ID of the permission required to call the `execute` function.
bytes32 public constant ADMIN_EXECUTE_PERMISSION_ID = keccak256('ADMIN_EXECUTE_PERMISSION');
address public admin;
/// @notice Initializes the contract.
/// @param _dao The associated DAO.
/// @param _admin The address of the admin.
function initialize(IDAO _dao, address _admin) external initializer {
__PluginCloneable_init(_dao);
admin = _admin;
}
/// @notice Executes actions in the associated DAO.
/// @param _actions The actions to be executed by the DAO.
function execute(IDAO.Action[] calldata _actions) external auth(ADMIN_EXECUTE_PERMISSION_ID) {
dao().execute({callId: 0x0, actions: _actions, allowFailureMap: 0});
}
}
For more details on the implementation step, read this page.
The Plugin Setup contract is where we will write the instructions needed for the framework to install and uninstall a plugin into a DAO.
The first thing we will do is deploy the Plugin Implementation contract within the Plugin Setup’s initialization.
Then, we will create the prepareInstallation and prepareUninstallation functions, in charge of granting or revoking permissions from the DAO to the plugin.
For example, we can see in the prepareInstallation function how we request the ADMIN_EXECUTE_PERMISSION_ID permission for the admin (who) address on the plugin (where). Second, we request the EXECUTE_PERMISSION_ID permission for the freshly deployed plugin (who) on the _dao (where). We don't add conditions to the permissions, so we use the NO_CONDITION constant provided by PermissionLib.
In the prepareUninstallation function, we do the opposite and revoke the permissions.
The first line, using Clones for address;, allows us to call OpenZeppelin Clones library to clone contracts deployed at an address. The second line introduces a custom error being thrown if the admin address specified is the zero address.
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity 0.8.17;
import {Clones} from '@openzeppelin/contracts/proxy/Clones.sol';
import {PermissionLib} from '@aragon/osx/core/permission/PermissionLib.sol';
import {PluginSetup, IPluginSetup} from '@aragon/osx/framework/plugin/setup/PluginSetup.sol';
import {SimpleAdmin} from './SimpleAdmin.sol';
contract SimpleAdminSetup is PluginSetup {
using Clones for address;
/// @notice The address of `SimpleAdmin` plugin logic contract to be cloned.
address private immutable simpleAdminImplementation;
/// @notice Thrown if the admin address is zero.
/// @param admin The admin address.
error AdminAddressInvalid(address admin);
/// @notice The constructor setting the `Admin` implementation contract to clone from.
constructor() {
simpleAdminImplementation = address(new SimpleAdmin());
}
/// @inheritdoc IPluginSetup
function prepareInstallation(
address _dao,
bytes calldata _data
) external returns (address plugin, PreparedSetupData memory preparedSetupData) {
// Decode `_data` to extract the params needed for cloning and initializing the `Admin` plugin.
address admin = abi.decode(_data, (address));
if (admin == address(0)) {
revert AdminAddressInvalid({admin: admin});
}
// Clone plugin contract.
plugin = implementation.clone();
// Initialize cloned plugin contract.
SimpleAdmin(plugin).initialize(IDAO(_dao), admin);
// Prepare permissions
PermissionLib.MultiTargetPermission[]
memory permissions = new PermissionLib.MultiTargetPermission[](2);
// Grant the `ADMIN_EXECUTE_PERMISSION` of the plugin to the admin.
permissions[0] = PermissionLib.MultiTargetPermission({
operation: PermissionLib.Operation.Grant,
where: plugin,
who: admin,
condition: PermissionLib.NO_CONDITION,
permissionId: SimpleAdmin(plugin).ADMIN_EXECUTE_PERMISSION_ID()
});
// Grant the `EXECUTE_PERMISSION` on the DAO to the plugin.
permissions[1] = PermissionLib.MultiTargetPermission({
operation: PermissionLib.Operation.Grant,
where: _dao,
who: plugin,
condition: PermissionLib.NO_CONDITION,
permissionId: DAO(payable(_dao)).EXECUTE_PERMISSION_ID()
});
preparedSetupData.permissions = permissions;
}
/// @inheritdoc IPluginSetup
function prepareUninstallation(
address _dao,
SetupPayload calldata _payload
) external view returns (PermissionLib.MultiTargetPermission[] memory permissions) {
// Collect addresses
address plugin = _payload.plugin;
address admin = SimpleAdmin(plugin).admin();
// Prepare permissions
permissions = new PermissionLib.MultiTargetPermission[](2);
permissions[0] = PermissionLib.MultiTargetPermission({
operation: PermissionLib.Operation.Revoke,
where: plugin,
who: admin,
condition: PermissionLib.NO_CONDITION,
permissionId: SimpleAdmin(plugin).ADMIN_EXECUTE_PERMISSION_ID()
});
permissions[1] = PermissionLib.MultiTargetPermission({
operation: PermissionLib.Operation.Revoke,
where: _dao,
who: plugin,
condition: PermissionLib.NO_CONDITION,
permissionId: DAO(payable(_dao)).EXECUTE_PERMISSION_ID()
});
}
/// @inheritdoc IPluginSetup
function implementation() external view returns (address) {
return simpleAdminImplementation;
}
}
Now we’re ready to publish our plugin in a PluginRepo and register it in Aragon's PluginRepoRegistry.
Every plugin in Aragon OSx can have future versions, so when publishing a plugin to the Aragon protocol, we're really deploying a PluginRepo contract instance for your plugin which will contain all of the plugin's versions.
To publish a plugin we will use Aragon's PluginRepoFactory contract, which is in charge of creating PluginRepo instances. We'll call on its createPluginRepoWithFirstVersion function, which will create the first version of a plugin and add your plugin’s PluginRepo address into the PluginRepoRegistry containing all available plugins.
You can find all of the addresses of PluginRepoFactory contracts by network here and deploy your plugin either through Etherscan or through a custom script you add to your project.
You can learn more about the publication step here.
Now, you’re done building your plugin!
Here are a few helpful tips to keep in mind while building your plugin:
Have more questions about this tutorial? Check out the plugin section in our developer documentation or reach out to our team through our Discord server.
We can’t wait to see what you create!