General Concepts
Conditions

Conditions

Zodiac Roles implements a robust conditions system that allows for defining complex conditions over calldata and ether value.

A condition is represented by a tree structure. Each node in the tree defines a condition operator and specifies a parameter type for the value in scope. Evaluating the condition tree on transaction calldata starts at the root node. As the tree is traversed the calldata is decoded and specific ranges are plucked according to the parameter type definitions specified with the nodes.

Node Properties

Each condition node is defined by the following properties:

paramType

Specifies how to decode and pluck the parameter value from calldata. It can be one of the following:

ValueDescription
StaticInterpret the word currently in scope as a static value, i.e., a value with a fixed length padded to 32 bytes.
DynamicInterpret the word currently in scope as an offset to a dynamic length value, i.e., bytes or string.
TupleDecode the value as a tuple. The children condition nodes inform the types of the tuple's fields.
ArrayInterpret the word as an offset to a dynamic length array. The children condition nodes inform the array element type.
AbiEncodedDecode the value using standard ABI decoding. The children condition nodes inform the individual field types.
CalldataDecode the value like AbiEncoded, but skip the first 4 bytes (function selector).
NoneUsed with logical expressions. The children condition nodes inform how to interpret the current scope.

children

An array of sub conditions, used for any of the following purposes:

  • Inform the decoding of complex types by defining types of their fields or elements (required for ParamType.AbiEncoded, ParamType.Calldata, ParamType.Tuple, ParamType.Array)
  • Set conditions on specific fields of a tuple type value (via Operator.Matches)
  • Set conditions on elements of an array type value (via Operator.ArraySome, Operator.ArrayEvery, Operator.ArraySubset)
  • Define branch conditions for n-ary logical operators (Operator.And, Operator.Or, Operator.Nor)

operator

Defines the conditional operation to apply to the value plucked from calldata. (Refer to the Operators section for a list of available operators.)

compValue

Defines the value to compare against (only used with comparison operators).

Operators

Matches

Allows setting conditions on selected fields of tuple type parameters. Used with ParamType.Calldata it allows setting conditions on specific parameters of the function call. Used with ParamType.AbiEncoded it allows setting conditions on selected ABI encoded variables within a bytes value.

Comparisons

EqualTo

Checks equality of the plucked value to a given comparison value.

EqualToAvatar

Equivalent to using EqualTo with the avatar address as compValue.

This extra operator is provided for convenience and optimization, since comparing with the avatar address is a common operation. The avatar address is available to the condition evaluation function and therefore does not need to be stored as a comparison value.

GreaterThan

Checks that the plucked uint value is greater than the comparison value.

LessThan

Checks that the plucked uint value is greater than the comparison value.

SignedIntGreaterThan

Checks that the plucked int value is greater than the comparison value.

SignedIntLessThan

Checks that the plucked int value is less than the comparison value.

Bitmask

Checks that bytes value in scope matches the bitmask encoded in the comparison value.

The bitmask is packed into a bytes32 compValue in the following way:

<bytes2 offset><bytes15 mask><bytes15 expected value>
  • offset specifies the offset in bytes from the start of the bytes value to the first byte that should be masked.
  • mask specifies which bytes should be checked. A 1 bit means that the bit at the same position should be checked, a 0 bit means that the bit should be ignored.
  • expected value specifies the expected values of the bits under the mask.

Array Expressions

ArraySome

Checks that at least one element of the array matches the condition.

ArrayEvery

Checks that every element of the array matches the condition.

ArraySubset

Checks that every element of the array passes at least one child condition. Every child condition must only be taken into account once, meaning that if multiple elements are supposed to match the same condition that condition must appear multiple times in the children.

Logical Expressions

Logical expressions are n-ary operators that take an array of child conditions and combine their evaluation results. Generally having a paramType of None logical expressions do not change the evaluation scope, meaning that every child condition will address the same calldata range.

And

Checks that every child condition evaluates to true.

Or

Checks that at least one of the child conditions evaluates to true.

Nor

Checks that every child condition evaluates to false.

Allowance Expressions

Allowances serve as a mechanism for tracking quotas. Each allowance expression references a specific allowance in the compValue field by its bytes32 key. After a call passes the condition and has been successfully executed, all allowances consumed within the condition will be updated. If the condition evaluates to false or executing the call reverts, allowances will not be updated.

WithinAllowance

Checks that the uint value in scope does not exceed the given allowance and consumes that amount.

EtherWithinAllowance

Checks that the transaction's ether value does not exceed the given allowance and consumes that amount.

CallWithinAllowance

Useful for tracking function call quotas, checks that the given allowance is not depleted and decrements it by `1.

Pass

Allows any value. Used for condition nodes that are required solely for informing how to decode complex type value.

For example, defining the types of all fields of a tuple is necessary for correct decoding.

Custom

Checks the value in scope against a custom condition defined in the compValue fields.

Examples

Balancer swap

The following condition enforces that the Balancer single swap (opens in a new tab) function must only be used to swap WETH for DAI and vice versa. Additionally, the sender and recipient for the swap must be set to the avatar address.

ABI inputs
condition
✅ example call data (valid)
⛔ example call data (violation)
0x
52bbbe2900000000000000000000000000000000000000000000000000000000000000e00000000000000000000000004f2083f5fbede34c2714affb3105539775f7fe6400000000000000000000000000000000000000000000000000000000000000000000000000000000000000004f2083f5fbede34c2714affb3105539775f7fe640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010b09dea16768f0799065c475be02919503cb2a3500020000000000000000001a0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
      sender: avatar address    recipient: avatar address        WETH/DAI pool ID    WETH token address  DAI token address        

The root condition node generally targets the entire calldata range after the 4 bytes function selector. It uses a ParamType.Calldata matches structure defining how to decode the bytes in that range.

We define Operator.Matches checks on both tuples.

  • For singleSwap an exact match on the poolId fields is required.
  • For the assetIn and assetOut fields, the values must be either WETH or DAI using Operator.Or.
  • For swapInfo the sender and recipient fields must be set to the address of the avatar using Operator.EqualToAvatar.

This example shows Operator.Matches being used to set conditions on specific fields of a tuple type value.

Custom conditions

Custom conditions can be defined by implementing the ICustomCondition interface. For an example of a custom condition, refer to the AvatarIsOwnerOfERC721.sol contract.

Importantly, custom conditions must not maintain a state that is updated for every transaction passing through the check. This is because, at the level of the custom condition, there is no way to determine whether or not the transaction will ultimately be executed.