Skip to content

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.

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

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

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