Skip to Content
DevelopersRolesConditions

Conditions

A condition limits the parameter values of an allowed function. Without conditions, a member can call an allowed function with any input. With conditions, the Roles Modifier inspects the calldata and reverts calls that do not match.

In the Zodiac App, the “allow any” toggle creates a wildcard, and a pinned parameter value becomes an equality condition. Record real calls with Zodiac Pilot to get a starting point. Author complex trees with the SDK, as this page shows.

Conditions are trees

A condition is a tree. The tree has the same shape as the ABI encoding of the function parameters. Each node checks one part of the calldata.

Example: allow transfer(address to, uint256 amount) only to the payroll address and only within a spending limit:

Calldata: Matches ├── Static (to): EqualTo <payroll address> └── Static (amount): WithinAllowance <allowance key>

Each node has four properties:

PropertyMeaning
paramTypeHow to decode this part of the calldata.
operatorThe check to apply.
compValueThe comparison value, when the operator needs one.
childrenThe child nodes, when the type has parts.

Parameter types

paramTypeUse for
StaticFixed-size values: uint256, address, bytes32, bool.
DynamicVariable-size values: bytes, string.
TupleStructs. The children describe the fields.
ArrayArrays. The children describe the elements.
AbiEncodedA bytes value that contains ABI-encoded data. The children describe the decoded content.
CalldataLike AbiEncoded, but the first 4 bytes (the function selector) are skipped. The root node of a condition uses this type.
NoneThe node does not point at calldata. Logic operators use this type.

Operators

Structure

OperatorCheck
PassNo check. The node exists only to describe the decoding.
MatchesAll children pass, in their positions.

Comparison

OperatorCheck
EqualToThe value equals compValue.
EqualToAvatarThe value equals the avatar address. Useful for recipient fields.
GreaterThan / LessThanThe unsigned integer is greater / less than compValue.
SignedIntGreaterThan / SignedIntLessThanThe same, for signed integers.
BitmaskSelected bits equal an expected pattern. compValue packs <2 bytes offset><15 bytes mask><15 bytes expected>.

Logic

OperatorCheck
AndEvery child passes.
OrAt least one child passes.
NorNo child passes.

Logic nodes have paramType: None. All their children address the same calldata range.

Array

OperatorCheck
ArrayEveryEvery element passes the child condition.
ArraySomeAt least one element passes the child condition.
ArraySubsetEach element passes a different child condition; extra elements fail.

Allowance

OperatorCheck
WithinAllowanceThe integer value fits in the allowance and consumes it.
EtherWithinAllowanceThe Ether value of the call fits in the allowance and consumes it.
CallWithinAllowanceThe call itself consumes 1 from the allowance (a rate limit).

See Allowances.

Extension

OperatorCheck
CustomAn external adapter contract decides. See Custom conditions.

Write conditions with the SDK

The SDK builds the tree for you. You describe values, and the SDK derives types from the contract ABI. The examples on this page are tested with zodiac-roles-sdk 4.0.0. In a constellation project, these expressions live in the permissions.ts files of your roles, and the condition helpers are available as the global c. In a standalone script, import them:

import { c } from 'zodiac-roles-sdk'

Equality. Pass a value where a condition is expected. These two are equal:

allow.eth.dai.approve(CURVE_3POOL) allow.eth.dai.approve(c.eq(CURVE_3POOL))

The avatar address. Use c.avatar for fields that must equal the Safe:

allow.eth.curve.gauge['claim_rewards(address)'](c.avatar) // ✅ c.avatar, not c.avatar()

Comparisons and skipped parameters. Use undefined for “any value”:

allow.eth.dai.transfer(undefined, c.lt(1000n * 10n ** 18n))

Structs. An object literal matches the given fields and accepts any value for missing fields. Use c.eq to force a full match:

c.matches({ recipient: c.avatar, deadline: c.lt(1735689600) })

Logic. Combine checks on the same value. Write the type parameters yourself, because TypeScript cannot infer them for c.or:

allow.eth.dai.approve(c.or<[string, string], string>(AAVE_POOL, SPARK_POOL))

For most cases, skip the logic helper: write one permission per branch. The SDK merges them — see One function, several conditions.

Nested calldata. c.calldataMatches scopes a bytes parameter that contains a call, for example in batch or relay functions:

c.calldataMatches([c.avatar], ['address'])

It also accepts a permission object, and options for Ether and call allowances:

c.calldataMatches([], [], { etherWithinAllowance: encodeKey('eth_budget') })

Allowances. Reference an allowance by its encoded key. A hand-written permission reaches the chain as authored, so the label an allowance declares has to be encoded here — encodeKey leaves an already-encoded key alone:

allow.eth.usdc.transfer( undefined, c.withinAllowance(encodeKey(payoutAllowance.key)), )

The SDK does not author Custom operator nodes yet. To use a custom condition adapter, configure scopeFunction at the contract level. See Custom conditions.

One function, several conditions

When several permissions in one role cover the same function of the same target, the SDK merges them into one condition tree with Or semantics. The call passes when any of the merged conditions passes. Use this to give one function several allowed shapes:

export default [ // payroll transfers, metered by the payroll budget allow.eth.usdc.transfer( PAYROLL_SAFE, c.withinAllowance(encodeKey(payroll_budget.key)), ), // grant transfers, metered by the grants budget allow.eth.usdc.transfer( GRANTS_SAFE, c.withinAllowance(encodeKey(grants_budget.key)), ), ] satisfies Permissions

The merge widens access. A second, stricter permission for the same function does not tighten the first one. This also applies to DeFi Kit presets: a preset that leaves an amount open keeps it open, also when a metered permission for the same function sits next to it. To meter an amount, author that function permission yourself, and do not include a preset that allows the same function openly.

Integrity checks

The contract validates every condition tree on write, with the Integrity periphery contract. Malformed trees revert. The SDK runs the same checks offline through targetIntegrity. If a tree passes the SDK check, it also passes onchain.

What conditions cannot do

A condition sees the current call only. It cannot:

  • Read other contract state, for example token balances or oracle prices.
  • Compare two parameters against each other.
  • Judge the economic outcome of a call, for example the received amount of a swap.

For state reads and complex decoding, write a custom condition. For batch transactions that hide inner calls, configure transaction unwrapping.

Last updated on