A hands-on tour of OSx
In one Foundry test, you'll deploy a DAO, fund it, make it perform an onchain action, and see the permission system reject the same action from the wrong caller.
The test demonstrates the DAO's three roles: a treasury, an executor, and its own permission database. The later guides build on these operations.
What you need
Do the one-time Setup first, a plugin project pointed at a chain where OSx is deployed. This guide reads just the DAOFactory address (DAO_FACTORY_ADDRESS); nothing plugin-specific.
We fork rather than deploy the whole protocol locally: standing up OSx from scratch is the Protocol Factory's job, not something you need for a first look.
Step 1, deploy a bare DAO
DAOFactory.createDao creates, registers, and self-governs a DAO in one transaction. Pass an empty plugin array and the factory grants you (msg.sender) EXECUTE_PERMISSION_ID on the new DAO, so a plugin-less DAO is yours to drive directly until you install governance.
// 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 {Action} from "@aragon/osx/common/executors/IExecutor.sol";
contract OsxInOneSitting is Test {
DAOFactory factory = DAOFactory(vm.envAddress("DAO_FACTORY_ADDRESS")); // exported by just-foundry
DAO dao;
function setUp() public {
vm.createSelectFork(vm.envString("RPC_URL"));
// No plugins -> the caller (this test) receives EXECUTE_PERMISSION_ID on the DAO.
DAOFactory.DAOSettings memory daoSettings = DAOFactory.DAOSettings({
trustedForwarder: address(0),
daoURI: "",
subdomain: "", // empty = skip ENS registration
metadata: ""
});
DAOFactory.PluginSettings[] memory noPlugins = new DAOFactory.PluginSettings[](0);
(dao, ) = factory.createDao(daoSettings, noPlugins);
}
}You now hold EXECUTE, the protocol's most consequential permission. Keeping it on a person is suitable for this demo but dangerous as a permanent arrangement; a DAO's governance plugin should hold it. See how ROOT is bootstrapped safely.
Step 2, fund it, then make it act
A DAO holds assets like any account, and does things by running a batch of actions (one Action is a single external call (to, value, data)). In one test, fund the DAO, then have it transfer 1 ETH back out. Add this function to the contract:
function test_fundAndSpend() public {
// Fund the treasury. Native coin sent directly hits receive(), which just records it.
vm.deal(address(this), 10 ether);
(bool funded, ) = address(dao).call{value: 5 ether}("");
assertTrue(funded);
assertEq(address(dao).balance, 5 ether);
// Ask the DAO to send 1 ETH to Bob.
address payable bob = payable(address(0xB0B));
Action[] memory actions = new Action[](1);
actions[0] = Action({to: bob, value: 1 ether, data: ""});
// callId is a caller-defined tag (a proposal id, normally); allowFailureMap = 0 = all must succeed.
dao.execute({_callId: bytes32(0), _actions: actions, _allowFailureMap: 0});
assertEq(bob.balance, 1 ether);
assertEq(address(dao).balance, 4 ether);
}Funding is permissionless: anyone can pay a DAO, but assets leave only through execute, which is permission-gated. The transfer runs as the DAO. At Bob's address, msg.sender is the DAO and the ether is the DAO's because execute uses call, not delegatecall. This is also why a DAO can call itself to change its own permissions; see why it's a call.
Step 3, watch the permission gate
The transfer worked only because you hold EXECUTE_PERMISSION_ID. The same call from any other address is denied by the permission system before a single action runs:
function test_strangerCannotExecute() public {
Action[] memory actions = new Action[](1);
actions[0] = Action({to: address(0xB0B), value: 0, data: ""});
vm.prank(address(0xDEAD)); // an address with no permission
vm.expectRevert(); // reverts with an Unauthorized-family error before executing
dao.execute(bytes32(0), actions, 0);
}The revert enforces OSx's access control. Every privileged call in the protocol, on the DAO and on every plugin, checks whether (where, who, permissionId) is granted. See how a decision is made.
What you just saw
- A DAO is one contract that holds assets, executes actions, and owns the rules for who may do what, the DAO.
- Actions are how it acts, atomically and as itself, actions and execution.
- Nothing privileged happens without a matching grant, permissions.
Next
- Deploy your first DAO — hand
EXECUTEto a governance plugin in the same transaction, so the organization's governance controls execution. - Manage permissions through governance — grant, revoke, and condition permissions through proposals.