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:
| Property | Meaning |
|---|---|
paramType | How to decode this part of the calldata. |
operator | The check to apply. |
compValue | The comparison value, when the operator needs one. |
children | The child nodes, when the type has parts. |
Parameter types
paramType | Use for |
|---|---|
Static | Fixed-size values: uint256, address, bytes32, bool. |
Dynamic | Variable-size values: bytes, string. |
Tuple | Structs. The children describe the fields. |
Array | Arrays. The children describe the elements. |
AbiEncoded | A bytes value that contains ABI-encoded data. The children describe the decoded content. |
Calldata | Like AbiEncoded, but the first 4 bytes (the function selector) are skipped. The root node of a condition uses this type. |
None | The node does not point at calldata. Logic operators use this type. |
Operators
Structure
| Operator | Check |
|---|---|
Pass | No check. The node exists only to describe the decoding. |
Matches | All children pass, in their positions. |
Comparison
| Operator | Check |
|---|---|
EqualTo | The value equals compValue. |
EqualToAvatar | The value equals the avatar address. Useful for recipient fields. |
GreaterThan / LessThan | The unsigned integer is greater / less than compValue. |
SignedIntGreaterThan / SignedIntLessThan | The same, for signed integers. |
Bitmask | Selected bits equal an expected pattern. compValue packs <2 bytes offset><15 bytes mask><15 bytes expected>. |
Logic
| Operator | Check |
|---|---|
And | Every child passes. |
Or | At least one child passes. |
Nor | No child passes. |
Logic nodes have paramType: None. All their children address the same calldata range.
Array
| Operator | Check |
|---|---|
ArrayEvery | Every element passes the child condition. |
ArraySome | At least one element passes the child condition. |
ArraySubset | Each element passes a different child condition; extra elements fail. |
Allowance
| Operator | Check |
|---|---|
WithinAllowance | The integer value fits in the allowance and consumes it. |
EtherWithinAllowance | The Ether value of the call fits in the allowance and consumes it. |
CallWithinAllowance | The call itself consumes 1 from the allowance (a rate limit). |
See Allowances.
Extension
| Operator | Check |
|---|---|
Custom | An 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 PermissionsThe 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.