Writing your plugin contract
This section will be focuses on non-upgradeable plugins development, for upgradeable plugins please check out our guide here.
Before continuing make sure you’ve read our documentation on Choosing the Best Type for Your Plugin to make sure you’re selecting the right type of contract for your Plugin.
How to Initialize the plugin
Every plugin should receive and store the address of the DAO it is associated with upon initialization. This is how the plugin will be able to interact with the DAO that has installed it.
In addition, your plugin implementation might want to introduce other storage variables that should be initialized immediately after the contract was created.
For example, in the SimpleAdmin
plugin example (which sets one address as the full admin of the DAO), we’d want to store the admin
address.
contract SimpleAdmin is Plugin {
address public admin;
}
The way we set up the plugin’s initialize()
function depends on the plugin type selected. Additionally, the way we deploy our contracts is directly correlated with how they’re initialized. For Non-Upgradeable Plugins,
there’s two ways in which we can deploy our plugin:
-
Deployment via Solidity’s
new
keyword, or -
Deployment via the Minimal Proxy Pattern
Option A: Deployment via Solidity’s new
Keyword
To instantiate the contract via Solidity’s new
keyword, you should inherit from the Plugin
Base Template Aragon created.
You can find it here.
In this case, the compiler will force you to write a constructor
function calling the Plugin
parent constructor
and
provide it with a contract of type IDAO
. Inside the constructor, you might want to initialize the storage variables that you have
added yourself, such as the admin
address in the example below.
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity 0.8.21;
import {Plugin, IDAO} from '@aragon/osx/core/plugin/Plugin.sol';
contract SimpleAdmin is Plugin {
address public immutable admin;
/// @notice Initializes the contract.
/// @param _dao The associated DAO.
/// @param _admin The address of the admin.
constructor(IDAO _dao, address _admin) Plugin(_dao) {
admin = _admin;
}
}
The admin variable is set as immutable so that it can never be changed. Immutable variables can only be initialized in
the constructor.
|
This type of constructor implementation stores the IDAO _dao
reference in the right place. If your plugin is deployed often, which we could expect, we can save significant amounts of gas by deployment through using the minimal proxy pattern.
Option B: Deployment via the Minimal Proxy Pattern
To deploy our plugin via the link:https://eips.ethereum.org/EIPS/eip-1167(minimal clones pattern (ERC-1167)), you inherit from the PluginCloneable
contract introducing the same features as Plugin
.
The only difference is that you now have to remember to write an initialize
function.
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity 0.8.21;
import {PluginCloneable, IDAO} from '@aragon/osx/core/plugin/PluginCloneable.sol';
contract SimpleAdmin is PluginCloneable {
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;
}
}
We must protect it from being called multiple times by using OpenZeppelin’s initializer
modifier made available through Initializable
and
call the internal function __PluginCloneable_init(IDAO _dao)
available through the PluginCloneable
base contract to
store the IDAO _dao
reference in the right place.
If you forget calling __PluginCloneable_init(_dao) inside your initialize function, your plugin won’t be associated
with a DAO and cannot use the DAO’s PermissionManager .
|
How to Build a the plugin
Once we’ve initialized our plugin (take a look at our guide on how to initialize the plugin here), we can start using the Non-Upgradeable Base Template to perform actions on the DAO.
1. Set the Permission Identifier
Firstly, we want to define a permission identifier bytes32
constant at the top
of the contract and establish a keccak256
hash of the permission name we want to choose.
In this example, we’re calling it the ADMIN_EXECUTE_PERMISSION
.
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.
function execute(IDAO.Action[] calldata _actions) external auth(ADMIN_EXECUTE_PERMISSION_ID) {
revert('Not implemented.');
}
}
NOTE:
You are free to choose the permission name however you like. For example, you could also have used keccak256('SIMPLE_ADMIN_PLUGIN:PERMISSION_1')
.
However, it is important that the permission names are descriptive and cannot be confused with each other.
Setting this permission is key because it ensures only signers who have been granted that permission are able to execute functions.
2. Add the logic implementation
Now that we have created the permission, we will use it to protect the implementation. We want to make sure only the authorized callers holding the ADMIN_EXECUTE_PERMISSION
, can use the function.
Because we have initialized the PluginCloneable
base contract,
we can now use its features, i.e., the auth modifier
provided through the DaoAuthorizable
base class. The auth('ADMIN_EXECUTE_PERMISSION')
returns an error if the address calling
on the function has not been granted that permission, effectively protecting from malicious use cases.
Later, we will also use the dao() getter function from the base contract, which returns the associated DAO for that plugin.
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});
}
}
In this example, we are building a governance plugin. To increase its capabilities and provide some standardization into the protocol, we recommend completing the governance plugin by
implementing the IProposal and IMembership interfaces.
Optionally, you can also allow certain actions to fail by using the failure map feature of the DAO executor.
|
For now, we used default values for the callId
and allowFailureMap
parameters required by the DAO’s execute
function.
With this, the plugin implementation could be used and deployed already. Feel free to add any additional logic to
your plugin’s capabilities here.
3. Plugin done, Setup contract next!
Now that we have the logic for the plugin implemented, we’ll need to define how this plugin should be installed/uninstalled from a DAO.
In the next step, we’ll write the PluginSetup
contract - the one containing the installation, uninstallation, and
upgrading instructions for the plugin.