Skip to Content
DevelopersQuickstart: Roles

Quickstart: permission a Safe with Roles

Goal — In about 20 minutes, you deploy a Roles Modifier on a Safe and give an operator address one narrow power: wrap and unwrap ETH through the WETH contract. Nothing else.

You need:

  • A Safe in your Zodiac  workspace. See Import a Safe if it is not there yet.
  • Node.js  22 or later. pnpm and Bun  work too.
  • A browser wallet that can sign for your Zodiac account.

This quickstart uses the Zodiac SDK, which applies changes through the Zodiac API.

Step 1: create the project

npm create @zodiaceco@latest my-constellation cd my-constellation

The command copies the project template into my-constellation, installs its dependencies, and commits it to a new git repository. Run it as pnpm dlx @zodiaceco/create or bunx @zodiaceco/create to install with pnpm or Bun instead.

The folders that matter:

constellation/index.ts # entry point; exported nodes get deployed constellation/roles/<role>/ # one folder per role: index, members, permissions zodiac.config.ts # contract registry .zodiac/ # generated code; do not edit

The template comes with an example on Ethereum: a new Safe, a Roles Modifier on it, and a role treasury_ops. This quickstart replaces it with a role on a Safe you already have.

Step 2: connect your account

The command offers to connect the project to Zodiac. Accept, and the browser opens for sign-in. The command writes an API key to .env, generates typed code into .zodiac/, and points the constellation at your workspace.

If you skipped it, connect later:

npm run pull

The API key in .env gives write access to your workspace drafts. Do not commit it. Revoke keys in the Zodiac App under Profile → API keys.

CI and agent runtimes have no browser. Write an existing key to .env as ZODIAC_API_KEY=… before the first npm run pull. The command then skips the sign-in. npx zodiac init mints a new key for the current directory.

Step 3: register the WETH contract

Contracts you want to permission go into zodiac.config.ts, per chain. The template already registers WETH:

import { defineConfig } from '@zodiaceco/sdk/cli/config' export default defineConfig({ contracts: { eth: { weth: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', }, }, })

When you add a contract, run npm run pull-contracts. The SDK fetches its ABI into abis/ and generates a typed permission builder.

The keys you choose here become the paths in the allow kit. The chain key eth gives you allow.eth.weth.…. Keep the same chain key across the whole project.

Step 4: define the role

Import constellation, allow, c, and the Permissions and Members types from @zodiaceco/sdk.

Create constellation/roles/ops/permissions.ts:

import { allow, type Permissions } from '@zodiaceco/sdk' export default [ allow.eth.weth.deposit({ send: true }), // wrap: deposit() with Ether allow.eth.weth.withdraw(), // unwrap: withdraw(uint256), any amount ] satisfies Permissions

Create constellation/roles/ops/members.ts with the operator address:

import type { Members } from '@zodiaceco/sdk' export default ['0xYourOperatorAddress'] satisfies Members

Create constellation/roles/ops/index.ts to combine the two files:

export { default as members } from './members' export { default as permissions } from './permissions'

In constellation/roles/index.ts, replace the template’s role with ops:

export * as ops from './ops'

Delete the template’s constellation/roles/treasury_ops/.

Replace constellation/index.ts to wire the role to your Safe:

import { constellation } from '@zodiaceco/sdk' import { ops } from './roles' const eth = constellation({ workspace: 'Default workspace', label: 'Ops permissions', chain: 1, }) // Reference the existing Safe from your workspace. Export it: push() // resolves referenced nodes by export name. export const treasury = eth.safe['Treasury'] export const opsRoles = eth.roles['Ops Roles']({ nonce: 0n, // deploy a new instance owner: treasury, avatar: treasury, target: treasury, roles: { ops }, })

A Roles node takes nonce for a new deployment, or address to bind an existing instance. Never both.

Export every node that another node references, including a Safe that already exists in your workspace. When a referenced node is not an export, npm run push stops with the error: Node "Treasury" is referenced but not included in the push() call.

Step 5: review and apply

Check what you are about to push. This prints the constellation without sending it, and needs no API key:

npx zodiac push --dry-run

Then push it:

npm run push

The SDK computes the difference between your files and the chain. The browser opens the Zodiac App with the change set: one deployment, the enableModule call, and the permission calls. Review it, then sign with the Safe.

The result on chain: a new Roles Modifier proxy, enabled on the Safe, with the role ops, its member, and the two WETH permissions.

Step 6: execute as the operator

The operator can now wrap ETH through the role — and can do nothing else. Everything you did not allow reverts onchain.

The operator has two ways to execute:

  • Zodiac Pilot — the browser extension routes normal dapp usage through the role.
  • A script — call execTransactionWithRole directly. See Execute as a role member for a complete viem example.

Test the boundary: try a USDC transfer as the operator. It must revert with ConditionViolation — see how to read revert reasons.

Where to go next

Last updated on