| type | concept | ||
|---|---|---|---|
| title | The PluginSetupProcessor (PSP) | ||
| tags |
|
||
| source | osx/src/framework/plugin/setup/PluginSetupProcessor.sol, osx/src/framework/plugin/setup/PluginSetupProcessorHelpers.sol |
The PSP installs, updates, and uninstalls plugins on a DAO. It is the only contract that changes a DAO's permissions on behalf of a plugin setup. To do that, it must be temporarily granted ROOT_PERMISSION_ID on the DAO. Controlling how the PSP receives and uses that permission is critical to the framework's security.
Every lifecycle operation is two steps:
prepare…— permissionless. Anyone can call it. It runs the setup'sprepare…function, which deploys the plugin and helpers and returns the requested permission changes. It does not touch the DAO; it only records that a specific preparation is now pending.apply…— permission-gated. It calls the DAO'sapplyMultiTargetPermissionsto actually grant/revoke, which is why the PSP needs ROOT on the DAO here.
Why split them? Because the party who computes an installation (permissionless, could be anyone, could be malicious) must not be the party who authorizes it. Splitting lets a DAO's governance review the exact permission set a preparation would apply, and approve it through a proposal, before any state changes. The preparer can grant themselves nothing; only a DAO-authorized apply mutates anything. A hashing scheme binds what gets applied to what was prepared, so nothing can be swapped in between.
This is the core safety property: preparation is permissionless, and application is governed.
Calling an apply… requires the caller to hold, on the PSP, the matching permission granted by the target DAO (or to be the DAO itself):
APPLY_INSTALLATION_PERMISSION_IDAPPLY_UPDATE_PERMISSION_IDAPPLY_UNINSTALLATION_PERMISSION_ID
Because apply… calls the DAO's permission functions, the PSP must hold ROOT_PERMISSION_ID on the DAO for the duration of the apply, and must not keep it. Leaving the PSP with ROOT is a critical vulnerability (it could then change any permission on the DAO). The safe pattern, which the DAOFactory performs atomically in one transaction:
grant PSP ROOT on DAO
grant caller APPLY_INSTALLATION on PSP
PSP.applyInstallation(...) // PSP grants the plugin's permissions
revoke caller APPLY_INSTALLATION on PSP
revoke PSP ROOT on DAO
Creating a DAO isn't the only time you install a plugin. More often, you add one to a DAO that's already running. The mechanism is the same grant→apply→revoke, but there's no factory to coordinate it, so governance does. The install becomes a proposal whose actions are exactly:
- grant the PSP
ROOTon the DAO, - call
PSP.applyInstallation(...)(against a preparation someone already made permissionlessly), - revoke the PSP's
ROOT.
When that proposal passes and the DAO executes it, the plugin is installed, its permission changes applied, and the temporary ROOT revoked, all in the one transaction the DAO's own governance authorized. For every "add a plugin" integration, the proposal grants ROOT before the apply and revokes it afterward. Updates and uninstalls work the same way, with the matching APPLY_* permission and apply… call.
The PSP tracks per-plugin state keyed by pluginInstallationId = keccak256(dao, plugin). It uses two hashes:
- prepared setup id — fingerprints a preparation: the version, the exact permissions, the helpers, the extra data, and the operation type (
Installation/Update/Uninstallation). Mixing the type in means an install preparation can't be replayed as an update. Atapply, the PSP recomputes this from the arguments you pass and requires a match, so you must apply exactly what was prepared. - applied setup id — fingerprints the live state: version + helpers (no permissions). It's what
prepareUpdate/prepareUninstallationrecompute from the payload you supply, which is how the PSP verifies you actually know the plugin's true current version and helper set (InvalidAppliedSetupIdotherwise).
The block-number invariant. A preparation is applicable only if it was prepared after the last applied action for that plugin. Every
apply…records the current block in the plugin's state, which invalidates all other pending preparations for that plugin. So if two competing installations are prepared, applying one automatically prevents the other from being applied (SetupNotApplicable). This prevents stale or competing preparations from being applied later.
-
Install —
prepareInstallationdeploys + records;applyInstallationgrants the permissions. Fails if the plugin is already installed. -
Update (UUPS plugins only) —
prepareUpdateis release-locked: the release number must stay the same and the build must strictly increase (InvalidUpdateVersionotherwise).applyUpdateperforms the actual UUPS proxy upgrade (using theinitDatafrom prepare) and applies any permission diff.There's a metadata-only update fast path, often called "UI-only": only the off-chain metadata, such as display text, docs, or the params schema, has changed. The onchain setup/implementation is identical. The proxy isn't upgraded and nothing is redeployed; only the version pointer changes.
Because it changes no permissions,
applyUpdateskips the DAO'sapplyMultiTargetPermissionscall entirely. It also skips the proxy upgrade, since the implementation is unchanged. A metadata-only update therefore needs onlyAPPLY_UPDATE_PERMISSION_IDon the PSP, not the temporary-ROOT window an update that changes permissions or code requires. It is the one lifecycle step that touches neither permissions nor code, which makes it cheap to authorize. -
Uninstall —
prepareUninstallationreturns the permissions to revoke;applyUninstallationrevokes them and clears the plugin's applied-setup id (so it could later be reinstalled).
Because updates can't cross releases, migrating across a release is an uninstall + reinstall, by design: a new release signals an incompatible change (see PluginRepo).
- Helper order matters. You must pass
currentHelpersin the exact order a prior prepare returned them, or the applied-setup-id check fails. - Preparing is cheap and permissionless, so a griefer can deploy plugin instances at will, but can never apply them. Don't treat a
…Preparedevent as an installation. - Never leave the PSP with ROOT. If you script installs manually, revoke ROOT in the same transaction.
- Plugin setup — the setups the PSP runs.
- The permission system —
applyMultiTargetPermissionsand ROOT. - DAOFactory — the reference implementation of the temporary-ROOT sequence.
- PluginRepo — where the PSP looks up which setup to run.