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 an encoding 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 encoding definitions specified with the nodes.
Node Properties
Each condition node is defined by the following properties:
encoding
Specifies how to decode and pluck the parameter value from calldata. It can be one of the following:
| Value | Description |
|---|---|
Static | Interpret the word currently in scope as a static value, i.e., a value with a fixed length padded to 32 bytes. |
Dynamic | Interpret the word currently in scope as an offset to a dynamic length value, i.e., bytes or string. |
Tuple | Decode the value as a tuple. The children condition nodes inform the types of the tuple’s fields. |
Array | Interpret the word as an offset to a dynamic length array. The children condition nodes inform the array element type. |
AbiEncoded | Decode the value using standard ABI decoding. The children condition nodes inform the individual field types. |
EtherValue | References the ether value sent with the transaction. Used with value-targeting operators like WithinAllowance. |
None | Used 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
Encoding.AbiEncoded,Encoding.Tuple,Encoding.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.ArrayTailMatches) - Define branch conditions for n-ary logical operators (
Operator.And,Operator.Or) - Define parallel comparison patterns across multiple arrays (via
Operator.ZipSome,Operator.ZipEvery) - Define extraction patterns for byte ranges (via
Operator.Slice) and back-references to extracted values (viaOperator.Pluck)
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 a comparison or configuration value associated with the node. Used by comparison operators (e.g., EqualTo, GreaterThan) to specify the value to compare against, and by extraction operators (e.g., Slice, Pluck) to encode extraction parameters.
Operators
Matches
Allows setting conditions on selected fields of tuple type parameters.
Used with Encoding.AbiEncoded it allows setting conditions on specific parameters of the function call or on selected ABI encoded variables within a bytes value.
Empty
Passes if the calldata is empty (i.e., the transaction has no calldata beyond the function selector, or no selector at all). Useful for selector-only checks or for validating plain ether transfers.
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 less 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>offsetspecifies the offset in bytes from the start of thebytesvalue to the first byte that should be masked.maskspecifies which bytes should be checked. A1bit means that the bit at the same position should be checked, a0bit means that the bit should be ignored.expected valuespecifies the expected values of the bits under the mask.
WithinRatio
Validates that the ratio between two plucked values falls within an acceptable range.
Typically used in combination with Pluck to reference previously extracted values, enabling slippage protection for swap operations.
A price adapter can be used to normalize the values to a common denomination before comparison.
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.
ArrayTailMatches
Matches trailing elements of an array against a condition pattern. This is useful when the meaningful elements are at the end of an array and the length of the prefix is not known ahead of time.
Extraction Operators
Slice
Extracts a range of bytes from the value currently in scope. The extracted bytes are made available for subsequent conditions in the tree to reference. This enables fine-grained inspection of packed or concatenated data.
Pluck
References a value that was previously extracted (e.g., by Slice or from a prior node in the tree) by its index.
This allows conditions deeper in the tree to compare or validate against values encountered earlier during evaluation.
Zip Operators
Zip operators enable parallel comparison across multiple arrays that were previously extracted using Pluck.
They iterate over the arrays element-by-element, applying a child condition to each corresponding tuple of elements.
ZipSome
Checks that at least one position across the zipped arrays satisfies the child condition.
ZipEvery
Checks that every position across the zipped arrays satisfies the child condition.
Logical Expressions
Logical expressions are n-ary operators that take an array of child conditions and combine their evaluation results.
Generally having an encoding 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.
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.
For limiting ether value, use Encoding.EtherValue with WithinAllowance to reference the transaction’s ether value.
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 field.
The compValue can contain unlimited extra bytes, allowing custom conditions to receive arbitrary configuration data.
Examples
Balancer swap
The following condition enforces that the Balancer single swap 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.
- singleSwap
tuple - funds
tuple - limit
uint256 - deadline
uint256
- Matches
AbiEncoded
0x52bbbe2900000000000000000000000000000000000000000000000000000000000000e00000000000000000000000004f2083f5fbede34c2714affb3105539775f7fe6400000000000000000000000000000000000000000000000000000000000000000000000000000000000000004f2083f5fbede34c2714affb3105539775f7fe640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010b09dea16768f0799065c475be02919503cb2a3500020000000000000000001a0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 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 an Encoding.AbiEncoded matches structure defining how to decode the bytes in that range.
We define Operator.Matches checks on both tuples.
- For
singleSwapan exact match on thepoolIdfields is required. - For the
assetInandassetOutfields, the values must be eitherWETHorDAIusingOperator.Or. - For
swapInfothesenderandrecipientfields must be set to the address of the avatar usingOperator.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.