Install a plugin into a live DAO
Install a plugin into a DAO that's already running through the PluginSetupProcessor (PSP): prepare permissionlessly, then apply through a governed proposal that grants temporary ROOT for one PSP call. In Deploy your first DAO, the factory handled permissions at creation. Adding plugins later is more common, and governance coordinates the permission changes in place of the factory.
The split is the safety property: whoever computes an installation isn't who authorizes it. Prepare touches nothing; apply is what the DAO approves. See why prepare and apply are separate.
What you need
The one-time Setup, with the multisig plugin's remapping enabled. This guide reads the DAOFactory, PluginSetupProcessor (PLUGIN_SETUP_PROCESSOR_ADDRESS), and Multisig PluginRepo addresses.
Step 1, the skeleton
As in manage permissions, the test uses a bare DAO where you hold EXECUTE, so you can dao.execute the apply batch directly; on a governed DAO those same actions are a proposal.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import {Test} from "forge-std/Test.sol";
import {DAO} from "@aragon/osx/core/dao/DAO.sol";
import {DAOFactory} from "@aragon/osx/framework/dao/DAOFactory.sol";
import {PluginRepo} from "@aragon/osx/framework/plugin/repo/PluginRepo.sol";
import {PluginSetupProcessor} from "@aragon/osx/framework/plugin/setup/PluginSetupProcessor.sol";
import {PluginSetupRef, hashHelpers} from "@aragon/osx/framework/plugin/setup/PluginSetupProcessorHelpers.sol";
import {PermissionManager} from "@aragon/osx/core/permission/PermissionManager.sol";
import {IPluginSetup} from "@aragon/osx/common/plugin/setup/IPluginSetup.sol";
import {Action} from "@aragon/osx/common/executors/IExecutor.sol";
import {IPlugin} from "@aragon/osx/common/plugin/IPlugin.sol";
import {Multisig} from "@aragon/multisig-plugin/Multisig.sol";
contract InstallPlugin is Test {
DAOFactory factory = DAOFactory(vm.envAddress("DAO_FACTORY_ADDRESS"));
PluginSetupProcessor psp = PluginSetupProcessor(vm.envAddress("PLUGIN_SETUP_PROCESSOR_ADDRESS"));
PluginRepo multisigRepo = PluginRepo(vm.envAddress("MULTISIG_PLUGIN_REPO_ADDRESS"));
DAO dao;
function setUp() public {
vm.createSelectFork(vm.envString("RPC_URL"));
DAOFactory.DAOSettings memory s = DAOFactory.DAOSettings(address(0), "", "", "");
DAOFactory.PluginSettings[] memory none = new DAOFactory.PluginSettings[](0);
(dao, ) = factory.createDao(s, none); // bare DAO: this test holds EXECUTE
}
}Step 2, prepare (permissionless)
Anyone may call prepareInstallation. It runs the plugin's setup, deploying the plugin and any helpers and returning the exact permissions the install wants, without touching the DAO. Here we prepare the same multisig as the deploy guide:
function test_installMultisig() public {
// Multisig install data (see "Deploy your first DAO").
address[] memory members = new address[](3);
members[0] = address(0xA11CE); members[1] = address(0xB0B); members[2] = address(0xCa201);
Multisig.MultisigSettings memory settings = Multisig.MultisigSettings({onlyListed: true, minApprovals: 2});
IPlugin.TargetConfig memory targetConfig =
IPlugin.TargetConfig({target: address(0), operation: IPlugin.Operation.Call});
bytes memory data = abi.encode(members, settings, targetConfig, bytes(""));
PluginSetupRef memory ref = PluginSetupRef({
versionTag: PluginRepo.Tag({release: 1, build: 3}),
pluginSetupRepo: multisigRepo
});
// Prepare: deploys the plugin + helpers, returns the requested permissions. Nothing on the DAO changes yet.
(address plugin, IPluginSetup.PreparedSetupData memory prepared) =
psp.prepareInstallation(address(dao), PluginSetupProcessor.PrepareInstallationParams(ref, data));
// ... apply in Step 3 (same function)
}A …Prepared event is not an installation. A griefer can prepare repeatedly and change nothing. See preparing is cheap and permissionless.
Step 3, apply (governed, inside a temporary-ROOT window)
Applying grants the permissions, so the PSP needs ROOT on the DAO for the duration, and no longer. The proposal has three actions: grant ROOT, apply, revoke ROOT. You must apply exactly what was prepared: the same plugin, permissions, and a hash of the helpers. A setup-id check enforces this match.
bytes32 ROOT = dao.ROOT_PERMISSION_ID();
PluginSetupProcessor.ApplyInstallationParams memory applyParams = PluginSetupProcessor.ApplyInstallationParams({
pluginSetupRef: ref,
plugin: plugin,
permissions: prepared.permissions,
helpersHash: hashHelpers(prepared.helpers)
});
Action[] memory actions = new Action[](3);
// 1. Grant the PSP ROOT on the DAO (so it can apply the plugin's permission changes).
actions[0] = Action(address(dao), 0,
abi.encodeCall(PermissionManager.grant, (address(dao), address(psp), ROOT)));
// 2. Apply: the PSP grants the plugin its permissions (incl. EXECUTE on the DAO).
actions[1] = Action(address(psp), 0,
abi.encodeCall(PluginSetupProcessor.applyInstallation, (address(dao), applyParams)));
// 3. Close the window: revoke the PSP's ROOT.
actions[2] = Action(address(dao), 0,
abi.encodeCall(PermissionManager.revoke, (address(dao), address(psp), ROOT)));
dao.execute(bytes32(0), actions, 0);
// The multisig now governs the DAO.
assertTrue(dao.hasPermission(address(dao), plugin, dao.EXECUTE_PERMISSION_ID(), ""));
}The DAO needs no APPLY_INSTALLATION_PERMISSION. applyInstallation runs as the DAO (msg.sender == dao), which the PSP's canApply check accepts directly. That permission is required only for a caller other than the DAO. The DAOFactory grants it to itself because the factory calls the PSP, so it needs an extra grant that a self-executing proposal doesn't. The temporary-ROOT window is otherwise the same and remains atomic: if any step reverts, the whole install rolls back and the PSP never keeps ROOT.
What you just saw
- Prepare is permissionless and mutates nothing; apply is the governed step, and the only thing that changes the DAO.
- Apply runs inside a temporary-ROOT window the same batch opens and closes, never leave the PSP with ROOT.
- You apply exactly what was prepared (
plugin+permissions+hashHelpers(helpers)); the setup-id binding rejects anything else.
Next
- Update a plugin, the same prepare/apply sequence for moving an installed plugin to a new version.
- Build a plugin, write the
PluginSetupwhose prepared permissions this flow applies.