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.
- Bun and Git. The project template uses Bun.
- 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
git clone https://github.com/gnosisguild/zodiac-constellation-template.git my-constellation
cd my-constellation
bun installThe 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 editStep 2: connect your account
bun pullThe browser opens for sign-in. The command writes an API key to .env and generates typed
code into .zodiac/.
The API key in .env gives write access to your workspace drafts. Do not
commit it. Revoke keys at app.zodiac.eco under API keys.
CI and agent runtimes have no browser. Write an existing key to .env as
ZODIAC_API_KEY=… before the first bun pull. The command then skips the
sign-in. 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:
import { defineConfig } from '@zodiaceco/sdk/cli/config'
export default defineConfig({
contracts: {
eth: {
weth: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
},
},
})Run bun pull-contracts. The SDK fetches the ABI 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
In a template project, constellation, allow, and c are globals, and
Permissions and Members are global types.
Create constellation/roles/ops/permissions.ts:
export default [
allow.eth.weth.deposit({ send: true }), // wrap: deposit() with Ether
allow.eth.weth.withdraw(), // unwrap: withdraw(uint256), any amount
] satisfies PermissionsCreate constellation/roles/ops/members.ts with the operator address:
export default ['0xYourOperatorAddress'] satisfies MembersCreate constellation/roles/ops/index.ts to combine the two files:
export { default as members } from './members'
export { default as permissions } from './permissions'Wire the role to your Safe in constellation/index.ts:
import * as ops from './roles/ops'
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, bun push
stops with the error: Node "Treasury" is referenced but not included in the push() call.
Step 5: review and apply
bun pushThe 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
execTransactionWithRoledirectly. 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
- The model you just used — nodes, the apply loop, the project layout: Constellations.
- Limit parameters and amounts: Conditions and Allowances.
- Use tested permission presets for Aave, CoW, Lido, and more: DeFi Kit presets.
- Add a veto window in front of the Safe: Quickstart: Delay.