Skip to content

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.

solidity
// 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:

solidity
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:

solidity
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:

solidity
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 + grantWithCondition in one batch. A plain re-grant can't strip a condition, and two transactions leave a period when the permission is denied.

Next ​