How to Build a DAO Plugin using Aragon OSx

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: 

  • Governance: alter the decision-making mechanism in your DAO, such as by installing a token voting plugin, a delegated voting plugin, an NFT voting plugin, or something else!
  • Asset management: swap tokens with a Uniswap plugin, stake assets with a Lido plugin, or buy NFTs with an OpenSea plugin.
  • Membership: define who is a member of the DAO, meaning has voting rights. Grant membership to individuals with a non-transferrable token plugin, to authorized wallets with a multisig plugin, or to token holders with an ERC-20 plugin.
  • Anything else that extends the functionality of your DAO onchain! 

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!

Basics of DAO plugins

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.

What’s the connection between Plugins and Permissions?

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….

  • controls the plugin setup process (installation, update, uninstallation)
  • authorizes calls to plugin functions carrying the auth modifier
  • authorizes calls to DAO functions, for example, the execute function allowing for acting as the DAO

by checking if the caller hasPermission.

The role of the permission manager is illustrated here:

Image source

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!

How to build a DAO plugin in 5 steps

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:

  • A Plugin implementation contract: containing all the logic functionality, extending what DAOs are able to do
  • A Plugin Setup contract: containing the installation, uninstallation, or upgrade instructions for a plugin into a DAO.

Step 1: Import the Aragon OSx 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:

Copied to clipboard!

   yarn add @aragon/osx
 

Now you’re ready to get started!

Step 2: Choose to develop an upgradable or non-upgradable plugin

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:

  • They’re simpler to create, deploy, and manage.
  • Instantiation is done via the new keyword or deployed via the minimal proxy pattern (ERC-1167)
  • The storage is contained within versions. This means that if your plugin is dependent on state information from previous versions, you won't have access to its upcoming versions, since every version is a blank new state. If this is a requirement for your project, we recommend you deploy an Upgradeable Plugin.

Step 3: Write the plugin’s implementation logic

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.

  • _dao is the DAO which the plugin will be installed in. We get this from inheriting from the PluginCloneable contract coming from the Aragon OSx contracts.
  • _admin is the address that will have full control of the DAO. This will be defined during installation when the plugin instance is deployed for the DAO. 
  • Keep in mind, you can add any other attribute here that you wish to pass over to the plugin upon initialization.

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.

Copied to clipboard!

    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.

Step 4: Setup the plugin

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.

Copied to clipboard!

  // 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.

Step 5: Publish your plugin to the Plugin Registry

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!

Do’s and Don'ts of developing a DAO plugin

Here are a few helpful tips to keep in mind while building your plugin:

Do

  • Document your contracts using NatSpec.
  • Test your contracts using toolkits such as hardhat (JS) or Foundry (Rust).
  • Use the auth modifier to control the access to functions in your plugin instead of onlyOwner or similar.
  • Write plugins implementations that need minimal permissions on the DAO.
  • Write PluginSetup contracts that remove all permissions requested duringon uninstallation, that they requested during installation or upgradesupgrdates.
  • Plan the lifecycle of your plugin and its upgrading strategy, like the need for upgrades in the future.
  • Follow our versioning guidelines.

Don’t

  • Leave any contract uninitialized.
  • Grant the ROOT_PERMISSION_ID permission to anything or anyone.
  • Grant with who: ANY_ADDR unless you know what you are doing.
  • Expect people to grant or revoke any permissions manually during the lifecycle of a plugin. The PluginSetup should take this complexity away from the user and after uninstallation, all permissions should be removed.
  • Write upgradable contracts that repurpose existing storage (in upgradeable plugins).
  • Write upgradable contracts that inherit from previous versions as this can mess up the inheritance chain. Instead, write self-contained contracts.

Let us know what you build!

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! 

Discover the Aragon App, the no-code way to build your DAO.
Get help starting your DAO from a DAO Expert.
Stay up to date with our weekly newsletter.

Explore more guides

Need Help? Find an Expert
Hire the DAO expertise you need and connect with DAO experts to build your DAO, your way.