Manage permissions through governance
A DAO owns its own permission database, so changing who may do what is itself an action the DAO performs on itself. A DAO that holds ROOT over itself can call dao.grant(...) through a passed proposal: the action runs as the DAO, which is its own permission admin. Here you'll grant a permission, gate one with a condition you write, and rotate that condition safely.
grant, revoke, and grantWithCondition are all auth(ROOT_PERMISSION_ID), so they only work when called by (or as) the ROOT holder, which is the DAO itself.
Step 1, the skeleton
This test uses a bare DAO where you hold EXECUTE (as in the hands-on tour), so you can dao.execute the permission-change actions directly and see the effect. On a governed DAO, these exact actions go inside a proposal. Either way, they run as the DAO.
// 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 {PermissionManager} from "@aragon/osx/core/permission/PermissionManager.sol";
import {Action} from "@aragon/osx/common/executors/IExecutor.sol";
import {IPermissionCondition} from "@aragon/osx/common/permission/condition/IPermissionCondition.sol";
contract ManagePermissions is Test {
DAOFactory factory = DAOFactory(vm.envAddress("DAO_FACTORY_ADDRESS"));
DAO dao;
address alice = address(0xA11CE);
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, grant a permission
Have the DAO grant Alice the right to set its metadata. The action targets the DAO's own grant:
function test_grant() public {
bytes32 SET_METADATA = dao.SET_METADATA_PERMISSION_ID();
Action[] memory actions = new Action[](1);
actions[0] = Action({
to: address(dao),
value: 0,
data: abi.encodeCall(PermissionManager.grant, (address(dao), alice, SET_METADATA))
});
dao.execute(bytes32(0), actions, 0);
assertTrue(dao.hasPermission(address(dao), alice, SET_METADATA, ""));
}The grant reads (where = dao, who = alice, permissionId = SET_METADATA), see the permission triple. It works because the action executes as the DAO, which holds ROOT. revoke is the mirror image with the same three arguments.
Step 3, write a custom condition
A plain grant is a static "yes". A condition makes it a dynamic yes: the permission system calls your contract at decision time. Inherit PermissionCondition (it supplies the ERC-165 wiring grantWithCondition checks for) and implement isGranted. Here, a permission that only holds until a deadline:
import {PermissionCondition} from "@aragon/osx/common/permission/condition/PermissionCondition.sol";
contract OnlyBeforeDeadline is PermissionCondition {
uint256 public immutable deadline;
constructor(uint256 _deadline) {
deadline = _deadline;
}
function isGranted(address /*where*/, address /*who*/, bytes32 /*permissionId*/, bytes calldata /*data*/)
external
view
returns (bool)
{
return block.timestamp <= deadline;
}
}A condition fails closed: a revert or invalid return counts as "denied". Its data argument is the full calldata of the gated call, so a condition can decode and check the arguments. For boolean combinations of standard checks (time, block, sub-conditions), RuledCondition expresses them as data, often avoiding bespoke code. The condition library also supplies ready-made conditions.
Step 4, gate a permission, then rotate the condition safely
Attach the condition with grantWithCondition. A plain grant over an already-conditional permission is a silent no-op: it will not strip the condition. Conditions are immutable once set, so changing one requires revoke followed by re-grant. Do both in one batch; otherwise, the permission is denied between the two transactions:
function test_conditionAndRotate() public {
bytes32 SET_METADATA = dao.SET_METADATA_PERMISSION_ID();
// Gate Alice's permission with a condition.
IPermissionCondition cond = new OnlyBeforeDeadline(block.timestamp + 30 days);
Action[] memory gate = new Action[](1);
gate[0] = Action({to: address(dao), value: 0,
data: abi.encodeCall(PermissionManager.grantWithCondition,
(address(dao), alice, SET_METADATA, cond))});
dao.execute(bytes32(0), gate, 0);
// Rotate to a new condition: revoke + re-grant in ONE proposal, never two.
IPermissionCondition cond2 = new OnlyBeforeDeadline(block.timestamp + 90 days);
Action[] memory rotate = new Action[](2);
rotate[0] = Action({to: address(dao), value: 0,
data: abi.encodeCall(PermissionManager.revoke, (address(dao), alice, SET_METADATA))});
rotate[1] = Action({to: address(dao), value: 0,
data: abi.encodeCall(PermissionManager.grantWithCondition,
(address(dao), alice, SET_METADATA, cond2))});
dao.execute(bytes32(0), rotate, 0);
// Alice still holds the permission, now under the new condition (evaluated live, before the deadline).
assertTrue(dao.hasPermission(address(dao), alice, SET_METADATA, ""));
}Batching the revoke and re-grant into one atomic execute prevents a period between transactions when the permission is denied. On a governed DAO, put both in the same proposal.
What you just saw
- Changing authority is an ordinary DAO action, gated by
ROOT, which the DAO holds over itself. Governance can therefore change the DAO's own rules. - A condition is a small contract you attach at grant time; it fails closed and can inspect the call's arguments.
- Rotate a condition with
revoke+grantWithConditionin one batch. A plain re-grant can't strip a condition, and two transactions leave a period when the permission is denied.
Next
- Install a plugin into a live DAO, the permission changes required to add governance after launch.
- Build a plugin, whose
PluginSetupreturns exactly these grant/revoke operations for the installer to apply.