Writing your plugin setup contract
This section is a continuation of the Writing your plugin contract, and is also focused on non-upgradeable plugins, for upgradeable plugins, see Writing your upgradeable plugin. |
What is the Plugin Setup contract?
The Plugin Setup contract is the contract defining the instructions for installing, uninstalling, or upgrading plugins into DAOs. This contract prepares the permission granting or revoking that needs to happen in order for plugins to be able to perform actions on behalf of the DAO.
You need it for the plugin to be installed into the DAO.
1. Finish the Plugin contract
Before building your Plugin Setup contract, make sure you have the logic for your plugin implemented. In this case, we’re building a simple admin plugin which grants one address permission to execute actions on behalf of the DAO.
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});
}
}
2. How to initialize the Plugin Setup contract
Each PluginSetup
contract is deployed only once and we will publish a separate PluginSetup
instance for each version. Accordingly, we instantiate the implementation
contract via Solidity’s new
keyword as deployment with the minimal proxy pattern would be more expensive in this case.
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity 0.8.21;
import {PluginSetup, IPluginSetup} from '@aragon/osx/framework/plugin/setup/PluginSetup.sol';
import {SimpleAdmin} from './SimpleAdmin.sol';
contract SimpleAdminSetup is PluginSetup {
/// @notice The address of `SimpleAdmin` plugin contract to be cloned.
address private immutable simpleAdminImplementation;
/// @notice The constructor setting the `SimpleAdmin` implementation contract to clone from.
constructor() {
simpleAdminImplementation = address(new SimpleAdmin());
}
/// @inheritdoc IPluginSetup
function implementation() external view returns (address) {
return simpleAdminImplementation;
}
}
3. Build the Skeleton
In order for the Plugin to be easily installed into the DAO, we need to define the permissions the plugin will need.
We will create a prepareInstallation()
function, as well as a prepareUninstallation()
function. These are the functions the PluginSetupProcessor.sol
(the contract in charge of installing plugins into the DAO) will use to prepare the installation/uninstallation of the plugin into the DAO.
For example, a skeleton for our SimpleAdminSetup
contract inheriting from PluginSetup
looks as follows:
import {PermissionLib} from '@aragon/osx/core/permission/PermissionLib.sol';
contract SimpleAdminSetup is PluginSetup {
/// @notice The address of `SimpleAdmin` plugin logic contract to be cloned.
address private immutable simpleAdminImplementation;
/// @notice The constructor setting the `SimpleAdmin` 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) {
revert('Not implemented yet.');
}
/// @inheritdoc IPluginSetup
function prepareUninstallation(
address _dao,
SetupPayload calldata _payload
) external view returns (PermissionLib.MultiTargetPermission[] memory permissions) {
revert('Not implemented yet.');
}
/// @inheritdoc IPluginSetup
function implementation() external view returns (address) {
return simpleAdminImplementation;
}
}
As you can see, we have a constructor storing the implementation contract instantiated via the new
method in the private immutable variable implementation
to save gas and a implementation
function to retrieve it.
Next, we will add the implementation for the prepareInstallation
and prepareUninstallation
functions.
4. Implementing the prepareInstallation()
function
The prepareInstallation()
function should take in two parameters:
-
the
DAO
it prepares the installation for, and -
the
_data
parameter containing all the information needed for this function to work properly, in this case, the address we want to set as admin of our DAO.
Hence, the first thing we should do when working on the prepareInstallation()
function is decode the information from the _data
parameter. We also want to check that the address is not accidentally set to address(0)
, which would freeze the DAO forever.
import {Clones} from '@openzeppelin/contracts/proxy/Clones.sol';
contract SimpleAdminSetup is PluginSetup {
using Clones for address;
/// @notice Thrown if the admin address is zero.
/// @param admin The admin address.
error AdminAddressInvalid(address admin);
// ...
}
Then, we will use OpenZeppelin’s Clones
library to clone our Plugin contract and initialize it with the admin
address. 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.
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;
}
Finally, we construct and return an array with the permissions that we need for our plugin to work properly.
-
First, we request granting the
ADMIN_EXECUTE_PERMISSION_ID
to theadmin
address received. This is what gives the address access to useplugin’s functionality - in this case, call on the plugin’s `execute
function so it can execute actions on behalf of the DAO. -
Second, we request that our newly deployed plugin can use the
EXECUTE_PERMISSION_ID
permission on the_dao
. We don’t add conditions to the permissions in this case, so we use theNO_CONDITION
constant provided byPermissionLib
.
5. Implementing the prepareUninstallation()
function
For the uninstallation, we have to make sure to revoke the two permissions that have been granted during the installation process.
First, we revoke the ADMIN_EXECUTE_PERMISSION_ID
from the admin
address that we have stored in the implementation contract.
Second, we revoke the EXECUTE_PERMISSION_ID
from the plugin
address that we obtain from the _payload
calldata.
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()
});
}
6. Putting Everything Together
Now, it’s time to wrap up everything together. You should have a contract that looks like this:
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity 0.8.21;
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;
}
}
Once done, our plugin is ready to be published on the Aragon plugin registry. With the address of the SimpleAdminSetup
contract, we are ready for creating our PluginRepo
, the plugin’s repository where all plugin versions will live.
Check out our how to guides on publishing your plugin here.
In the future: Subsequent Builds
For subsequent builds or releases of your plugin, you’ll simply write a new implementation and associated Plugin Setup contract providing a new prepareInstallation
and prepareUninstallation
function.
If a DAO wants to install the new build or release, it must uninstall its current plugin and freshly install the new plugin version, which can happen in the same action array in a governance proposal. However, the plugin storage and event history will be lost since this is a non-upgradeable plugin. If you want to prevent the latter, you can learn how to write an upgradeable plugin here.