Authorizing against a DAO
A plugin does not keep its own access-control list. It defers every authorization decision to its DAO's permission system. DaoAuthorizable is the small base contract that wires that up, and it is where the plugin-to-DAO coupling lives.
The pattern
Inherit DaoAuthorizable (or its upgradeable variant), store a reference to your DAO, and gate functions with the auth modifier:
contract MyPlugin is DaoAuthorizableUpgradeable {
bytes32 public constant DO_THING_PERMISSION_ID = keccak256("DO_THING_PERMISSION");
function initialize(IDAO _dao) external initializer {
__DaoAuthorizableUpgradeable_init(_dao);
}
function doThing() external auth(DO_THING_PERMISSION_ID) {
// only callers the DAO has granted DO_THING_PERMISSION_ID on this plugin
}
}The auth(permissionId) modifier calls:
dao().hasPermission(
address(this), // where = the plugin
_msgSender(), // who = the caller
permissionId,
_msgData() // data = full calldata, available to a condition
);and reverts DaoUnauthorized if the DAO says no. So the plugin is the where and its DAO is the authority. To let Alice call doThing, the DAO's ROOT holder grants (myPlugin, alice, DO_THING_PERMISSION_ID), see granting.
This is also how a governance plugin becomes able to act: the DAO grants the plugin EXECUTE_PERMISSION_ID on the DAO, so the plugin may call dao.execute(...).
Two variants
DaoAuthorizable | DaoAuthorizableUpgradeable | |
|---|---|---|
| For | non-upgradeable contracts (new) | proxy-based contracts (clones, UUPS) |
| DAO stored as | immutable, set in constructor | storage var, set in initializer |
| Wire-up | constructor(IDAO _dao) | __DaoAuthorizableUpgradeable_init(_dao) |
Pick the one matching your plugin type. The three plugin base contracts already inherit the right variant, so you normally get auth for free by extending a plugin base.
Both are meta-transaction aware (they use OpenZeppelin Context's _msgSender()/_msgData()), so they work behind an ERC-2771 trusted forwarder.
Which auth? (Unauthorized vs DaoUnauthorized)
auth, and its underscore helper _auth, appear in two places, and which one you're looking at tells you who the authority is:
DaoAuthorizable.auth(this page) — for a contract that defers to a separate DAO: plugins, and anything else built onDaoAuthorizable. The modifier calls the free function_auth(dao, where, who, permissionId, data)inauth.sol, which asksdao.hasPermission(...)and, on failure, revertsDaoUnauthorized(dao, where, who, permissionId). The extradaofield is there because the reverting contract isn't the DAO, so the error has to say which DAO denied the call.PermissionManager.auth(core) — for the DAO, which is its own permission manager, gating its own privileged functions. Its modifier calls an internal_auth(permissionId)that checksmsg.senderagainstaddress(this)and revertsUnauthorized(where, who, permissionId), nodaofield, because here the contract is the DAO (whereis it).
It's the same-named modifier and the same _auth convention (the underscore version is the implementation the modifier delegates to), on opposite sides of the coupling. As a rule of thumb, DaoUnauthorized comes from a contract authorizing against a DAO elsewhere; Unauthorized comes from the permission manager authorizing itself.
Keep in mind
- Forgetting
__DaoAuthorizableUpgradeable_init(_dao)in yourinitializeleaves the DAO reference ataddress(0), so everyauthcheck callsaddress(0).hasPermission(...)and reverts. If a freshly installed plugin reverts on every gated call, check the init. - The plugin is the
where. Permissions on a plugin's functions are keyed to the plugin's address, not the DAO's; grant on the plugin, not on the DAO.
See also
- The permission system — what
hasPermissionresolves. - Permission conditions — the
_msgData()passed through lets a condition inspect call arguments. - Choosing a plugin base — which variant you inherit.